aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/hpkg/main.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-07-31 23:57:11 +0900
committerOphestra <cat@gensokyo.uk>2025-07-31 23:57:11 +0900
commita8a79a866469c281f5c15daa7b2bd1de91f1d99c (patch)
treea43ec55565b9207b813528ac21da6c07ad2f5bc3 /cmd/hpkg/main.go
parent3ae0cec000408562452703b2c0faad42260e3b53 (diff)
cmd/hpkg: rename from planterette
Planterette is now developed in another repository, so rename this proof of concept to avoid confusion. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd/hpkg/main.go')
-rw-r--r--cmd/hpkg/main.go333
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")
+}