diff --git a/common/modules/pkgselect/PackageSelectViewStep.cpp b/common/modules/pkgselect/PackageSelectViewStep.cpp index e12bd07..7c58b3a 100644 --- a/common/modules/pkgselect/PackageSelectViewStep.cpp +++ b/common/modules/pkgselect/PackageSelectViewStep.cpp @@ -146,8 +146,16 @@ void PackageSelectViewStep::updatePackageSelections(bool checked) { QObject* sender_obj = sender(); if (!sender_obj) return; - QString key = "packages." + sender_obj->objectName(); - m_packageSelections[key] = checked; + QString key = sender_obj->objectName(); + + // snake_case -> camelCase + QStringList parts = key.split("_", Qt::SkipEmptyParts); + for (int i = 1; i < parts.size(); ++i) { + parts[i][0] = parts[i][0].toUpper(); + } + QString camelCaseKey = parts.join(""); + + m_packageSelections[camelCaseKey] = checked; } CALAMARES_PLUGIN_FACTORY_DEFINITION( PackageSelectViewStepFactory, registerPlugin< PackageSelectViewStep >(); ) diff --git a/common/snap-seed-glue/go.mod b/common/snap-seed-glue/go.mod new file mode 100644 index 0000000..ac4aac1 --- /dev/null +++ b/common/snap-seed-glue/go.mod @@ -0,0 +1,5 @@ +module snap-seed-glue + +go 1.22.1 + +require github.com/snapcore/snapd v0.0.0-20240328101726-fdc222fc37a0 diff --git a/common/snap-seed-glue/main.go b/common/snap-seed-glue/main.go new file mode 100644 index 0000000..cf95ab1 --- /dev/null +++ b/common/snap-seed-glue/main.go @@ -0,0 +1,289 @@ +package main + +// Copyright (C) 2024 Simon Quigley +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/interfaces/builtin" + "gopkg.in/yaml.v2" +) + +type seed struct { + Snaps []struct { + Name string `yaml:"name"` + Channel string `yaml:"channel"` + File string `yaml:"file"` + } `yaml:"snaps"` +} + +func main() { + snap.SanitizePlugsSlots = builtin.SanitizePlugsSlots + + var seed_directory string + flag.StringVar(&seed_directory, "seed", "/var/lib/snapd/seed", "Specify the seed directory") + flag.Parse() + + snap_set := make(map[string]bool) + + snaps_dir := filepath.Join(seed_directory, "snaps") + assertions_dir := filepath.Join(seed_directory, "assertions") + seed_yaml := filepath.Join(seed_directory, "seed.yaml") + + ensure_seed_yaml(seed_yaml) + + existing_snaps_in_yaml := load_existing_snaps(seed_yaml) + + for _, snap_info := range flag.Args() { + parts := strings.SplitN(snap_info, "=", 2) + snap_name := parts[0] + channel := "stable" // Default to stable if no channel is specified + if len(parts) == 2 { + channel = parts[1] + } + process_snap_with_prereqs(snap_name, channel, &snap_set, snaps_dir, assertions_dir, seed_yaml, existing_snaps_in_yaml) + } + + essentialSnaps := []string{"snapd", "bare"} + for _, snapName := range essentialSnaps { + if !existing_snaps_in_yaml[snapName] { + process_snap_with_prereqs(snapName, "stable", &snap_set, snaps_dir, assertions_dir, seed_yaml, existing_snaps_in_yaml) + } + } + + update_seed_yaml(snaps_dir, seed_yaml, snap_set, existing_snaps_in_yaml) + + remove_state_json(filepath.Join(seed_directory, "..", "state.json")) + ensure_assertions(assertions_dir) + validate_seed(seed_yaml) +} + +func ensure_seed_yaml(seed_yaml string) { + if _, err := os.Stat(seed_yaml); os.IsNotExist(err) { + file, err := os.Create(seed_yaml) + if err != nil { + log.Fatalf("Failed to create seed.yaml: %v", err) + } + defer file.Close() + file.WriteString("snaps:\n") + } +} + +func load_existing_snaps(seed_yaml string) map[string]bool { + file, err := ioutil.ReadFile(seed_yaml) + if err != nil { + log.Fatalf("Failed to read seed.yaml: %v", err) + } + + var seed_data seed + if err := yaml.Unmarshal(file, &seed_data); err != nil { + log.Fatalf("Failed to parse seed.yaml: %v", err) + } + + existing := make(map[string]bool) + for _, snap := range seed_data.Snaps { + existing[snap.Name] = true + } + return existing +} + +func update_seed_yaml(snaps_dir, seed_yaml string, snap_set map[string]bool, existing_snaps map[string]bool) { + seed_data := load_seed_data(seed_yaml) + + for snap_name := range snap_set { + if !existing_snaps[snap_name] { + snap_files, err := filepath.Glob(filepath.Join(snaps_dir, fmt.Sprintf("%s_*.snap", snap_name))) + if err != nil || len(snap_files) == 0 { + log.Printf("No snap file found for %s", snap_name) + return + } + + snap_file := filepath.Base(snap_files[0]) + log.Printf(snap_file) + + // FIXME: should put the real name of the channel in here + seed_data.Snaps = append(seed_data.Snaps, struct { + Name string `yaml:"name"` + Channel string `yaml:"channel"` + File string `yaml:"file"` + }{snap_name, "latest/stable", snap_file}) + } + } + + // Marshal to YAML and write back to file + data, err := yaml.Marshal(&seed_data) + if err != nil { + log.Fatalf("Failed to marshal seed data to YAML: %v", err) + } + + if err := ioutil.WriteFile(seed_yaml, data, 0644); err != nil { + log.Fatalf("Failed to write updated seed.yaml: %v", err) + } +} + +func load_seed_data(seed_yaml string) seed { + file, err := ioutil.ReadFile(seed_yaml) + if err != nil { + log.Fatalf("Failed to read seed.yaml: %v", err) + } + + var seed_data seed + if err := yaml.Unmarshal(file, &seed_data); err != nil { + log.Fatalf("Failed to parse seed.yaml: %v", err) + } + return seed_data +} + +func remove_state_json(state_json_path string) { + if _, err := os.Stat(state_json_path); err == nil { + os.Remove(state_json_path) + } +} + +func validate_seed(seed_yaml string) { + cmd := exec.Command("snap", "debug", "validate-seed", seed_yaml) + if err := cmd.Run(); err != nil { + log.Printf("Error validating seed: %v", err) + } +} + +func process_snap_with_prereqs(snap_name, channel string, snap_set *map[string]bool, snaps_dir, assertions_dir, seed_yaml string, existing_snaps_in_yaml map[string]bool) { + if (*snap_set)[snap_name] { + return + } + + // Download the snap if not already processed or listed in seed.yaml + if !existing_snaps_in_yaml[snap_name] { + cmd := exec.Command("snap", "download", snap_name, "--channel="+channel, "--target-directory="+snaps_dir) + if err := cmd.Run(); err != nil { + log.Printf("Error downloading snap %s from channel %s: %v", snap_name, channel, err) + return + } + } + + snap_files, err := filepath.Glob(filepath.Join(snaps_dir, fmt.Sprintf("%s_*.snap", snap_name))) + if err != nil || len(snap_files) == 0 { + log.Printf("No snap file found for %s in channel %s", snap_name, channel) + return + } + + snap_file := snap_files[0] + + cmd := exec.Command("unsquashfs", "-n", "-d", filepath.Join(snaps_dir, fmt.Sprintf("%s_meta", snap_name)), snap_file, "meta/snap.yaml") + if err := cmd.Run(); err != nil { + log.Printf("Error extracting meta/snap.yaml from snap %s: %v", snap_name, err) + return + } + + yaml_data, err := ioutil.ReadFile(filepath.Join(snaps_dir, fmt.Sprintf("%s_meta/meta/snap.yaml", snap_name))) + if err != nil { + log.Printf("Error reading snap.yaml file for %s: %v", snap_name, err) + return + } + + info, err := snap.InfoFromSnapYaml(yaml_data) + if err != nil { + log.Printf("Error parsing snap.yaml data for %s: %v", snap_name, err) + return + } + + (*snap_set)[snap_name] = true + + tracker := snap.SimplePrereqTracker{} + missing_provider_content_tags := tracker.MissingProviderContentTags(info, nil) + for provider_snap := range missing_provider_content_tags { + if !(*snap_set)[provider_snap] { + process_snap_with_prereqs(provider_snap, "stable", snap_set, snaps_dir, assertions_dir, seed_yaml, existing_snaps_in_yaml) + } + } + + if info.Base != "" && !(*snap_set)[info.Base] { + process_snap_with_prereqs(info.Base, "stable", snap_set, snaps_dir, assertions_dir, seed_yaml, existing_snaps_in_yaml) + } + + assert_files, err := filepath.Glob(filepath.Join(snaps_dir, "*.assert")) + for _, file := range assert_files { + target_path := filepath.Join(assertions_dir, filepath.Base(file)) + err := os.Rename(file, target_path) + if err != nil { + log.Printf("Failed to move %s to %s: %v", file, assertions_dir, err) + } + } + + os.RemoveAll(filepath.Join(snaps_dir, fmt.Sprintf("%s_meta", snap_name))) +} + +func ensure_assertions(assertions_dir string) { + model := "generic-classic" + brand := "generic" + series := "16" + + model_assertion_path := filepath.Join(assertions_dir, "model") + account_key_assertion_path := filepath.Join(assertions_dir, "account-key") + account_assertion_path := filepath.Join(assertions_dir, "account") + + // Check and generate model assertion + if _, err := os.Stat(model_assertion_path); os.IsNotExist(err) { + output, err := exec.Command("snap", "known", "--remote", "model", "series="+series, "model="+model, "brand-id="+brand).CombinedOutput() + if err != nil { + log.Fatalf("Failed to fetch model assertion: %v, Output: %s", err, string(output)) + } + ioutil.WriteFile(model_assertion_path, output, 0644) + } + + // Generate account-key assertion if not exists + if _, err := os.Stat(account_key_assertion_path); os.IsNotExist(err) { + signKeySha3 := grep_pattern(model_assertion_path, "sign-key-sha3-384: ") + output, err := exec.Command("snap", "known", "--remote", "account-key", "public-key-sha3-384="+signKeySha3).CombinedOutput() + if err != nil { + log.Fatalf("Failed to fetch account-key assertion: %v, Output: %s", err, string(output)) + } + ioutil.WriteFile(account_key_assertion_path, output, 0644) + } + + // Generate account assertion if not exists + if _, err := os.Stat(account_assertion_path); os.IsNotExist(err) { + accountId := grep_pattern(account_key_assertion_path, "account-id: ") + output, err := exec.Command("snap", "known", "--remote", "account", "account-id="+accountId).CombinedOutput() + if err != nil { + log.Fatalf("Failed to fetch account assertion: %v, Output: %s", err, string(output)) + } + ioutil.WriteFile(account_assertion_path, output, 0644) + } +} + +func grep_pattern(filePath, pattern string) string { + content, err := ioutil.ReadFile(filePath) + if err != nil { + log.Fatalf("Failed to read from file %s: %v", filePath, err) + } + lines := strings.Split(string(content), "\n") + for _, line := range lines { + if strings.Contains(line, pattern) { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + log.Fatalf("Pattern %s not found in file %s", pattern, filePath) + return "" +} diff --git a/common/snap_install b/common/snap_install deleted file mode 100755 index 2cd8e18..0000000 --- a/common/snap_install +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -chroot_root="$1" -shift -new_snaps=("$@") -seed_dir="$chroot_root/var/lib/snapd/seed" -snaps_dir="$seed_dir/snaps" -assertions_dir="$seed_dir/assertions" -seed_yaml="$seed_dir/seed.yaml" - -# Loop through each snap and download it, then update seed.yaml -for snap_info in "${new_snaps[@]}"; do - snap_name=${snap_info%=*} - channel=${snap_info#*=} - - # Download - snap download --channel="$channel" "$snap_name" --target-directory="$snaps_dir" - - sleep 5 - - # Get revision number - snap_file=$(ls "$snaps_dir" | grep ".snap" | grep "^${snap_name}_") - rev_num=$(echo "$snap_file" | grep -oP '(?<=_)\d+') - - # Move assertions - mv "$snaps_dir/${snap_name}_${rev_num}.assert" "$assertions_dir" - - # Append to seed.yaml - { - echo " - name: $snap_name" - echo " file: $snap_file" - echo " channel: $channel" - } >> "$seed_yaml" -done - -# Remove state.json if exists -[ -f "$chroot_root/var/lib/snapd/state.json" ] && rm "$chroot_root/var/lib/snapd/state.json" - -# Validate -snap debug validate-seed "$seed_yaml" diff --git a/debian/control b/debian/control index d26be4c..f3ae3e6 100644 --- a/debian/control +++ b/debian/control @@ -7,6 +7,8 @@ Build-Depends: calamares (>= 3.3.1-0ubuntu3), cmake, debhelper-compat (= 13), extra-cmake-modules, + golang-github-snapcore-snapd-dev (>= 2.62), + golang-gopkg-yaml.v2-dev, intltool, libkf5coreaddons-dev, libqt5svg5-dev, diff --git a/debian/rules b/debian/rules index 0f09b7a..d2f4aa9 100755 --- a/debian/rules +++ b/debian/rules @@ -2,6 +2,8 @@ export LC_ALL=C.UTF-8 export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export GOPATH=/usr/share/gocode +export GO111MODULE=off DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH) PKGSELECT = "common/modules/pkgselect" @@ -16,6 +18,7 @@ override_dh_auto_configure: override_dh_auto_build: make; (cd $(PKGSELECT)/build && $(MAKE)) + (cd common/snap-seed-glue && go build -gcflags="all=-N -l" -ldflags="-compressdwarf=false" -o snap-seed-glue main.go) override_dh_auto_install: (cd $(PKGSELECT)/build && $(MAKE) DESTDIR=$(CURDIR)/debian/calamares-settings-ubuntu-common/ install) @@ -32,7 +35,7 @@ override_dh_missing: chmod 644 $(MODULES_DIR)/pkgselect/libcalamares_viewmodule_pkgselect.so chmod 644 $(MODULES_DIR)/pkgselect/module.desc mkdir -pv debian/calamares-settings-ubuntu-common/usr/bin/ - cp -v common/snap_install debian/calamares-settings-ubuntu-common/usr/bin/calamares_snap_install + cp -v common/snap-seed-glue/snap-seed-glue debian/calamares-settings-ubuntu-common/usr/bin/snap-seed-glue mkdir -pv debian/calamares-settings-ubuntu-common/usr/libexec/ cp -v common/fixconkeys-part1 debian/calamares-settings-ubuntu-common/usr/libexec/fixconkeys-part1 cp -v common/fixconkeys-part2 debian/calamares-settings-ubuntu-common/usr/libexec/fixconkeys-part2 diff --git a/lubuntu/modules/pkgselect_context.conf b/lubuntu/modules/pkgselect_context.conf index 2669fbd..b563faf 100644 --- a/lubuntu/modules/pkgselect_context.conf +++ b/lubuntu/modules/pkgselect_context.conf @@ -1,17 +1,15 @@ --- dontChroot: false timeout: 300 -"packages.minimal_button": +"packages.minimalButton": true: - "apt-get -y --purge remove snapd vlc plasma-discover transmission-qt quassel 2048-qt featherpad noblenote kcalc qps zsync partitionmanager qapt-deb-installer picom qlipper qtpass libreoffice*" - "apt-get -y autoremove" -"packages.party_button": +"packages.partyButton": true: - "apt-get update" - "apt-get -y install ubuntu-restricted-addons unrar" -"packages.updates_button": +"packages.updatesButton": true: "apt-get -y full-upgrade" -"packages.virtmanager_button": +"packages.virtmanagerButton": true: "apt-get -y install virt-manager" -"packages.thunderbird_button": - true: "apt-get -y install thunderbird" diff --git a/lubuntu/modules/pkgselect_snap_context.conf b/lubuntu/modules/pkgselect_snap_context.conf index 2116686..f17f7cb 100644 --- a/lubuntu/modules/pkgselect_snap_context.conf +++ b/lubuntu/modules/pkgselect_snap_context.conf @@ -1,9 +1,9 @@ --- dontChroot: true timeout: 600 -"packages.element_button": - true: "calamares_snap_install ${ROOT} element-desktop=stable gtk-common-themes=stable gnome-42-2204=stable core22=stable" -"packages.krita_button": - true: "calamares_snap_install ${ROOT} cups=stable kf5-5-111-qt-5-15-11-core22=stable gtk-common-themes=stable core22=stable krita=stable" -"packages.full_button": - true: "calamares_snap_install ${ROOT} gtk-common-themes=stable gnome-42-2204=stable core22=stable kf5-5-111-qt-5-15-11-core22=stable bare=stable element-desktop=stable cups=stable snapd=stable krita=stable" +"packages.elementButton": + true: "snap-seed-glue --seed ${ROOT}/var/lib/snapd/seed element-desktop" +"packages.kritaButton": + true: "snap-seed-glue --seed ${ROOT}/var/lib/snapd/seed krita" +"packages.thunderbirdButton": + true: "snap-seed-glue --seed ${ROOT}/var/lib/snapd/seed thunderbird"