├── .gitattributes ├── FUNDING.md ├── assets └── lvm-partitioning-structure.png ├── sonar-project.properties ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── go.yml │ └── release.yml ├── samples ├── grub │ ├── rootPart.abroot.cfg │ └── bootPart.grub.cfg └── sbin │ └── init ├── .gitignore ├── extras └── dpkg │ ├── dpkg_test.go │ └── dpkg.go ├── config └── abroot.json ├── tests ├── logger_test.go ├── kargs_test.go ├── atomic-io_test.go ├── chroot_test.go ├── diff_test.go └── pkg_test.go ├── cmd ├── root.go ├── conf.go ├── kargs.go ├── update-initramfs.go ├── rollback.go ├── rebase.go ├── unlock-var.go ├── pkg.go └── mount-sys.go ├── core ├── atomic-io.go ├── kernel.go ├── conf.go ├── image-recipe.go ├── specs.go ├── diff.go ├── image.go ├── checks.go ├── integrity.go ├── package-diff.go ├── chroot.go ├── rsync.go ├── utils.go ├── grub.go ├── logging.go └── kargs.go ├── main.go ├── NOTES.md ├── man └── man1 │ └── abroot.1 ├── locales ├── zh_Hans.yml ├── zh_Hant.yml ├── ko.yml ├── ar.yml ├── en_GB.yml ├── en.yml ├── tr.yml ├── bn.yml ├── cs.yml ├── da.yml ├── ka.yml └── be.yml ├── settings └── config.go └── abroot-logo.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ref: https://git-scm.com/docs/gitattributes 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /FUNDING.md: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vanilla-os 4 | liberapay: fabricators 5 | -------------------------------------------------------------------------------- /assets/lvm-partitioning-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vanilla-OS/ABRoot/HEAD/assets/lvm-partitioning-structure.png -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Vanilla-OS_ABRoot_AYiLUFMb9CPJrZw6oxC4 2 | sonar.projectName=ABRoot 3 | 4 | sonar.sources=. 5 | sonar.exclusions=**/*_test.go, vendor/** 6 | 7 | sonar.c.file.suffixes=- 8 | sonar.cpp.file.suffixes=- 9 | sonar.objc.file.suffixes=- 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /samples/grub/rootPart.abroot.cfg: -------------------------------------------------------------------------------- 1 | insmod gzio 2 | insmod part_gpt 3 | insmod ext2 4 | search --no-floppy --fs-uuid --set=root abde4f88-d06c-4137-961e-da49ecb80654 5 | linux /.system/boot/vmlinuz-6.1.0-7-amd64 root=UUID=abde4f88-d06c-4137-961e-da49ecb80654 quiet splash bgrt_disable $vt_handoff 6 | initrd /.system/boot/initrd.img-6.1.0-7-amd64 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /abroot/abroot 2 | /overlay_test 3 | /abroot*.deb 4 | /abroot*.buildinfo 5 | /abroot*.changes 6 | /abroot*.dsc 7 | /abroot*.tar.xz 8 | /obj-x86_64-linux-gnu 9 | /debian/*.debhelper 10 | /debian/*.substvars 11 | /debian/debhelper-build-stamp 12 | /debian/obj-x86_64-linux-gnu 13 | /debian/abroot 14 | /abroot 15 | ABRoot 16 | tags 17 | test 18 | abrootv2 -------------------------------------------------------------------------------- /samples/sbin/init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | echo "ABRoot: Initializing mount points..." 4 | 5 | # /var mount 6 | mount -U a834618a-39a6-415a-b9a7-31d30f2db2e2 /var 7 | 8 | # /etc overlay 9 | mount -t overlay overlay -o lowerdir=/.system/etc,upperdir=/var/lib/abroot/etc/a,workdir=/var/lib/abroot/etc/a-work /etc 10 | 11 | # /var binds 12 | mount -o bind /var/home /home 13 | mount -o bind /var/opt /opt 14 | mount -o bind,ro /.system/usr /usr 15 | 16 | echo "ABRoot: Starting systemd..." 17 | 18 | # Start systemd 19 | exec /lib/systemd/systemd 20 | -------------------------------------------------------------------------------- /extras/dpkg/dpkg_test.go: -------------------------------------------------------------------------------- 1 | package dpkg 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetPackageVersion(t *testing.T) { 9 | version := DpkgGetPackageVersion("git") 10 | fmt.Printf("Version: %s\n", version) 11 | 12 | if version != "1:2.40.1-1" { 13 | t.Fail() 14 | } 15 | } 16 | 17 | func TestBatchGetPackageVersion(t *testing.T) { 18 | versions := DpkgBatchGetPackageVersion([]string{"git", "golang"}) 19 | fmt.Printf("Versions: %v\n", versions) 20 | 21 | if versions[0] != "1:2.40.1-1" || versions[1] != "2:1.21~2" { 22 | t.Fail() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/grub/bootPart.grub.cfg: -------------------------------------------------------------------------------- 1 | # This is a sample grub configuration for ABRoot 2 | 3 | set default=0 4 | set timeout=5 5 | 6 | menuentry "ABRoot A (current)" --class abroot-a { 7 | set root=(hd0,4) 8 | configfile "/.system/boot/grub/abroot.cfg" 9 | } 10 | 11 | menuentry "ABRoot B (previous)" --class abroot-b { 12 | set root=(hd0,3) 13 | configfile "/.system/boot/grub/abroot.cfg" 14 | } 15 | 16 | # to allow the user to edit "e" the menu entries 17 | # at boot time, the abroot.cfg file must contain 18 | # a menuentry, so when the user presses enter 19 | # a new menu will be shown with the pure grub 20 | # configuration, then it will be possible to 21 | # edit the menu entries. 22 | -------------------------------------------------------------------------------- /config/abroot.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxParallelDownloads": 2, 3 | 4 | "registry": "ghcr.io", 5 | "registryService": "registry.ghcr.io", 6 | "registryAPIVersion": "v2", 7 | "name": "vanilla-os/desktop", 8 | "tag": "main", 9 | 10 | "iPkgMngPre": "lpkg --unlock", 11 | "iPkgMngPost": "lpkg --lock", 12 | "iPkgMngAdd": "apt-get install -y", 13 | "iPkgMngRm": "apt-get remove -y --autoremove", 14 | "iPkgMngApi": "https://packages.vanillaos.org/api/pkg/{packageName}", 15 | "iPkgMngStatus": 1, 16 | 17 | "updateInitramfsCmd": "lpkg --unlock && /usr/sbin/update-initramfs -u && lpkg --lock", 18 | "updateGrubCmd": "/usr/sbin/grub-mkconfig -o '%s'", 19 | 20 | "differURL": "https://differ.vanillaos.org", 21 | 22 | "partLabelVar": "vos-var", 23 | "partLabelA": "vos-a", 24 | "partLabelB": "vos-b", 25 | "partLabelBoot": "vos-boot", 26 | "partLabelEfi": "vos-efi", 27 | "PartCryptVar": "/dev/mapper/vos--var-var", 28 | 29 | "thinProvisioning": false, 30 | "thinInitVolume": "" 31 | } 32 | -------------------------------------------------------------------------------- /tests/logger_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/vanilla-os/abroot/core" 7 | ) 8 | 9 | // TestGetLogFile tests the GetLogFile function by getting the log file and 10 | // checking if it is not nil. 11 | func TestGetLogFile(t *testing.T) { 12 | t.Log("TestGetLogFile: running...") 13 | 14 | logFile := core.GetLogFile() 15 | if logFile == nil { 16 | t.Fatal("TestGetLogFile: logFile is nil") 17 | } 18 | 19 | t.Log("TestGetLogFile: done") 20 | } 21 | 22 | // TestWriteToLog tests the LogToFile function by writing a bunch of messages 23 | // to the log file. 24 | func TestWriteToLog(t *testing.T) { 25 | t.Log("TestWriteToLog: running...") 26 | err := core.LogToFile("TestWriteToLog: running...") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | for i := 0; i < 100; i++ { 32 | t.Logf("TestWriteToLog: writing %d to the log file", i) 33 | err := core.LogToFile("TestWriteToLog: writing %d to the log file", i) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | t.Log("TestWriteToLog: done") 40 | err = core.LogToFile("TestWriteToLog: done") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "embed" 18 | 19 | "github.com/vanilla-os/orchid/cmdr" 20 | ) 21 | 22 | var abroot *cmdr.App 23 | 24 | const ( 25 | verboseFlag string = "verbose" 26 | ) 27 | 28 | func New(version string, fs embed.FS) *cmdr.App { 29 | abroot = cmdr.NewApp("abroot", version, fs) 30 | return abroot 31 | } 32 | 33 | func NewRootCommand(version string) *cmdr.Command { 34 | root := cmdr.NewCommand( 35 | abroot.Trans("abroot.use"), 36 | abroot.Trans("abroot.long"), 37 | abroot.Trans("abroot.short"), 38 | nil). 39 | WithPersistentBoolFlag( 40 | cmdr.NewBoolFlag( 41 | verboseFlag, 42 | "V", 43 | abroot.Trans("abroot.verboseFlag"), 44 | false)) 45 | root.Version = version 46 | 47 | return root 48 | } 49 | -------------------------------------------------------------------------------- /tests/kargs_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/google/uuid" 10 | "github.com/vanilla-os/abroot/core" 11 | ) 12 | 13 | // TestKargsWrite tests the KargsWrite function by writing a string to a file, 14 | // mimicking the kernel command line arguments. 15 | 16 | func TestKargsWrite(t *testing.T) { 17 | core.KargsPath = fmt.Sprintf("%s/kargs-%s", os.TempDir(), uuid.New().String()) 18 | 19 | err := core.KargsWrite("test") 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | content, err := ioutil.ReadFile(core.KargsPath) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | 29 | t.Log(string(content)) 30 | t.Log("TestKargsWrite: done") 31 | } 32 | 33 | // TestKargsRead tests the KargsRead function by reading the content of the file 34 | // that was written by the TestKargsWrite function. 35 | func TestKargsRead(t *testing.T) { 36 | core.KargsPath = fmt.Sprintf("%s/kargs-%s", os.TempDir(), uuid.New().String()) 37 | 38 | err := core.KargsWrite("test") 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | 43 | content, err := core.KargsRead() 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | t.Log(content) 49 | t.Log("TestKargsRead: done") 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v5 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v6 16 | with: 17 | go-version-file: go.mod 18 | 19 | - name: Install pallas 20 | run: | 21 | curl -L -o pallas https://github.com/Vanilla-OS/Pallas/releases/download/continuous/pallas 22 | chmod +x pallas 23 | 24 | - name: Generate Docs 25 | run: ./pallas 26 | 27 | - name: Setup Pages 28 | id: pages 29 | uses: actions/configure-pages@v5 30 | 31 | - name: Build with Jekyll 32 | uses: actions/jekyll-build-pages@v1 33 | with: 34 | source: ./dist 35 | destination: ./_site 36 | 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v4 39 | 40 | deploy: 41 | permissions: 42 | contents: read 43 | pages: write 44 | id-token: write 45 | environment: 46 | name: github-pages 47 | url: ${{steps.deployment.outputs.page_url}} 48 | runs-on: ubuntu-latest 49 | needs: build 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /tests/atomic-io_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/vanilla-os/abroot/core" 10 | ) 11 | 12 | // TestAtomicSwap tests the AtomicSwap function by creating 2 files and swapping 13 | // them. As a result, the 2 files should change their locations. 14 | func TestAtomicSwap(t *testing.T) { 15 | tmpfile, err := ioutil.TempFile("", uuid.New().String()) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | if _, err := tmpfile.Write([]byte("ABRoot")); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if err := tmpfile.Close(); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | file, err := os.Open(tmpfile.Name()) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | newfile, err := ioutil.TempFile("", uuid.New().String()) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | if _, err := newfile.Write([]byte("ABRoot")); err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if err := newfile.Close(); err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | newfile, err = os.Open(newfile.Name()) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | err = core.AtomicSwap(file.Name(), newfile.Name()) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | _, err = ioutil.ReadFile(file.Name()) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | t.Log("TestAtomicSwap: done") 62 | } 63 | -------------------------------------------------------------------------------- /core/atomic-io.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Luca di Maio 8 | Copyright: 2024 9 | Description: 10 | ABRoot is utility which provides full immutability and 11 | atomicity to a Linux system, by transacting between 12 | two root filesystems. Updates are performed using OCI 13 | images, to ensure that the system is always in a 14 | consistent state. 15 | */ 16 | 17 | import ( 18 | "os" 19 | 20 | "golang.org/x/sys/unix" 21 | ) 22 | 23 | // atomicSwap allows swapping 2 files or directories in-place and atomically, 24 | // using the renameat2 syscall. This should be used instead of os.Rename, 25 | // which is not atomic at all 26 | func AtomicSwap(src, dst string) error { 27 | PrintVerboseInfo("AtomicSwap", "running...") 28 | 29 | orig, err := os.Open(src) 30 | if err != nil { 31 | PrintVerboseErr("AtomicSwap", 0, err) 32 | return err 33 | } 34 | 35 | newfile, err := os.Open(dst) 36 | if err != nil { 37 | PrintVerboseErr("AtomicSwap", 1, err) 38 | return err 39 | } 40 | 41 | err = unix.Renameat2(int(orig.Fd()), src, int(newfile.Fd()), dst, unix.RENAME_EXCHANGE) 42 | if err != nil { 43 | PrintVerboseErr("AtomicSwap", 2, err) 44 | return err 45 | } 46 | 47 | PrintVerboseInfo("AtomicSwap", "done") 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /core/kernel.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "path/filepath" 19 | 20 | "github.com/hashicorp/go-version" 21 | ) 22 | 23 | // getKernelVersion returns the latest kernel version available in the root 24 | func getKernelVersion(bootPath string) string { 25 | PrintVerboseInfo("getKernelVersion", "running...") 26 | 27 | kernelDir := filepath.Join(bootPath, "vmlinuz-*") 28 | files, err := filepath.Glob(kernelDir) 29 | if err != nil { 30 | PrintVerboseErr("getKernelVersion", 0, err) 31 | return "" 32 | } 33 | 34 | if len(files) == 0 { 35 | PrintVerboseErr("getKernelVersion", 1, errors.New("no kernel found")) 36 | return "" 37 | } 38 | 39 | var maxVer *version.Version 40 | for _, file := range files { 41 | verStr := filepath.Base(file)[8:] 42 | ver, err := version.NewVersion(verStr) 43 | if err != nil { 44 | continue 45 | } 46 | if maxVer == nil || ver.GreaterThan(maxVer) { 47 | maxVer = ver 48 | } 49 | } 50 | 51 | if maxVer != nil { 52 | return maxVer.String() 53 | } 54 | 55 | return "" 56 | } 57 | -------------------------------------------------------------------------------- /cmd/conf.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "os" 18 | 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/vanilla-os/abroot/core" 22 | "github.com/vanilla-os/orchid/cmdr" 23 | ) 24 | 25 | func NewConfCommand() *cmdr.Command { 26 | cmd := cmdr.NewCommand( 27 | "config-editor", 28 | abroot.Trans("cnf.long"), 29 | abroot.Trans("cnf.short"), 30 | func(cmd *cobra.Command, args []string) error { 31 | err := cnf(cmd, args) 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | return nil 36 | }, 37 | ) 38 | 39 | return cmd 40 | } 41 | 42 | func cnf(cmd *cobra.Command, args []string) error { 43 | if !core.RootCheck(false) { 44 | cmdr.Error.Println(abroot.Trans("cnf.rootRequired")) 45 | return nil 46 | } 47 | 48 | result, err := core.ConfEdit() 49 | switch result { 50 | case core.CONF_CHANGED: 51 | cmdr.Info.Println(abroot.Trans("cnf.changed")) 52 | case core.CONF_UNCHANGED: 53 | cmdr.Info.Println(abroot.Trans("cnf.unchanged")) 54 | case core.CONF_FAILED: 55 | cmdr.Error.Println(abroot.Trans("cnf.failed", err)) 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /extras/dpkg/dpkg.go: -------------------------------------------------------------------------------- 1 | package dpkg 2 | 3 | // #cgo LDFLAGS: -ldpkg -lmd 4 | // #define LIBDPKG_VOLATILE_API 1 5 | // 6 | // #include 7 | // #include 8 | // #include 9 | import "C" 10 | import ( 11 | "errors" 12 | ) 13 | 14 | var DpkgInstanced bool 15 | 16 | func NewDpkgInstance() error { 17 | if DpkgInstanced { 18 | return errors.New("another dpkg instance already exists") 19 | } 20 | 21 | C.dpkg_program_init(C.CString("a.out")) 22 | C.modstatdb_open(C.msdbrw_available_readonly) 23 | 24 | DpkgInstanced = true 25 | return nil 26 | } 27 | 28 | func DpkgDispose() error { 29 | if !DpkgInstanced { 30 | return errors.New("no dpkg instance exists") 31 | } 32 | 33 | C.dpkg_program_done() 34 | 35 | DpkgInstanced = false 36 | return nil 37 | } 38 | 39 | func getPackageVersion(pkgName string) string { 40 | pkgInfo := C.pkg_hash_find_singleton(C.CString(pkgName)) 41 | version := C.GoString(C.versiondescribe(&pkgInfo.configversion, C.vdew_nonambig)) 42 | 43 | if version == "" { 44 | version = "" 45 | } 46 | 47 | return version 48 | } 49 | 50 | func DpkgGetPackageVersion(pkgName string) string { 51 | NewDpkgInstance() 52 | version := getPackageVersion(pkgName) 53 | DpkgDispose() 54 | 55 | return version 56 | } 57 | 58 | func DpkgBatchGetPackageVersion(pkgNames []string) []string { 59 | versions := make([]string, len(pkgNames)) 60 | 61 | NewDpkgInstance() 62 | for i := 0; i < len(pkgNames); i++ { 63 | versions[i] = getPackageVersion(pkgNames[i]) 64 | } 65 | DpkgDispose() 66 | 67 | return versions 68 | } 69 | -------------------------------------------------------------------------------- /tests/chroot_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "testing" 10 | 11 | "github.com/google/uuid" 12 | "github.com/vanilla-os/abroot/core" 13 | ) 14 | 15 | // TestChroot tests the Chroot function by creating a chroot environment and 16 | // executing a command inside it, which should create a file. 17 | func TestChroot(t *testing.T) { 18 | if os.Getenv("ABROOT_TEST_ROOTFS_URL") == "" { 19 | t.Skip("ABROOT_TEST_ROOTFS_URL is not set") 20 | } 21 | 22 | chrootPath := fmt.Sprintf("%s/chroot-%s", os.TempDir(), uuid.New().String()) 23 | chrootUuid := uuid.New().String() // fake 24 | chrootDevice := "/dev/fk1" // fake 25 | 26 | err := os.MkdirAll(chrootPath, 0755) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | // rootfs setup 32 | _, err = os.Stat(fmt.Sprintf("%s/rootfs.tar.gz", os.TempDir())) 33 | if err != nil { 34 | resp, err := http.Get(os.Getenv("ABROOT_TEST_ROOTFS_URL")) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | f, err := os.Create(fmt.Sprintf("%s/rootfs.tar.gz", os.TempDir())) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | _, err = io.Copy(f, resp.Body) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | err = resp.Body.Close() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | err = exec.Command("tar", "-xzf", fmt.Sprintf("%s/rootfs.tar.gz", os.TempDir()), "-C", chrootPath).Run() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | // chroot setup 61 | chroot, err := core.NewChroot(chrootPath, chrootUuid, chrootDevice, false, "") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | // chroot test 67 | err = chroot.Execute("touch /test") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | _, err = os.Stat(fmt.Sprintf("%s/test", chrootPath)) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | // chroot teardown 78 | err = chroot.Close() 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | t.Log("TestChroot: done") 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | container: 14 | image: ghcr.io/vanilla-os/pico:dev 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v6 21 | with: 22 | go-version-file: go.mod 23 | 24 | - name: Install Build Dependencies 25 | run: | 26 | dpkg --add-architecture arm64 27 | apt-get update 28 | apt-get install -y libbtrfs-dev libdevmapper-dev libgpgme-dev libdpkg-dev gcc patch pkgconf libdevmapper-dev:arm64 libdpkg-dev:arm64 gcc-aarch64-linux-gnu 29 | 30 | - name: Run Tests 31 | run: go test -v ./tests/... 32 | 33 | - name: Build 34 | run: | 35 | go build -o abrootv2 -ldflags="-X 'main.Version=${{ github.sha }}'" 36 | tar -czvf abrootv2-amd64.tar.gz abrootv2 37 | apt-get install -y libgpgme-dev:arm64 38 | GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig go build -o abrootv2 -ldflags="-X 'main.Version=${{ github.sha }}'" 39 | tar -czvf abrootv2-arm64.tar.gz abrootv2 40 | tar -czvf abroot-man.tar.gz man/man1/abroot.1 41 | 42 | - name: Check for Missing Strings 43 | uses: vanilla-os/missing-strings-golang-action@v0.1.0 44 | 45 | - name: Calculate and Save Checksums 46 | run: | 47 | sha256sum abrootv2*.tar.gz >> checksums.txt 48 | sha256sum abroot-man.tar.gz >> checksums.txt 49 | 50 | - uses: actions/upload-artifact@v6 51 | with: 52 | name: abrootv2 53 | path: | 54 | abrootv2*.tar.gz 55 | abroot-man.tar.gz 56 | checksums.txt 57 | 58 | - uses: softprops/action-gh-release@v2 59 | if: github.repository == 'vanilla-os/ABRoot' && github.ref == 'refs/heads/main' 60 | with: 61 | token: "${{ secrets.GITHUB_TOKEN }}" 62 | tag_name: "continuous" 63 | prerelease: true 64 | name: "Continuous Build" 65 | files: | 66 | abrootv2*.tar.gz 67 | abroot-man.tar.gz 68 | checksums.txt 69 | -------------------------------------------------------------------------------- /cmd/kargs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "os" 19 | 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/vanilla-os/abroot/core" 23 | "github.com/vanilla-os/orchid/cmdr" 24 | ) 25 | 26 | var validKargsArgs = []string{"edit", "show"} 27 | 28 | func NewKargsCommand() *cmdr.Command { 29 | cmd := cmdr.NewCommand( 30 | "kargs edit|show", 31 | abroot.Trans("kargs.long"), 32 | abroot.Trans("kargs.short"), 33 | func(cmd *cobra.Command, args []string) error { 34 | err := kargs(cmd, args) 35 | if err != nil { 36 | os.Exit(1) 37 | } 38 | return nil 39 | }, 40 | ) 41 | 42 | cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs) 43 | cmd.ValidArgs = validKargsArgs 44 | cmd.Example = "abroot kargs edit" 45 | 46 | return cmd 47 | } 48 | 49 | func kargs(cmd *cobra.Command, args []string) error { 50 | if !core.RootCheck(false) { 51 | cmdr.Error.Println(abroot.Trans("kargs.rootRequired")) 52 | return nil 53 | } 54 | 55 | switch args[0] { 56 | case "edit": 57 | changed, err := core.KargsEdit() 58 | if err != nil { 59 | cmdr.Error.Println(err) 60 | return err 61 | } 62 | 63 | if !changed { 64 | cmdr.Info.Println(abroot.Trans("kargs.notChanged")) 65 | return nil 66 | } 67 | 68 | aBsys, err := core.NewABSystem() 69 | if err != nil { 70 | cmdr.Error.Println(err) 71 | return err 72 | } 73 | err = aBsys.RunOperation(core.APPLY, false) 74 | if err != nil { 75 | cmdr.Error.Println(abroot.Trans("pkg.applyFailed")) 76 | return err 77 | } 78 | case "show": 79 | kargsStr, err := core.KargsRead() 80 | if err != nil { 81 | cmdr.Error.Println(err) 82 | return err 83 | } 84 | cmdr.Info.Println(kargsStr) 85 | default: 86 | return errors.New(abroot.Trans("kargs.unknownCommand", args[0])) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /core/conf.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/spf13/viper" 9 | "github.com/vanilla-os/abroot/settings" 10 | ) 11 | 12 | // Supported results for the ConfEditResult type 13 | const ( 14 | CONF_CHANGED = iota 15 | CONF_UNCHANGED 16 | CONF_FAILED 17 | ) 18 | 19 | // ConfEditResult is the result of the ConfEdit function 20 | type ConfEditResult int 21 | 22 | // ConfEdit opens the configuration file in the default editor 23 | func ConfEdit() (ConfEditResult, error) { 24 | editor := os.Getenv("EDITOR") 25 | if editor == "" { 26 | nanoBin, err := exec.LookPath("nano") 27 | if err == nil { 28 | editor = nanoBin 29 | } 30 | 31 | viBin, err := exec.LookPath("vi") 32 | if err == nil { 33 | editor = viBin 34 | } 35 | 36 | editorBin, err := exec.LookPath("editor") 37 | if err == nil { 38 | editor = editorBin 39 | } 40 | 41 | if editor == "" { 42 | return CONF_FAILED, fmt.Errorf("no editor found in $EDITOR, nano or vi") 43 | } 44 | } 45 | 46 | // getting the configuration content so as we can compare it later 47 | // to see if it has been changed 48 | cnfContent, err := os.ReadFile(settings.CnfPathAdmin) 49 | if err != nil { 50 | return CONF_FAILED, err 51 | } 52 | 53 | tmpFilePath := settings.CnfPathAdmin + ".tmp.json" 54 | 55 | err = os.WriteFile(tmpFilePath, cnfContent, 0o755) 56 | if err != nil { 57 | return CONF_FAILED, err 58 | } 59 | defer os.Remove(tmpFilePath) 60 | 61 | // open the editor 62 | cmd := exec.Command(editor, tmpFilePath) 63 | cmd.Stdin = os.Stdin 64 | cmd.Stdout = os.Stdout 65 | cmd.Stderr = os.Stderr 66 | err = cmd.Run() 67 | if err != nil { 68 | return CONF_FAILED, err 69 | } 70 | 71 | // getting the new content 72 | newCnfContent, err := os.ReadFile(tmpFilePath) 73 | if err != nil { 74 | return CONF_FAILED, err 75 | } 76 | 77 | // we compare the old and new content to return the proper result 78 | if string(cnfContent) == string(newCnfContent) { 79 | return CONF_UNCHANGED, nil 80 | } 81 | 82 | viper.SetConfigFile(tmpFilePath) 83 | err = viper.ReadInConfig() 84 | if err != nil { 85 | return CONF_FAILED, err 86 | } 87 | 88 | err = AtomicSwap(settings.CnfPathAdmin, tmpFilePath) 89 | if err != nil { 90 | return CONF_FAILED, err 91 | } 92 | 93 | return CONF_CHANGED, nil 94 | } 95 | -------------------------------------------------------------------------------- /cmd/update-initramfs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "os" 18 | 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/vanilla-os/abroot/core" 22 | "github.com/vanilla-os/orchid/cmdr" 23 | ) 24 | 25 | func NewUpdateInitfsCommand() *cmdr.Command { 26 | cmd := cmdr.NewCommand( 27 | "update-initramfs", 28 | abroot.Trans("updateInitramfs.long"), 29 | abroot.Trans("updateInitramfs.short"), 30 | func(cmd *cobra.Command, args []string) error { 31 | err := updateInitramfs(cmd, args) 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | return nil 36 | }, 37 | ) 38 | 39 | cmd.WithBoolFlag( 40 | cmdr.NewBoolFlag( 41 | "dry-run", 42 | "d", 43 | abroot.Trans("updateInitramfs.dryRunFlag"), 44 | false)) 45 | 46 | cmd.WithBoolFlag( 47 | cmdr.NewBoolFlag( 48 | "delete-old-system", 49 | "", 50 | abroot.Trans("upgrade.deleteOld"), 51 | false)) 52 | 53 | cmd.Example = "abroot update-initramfs" 54 | 55 | return cmd 56 | } 57 | 58 | func updateInitramfs(cmd *cobra.Command, args []string) error { 59 | if !core.RootCheck(false) { 60 | cmdr.Error.Println(abroot.Trans("updateInitramfs.rootRequired")) 61 | return nil 62 | } 63 | 64 | dryRun, err := cmd.Flags().GetBool("dry-run") 65 | if err != nil { 66 | cmdr.Error.Println(err) 67 | return err 68 | } 69 | 70 | freeSpace, err := cmd.Flags().GetBool("delete-old-system") 71 | if err != nil { 72 | cmdr.Error.Println(err) 73 | return err 74 | } 75 | 76 | aBsys, err := core.NewABSystem() 77 | if err != nil { 78 | cmdr.Error.Println(err) 79 | return err 80 | } 81 | 82 | if dryRun { 83 | err = aBsys.RunOperation(core.DRY_RUN_INITRAMFS, freeSpace) 84 | } else { 85 | err = aBsys.RunOperation(core.INITRAMFS, freeSpace) 86 | } 87 | if err != nil { 88 | cmdr.Error.Printf(abroot.Trans("updateInitramfs.updateFailed"), err) 89 | return err 90 | } 91 | 92 | cmdr.Info.Println(abroot.Trans("updateInitramfs.updateSuccess")) 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /core/image-recipe.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | ) 20 | 21 | // An ImageRecipe represents a Dockerfile/Containerfile-like recipe 22 | type ImageRecipe struct { 23 | From string 24 | Labels map[string]string 25 | Args map[string]string 26 | Content string 27 | } 28 | 29 | // NewImageRecipe creates a new ImageRecipe instance and returns a pointer to it 30 | func NewImageRecipe(image string, labels map[string]string, args map[string]string, content string) *ImageRecipe { 31 | PrintVerboseInfo("NewImageRecipe", "running...") 32 | 33 | return &ImageRecipe{ 34 | From: image, 35 | Labels: labels, 36 | Args: args, 37 | Content: content, 38 | } 39 | } 40 | 41 | // Write writes a ImageRecipe to the given path, returning an error if any 42 | func (c *ImageRecipe) Write(path string) error { 43 | PrintVerboseInfo("ImageRecipe.Write", "running...") 44 | 45 | // create file 46 | file, err := os.Create(path) 47 | if err != nil { 48 | PrintVerboseErr("ImageRecipe.Write", 0, err) 49 | return err 50 | } 51 | defer file.Close() 52 | 53 | // write from 54 | _, err = file.WriteString(fmt.Sprintf("FROM %s\n", c.From)) 55 | if err != nil { 56 | PrintVerboseErr("ImageRecipe.Write", 1, err) 57 | return err 58 | } 59 | 60 | // write labels 61 | for key, value := range c.Labels { 62 | _, err = file.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value)) 63 | if err != nil { 64 | PrintVerboseErr("ImageRecipe.Write", 2, err) 65 | return err 66 | } 67 | } 68 | 69 | // write args 70 | for key, value := range c.Args { 71 | _, err = file.WriteString(fmt.Sprintf("ARG %s=%s\n", key, value)) 72 | if err != nil { 73 | PrintVerboseErr("ImageRecipe.Write", 3, err) 74 | return err 75 | } 76 | } 77 | 78 | // write content 79 | _, err = file.WriteString(c.Content) 80 | if err != nil { 81 | PrintVerboseErr("ImageRecipe.Write", 4, err) 82 | return err 83 | } 84 | 85 | PrintVerboseInfo("ImageRecipe.Write", "done") 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /cmd/rollback.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "os" 18 | 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/vanilla-os/abroot/core" 22 | "github.com/vanilla-os/orchid/cmdr" 23 | ) 24 | 25 | func NewRollbackCommand() *cmdr.Command { 26 | cmd := cmdr.NewCommand( 27 | "rollback", 28 | abroot.Trans("rollback.long"), 29 | abroot.Trans("rollback.short"), 30 | func(cmd *cobra.Command, args []string) error { 31 | err := rollback(cmd, args) 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | return nil 36 | }, 37 | ) 38 | 39 | cmd.WithBoolFlag( 40 | cmdr.NewBoolFlag( 41 | "check-only", 42 | "c", 43 | abroot.Trans("rollback.checkOnlyFlag"), 44 | false)) 45 | 46 | cmd.Example = "abroot rollback" 47 | 48 | return cmd 49 | } 50 | 51 | func rollback(cmd *cobra.Command, args []string) error { 52 | if !core.RootCheck(false) { 53 | cmdr.Error.Println(abroot.Trans("rollback.rootRequired")) 54 | return nil 55 | } 56 | 57 | checkOnly, err := cmd.Flags().GetBool("check-only") 58 | if err != nil { 59 | cmdr.Error.Println(err) 60 | return err 61 | } 62 | 63 | aBsys, err := core.NewABSystem() 64 | if err != nil { 65 | cmdr.Error.Println(err) 66 | return err 67 | } 68 | 69 | response, err := aBsys.Rollback(checkOnly) 70 | if err != nil { 71 | cmdr.Error.Println(err) 72 | os.Exit(2) 73 | return err 74 | } 75 | switch response { 76 | case core.ROLLBACK_RES_YES: 77 | // NOTE: the following strings could lead to misinterpretation, with 78 | // "can" and "cannot", we don't mean "is it possible to rollback?", 79 | // but "is it necessary to rollback?" 80 | cmdr.Info.Println(abroot.Trans("rollback.canRollback")) 81 | os.Exit(0) 82 | case core.ROLLBACK_RES_NO: 83 | cmdr.Info.Println(abroot.Trans("rollback.cannotRollback")) 84 | os.Exit(1) 85 | case core.ROLLBACK_UNNECESSARY: 86 | cmdr.Info.Println(abroot.Trans("rollback.rollbackUnnecessary")) 87 | os.Exit(0) 88 | case core.ROLLBACK_SUCCESS: 89 | cmdr.Info.Println(abroot.Trans("rollback.rollbackSuccess")) 90 | os.Exit(0) 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /core/specs.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "fmt" 18 | "os/exec" 19 | "strings" 20 | 21 | "github.com/shirou/gopsutil/cpu" 22 | "github.com/shirou/gopsutil/mem" 23 | ) 24 | 25 | type PCSpecs struct { 26 | CPU string 27 | GPU []string 28 | Memory string 29 | } 30 | 31 | type GPUInfo struct { 32 | Address string 33 | Description string 34 | } 35 | 36 | func getCPUInfo() (string, error) { 37 | info, err := cpu.Info() 38 | if err != nil { 39 | return "", err 40 | } 41 | if len(info) == 0 { 42 | return "", fmt.Errorf("CPU information not found") 43 | } 44 | return info[0].ModelName, nil 45 | } 46 | 47 | func parseGPUInfo(line string) (string, error) { 48 | parts := strings.SplitN(line, " ", 3) 49 | if len(parts) < 3 { 50 | return "", fmt.Errorf("GPU information not found") 51 | } 52 | 53 | parts = strings.SplitN(parts[2], ":", 2) 54 | if len(parts) < 2 { 55 | return "", fmt.Errorf("GPU information not found") 56 | } 57 | 58 | return strings.TrimSpace(parts[1]), nil 59 | } 60 | 61 | func getGPUInfo() ([]string, error) { 62 | cmd := exec.Command("lspci") 63 | output, err := cmd.CombinedOutput() 64 | if err != nil { 65 | fmt.Println("Error getting GPU info:", err) 66 | return nil, err 67 | } 68 | 69 | lines := strings.Split(string(output), "\n") 70 | 71 | var gpus []string 72 | for _, line := range lines { 73 | if strings.Contains(line, "VGA compatible controller") { 74 | gpu, err := parseGPUInfo(line) 75 | if err != nil { 76 | continue 77 | } 78 | gpus = append(gpus, gpu) 79 | } 80 | } 81 | 82 | return gpus, nil 83 | } 84 | 85 | func getMemoryInfo() (string, error) { 86 | vm, err := mem.VirtualMemory() 87 | if err != nil { 88 | return "", err 89 | } 90 | return fmt.Sprintf("%d MB", vm.Total/1024/1024), nil 91 | } 92 | 93 | func GetPCSpecs() PCSpecs { 94 | cpu, _ := getCPUInfo() 95 | gpu, _ := getGPUInfo() 96 | memory, _ := getMemoryInfo() 97 | 98 | return PCSpecs{ 99 | CPU: cpu, 100 | GPU: gpu, 101 | Memory: memory, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/diff_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/vanilla-os/abroot/core" 8 | ) 9 | 10 | // TestMergeDiff tests the MergeDiff function by creating 2 files with 11 | // different content and merging them into a destination file. The destination 12 | // file should reflect the changes. 13 | func TestMergeDiff(t *testing.T) { 14 | firstFile := t.TempDir() + "/first" 15 | err := os.WriteFile(firstFile, []byte("first file"), 0644) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | secondFile := t.TempDir() + "/second" 21 | err = os.WriteFile(secondFile, []byte("second file"), 0644) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | destination := t.TempDir() + "/destination" 27 | 28 | err = core.MergeDiff(firstFile, secondFile, destination) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | 33 | firstFileContents, err := os.ReadFile(firstFile) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | 38 | destinationContents, err := os.ReadFile(destination) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | 43 | if string(firstFileContents) != string(destinationContents) { 44 | t.Error("destination file does not have the same content as the second file") 45 | } 46 | 47 | t.Log("TestMergeDiff: destination file:", string(destinationContents)) 48 | t.Log("TestMergeDiff: done") 49 | } 50 | 51 | // TestDiffFiles tests the DiffFiles function in 2 cases: 52 | // 1. when the source and dest files have the same content 53 | // 2. when the source and dest files have different content 54 | func TestDiffFiles(t *testing.T) { 55 | // same content 56 | sourceFile := t.TempDir() + "/source" 57 | err := os.WriteFile(sourceFile, []byte("same file"), 0644) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | destFile := t.TempDir() + "/dest" 63 | err = os.WriteFile(destFile, []byte("same file"), 0644) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | diffLines, err := core.DiffFiles(sourceFile, destFile) 69 | if err != nil { 70 | t.Error(err) 71 | } 72 | 73 | if diffLines != nil { 74 | t.Error("diff lines should be nil") 75 | } 76 | 77 | t.Log("No diff found. Expected behavior.") 78 | 79 | // different content 80 | err = os.WriteFile(destFile, []byte("different file"), 0644) 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | 85 | diffLines, err = core.DiffFiles(sourceFile, destFile) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | 90 | if diffLines == nil { 91 | t.Error("diff lines should not be nil") 92 | } 93 | 94 | t.Log("Diff: ", string(diffLines)) 95 | t.Log("Diff found. Expected behavior.") 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build-artifacts: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ghcr.io/vanilla-os/pico:dev 13 | permissions: 14 | contents: read 15 | 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v6 21 | with: 22 | go-version-file: go.mod 23 | 24 | - name: Install Build Dependencies 25 | run: | 26 | dpkg --add-architecture arm64 27 | apt-get update 28 | apt-get install -y libbtrfs-dev libdevmapper-dev libgpgme-dev libdpkg-dev gcc patch pkgconf libdevmapper-dev:arm64 libdpkg-dev:arm64 gcc-aarch64-linux-gnu 29 | 30 | - name: Build 31 | run: | 32 | go build -o abrootv2 -ldflags="-X 'main.Version=${{ github.ref_name }}'" 33 | tar -czvf abrootv2-amd64.tar.gz abrootv2 34 | apt-get install -y libgpgme-dev:arm64 35 | GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig go build -o abrootv2 -ldflags="-X 'main.Version=${{ github.ref_name }}'" 36 | tar -czvf abrootv2-arm64.tar.gz abrootv2 37 | tar -czvf abroot-man.tar.gz man/man1/abroot.1 38 | 39 | - name: Calculate and Save Checksums 40 | run: | 41 | sha256sum abrootv2*.tar.gz >> checksums.txt 42 | sha256sum abroot-man.tar.gz >> checksums.txt 43 | 44 | - uses: actions/upload-artifact@v6 45 | with: 46 | name: abroot 47 | path: | 48 | abrootv2*.tar.gz 49 | abroot-man.tar.gz 50 | checksums.txt 51 | 52 | release: 53 | runs-on: ubuntu-latest 54 | needs: build-artifacts 55 | permissions: 56 | contents: write # to create and upload assets to releases 57 | attestations: write # to upload assets attestation for build provenance 58 | id-token: write # grant additional permission to attestation action to mint the OIDC token permission 59 | 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v5 63 | with: 64 | fetch-depth: 0 65 | 66 | - name: Download Artifact 67 | uses: actions/download-artifact@v7 68 | with: 69 | name: abroot 70 | 71 | - name: Create Release 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: gh release create "${{ github.ref_name }}" --generate-notes *.tar.gz checksums.txt 75 | 76 | - name: Attest Release Files 77 | id: attest 78 | uses: actions/attest-build-provenance@v3 79 | with: 80 | subject-path: '*.tar.gz, checksums.txt' 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "embed" 18 | 19 | "go.podman.io/storage/pkg/reexec" 20 | "github.com/vanilla-os/abroot/cmd" 21 | "github.com/vanilla-os/abroot/settings" 22 | "github.com/vanilla-os/orchid/cmdr" 23 | ) 24 | 25 | var Version = "development" 26 | 27 | //go:embed locales/*.yml 28 | var fs embed.FS 29 | var abroot *cmdr.App 30 | 31 | func main() { 32 | if reexec.Init() { 33 | return 34 | } 35 | 36 | abroot = cmd.New(Version, fs) 37 | 38 | // root command 39 | root := cmd.NewRootCommand(Version) 40 | abroot.CreateRootCommand(root, abroot.Trans("abroot.msg.help"), abroot.Trans("abroot.msg.version")) 41 | 42 | msgs := cmdr.UsageStrings{ 43 | Usage: abroot.Trans("abroot.msg.usage"), 44 | Aliases: abroot.Trans("abroot.msg.aliases"), 45 | Examples: abroot.Trans("abroot.msg.examples"), 46 | AvailableCommands: abroot.Trans("abroot.msg.availableCommands"), 47 | AdditionalCommands: abroot.Trans("abroot.msg.additionalCommands"), 48 | Flags: abroot.Trans("abroot.msg.flags"), 49 | GlobalFlags: abroot.Trans("abroot.msg.globalFlags"), 50 | AdditionalHelpTopics: abroot.Trans("abroot.msg.additionalHelpTopics"), 51 | MoreInfo: abroot.Trans("abroot.msg.moreInfo"), 52 | } 53 | abroot.SetUsageStrings(msgs) 54 | 55 | upgrade := cmd.NewUpgradeCommand() 56 | root.AddCommand(upgrade) 57 | 58 | kargs := cmd.NewKargsCommand() 59 | root.AddCommand(kargs) 60 | 61 | // we only add the pkg command if the ABRoot configuration 62 | // has the IPkgMng enabled in any way (1 or 2) 63 | if settings.Cnf.IPkgMngStatus > 0 { 64 | pkg := cmd.NewPkgCommand() 65 | root.AddCommand(pkg) 66 | } 67 | 68 | rollback := cmd.NewRollbackCommand() 69 | root.AddCommand(rollback) 70 | 71 | status := cmd.NewStatusCommand() 72 | root.AddCommand(status) 73 | 74 | updateInitramfs := cmd.NewUpdateInitfsCommand() 75 | root.AddCommand(updateInitramfs) 76 | 77 | cnf := cmd.NewConfCommand() 78 | root.AddCommand(cnf) 79 | 80 | unlockVar := cmd.NewUnlockVarCommand() 81 | root.AddCommand(unlockVar) 82 | 83 | mntSys := cmd.NewMountSysCommand() 84 | root.AddCommand(mntSys) 85 | 86 | rebase := cmd.NewRebaseCommand() 87 | root.AddCommand(rebase) 88 | 89 | // run the app 90 | err := abroot.Run() 91 | if err != nil { 92 | cmdr.Error.Println(err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/pkg_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | digest "github.com/opencontainers/go-digest" 9 | "github.com/vanilla-os/abroot/core" 10 | "github.com/vanilla-os/abroot/settings" 11 | ) 12 | 13 | // TestPackageManager tests the PackageManager functions by adding a package 14 | // and ensuring it gets added to the proper file. As a result, the final command 15 | // should not be empty. 16 | func TestPackageManager(t *testing.T) { 17 | pm, err := core.NewPackageManager(true) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | // Add a package 23 | pkg := "bash htop" 24 | err = pm.Add(pkg) 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | 29 | // Check if package is in packages.add 30 | pkgs, err := pm.GetAddPackages() 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | found := false 36 | for _, p := range pkgs { 37 | if p == pkg { 38 | found = true 39 | break 40 | } 41 | } 42 | 43 | if !found { 44 | t.Error("package was not added to packages.add") 45 | } 46 | 47 | // Get final cmd 48 | cmd, err := pm.GetFinalCmd() 49 | if len(cmd) == 0 || err != nil { 50 | t.Error("final cmd is empty") 51 | } 52 | 53 | // Check if package exists in repo 54 | for _, _pkg := range strings.Split(pkg, " ") { 55 | err = pm.ExistsInRepo(_pkg) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | } 60 | 61 | t.Log("TestPackageManager: done") 62 | } 63 | 64 | // TestBaseImagePackageDiff tests the BaseImagePackageDiff function by comparing 65 | // the packages of two different base images. 66 | func TestBaseImagePackageDiff(t *testing.T) { 67 | settings.Cnf.Name = "vanilla-os/core" 68 | 69 | oldDigest := digest.FromString("sha256:eac5693376d75cee2e676a83a67f4ce5db17d21e30bbde6a752480928719c842") 70 | newDigest := digest.FromString("sha256:eaa30f5a907f6f7785936a31f94fe291c6ce00943dcd1d3a8a6e40f1fc890346") 71 | 72 | added, upgraded, downgraded, removed, err := core.BaseImagePackageDiff(oldDigest, newDigest) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | fmt.Printf("Added: %v\n", added) 78 | fmt.Printf("Upgraded: %v\n", upgraded) 79 | fmt.Printf("Downgraded: %v\n", downgraded) 80 | fmt.Printf("Removed: %v\n", removed) 81 | 82 | t.Log("TestBaseImagePackageDiff: done") 83 | } 84 | 85 | // TestOverlayPackageDiff tests the OverlayPackageDiff function by obtaining the 86 | // added, removed, upgraded, and downgraded overlay packages. 87 | func TestOverlayPackageDiff(t *testing.T) { 88 | added, upgraded, downgraded, removed, err := core.OverlayPackageDiff() 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | fmt.Printf("Added: %v\n", added) 94 | fmt.Printf("Upgraded: %v\n", upgraded) 95 | fmt.Printf("Downgraded: %v\n", downgraded) 96 | fmt.Printf("Removed: %v\n", removed) 97 | 98 | t.Log("TestOverlayPackageDiff: done") 99 | } 100 | -------------------------------------------------------------------------------- /core/diff.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "bytes" 18 | "os" 19 | "os/exec" 20 | ) 21 | 22 | // MergeDiff merges the diff lines between the first and second files into 23 | // the destination file. If any errors occur, they are returned. 24 | func MergeDiff(firstFile, secondFile, destination string) error { 25 | PrintVerboseInfo("MergeDiff", "merging", firstFile, "+", secondFile, "->", destination) 26 | 27 | // get the diff lines 28 | diffLines, err := DiffFiles(firstFile, secondFile) 29 | if err != nil { 30 | PrintVerboseErr("MergeDiff", 0, err) 31 | return err 32 | } 33 | 34 | // copy second file to destination to apply patch 35 | secondFileContents, err := os.ReadFile(secondFile) 36 | if err != nil { 37 | PrintVerboseErr("MergeDiff", 1, err) 38 | return err 39 | } 40 | err = os.WriteFile(destination, secondFileContents, 0644) 41 | if err != nil { 42 | PrintVerboseErr("MergeDiff", 2, err) 43 | return err 44 | } 45 | 46 | // write the diff to the destination 47 | err = WriteDiff(destination, diffLines) 48 | if err != nil { 49 | PrintVerboseErr("MergeDiff", 3, err) 50 | return err 51 | } 52 | 53 | PrintVerboseInfo("MergeDiff", "merge completed") 54 | return nil 55 | } 56 | 57 | // DiffFiles returns the diff lines between source and dest files using the 58 | // diff command (assuming it is installed). If no diff is found, nil is 59 | // returned. If any errors occur, they are returned. 60 | func DiffFiles(sourceFile, destFile string) ([]byte, error) { 61 | PrintVerboseInfo("DiffFiles", "diffing", sourceFile, "and", destFile) 62 | 63 | cmd := exec.Command("diff", "-u", sourceFile, destFile) 64 | var out bytes.Buffer 65 | cmd.Stdout = &out 66 | errCode := 0 67 | err := cmd.Run() 68 | if err != nil { 69 | if exitError, ok := err.(*exec.ExitError); ok { 70 | errCode = exitError.ExitCode() 71 | } 72 | } 73 | 74 | // diff returns 1 if there are differences 75 | if errCode == 1 { 76 | PrintVerboseInfo("DiffFiles", "diff found") 77 | return out.Bytes(), nil 78 | } 79 | 80 | PrintVerboseInfo("DiffFiles", "no diff found") 81 | return nil, nil 82 | } 83 | 84 | // WriteDiff applies the diff lines to the destination file using the patch 85 | // command (assuming it is installed). If any errors occur, they are returned. 86 | func WriteDiff(destFile string, diffLines []byte) error { 87 | PrintVerboseInfo("WriteDiff", "applying diff to", destFile) 88 | if len(diffLines) == 0 { 89 | PrintVerboseInfo("WriteDiff", "no changes to apply") 90 | return nil // no changes to apply 91 | } 92 | 93 | cmd := exec.Command("patch", "-R", destFile) 94 | cmd.Stdin = bytes.NewReader(diffLines) 95 | cmd.Stdout = os.Stdout 96 | cmd.Stderr = os.Stderr 97 | err := cmd.Run() 98 | if err != nil { 99 | PrintVerboseErr("WriteDiff", 0, err) 100 | return err 101 | } 102 | 103 | PrintVerboseInfo("WriteDiff", "diff applied") 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /core/image.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "time" 22 | 23 | digest "github.com/opencontainers/go-digest" 24 | ) 25 | 26 | // The ABImage is the representation of an OCI image used by ABRoot, it 27 | // contains the digest, the timestamp and the image name. If you need to 28 | // investigate the current ABImage on an ABRoot system, you can find it 29 | // at /abimage.abr 30 | type ABImage struct { 31 | Digest digest.Digest `json:"digest"` 32 | Timestamp time.Time `json:"timestamp"` 33 | Image string `json:"image"` 34 | } 35 | 36 | // NewABImage creates a new ABImage instance and returns a pointer to it, 37 | // if the digest is empty, it returns an error 38 | func NewABImage(digest digest.Digest, image string) (*ABImage, error) { 39 | if digest == "" { 40 | return nil, fmt.Errorf("NewABImage: digest is empty") 41 | } 42 | 43 | return &ABImage{ 44 | Digest: digest, 45 | Timestamp: time.Now(), 46 | Image: image, 47 | }, nil 48 | } 49 | 50 | // NewABImageFromRoot returns the current ABImage by parsing /abimage.abr, if 51 | // it fails, it returns an error (e.g. if the file doesn't exist). 52 | // Note for distro maintainers: if the /abimage.abr is not present, it could 53 | // mean that the user is running an older version of ABRoot (pre v2) or the 54 | // root state is corrupted. In the latter case, generating a new ABImage should 55 | // fix the issue, Digest and Timestamp can be random, but Image should reflect 56 | // an existing image on the configured Docker registry. Anyway, support on this 57 | // is not guaranteed, so please don't open issues about this. 58 | func NewABImageFromRoot() (*ABImage, error) { 59 | PrintVerboseInfo("NewABImageFromRoot", "running...") 60 | 61 | abimage, err := os.ReadFile("/abimage.abr") 62 | if err != nil { 63 | PrintVerboseErr("NewABImageFromRoot", 0, err) 64 | return nil, err 65 | } 66 | 67 | var a ABImage 68 | err = json.Unmarshal(abimage, &a) 69 | if err != nil { 70 | PrintVerboseErr("NewABImageFromRoot", 1, err) 71 | return nil, err 72 | } 73 | 74 | PrintVerboseInfo("NewABImageFromRoot", "found abimage.abr: "+a.Digest) 75 | return &a, nil 76 | } 77 | 78 | // WriteTo writes the json to a destination path, if the suffix is not empty, 79 | // it will be appended to the filename 80 | func (a *ABImage) WriteTo(dest string) error { 81 | PrintVerboseInfo("ABImage.WriteTo", "running...") 82 | 83 | if _, err := os.Stat(dest); os.IsNotExist(err) { 84 | err = os.MkdirAll(dest, 0755) 85 | if err != nil { 86 | PrintVerboseErr("ABImage.WriteTo", 0, err) 87 | return err 88 | } 89 | } 90 | 91 | imagePath := filepath.Join(dest, "abimage.abr") 92 | 93 | abimage, err := json.Marshal(a) 94 | if err != nil { 95 | PrintVerboseErr("ABImage.WriteTo", 1, err) 96 | return err 97 | } 98 | 99 | err = os.WriteFile(imagePath, abimage, 0644) 100 | if err != nil { 101 | PrintVerboseErr("ABImage.WriteTo", 2, err) 102 | return err 103 | } 104 | 105 | PrintVerboseInfo("ABImage.WriteTo", "successfully wrote abimage.abr to "+imagePath) 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /cmd/rebase.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/vanilla-os/abroot/core" 10 | "github.com/vanilla-os/orchid/cmdr" 11 | ) 12 | 13 | func NewRebaseCommand() *cmdr.Command { 14 | cmd := cmdr.NewCommand( 15 | "rebase ", 16 | abroot.Trans("rebase.long"), 17 | abroot.Trans("rebase.short"), 18 | rebase, 19 | ) 20 | 21 | cmd.WithBoolFlag( 22 | cmdr.NewBoolFlag( 23 | "dry-run", 24 | "d", 25 | abroot.Trans("rebase.dryRunFlag"), 26 | false, 27 | )) 28 | 29 | cmd.WithBoolFlag( 30 | cmdr.NewBoolFlag( 31 | "remove-packages", 32 | "r", 33 | abroot.Trans("rebase.removePackagesShort"), 34 | false, 35 | )) 36 | 37 | cmd.WithBoolFlag( 38 | cmdr.NewBoolFlag( 39 | "keep-packages", 40 | "k", 41 | abroot.Trans("rebase.keepPackages"), 42 | false, 43 | )) 44 | 45 | cmd.WithBoolFlag( 46 | cmdr.NewBoolFlag( 47 | "rebase-only", 48 | "n", 49 | abroot.Trans("rebase.rebaseOnly"), 50 | false, 51 | )) 52 | 53 | cmd.Args = cobra.ExactArgs(1) 54 | cmd.Example = "abroot rebase ghcr.io/vanilla-os/desktop:main" 55 | 56 | return cmd 57 | } 58 | 59 | func rebase(cmd *cobra.Command, args []string) error { 60 | 61 | if !core.RootCheck(false) { 62 | cmdr.Error.Println(abroot.Trans("rebase.rootRequired")) 63 | return nil 64 | } 65 | 66 | dryRun, err := cmd.Flags().GetBool("dry-run") 67 | if err != nil { 68 | cmdr.Error.Println(err) 69 | return err 70 | } 71 | 72 | if err != nil { 73 | cmdr.Error.Println(err) 74 | return err 75 | } 76 | 77 | removePackages, err := cmd.Flags().GetBool("remove-packages") 78 | if err != nil { 79 | cmdr.Error.Println(err) 80 | return err 81 | } 82 | 83 | keepPackages, err := cmd.Flags().GetBool("keep-packages") 84 | if err != nil { 85 | cmdr.Error.Println(err) 86 | return err 87 | } 88 | 89 | if removePackages && keepPackages { 90 | flagError := errors.New(abroot.Trans("rebase.flagError")) 91 | cmdr.Error.Println(flagError) 92 | return err 93 | } 94 | 95 | name := args[0] 96 | abSys, err := core.NewABSystem() 97 | if err != nil { 98 | cmdr.Error.Println(err) 99 | return err 100 | } 101 | 102 | pkgM, err := core.NewPackageManager(dryRun) 103 | if pkgM.Status == core.PKG_MNG_ENABLED { 104 | var removePackagesPrompt string 105 | if !keepPackages && !removePackages { 106 | cmdr.Info.Print(abroot.Trans("rebase.removePackagesLong"), " (y/N) ", " ") 107 | fmt.Scanln(&removePackagesPrompt) 108 | } 109 | if strings.Contains(removePackagesPrompt, "y") || removePackages { 110 | addedPackages, err := pkgM.GetAddPackages() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | core.PrintVerboseInfo("cmd.Rebase", "Removing packages: ", addedPackages) 116 | 117 | if !dryRun { 118 | for _, v := range addedPackages { 119 | pkgM.Remove(v) 120 | } 121 | } 122 | cmdr.Info.Println(abroot.Trans("rebase.pkgRemoveSuccess")) 123 | } 124 | } 125 | 126 | err = abSys.Rebase(name, dryRun) 127 | if err != nil { 128 | cmdr.Error.Println(err) 129 | return err 130 | } 131 | 132 | if dryRun { 133 | cmdr.Info.Println(abroot.Trans("rebase.dryRunSuccess")) 134 | } 135 | 136 | rebaseOnly, err := cmd.Flags().GetBool("rebase-only") 137 | if err != nil { 138 | cmdr.Error.Println(err) 139 | return err 140 | } 141 | if rebaseOnly { 142 | cmdr.Info.Println(abroot.Trans("rebase.success")) 143 | return nil 144 | } 145 | 146 | cmdr.Info.Println(abroot.Trans("rebase.successUpdate")) 147 | return abSys.RunOperation("UPGRADE", false) 148 | } 149 | -------------------------------------------------------------------------------- /core/checks.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "net" 20 | "os" 21 | "os/exec" 22 | "runtime" 23 | "time" 24 | ) 25 | 26 | // Represents a Checks struct which contains all the checks which can 27 | // be performed one by one or all at once using PerformAllChecks() 28 | type Checks struct{} 29 | 30 | // NewChecks returns a new Checks struct 31 | func NewChecks() *Checks { 32 | return &Checks{} 33 | } 34 | 35 | // PerformAllChecks performs all checks 36 | func (c *Checks) PerformAllChecks() error { 37 | err := c.CheckCompatibilityFS() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | err = c.CheckConnectivity() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = c.CheckRoot() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // CheckCompatibilityFS checks if the filesystem is compatible with ABRoot v2 56 | // if not, it returns an error. Note that currently only ext4, btrfs and xfs 57 | // are supported/tested. Here we assume some utilities are installed, such as 58 | // findmnt and lsblk 59 | func (c *Checks) CheckCompatibilityFS() error { 60 | PrintVerboseInfo("Checks.CheckCompatibilityFS", "running...") 61 | 62 | var fs []string 63 | if runtime.GOOS == "linux" { 64 | fs = []string{"ext4", "btrfs", "xfs"} 65 | } else { 66 | err := fmt.Errorf("your OS (%s) is not supported", runtime.GOOS) 67 | PrintVerboseErr("Checks.CheckCompatibilityFS", 0, err) 68 | return err 69 | } 70 | 71 | cmd, err := exec.Command("findmnt", "-n", "-o", "source", "/").Output() 72 | if err != nil { 73 | PrintVerboseErr("Checks.CheckCompatibilityFS", 1, err) 74 | return err 75 | } 76 | device := string([]byte(cmd[:len(cmd)-1])) 77 | 78 | cmd, err = exec.Command("lsblk", "-o", "fstype", "-n", device).Output() 79 | if err != nil { 80 | PrintVerboseErr("Checks.CheckCompatibilityFS", 2, err) 81 | return err 82 | } 83 | fsType := string([]byte(cmd[:len(cmd)-1])) 84 | 85 | for _, f := range fs { 86 | if f == string(fsType) { 87 | PrintVerboseInfo("CheckCompatibilityFS", fsType, "is supported") 88 | return nil 89 | } 90 | } 91 | 92 | err = fmt.Errorf("the filesystem (%s) is not supported", fsType) 93 | PrintVerboseErr("Checks.CheckCompatibilityFS", 3, err) 94 | return err 95 | } 96 | 97 | // CheckConnectivity checks if the system is connected to the internet 98 | func (c *Checks) CheckConnectivity() error { 99 | PrintVerboseInfo("Checks.CheckConnectivity", "running...") 100 | 101 | timeout := 5 * time.Second 102 | _, err := net.DialTimeout("tcp", "vanillaos.org:80", timeout) 103 | if err != nil { 104 | PrintVerboseErr("Checks.CheckConnectivity", 1, err) 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // CheckRoot checks if the user is root and returns an error if not 112 | func (c *Checks) CheckRoot() error { 113 | PrintVerboseInfo("Checks.CheckRoot", "running...") 114 | 115 | if os.Geteuid() == 0 { 116 | PrintVerboseInfo("Checks.CheckRoot", "you are root") 117 | return nil 118 | } 119 | 120 | err := errors.New("not root") 121 | PrintVerboseErr("Checks.CheckRoot", 1, err) 122 | return err 123 | } 124 | -------------------------------------------------------------------------------- /core/integrity.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "os" 19 | "os/exec" 20 | "path/filepath" 21 | ) 22 | 23 | // links that must exist in the root partition 24 | var linksToRepair = [...][2]string{ 25 | {"run/media", "media"}, 26 | {"usr/bin", "bin"}, 27 | {"usr/lib", "lib"}, 28 | {"usr/lib64", "lib64"}, 29 | {"usr/sbin", "sbin"}, 30 | {"var/home", "home"}, 31 | {"var/mnt", "mnt"}, 32 | {"var/root", "root"}, 33 | {"var/srv", "srv"}, 34 | } 35 | 36 | // paths that must exist in the root partition 37 | var pathsToRepair = [...]string{ 38 | "boot", 39 | "dev", 40 | "opt", 41 | "part-future", 42 | "proc", 43 | "run", 44 | "sys", 45 | "tmp", 46 | "var", 47 | "var/home", 48 | "var/mnt", 49 | "var/opt", 50 | "var/root", 51 | "var/srv", 52 | } 53 | 54 | func RepairRootIntegrity(rootPath string) (err error) { 55 | fixupOlderSystems(rootPath) 56 | 57 | err = repairLinks(rootPath) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | err = repairPaths(rootPath) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func repairLinks(rootPath string) (err error) { 71 | for _, link := range linksToRepair { 72 | sourceAbs := filepath.Join(rootPath, link[0]) 73 | targetAbs := filepath.Join(rootPath, link[1]) 74 | 75 | err = repairLink(sourceAbs, targetAbs) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func repairLink(sourceAbs, targetAbs string) (err error) { 84 | target := targetAbs 85 | source, err := filepath.Rel(filepath.Dir(target), sourceAbs) 86 | if err != nil { 87 | PrintVerboseErr("repairLink", 1, "Can't make ", source, " relative to ", target, " : ", err) 88 | return err 89 | } 90 | 91 | dest, err := os.Readlink(target) 92 | if err != nil || dest != source { 93 | err = os.RemoveAll(target) 94 | if err != nil && !os.IsNotExist(err) { 95 | PrintVerboseErr("repairLink", 2, "Can't remove ", target, " : ", err) 96 | return err 97 | } 98 | 99 | PrintVerboseInfo("repairLink", "Repairing ", target, " -> ", source) 100 | err = os.Symlink(source, target) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func repairPaths(rootPath string) (err error) { 110 | for _, path := range pathsToRepair { 111 | err = repairPath(filepath.Join(rootPath, path)) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | func repairPath(path string) (err error) { 120 | if info, err := os.Lstat(path); err == nil && info.IsDir() { 121 | return nil 122 | } 123 | 124 | err = os.Remove(path) 125 | if err != nil && !os.IsNotExist(err) { 126 | PrintVerboseErr("repairPath", 1, "Can't remove ", path, " : ", err) 127 | return err 128 | } 129 | 130 | PrintVerboseInfo("repairPath", "Repairing ", path) 131 | err = os.MkdirAll(path, 0o755) 132 | if err != nil { 133 | PrintVerboseErr("repairPath", 2, "Can't create ", path, " : ", err) 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | 140 | // this is here to keep compatibility with older systems 141 | func fixupOlderSystems(rootPath string) { 142 | paths := []string{ 143 | "mnt", 144 | "root", 145 | } 146 | 147 | for _, path := range paths { 148 | legacyPath := filepath.Join(rootPath, path) 149 | newPath := filepath.Join("/var", path) 150 | 151 | if _, err := os.Lstat(newPath); errors.Is(err, os.ErrNotExist) { 152 | err = exec.Command("mv", legacyPath, newPath).Run() 153 | if err != nil { 154 | PrintVerboseErr("fixupOlderSystems", 1, "could not move ", legacyPath, " to ", newPath, " : ", err) 155 | // if moving failed it probably means that it migrated successfully in the past 156 | // so it's safe to ignore errors 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Setup Notes 2 | 3 | ## Root Structure 4 | 5 | - / 6 | - /abimage.abr 7 | - /bin -> usr/bin 8 | - /boot 9 | - /dev 10 | - /etc 11 | - /FsGuard 12 | - /home -> var/home 13 | - /lib -> usr/lib 14 | - /lib64 -> usr/lib64 15 | - /media -> run/media 16 | - /mnt -> var/mnt 17 | - /opt 18 | - /part-future 19 | - /proc 20 | - /root -> var/root 21 | - /run 22 | - /sbin -> usr/sbin 23 | - /srv -> var/srv 24 | - /sys 25 | - /sysconf 26 | - /tmp 27 | - /usr 28 | - /var 29 | 30 | ## abimage.abr Example 31 | 32 | ```json 33 | { 34 | "digest":"sha256:e9f2773ab60fabf4e456e4549d86e345b38754f1f8397183ce4dc28d52bab66e", 35 | "timestamp":"2023-04-23T15:13:19.066903531+02:00", 36 | "image":"registry.vanillaos.org/vanillaos/desktop:main" 37 | } 38 | ``` 39 | 40 | ## Boot Structure 41 | 42 | - / 43 | - /grub 44 | - /grub/grub.cfg 45 | - /grub/grub.cfg.future 46 | 47 | > Check the `/samples/grub` folder for examples. 48 | 49 | Essentially, we need 2 copy of the `/samples/grub/bootPart.grub.cfg` file, 50 | one for the current root and one for the future root. What changes is the 51 | order of the menu entries, the present is always the first entry. So we 52 | have 1 file with A (current) and B (previous) and another file with 53 | B (current) and A (previous). 54 | 55 | We can use `set default=0` too but this way the result should be more 56 | understandable for the user. 57 | 58 | After a successful update, `grub.cfg` and `grub.cfg.future` are swapped. 59 | 60 | ## Init Structure 61 | 62 | Each root has a folder in init partition with the following structure: 63 | 64 | - /vos-a/abroot.cfg 65 | - /vos-a/config- 66 | - /vos-a/initrd.img- 67 | - /vos-a/System.map- 68 | - /vos-a/vmlinuz- 69 | 70 | > Check the `/samples/grub` folder for examples. 71 | 72 | The `abroot.cfg` file is the same as the `rootPart.abroot.cfg` file but 73 | with the `root=` parameter set to the correct UUID. 74 | 75 | Note that this file is being loaded as a configuration using the `configfile` 76 | command. Do not include a `menuentry`, otherwise it will result in 77 | a submenu. (Good for advanced cases?). 78 | 79 | # Development Notes 80 | 81 | ## Root Lifecycle 82 | 83 | The root lifecycle is composed of 2 stages: 84 | 85 | - **Current**: The root which is currently being used by the system. 86 | - **Future**: The root which will be used by the system after an update. 87 | 88 | The "current" root should never be modified, in any way. The "future" root is 89 | the root which is being modified by the update process and any other process 90 | which may require root modification (e.g. kargs, fstab..). 91 | 92 | ## System Update / Root Re-generation 93 | 94 | When developing new features which does not envolve the update process, it is 95 | important to consider the possibility of regenerating the root, as opposed to 96 | performing a specific update. This may be necessary, for example, when updating 97 | a kernel flag or fstab entry. To regenerate the root, developers should use the 98 | Containerfile and avoid taking data from the current root. 99 | 100 | It is important to note that regenerating the root does not require pulling a 101 | new image; rather, the latest image in the storage can be used. In ABRoot, 102 | Prometheus (the container runtime used) makes this process easier, as 103 | generating an image from the Containerfile does not execute a pull if the image 104 | is already present in the store. During the update process, however, it is 105 | necessary to force the pull to take the updated image (this may not happen 106 | during the development process). 107 | 108 | In the case of a rollback, the AtomicSwap function should be used to swap the 109 | current grub configuration with the future one. 110 | 111 | ## Function Data Files 112 | 113 | Some functions like kargs, fstab and pkg, need to store data to be used in the 114 | future. Those should always be placed in the current `/etc/abroot/..` path. 115 | 116 | If you are concerned that the current root should never be changed, you are 117 | correct but in this case the `/etc` path is a combined overlay of the root and 118 | respective `/etc` path files in the var partition: 119 | 120 | ``` 121 | /etc -> /var/lib/abroot/etc/a 122 | ``` 123 | 124 | Even if the root is read-only, the `/etc` path is not. 125 | -------------------------------------------------------------------------------- /core/package-diff.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "strings" 22 | 23 | digest "github.com/opencontainers/go-digest" 24 | "github.com/vanilla-os/abroot/extras/dpkg" 25 | "github.com/vanilla-os/abroot/settings" 26 | "github.com/vanilla-os/differ/diff" 27 | ) 28 | 29 | // BaseImagePackageDiff retrieves the added, removed, upgraded and downgraded 30 | // base packages (the ones bundled with the image). 31 | func BaseImagePackageDiff(currentDigest, newDigest digest.Digest) ( 32 | added, upgraded, downgraded, removed []diff.PackageDiff, 33 | err error, 34 | ) { 35 | PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "running...") 36 | 37 | imageComponents := strings.Split(settings.Cnf.Name, "/") 38 | imageName := imageComponents[len(imageComponents)-1] 39 | reqUrl := fmt.Sprintf("%s/images/%s/diff", settings.Cnf.DifferURL, imageName) 40 | body := fmt.Sprintf("{\"old_digest\": \"%s\", \"new_digest\": \"%s\"}", currentDigest, newDigest) 41 | 42 | PrintVerboseInfo("PackageDiff.BaseImagePackageDiff", "Requesting base image diff to", reqUrl, "with body", body) 43 | 44 | request, err := http.NewRequest(http.MethodGet, reqUrl, strings.NewReader(body)) 45 | if err != nil { 46 | PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 0, err) 47 | return 48 | } 49 | defer request.Body.Close() 50 | 51 | resp, err := http.DefaultClient.Do(request) 52 | if err != nil { 53 | PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 1, err) 54 | return 55 | } 56 | defer resp.Body.Close() 57 | 58 | if resp.StatusCode != http.StatusOK { 59 | PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 2, fmt.Errorf("received non-OK status %s", resp.Status)) 60 | return 61 | } 62 | 63 | contents, err := io.ReadAll(resp.Body) 64 | if err != nil { 65 | PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 3, err) 66 | return 67 | } 68 | 69 | pkgDiff := struct { 70 | Added, Upgraded, Downgraded, Removed []diff.PackageDiff 71 | }{} 72 | err = json.Unmarshal(contents, &pkgDiff) 73 | if err != nil { 74 | PrintVerboseErr("PackageDiff.BaseImagePackageDiff", 4, err) 75 | return 76 | } 77 | 78 | added = pkgDiff.Added 79 | upgraded = pkgDiff.Upgraded 80 | downgraded = pkgDiff.Downgraded 81 | removed = pkgDiff.Removed 82 | 83 | return 84 | } 85 | 86 | // OverlayPackageDiff retrieves the added, removed, upgraded and downgraded 87 | // overlay packages (the ones added manually via `abroot pkg add`). 88 | func OverlayPackageDiff() ( 89 | added, upgraded, downgraded, removed []diff.PackageDiff, 90 | err error, 91 | ) { 92 | PrintVerboseInfo("OverlayPackageDiff", "running...") 93 | 94 | pkgM, err := NewPackageManager(false) 95 | if err != nil { 96 | PrintVerboseErr("OverlayPackageDiff", 0, err) 97 | return 98 | } 99 | 100 | addedPkgs, err := pkgM.GetAddPackages() 101 | if err != nil { 102 | PrintVerboseErr("PackageDiff.OverlayPackageDiff", 0, err) 103 | return 104 | } 105 | 106 | localAddedVersions := dpkg.DpkgBatchGetPackageVersion(addedPkgs) 107 | localAdded := map[string]string{} 108 | for i := 0; i < len(addedPkgs); i++ { 109 | if localAddedVersions[i] != "" { 110 | localAdded[addedPkgs[i]] = localAddedVersions[i] 111 | } 112 | } 113 | 114 | remoteAdded := map[string]string{} 115 | var pkgInfo map[string]interface{} 116 | for pkgName := range localAdded { 117 | pkgInfo, err = GetRepoContentsForPkg(pkgName) 118 | if err != nil { 119 | PrintVerboseErr("PackageDiff.OverlayPackageDiff", 1, err) 120 | return 121 | } 122 | version, ok := pkgInfo["version"].(string) 123 | if !ok { 124 | err = fmt.Errorf("unexpected value when retrieving upstream version of '%s'", pkgName) 125 | return 126 | } 127 | remoteAdded[pkgName] = version 128 | } 129 | 130 | added, upgraded, downgraded, removed = diff.DiffPackages(localAdded, remoteAdded) 131 | return 132 | } 133 | -------------------------------------------------------------------------------- /man/man1/abroot.1: -------------------------------------------------------------------------------- 1 | .TH ABROOT 1 "2024-01-21" "abroot" "User Manual" 2 | .SH NAME 3 | .RS 4 4 | abroot - ABRoot provides full immutability and atomicity by performing transactions between 2 root partitions (A<->B) 5 | .RE 6 | .SH SYNOPSIS 7 | .RS 4 8 | abroot [command] [flags] [arguments] 9 | .RE 10 | .SH DESCRIPTION 11 | .RS 4 12 | ABRoot provides full immutability and atomicity by performing transactions between 2 root partitions (A<->B) 13 | .RE 14 | .SH OPTIONS 15 | -v, --verbose show more detailed output 16 | .PP 17 | .SH ABROOT COMMANDS 18 | .RS 4 19 | \fBupgrade\fP 20 | .RS 4 21 | Upgrade the system 22 | .PP 23 | .RE 24 | \fBkargs\fP 25 | .RS 4 26 | Manage kernel parameters 27 | .PP 28 | .RE 29 | \fBpkg\fP 30 | .RS 4 31 | Manage packages 32 | .PP 33 | .RE 34 | \fBrollback\fP 35 | .RS 4 36 | Return the system to a previous state 37 | .PP 38 | .RE 39 | \fBstatus\fP 40 | .RS 4 41 | Display status 42 | .PP 43 | .RE 44 | \fBupdate-initramfs\fP 45 | .RS 4 46 | Update the initramfs 47 | .PP 48 | .RE 49 | .RE 50 | .SH AUTHORS 51 | .PP 52 | Written by Vanilla OS contributors\&. 53 | .SH REPORT BUGS TO 54 | .PP 55 | https://github\&.com/vanilla-os/abroot/issues 56 | .SH LICENSE 57 | .PP 58 | GPLv3+: GNU GPL version 3 or later \&. 59 | .SH ABROOT SUBCOMMANDS 60 | .SH SUBCOMMAND UPGRADE 61 | .RS 4 62 | Upgrade the system 63 | .RE 64 | .SS SYNOPSIS 65 | .RS 4 66 | \fBupgrade\fP [command] [flags] [arguments] 67 | .RE 68 | .SS DESCRIPTION 69 | .RS 4 70 | .TP 4 71 | Check for a new system image and apply it\&. 72 | .RE 73 | .SS OPTIONS 74 | -c, --check-only check for updates but do not apply them 75 | .PP 76 | -d, --dry-run perform a dry run of the operation 77 | .PP 78 | -f, --force force update even if the system is up to date 79 | .PP 80 | .SS GLOBAL OPTIONS 81 | -v, --verbose show more detailed output 82 | .PP 83 | .SS EXAMPLES 84 | .RS 4 85 | abroot upgrade 86 | .RE 87 | .SH SUBCOMMAND KARGS 88 | .RS 4 89 | Manage kernel parameters 90 | .RE 91 | .SS SYNOPSIS 92 | .RS 4 93 | \fBkargs\fP [command] [flags] [arguments] 94 | .RE 95 | .SS DESCRIPTION 96 | .RS 4 97 | .TP 4 98 | Manage kernel parameters\&. 99 | .RE 100 | .SS OPTIONS 101 | .SS GLOBAL OPTIONS 102 | -v, --verbose show more detailed output 103 | .PP 104 | .SS EXAMPLES 105 | .RS 4 106 | abroot kargs edit 107 | .RE 108 | .SH SUBCOMMAND PKG 109 | .RS 4 110 | Manage packages 111 | .RE 112 | .SS SYNOPSIS 113 | .RS 4 114 | \fBpkg\fP [command] [flags] [arguments] 115 | .RE 116 | .SS DESCRIPTION 117 | .RS 4 118 | .TP 4 119 | Install and manage packages\&. 120 | .RE 121 | .SS OPTIONS 122 | -d, --dry-run perform a dry run of the operation 123 | .PP 124 | -f, --force-enable-user-agreement force enable user agreement, for embedded systems 125 | .PP 126 | .SS GLOBAL OPTIONS 127 | -v, --verbose show more detailed output 128 | .PP 129 | .SS EXAMPLES 130 | .RS 4 131 | abroot pkg add 132 | .RE 133 | .SH SUBCOMMAND ROLLBACK 134 | .RS 4 135 | Return the system to a previous state 136 | .RE 137 | .SS SYNOPSIS 138 | .RS 4 139 | \fBrollback\fP [command] [flags] [arguments] 140 | .RE 141 | .SS DESCRIPTION 142 | .RS 4 143 | .TP 4 144 | Executes a system rollback, discarding changes made to the present root\&. 145 | .RE 146 | .SS OPTIONS 147 | -c, --check-only rollback\&.checkOnlyFlag 148 | .PP 149 | .SS GLOBAL OPTIONS 150 | -v, --verbose show more detailed output 151 | .PP 152 | .SS EXAMPLES 153 | .RS 4 154 | abroot rollback 155 | .RE 156 | .SH SUBCOMMAND STATUS 157 | .RS 4 158 | Display status 159 | .RE 160 | .SS SYNOPSIS 161 | .RS 4 162 | \fBstatus\fP [command] [flags] [arguments] 163 | .RE 164 | .SS DESCRIPTION 165 | .RS 4 166 | .TP 4 167 | Display the current ABRoot status\&. 168 | .RE 169 | .SS OPTIONS 170 | -d, --dump Dump the ABRoot status to an archive 171 | .PP 172 | -j, --json Show output in JSON format 173 | .PP 174 | .SS GLOBAL OPTIONS 175 | -v, --verbose show more detailed output 176 | .PP 177 | .SS EXAMPLES 178 | .RS 4 179 | abroot status 180 | .RE 181 | .SH SUBCOMMAND UPDATE-INITRAMFS 182 | .RS 4 183 | Update the initramfs 184 | .RE 185 | .SS SYNOPSIS 186 | .RS 4 187 | \fBupdate-initramfs\fP [command] [flags] [arguments] 188 | .RE 189 | .SS DESCRIPTION 190 | .RS 4 191 | .TP 4 192 | Update the initramfs of the future root\&. 193 | .RE 194 | .SS OPTIONS 195 | -d, --dry-run updateInitramfs\&.dryRunFlag 196 | .PP 197 | .SS GLOBAL OPTIONS 198 | -v, --verbose show more detailed output 199 | .PP 200 | .SS EXAMPLES 201 | .RS 4 202 | abroot update-initramfs 203 | .RE 204 | 205 | -------------------------------------------------------------------------------- /cmd/unlock-var.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "os" 19 | "os/exec" 20 | "path/filepath" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/vanilla-os/abroot/core" 25 | "github.com/vanilla-os/abroot/settings" 26 | "github.com/vanilla-os/orchid/cmdr" 27 | ) 28 | 29 | type VarInvalidError struct { 30 | passedDisk string 31 | } 32 | 33 | func (e *VarInvalidError) Error() string { 34 | return "the /var disk " + e.passedDisk + " does not exist" 35 | } 36 | 37 | type NotEncryptedError struct{} 38 | 39 | func (e *NotEncryptedError) Error() string { 40 | return "the var partition is not encrypted" 41 | } 42 | 43 | func NewUnlockVarCommand() *cmdr.Command { 44 | cmd := cmdr.NewCommand( 45 | "unlock-var", 46 | "", 47 | "", 48 | unlockVarCmd, 49 | ) 50 | 51 | cmd.WithBoolFlag( 52 | cmdr.NewBoolFlag( 53 | "dry-run", 54 | "d", 55 | "perform a dry run of the operation", 56 | false, 57 | ), 58 | ) 59 | 60 | // this is just meant for compatability with old Installations 61 | cmd.WithStringFlag( 62 | cmdr.NewStringFlag( 63 | "var-disk", 64 | "m", 65 | "pass /var disk directly instead of reading from configuration", 66 | "", 67 | ), 68 | ) 69 | 70 | cmd.WithBoolFlag( 71 | cmdr.NewBoolFlag( 72 | "check-encrypted", 73 | "c", 74 | "check if drive is encrypted and return", 75 | false, 76 | ), 77 | ) 78 | 79 | cmd.Example = "abroot unlock-var" 80 | 81 | cmd.Hidden = true 82 | 83 | return cmd 84 | } 85 | 86 | // helper function which only returns syntax errors and prints other ones 87 | func unlockVarCmd(cmd *cobra.Command, args []string) error { 88 | err := unlockVar(cmd, args) 89 | if err != nil { 90 | cmdr.Error.Println(err) 91 | os.Exit(1) 92 | return nil 93 | } 94 | return nil 95 | } 96 | 97 | func unlockVar(cmd *cobra.Command, _ []string) error { 98 | if !core.RootCheck(false) { 99 | cmdr.Error.Println("You must be root to run this command.") 100 | os.Exit(2) 101 | return nil 102 | } 103 | 104 | varDisk, err := cmd.Flags().GetString("var-disk") 105 | if err != nil { 106 | return err 107 | } 108 | 109 | check_only, err := cmd.Flags().GetBool("check-encrypted") 110 | if err != nil { 111 | return err 112 | } 113 | 114 | _, err = os.Stat(filepath.Join("/dev/disk/by-label/", settings.Cnf.PartLabelVar)) 115 | if err == nil || !errors.Is(err, os.ErrNotExist) { 116 | return &NotEncryptedError{} 117 | } 118 | if check_only { 119 | cmdr.Info.Println("The var partition is encrypted.") 120 | return nil 121 | } 122 | 123 | if varDisk == "" { 124 | if settings.Cnf.PartCryptVar == "" { 125 | cmdr.Error.Println("Encrypted var partition not found in configuration.") 126 | os.Exit(3) 127 | return nil 128 | } 129 | 130 | varDisk = settings.Cnf.PartCryptVar 131 | } 132 | 133 | dryRun, err := cmd.Flags().GetBool("dry-run") 134 | if err != nil { 135 | return err 136 | } 137 | 138 | partitions, err := core.NewDiskManager().GetPartitions("") 139 | if err != nil { 140 | return err 141 | } 142 | 143 | var varLuksPart core.Partition 144 | foundPart := false 145 | 146 | for _, partition := range partitions { 147 | devName := "/dev/" 148 | if partition.IsDevMapper() { 149 | devName += "mapper/" 150 | } 151 | devName += partition.Device 152 | 153 | if devName == varDisk { 154 | varLuksPart = partition 155 | foundPart = true 156 | break 157 | } 158 | } 159 | if !foundPart { 160 | return &VarInvalidError{varDisk} 161 | } 162 | 163 | uuid := varLuksPart.Uuid 164 | cmdr.FgDefault.Println("unlocking", varDisk) 165 | 166 | if dryRun { 167 | cmdr.Info.Println("Dry run complete.") 168 | } else { 169 | cryptsetupCmd := exec.Command("/usr/sbin/cryptsetup", "luksOpen", varDisk, "luks-"+uuid) 170 | cryptsetupCmd.Stdin = os.Stdin 171 | cryptsetupCmd.Stderr = os.Stderr 172 | cryptsetupCmd.Stdout = os.Stdout 173 | err := cryptsetupCmd.Run() 174 | if err != nil { 175 | return err 176 | } 177 | cmdr.Info.Println("The system mounts have been performed successfully.") 178 | } 179 | 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /core/chroot.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "os" 18 | "os/exec" 19 | "path/filepath" 20 | "strings" 21 | "syscall" 22 | ) 23 | 24 | // Chroot represents a chroot instance, which can be used to run commands 25 | // inside a chroot environment 26 | type Chroot struct { 27 | root string 28 | rootUuid string 29 | rootDevice string 30 | etcMounted bool 31 | } 32 | 33 | // ReservedMounts is a list of mount points from host which should be 34 | // mounted inside the chroot environment to ensure it works properly in 35 | // some cases, such as grub-mkconfig 36 | var ReservedMounts = []string{ 37 | "/dev", 38 | "/dev/pts", 39 | "/proc", 40 | "/run", 41 | "/sys", 42 | } 43 | 44 | // NewChroot creates a new chroot environment from the given root path and 45 | // returns its Chroot instance or an error if something went wrong 46 | func NewChroot(root string, rootUuid string, rootDevice string, mountUserEtc bool, userEtcPath string) (*Chroot, error) { 47 | PrintVerboseInfo("NewChroot", "running...") 48 | 49 | root = strings.ReplaceAll(root, "//", "/") 50 | 51 | if _, err := os.Stat(root); os.IsNotExist(err) { 52 | PrintVerboseErr("NewChroot", 0, err) 53 | return nil, err 54 | } 55 | 56 | chroot := &Chroot{ 57 | root: root, 58 | rootUuid: rootUuid, 59 | rootDevice: rootDevice, 60 | etcMounted: mountUserEtc, 61 | } 62 | 63 | // workaround for grub-mkconfig, not able to find the device 64 | // inside a chroot environment 65 | err := chroot.Execute("mount --bind / /") 66 | if err != nil { 67 | PrintVerboseErr("NewChroot", 1, err) 68 | return nil, err 69 | } 70 | 71 | for _, mount := range ReservedMounts { 72 | PrintVerboseInfo("NewChroot", "mounting", mount) 73 | err := syscall.Mount(mount, filepath.Join(root, mount), "", syscall.MS_BIND, "") 74 | if err != nil { 75 | PrintVerboseErr("NewChroot", 2, err) 76 | return nil, err 77 | } 78 | } 79 | 80 | if mountUserEtc { 81 | err = syscall.Mount("overlay", filepath.Join(root, "etc"), "overlay", syscall.MS_RDONLY, "lowerdir="+userEtcPath+":"+filepath.Join(root, "/etc")) 82 | if err != nil { 83 | PrintVerboseErr("NewChroot", 3, "failed to mount user etc:", err) 84 | return nil, err 85 | } 86 | } 87 | 88 | PrintVerboseInfo("NewChroot", "successfully created.") 89 | return chroot, nil 90 | } 91 | 92 | // Close unmounts all the bind mounts and closes the chroot environment 93 | func (c *Chroot) Close() error { 94 | PrintVerboseInfo("Chroot.Close", "running...") 95 | 96 | err := syscall.Unmount(filepath.Join(c.root, "/dev/pts"), 0) 97 | if err != nil { 98 | PrintVerboseErr("Chroot.Close", 0, err) 99 | return err 100 | } 101 | 102 | mountList := ReservedMounts 103 | if c.etcMounted { 104 | mountList = append(mountList, "/etc") 105 | } 106 | mountList = append(mountList, "") 107 | 108 | for _, mount := range mountList { 109 | if mount == "/dev/pts" { 110 | continue 111 | } 112 | 113 | mountDir := filepath.Join(c.root, mount) 114 | PrintVerboseInfo("Chroot.Close", "unmounting", mountDir) 115 | err := syscall.Unmount(mountDir, 0) 116 | if err != nil { 117 | PrintVerboseErr("Chroot.Close", 1, err) 118 | return err 119 | } 120 | } 121 | 122 | PrintVerboseInfo("Chroot.Close", "successfully closed.") 123 | return nil 124 | } 125 | 126 | // Execute runs a command in the chroot environment, the command is 127 | // a string and the arguments are a list of strings. If an error occurs 128 | // it is returned. 129 | func (c *Chroot) Execute(cmd string) error { 130 | PrintVerboseInfo("Chroot.Execute", "running...") 131 | 132 | PrintVerboseInfo("Chroot.Execute", "running command:", cmd) 133 | e := exec.Command("chroot", c.root, "/bin/sh", "-c", cmd) 134 | e.Stdout = os.Stdout 135 | e.Stderr = os.Stderr 136 | e.Stdin = os.Stdin 137 | err := e.Run() 138 | if err != nil { 139 | PrintVerboseErr("Chroot.Execute", 0, err) 140 | return err 141 | } 142 | 143 | PrintVerboseInfo("Chroot.Execute", "successfully ran.") 144 | return nil 145 | } 146 | 147 | // ExecuteCmds runs a list of commands in the chroot environment, 148 | // stops at the first error 149 | func (c *Chroot) ExecuteCmds(cmds []string) error { 150 | PrintVerboseInfo("Chroot.ExecuteCmds", "running...") 151 | 152 | for _, cmd := range cmds { 153 | err := c.Execute(cmd) 154 | if err != nil { 155 | PrintVerboseErr("Chroot.ExecuteCmds", 0, err) 156 | return err 157 | } 158 | } 159 | 160 | PrintVerboseInfo("Chroot.ExecuteCmds", "successfully ran.") 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /core/rsync.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Luca di Maio 8 | Mateus B. Melchiades 9 | Copyright: 2024 10 | Description: 11 | ABRoot is utility which provides full immutability and 12 | atomicity to a Linux system, by transacting between 13 | two root filesystems. Updates are performed using OCI 14 | images, to ensure that the system is always in a 15 | consistent state. 16 | */ 17 | 18 | import ( 19 | "bufio" 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/vanilla-os/orchid/cmdr" 27 | ) 28 | 29 | // rsyncCmd executes the rsync command with the requested options. 30 | // If silent is true, rsync progress will not appear in stdout. 31 | func rsyncCmd(src, dst string, opts []string, silent bool) error { 32 | args := []string{"-avxHAX"} 33 | args = append(args, opts...) 34 | args = append(args, src) 35 | args = append(args, dst) 36 | 37 | cmd := exec.Command("rsync", args...) 38 | stdout, _ := cmd.StdoutPipe() 39 | 40 | var totalFiles int 41 | 42 | if !silent { 43 | countCmdOut, _ := exec.Command( 44 | "/bin/sh", 45 | "-c", 46 | fmt.Sprintf("echo -n $(($(rsync --dry-run %s | wc -l) - 4))", strings.Join(args, " ")), 47 | ).Output() 48 | totalFiles, _ = strconv.Atoi(string(countCmdOut)) 49 | } 50 | 51 | reader := bufio.NewReader(stdout) 52 | 53 | err := cmd.Start() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if !silent { 59 | verbose := IsVerbose() 60 | 61 | p, _ := cmdr.ProgressBar.WithTotal(totalFiles).WithTitle("Sync in progress").WithMaxWidth(120).Start() 62 | maxLineLen := cmdr.TerminalWidth() / 4 63 | 64 | for i := 0; i < p.Total; i++ { 65 | line, _ := reader.ReadString('\n') 66 | line = strings.TrimSpace(line) 67 | 68 | if verbose { 69 | cmdr.Info.Println(line + " synced") 70 | } 71 | 72 | if len(line) > maxLineLen { 73 | startingLen := len(line) - maxLineLen + 1 74 | line = "<" + line[startingLen:] 75 | } else { 76 | padding := maxLineLen - len(line) 77 | line += strings.Repeat(" ", padding) 78 | } 79 | 80 | p.UpdateTitle("Syncing " + line) 81 | p.Increment() 82 | } 83 | } else { 84 | stdout.Close() 85 | } 86 | 87 | err = cmd.Wait() 88 | if err != nil { 89 | // exit status 24 is a warning, not an error, we don't care about it 90 | // since rsync is going to be removed in the OCI version 91 | if !strings.Contains(err.Error(), "exit status 24") { 92 | return err 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // rsyncDryRun executes the rsync command with the --dry-run option. 100 | func rsyncDryRun(src, dst string, excluded []string) error { 101 | opts := []string{"--dry-run"} 102 | 103 | if len(excluded) > 0 { 104 | for _, exclude := range excluded { 105 | opts = append(opts, "--exclude="+exclude) 106 | } 107 | } 108 | 109 | return rsyncCmd(src, dst, opts, false) 110 | } 111 | 112 | // AtomicRsync executes the rsync command in an atomic-like manner. 113 | // It does so by dry-running the rsync, and if it succeeds, it runs 114 | // the rsync again performing changes. 115 | // If the keepUnwanted option 116 | // is set to true, it will omit the --delete option, so that the already 117 | // existing and unwanted files will not be deleted. 118 | // To ensure the changes are applied atomically, we rsync on a _new directory first, 119 | // and use atomicSwap to replace the _new with the dst directory. 120 | func AtomicRsync(src, dst string, transitionalPath string, finalPath string, excluded []string, keepUnwanted bool) error { 121 | PrintVerboseInfo("AtomicRsync", "Running...") 122 | if _, err := os.Stat(transitionalPath); os.IsNotExist(err) { 123 | err = os.Mkdir(transitionalPath, 0755) 124 | if err != nil { 125 | PrintVerboseErr("AtomicRsync", 0, err) 126 | return err 127 | } 128 | } 129 | 130 | PrintVerboseInfo("AtomicRsync", "Starting dry run process...") 131 | err := rsyncDryRun(src, transitionalPath, excluded) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | opts := []string{"--link-dest", dst, "--exclude", finalPath, "--exclude", transitionalPath} 137 | 138 | if len(excluded) > 0 { 139 | for _, exclude := range excluded { 140 | opts = append(opts, "--exclude", exclude) 141 | } 142 | } 143 | 144 | if !keepUnwanted { 145 | opts = append(opts, "--delete") 146 | } 147 | 148 | PrintVerboseInfo("AtomicRsync", "Starting rsync process...") 149 | 150 | err = rsyncCmd(src, transitionalPath, opts, true) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | PrintVerboseInfo("AtomicRsync", "Starting atomic swap process...") 156 | 157 | err = AtomicSwap(transitionalPath, finalPath) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | PrintVerboseInfo("AtomicRsync", "Removing transitional path...") 163 | 164 | return os.RemoveAll(transitionalPath) 165 | } 166 | -------------------------------------------------------------------------------- /locales/zh_Hans.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot 藉由在两个根分区(A<->B)间执行事务以提供完全的不变性及原子性" 4 | short: "ABRoot 藉由在两个根分区(A<->B)间执行事务以提供完全的不变性及原子性" 5 | verboseFlag: "显示更详细的输出" 6 | 7 | msg: 8 | additionalCommands: 附加命令 9 | version: 显示 abroot 的版本。 10 | moreInfo: 使用 %s 获取有关命令的更多信息 11 | aliases: 别名 12 | flags: 参数 13 | additionalHelpTopics: 附加帮助主题 14 | availableCommands: 可用命令 15 | examples: 示例 16 | globalFlags: 全局参数 17 | help: 显示 abroot 的帮助。 18 | usage: 用法 19 | kargs: 20 | use: "kargs" 21 | long: "管理内核参数。" 22 | short: "管理内核参数" 23 | rootRequired: "必须是 root 用户才能运行此命令。" 24 | applyFailed: "应用命令失败:%s\n" 25 | notChanged: 未对内核参数做出任何更改。 26 | unknownCommand: 未知的命令 '%s'。运行 'abroot kargs --help' 获取用法示例。 27 | rollback: 28 | use: "rollback" 29 | long: "执行系统回滚,丢弃对当前根分区的修改。" 30 | short: "将系统恢复到先前的状态" 31 | rootRequired: "必须是 root 用户才能运行此命令。" 32 | rollbackFailed: "回滚失败:%s\n" 33 | rollbackSuccess: 回滚成功完成。 34 | rollbackUnnecessary: 无需回滚,根分区已经是当前根分区。 35 | canRollback: 可以回滚到上一个根分区。 36 | cannotRollback: 无法回滚到上一个根分区。 37 | checkOnlyFlag: 检查是否可回滚到上一个根分区 38 | pkg: 39 | applyFailed: "应用命令失败:%s\n" 40 | removedMsg: "已移除软件包 %s。\n" 41 | listMsg: "添加的软件包:\n%s\n移除的软件包:\n%s\n" 42 | use: pkg 43 | long: 安装及管理软件包。 44 | noPackageNameProvided: 此操作至少需要一个软件包名称。 45 | short: 管理软件包 46 | rootRequired: 必须是 root 用户才能运行此命令。 47 | addedMsg: "已添加软件包 %s。\n" 48 | dryRunFlag: 为操作执行试运行(dry run) 49 | agreementDeclined: 您已拒绝该协议。在您同意之前,该功能将保持禁用状态。 50 | agreementMsg: "要使用 ABRoot 的 abroot pkg 命令,您需要明确同意用户协议。此命令协助用户安装软件包,但会为系统引入不确定因素,从而影响系统可靠性。同意协议即表明您知晓且接受这些潜在影响,且您确认您了解该命令对系统行为的潜在影响。[y/N]: " 51 | agreementSignFailed: "无法签署协议:%s\n" 52 | forceEnableUserAgreementFlag: 强制启用用户协议,用于嵌入式系统 53 | failedGettingPkgManagerInstance: "无法获取软件包管理器实例:%s\n" 54 | noChanges: 没有需要应用的更改。 55 | unknownCommand: 未知的命令 '%s'。运行 'abroot pkg --help' 获取用法示例。 56 | forceApply: force apply changes even if they've already been applied 57 | applySuccess: Successfully applied packages. 58 | status: 59 | use: status 60 | unstagedFoundMsg: "\n\t\t有 %d 个软件包尚未应用更改。请运行 'abroot pkg apply' 应用更改。" 61 | dumpMsg: "已将 ABRoot 状态转储至 %s\n" 62 | long: 显示当前 ABRoot 状态。 63 | short: 显示状态 64 | jsonFlag: 以 JSON 格式显示输出 65 | dumpFlag: 将 ABRoot 状态转储至归档文件 66 | rootRequired: 必须是 root 用户才能运行此命令。 67 | specs: 68 | cpu: 'CPU:%s' 69 | gpu: 'GPU:%s' 70 | title: '设备规格:' 71 | memory: '内存:%s' 72 | loadedConfig: '已加载的配置:' 73 | packages: 74 | removed: '已移除:%s' 75 | unstaged: '尚未应用更改:%s%s' 76 | title: '软件包:' 77 | added: '已添加:%s' 78 | partitions: 79 | future: '将来分区:%s%s' 80 | present: '当前分区:%s%s' 81 | title: 'ABRoot 分区:' 82 | kargs: '内核参数:' 83 | abimage: 84 | timestamp: '时间戳:%s' 85 | title: 'ABImage:' 86 | digest: '散列值:%s' 87 | image: '映像:%s' 88 | agreementStatus: '软件包协议状态:' 89 | upgrade: 90 | use: upgrade 91 | long: 检查是否有新的系统映像并应用。 92 | short: 升级系统 93 | forceFlag: 强制更新,即便系统已经是最新的 94 | rootRequired: 必须是 root 用户才能运行此命令。 95 | noUpdateAvailable: 您的系统暂时没有可用的更新。 96 | checkOnlyFlag: 检查更新但不立即应用 97 | removed: 已移除 98 | downgraded: 已降级 99 | packageUpdateAvailable: 有 %d 个软件包更新。 100 | systemUpdateAvailable: 您的系统有更新可用。 101 | upgraded: 已升级 102 | added: 已添加 103 | checkingPackageUpdate: 正在检查软件包更新… 104 | checkingSystemUpdate: 正在检查系统更新… 105 | dryRunFlag: 为操作执行试运行(dry run) 106 | dryRunSuccess: 试运行成功完成。 107 | success: 升级成功完成。 108 | cancel: Temporarily block or cancel any abroot operation. 109 | deleteOld: Delete old image to free up space, will make future root 110 | temporarily unusable. 111 | unblock: Remove temporary user block. 112 | updateInitramfs: 113 | short: 更新 initramfs 114 | updateFailed: "无法更新将来根分区的 initramfs。\n" 115 | rootRequired: 必须是 root 用户才能运行此命令。 116 | updateSuccess: 已更新将来根分区的 initramfs。 117 | long: 更新将来根分区的初始化内存盘(initramfs)。 118 | use: update-initramfs 119 | dryRunFlag: 为操作执行试运行(dry run) 120 | cnf: 121 | unchanged: 未对配置做出任何更改。 122 | editorFailed: "无法打开编辑器:%s\n" 123 | long: 打开编辑器来编辑 ABRoot 的配置。 124 | short: 编辑 ABRoot 配置 125 | use: cnf 126 | changed: 已更改配置。 127 | failed: "操作配置时发生错误:%s\n" 128 | rootRequired: 必须是 root 用户才能运行此命令。 129 | rebase: 130 | short: Rebase to new image 131 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 132 | cannot be used together." 133 | rootRequired: You must be root to run this command. 134 | rebaseOnly: Do not update the system after chaning the configured image 135 | pkgRemoveSuccess: All added packages have been removed successfully 136 | successUpdate: Rebase completed successfully. The system will update in a 137 | moment. 138 | success: Rebase completed successfully. Your next update will use the new 139 | image. 140 | dryRunSuccess: Dry run completed successfully 141 | dryRunFlag: perform a dry run of the operation 142 | long: Change the OCI image the system is using 143 | keepPackages: Keep layered packages while rebasing the system 144 | removePackagesShort: Remove all layered packages while rebasing to prevent 145 | issues caused by a change in repositories. 146 | removePackagesLong: "The new image may use a different software repository, making 147 | some installed packages inaccessible. If this is the case, the next upgrade will 148 | fail.\nWould you like to remove your added packages to resolve this issue?" 149 | -------------------------------------------------------------------------------- /core/utils.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "fmt" 18 | "io" 19 | "io/fs" 20 | "os" 21 | "os/exec" 22 | "path/filepath" 23 | "slices" 24 | "syscall" 25 | ) 26 | 27 | var abrootDir = "/etc/abroot" 28 | 29 | func init() { 30 | if !RootCheck(false) { 31 | return 32 | } 33 | 34 | if _, err := os.Stat(abrootDir); os.IsNotExist(err) { 35 | err := os.Mkdir(abrootDir, 0755) 36 | if err != nil { 37 | fmt.Println(err) 38 | os.Exit(1) 39 | } 40 | } 41 | } 42 | 43 | func RootCheck(display bool) bool { 44 | if os.Geteuid() != 0 { 45 | if display { 46 | fmt.Println("You must be root to run this command") 47 | } 48 | 49 | return false 50 | } 51 | 52 | return true 53 | } 54 | 55 | // fileExists checks if a file exists 56 | func fileExists(path string) bool { 57 | if _, err := os.Stat(path); err == nil { 58 | PrintVerboseInfo("fileExists", "File exists:", path) 59 | return true 60 | } 61 | 62 | PrintVerboseInfo("fileExists", "File does not exist:", path) 63 | return false 64 | } 65 | 66 | // CopyFile copies a file from source to dest 67 | func CopyFile(source, dest string) error { 68 | PrintVerboseInfo("CopyFile", "Running...") 69 | 70 | PrintVerboseInfo("CopyFile", "Opening source file") 71 | srcFile, err := os.Open(source) 72 | if err != nil { 73 | PrintVerboseErr("CopyFile", 0, err) 74 | return err 75 | } 76 | defer srcFile.Close() 77 | 78 | PrintVerboseInfo("CopyFile", "Opening destination file") 79 | destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0755) 80 | if err != nil { 81 | PrintVerboseErr("CopyFile", 1, err) 82 | return err 83 | } 84 | defer destFile.Close() 85 | 86 | PrintVerboseInfo("CopyFile", "Performing copy operation") 87 | if _, err := io.Copy(destFile, srcFile); err != nil { 88 | PrintVerboseErr("CopyFile", 2, err) 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // MoveFile copies a file from source to dest, then remove the source file 96 | func MoveFile(source, dest string) error { 97 | err := CopyFile(source, dest) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | PrintVerboseInfo("MoveFile", "Removing source file") 103 | err = os.Remove(source) 104 | if err != nil { 105 | PrintVerboseErr("MoveFile", 0, err) 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // ClearDirectory deletes all contents of a directory without removing the directory itself. 113 | // Skips files/directories specified in the skip list. 114 | func ClearDirectory(path string, skipList []string) error { 115 | files, err := os.ReadDir(path) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | for _, file := range files { 121 | if slices.Contains(skipList, file.Name()) { 122 | continue 123 | } 124 | err = os.RemoveAll(filepath.Join(path, file.Name())) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // isDeviceLUKSEncrypted checks whether a device specified by devicePath is a LUKS-encrypted device 134 | func isDeviceLUKSEncrypted(devicePath string) (bool, error) { 135 | PrintVerboseInfo("isDeviceLUKSEncrypted", "Verifying if", devicePath, "is encrypted") 136 | 137 | isLuksCmd := "cryptsetup isLuks %s" 138 | 139 | cmd := exec.Command("sh", "-c", fmt.Sprintf(isLuksCmd, devicePath)) 140 | err := cmd.Run() 141 | if err != nil { 142 | // We expect the command to return exit status 1 if partition isn't 143 | // LUKS-encrypted 144 | if exitError, ok := err.(*exec.ExitError); ok { 145 | if exitError.ExitCode() == 1 { 146 | return false, nil 147 | } 148 | } 149 | err = fmt.Errorf("failed to check if %s is LUKS-encrypted: %s", devicePath, err) 150 | PrintVerboseErr("isDeviceLUKSEncrypted", 0, err) 151 | return false, err 152 | } 153 | 154 | return true, nil 155 | } 156 | 157 | // getDirSize calculates the total size of a directory recursively. 158 | func getDirSize(path string) (int64, error) { 159 | ds, err := os.Stat(path) 160 | if err != nil { 161 | return 0, err 162 | } 163 | if !ds.IsDir() { 164 | return 0, fmt.Errorf("%s is not a directory", path) 165 | } 166 | 167 | inodes := map[uint64]bool{} 168 | var totalSize int64 = 0 169 | 170 | dfs := os.DirFS(path) 171 | err = fs.WalkDir(dfs, ".", func(path string, d fs.DirEntry, err error) error { 172 | if err != nil { 173 | return err 174 | } 175 | 176 | if !d.IsDir() { 177 | fileinfo, err := d.Info() 178 | if err != nil { 179 | return err 180 | } 181 | 182 | fileinfoSys := fileinfo.Sys().(*syscall.Stat_t) 183 | if fileinfoSys.Nlink > 1 { 184 | if _, ok := inodes[fileinfoSys.Ino]; !ok { 185 | totalSize += fileinfo.Size() 186 | inodes[fileinfoSys.Ino] = true 187 | } 188 | } else { 189 | totalSize += fileinfo.Size() 190 | inodes[fileinfoSys.Ino] = true 191 | } 192 | } 193 | 194 | return nil 195 | }) 196 | if err != nil { 197 | return 0, err 198 | } 199 | 200 | return totalSize, nil 201 | } 202 | -------------------------------------------------------------------------------- /core/grub.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | 23 | "github.com/vanilla-os/abroot/settings" 24 | ) 25 | 26 | // Grub represents a grub instance, it exposes methods to generate a new grub 27 | // config compatible with ABRoot, and to check if the system is booted into 28 | // the present root or the future root 29 | type Grub struct { 30 | PresentRoot string 31 | FutureRoot string 32 | } 33 | 34 | // generateABGrubConf generates a new grub config with the given details 35 | func generateABGrubConf(kernelVersion string, rootPath string, rootUuid string, rootLabel string, generatedGrubConfigPath string) error { 36 | PrintVerboseInfo("generateABGrubConf", "generating grub config for ABRoot") 37 | 38 | kargs, err := KargsRead() 39 | if err != nil { 40 | PrintVerboseErr("generateABGrubConf", 0, err) 41 | return err 42 | } 43 | 44 | var grubPath, bootPrefix, systemRoot string 45 | if settings.Cnf.ThinProvisioning { 46 | grubPath = filepath.Join(rootPath, "boot", "init", rootLabel) 47 | bootPrefix = "/" + rootLabel 48 | 49 | diskM := NewDiskManager() 50 | sysRootPart, err := diskM.GetPartitionByLabel(rootLabel) 51 | if err != nil { 52 | PrintVerboseErr("generateABGrubConf", 3, err) 53 | return err 54 | } 55 | systemRoot = "/dev/mapper/" + sysRootPart.Device 56 | } else { 57 | grubPath = filepath.Join(rootPath, "boot", "grub") 58 | bootPrefix = "/.system/boot" 59 | systemRoot = "UUID=" + rootUuid 60 | } 61 | 62 | confPath := filepath.Join(grubPath, "abroot.cfg") 63 | template := ` search --no-floppy --fs-uuid --set=root %s 64 | linux %s/vmlinuz-%s root=%s %s 65 | initrd %s/initrd.img-%s 66 | ` 67 | 68 | err = os.MkdirAll(grubPath, 0755) 69 | if err != nil { 70 | PrintVerboseErr("generateABGrubConf", 2, err) 71 | return err 72 | } 73 | 74 | abrootBootConfig := fmt.Sprintf(template, rootUuid, bootPrefix, kernelVersion, systemRoot, kargs, bootPrefix, kernelVersion) 75 | 76 | generatedGrubConfigContents, err := os.ReadFile(filepath.Join(rootPath, generatedGrubConfigPath)) 77 | if err != nil { 78 | PrintVerboseErr("generateABGrubConf", 3, "could not read grub config", err) 79 | return err 80 | } 81 | 82 | generatedGrubConfig := string(generatedGrubConfigContents) 83 | 84 | replacementString := "REPLACED_BY_ABROOT" 85 | if !strings.Contains(generatedGrubConfig, replacementString) { 86 | err := errors.New("could not find replacement string \"" + replacementString + "\", check /etc/grub.d configuration") 87 | PrintVerboseErr("generateABGrubConf", 3.1, err) 88 | return err 89 | } 90 | grubConfigWithBootEntry := strings.Replace(generatedGrubConfig, "REPLACED_BY_ABROOT", abrootBootConfig, 1) 91 | 92 | err = os.WriteFile(confPath, []byte(grubConfigWithBootEntry), 0644) 93 | if err != nil { 94 | PrintVerboseErr("generateABGrubConf", 4, "could not read grub config", err) 95 | return err 96 | } 97 | 98 | PrintVerboseInfo("generateABGrubConf", "done") 99 | return nil 100 | } 101 | 102 | // NewGrub creates a new Grub instance 103 | func NewGrub(bootPart Partition) (*Grub, error) { 104 | PrintVerboseInfo("NewGrub", "running...") 105 | 106 | grubPath := filepath.Join(bootPart.MountPoint, "grub") 107 | confPath := filepath.Join(grubPath, "grub.cfg") 108 | 109 | cfg, err := os.ReadFile(confPath) 110 | if err != nil { 111 | PrintVerboseErr("NewGrub", 0, err) 112 | return nil, err 113 | } 114 | 115 | var presentRoot, futureRoot string 116 | 117 | for _, entry := range strings.Split(string(cfg), "\n") { 118 | if strings.Contains(entry, "abroot-a") { 119 | if strings.Contains(entry, "Current State") { 120 | presentRoot = "a" 121 | } else if strings.Contains(entry, "Previous State") { 122 | futureRoot = "a" 123 | } 124 | } else if strings.Contains(entry, "abroot-b") { 125 | if strings.Contains(entry, "Current State") { 126 | presentRoot = "b" 127 | } else if strings.Contains(entry, "Previous State") { 128 | futureRoot = "b" 129 | } 130 | } 131 | } 132 | 133 | if presentRoot == "" || futureRoot == "" { 134 | err := errors.New("could not find root partitions") 135 | PrintVerboseErr("NewGrub", 1, err) 136 | return nil, err 137 | } 138 | 139 | PrintVerboseInfo("NewGrub", "done") 140 | return &Grub{ 141 | PresentRoot: presentRoot, 142 | FutureRoot: futureRoot, 143 | }, nil 144 | } 145 | 146 | func (g *Grub) IsBootedIntoPresentRoot() (bool, error) { 147 | PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "running...") 148 | 149 | a := NewABRootManager() 150 | future, err := a.GetFuture() 151 | if err != nil { 152 | return false, err 153 | } 154 | 155 | if g.FutureRoot == "a" { 156 | PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done") 157 | return future.Label == settings.Cnf.PartLabelA, nil 158 | } else { 159 | PrintVerboseInfo("Grub.IsBootedIntoPresentRoot", "done") 160 | return future.Label == settings.Cnf.PartLabelB, nil 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /locales/zh_Hant.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot 透過在兩個 Root 分割區(A<->B)之間執行系統變更以提供完全的不可變性及原子性" 4 | short: "ABRoot 透過在兩個 Root 分割區(A<->B)之間執行系統變更以提供完全的不可變性及原子性" 5 | verboseFlag: "顯示更詳細的輸出" 6 | 7 | msg: 8 | version: 顯示 abroot 的版本。 9 | help: 顯示 abroot 的說明。 10 | usage: 用法 11 | additionalCommands: 附加命令 12 | moreInfo: 使用 %s 取得關於命令的詳細資訊 13 | aliases: 別名 14 | flags: 參數 15 | additionalHelpTopics: 附加說明主題 16 | availableCommands: 可使用命令 17 | globalFlags: 全域參數 18 | examples: 範例 19 | kargs: 20 | use: "kargs" 21 | long: "管理作業系統核心參數。" 22 | short: "管理核心參數" 23 | rootRequired: "必須是 root 使用者才能執行此命令。" 24 | notChanged: 沒有對作業系統核心參數作出任何變更。 25 | applyFailed: "套用命令失敗:%s\n" 26 | unknownCommand: 未知的命令 '%s'。執行 'abroot kargs --help' 查看用法示例。 27 | rollback: 28 | use: "rollback" 29 | long: "執行系統復原,捨棄對目前 Root 分割區的變更。" 30 | short: "將系統復原到先前的狀態" 31 | rootRequired: "您必須是 root 使用者才能執行此命令。" 32 | rollbackFailed: "復原失敗:%s\n" 33 | rollbackSuccess: 復原成功完成。 34 | rollbackUnnecessary: 無需復原,已經是目前 Root 分割區。 35 | canRollback: 可以復原到上一個 Root 分割區。 36 | cannotRollback: 無法復原到上一個 Root 分割區。 37 | checkOnlyFlag: 檢查是否可以復原到上一個 Root 分割區 38 | status: 39 | use: status 40 | unstagedFoundMsg: "\n\t\t有 %d 個軟體包尚未套用變更。請執行 'abroot pkg apply' 套用變更。" 41 | dumpMsg: "已將 ABRoot 狀態傾印至 %s\n" 42 | long: 顯示目前 ABRoot 狀態。 43 | short: 顯示狀態 44 | jsonFlag: 以 JSON 格式顯示輸出 45 | dumpFlag: 將 ABRoot 狀態傾印至歸檔 46 | rootRequired: 必須是 root 使用者才能執行此命令。 47 | packages: 48 | removed: '已移除:%s' 49 | unstaged: '未套用變更:%s%s' 50 | title: '軟體包:' 51 | added: '已新增:%s' 52 | kargs: '作業系統核心參數:' 53 | abimage: 54 | timestamp: '時戳:%s' 55 | title: 'ABImage:' 56 | digest: '雜湊碼:%s' 57 | image: '映像:%s' 58 | specs: 59 | cpu: 'CPU:%s' 60 | gpu: 'GPU:%s' 61 | title: '裝置規格:' 62 | memory: '記憶體:%s' 63 | loadedConfig: '已載入的組態:' 64 | partitions: 65 | future: '將來分割區:%s%s' 66 | present: '目前分割區:%s%s' 67 | title: 'ABRoot 分割區:' 68 | agreementStatus: '軟體包合約狀態:' 69 | pkg: 70 | use: pkg 71 | listMsg: "新增的軟體包:\n%s\n移除的軟體包:\n%s\n" 72 | long: 安裝及管理軟體包。 73 | short: 管理軟體包 74 | rootRequired: 必須是 root 使用者才能執行此命令。 75 | noPackageNameProvided: 此操作至少需要一個軟體包名稱。 76 | addedMsg: "已新增軟體包 %s。\n" 77 | applyFailed: "套用命令失敗:%s\n" 78 | removedMsg: "已移除軟體包 %s。\n" 79 | dryRunFlag: 為操作執行事先嘗試(dry run) 80 | forceEnableUserAgreementFlag: 強制啟用使用者合約,用於嵌入式系統 81 | agreementDeclined: 您已拒絕此合約。在您同意之前,該功能將保持停用狀態。 82 | agreementMsg: "要使用 ABRoot 的 abroot pkg 命令,您需要明確同意使用者合約。此命令協助使用者安裝軟體包,但會為系統引入不確定因素,從而影響系統可靠性。同意合約即表明您知曉且接受這些潛在影響,並且您確認您瞭解此命令對系統行為的潛在影響。[y/N]: " 83 | agreementSignFailed: "無法簽署合約:%s\n" 84 | failedGettingPkgManagerInstance: "無法取得軟體包管理器實例:%s\n" 85 | noChanges: 沒有變更需要套用。 86 | unknownCommand: 未知的命令 '%s'。執行 'abroot pkg --help' 查看用法示例。 87 | forceApply: force apply changes even if they've already been applied 88 | applySuccess: Successfully applied packages. 89 | upgrade: 90 | use: upgrade 91 | long: 檢查是否有新的系統映像並套用。 92 | short: 升級系統 93 | checkOnlyFlag: 檢查更新但不立即套用 94 | forceFlag: 強制更新,即便系統已經是最新的 95 | rootRequired: 必須是 root 使用者才能執行此命令。 96 | noUpdateAvailable: 您的系統暫時沒有可用更新。 97 | removed: 已移除 98 | downgraded: 已降級 99 | packageUpdateAvailable: 有 %d 個軟體包更新。 100 | systemUpdateAvailable: 您的系統有更新可用。 101 | upgraded: 已升級 102 | added: 已新增 103 | checkingPackageUpdate: 正在檢查軟體包更新… 104 | checkingSystemUpdate: 正在檢查系統更新… 105 | dryRunFlag: 為操作執行事先嘗試(dry run) 106 | dryRunSuccess: 事先嘗試(dry run)成功完成。 107 | success: 升級成功完成。 108 | cancel: Temporarily block or cancel any abroot operation. 109 | deleteOld: Delete old image to free up space, will make future root 110 | temporarily unusable. 111 | unblock: Remove temporary user block. 112 | updateInitramfs: 113 | short: 更新 initramfs 114 | updateFailed: "無法更新將來 Root 分割區的 initramfs。\n" 115 | rootRequired: 必須是 root 使用者才能執行此命令。 116 | updateSuccess: 已更新將來 Root 分割區的 initramfs。 117 | long: 更新將來 Root 分割區的 initramfs。 118 | use: update-initramfs 119 | dryRunFlag: 為操作執行事先嘗試(dry run) 120 | cnf: 121 | unchanged: 沒有對組態作出任何變更。 122 | editorFailed: "無法開啟編輯器:%s\n" 123 | long: 開啟編輯器以編輯ABRoot 的組態。 124 | short: 編輯 ABRoot 組態 125 | use: cnf 126 | changed: 已變更組態。 127 | failed: "操作組態時發生錯誤:%s\n" 128 | rootRequired: 必須是 root 使用者才能執行此命令。 129 | rebase: 130 | short: Rebase to new image 131 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 132 | cannot be used together." 133 | rootRequired: You must be root to run this command. 134 | rebaseOnly: Do not update the system after chaning the configured image 135 | pkgRemoveSuccess: All added packages have been removed successfully 136 | successUpdate: Rebase completed successfully. The system will update in a 137 | moment. 138 | success: Rebase completed successfully. Your next update will use the new 139 | image. 140 | dryRunSuccess: Dry run completed successfully 141 | dryRunFlag: perform a dry run of the operation 142 | long: Change the OCI image the system is using 143 | keepPackages: Keep layered packages while rebasing the system 144 | removePackagesShort: Remove all layered packages while rebasing to prevent 145 | issues caused by a change in repositories. 146 | removePackagesLong: "The new image may use a different software repository, making 147 | some installed packages inaccessible. If this is the case, the next upgrade will 148 | fail.\nWould you like to remove your added packages to resolve this issue?" 149 | -------------------------------------------------------------------------------- /settings/config.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | 22 | "github.com/spf13/viper" 23 | ) 24 | 25 | type Config struct { 26 | // Common 27 | MaxParallelDownloads uint `json:"maxParallelDownloads"` 28 | 29 | // Registry 30 | Registry string `json:"registry"` 31 | RegistryAPIVersion string `json:"registryAPIVersion"` 32 | RegistryService string `json:"registryService"` 33 | Name string `json:"name"` 34 | Tag string `json:"tag"` 35 | 36 | // Package manager 37 | IPkgMngPre string `json:"iPkgMngPre"` 38 | IPkgMngPost string `json:"iPkgMngPost"` 39 | IPkgMngAdd string `json:"iPkgMngAdd"` 40 | IPkgMngRm string `json:"iPkgMngRm"` 41 | IPkgMngApi string `json:"iPkgMngApi"` 42 | IPkgMngStatus int `json:"iPkgMngStatus"` 43 | 44 | // Boot configuration commands 45 | UpdateInitramfsCmd string `json:"updateInitramfsCmd"` 46 | UpdateGrubCmd string `json:"updateGrubCmd"` 47 | 48 | // Package diff API (Differ) 49 | DifferURL string `json:"differURL"` 50 | 51 | // Partitions 52 | PartLabelVar string `json:"partLabelVar"` 53 | PartLabelA string `json:"partLabelA"` 54 | PartLabelB string `json:"partLabelB"` 55 | PartLabelBoot string `json:"partLabelBoot"` 56 | PartLabelEfi string `json:"partLabelEfivar"` 57 | PartCryptVar string `json:"PartCryptVar"` 58 | 59 | // Structure 60 | ThinProvisioning bool `json:"thinProvisioning"` 61 | ThinInitVolume string `json:"thinInitVolume"` 62 | } 63 | 64 | var Cnf *Config 65 | 66 | const CnfFileName = "abroot.json" 67 | 68 | var ( 69 | CnfPathSystem = filepath.Join("/usr/share/abroot/", CnfFileName) 70 | CnfPathAdmin = filepath.Join("/etc/abroot/", CnfFileName) 71 | CnfPathDev1 = filepath.Join("../config/", CnfFileName) 72 | CnfPathDev2 = filepath.Join("./config/", CnfFileName) 73 | ) 74 | 75 | func init() { 76 | 77 | foundConfig := false 78 | 79 | // system path 80 | viper.SetConfigFile(CnfPathSystem) 81 | err := viper.ReadInConfig() 82 | if err != nil { 83 | fmt.Fprintln(os.Stderr, "could not read config file in /usr", err) 84 | } else { 85 | foundConfig = true 86 | } 87 | 88 | // admin path 89 | viper.SetConfigFile(CnfPathAdmin) 90 | err = viper.MergeInConfig() 91 | if err != nil { 92 | fmt.Fprintln(os.Stderr, "could not read config file in /etc", err) 93 | } else { 94 | foundConfig = true 95 | } 96 | 97 | // dev paths 98 | viper.SetConfigFile(CnfPathDev1) 99 | err = viper.MergeInConfig() 100 | if err == nil { 101 | foundConfig = true 102 | } 103 | viper.SetConfigFile(CnfPathDev2) 104 | err = viper.MergeInConfig() 105 | if err == nil { 106 | foundConfig = true 107 | } 108 | 109 | if !foundConfig { 110 | panic("could not find an intact config") 111 | } 112 | 113 | // VanillaOS specific defaults for backwards compatibility 114 | viper.SetDefault("updateInitramfsCmd", "lpkg --unlock && /usr/sbin/update-initramfs -u && lpkg --lock") 115 | viper.SetDefault("updateGrubCmd", "/usr/sbin/grub-mkconfig -o '%s'") 116 | 117 | Cnf = &Config{ 118 | // Common 119 | MaxParallelDownloads: viper.GetUint("maxParallelDownloads"), 120 | 121 | // Registry 122 | Registry: viper.GetString("registry"), 123 | RegistryAPIVersion: viper.GetString("registryAPIVersion"), 124 | RegistryService: viper.GetString("registryService"), 125 | Name: viper.GetString("name"), 126 | Tag: viper.GetString("tag"), 127 | 128 | // Package manager 129 | IPkgMngPre: viper.GetString("iPkgMngPre"), 130 | IPkgMngPost: viper.GetString("iPkgMngPost"), 131 | IPkgMngAdd: viper.GetString("iPkgMngAdd"), 132 | IPkgMngRm: viper.GetString("iPkgMngRm"), 133 | IPkgMngApi: viper.GetString("iPkgMngApi"), 134 | IPkgMngStatus: viper.GetInt("iPkgMngStatus"), 135 | 136 | // Boot configuration commands 137 | UpdateInitramfsCmd: viper.GetString("updateInitramfsCmd"), 138 | UpdateGrubCmd: viper.GetString("updateGrubCmd"), 139 | 140 | // Package diff API (Differ) 141 | DifferURL: viper.GetString("differURL"), 142 | 143 | // Partitions 144 | PartLabelVar: viper.GetString("partLabelVar"), 145 | PartLabelA: viper.GetString("partLabelA"), 146 | PartLabelB: viper.GetString("partLabelB"), 147 | PartLabelBoot: viper.GetString("partLabelBoot"), 148 | PartLabelEfi: viper.GetString("partLabelEfi"), 149 | PartCryptVar: viper.GetString("PartCryptVar"), 150 | 151 | // Structure 152 | ThinProvisioning: viper.GetBool("thinProvisioning"), 153 | ThinInitVolume: viper.GetString("thinInitVolume"), 154 | } 155 | } 156 | 157 | func GetFullImageName() string { 158 | return fmt.Sprintf("%s/%s", Cnf.Registry, Cnf.Name) 159 | } 160 | 161 | func GetFullImageNameWithTag() string { 162 | return fmt.Sprintf("%s:%s", GetFullImageName(), Cnf.Tag) 163 | } 164 | 165 | // WriteConfigToFile writes the current configuration to a file 166 | func WriteConfigToFile(file string) error { 167 | jsonOutput, err := json.MarshalIndent(Cnf, "", " ") 168 | if err != nil { 169 | return err 170 | } 171 | 172 | outputFile, err := os.OpenFile(file, os.O_WRONLY|os.O_TRUNC, 0o644) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | _, err = outputFile.Write(jsonOutput) 178 | 179 | return err 180 | } 181 | -------------------------------------------------------------------------------- /core/logging.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | import ( 16 | "fmt" 17 | "log" 18 | "os" 19 | "path/filepath" 20 | "time" 21 | 22 | "github.com/vanilla-os/orchid/cmdr" 23 | ) 24 | 25 | // logFile is a file handle for the log file 26 | var logFile *os.File 27 | 28 | // printLog is a logger to Stdout for verbose information 29 | var printLog = log.New(os.Stdout, "(Verbose) ", 0) 30 | 31 | // init initializes the log file and sets up logging 32 | func init() { 33 | PrintVerboseInfo("NewLogFile", "running...") 34 | 35 | // Incremental value to append to log file name 36 | incremental := 0 37 | 38 | // Check for existing log files 39 | logFiles, err := filepath.Glob("/var/log/abroot.log.*") 40 | if err != nil { 41 | // If there are no log files, start with incremental 1 42 | incremental = 1 43 | } else { 44 | allIncrementals := []int{} 45 | // Extract incremental values from existing log file names 46 | for _, logFile := range logFiles { 47 | _, err := fmt.Sscanf(logFile, "/var/log/abroot.log.%d", &incremental) 48 | if err != nil { 49 | continue 50 | } 51 | allIncrementals = append(allIncrementals, incremental) 52 | } 53 | // Set incremental to the next available value 54 | if len(allIncrementals) == 0 { 55 | incremental = 1 56 | } else { 57 | incremental = allIncrementals[len(allIncrementals)-1] + 1 58 | } 59 | } 60 | 61 | // Open or create the log file 62 | logFile, err = os.OpenFile( 63 | fmt.Sprintf("/var/log/abroot.log.%d", incremental), 64 | os.O_RDWR|os.O_CREATE|os.O_APPEND, 65 | 0666, 66 | ) 67 | if err != nil { 68 | PrintVerboseErrNoLog("NewLogFile", 0, "failed to open log file", err) 69 | } 70 | } 71 | 72 | // IsVerbose checks if verbose mode is enabled 73 | func IsVerbose() bool { 74 | flag := cmdr.FlagValBool("verbose") 75 | _, arg := os.LookupEnv("ABROOT_VERBOSE") 76 | return flag || arg 77 | } 78 | 79 | // formatMessage formats log messages based on prefix, level, and depth 80 | func formatMessage(prefix, level string, depth float32, args ...interface{}) string { 81 | if prefix == "" && level == "" && depth == -1 { 82 | return fmt.Sprint(args...) 83 | } 84 | 85 | if depth > -1 { 86 | level = fmt.Sprintf("%s(%f)", level, depth) 87 | } 88 | return fmt.Sprintf("%s:%s:%s", prefix, level, fmt.Sprint(args...)) 89 | } 90 | 91 | // printFormattedMessage prints formatted log messages to Stdout 92 | func printFormattedMessage(formattedMsg string) { 93 | printLog.Printf("%s\n", formattedMsg) 94 | } 95 | 96 | // logToFileIfEnabled logs messages to the file if logging is enabled 97 | func logToFileIfEnabled(formattedMsg string) { 98 | if logFile != nil { 99 | LogToFile(formattedMsg) 100 | } 101 | } 102 | 103 | // PrintVerboseNoLog prints verbose messages without logging to the file 104 | func PrintVerboseNoLog(prefix, level string, depth float32, args ...interface{}) { 105 | if IsVerbose() { 106 | formattedMsg := formatMessage(prefix, level, depth, args...) 107 | printFormattedMessage(formattedMsg) 108 | } 109 | } 110 | 111 | // PrintVerbose prints verbose messages and logs to the file if enabled 112 | func PrintVerbose(prefix, level string, depth float32, args ...interface{}) { 113 | PrintVerboseNoLog(prefix, level, depth, args...) 114 | 115 | logToFileIfEnabled(formatMessage(prefix, level, depth, args...)) 116 | } 117 | 118 | // PrintVerboseSimpleNoLog prints simple verbose messages without logging to the file 119 | func PrintVerboseSimpleNoLog(args ...interface{}) { 120 | PrintVerboseNoLog("", "", -1, args...) 121 | } 122 | 123 | // PrintVerboseSimple prints simple verbose messages and logs to the file if enabled 124 | func PrintVerboseSimple(args ...interface{}) { 125 | PrintVerbose("", "", -1, args...) 126 | } 127 | 128 | // PrintVerboseErrNoLog prints verbose error messages without logging to the file 129 | func PrintVerboseErrNoLog(prefix string, depth float32, args ...interface{}) { 130 | PrintVerboseNoLog(prefix, "err", depth, args...) 131 | } 132 | 133 | // PrintVerboseErr prints verbose error messages and logs to the file if enabled 134 | func PrintVerboseErr(prefix string, depth float32, args ...interface{}) { 135 | PrintVerbose(prefix, "err", depth, args...) 136 | } 137 | 138 | // PrintVerboseWarnNoLog prints verbose warning messages without logging to the file 139 | func PrintVerboseWarnNoLog(prefix string, depth float32, args ...interface{}) { 140 | PrintVerboseNoLog(prefix, "warn", depth, args...) 141 | } 142 | 143 | // PrintVerboseWarn prints verbose warning messages and logs to the file if enabled 144 | func PrintVerboseWarn(prefix string, depth float32, args ...interface{}) { 145 | PrintVerbose(prefix, "warn", depth, args...) 146 | } 147 | 148 | // PrintVerboseInfoNoLog prints verbose info messages without logging to the file 149 | func PrintVerboseInfoNoLog(prefix string, args ...interface{}) { 150 | PrintVerboseNoLog(prefix, "info", -1, args...) 151 | } 152 | 153 | // PrintVerboseInfo prints verbose info messages and logs to the file if enabled 154 | func PrintVerboseInfo(prefix string, args ...interface{}) { 155 | PrintVerbose(prefix, "info", -1, args...) 156 | } 157 | 158 | // LogToFile writes messages to the log file 159 | func LogToFile(msg string, args ...interface{}) error { 160 | if logFile != nil { 161 | _, err := fmt.Fprintf( 162 | logFile, 163 | "%s: %s\n", 164 | time.Now().Format("2006-01-02 1 15:04:05"), 165 | fmt.Sprintf(msg, args...), 166 | ) 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | // GetLogFile returns the log file handle 173 | func GetLogFile() *os.File { 174 | return logFile 175 | } 176 | -------------------------------------------------------------------------------- /locales/ko.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot는 2개의 루트 파티션(A<->B) 간에 트랜잭션을 수행하여 완전한 불변성과 원자성을 제공합니다" 4 | short: "ABRoot는 2개의 루트 파티션(A<->B) 간에 트랜잭션을 수행하여 완전한 불변성과 원자성을 제공합니다" 5 | verboseFlag: "더 자세한 출력 표시" 6 | 7 | msg: 8 | additionalCommands: 추가 명령 9 | moreInfo: 명령에 대한 자세한 정보를 보려면 %s를 사용합니다 10 | aliases: 별칭 11 | additionalHelpTopics: 추가 도움말 항목 12 | availableCommands: 사용 가능한 명령 13 | globalFlags: 전역 플래그 14 | examples: 예시 15 | usage: 사용법 16 | version: abroot 버전 표시. 17 | help: abroot에 대한 도움말을 표시합니다. 18 | flags: 플래그 19 | kargs: 20 | use: "kargs" 21 | long: "커널 매개변수를 관리합니다." 22 | short: "커널 매개변수를 관리합니다" 23 | rootRequired: "이 명령을 실행하려면 루트 권한이 있어야 합니다." 24 | notChanged: 커널 매개변수에 변경 사항이 없습니다. 25 | applyFailed: "명령 적용이 실패했습니다: %s\n" 26 | unknownCommand: 알 수 없는 명령 '%s'. 사용 예제를 보려면 'abroot kargs --help'를 실행하세요. 27 | rollback: 28 | use: "rollback" 29 | long: "현재 루트에 대한 변경 사항을 버림으로써 시스템 rollback을 실행합니다." 30 | short: "시스템을 이전 상태로 복원합니다" 31 | rootRequired: "이 명령을 실행하려면 루트 권한이 있어야 합니다." 32 | rollbackFailed: "Rollback 실패: %s\n" 33 | rollbackSuccess: Rollback이 성공적으로 완료되었습니다. 34 | rollbackUnnecessary: Rollback은 필요하지 않으며 현재 루트가 이미 현재 루트입니다. 35 | canRollback: 이전 루트로 rollback할 수 있습니다. 36 | cannotRollback: 이전 루트로 rollback 할 수 없습니다. 37 | checkOnlyFlag: 이전 루트로의 rollback 이 가능한지 확인 38 | pkg: 39 | addedMsg: "패키지(들) %s가 추가되었습니다.\n" 40 | use: pkg 41 | long: 패키지를 설치하고 관리합니다. 42 | short: 패키지 관리 43 | rootRequired: 이 명령을 실행하려면 루트 권한이 있어야 합니다. 44 | noPackageNameProvided: 이 작업에는 적어도 하나의 패키지 이름을 제공해야 합니다. 45 | applyFailed: "명령 적용이 실패했습니다: %s\n" 46 | removedMsg: "패키지(들) %s가 제거되었습니다.\n" 47 | listMsg: "추가된 패키지:\n%s\n제거된 패키지:\n%s\n" 48 | dryRunFlag: 작업의 시뮬레이션을 실행하세요 49 | agreementSignFailed: "계약에 서명하지 못했습니다: %s\n" 50 | agreementDeclined: 동의를 거부했습니다. 이 기능은 동의할 때까지 비활성화된 상태로 유지됩니다. 51 | agreementMsg: "ABRoot의 abroot pkg 명령을 사용하려면 명시적인 사용자 동의가 필요합니다. 이 명령은 패키지 설치를 용이하게 52 | 하지만 비결정적 요소를 도입하여 시스템 신뢰성에 영향을 줍니다. 동의함으로써 귀하는 이러한 영향을 인정하고 수락하여 이 명령이 시스템 동작에 53 | 미칠 수 있는 잠재적 영향을 인지하고 있음을 확인합니다. [y/N]: " 54 | forceEnableUserAgreementFlag: 임베디드 시스템의 경우, 사용자 동의 강제 활성화 55 | failedGettingPkgManagerInstance: "패키지 관리자 인스턴스를 가져오지 못했습니다: %s\n" 56 | noChanges: 적용할 변경 사항이 없습니다. 57 | unknownCommand: 알 수 없는 명령 '%s'. 사용 예제를 보려면 'abroot pkg --help'를 실행하세요. 58 | forceApply: force apply changes even if they've already been applied 59 | applySuccess: Successfully applied packages. 60 | status: 61 | use: status 62 | long: 현재 ABRoot 상태를 표시합니다. 63 | short: 상태 표시 64 | dumpFlag: ABRoot 상태를 아카이브에 덤프합니다 65 | rootRequired: 이 명령을 실행하려면 루트 권한이 있어야 합니다. 66 | jsonFlag: JSON 형식으로 출력을 표시합니다 67 | unstagedFoundMsg: "\n\t\t미적용된 패키지가 %d개 있습니다. 'abroot pkg apply'를 실행하여 적용하십시오." 68 | dumpMsg: "ABRoot 상태를 %s에 덤프했습니다.\n" 69 | specs: 70 | cpu: 'CPU: %s' 71 | gpu: 'GPU: %s' 72 | title: '기기 사양:' 73 | memory: '메모리: %s' 74 | loadedConfig: '로드된 구성:' 75 | packages: 76 | removed: '제거됨: %s' 77 | unstaged: '준비되지 않음: %s%s' 78 | title: '패키지:' 79 | added: '추가됨: %s' 80 | partitions: 81 | future: '미래: %s%s' 82 | present: '현재: %s%s' 83 | title: 'ABRoot 파티션:' 84 | kargs: '커널 인자: %s' 85 | abimage: 86 | timestamp: 'Timestamp: %s' 87 | title: 'ABImage:' 88 | digest: 'Digest: %s' 89 | image: '이미지: %s' 90 | agreementStatus: '패키지 계약:' 91 | upgrade: 92 | forceFlag: 확인 없이 부트 파티션을 강제로 업데이트합니다 93 | rootRequired: 이 명령을 실행하려면 루트여야 합니다. 94 | use: upgrade 95 | long: 유지 보수 목적으로 부트 파티션을 업데이트합니다 (고급 사용자 전용). 96 | short: 시스템을 업그레이드 합니다 97 | noUpdateAvailable: 시스템에 대한 업데이트가 없습니다. 98 | checkOnlyFlag: 업데이트를 적용하지 않고 오직 확인합니다 99 | removed: 제거된 항목 100 | downgraded: 다운그레이드 된 항목 101 | packageUpdateAvailable: '%d개의 패키지 업데이트가 있습니다.' 102 | systemUpdateAvailable: 시스템에 대한 업데이트가 있습니다. 103 | upgraded: 업그레이드 된 항목 104 | added: 추가된 항목 105 | checkingPackageUpdate: 패키지 업데이트 확인중... 106 | checkingSystemUpdate: 시스템 업데이트 확인중... 107 | dryRunFlag: 작업의 시뮬레이션을 실행하세요 108 | dryRunSuccess: 시뮬레이션이 성공적으로 완료되었습니다. 109 | success: 업그레이드가 성공적으로 완료되었습니다. 110 | cancel: Temporarily block or cancel any abroot operation. 111 | deleteOld: Delete old image to free up space, will make future root 112 | temporarily unusable. 113 | unblock: Remove temporary user block. 114 | updateInitramfs: 115 | short: initramfs를 업데이트 합니다 116 | updateFailed: "미래 루트의 initramf를 업데이트하지 못했습니다.\n" 117 | rootRequired: 이 명령을 실행하려면 루트 권한이 있어야 합니다. 118 | updateSuccess: 미래 루트의 initramf 업데이트. 119 | long: 미래 루트의 initramf를 업데이트합니다. 120 | use: update-initramfs 121 | dryRunFlag: 작업의 예비 실행을 수행합니다 122 | cnf: 123 | editorFailed: "편집기를 열지 못했습니다: %s\n" 124 | long: 편집기를 열어 ABRoot 구성을 편집합니다. 125 | short: ABRoot 구성 편집 126 | use: cnf 127 | changed: 구성이 변경되었습니다. 128 | failed: "구성과 상호 작용하는 동안 오류가 발생했습니다: %s\n" 129 | rootRequired: 이 명령을 실행하려면 루트 권한이 있어야 합니다. 130 | unchanged: 구성은 변경되지 않았습니다. 131 | rebase: 132 | short: Rebase to new image 133 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 134 | cannot be used together." 135 | rootRequired: You must be root to run this command. 136 | rebaseOnly: Do not update the system after chaning the configured image 137 | pkgRemoveSuccess: All added packages have been removed successfully 138 | successUpdate: Rebase completed successfully. The system will update in a 139 | moment. 140 | success: Rebase completed successfully. Your next update will use the new 141 | image. 142 | dryRunSuccess: Dry run completed successfully 143 | dryRunFlag: perform a dry run of the operation 144 | long: Change the OCI image the system is using 145 | keepPackages: Keep layered packages while rebasing the system 146 | removePackagesShort: Remove all layered packages while rebasing to prevent 147 | issues caused by a change in repositories. 148 | removePackagesLong: "The new image may use a different software repository, making 149 | some installed packages inaccessible. If this is the case, the next upgrade will 150 | fail.\nWould you like to remove your added packages to resolve this issue?" 151 | -------------------------------------------------------------------------------- /cmd/pkg.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "bufio" 18 | "errors" 19 | "os" 20 | "strings" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/vanilla-os/abroot/core" 25 | "github.com/vanilla-os/orchid/cmdr" 26 | ) 27 | 28 | var validPkgArgs = []string{"add", "remove", "list", "apply"} 29 | 30 | func NewPkgCommand() *cmdr.Command { 31 | cmd := cmdr.NewCommand( 32 | "pkg add|remove|list|apply", 33 | abroot.Trans("pkg.long"), 34 | abroot.Trans("pkg.short"), 35 | func(cmd *cobra.Command, args []string) error { 36 | err := pkg(cmd, args) 37 | if err != nil { 38 | os.Exit(1) 39 | } 40 | return nil 41 | }, 42 | ) 43 | 44 | cmd.WithBoolFlag( 45 | cmdr.NewBoolFlag( 46 | "dry-run", 47 | "d", 48 | abroot.Trans("pkg.dryRunFlag"), 49 | false)) 50 | 51 | cmd.WithBoolFlag( 52 | cmdr.NewBoolFlag( 53 | "force-enable-user-agreement", 54 | "f", 55 | abroot.Trans("pkg.forceEnableUserAgreementFlag"), 56 | false)) 57 | 58 | cmd.WithBoolFlag( 59 | cmdr.NewBoolFlag( 60 | "force-apply", 61 | "", 62 | abroot.Trans("pkg.forceApply"), 63 | false)) 64 | 65 | cmd.WithBoolFlag( 66 | cmdr.NewBoolFlag( 67 | "delete-old-system", 68 | "", 69 | abroot.Trans("upgrade.deleteOld"), 70 | false)) 71 | 72 | cmd.Args = cobra.MinimumNArgs(1) 73 | cmd.ValidArgs = validPkgArgs 74 | cmd.Example = "abroot pkg add " 75 | 76 | return cmd 77 | } 78 | 79 | func pkg(cmd *cobra.Command, args []string) error { 80 | if !core.RootCheck(false) { 81 | cmdr.Error.Println(abroot.Trans("pkg.rootRequired")) 82 | return nil 83 | } 84 | 85 | dryRun, err := cmd.Flags().GetBool("dry-run") 86 | if err != nil { 87 | cmdr.Error.Println(err) 88 | return err 89 | } 90 | 91 | freeSpace, err := cmd.Flags().GetBool("delete-old-system") 92 | if err != nil { 93 | cmdr.Error.Println(err) 94 | return err 95 | } 96 | 97 | forceEnableUserAgreement, err := cmd.Flags().GetBool("force-enable-user-agreement") 98 | if err != nil { 99 | cmdr.Error.Println(err) 100 | return err 101 | } 102 | 103 | forceApply, err := cmd.Flags().GetBool("force-apply") 104 | if err != nil { 105 | cmdr.Error.Println(err) 106 | return err 107 | } 108 | 109 | pkgM, err := core.NewPackageManager(false) 110 | if err != nil { 111 | cmdr.Error.Println(abroot.Trans("pkg.failedGettingPkgManagerInstance", err)) 112 | return err 113 | } 114 | 115 | // Check for user agreement, here we could simply call the CheckStatus 116 | // function which also checks if the package manager is enabled or not 117 | // since this pkg command is not even added to the root command if the 118 | // package manager is disabled, but we want to be explicit here to avoid 119 | // potential hard to debug errors in the future in weird development 120 | // scenarios. Yeah, trust me, I've been there. 121 | if pkgM.Status == core.PKG_MNG_REQ_AGREEMENT { 122 | err = pkgM.CheckStatus() 123 | if err != nil { 124 | if !forceEnableUserAgreement { 125 | cmdr.Info.Println(abroot.Trans("pkg.agreementMsg")) 126 | reader := bufio.NewReader(os.Stdin) 127 | answer, _ := reader.ReadString('\n') 128 | answer = strings.TrimSpace(answer) 129 | if answer == "y" || answer == "Y" { 130 | err := pkgM.AcceptUserAgreement() 131 | if err != nil { 132 | cmdr.Error.Println(abroot.Trans("pkg.agreementSignFailed"), err) 133 | return err 134 | } 135 | } else { 136 | cmdr.Info.Println(abroot.Trans("pkg.agreementDeclined")) 137 | return nil 138 | } 139 | } else { 140 | err := pkgM.AcceptUserAgreement() 141 | if err != nil { 142 | cmdr.Error.Println(abroot.Trans("pkg.agreementSignFailed"), err) 143 | return err 144 | } 145 | } 146 | } 147 | } 148 | 149 | switch args[0] { 150 | case "add": 151 | if len(args) < 2 { 152 | return errors.New(abroot.Trans("pkg.noPackageNameProvided")) 153 | } 154 | for _, pkg := range args[1:] { 155 | err := pkgM.Add(pkg) 156 | if err != nil { 157 | cmdr.Error.Println(err) 158 | return err 159 | } 160 | } 161 | cmdr.Info.Printf(abroot.Trans("pkg.addedMsg"), strings.Join(args[1:], ", ")) 162 | case "remove": 163 | if len(args) < 2 { 164 | return errors.New(abroot.Trans("pkg.noPackageNameProvided")) 165 | } 166 | for _, pkg := range args[1:] { 167 | err := pkgM.Remove(pkg) 168 | if err != nil { 169 | cmdr.Error.Println(err) 170 | return err 171 | } 172 | } 173 | cmdr.Info.Printf(abroot.Trans("pkg.removedMsg"), strings.Join(args[1:], ", ")) 174 | case "list": 175 | added, err := pkgM.GetAddPackagesString("\n") 176 | if err != nil { 177 | cmdr.Error.Println(err) 178 | return err 179 | } 180 | 181 | removed, err := pkgM.GetRemovePackagesString("\n") 182 | if err != nil { 183 | cmdr.Error.Println(err) 184 | return err 185 | } 186 | 187 | cmdr.Info.Printf(abroot.Trans("pkg.listMsg"), added, removed) 188 | return nil 189 | case "apply": 190 | unstagedAdded, unstagedRemoved, err := pkgM.GetUnstagedPackages("/") 191 | if err != nil { 192 | cmdr.Error.Println(err) 193 | return err 194 | } 195 | 196 | if !forceApply && len(unstagedAdded) == 0 && len(unstagedRemoved) == 0 { 197 | cmdr.Info.Println(abroot.Trans("pkg.noChanges")) 198 | return nil 199 | } 200 | 201 | aBsys, err := core.NewABSystem() 202 | if err != nil { 203 | cmdr.Error.Println(err) 204 | return err 205 | } 206 | 207 | if dryRun { 208 | err = aBsys.RunOperation(core.DRY_RUN_APPLY, freeSpace) 209 | } else { 210 | err = aBsys.RunOperation(core.APPLY, freeSpace) 211 | } 212 | if err != nil { 213 | cmdr.Error.Printf(abroot.Trans("pkg.applyFailed"), err) 214 | return err 215 | } 216 | cmdr.Info.Println(abroot.Trans("pkg.applySuccess")) 217 | default: 218 | cmdr.Error.Println(abroot.Trans("pkg.unknownCommand", args[0])) 219 | return nil 220 | } 221 | 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /core/kargs.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "os" 18 | "os/exec" 19 | "strings" 20 | ) 21 | 22 | var KargsPath = "/etc/abroot/kargs" 23 | 24 | const ( 25 | DefaultKargs = "quiet splash bgrt_disable $vt_handoff" 26 | KargsTmpFile = "/tmp/kargs-temp" 27 | ) 28 | 29 | func init() { 30 | if os.Getenv("ABROOT_KARGS_PATH") != "" { 31 | KargsPath = os.Getenv("ABROOT_KARGS_PATH") 32 | } 33 | } 34 | 35 | // kargsCreateIfMissing creates the kargs file if it doesn't exist 36 | func kargsCreateIfMissing() error { 37 | PrintVerboseInfo("kargsCreateIfMissing", "running...") 38 | 39 | if _, err := os.Stat(KargsPath); os.IsNotExist(err) { 40 | PrintVerboseInfo("kargsCreateIfMissing", "creating kargs file...") 41 | err = os.WriteFile(KargsPath, []byte(DefaultKargs), 0644) 42 | if err != nil { 43 | PrintVerboseErr("kargsCreateIfMissing", 0, err) 44 | return err 45 | } 46 | } 47 | 48 | PrintVerboseInfo("kargsCreateIfMissing", "done") 49 | return nil 50 | } 51 | 52 | // KargsWrite makes a backup of the current kargs file and then 53 | // writes the new content to it 54 | func KargsWrite(content string) error { 55 | PrintVerboseInfo("KargsWrite", "running...") 56 | 57 | err := kargsCreateIfMissing() 58 | if err != nil { 59 | PrintVerboseErr("KargsWrite", 0, err) 60 | return err 61 | } 62 | 63 | validated, err := KargsFormat(content) 64 | if err != nil { 65 | PrintVerboseErr("KargsWrite", 1, err) 66 | return err 67 | } 68 | 69 | err = KargsBackup() 70 | if err != nil { 71 | PrintVerboseErr("KargsWrite", 2, err) 72 | return err 73 | } 74 | 75 | err = os.WriteFile(KargsPath, []byte(validated), 0644) 76 | if err != nil { 77 | PrintVerboseErr("KargsWrite", 3, err) 78 | return err 79 | } 80 | 81 | PrintVerboseInfo("KargsWrite", "done") 82 | return nil 83 | } 84 | 85 | // KargsBackup makes a backup of the current kargs file 86 | func KargsBackup() error { 87 | PrintVerboseInfo("KargsBackup", "running...") 88 | 89 | content, err := KargsRead() 90 | if err != nil { 91 | PrintVerboseErr("KargsBackup", 0, err) 92 | return err 93 | } 94 | 95 | err = os.WriteFile(KargsPath+".bak", []byte(content), 0644) 96 | if err != nil { 97 | PrintVerboseErr("KargsBackup", 1, err) 98 | return err 99 | } 100 | 101 | PrintVerboseInfo("KargsBackup", "done") 102 | return nil 103 | } 104 | 105 | // KargsRead reads the content of the kargs file 106 | func KargsRead() (string, error) { 107 | PrintVerboseInfo("KargsRead", "running...") 108 | 109 | err := kargsCreateIfMissing() 110 | if err != nil { 111 | PrintVerboseErr("KargsRead", 0, err) 112 | return "", err 113 | } 114 | 115 | content, err := os.ReadFile(KargsPath) 116 | if err != nil { 117 | PrintVerboseErr("KargsRead", 1, err) 118 | return "", err 119 | } 120 | 121 | PrintVerboseInfo("KargsRead", "done") 122 | return string(content), nil 123 | } 124 | 125 | // KargsFormat formats the contents of the kargs file, ensuring that 126 | // there are no duplicate entries, multiple spaces or trailing newline 127 | func KargsFormat(content string) (string, error) { 128 | PrintVerboseInfo("KargsValidate", "running...") 129 | 130 | kargs := []string{} 131 | 132 | lines := strings.Split(content, "\n") 133 | for _, line := range lines { 134 | if line == "" { 135 | continue 136 | } 137 | 138 | lineArgs := strings.Split(line, " ") 139 | for _, larg := range lineArgs { 140 | // Check for duplicates 141 | isDuplicate := false 142 | for _, ka := range kargs { 143 | if ka == larg { 144 | isDuplicate = true 145 | break 146 | } 147 | } 148 | 149 | if !isDuplicate { 150 | kargs = append(kargs, larg) 151 | } 152 | } 153 | } 154 | 155 | PrintVerboseInfo("KargsValidate", "done") 156 | return strings.Join(kargs, " "), nil 157 | } 158 | 159 | // KargsEdit copies the kargs file to a temporary file and opens it in the 160 | // user's preferred editor by querying the $EDITOR environment variable. 161 | // Once closed, its contents are written back to the main kargs file. 162 | // This function returns a boolean parameter indicating whether any changes 163 | // were made to the kargs file. 164 | func KargsEdit() (bool, error) { 165 | PrintVerboseInfo("KargsEdit", "running...") 166 | 167 | editor := os.Getenv("EDITOR") 168 | if editor == "" { 169 | editor = "nano" 170 | } 171 | 172 | err := kargsCreateIfMissing() 173 | if err != nil { 174 | PrintVerboseErr("KargsEdit", 0, err) 175 | return false, err 176 | } 177 | 178 | // Open a temporary file, so editors installed via apx can also be used 179 | PrintVerboseInfo("KargsEdit", "Copying kargs file to /tmp") 180 | err = CopyFile(KargsPath, KargsTmpFile) 181 | if err != nil { 182 | PrintVerboseErr("KargsEdit", 1, err) 183 | return false, err 184 | } 185 | 186 | // Call $EDITOR on temp file 187 | PrintVerboseInfo("KargsEdit", "Opening", KargsTmpFile, "in", editor) 188 | cmd := exec.Command(editor, KargsTmpFile) 189 | cmd.Stdin = os.Stdin 190 | cmd.Stdout = os.Stdout 191 | cmd.Stderr = os.Stderr 192 | err = cmd.Run() 193 | if err != nil { 194 | PrintVerboseErr("KargsEdit", 2, err) 195 | return false, err 196 | } 197 | 198 | content, err := os.ReadFile(KargsTmpFile) 199 | if err != nil { 200 | PrintVerboseErr("KargsEdit", 3, err) 201 | return false, err 202 | } 203 | 204 | // Check whether there were any changes 205 | ogContent, err := os.ReadFile(KargsPath) 206 | if err != nil { 207 | PrintVerboseErr("KargsEdit", 4, err) 208 | return false, err 209 | } 210 | if string(ogContent) == string(content) { 211 | PrintVerboseInfo("KargsEdit", "No changes were made to kargs, skipping save.") 212 | return false, nil 213 | } 214 | 215 | PrintVerboseInfo("KargsEdit", "Writing contents of", KargsTmpFile, "to the original location") 216 | err = KargsWrite(string(content)) 217 | if err != nil { 218 | PrintVerboseErr("KargsEdit", 5, err) 219 | return false, err 220 | } 221 | 222 | PrintVerboseInfo("KargsEdit", "Done") 223 | return true, nil 224 | } 225 | -------------------------------------------------------------------------------- /abroot-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/ar.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "يوفر ABRoot الثبات الكامل والذرية عن طريق إجراء المعاملات بين قسمين جذر (A<->B)" 4 | short: "يوفر ABRoot الثبات الكامل والذرية عن طريق إجراء المعاملات بين قسمين جذر 5 | (A<->B)" 6 | verboseFlag: "إظهار إخراج أكثر تفصيلاً" 7 | 8 | msg: 9 | examples: أمثلة 10 | version: إظهار إصدار لـabroot. 11 | additionalHelpTopics: مواضيع مساعدة إضافية 12 | availableCommands: الأوامر المتاحة 13 | globalFlags: الأعلام العالمية 14 | help: عرض المساعدة لـabroot. 15 | usage: الاستخدام 16 | additionalCommands: أوامر إضافية 17 | aliases: الأسماء المستعارة 18 | flags: الأعلام 19 | moreInfo: استخدم %s لمزيد من المعلومات حول الأمر 20 | kargs: 21 | use: "kargs" 22 | long: "إدارة معلمات النواة." 23 | short: "إدارة معلمات النواة." 24 | rootRequired: "يجب أن تكون جذرًا لتشغيل هذا الأمر." 25 | notChanged: لم يتم التعديل في معلمات النواة 26 | applyFailed: "فشل تطبيق الأمر: %s\n" 27 | unknownCommand: أمر غير معروف '%s'. شغّل 'abroot kargs --help' للحصول على 28 | أمثلة الاستخدام. 29 | rollback: 30 | use: "تراجع" 31 | long: "ينفذ تراجعًا عن النظام، متجاهلًا التغييرات التي تم إجراؤها على الجذر الحالي." 32 | short: "إعادة النظام إلى حالته السابقة." 33 | rootRequired: "يجب أن تكون جذرًا لتشغيل هذا الأمر." 34 | rollbackFailed: "فشل التراجع: %s\n" 35 | rollbackSuccess: تم التراجع بنجاح. 36 | rollbackUnnecessary: التراجع غير ضروري، الجذر الحالي هو الجذر الحالي بالفعل. 37 | canRollback: يمكن التراجع للجذر السابق. 38 | cannotRollback: لا يمكن التراجع للجذر السابق. 39 | checkOnlyFlag: تحقق مما إذا كان التراجع إلى الجذر السابق ممكنًا 40 | status: 41 | use: الحالة 42 | dumpMsg: "تم تفريغ حالة ABRoot إلى %s\n" 43 | long: عرض حالة ABRoot الحالية. 44 | jsonFlag: عرض الإخراج بتنسيق JSON 45 | dumpFlag: تفريغ حالة ABRoot إلى أرشيف 46 | rootRequired: يجب أن تكون جذرًا لتشغيل هذا الأمر. 47 | short: عرض الحالة 48 | unstagedFoundMsg: "\n\t\tهناك %d حزمة غير جاهزة. يرجى تشغيل 'abroot pkg apply' لتطبيقها." 49 | specs: 50 | cpu: 'المعالج: %s' 51 | gpu: 'معالج الرسومات: %s' 52 | title: 'مواصفات الجهاز:' 53 | memory: 'الذاكرة: %s' 54 | loadedConfig: 'التكوين المحمل:' 55 | packages: 56 | removed: 'تمت الإزالة: %s' 57 | unstaged: 'غير جاهزة: %s%s' 58 | title: 'الحزم:' 59 | added: 'أضيفت: %s' 60 | partitions: 61 | future: 'مستقبلي: %s%s' 62 | present: 'حالي: %s%s' 63 | title: 'أقسام ABRoot:' 64 | kargs: 'معلمات النواة: %s' 65 | abimage: 66 | timestamp: 'الطابع الزمني: %s' 67 | title: 'صورة AB:' 68 | digest: 'التجزئة: %s' 69 | image: 'الصورة: %s' 70 | agreementStatus: 'اتفاقية الحزمة:' 71 | pkg: 72 | use: الحزمة 73 | long: تثبيت وإدارة الحزم. 74 | short: إدارة الحزم 75 | rootRequired: يجب أن تكون مستخدم الجذر لتشغيل هذا الأمر. 76 | noPackageNameProvided: يجب توفير اسم حزمة واحد على الأقل لهذه العملية. 77 | addedMsg: "أضيفت الحزمة(ات) %s.\n" 78 | applyFailed: "فشل تطبيق الأمر: %s\n" 79 | removedMsg: "تمت إزالة الحزمة(ات) %s.\n" 80 | listMsg: "الحزم المضافة:\n%s\nالحزم المزالة:\n%s\n" 81 | dryRunFlag: إجراء تجربة جافة للعملية 82 | agreementDeclined: لقد رفضت الاتفاقية. ستبقى الميزة معطلة حتى توافق عليها. 83 | agreementMsg: "لاستخدام أمر abroot pkg في ABRoot، يتطلب الأمر موافقة صريحة من المستخدم. 84 | يتيح هذا الأمر تثبيت الحزم ولكنه يقدم عناصر غير حتمية، مما يؤثر على موثوقية النظام. 85 | من خلال الموافقة، فإنك تقر وتقبل هذه التداعيات، مؤكدًا وعيك بالتأثير المحتمل للأمر 86 | على سلوك النظام. [y/N]: " 87 | agreementSignFailed: "فشل في توقيع الاتفاقية: %s\n" 88 | forceEnableUserAgreementFlag: فرض تمكين اتفاقية المستخدم، للأنظمة المدمجة 89 | failedGettingPkgManagerInstance: "فشل في الحصول على مثيل مدير الحزم: %s\n" 90 | noChanges: لا توجد تغييرات لتطبيقها. 91 | unknownCommand: أمر غير معروف '%s'. شغّل 'abroot pkg --help' للحصول على أمثلة 92 | الاستخدام. 93 | forceApply: force apply changes even if they've already been applied 94 | applySuccess: Successfully applied packages. 95 | upgrade: 96 | use: الترقية 97 | long: تحديث قسم الإقلاع لأغراض الصيانة (للمستخدمين المتقدمين فقط) 98 | short: تحديث قسم الإقلاع 99 | forceFlag: فرض تحديث قسم الإقلاع دون طلب تأكيد 100 | rootRequired: يجب أن تكون جذرًا لتشغيل هذا الأمر. 101 | noUpdateAvailable: لا يوجد تحديث متاح لنظامك. 102 | checkOnlyFlag: تحقق من وجود تحديثات ولكن لا تطبقها 103 | removed: تمت الإزالة 104 | downgraded: تم تخفيض النسخة 105 | packageUpdateAvailable: هناك %d تحديثات للحزم. 106 | systemUpdateAvailable: هناك تحديث لنظامك. 107 | upgraded: تم الترقية 108 | added: تمت الإضافة 109 | checkingPackageUpdate: جارٍ التحقق من تحديثات الحزم... 110 | checkingSystemUpdate: جارٍ التحقق من تحديثات النظام... 111 | dryRunFlag: إجراء تجربة جافة للعملية 112 | dryRunSuccess: تم إكمال التجربة الجافة بنجاح. 113 | success: تم إكمال الترقية بنجاح. 114 | cancel: Temporarily block or cancel any abroot operation. 115 | deleteOld: Delete old image to free up space, will make future root 116 | temporarily unusable. 117 | unblock: Remove temporary user block. 118 | updateInitramfs: 119 | short: تحديث initramfs 120 | updateFailed: "فشل في تحديث initramfs للجذر المستقبلي.\n" 121 | rootRequired: يجب أن تكون جذرًا لتشغيل هذا الأمر. 122 | updateSuccess: تم تحديث initramfs للجذر المستقبلي. 123 | long: تحديث initramfs للجذر المستقبلي. 124 | use: تحديث-initramfs 125 | dryRunFlag: إجراء تجربة جافة للعملية 126 | cnf: 127 | use: cnf 128 | unchanged: لم يتم إجراء أي تغييرات على التكوين. 129 | editorFailed: "فشل في فتح المحرر: %s\n" 130 | long: افتح محررًا لتحرير تكوين ABRoot. 131 | short: تحرير تكوين ABRoot 132 | changed: تم تغيير التكوين. 133 | failed: "حدث خطأ أثناء التفاعل مع التكوين: %s\n" 134 | rootRequired: يجب أن تكون جذرًا لتشغيل هذا الأمر. 135 | rebase: 136 | short: Rebase to new image 137 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 138 | cannot be used together." 139 | rootRequired: You must be root to run this command. 140 | rebaseOnly: Do not update the system after chaning the configured image 141 | pkgRemoveSuccess: All added packages have been removed successfully 142 | successUpdate: Rebase completed successfully. The system will update in a 143 | moment. 144 | success: Rebase completed successfully. Your next update will use the new 145 | image. 146 | dryRunSuccess: Dry run completed successfully 147 | dryRunFlag: perform a dry run of the operation 148 | long: Change the OCI image the system is using 149 | keepPackages: Keep layered packages while rebasing the system 150 | removePackagesShort: Remove all layered packages while rebasing to prevent 151 | issues caused by a change in repositories. 152 | removePackagesLong: "The new image may use a different software repository, making 153 | some installed packages inaccessible. If this is the case, the next upgrade will 154 | fail.\nWould you like to remove your added packages to resolve this issue?" 155 | -------------------------------------------------------------------------------- /cmd/mount-sys.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | /* License: GPLv3 4 | Authors: 5 | Mirko Brombin 6 | Vanilla OS Contributors 7 | Copyright: 2024 8 | Description: 9 | ABRoot is utility which provides full immutability and 10 | atomicity to a Linux system, by transacting between 11 | two root filesystems. Updates are performed using OCI 12 | images, to ensure that the system is always in a 13 | consistent state. 14 | */ 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "slices" 20 | "strings" 21 | "syscall" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | "github.com/vanilla-os/abroot/core" 26 | "github.com/vanilla-os/abroot/settings" 27 | "github.com/vanilla-os/orchid/cmdr" 28 | ) 29 | 30 | type DiskLayoutError struct { 31 | Device string 32 | } 33 | 34 | func (e *DiskLayoutError) Error() string { 35 | return fmt.Sprintf("device %s has an unsupported layout", e.Device) 36 | } 37 | 38 | type PartNotFoundError struct { 39 | Partition string 40 | } 41 | 42 | func (e *PartNotFoundError) Error() string { 43 | return fmt.Sprintf("partition %s could not be found", e.Partition) 44 | } 45 | 46 | func NewMountSysCommand() *cmdr.Command { 47 | cmd := cmdr.NewCommand( 48 | "mount-sys", 49 | "", 50 | "", 51 | mountSysCmd, 52 | ) 53 | 54 | cmd.WithBoolFlag( 55 | cmdr.NewBoolFlag( 56 | "dry-run", 57 | "d", 58 | "perform a dry run of the operation", 59 | false, 60 | ), 61 | ) 62 | 63 | cmd.Example = "abroot mount-sys" 64 | 65 | cmd.Hidden = true 66 | 67 | return cmd 68 | } 69 | 70 | // helper function which only returns syntax errors and prints other ones 71 | func mountSysCmd(cmd *cobra.Command, args []string) error { 72 | err := mountSys(cmd, args) 73 | if err != nil { 74 | cmdr.Error.Println(err) 75 | os.Exit(1) 76 | } 77 | return nil 78 | } 79 | 80 | func mountSys(cmd *cobra.Command, _ []string) error { 81 | if !core.RootCheck(false) { 82 | cmdr.Error.Println("This operation requires root.") 83 | return nil 84 | } 85 | 86 | dryRun, err := cmd.Flags().GetBool("dry-run") 87 | if err != nil { 88 | return err 89 | } 90 | 91 | manager := core.NewABRootManager() 92 | present, err := manager.GetPresent() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // remount as writeable 98 | if !dryRun { 99 | err := syscall.Mount("/", "/", "", syscall.MS_REMOUNT, "") 100 | if err != nil { 101 | cmdr.Error.Println("failed to remount root", err) 102 | } 103 | } 104 | 105 | err = mountVar(manager.VarPartition, dryRun) 106 | if err != nil { 107 | cmdr.Error.Println(err) 108 | os.Exit(5) 109 | } 110 | 111 | if !dryRun { 112 | err = core.RepairRootIntegrity("/") 113 | if err != nil { 114 | cmdr.Error.Println(err) 115 | os.Exit(4) 116 | } 117 | } 118 | 119 | if present.Label == "" { 120 | return &PartNotFoundError{"current root"} 121 | } 122 | err = mountOverlayMounts(present.Label, dryRun) 123 | if err != nil { 124 | cmdr.Error.Println(err) 125 | os.Exit(7) 126 | } 127 | 128 | if present.Uuid == "" { 129 | return &PartNotFoundError{"current root"} 130 | } 131 | err = adjustFstab(present.Uuid, dryRun) 132 | if err != nil { 133 | cmdr.Error.Println(err) 134 | os.Exit(8) 135 | } 136 | 137 | if dryRun { 138 | cmdr.Info.Println("Dry run complete.") 139 | } else { 140 | cmdr.Info.Println("The system mounts have been performed successfully.") 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func mountVar(varPart core.Partition, dryRun bool) error { 147 | cmdr.FgDefault.Println("mounting " + varPart.Device + " in /var") 148 | 149 | if varPart.Device == "" { 150 | return &PartNotFoundError{settings.Cnf.PartLabelVar} 151 | } 152 | 153 | if !dryRun { 154 | err := varPart.Mount("/var") 155 | if err != nil { 156 | return err 157 | } 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func mountOverlayMounts(rootLabel string, dryRun bool) error { 164 | type overlayMount struct { 165 | destination string 166 | lowerdirs []string 167 | upperdir, workdir string 168 | } 169 | 170 | overlays := []overlayMount{ 171 | {"/etc", []string{"/etc"}, "/var/lib/abroot/etc/" + rootLabel, "/var/lib/abroot/etc/" + rootLabel + "-work"}, 172 | {"/opt", []string{"/opt"}, "/var/opt", "/var/opt-work"}, 173 | } 174 | 175 | for _, overlay := range overlays { 176 | if _, err := os.Lstat(overlay.workdir); os.IsNotExist(err) { 177 | err := os.MkdirAll(overlay.workdir, 0o755) 178 | cmdr.Warning.Println(err) 179 | // failing the boot here won't help so ingore any error 180 | } 181 | 182 | lowerCombined := strings.Join(overlay.lowerdirs, ":") 183 | options := "lowerdir=" + lowerCombined + ",upperdir=" + overlay.upperdir + ",workdir=" + overlay.workdir 184 | 185 | cmdr.FgDefault.Println("mounting overlay mount " + overlay.destination + " with options " + options) 186 | 187 | if !dryRun { 188 | err := syscall.Mount("overlay", overlay.destination, "overlay", 0, options) 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func adjustFstab(uuid string, dryRun bool) error { 199 | cmdr.FgDefault.Println("switching the root in fstab") 200 | 201 | const fstabFile = "/etc/fstab" 202 | systemMounts := []string{"/", "/var", "/usr", "/etc"} 203 | varBindMounts := []string{"/home", "/media", "/mnt", "/root"} 204 | 205 | fstabContentsRaw, err := os.ReadFile(fstabFile) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | fstabContents := string(fstabContentsRaw) 211 | 212 | lines := strings.Split(fstabContents, "\n") 213 | 214 | linesNew := make([]string, 0, len(lines)) 215 | 216 | for _, line := range lines { 217 | 218 | line = strings.TrimSpace(line) 219 | if strings.HasPrefix(line, "#") { 220 | linesNew = append(linesNew, line) 221 | continue 222 | } 223 | 224 | words := strings.Fields(line) 225 | if len(words) < 2 { 226 | linesNew = append(linesNew, line) 227 | continue 228 | } 229 | 230 | mountpoint := words[1] 231 | varBindMountLine := fmt.Sprintf("/var%s %s none defaults,bind 0 0", mountpoint, mountpoint) 232 | 233 | if slices.Contains(systemMounts, mountpoint) || (slices.Contains(varBindMounts, mountpoint) && line == varBindMountLine) { 234 | cmdr.FgDefault.Println("Deleting line: ", line) 235 | continue 236 | } 237 | 238 | linesNew = append(linesNew, line) 239 | } 240 | 241 | currentRootLine := "UUID=" + uuid + " / btrfs ro,defaults 0 0" 242 | 243 | cmdr.FgDefault.Println("Adding line: ", currentRootLine) 244 | 245 | linesNew = append([]string{currentRootLine}, linesNew...) 246 | 247 | newFstabContents := strings.Join(linesNew, "\n") 248 | 249 | newFstabFile := fstabFile + ".new" 250 | 251 | if !dryRun { 252 | cmdr.FgDefault.Println("writing new fstab file") 253 | err := os.WriteFile(newFstabFile, []byte(newFstabContents), 0o644) 254 | if err != nil { 255 | return err 256 | } 257 | err = core.AtomicSwap(fstabFile, newFstabFile) 258 | if err != nil { 259 | return err 260 | } 261 | err = os.Rename(newFstabFile, fstabFile+".old") 262 | if err != nil { 263 | cmdr.Warning.Println("Old Fstab file will keep .new suffix") 264 | // ignore, backup is not neccessary to boot 265 | } 266 | } 267 | 268 | return nil 269 | } 270 | -------------------------------------------------------------------------------- /locales/en_GB.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot provides full immutability and atomicity by performing transactions 4 | between 2 root partitions (A<->B)" 5 | short: "ABRoot provides full immutability and atomicity by performing transactions 6 | between 2 root partitions (A<->B)" 7 | verboseFlag: "show more detailed output" 8 | 9 | msg: 10 | moreInfo: Use %s for more information about a command 11 | aliases: Aliases 12 | flags: Flags 13 | additionalHelpTopics: Additional help topics 14 | availableCommands: Available Commands 15 | globalFlags: Global Flags 16 | examples: Examples 17 | help: Show help for abroot. 18 | usage: Usage 19 | additionalCommands: Additional Commands 20 | version: Show version for abroot. 21 | kargs: 22 | use: "kargs" 23 | long: "Manage kernel parameters." 24 | short: "Manage kernel parameters" 25 | rootRequired: "You must be root to run this command." 26 | notChanged: No changes were made to kernel parameters. 27 | applyFailed: "Apply command failed: %s\n" 28 | unknownCommand: Unknown command '%s'. Run 'abroot kargs --help' for usage 29 | examples. 30 | rollback: 31 | use: "rollback" 32 | long: "Executes a system rollback, discarding changes made to the present root." 33 | short: "Return the system to a previous state" 34 | rootRequired: "You must be root to run this command." 35 | rollbackFailed: "Rollback failed: %s\n" 36 | rollbackSuccess: Rollback completed successfully. 37 | rollbackUnnecessary: Rollback is not necessary, current root is already the 38 | present one. 39 | canRollback: It is possible to rollback to the previous root. 40 | cannotRollback: It is not possible to rollback to the previous root. 41 | checkOnlyFlag: check if rollback to previous root is possible 42 | pkg: 43 | applyFailed: "Apply command failed: %s\n" 44 | short: Manage packages 45 | removedMsg: "Package(s) %s removed.\n" 46 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 47 | use: pkg 48 | long: Install and manage packages. 49 | rootRequired: You must be root to run this command. 50 | noPackageNameProvided: You must provide at least one package name for this 51 | operation. 52 | addedMsg: "Package(s) %s added.\n" 53 | dryRunFlag: perform a dry run of the operation 54 | agreementDeclined: You declined the agreement. The feature will stay disabled 55 | until you agree to it. 56 | agreementSignFailed: "Failed to sign the agreement: %s\n" 57 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 58 | required. This command facilitates package installations but introduces non-deterministic 59 | elements, impacting system trustworthiness. By consenting, you acknowledge and 60 | accept these implications, confirming your awareness of the command's potential 61 | impact on system behaviour. [y/N]: " 62 | forceEnableUserAgreementFlag: force enable user agreement, for embedded 63 | systems 64 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 65 | noChanges: No changes to apply. 66 | unknownCommand: Unknown command '%s'. Run 'abroot pkg --help' for usage 67 | examples. 68 | forceApply: force apply changes even if they've already been applied 69 | applySuccess: Successfully applied packages. 70 | status: 71 | rootRequired: You must be root to run this command. 72 | use: status 73 | long: Display the current ABRoot status. 74 | short: Display status 75 | jsonFlag: show output in JSON format 76 | dumpFlag: dump the ABRoot status to an archive 77 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 78 | apply' to apply them." 79 | dumpMsg: "Dumped ABRoot status to %s\n" 80 | specs: 81 | cpu: 'CPU: %s' 82 | gpu: 'GPU: %s' 83 | title: 'Device Specifications:' 84 | memory: 'Memory: %s' 85 | loadedConfig: 'Loaded Configuration:' 86 | packages: 87 | removed: 'Removed: %s' 88 | unstaged: 'Unstaged: %s%s' 89 | title: 'Packages:' 90 | added: 'Added: %s' 91 | partitions: 92 | future: 'Future: %s%s' 93 | present: 'Present: %s%s' 94 | title: 'ABRoot Partitions:' 95 | kargs: 'Kernel Arguments:' 96 | abimage: 97 | timestamp: 'Timestamp: %s' 98 | title: 'ABImage:' 99 | digest: 'Digest: %s' 100 | image: 'Image: %s' 101 | agreementStatus: 'Package agreement:' 102 | upgrade: 103 | rootRequired: You must be root to run this command. 104 | use: upgrade 105 | long: Check for a new system image and apply it. 106 | short: Upgrade the system 107 | forceFlag: force update even if the system is up to date 108 | noUpdateAvailable: No update available. 109 | checkOnlyFlag: check for updates but do not apply them 110 | removed: Removed 111 | downgraded: Downgraded 112 | packageUpdateAvailable: There are %d package updates. 113 | systemUpdateAvailable: There is an update for your system. 114 | upgraded: Upgraded 115 | added: Added 116 | checkingPackageUpdate: Checking for package updates... 117 | checkingSystemUpdate: Checking for system updates... 118 | dryRunFlag: perform a dry run of the operation 119 | dryRunSuccess: Dry run completed successfully. 120 | success: Upgrade completed successfully. 121 | cancel: Temporarily block or cancel any abroot operation. 122 | deleteOld: Delete old image to free up space, will make future root 123 | temporarily unusable. 124 | unblock: Remove temporary user block. 125 | updateInitramfs: 126 | short: Update the initramfs 127 | updateFailed: "Failed to update initramfs of future root.\n" 128 | rootRequired: You must be root to run this command. 129 | updateSuccess: Updated initramfs of future root. 130 | long: Update the initramfs of the future root. 131 | use: update-initramfs 132 | dryRunFlag: perform a dry run of the operation 133 | cnf: 134 | unchanged: No changes were made to the configuration. 135 | editorFailed: "Failed to open the editor: %s\n" 136 | long: Open an editor to edit the ABRoot configuration. 137 | short: Edit ABRoot configuration 138 | use: cnf 139 | changed: Configuration changed. 140 | failed: "An error occurred while interacting with the configuration: %s\n" 141 | rootRequired: You must be root to run this command. 142 | rebase: 143 | short: Rebase to the new image 144 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 145 | cannot be used together." 146 | rootRequired: You must be root to run this command. 147 | rebaseOnly: Do not update the system after changing the configured image 148 | pkgRemoveSuccess: All added packages have been removed successfully 149 | successUpdate: Rebase completed successfully. The system will update in a 150 | moment. 151 | success: Rebase completed successfully. Your next update will use the new 152 | image. 153 | dryRunSuccess: Dry run completed successfully 154 | dryRunFlag: perform a dry run of the operation 155 | long: Change the OCI image that the system is using 156 | keepPackages: Keep layered packages while rebasing the system 157 | removePackagesShort: Remove all layered packages while rebasing to prevent 158 | issues caused by a change in repositories. 159 | removePackagesLong: "The new image may use a different software repository, making 160 | some installed packages inaccessible. If this is the case, the next upgrade will 161 | fail.\nWould you like to remove your added packages to resolve this issue?" 162 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot provides full immutability and atomicity by performing transactions 4 | between 2 root partitions (A<->B)" 5 | short: "ABRoot provides full immutability and atomicity by performing transactions 6 | between 2 root partitions (A<->B)" 7 | verboseFlag: "show more detailed output" 8 | msg: 9 | help: "Show help for abroot." 10 | version: "Show version for abroot." 11 | usage: "Usage" 12 | aliases: "Aliases" 13 | examples: "Examples" 14 | availableCommands: "Available Commands" 15 | additionalCommands: "Additional Commands" 16 | flags: "Flags" 17 | globalFlags: "Global Flags" 18 | additionalHelpTopics: "Additional help topics" 19 | moreInfo: "Use %s for more information about a command" 20 | 21 | kargs: 22 | use: "kargs" 23 | long: "Manage kernel parameters." 24 | short: "Manage kernel parameters" 25 | unknownCommand: "Unknown command '%s'. Run 'abroot kargs --help' for usage examples." 26 | rootRequired: "You must be root to run this command." 27 | notChanged: "No changes were made to kernel parameters." 28 | applyFailed: "Apply command failed: %s\n" 29 | 30 | cnf: 31 | use: "cnf" 32 | long: "Open an editor to edit the ABRoot configuration." 33 | short: "Edit ABRoot configuration" 34 | rootRequired: "You must be root to run this command." 35 | editorFailed: "Failed to open the editor: %s\n" 36 | changed: "Configuration changed." 37 | unchanged: "No changes were made to the configuration." 38 | failed: "An error occurred while interacting with the configuration: %s\n" 39 | 40 | rollback: 41 | use: "rollback" 42 | long: "Executes a system rollback, discarding changes made to the present root." 43 | short: "Return the system to a previous state" 44 | rootRequired: "You must be root to run this command." 45 | rollbackUnnecessary: "Rollback is not necessary, current root is already the present one." 46 | rollbackFailed: "Rollback failed: %s\n" 47 | rollbackSuccess: "Rollback completed successfully." 48 | canRollback: "It is possible to rollback to the previous root." 49 | cannotRollback: "It is not possible to rollback to the previous root." 50 | checkOnlyFlag: "check if rollback to previous root is possible" 51 | 52 | pkg: 53 | use: "pkg" 54 | long: "Install and manage packages." 55 | short: "Manage packages" 56 | unknownCommand: "Unknown command '%s'. Run 'abroot pkg --help' for usage examples." 57 | rootRequired: "You must be root to run this command." 58 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 59 | noPackageNameProvided: "You must provide at least one package name for this operation." 60 | addedMsg: "Package(s) %s added.\n" 61 | applyFailed: "Apply command failed: %s\n" 62 | removedMsg: "Package(s) %s removed.\n" 63 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 64 | applySuccess: "Successfully applied packages." 65 | noChanges: "No changes to apply." 66 | dryRunFlag: "perform a dry run of the operation" 67 | forceEnableUserAgreementFlag: "force enable user agreement, for embedded systems" 68 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is required. This command facilitates package installations but introduces non-deterministic elements, impacting system trustworthiness. By consenting, you acknowledge and accept these implications, confirming your awareness of the command's potential impact on system behavior. [y/N]: " 69 | agreementSignFailed: "Failed to sign the agreement: %s\n" 70 | agreementDeclined: "You declined the agreement. The feature will stay disabled until you agree to it." 71 | forceApply: "force apply changes even if they've already been applied" 72 | 73 | status: 74 | use: "status" 75 | long: "Display the current ABRoot status." 76 | short: "Display status" 77 | jsonFlag: "show output in JSON format" 78 | dumpFlag: "dump the ABRoot status to an archive" 79 | rootRequired: "You must be root to run this command." 80 | partitions: 81 | title: "ABRoot Partitions:" 82 | present: "Present: %s%s" 83 | future: "Future: %s%s" 84 | loadedConfig: "Loaded Configuration:" 85 | specs: 86 | title: "Device Specifications:" 87 | cpu: "CPU: %s" 88 | gpu: "GPU: %s" 89 | memory: "Memory: %s" 90 | abimage: 91 | title: "ABImage:" 92 | digest: "Digest: %s" 93 | timestamp: "Timestamp: %s" 94 | image: "Image: %s" 95 | kargs: "Kernel Arguments:" 96 | packages: 97 | title: "Packages:" 98 | added: "Added: %s" 99 | removed: "Removed: %s" 100 | unstaged: "Unstaged: %s%s" 101 | agreementStatus: "Package agreement:" 102 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 103 | apply' to apply them." 104 | dumpMsg: "Dumped ABRoot status to %s\n" 105 | 106 | upgrade: 107 | use: "upgrade" 108 | long: "Check for a new system image and apply it." 109 | short: "Upgrade the system" 110 | forceFlag: "force update even if the system is up to date" 111 | deleteOld: "Delete old image to free up space, will make future root temporarily unusable." 112 | rootRequired: "You must be root to run this command." 113 | checkingSystemUpdate: "Checking for system updates..." 114 | checkingPackageUpdate: "Checking for package updates..." 115 | systemUpdateAvailable: "There is an update for your system." 116 | packageUpdateAvailable: "There are %d package updates." 117 | noUpdateAvailable: "No update available." 118 | checkOnlyFlag: "check for updates but do not apply them" 119 | dryRunFlag: "perform a dry run of the operation" 120 | dryRunSuccess: "Dry run completed successfully." 121 | success: "Upgrade completed successfully." 122 | added: "Added" 123 | upgraded: "Upgraded" 124 | downgraded: "Downgraded" 125 | removed: "Removed" 126 | cancel: "Temporarily block or cancel any abroot operation." 127 | unblock: "Remove temporary user block." 128 | 129 | updateInitramfs: 130 | use: "update-initramfs" 131 | long: "Update the initramfs of the future root." 132 | short: "Update the initramfs" 133 | rootRequired: "You must be root to run this command." 134 | updateSuccess: "Updated initramfs of future root." 135 | updateFailed: "Failed to update initramfs of future root.\n" 136 | dryRunFlag: "perform a dry run of the operation" 137 | 138 | rebase: 139 | long: "Change the OCI image the system is using" 140 | short: "Rebase to new image" 141 | removePackagesShort: "Remove all layered packages while rebasing to prevent issues caused by a change in repositories." 142 | keepPackages: "Keep layered packages while rebasing the system" 143 | rootRequired: "You must be root to run this command." 144 | removePackagesLong: "The new image may use a different software repository, making some installed packages inaccessible. If this is the case, the next upgrade will fail.\nWould you like to remove your added packages to resolve this issue?" 145 | pkgRemoveSuccess: "All added packages have been removed successfully" 146 | dryRunFlag: "perform a dry run of the operation" 147 | dryRunSuccess: "Dry run completed successfully" 148 | success: "Rebase completed successfully. Your next update will use the new image." 149 | successUpdate: "Rebase completed successfully. The system will update in a moment." 150 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and cannot be used together." 151 | rebaseOnly: "Do not update the system after chaning the configured image" 152 | -------------------------------------------------------------------------------- /locales/tr.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot, 2 kök bölüm (A<->B) arasında işlemler gerçekleştirerek tam değişmezlik 4 | ve atomiklik sağlar" 5 | short: "ABRoot, 2 kök bölüm (A<->B) arasında işlemler gerçekleştirerek tam değişmezlik 6 | ve atomiklik sağlar" 7 | verboseFlag: "daha ayrıntılı çıktı göster" 8 | 9 | msg: 10 | version: Abroot için sürüm göster. 11 | moreInfo: Komutla ilgili daha çok bilgi için %s kullan 12 | aliases: Diğer Adlar 13 | additionalCommands: Ek Komutlar 14 | flags: İmler 15 | additionalHelpTopics: Ek yardım konuları 16 | availableCommands: Uygun Komutlar 17 | globalFlags: Küresel İmler 18 | examples: Örnekler 19 | help: Abroot için yardım göster. 20 | usage: Kullanım 21 | kargs: 22 | use: "kargs" 23 | long: "Çekirdek parametrelerini yönet." 24 | short: "Çekirdek parametrelerini yönet" 25 | rootRequired: "Bu komutu çalıştırmak için kök olmanız gerekir." 26 | notChanged: Çekirdek parametrelerinde herhangi bir değişiklik yapılmadı. 27 | applyFailed: "Komut uygulanamadı: %s\n" 28 | unknownCommand: Bilinmeyen komut '%s'. Kullanım örnekleri için 'abroot kargs 29 | --help' çalıştırın. 30 | rollback: 31 | use: "rollback" 32 | long: "Şu anki kökte yapılan değişiklikleri iptal ederek, sistemi geri alır." 33 | short: "Sistemi önceki duruma döndür" 34 | rootRequired: "Bu komutu çalıştırmak için kök olmanız gerekir." 35 | rollbackFailed: "Geri döndürme başarısız: %s\n" 36 | rollbackSuccess: Geri döndürme başarıyla tamamlandı. 37 | rollbackUnnecessary: Geri döndürme gerekmiyor, geçerli kök şu anki olan. 38 | canRollback: Önceki köke geri döndürmek olasıdır. 39 | cannotRollback: Önceki köke geri döndürmek olanaksızdır. 40 | checkOnlyFlag: önceki köke geri dönülebilirliği denetle 41 | pkg: 42 | listMsg: "Eklenen paketler:\n%s\nKaldırılan paketler:\n%s\n" 43 | use: pkg 44 | long: Paket kur ve yönet. 45 | short: Paketleri yönet 46 | rootRequired: Bu komutu çalıştırmak için kök olmanız gerekir. 47 | noPackageNameProvided: Bu işlem için en az bir paket adı sağlamalısınız. 48 | addedMsg: "Paket(ler) %s eklendi.\n" 49 | applyFailed: "Komut uygulanamadı: %s\n" 50 | removedMsg: "Paketler %s kaldırıldı.\n" 51 | dryRunFlag: işlemin provasını gerçekleştir 52 | agreementDeclined: Anlaşmayı reddettiniz. Kabul edene dek özellik devre dışı 53 | kalacak. 54 | agreementMsg: "ABRoot'un abroot pkg komutundan yararlanmak için kullanıcının açık 55 | anlaşması gereklidir. Bu komut, paket kurulumlarını kolaylaştırır ancak sistem 56 | güvenirliğini etkileyen gerekirci olmayan ögeler açığa çıkarır. Uygun bularak, 57 | komutun sistem davranışında olası etkilerini bildiğinizi onaylayarak bu etkileri 58 | kabul edersiniz. [y/N]: " 59 | agreementSignFailed: "Anlaşma imzalanamadı: %s\n" 60 | forceEnableUserAgreementFlag: kullanıcı anlaşmasını etkinleştirmeye zorla, 61 | gömülü sistemler için 62 | failedGettingPkgManagerInstance: "Paket yönetici örneği alınamadı: %s\n" 63 | noChanges: Uygulanacak değişiklik yok. 64 | unknownCommand: Bilinmeyen komut '%s'. Kullanım örnekleri için 'abroot pkg 65 | --help' çalıştırın. 66 | forceApply: halihazırda uygulanmış olsalar bile değişiklikleri uygulamaya 67 | zorla 68 | applySuccess: Paketler başarıyla uygulandı. 69 | status: 70 | use: status 71 | long: Şu anki ABRoot durumunu görüntüle. 72 | rootRequired: Bu komutu çalıştırmak için kök olmanız gerekir. 73 | short: Durumu görüntüle 74 | jsonFlag: çıktıyı JSON formatında göster 75 | dumpFlag: ABRoot durumunu bir arşive aktarın 76 | unstagedFoundMsg: "\n\t\tProvasız %d paket var. Uygulamak için lütfen 'abroot pkg 77 | apply' çalıştırın." 78 | dumpMsg: "ABRoot durumu şuna döküldü: %s\n" 79 | specs: 80 | cpu: 'MİB: %s' 81 | gpu: 'GİB: %s' 82 | memory: 'Bellek: %s' 83 | title: 'Aygıt Özellikleri:' 84 | abimage: 85 | title: 'ABImage:' 86 | digest: 'Özet: %s' 87 | image: 'Görüntü: %s' 88 | timestamp: 'Zaman Damgası: %s' 89 | partitions: 90 | present: 'Şu Anki: %s%s' 91 | future: 'Gelecek: %s%s' 92 | title: 'ABRoot Bölümleri:' 93 | agreementStatus: 'Paket anlaşması:' 94 | loadedConfig: 'Yüklü Yapılandırma:' 95 | packages: 96 | removed: 'Kaldırıldı: %s' 97 | unstaged: 'Provasız: %s%s' 98 | title: 'Paketler:' 99 | added: 'Eklendi: %s' 100 | kargs: 'Çekirdek Argümanları:' 101 | upgrade: 102 | use: upgrade 103 | long: Yeni sistem görüntüsünü denetle ve uygula. 104 | short: Sistemi yükselt 105 | forceFlag: sistem güncel olsa da güncellemeye zorla 106 | rootRequired: Bu komutu çalıştırmak için kök olmalısınız. 107 | noUpdateAvailable: Sisteminiz için güncelleme yok. 108 | checkOnlyFlag: güncellemeleri denetle ancak uygulama 109 | removed: Kaldırıldı 110 | downgraded: Düşürüldü 111 | packageUpdateAvailable: '%d paket güncellemesi var.' 112 | systemUpdateAvailable: Sisteminiz için güncelleme var. 113 | upgraded: Yükseltildi 114 | added: Eklendi 115 | checkingPackageUpdate: Paket güncellemeleri denetleniyor... 116 | checkingSystemUpdate: Sistem güncellemeleri denetleniyor... 117 | dryRunFlag: işlemin provasını gerçekleştir 118 | dryRunSuccess: Prova başarıyla tamamlandı. 119 | success: Yükseltme başarıyla tamamlandı. 120 | cancel: Herhangi bir abroot işlemini geçici olarak engelle ya da iptal et. 121 | deleteOld: Alan açmak için eski görüntüyü sil, gelecek kökü geçici olarak 122 | kullanım dışı bırakacak. 123 | unblock: Geçici kullanıcı engelini kaldır. 124 | updateInitramfs: 125 | short: initramfs'i güncelle 126 | updateFailed: "Gelecek kökün initramfs'i güncellenemedi.\n" 127 | rootRequired: Bu komutu çalıştırmak için kök olmalısınız. 128 | updateSuccess: Gelecek kökün initramfs'i güncellendi. 129 | long: Gelecek kökün initramfs'ini güncelle. 130 | use: update-initramfs 131 | dryRunFlag: işlemin provasını gerçekleştir 132 | cnf: 133 | unchanged: Yapılandırmaya değişiklik yapılmadı. 134 | editorFailed: "Düzenleyici açılamadı: %s\n" 135 | long: ABRoot yapılandırmasını düzenlemek için düzenleyici aç. 136 | short: ABRoot yapılandırmasını düzenle 137 | use: cnf 138 | changed: Yapılandırma değiştirildi. 139 | failed: "Yapılandırmayla etkileşime geçilirken hata oluştu: %s\n" 140 | rootRequired: Bu komutu çalıştırmak için kök olmalısınız. 141 | rebase: 142 | short: Yeni görüntüye yeniden tabanla 143 | flagError: "'--keep-packages' ve '--remove-packages' çakışan bayraklardır ve birlikte 144 | kullanılamaz." 145 | rootRequired: Bu komutu çalıştırmak için kök olmalısınız. 146 | rebaseOnly: Yapılandırılmış görüntüyü değiştirdikten sonra sistemi güncelleme 147 | pkgRemoveSuccess: Tüm ekli paketler başarıyla kaldırıldı 148 | successUpdate: Yeniden tabanlama başarılı. Sistem az sonra güncellenecek. 149 | success: Yeniden tabanlama başarılı. Gelecek güncellemeniz yeni görüntüyü 150 | kullanacak. 151 | dryRunSuccess: Prova başarıyla tamamlandı 152 | dryRunFlag: işlemin provasını gerçekleştir 153 | long: Sistemin kullandığı OCI görüntüsünü değiştir 154 | keepPackages: Sistemi yeniden tabanlarken katmanlı paketleri tut 155 | removePackagesShort: Yeniden tabanlarken depolardaki değişikliğin neden 156 | olabileceği sorunları önlemek için tüm katmanlı paketleri kaldır. 157 | removePackagesLong: "Yeni görüntü başka yazılım deposu kullanabilir, kimi kurulu 158 | paketleri erişilmez kılabilir. Eğer durum buysa, sonraki yükseltme başarısız olacaktır.\n\ 159 | Bu sorunu gidermek için ekli paketlerinizi kaldırmak ister misiniz?" 160 | -------------------------------------------------------------------------------- /locales/bn.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot provides full immutability and atomicity by performing transactions 4 | between 2 root partitions (A<->B)" 5 | short: "ABRoot provides full immutability and atomicity by performing transactions 6 | between 2 root partitions (A<->B)" 7 | verboseFlag: "show more detailed output" 8 | 9 | msg: 10 | globalFlags: Global Flags 11 | additionalCommands: Additional Commands 12 | moreInfo: Use %s for more information about a command 13 | flags: Flags 14 | additionalHelpTopics: Additional help topics 15 | availableCommands: Available Commands 16 | examples: Examples 17 | help: Show help for abroot। 18 | usage: Usage 19 | version: Show version for abroot। 20 | aliases: Aliases 21 | kargs: 22 | use: "kargs" 23 | long: "Manage kernel parameters." 24 | short: "Manage kernel parameters." 25 | rootRequired: "You must be root to run this command." 26 | notChanged: No changes were made to kernel parameters। 27 | applyFailed: "Apply command failed: %s\n" 28 | unknownCommand: Unknown command '%s'. Run 'abroot kargs --help' for usage 29 | examples। 30 | rollback: 31 | use: "rollback" 32 | long: "Executes a system rollback, discarding changes made to the present root." 33 | short: "Return the system to a previous state." 34 | rootRequired: "You must be root to run this command." 35 | rollbackFailed: "Rollback failed: %s\n" 36 | rollbackSuccess: Rollback completed successfully। 37 | rollbackUnnecessary: Rollback is not necessary, current root is already the 38 | present one। 39 | canRollback: It is possible to rollback to the previous root। 40 | cannotRollback: It is not possible to rollback to the previous root। 41 | checkOnlyFlag: check if rollback to previous root is possible 42 | pkg: 43 | rootRequired: You must be root to run this command। 44 | noPackageNameProvided: You must provide at least one package name for this 45 | operation। 46 | addedMsg: "Package(s) %s added.\n" 47 | applyFailed: "Apply command failed: %s\n" 48 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 49 | removedMsg: "Package(s) %s removed.\n" 50 | use: pkg 51 | long: Install and manage packages। 52 | short: Manage packages 53 | dryRunFlag: perform a dry run of the operation 54 | agreementDeclined: You declined the agreement. The feature will stay disabled 55 | until you agree to it। 56 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 57 | required. This command facilitates package installations but introduces non-deterministic 58 | elements, impacting system trustworthiness. By consenting, you acknowledge and 59 | accept these implications, confirming your awareness of the command's potential 60 | impact on system behavior. [y/N]: " 61 | agreementSignFailed: "Failed to sign the agreement: %s\n" 62 | forceEnableUserAgreementFlag: force enable user agreement, for embedded 63 | systems 64 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 65 | noChanges: No changes to apply। 66 | unknownCommand: Unknown command '%s'. Run 'abroot pkg --help' for usage 67 | examples। 68 | forceApply: force apply changes even if they've already been applied 69 | applySuccess: Successfully applied packages। 70 | status: 71 | use: status 72 | long: Display the current ABRoot status। 73 | short: Display status 74 | jsonFlag: Show output in JSON format 75 | dumpFlag: Dump the ABRoot status to an archive 76 | rootRequired: You must be root to run this command। 77 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 78 | apply' to apply them।" 79 | dumpMsg: "Dumped ABRoot status to %s\n" 80 | specs: 81 | cpu: 'CPU: %s' 82 | gpu: 'GPU: %s' 83 | title: 'Device Specifications:' 84 | memory: 'Memory: %s' 85 | loadedConfig: 'Loaded Configuration:' 86 | packages: 87 | removed: 'Removed: %s' 88 | unstaged: 'Unstaged: %s%s' 89 | title: 'Packages:' 90 | added: 'Added: %s' 91 | partitions: 92 | future: 'Future: %s%s' 93 | present: 'Present: %s%s' 94 | title: 'ABRoot Partitions:' 95 | kargs: 'Kernel Arguments: %s' 96 | abimage: 97 | timestamp: 'Timestamp: %s' 98 | title: 'ABImage:' 99 | digest: 'Digest: %s' 100 | image: 'Image: %s' 101 | agreementStatus: 'Package agreement:' 102 | upgrade: 103 | use: upgrade 104 | long: Update the boot partition for maintenance purposes (for advanced users 105 | only) 106 | short: Update the boot partition 107 | forceFlag: force update the boot partition without asking for confirmation 108 | rootRequired: You must be root to run this command। 109 | noUpdateAvailable: No update is available for your system। 110 | checkOnlyFlag: check for updates but do not apply them 111 | removed: Removed 112 | downgraded: Downgraded 113 | packageUpdateAvailable: There are %d package updates। 114 | systemUpdateAvailable: There is an update for your system। 115 | upgraded: Upgraded 116 | added: Added 117 | checkingPackageUpdate: Checking for package updates..। 118 | checkingSystemUpdate: Checking for system updates..। 119 | dryRunFlag: perform a dry run of the operation 120 | dryRunSuccess: Dry run completed successfully। 121 | success: Upgrade completed successfully। 122 | cancel: Temporarily block or cancel any abroot operation। 123 | deleteOld: Delete old image to free up space, will make future root 124 | temporarily unusable। 125 | unblock: Remove temporary user block। 126 | updateInitramfs: 127 | short: Update the initramfs 128 | updateFailed: "Failed to update initramfs of future root.\n" 129 | rootRequired: You must be root to run this command। 130 | updateSuccess: Updated initramfs of future root। 131 | long: Update the initramfs of the future root। 132 | use: update-initramfs 133 | dryRunFlag: perform a dry run of the operation 134 | cnf: 135 | editorFailed: "Failed to open the editor: %s\n" 136 | long: Open an editor to edit the ABRoot configuration। 137 | short: Edit ABRoot configuration 138 | use: cnf 139 | changed: Configuration changed। 140 | failed: "An error occurred while interacting with the configuration: %s\n" 141 | rootRequired: You must be root to run this command। 142 | unchanged: No changes were made to the configuration। 143 | rebase: 144 | short: Rebase to new image 145 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 146 | cannot be used together।" 147 | rootRequired: You must be root to run this command। 148 | rebaseOnly: Do not update the system after chaning the configured image 149 | pkgRemoveSuccess: All added packages have been removed successfully 150 | successUpdate: Rebase completed successfully. The system will update in a 151 | moment। 152 | success: Rebase completed successfully. Your next update will use the new 153 | image। 154 | dryRunSuccess: Dry run completed successfully 155 | dryRunFlag: perform a dry run of the operation 156 | long: Change the OCI image the system is using 157 | keepPackages: Keep layered packages while rebasing the system 158 | removePackagesShort: Remove all layered packages while rebasing to prevent 159 | issues caused by a change in repositories। 160 | removePackagesLong: "The new image may use a different software repository, making 161 | some installed packages inaccessible. If this is the case, the next upgrade will 162 | fail.\nWould you like to remove your added packages to resolve this issue?" 163 | -------------------------------------------------------------------------------- /locales/cs.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot provides full immutability and atomicity by performing transactions 4 | between 2 root partitions (A<->B)" 5 | short: "ABRoot provides full immutability and atomicity by performing transactions 6 | between 2 root partitions (A<->B)" 7 | verboseFlag: "zobrazit více podrobný výstup" 8 | msg: 9 | help: "Zobrazit nápovědu pro abroot." 10 | version: "Zobrazit verzi abroot" 11 | usage: "Využití" 12 | aliases: "Aliasy" 13 | examples: "Příklady" 14 | availableCommands: "Dostupné Příkazy" 15 | additionalCommands: "Dodatečné Příkazy" 16 | flags: "Flags" 17 | globalFlags: "Global Flags" 18 | additionalHelpTopics: "Další témata nápovědy" 19 | moreInfo: "Použijte %s pro více informací o příkazu" 20 | 21 | kargs: 22 | use: "kargs" 23 | long: "Nastavit parametry jádra" 24 | short: "Upravit podrobnosti jádra" 25 | unknownCommand: "Neznámý příkaz '%s'. Spusťte 'abroot kargs --help' pro ukázky použití" 26 | rootRequired: "Musíte být root ke spuštění tohoto příkazu" 27 | notChanged: "No changes were made to kernel parameters." 28 | applyFailed: "Apply command failed: %s\n" 29 | 30 | cnf: 31 | use: "cnf" 32 | long: "Open an editor to edit the ABRoot configuration." 33 | short: "Edit ABRoot configuration" 34 | rootRequired: "You must be root to run this command." 35 | editorFailed: "Failed to open the editor: %s\n" 36 | changed: "Configuration changed." 37 | unchanged: "No changes were made to the configuration." 38 | failed: "An error occurred while interacting with the configuration: %s\n" 39 | 40 | rollback: 41 | use: "rollback" 42 | long: "Executes a system rollback, discarding changes made to the present root." 43 | short: "Return the system to a previous state" 44 | rootRequired: "You must be root to run this command." 45 | rollbackUnnecessary: "Rollback is not necessary, current root is already the present 46 | one." 47 | rollbackFailed: "Rollback failed: %s\n" 48 | rollbackSuccess: "Rollback completed successfully." 49 | canRollback: "It is possible to rollback to the previous root." 50 | cannotRollback: "It is not possible to rollback to the previous root." 51 | checkOnlyFlag: "check if rollback to previous root is possible" 52 | 53 | pkg: 54 | use: "pkg" 55 | long: "Install and manage packages." 56 | short: "Manage packages" 57 | unknownCommand: "Unknown command '%s'. Run 'abroot pkg --help' for usage examples." 58 | rootRequired: "You must be root to run this command." 59 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 60 | noPackageNameProvided: "You must provide at least one package name for this operation." 61 | addedMsg: "Package(s) %s added.\n" 62 | applyFailed: "Apply command failed: %s\n" 63 | removedMsg: "Package(s) %s removed.\n" 64 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 65 | noChanges: "No changes to apply." 66 | dryRunFlag: "perform a dry run of the operation" 67 | forceEnableUserAgreementFlag: "force enable user agreement, for embedded systems" 68 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 69 | required. This command facilitates package installations but introduces non-deterministic 70 | elements, impacting system trustworthiness. By consenting, you acknowledge and 71 | accept these implications, confirming your awareness of the command's potential 72 | impact on system behavior. [y/N]: " 73 | agreementSignFailed: "Failed to sign the agreement: %s\n" 74 | agreementDeclined: "You declined the agreement. The feature will stay disabled until 75 | you agree to it." 76 | 77 | forceApply: force apply changes even if they've already been applied 78 | applySuccess: Successfully applied packages. 79 | status: 80 | use: "status" 81 | long: "Display the current ABRoot status." 82 | short: "Display status" 83 | jsonFlag: "show output in JSON format" 84 | dumpFlag: "dump the ABRoot status to an archive" 85 | rootRequired: "You must be root to run this command." 86 | partitions: 87 | title: "ABRoot Partitions:" 88 | present: "Present: %s%s" 89 | future: "Future: %s%s" 90 | loadedConfig: "Loaded Configuration:" 91 | specs: 92 | title: "Device Specifications:" 93 | cpu: "CPU: %s" 94 | gpu: "GPU: %s" 95 | memory: "Memory: %s" 96 | abimage: 97 | title: "ABImage:" 98 | digest: "Digest: %s" 99 | timestamp: "Timestamp: %s" 100 | image: "Image: %s" 101 | kargs: "Kernel Arguments:" 102 | packages: 103 | title: "Packages:" 104 | added: "Added: %s" 105 | removed: "Removed: %s" 106 | unstaged: "Unstaged: %s%s" 107 | agreementStatus: "Package agreement:" 108 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 109 | apply' to apply them." 110 | dumpMsg: "Dumped ABRoot status to %s\n" 111 | 112 | upgrade: 113 | use: "upgrade" 114 | long: "Check for a new system image and apply it." 115 | short: "Upgrade the system" 116 | forceFlag: "force update even if the system is up to date" 117 | rootRequired: "You must be root to run this command." 118 | checkingSystemUpdate: "Checking for system updates..." 119 | checkingPackageUpdate: "Checking for package updates..." 120 | systemUpdateAvailable: "There is an update for your system." 121 | packageUpdateAvailable: "There are %d package updates." 122 | noUpdateAvailable: "No update available." 123 | checkOnlyFlag: "check for updates but do not apply them" 124 | dryRunFlag: "perform a dry run of the operation" 125 | dryRunSuccess: "Dry run completed successfully." 126 | success: "Upgrade completed successfully." 127 | added: "Added" 128 | upgraded: "Upgraded" 129 | downgraded: "Downgraded" 130 | removed: "Removed" 131 | 132 | cancel: Temporarily block or cancel any abroot operation. 133 | deleteOld: Delete old image to free up space, will make future root 134 | temporarily unusable. 135 | unblock: Remove temporary user block. 136 | updateInitramfs: 137 | use: "update-initramfs" 138 | long: "Update the initramfs of the future root." 139 | short: "Update the initramfs" 140 | rootRequired: "You must be root to run this command." 141 | updateSuccess: "Updated initramfs of future root." 142 | updateFailed: "Failed to update initramfs of future root.\n" 143 | dryRunFlag: "perform a dry run of the operation" 144 | rebase: 145 | short: Rebase to new image 146 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 147 | cannot be used together." 148 | rootRequired: You must be root to run this command. 149 | rebaseOnly: Do not update the system after chaning the configured image 150 | pkgRemoveSuccess: All added packages have been removed successfully 151 | successUpdate: Rebase completed successfully. The system will update in a 152 | moment. 153 | success: Rebase completed successfully. Your next update will use the new 154 | image. 155 | dryRunSuccess: Dry run completed successfully 156 | dryRunFlag: perform a dry run of the operation 157 | long: Change the OCI image the system is using 158 | keepPackages: Keep layered packages while rebasing the system 159 | removePackagesShort: Remove all layered packages while rebasing to prevent 160 | issues caused by a change in repositories. 161 | removePackagesLong: "The new image may use a different software repository, making 162 | some installed packages inaccessible. If this is the case, the next upgrade will 163 | fail.\nWould you like to remove your added packages to resolve this issue?" 164 | -------------------------------------------------------------------------------- /locales/da.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot giver fuld uforanderlighed og atomicity ved at udføre transaktioner 4 | mellem 2 root partitioner (A->B)" 5 | short: "ABRoot provides full immutability and atomicity by performing transactions 6 | between 2 root partitions (A<->B)" 7 | verboseFlag: "show more detailed output" 8 | 9 | msg: 10 | additionalCommands: Additional Commands 11 | availableCommands: Available Commands 12 | version: Show version for abroot. 13 | moreInfo: Use %s for more information about a command 14 | aliases: Aliases 15 | flags: Flags 16 | additionalHelpTopics: Additional help topics 17 | globalFlags: Global Flags 18 | examples: Examples 19 | help: Show help for abroot. 20 | usage: Usage 21 | kargs: 22 | use: "kargs" 23 | long: "Manage kernel parameters." 24 | short: "Manage kernel parameters" 25 | rootRequired: "You must be root to run this command." 26 | notChanged: "No changes were made to kernel parameters." 27 | applyFailed: "Apply command failed: %s\n" 28 | 29 | unknownCommand: Unknown command '%s'. Run 'abroot kargs --help' for usage 30 | examples. 31 | cnf: 32 | use: "cnf" 33 | long: "Open an editor to edit the ABRoot configuration." 34 | short: "Edit ABRoot configuration" 35 | rootRequired: "You must be root to run this command." 36 | editorFailed: "Failed to open the editor: %s\n" 37 | changed: "Configuration changed." 38 | unchanged: "No changes were made to the configuration." 39 | failed: "An error occurred while interacting with the configuration: %s\n" 40 | 41 | rollback: 42 | use: "rollback" 43 | long: "Udfører en system tilbagerulning, kasserer ændringer lavet til den nuværende 44 | root." 45 | short: "Return the system to a previous state" 46 | rootRequired: "You must be root to run this command." 47 | rollbackUnnecessary: "Rollback is not necessary, current root is already the present 48 | one." 49 | rollbackFailed: "Rollback failed: %s\n" 50 | rollbackSuccess: "Rollback completed successfully." 51 | canRollback: "It is possible to rollback to the previous root." 52 | cannotRollback: "It is not possible to rollback to the previous root." 53 | 54 | checkOnlyFlag: check if rollback to previous root is possible 55 | pkg: 56 | use: "pkg" 57 | long: "Install and manage packages." 58 | short: "Manage packages" 59 | rootRequired: "You must be root to run this command." 60 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 61 | noPackageNameProvided: "You must provide at least one package name for this operation." 62 | addedMsg: "Pakker(er) %s tilføjet.\n" 63 | applyFailed: "Apply command failed: %s\n" 64 | removedMsg: "Package(s) %s removed.\n" 65 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 66 | dryRunFlag: "perform a dry run of the operation" 67 | forceEnableUserAgreementFlag: "force enable user agreement, for embedded systems" 68 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 69 | required. This command facilitates package installations but introduces non-deterministic 70 | elements, impacting system trustworthiness. By consenting, you acknowledge and 71 | accept these implications, confirming your awareness of the command's potential 72 | impact on system behavior. [y/N]: " 73 | agreementSignFailed: "Failed to sign the agreement: %s\n" 74 | agreementDeclined: "You declined the agreement. The feature will stay disabled until 75 | you agree to it." 76 | 77 | noChanges: No changes to apply. 78 | unknownCommand: Unknown command '%s'. Run 'abroot pkg --help' for usage 79 | examples. 80 | forceApply: force apply changes even if they've already been applied 81 | applySuccess: Successfully applied packages. 82 | status: 83 | use: "status" 84 | long: "Display the current ABRoot status." 85 | short: "Display status" 86 | jsonFlag: "Show output in JSON format" 87 | dumpFlag: "Dump the ABRoot status to an archive" 88 | rootRequired: "You must be root to run this command." 89 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 90 | apply' to apply them." 91 | dumpMsg: "Dumped ABRoot status to %s\n" 92 | 93 | specs: 94 | cpu: 'CPU: %s' 95 | gpu: 'GPU: %s' 96 | title: 'Device Specifications:' 97 | memory: 'Memory: %s' 98 | loadedConfig: 'Loaded Configuration:' 99 | packages: 100 | removed: 'Removed: %s' 101 | unstaged: 'Unstaged: %s%s' 102 | title: 'Packages:' 103 | added: 'Added: %s' 104 | partitions: 105 | future: 'Future: %s%s' 106 | present: 'Present: %s%s' 107 | title: 'ABRoot Partitions:' 108 | kargs: 'Kernel Arguments: %s' 109 | abimage: 110 | timestamp: 'Timestamp: %s' 111 | title: 'ABImage:' 112 | digest: 'Digest: %s' 113 | image: 'Image: %s' 114 | agreementStatus: 'Package agreement:' 115 | upgrade: 116 | use: "upgrade" 117 | long: "Check for a new system image and apply it." 118 | short: "Upgrade the system" 119 | forceFlag: "force update even if the system is up to date" 120 | rootRequired: "You must be root to run this command." 121 | checkingSystemUpdate: "Checking for system updates..." 122 | checkingPackageUpdate: "Checking for package updates..." 123 | systemUpdateAvailable: "There is an update for your system." 124 | packageUpdateAvailable: "There are %d package updates." 125 | noUpdateAvailable: "No update available." 126 | checkOnlyFlag: "check for updates but do not apply them" 127 | dryRunFlag: "perform a dry run of the operation" 128 | dryRunSuccess: "Dry run completed successfully." 129 | success: "Upgrade completed successfully." 130 | added: "Added" 131 | upgraded: "Upgraded" 132 | downgraded: "Downgraded" 133 | removed: "Removed" 134 | 135 | cancel: Temporarily block or cancel any abroot operation. 136 | deleteOld: Delete old image to free up space, will make future root 137 | temporarily unusable. 138 | unblock: Remove temporary user block. 139 | updateInitramfs: 140 | use: "update-initramfs" 141 | long: "Opdater initramfs'et af den fremtidige oot." 142 | short: "Update the initramfs" 143 | rootRequired: "You must be root to run this command." 144 | updateSuccess: "Updated initramfs of future root." 145 | updateFailed: "Failed to update initramfs of future root.\n" 146 | dryRunFlag: perform a dry run of the operation 147 | rebase: 148 | short: Rebase to new image 149 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 150 | cannot be used together." 151 | rootRequired: You must be root to run this command. 152 | rebaseOnly: Do not update the system after chaning the configured image 153 | pkgRemoveSuccess: All added packages have been removed successfully 154 | successUpdate: Rebase completed successfully. The system will update in a 155 | moment. 156 | success: Rebase completed successfully. Your next update will use the new 157 | image. 158 | dryRunSuccess: Dry run completed successfully 159 | dryRunFlag: perform a dry run of the operation 160 | long: Change the OCI image the system is using 161 | keepPackages: Keep layered packages while rebasing the system 162 | removePackagesShort: Remove all layered packages while rebasing to prevent 163 | issues caused by a change in repositories. 164 | removePackagesLong: "The new image may use a different software repository, making 165 | some installed packages inaccessible. If this is the case, the next upgrade will 166 | fail.\nWould you like to remove your added packages to resolve this issue?" 167 | -------------------------------------------------------------------------------- /locales/ka.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "abroot" 3 | long: "ABRoot უზრუნველყოფს სრულ უცვლელობას და ატომურობას ტრანზაქციების შესრულებით 4 | 2 root დანაყოფს შორის (A<->B)" 5 | short: "ABRoot უზრუნველყოფს სრულ უცვლელობას და ატომურობას ტრანზაქციების შესრულებით 6 | 2 root დანაყოფს შორის (A<->B)" 7 | verboseFlag: "ინფორმაციის მეტი დეტალის გამოტანა" 8 | 9 | msg: 10 | aliases: Aliases 11 | flags: Flags 12 | additionalHelpTopics: Additional help topics 13 | availableCommands: Available Commands 14 | globalFlags: Global Flags 15 | examples: Examples 16 | help: Show help for abroot. 17 | usage: Usage 18 | additionalCommands: Additional Commands 19 | version: Show version for abroot. 20 | moreInfo: Use %s for more information about a command 21 | kargs: 22 | use: "kargs" 23 | long: "ბირთვის პარამეტრების მართვა." 24 | short: "ბირთვის პარამეტრების მართვა" 25 | rootRequired: "ამ ბრძანების გასაშვებად root უნდა ბრძანდებოდეთ." 26 | notChanged: ბირთვის პარამეტრები არ შეცვლილა. 27 | applyFailed: "გადატარების ბრძანება ჩავარდა: %s\n" 28 | unknownCommand: Unknown command '%s'. Run 'abroot kargs --help' for usage 29 | examples. 30 | rollback: 31 | use: "rollback" 32 | long: "ახორციელებს სისტემის დაბრუნებას, უგულებელყოფს მიმდინარე ძირითად საქაღალდეში 33 | განხორციელებულ ცვლილებებს." 34 | short: "სისტემის დაბრუნება წინა მდგომარეობაში" 35 | rootRequired: "ამ ბრძანების გასაშვებად root უნდა ბრძანდებოდეთ." 36 | rollbackFailed: "Rollback failed: %s\n" 37 | rollbackSuccess: Rollback completed successfully. 38 | rollbackUnnecessary: Rollback is not necessary, current root is already the 39 | present one. 40 | canRollback: It is possible to rollback to the previous root. 41 | cannotRollback: It is not possible to rollback to the previous root. 42 | checkOnlyFlag: check if rollback to previous root is possible 43 | status: 44 | use: status 45 | rootRequired: You must be root to run this command. 46 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 47 | apply' to apply them." 48 | long: Display the current ABRoot status. 49 | short: Display status 50 | jsonFlag: Show output in JSON format 51 | dumpFlag: Dump the ABRoot status to an archive 52 | dumpMsg: "Dumped ABRoot status to %s\n" 53 | specs: 54 | cpu: 'CPU: %s' 55 | gpu: 'GPU: %s' 56 | title: 'Device Specifications:' 57 | memory: 'Memory: %s' 58 | loadedConfig: 'Loaded Configuration:' 59 | packages: 60 | removed: 'Removed: %s' 61 | unstaged: 'Unstaged: %s%s' 62 | title: 'Packages:' 63 | added: 'Added: %s' 64 | partitions: 65 | future: 'Future: %s%s' 66 | present: 'Present: %s%s' 67 | title: 'ABRoot Partitions:' 68 | kargs: 'Kernel Arguments: %s' 69 | abimage: 70 | timestamp: 'Timestamp: %s' 71 | title: 'ABImage:' 72 | digest: 'Digest: %s' 73 | image: 'Image: %s' 74 | agreementStatus: 'Package agreement:' 75 | pkg: 76 | use: pkg 77 | rootRequired: ამ ბრძანების გასაშვებად root უნდა ბრძანდებოდეთ. 78 | noPackageNameProvided: ამ ოპერაციისთვის აუცილებელია სულ ცოტა, ერთი პაკეტის 79 | მითითება. 80 | long: პაკეტების დაყენება და მართვა. 81 | short: პაკეტების მართვა 82 | addedMsg: "Package(s) %s added.\n" 83 | applyFailed: "Apply command failed: %s\n" 84 | removedMsg: "Package(s) %s removed.\n" 85 | listMsg: "Added packages:\n%s\nRemoved packages:\n%s\n" 86 | dryRunFlag: perform a dry run of the operation 87 | agreementDeclined: You declined the agreement. The feature will stay disabled 88 | until you agree to it. 89 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 90 | required. This command facilitates package installations but introduces non-deterministic 91 | elements, impacting system trustworthiness. By consenting, you acknowledge and 92 | accept these implications, confirming your awareness of the command's potential 93 | impact on system behavior. [y/N]: " 94 | agreementSignFailed: "Failed to sign the agreement: %s\n" 95 | forceEnableUserAgreementFlag: force enable user agreement, for embedded 96 | systems 97 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 98 | noChanges: No changes to apply. 99 | unknownCommand: Unknown command '%s'. Run 'abroot pkg --help' for usage 100 | examples. 101 | forceApply: force apply changes even if they've already been applied 102 | applySuccess: Successfully applied packages. 103 | upgrade: 104 | use: upgrade 105 | long: Update the boot partition for maintenance purposes (for advanced users 106 | only) 107 | short: Update the boot partition 108 | forceFlag: force update the boot partition without asking for confirmation 109 | rootRequired: You must be root to run this command. 110 | noUpdateAvailable: No update is available for your system. 111 | checkOnlyFlag: check for updates but do not apply them 112 | removed: Removed 113 | downgraded: Downgraded 114 | packageUpdateAvailable: There are %d package updates. 115 | systemUpdateAvailable: There is an update for your system. 116 | upgraded: Upgraded 117 | added: Added 118 | checkingPackageUpdate: Checking for package updates... 119 | checkingSystemUpdate: Checking for system updates... 120 | dryRunFlag: perform a dry run of the operation 121 | dryRunSuccess: Dry run completed successfully. 122 | success: Upgrade completed successfully. 123 | cancel: Temporarily block or cancel any abroot operation. 124 | deleteOld: Delete old image to free up space, will make future root 125 | temporarily unusable. 126 | unblock: Remove temporary user block. 127 | updateInitramfs: 128 | short: Update the initramfs 129 | updateFailed: "Failed to update initramfs of future root.\n" 130 | rootRequired: You must be root to run this command. 131 | updateSuccess: Updated initramfs of future root. 132 | long: Update the initramfs of the future root. 133 | use: update-initramfs 134 | dryRunFlag: perform a dry run of the operation 135 | cnf: 136 | unchanged: No changes were made to the configuration. 137 | editorFailed: "Failed to open the editor: %s\n" 138 | long: Open an editor to edit the ABRoot configuration. 139 | short: Edit ABRoot configuration 140 | use: cnf 141 | changed: Configuration changed. 142 | rootRequired: You must be root to run this command. 143 | failed: "An error occurred while interacting with the configuration: %s\n" 144 | rebase: 145 | short: Rebase to new image 146 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 147 | cannot be used together." 148 | rootRequired: You must be root to run this command. 149 | rebaseOnly: Do not update the system after chaning the configured image 150 | pkgRemoveSuccess: All added packages have been removed successfully 151 | successUpdate: Rebase completed successfully. The system will update in a 152 | moment. 153 | success: Rebase completed successfully. Your next update will use the new 154 | image. 155 | dryRunSuccess: Dry run completed successfully 156 | dryRunFlag: perform a dry run of the operation 157 | long: Change the OCI image the system is using 158 | keepPackages: Keep layered packages while rebasing the system 159 | removePackagesShort: Remove all layered packages while rebasing to prevent 160 | issues caused by a change in repositories. 161 | removePackagesLong: "The new image may use a different software repository, making 162 | some installed packages inaccessible. If this is the case, the next upgrade will 163 | fail.\nWould you like to remove your added packages to resolve this issue?" 164 | -------------------------------------------------------------------------------- /locales/be.yml: -------------------------------------------------------------------------------- 1 | abroot: 2 | use: "абарваны" 3 | long: "ABRoot забяспечвае поўную нязменнасць і атамарнасць, выконваючы транзакцыі 4 | паміж 2 каранёвымі раздзеламі (A<->B)" 5 | short: "ABRoot забяспечвае поўную нязменнасць і атамарнасць, выконваючы транзакцыі 6 | паміж 2 каранёвымі раздзеламі (A<->B)" 7 | verboseFlag: "паказаць больш падрабязны вынік" 8 | 9 | msg: 10 | aliases: Aliases 11 | additionalHelpTopics: Additional help topics 12 | availableCommands: Available Commands 13 | globalFlags: Global Flags 14 | examples: Examples 15 | help: Show help for abroot. 16 | usage: Usage 17 | additionalCommands: Additional Commands 18 | version: Show version for abroot. 19 | moreInfo: Use %s for more information about a command 20 | flags: Flags 21 | kargs: 22 | use: "kargs" 23 | long: "Кіраванне параметрамі ядра." 24 | short: "Кіраванне параметрамі ядра." 25 | rootRequired: "Вы павінны быць root, каб выканаць гэтую каманду." 26 | notChanged: Змены параметраў ядра не рабіліся. 27 | applyFailed: "Не атрымалася ўжыць каманду: %s\n" 28 | unknownCommand: Unknown command '%s'. Run 'abroot kargs --help' for usage 29 | examples. 30 | rollback: 31 | use: "адкат" 32 | long: "Выконвае адкат сістэмы, адмяняючы змены, унесеныя ў цяперашні корань." 33 | short: "Вярнуць сістэму ў папярэдні стан." 34 | rootRequired: "Вы павінны быць root, каб выканаць гэтую каманду." 35 | rollbackFailed: "Rollback failed: %s\n" 36 | rollbackSuccess: Rollback completed successfully. 37 | rollbackUnnecessary: Rollback is not necessary, current root is already the 38 | present one. 39 | canRollback: It is possible to rollback to the previous root. 40 | cannotRollback: It is not possible to rollback to the previous root. 41 | checkOnlyFlag: check if rollback to previous root is possible 42 | pkg: 43 | use: pkg 44 | long: Устаноўка пакетаў і кіраванне імі. 45 | listMsg: "Дададзены пакеты:\n%s\nВыдаленыя пакеты:\n%s\n" 46 | short: Кіраванне пакетамі 47 | rootRequired: Вы павінны быць root, каб выканаць гэтую каманду. 48 | noPackageNameProvided: Вы павінны ўказаць хаця б адно імя пакета для гэтай 49 | аперацыі. 50 | addedMsg: "Пакет(-ы) %s дададзены.\n" 51 | applyFailed: "Збой прымянення каманды: %s\n" 52 | removedMsg: "Пакет(ы) %s выдалены.\n" 53 | dryRunFlag: perform a dry run of the operation 54 | agreementMsg: "To utilize ABRoot's abroot pkg command, explicit user agreement is 55 | required. This command facilitates package installations but introduces non-deterministic 56 | elements, impacting system trustworthiness. By consenting, you acknowledge and 57 | accept these implications, confirming your awareness of the command's potential 58 | impact on system behavior. [y/N]: " 59 | agreementSignFailed: "Failed to sign the agreement: %s\n" 60 | forceEnableUserAgreementFlag: force enable user agreement, for embedded 61 | systems 62 | agreementDeclined: You declined the agreement. The feature will stay disabled 63 | until you agree to it. 64 | failedGettingPkgManagerInstance: "Failed to get package manager instance: %s\n" 65 | noChanges: No changes to apply. 66 | unknownCommand: Unknown command '%s'. Run 'abroot pkg --help' for usage 67 | examples. 68 | forceApply: force apply changes even if they've already been applied 69 | applySuccess: Successfully applied packages. 70 | status: 71 | use: Статус 72 | long: Адлюстраванне бягучага стану ABRoot. 73 | short: Адлюстраванне стану 74 | jsonFlag: Паказаць вывад у фармаце JSON 75 | dumpFlag: Выгрузка стану ABRoot у архіў 76 | rootRequired: Для выканання гэтай каманды неабходна быць карыстачом root. 77 | unstagedFoundMsg: "\n\t\tThere are %d unstaged packages. Please run 'abroot pkg 78 | apply' to apply them." 79 | dumpMsg: "Dumped ABRoot status to %s\n" 80 | specs: 81 | cpu: 'CPU: %s' 82 | gpu: 'GPU: %s' 83 | title: 'Device Specifications:' 84 | memory: 'Memory: %s' 85 | loadedConfig: 'Loaded Configuration:' 86 | packages: 87 | removed: 'Removed: %s' 88 | unstaged: 'Unstaged: %s%s' 89 | title: 'Packages:' 90 | added: 'Added: %s' 91 | partitions: 92 | future: 'Future: %s%s' 93 | present: 'Present: %s%s' 94 | title: 'ABRoot Partitions:' 95 | kargs: 'Kernel Arguments: %s' 96 | abimage: 97 | timestamp: 'Timestamp: %s' 98 | title: 'ABImage:' 99 | digest: 'Digest: %s' 100 | image: 'Image: %s' 101 | agreementStatus: 'Package agreement:' 102 | upgrade: 103 | use: upgrade 104 | long: Update the boot partition for maintenance purposes (for advanced users 105 | only) 106 | short: Update the boot partition 107 | forceFlag: force update the boot partition without asking for confirmation 108 | rootRequired: You must be root to run this command. 109 | noUpdateAvailable: No update is available for your system. 110 | checkOnlyFlag: check for updates but do not apply them 111 | removed: Removed 112 | downgraded: Downgraded 113 | packageUpdateAvailable: There are %d package updates. 114 | systemUpdateAvailable: There is an update for your system. 115 | upgraded: Upgraded 116 | added: Added 117 | checkingPackageUpdate: Checking for package updates... 118 | checkingSystemUpdate: Checking for system updates... 119 | dryRunFlag: perform a dry run of the operation 120 | dryRunSuccess: Dry run completed successfully. 121 | success: Upgrade completed successfully. 122 | cancel: Temporarily block or cancel any abroot operation. 123 | deleteOld: Delete old image to free up space, will make future root 124 | temporarily unusable. 125 | unblock: Remove temporary user block. 126 | updateInitramfs: 127 | short: Update the initramfs 128 | updateFailed: "Failed to update initramfs of future root.\n" 129 | rootRequired: You must be root to run this command. 130 | updateSuccess: Updated initramfs of future root. 131 | long: Update the initramfs of the future root. 132 | use: update-initramfs 133 | dryRunFlag: perform a dry run of the operation 134 | cnf: 135 | unchanged: No changes were made to the configuration. 136 | editorFailed: "Failed to open the editor: %s\n" 137 | long: Open an editor to edit the ABRoot configuration. 138 | short: Edit ABRoot configuration 139 | use: cnf 140 | changed: Configuration changed. 141 | failed: "An error occurred while interacting with the configuration: %s\n" 142 | rootRequired: You must be root to run this command. 143 | rebase: 144 | short: Rebase to new image 145 | flagError: "'--keep-packages' and '--remove-packages' are conflicting flags and 146 | cannot be used together." 147 | rootRequired: You must be root to run this command. 148 | rebaseOnly: Do not update the system after chaning the configured image 149 | pkgRemoveSuccess: All added packages have been removed successfully 150 | successUpdate: Rebase completed successfully. The system will update in a 151 | moment. 152 | success: Rebase completed successfully. Your next update will use the new 153 | image. 154 | dryRunSuccess: Dry run completed successfully 155 | dryRunFlag: perform a dry run of the operation 156 | long: Change the OCI image the system is using 157 | keepPackages: Keep layered packages while rebasing the system 158 | removePackagesShort: Remove all layered packages while rebasing to prevent 159 | issues caused by a change in repositories. 160 | removePackagesLong: "The new image may use a different software repository, making 161 | some installed packages inaccessible. If this is the case, the next upgrade will 162 | fail.\nWould you like to remove your added packages to resolve this issue?" 163 | --------------------------------------------------------------------------------