All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 33s
				
			Test / Sandbox (push) Successful in 2m13s
				
			Test / Hakurei (push) Successful in 3m3s
				
			Test / Hpkg (push) Successful in 3m57s
				
			Test / Sandbox (race detector) (push) Successful in 4m30s
				
			Test / Hakurei (race detector) (push) Successful in 5m16s
				
			Test / Flake checks (push) Successful in 1m20s
				
			This provides disambiguation from fhs.AbsTmp. Signed-off-by: Ophestra <cat@gensokyo.uk>
		
			
				
	
	
		
			336 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"os/signal"
 | |
| 	"path"
 | |
| 	"syscall"
 | |
| 
 | |
| 	"hakurei.app/command"
 | |
| 	"hakurei.app/container/check"
 | |
| 	"hakurei.app/container/fhs"
 | |
| 	"hakurei.app/hst"
 | |
| 	"hakurei.app/message"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	errSuccess = errors.New("success")
 | |
| )
 | |
| 
 | |
| func main() {
 | |
| 	log.SetPrefix("hpkg: ")
 | |
| 	log.SetFlags(0)
 | |
| 	msg := message.NewMsg(log.Default())
 | |
| 
 | |
| 	if err := os.Setenv("SHELL", pathShell.String()); err != nil {
 | |
| 		log.Fatalf("cannot set $SHELL: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	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 { msg.SwapVerbose(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 *check.Absolute
 | |
| 			if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
 | |
| 				log.Printf("cannot create temporary directory: %v", err)
 | |
| 				return err
 | |
| 			} else if workDir, err = check.NewAbs(p); err != nil {
 | |
| 				log.Printf("invalid temporary directory: %v", err)
 | |
| 				return err
 | |
| 			}
 | |
| 			cleanup := func() {
 | |
| 				// should be faster than a native implementation
 | |
| 				mustRun(msg, chmod, "-R", "+w", workDir.String())
 | |
| 				mustRun(msg, rm, "-rf", workDir.String())
 | |
| 			}
 | |
| 			beforeRunFail.Store(&cleanup)
 | |
| 
 | |
| 			mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
 | |
| 
 | |
| 			/*
 | |
| 				Parse bundle and app metadata, do pre-install checks.
 | |
| 			*/
 | |
| 
 | |
| 			bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
 | |
| 			pathSet := pathSetByApp(bundle.ID)
 | |
| 
 | |
| 			a := bundle
 | |
| 			if s, err := os.Stat(pathSet.metaPath.String()); 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.String(), 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
 | |
| 				msg.Verbosef("installing application %q version %q over local %q",
 | |
| 					bundle.ID, bundle.Version, a.Version)
 | |
| 			} else {
 | |
| 				msg.Verbosef("application %q clean installation", bundle.ID)
 | |
| 				// sec: should install credentials
 | |
| 			}
 | |
| 
 | |
| 			/*
 | |
| 				Setup steps for files owned by the target user.
 | |
| 			*/
 | |
| 
 | |
| 			withCacheDir(ctx, msg, "install", []string{
 | |
| 				// export inner bundle path in the environment
 | |
| 				"export BUNDLE=" + hst.PrivateTmp + "/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, msg, "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, msg, "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.String()+"~", 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.String()+"~", pathSet.metaPath.String()); 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.String(), 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, msg, "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.FilesystemConfigJSON{
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
 | |
| 						{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
 | |
| 					}...)
 | |
| 					appendGPUFilesystem(config)
 | |
| 					return config
 | |
| 				}, a, pathSet, flagDropShellNixGL, func() {})
 | |
| 			}
 | |
| 
 | |
| 			/*
 | |
| 				Create app configuration.
 | |
| 			*/
 | |
| 
 | |
| 			pathname := a.Launcher
 | |
| 			argv := make([]string, 1, len(args))
 | |
| 			if flagDropShell {
 | |
| 				pathname = pathShell
 | |
| 				argv[0] = bash
 | |
| 			} else {
 | |
| 				argv[0] = a.Launcher.String()
 | |
| 			}
 | |
| 			argv = append(argv, args[1:]...)
 | |
| 			config := a.toHst(pathSet, pathname, argv, flagDropShell)
 | |
| 
 | |
| 			/*
 | |
| 				Expose GPU devices.
 | |
| 			*/
 | |
| 
 | |
| 			if a.GPU {
 | |
| 				config.Container.Filesystem = append(config.Container.Filesystem,
 | |
| 					hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsPrivateTmp.Append("nixGL")}})
 | |
| 				appendGPUFilesystem(config)
 | |
| 			}
 | |
| 
 | |
| 			/*
 | |
| 				Spawn app.
 | |
| 			*/
 | |
| 
 | |
| 			mustRunApp(ctx, msg, 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) {
 | |
| 		msg.Verbosef("command returned %v", err)
 | |
| 		if errors.Is(err, errSuccess) {
 | |
| 			msg.BeforeExit()
 | |
| 			os.Exit(0)
 | |
| 		}
 | |
| 	})
 | |
| 	log.Fatal("unreachable")
 | |
| }
 |