diff options
Diffstat (limited to 'cmd/hpkg/main.go')
| -rw-r--r-- | cmd/hpkg/main.go | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/cmd/hpkg/main.go b/cmd/hpkg/main.go new file mode 100644 index 00000000..d808aa01 --- /dev/null +++ b/cmd/hpkg/main.go @@ -0,0 +1,333 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "log" + "os" + "os/signal" + "path" + "syscall" + + "hakurei.app/command" + "hakurei.app/hst" + "hakurei.app/internal" + "hakurei.app/internal/hlog" +) + +const shellPath = "/run/current-system/sw/bin/bash" + +var ( + errSuccess = errors.New("success") +) + +func init() { + hlog.Prepare("hpkg") + if err := os.Setenv("SHELL", shellPath); err != nil { + log.Fatalf("cannot set $SHELL: %v", err) + } +} + +func main() { + if os.Geteuid() == 0 { + log.Fatal("this program must not run as root") + } + + ctx, stop := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer stop() // unreachable + + var ( + flagVerbose bool + flagDropShell bool + ) + c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). + Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). + Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action") + + { + var ( + flagDropShellActivate bool + ) + c.NewCommand("install", "Install an application from its package", func(args []string) error { + if len(args) != 1 { + log.Println("invalid argument") + return syscall.EINVAL + } + pkgPath := args[0] + if !path.IsAbs(pkgPath) { + if dir, err := os.Getwd(); err != nil { + log.Printf("cannot get current directory: %v", err) + return err + } else { + pkgPath = path.Join(dir, pkgPath) + } + } + + /* + Look up paths to programs started by hpkg. + This is done here to ease error handling as cleanup is not yet required. + */ + + var ( + _ = lookPath("zstd") + tar = lookPath("tar") + chmod = lookPath("chmod") + rm = lookPath("rm") + ) + + /* + Extract package and set up for cleanup. + */ + + var workDir string + if p, err := os.MkdirTemp("", "hpkg.*"); err != nil { + log.Printf("cannot create temporary directory: %v", err) + return err + } else { + workDir = p + } + cleanup := func() { + // should be faster than a native implementation + mustRun(chmod, "-R", "+w", workDir) + mustRun(rm, "-rf", workDir) + } + beforeRunFail.Store(&cleanup) + + mustRun(tar, "-C", workDir, "-xf", pkgPath) + + /* + Parse bundle and app metadata, do pre-install checks. + */ + + bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup) + pathSet := pathSetByApp(bundle.ID) + + a := bundle + if s, err := os.Stat(pathSet.metaPath); err != nil { + if !os.IsNotExist(err) { + cleanup() + log.Printf("cannot access %q: %v", pathSet.metaPath, err) + return err + } + // did not modify app, clean installation condition met later + } else if s.IsDir() { + cleanup() + log.Printf("metadata path %q is not a file", pathSet.metaPath) + return syscall.EBADMSG + } else { + a = loadAppInfo(pathSet.metaPath, cleanup) + if a.ID != bundle.ID { + cleanup() + log.Printf("app %q claims to have identifier %q", + bundle.ID, a.ID) + return syscall.EBADE + } + // sec: should verify credentials + } + + if a != bundle { + // do not try to re-install + if a.NixGL == bundle.NixGL && + a.CurrentSystem == bundle.CurrentSystem && + a.Launcher == bundle.Launcher && + a.ActivationPackage == bundle.ActivationPackage { + cleanup() + log.Printf("package %q is identical to local application %q", + pkgPath, a.ID) + return errSuccess + } + + // identity determines uid + if a.Identity != bundle.Identity { + cleanup() + log.Printf("package %q identity %d differs from installed %d", + pkgPath, bundle.Identity, a.Identity) + return syscall.EBADE + } + + // sec: should compare version string + hlog.Verbosef("installing application %q version %q over local %q", + bundle.ID, bundle.Version, a.Version) + } else { + hlog.Verbosef("application %q clean installation", bundle.ID) + // sec: should install credentials + } + + /* + Setup steps for files owned by the target user. + */ + + withCacheDir(ctx, "install", []string{ + // export inner bundle path in the environment + "export BUNDLE=" + hst.Tmp + "/bundle", + // replace inner /etc + "mkdir -p etc", + "chmod -R +w etc", + "rm -rf etc", + "cp -dRf $BUNDLE/etc etc", + // replace inner /nix + "mkdir -p nix", + "chmod -R +w nix", + "rm -rf nix", + "cp -dRf /nix nix", + // copy from binary cache + "nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD", + // deduplicate nix store + "nix store --offline --store $PWD optimise", + // make cache directory world-readable for autoetc + "chmod 0755 .", + }, workDir, bundle, pathSet, flagDropShell, cleanup) + + if bundle.GPU { + withCacheDir(ctx, "mesa-wrappers", []string{ + // link nixGL mesa wrappers + "mkdir -p nix/.nixGL", + "ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL", + "ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan", + }, workDir, bundle, pathSet, false, cleanup) + } + + /* + Activate home-manager generation. + */ + + withNixDaemon(ctx, "activate", []string{ + // clean up broken links + "mkdir -p .local/state/{nix,home-manager}", + "chmod -R +w .local/state/{nix,home-manager}", + "rm -rf .local/state/{nix,home-manager}", + // run activation script + bundle.ActivationPackage + "/activate", + }, false, func(config *hst.Config) *hst.Config { return config }, + bundle, pathSet, flagDropShellActivate, cleanup) + + /* + Installation complete. Write metadata to block re-installs or downgrades. + */ + + // serialise metadata to ensure consistency + if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { + cleanup() + log.Printf("cannot create metadata file: %v", err) + return err + } else if err = json.NewEncoder(f).Encode(bundle); err != nil { + cleanup() + log.Printf("cannot write metadata: %v", err) + return err + } else if err = f.Close(); err != nil { + log.Printf("cannot close metadata file: %v", err) + // not fatal + } + + if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil { + cleanup() + log.Printf("cannot rename metadata file: %v", err) + return err + } + + cleanup() + return errSuccess + }). + Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation") + } + + { + var ( + flagDropShellNixGL bool + flagAutoDrivers bool + ) + c.NewCommand("start", "Start an application", func(args []string) error { + if len(args) < 1 { + log.Println("invalid argument") + return syscall.EINVAL + } + + /* + Parse app metadata. + */ + + id := args[0] + pathSet := pathSetByApp(id) + a := loadAppInfo(pathSet.metaPath, func() {}) + if a.ID != id { + log.Printf("app %q claims to have identifier %q", id, a.ID) + return syscall.EBADE + } + + /* + Prepare nixGL. + */ + + if a.GPU && flagAutoDrivers { + withNixDaemon(ctx, "nix-gl", []string{ + "mkdir -p /nix/.nixGL/auto", + "rm -rf /nix/.nixGL/auto", + "export NIXPKGS_ALLOW_UNFREE=1", + "nix build --impure " + + "--out-link /nix/.nixGL/auto/opengl " + + "--override-input nixpkgs path:/etc/nixpkgs " + + "path:" + a.NixGL, + "nix build --impure " + + "--out-link /nix/.nixGL/auto/vulkan " + + "--override-input nixpkgs path:/etc/nixpkgs " + + "path:" + a.NixGL + "#nixVulkanNvidia", + }, true, func(config *hst.Config) *hst.Config { + config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{ + {Src: "/etc/resolv.conf"}, + {Src: "/sys/block"}, + {Src: "/sys/bus"}, + {Src: "/sys/class"}, + {Src: "/sys/dev"}, + {Src: "/sys/devices"}, + }...) + appendGPUFilesystem(config) + return config + }, a, pathSet, flagDropShellNixGL, func() {}) + } + + /* + Create app configuration. + */ + + argv := make([]string, 1, len(args)) + if !flagDropShell { + argv[0] = a.Launcher + } else { + argv[0] = shellPath + } + argv = append(argv, args[1:]...) + + config := a.toFst(pathSet, argv, flagDropShell) + + /* + Expose GPU devices. + */ + + if a.GPU { + config.Container.Filesystem = append(config.Container.Filesystem, + &hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")}) + appendGPUFilesystem(config) + } + + /* + Spawn app. + */ + + mustRunApp(ctx, config, func() {}) + return errSuccess + }). + Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build"). + Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection") + } + + c.MustParse(os.Args[1:], func(err error) { + hlog.Verbosef("command returned %v", err) + if errors.Is(err, errSuccess) { + hlog.BeforeExit() + os.Exit(0) + } + }) + log.Fatal("unreachable") +} |
