From 756f140cb2f9b1dd0b083907bc60ecf74adcedc0 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Tue, 2 Jun 2026 09:02:19 +0700 Subject: [PATCH 1/7] skeleton implementation --- env.go | 58 ++++++++++++++++++++++++++++++++++++++ env/main.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ env/util.go | 53 +++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 4 +++ main.go | 13 +++++++++ 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 env.go create mode 100644 env/main.go create mode 100644 env/util.go diff --git a/env.go b/env.go new file mode 100644 index 0000000..15f7b77 --- /dev/null +++ b/env.go @@ -0,0 +1,58 @@ +// um env is a feature for ultramarine bootc specifically that lets you build local derivations "transactionally" +// +// it works similar to what you would do with `cargo add` or `pnpm add` would do, as in it: +// +// - adds the desired system package into a manifest file (`environment.toml`) +// - rebuilds the system derivation with the changes using Podman +// - optionally, attempt to apply the changes to the running system (unstable, may not work depending on the system's state) +// - use `bootc switch` to apply the update transactionally, marking the new local build as the next default +// +// Requires Podman to be already installed on the base image, or installed temporarily on the ephemeral environment (via `bootc usr-overlay`) + +package main + +// import ( +// "fmt" + +// "github.com/charmbracelet/huh" +// "github.com/urfave/cli/v2" +// "github.com/BurntSushi/toml" +// ) +// + +import ( + "fmt" + + "github.com/Ultramarine-Linux/um/env" + "github.com/urfave/cli/v2" +) + + +// todo: don't just print image name lol +func envStatus(c *cli.Context) error { + image, err := env.GetBootcImage() + if err != nil { + return err + } + + fmt.Println("Bootc image:", image) + return nil +} + +func envApplyChanges(c *cli.Context) error { + fmt.Println("Applying changes...") + + manifest, err := env.LoadEnvManifest() + if err != nil { + return err + } + + err = manifest.ApplyChanges() + if err != nil { + return err + } + + fmt.Println("Changes applied successfully.") + + return nil +} diff --git a/env/main.go b/env/main.go new file mode 100644 index 0000000..98f25f5 --- /dev/null +++ b/env/main.go @@ -0,0 +1,80 @@ +// um env library stuff + +package env + +import ( + "log" + "os" + "os/exec" + + "github.com/BurntSushi/toml" +) + +const umEnvContext = "/var/um/env" +const umEnvManifest = umEnvContext + "/environment.toml" + +// stuff to do in the environment +type EnvManifest struct { + // [packages] + Packages Packages `toml:"packages"` +} + +type Packages struct { + Install []string `toml:"install"` + Remove []string `toml:"remove"` + Reinstall []string `toml:"reinstall"` +} + +func LoadEnvManifest() (*EnvManifest, error) { + // load from file + data, err := os.ReadFile(umEnvManifest) + if err != nil { + return nil, err + } + // set CWD + if err := os.Chdir(umEnvContext); err != nil { + return nil, err + } + + var manifest EnvManifest + if err := toml.Unmarshal(data, &manifest); err != nil { + return nil, err + } + + return &manifest, nil +} + +func appendPackageAction(args []string, action string, packages []string) []string { + if len(packages) == 0 { + return args + } + + args = append(args, "--action="+action) + args = append(args, packages...) + + return args +} + +func (p *Packages) CommitTransaction() error { + args := []string{"do"} + args = appendPackageAction(args, "install", p.Install) + args = appendPackageAction(args, "remove", p.Remove) + args = appendPackageAction(args, "reinstall", p.Reinstall) + + if len(args) == 1 { + log.Println("no package changes to apply") + return nil + } + + cmd := exec.Command("dnf", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() + +} + +// Apply changes to the environment +func (e *EnvManifest) ApplyChanges() error { + return e.Packages.CommitTransaction() +} diff --git a/env/util.go b/env/util.go new file mode 100644 index 0000000..382086c --- /dev/null +++ b/env/util.go @@ -0,0 +1,53 @@ +package env + +import ( + "bytes" + "errors" + "os" + "os/exec" + "strings" + + "github.com/Ultramarine-Linux/um/pkg/util" + "go.yaml.in/yaml/v4" +) + +// minimal struct to parse `bootc edit` output +type bootcEditSpec struct { + Spec struct { + Image struct { + Image string `yaml:"image"` + } `yaml:"image"` + } `yaml:"spec"` +} + +func GetBootcImage() (string, error) { + util.SudoIfNeeded([]string{}) + + // run `bootc edit` and pipe output + // which then gets parsed by YAML + + bootcEditCmd := exec.Command("bootc", "edit") + bootcEditCmd.Env = append(os.Environ(), + "EDITOR=cat", + ) + + bootcEditOutput, err := bootcEditCmd.Output() + + if err != nil { + return "", err + } + + bootcEditOutput = bytes.TrimSpace(bootcEditOutput) + bootcEditOutput = []byte(strings.TrimSuffix(string(bootcEditOutput), "\nEdit cancelled, no changes made.")) + + var bootcEditParsed bootcEditSpec + if err := yaml.Unmarshal(bootcEditOutput, &bootcEditParsed); err != nil { + return "", err + } + + if bootcEditParsed.Spec.Image.Image == "" { + return "", errors.New("bootc edit output did not contain spec.image.image") + } + + return bootcEditParsed.Spec.Image.Image, nil +} diff --git a/go.mod b/go.mod index 3f6aad5..12a8813 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,11 @@ require ( github.com/mackerelio/go-osstat v0.2.5 github.com/samber/lo v1.49.1 github.com/urfave/cli/v2 v2.27.6 - go.yaml.in/yaml/v4 v4.0.0-rc.2 + go.yaml.in/yaml/v4 v4.0.0-rc.4 ) require ( + github.com/BurntSushi/toml v1.6.0 // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/go.sum b/go.sum index 42da2c2..bf53597 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= @@ -100,6 +102,8 @@ go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= diff --git a/main.go b/main.go index 73df016..923e765 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,19 @@ func runCli() error { }, }, + { + Name: "env", + Usage: "Manage local bootc derivations", + Action: envStatus, + Subcommands: []*cli.Command{ + { + Name: "apply-changes", + Usage: "Apply pending changes to the bootc environment", + Action: envApplyChanges, + }, + }, + }, + // { // Name: "experiments", // Usage: "manage Ultramarine Linux experiments, a preview of features to come", From e0b3ac0bd89d532a31e8ba6044e31a72d89fd2f8 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Tue, 2 Jun 2026 09:53:25 +0700 Subject: [PATCH 2/7] init templates, umpkg add --- env.go | 61 ++++++++++++++++++++++- env/Containerfile.gotmpl | 4 ++ env/main.go | 102 +++++++++++++++++++++++++++++++++++++++ main.go | 19 +++++++- 4 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 env/Containerfile.gotmpl diff --git a/env.go b/env.go index 15f7b77..0f860f8 100644 --- a/env.go +++ b/env.go @@ -24,10 +24,10 @@ import ( "fmt" "github.com/Ultramarine-Linux/um/env" + "github.com/Ultramarine-Linux/um/pkg/util" "github.com/urfave/cli/v2" ) - // todo: don't just print image name lol func envStatus(c *cli.Context) error { image, err := env.GetBootcImage() @@ -53,6 +53,63 @@ func envApplyChanges(c *cli.Context) error { } fmt.Println("Changes applied successfully.") - + + return nil +} + +// initializes a new environment by creating an environment.toml and a template Containerfile +func envInit(c *cli.Context) error { + baseImage, err := env.GetBootcImage() + if err != nil { + return err + } + + if err := env.InitEnvironment(baseImage); err != nil { + return err + } + + fmt.Println("Initialized environment in /var/um/env") + + return nil +} + +func buildEnv(c *cli.Context) error { + util.SudoIfNeeded([]string{}) + manifest, err := env.LoadEnvManifest() + if err != nil { + return err + } + + if err := manifest.BuildContainerfile(); err != nil { + return err + } + + fmt.Println("Containerfile built successfully.") + + return nil +} + +// add a package to the environment +func envAddPackage(c *cli.Context) error { + util.SudoIfNeeded([]string{}) + manifest, err := env.LoadEnvManifest() + if err != nil { + return err + } + + for _, pkg := range c.Args().Slice() { + if manifest.AddPackage(pkg) { + fmt.Println("Adding package:", pkg) + } else { + fmt.Println("Package already exists:", pkg) + } + } + + if err := manifest.Save(); err != nil { + return err + } + + fmt.Println("Package added successfully.") + return nil } diff --git a/env/Containerfile.gotmpl b/env/Containerfile.gotmpl new file mode 100644 index 0000000..6e9125b --- /dev/null +++ b/env/Containerfile.gotmpl @@ -0,0 +1,4 @@ +FROM {{ .BaseImage }} + +# Insert your changes here, the following RUN directive commits changes from environment.toml +RUN um env apply-changes diff --git a/env/main.go b/env/main.go index 98f25f5..f4d226f 100644 --- a/env/main.go +++ b/env/main.go @@ -3,15 +3,24 @@ package env import ( + _ "embed" "log" "os" "os/exec" + "text/template" "github.com/BurntSushi/toml" ) const umEnvContext = "/var/um/env" const umEnvManifest = umEnvContext + "/environment.toml" +const umEnvContainerfile = umEnvContext + "/Containerfile" +const umEnvManagedImage = "localhost/um-env" + +//go:embed Containerfile.gotmpl +var containerfileTemplateSource string + +var containerfileTemplate = template.Must(template.New("Containerfile").Parse(containerfileTemplateSource)) // stuff to do in the environment type EnvManifest struct { @@ -25,6 +34,58 @@ type Packages struct { Reinstall []string `toml:"reinstall"` } +type containerfileTemplateData struct { + BaseImage string +} + +func containsPackage(packages []string, packageName string) bool { + for _, existing := range packages { + if existing == packageName { + return true + } + } + + return false +} + +func InitEnvironment(baseImage string) error { + if err := os.MkdirAll(umEnvContext, 0o755); err != nil { + return err + } + + manifest := EnvManifest{ + Packages: Packages{ + Install: []string{}, + Remove: []string{}, + Reinstall: []string{}, + }, + } + + manifestFile, err := os.OpenFile(umEnvManifest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return err + } + defer manifestFile.Close() + + if err := toml.NewEncoder(manifestFile).Encode(manifest); err != nil { + return err + } + + containerfile, err := os.OpenFile(umEnvContainerfile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + if err != nil { + return err + } + defer containerfile.Close() + + if err := containerfileTemplate.Execute(containerfile, containerfileTemplateData{ + BaseImage: baseImage, + }); err != nil { + return err + } + + return nil +} + func LoadEnvManifest() (*EnvManifest, error) { // load from file data, err := os.ReadFile(umEnvManifest) @@ -44,6 +105,25 @@ func LoadEnvManifest() (*EnvManifest, error) { return &manifest, nil } +// saves the manifest to the specified path +func (e *EnvManifest) Save() error { + data, err := toml.Marshal(e) + if err != nil { + return err + } + return os.WriteFile(umEnvManifest, data, 0o644) +} + +func (e *EnvManifest) AddPackage(packageName string) bool { + if containsPackage(e.Packages.Install, packageName) { + return false + } + + e.Packages.Install = append(e.Packages.Install, packageName) + + return true +} + func appendPackageAction(args []string, action string, packages []string) []string { if len(packages) == 0 { return args @@ -78,3 +158,25 @@ func (p *Packages) CommitTransaction() error { func (e *EnvManifest) ApplyChanges() error { return e.Packages.CommitTransaction() } + +func (e *EnvManifest) BuildContainerfile() error { + // run podman build -t + podmanArgs := []string{ + "build", + "--pull=newer", + "-f", + umEnvContainerfile, + "-t", + umEnvManagedImage, + umEnvContext, + } + cmd := exec.Command("podman", podmanArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/main.go b/main.go index 923e765..edb9efa 100644 --- a/main.go +++ b/main.go @@ -81,10 +81,25 @@ func runCli() error { }, { - Name: "env", - Usage: "Manage local bootc derivations", + Name: "env", + Usage: "Manage local bootc derivations", Action: envStatus, Subcommands: []*cli.Command{ + { + Name: "init", + Usage: "Initialize the bootc environment manifest and Containerfile", + Action: envInit, + }, + { + Name: "build", + Usage: "Build the local derivation from a Containerfile", + Action: buildEnv, + }, + { + Name: "add", + Usage: "Add a package to the environment", + Action: envAddPackage, + }, { Name: "apply-changes", Usage: "Apply pending changes to the bootc environment", From c4c629da9e1151a437258f4b5d8a5d666f1d4f99 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Tue, 2 Jun 2026 10:51:59 +0700 Subject: [PATCH 3/7] implement updates! --- env.go | 60 ++++++++++++++++++++++++++++++++++++++-- env/Containerfile.gotmpl | 7 ++++- env/main.go | 49 +++++++++++++++++--------------- main.go | 12 +++++--- 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/env.go b/env.go index 0f860f8..556baaf 100644 --- a/env.go +++ b/env.go @@ -22,9 +22,12 @@ package main import ( "fmt" + "os" + "os/exec" "github.com/Ultramarine-Linux/um/env" "github.com/Ultramarine-Linux/um/pkg/util" + "github.com/charmbracelet/huh" "github.com/urfave/cli/v2" ) @@ -59,21 +62,52 @@ func envApplyChanges(c *cli.Context) error { // initializes a new environment by creating an environment.toml and a template Containerfile func envInit(c *cli.Context) error { + var confirmed bool + baseImage, err := env.GetBootcImage() if err != nil { return err } + if err := huh.NewConfirm(). + Title("Initialize the local bootc derivation?"). + Description(fmt.Sprintf("This will create environment.toml and a template Containerfile based on `%s` at `%s`."+ + "\n\n"+ + "The system bootc image will be switched to `%s` and updates must now be managed via `um env update`.", baseImage, env.UmEnvContext, env.UmEnvManagedImage)). + Affirmative("Initialize"). + Negative("Cancel"). + Value(&confirmed). + Run(); err != nil { + return err + } + + if !confirmed { + fmt.Println("Aborting...") + return nil + } + if err := env.InitEnvironment(baseImage); err != nil { return err } - fmt.Println("Initialized environment in /var/um/env") + fmt.Println("Initialized environment in", env.UmEnvContext) + + fmt.Println("Building initial derivation...") + + if err := envBuild(c); err != nil { + return err + } + + fmt.Println("Initial derivation built successfully, switching to um managed image...") + + if err := env.EnvBootcSwitch(); err != nil { + return err + } return nil } -func buildEnv(c *cli.Context) error { +func envBuild(c *cli.Context) error { util.SudoIfNeeded([]string{}) manifest, err := env.LoadEnvManifest() if err != nil { @@ -113,3 +147,25 @@ func envAddPackage(c *cli.Context) error { return nil } + +func envUpdate(c *cli.Context) error { + util.SudoIfNeeded([]string{}) + + if err := envBuild(c); err != nil { + return err + } + + fmt.Println("Rebuilt environment image, applying changes with bootc") + + // bootc update actually pulls straight from containers-storage, + // so we can simply just do this instead of bootc switch again, assuming + // um env is already initialized + bootcCmd := exec.Command("bootc", "update") + bootcCmd.Stdout = os.Stdout + bootcCmd.Stderr = os.Stderr + if err := bootcCmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/env/Containerfile.gotmpl b/env/Containerfile.gotmpl index 6e9125b..e6dad30 100644 --- a/env/Containerfile.gotmpl +++ b/env/Containerfile.gotmpl @@ -1,4 +1,9 @@ FROM {{ .BaseImage }} +# Copy environment files into the image for reproducibility +RUN mkdir -p /var/um/env +COPY . /var/um/env + # Insert your changes here, the following RUN directive commits changes from environment.toml -RUN um env apply-changes +RUN --mount=type=cache,target=/var/cache \ + um env apply-changes diff --git a/env/main.go b/env/main.go index f4d226f..fe0bf79 100644 --- a/env/main.go +++ b/env/main.go @@ -9,13 +9,15 @@ import ( "os/exec" "text/template" + "slices" + "github.com/BurntSushi/toml" ) -const umEnvContext = "/var/um/env" -const umEnvManifest = umEnvContext + "/environment.toml" -const umEnvContainerfile = umEnvContext + "/Containerfile" -const umEnvManagedImage = "localhost/um-env" +const UmEnvContext = "/var/um/env" +const UmEnvManifest = UmEnvContext + "/environment.toml" +const UmEnvContainerfile = UmEnvContext + "/Containerfile" +const UmEnvManagedImage = "localhost/um-env" //go:embed Containerfile.gotmpl var containerfileTemplateSource string @@ -39,17 +41,11 @@ type containerfileTemplateData struct { } func containsPackage(packages []string, packageName string) bool { - for _, existing := range packages { - if existing == packageName { - return true - } - } - - return false + return slices.Contains(packages, packageName) } func InitEnvironment(baseImage string) error { - if err := os.MkdirAll(umEnvContext, 0o755); err != nil { + if err := os.MkdirAll(UmEnvContext, 0o755); err != nil { return err } @@ -61,7 +57,7 @@ func InitEnvironment(baseImage string) error { }, } - manifestFile, err := os.OpenFile(umEnvManifest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + manifestFile, err := os.OpenFile(UmEnvManifest, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return err } @@ -71,7 +67,7 @@ func InitEnvironment(baseImage string) error { return err } - containerfile, err := os.OpenFile(umEnvContainerfile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + containerfile, err := os.OpenFile(UmEnvContainerfile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return err } @@ -88,12 +84,12 @@ func InitEnvironment(baseImage string) error { func LoadEnvManifest() (*EnvManifest, error) { // load from file - data, err := os.ReadFile(umEnvManifest) + data, err := os.ReadFile(UmEnvManifest) if err != nil { return nil, err } // set CWD - if err := os.Chdir(umEnvContext); err != nil { + if err := os.Chdir(UmEnvContext); err != nil { return nil, err } @@ -111,7 +107,7 @@ func (e *EnvManifest) Save() error { if err != nil { return err } - return os.WriteFile(umEnvManifest, data, 0o644) + return os.WriteFile(UmEnvManifest, data, 0o644) } func (e *EnvManifest) AddPackage(packageName string) bool { @@ -136,12 +132,12 @@ func appendPackageAction(args []string, action string, packages []string) []stri } func (p *Packages) CommitTransaction() error { - args := []string{"do"} + args := []string{"do", "-y"} args = appendPackageAction(args, "install", p.Install) args = appendPackageAction(args, "remove", p.Remove) args = appendPackageAction(args, "reinstall", p.Reinstall) - if len(args) == 1 { + if len(args) == 2 { log.Println("no package changes to apply") return nil } @@ -165,10 +161,10 @@ func (e *EnvManifest) BuildContainerfile() error { "build", "--pull=newer", "-f", - umEnvContainerfile, + UmEnvContainerfile, "-t", - umEnvManagedImage, - umEnvContext, + UmEnvManagedImage, + UmEnvContext, } cmd := exec.Command("podman", podmanArgs...) cmd.Stdout = os.Stdout @@ -180,3 +176,12 @@ func (e *EnvManifest) BuildContainerfile() error { return nil } + +func EnvBootcSwitch() error { + args := []string{"switch", "--transport=containers-storage", UmEnvManagedImage} + cmd := exec.Command("bootc", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/main.go b/main.go index edb9efa..7175d1c 100644 --- a/main.go +++ b/main.go @@ -81,9 +81,8 @@ func runCli() error { }, { - Name: "env", - Usage: "Manage local bootc derivations", - Action: envStatus, + Name: "env", + Usage: "Manage local bootc derivations", Subcommands: []*cli.Command{ { Name: "init", @@ -93,7 +92,7 @@ func runCli() error { { Name: "build", Usage: "Build the local derivation from a Containerfile", - Action: buildEnv, + Action: envBuild, }, { Name: "add", @@ -105,6 +104,11 @@ func runCli() error { Usage: "Apply pending changes to the bootc environment", Action: envApplyChanges, }, + { + Name: "update", + Usage: "Update the base image and rebuild the environment", + Action: envUpdate, + }, }, }, From 114c6c37e8e2657f84de575c1639a5d439b17c92 Mon Sep 17 00:00:00 2001 From: Pornpipat Popum Date: Tue, 2 Jun 2026 10:55:12 +0700 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- env/main.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/env/main.go b/env/main.go index fe0bf79..cc2859a 100644 --- a/env/main.go +++ b/env/main.go @@ -44,7 +44,7 @@ func containsPackage(packages []string, packageName string) bool { return slices.Contains(packages, packageName) } -func InitEnvironment(baseImage string) error { +func InitEnvironment(baseImage string) (err error) { if err := os.MkdirAll(UmEnvContext, 0o755); err != nil { return err } @@ -61,7 +61,15 @@ func InitEnvironment(baseImage string) error { if err != nil { return err } - defer manifestFile.Close() + defer func() { + if closeErr := manifestFile.Close(); closeErr != nil { + if err == nil { + err = closeErr + } else { + log.Printf("failed to close %s: %v", UmEnvManifest, closeErr) + } + } + }() if err := toml.NewEncoder(manifestFile).Encode(manifest); err != nil { return err @@ -71,7 +79,15 @@ func InitEnvironment(baseImage string) error { if err != nil { return err } - defer containerfile.Close() + defer func() { + if closeErr := containerfile.Close(); closeErr != nil { + if err == nil { + err = closeErr + } else { + log.Printf("failed to close %s: %v", UmEnvContainerfile, closeErr) + } + } + }() if err := containerfileTemplate.Execute(containerfile, containerfileTemplateData{ BaseImage: baseImage, From e967731423cddd28fcc36aff9950707b987a4b99 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Wed, 3 Jun 2026 01:03:26 +0700 Subject: [PATCH 5/7] add live apply for env add --- env.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/env.go b/env.go index 556baaf..905a854 100644 --- a/env.go +++ b/env.go @@ -145,6 +145,27 @@ func envAddPackage(c *cli.Context) error { fmt.Println("Package added successfully.") + var applyLive bool + if c.Bool("apply-live") { + applyLive = true + } + + if applyLive { + fmt.Println("Applying changes live...") + + // enable bootc usr-overlay + + if err := exec.Command("bootc", "usr-overlay").Run(); err != nil { + return err + } + + if err := envApplyChanges(c); err != nil { + return err + } + } + + fmt.Println("Added packages successfully. Commit pending changes with `um env update`") + return nil } From 5357a6dcfd4a229d53a3105d6e33b1748e73ab1a Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Wed, 3 Jun 2026 01:13:14 +0700 Subject: [PATCH 6/7] add remove verb --- env.go | 65 +++++++++++++++++++++++++++++++++++++++++------------ env/main.go | 31 +++++++++++++++++++++++++ main.go | 5 +++++ 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/env.go b/env.go index 905a854..8cf0cec 100644 --- a/env.go +++ b/env.go @@ -123,6 +123,29 @@ func envBuild(c *cli.Context) error { return nil } +func handleApplyLive(c *cli.Context) error { + var applyLive bool + if c.Bool("apply-live") { + applyLive = true + } + + if applyLive { + fmt.Println("Applying changes live...") + + // enable bootc usr-overlay + + if err := exec.Command("bootc", "usr-overlay").Run(); err != nil { + return err + } + + if err := envApplyChanges(c); err != nil { + return err + } + } + + return nil +} + // add a package to the environment func envAddPackage(c *cli.Context) error { util.SudoIfNeeded([]string{}) @@ -143,28 +166,42 @@ func envAddPackage(c *cli.Context) error { return err } - fmt.Println("Package added successfully.") - - var applyLive bool - if c.Bool("apply-live") { - applyLive = true + if err := handleApplyLive(c); err != nil { + return err } - if applyLive { - fmt.Println("Applying changes live...") + fmt.Println("Packages added successfully.") - // enable bootc usr-overlay + fmt.Println("Added packages successfully. Commit pending changes with `um env update`") - if err := exec.Command("bootc", "usr-overlay").Run(); err != nil { - return err - } + return nil +} - if err := envApplyChanges(c); err != nil { - return err +func envRemovePackage(c *cli.Context) error { + util.SudoIfNeeded([]string{}) + manifest, err := env.LoadEnvManifest() + if err != nil { + return err + } + + for _, pkg := range c.Args().Slice() { + if manifest.RemovePackage(pkg) { + fmt.Println("Removed package from install list:", pkg) + } else { + fmt.Println("Adding package to removal list:", pkg) } } - fmt.Println("Added packages successfully. Commit pending changes with `um env update`") + if err := manifest.Save(); err != nil { + return err + } + + if err := handleApplyLive(c); err != nil { + return err + } + + fmt.Println("Packages removed successfully.") + fmt.Println("Committed pending changes with `um env update`") return nil } diff --git a/env/main.go b/env/main.go index cc2859a..f325b8b 100644 --- a/env/main.go +++ b/env/main.go @@ -44,6 +44,12 @@ func containsPackage(packages []string, packageName string) bool { return slices.Contains(packages, packageName) } +func removePackage(packages []string, packageName string) []string { + return slices.DeleteFunc(packages, func(existing string) bool { + return existing == packageName + }) +} + func InitEnvironment(baseImage string) (err error) { if err := os.MkdirAll(UmEnvContext, 0o755); err != nil { return err @@ -136,6 +142,31 @@ func (e *EnvManifest) AddPackage(packageName string) bool { return true } +func (e *EnvManifest) ReinstallPackage(packageName string) bool { + if containsPackage(e.Packages.Reinstall, packageName) { + return false + } + + e.Packages.Reinstall = append(e.Packages.Reinstall, packageName) + + return true +} + +func (e *EnvManifest) RemovePackage(packageName string) bool { + // if package is in the add list, simply remove it + if containsPackage(e.Packages.Install, packageName) { + e.Packages.Install = removePackage(e.Packages.Install, packageName) + return true + } + + // if package is not, then add it to the remove list + if !containsPackage(e.Packages.Remove, packageName) { + e.Packages.Remove = append(e.Packages.Remove, packageName) + } + + return false +} + func appendPackageAction(args []string, action string, packages []string) []string { if len(packages) == 0 { return args diff --git a/main.go b/main.go index 7175d1c..54e0f7f 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,11 @@ func runCli() error { Usage: "Add a package to the environment", Action: envAddPackage, }, + { + Name: "remove", + Usage: "Remove a package from the environment", + Action: envRemovePackage, + }, { Name: "apply-changes", Usage: "Apply pending changes to the bootc environment", From 8740ce0a02afc4eca8c1d7987f1c3708190f960d Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Wed, 3 Jun 2026 01:14:25 +0700 Subject: [PATCH 7/7] typo --- env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.go b/env.go index 8cf0cec..cd546c0 100644 --- a/env.go +++ b/env.go @@ -201,7 +201,7 @@ func envRemovePackage(c *cli.Context) error { } fmt.Println("Packages removed successfully.") - fmt.Println("Committed pending changes with `um env update`") + fmt.Println("Commit pending changes with `um env update`") return nil }