From ebb58ddda65ae0953c53b84bc951ac127cd6fe4f Mon Sep 17 00:00:00 2001 From: guillermodotn Date: Thu, 7 May 2026 08:46:05 +0000 Subject: [PATCH 1/2] main: default to XDG cache directory for non-root users --- cmd/image-builder/export_test.go | 1 + cmd/image-builder/main.go | 28 ++++++++++++++++++++++++++-- cmd/image-builder/main_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/cmd/image-builder/export_test.go b/cmd/image-builder/export_test.go index 55371648..1087380c 100644 --- a/cmd/image-builder/export_test.go +++ b/cmd/image-builder/export_test.go @@ -19,6 +19,7 @@ var ( DescribeImage = describeImage ProgressFromCmd = progressFromCmd BasenameFor = basenameFor + DefaultCacheDir = defaultCacheDir ) type DescribeImgYAML describeImgYAML diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 7a765ef3..2ab1497b 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -9,6 +9,7 @@ import ( "log" "os" "os/signal" + "path/filepath" "strings" "syscall" @@ -37,6 +38,25 @@ var ( osStderr io.Writer = os.Stderr ) +// defaultCacheDir returns the default cache directory for osbuild +// intermediate build artifacts. When running as root it uses the +// system-wide /var/cache path. When running as a non-root user it +// follows the XDG Base Directory specification and falls back to +// ~/.cache. +func defaultCacheDir() string { + if os.Getuid() == 0 { + return "/var/cache/image-builder/store" + } + if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" { + return filepath.Join(cacheHome, "image-builder", "store") + } + home, err := os.UserHomeDir() + if err != nil { + return "/var/cache/image-builder/store" + } + return filepath.Join(home, ".cache", "image-builder", "store") +} + // basenameFor returns the basename for directory and filenames // for the given imageType. This can be user overriden via userBasename. func basenameFor(img *imagefilter.Result, userBasename string) string { @@ -434,7 +454,11 @@ func cmdBuild(cmd *cobra.Command, args []string) error { if err != nil { return err } - // XXX: check env here, i.e. if user is root and osbuild is installed + // Fail early if the cache directory is not writable, instead of + // waiting for osbuild to fail after slow manifest generation. + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return fmt.Errorf("cannot create cache directory %q: %w\nHint: use --cache to specify a writable path", cacheDir, err) + } // Setup osbuild environment if running in a container if setup.IsContainer() { @@ -697,7 +721,7 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support. buildCmd.Flags().AddFlagSet(manifestCmd.Flags()) buildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`) buildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`) - buildCmd.Flags().String("cache", "/var/cache/image-builder/store", `osbuild directory to cache intermediate build artifacts"`) + buildCmd.Flags().String("cache", defaultCacheDir(), `osbuild directory to cache intermediate build artifacts"`) // XXX: add "--verbose" here, similar to how bib is doing this // (see https://github.com/osbuild/bootc-image-builder/pull/790/commits/5cec7ffd8a526e2ca1e8ada0ea18f927695dfe43) buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)") diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index 05ae3418..ac6e0429 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -1115,3 +1115,29 @@ customizations.FIPS = true }) } } + +func TestDefaultCacheDirAsRoot(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("test requires running as root") + } + assert.Equal(t, "/var/cache/image-builder/store", main.DefaultCacheDir()) +} + +func TestDefaultCacheDirNonRootXDG(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("test requires running as non-root") + } + t.Setenv("XDG_CACHE_HOME", "/tmp/test-xdg-cache") + assert.Equal(t, "/tmp/test-xdg-cache/image-builder/store", main.DefaultCacheDir()) +} + +func TestDefaultCacheDirNonRootFallback(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("test requires running as non-root") + } + t.Setenv("XDG_CACHE_HOME", "") + home, err := os.UserHomeDir() + require.NoError(t, err) + expected := filepath.Join(home, ".cache", "image-builder", "store") + assert.Equal(t, expected, main.DefaultCacheDir()) +} From 900b6f8852c76c7627372ad260e8c28b4002f90a Mon Sep 17 00:00:00 2001 From: guillermodotn Date: Fri, 15 May 2026 10:20:57 +0000 Subject: [PATCH 2/2] main: refactor defaultCacheDir to make tests uid-independent --- cmd/image-builder/export_test.go | 2 +- cmd/image-builder/main.go | 18 +++++++++++------- cmd/image-builder/main_test.go | 21 ++++++--------------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/cmd/image-builder/export_test.go b/cmd/image-builder/export_test.go index 1087380c..a9a9a793 100644 --- a/cmd/image-builder/export_test.go +++ b/cmd/image-builder/export_test.go @@ -19,7 +19,7 @@ var ( DescribeImage = describeImage ProgressFromCmd = progressFromCmd BasenameFor = basenameFor - DefaultCacheDir = defaultCacheDir + CacheDirForUid = cacheDirForUid ) type DescribeImgYAML describeImgYAML diff --git a/cmd/image-builder/main.go b/cmd/image-builder/main.go index 2ab1497b..446ca48e 100644 --- a/cmd/image-builder/main.go +++ b/cmd/image-builder/main.go @@ -38,13 +38,12 @@ var ( osStderr io.Writer = os.Stderr ) -// defaultCacheDir returns the default cache directory for osbuild -// intermediate build artifacts. When running as root it uses the -// system-wide /var/cache path. When running as a non-root user it -// follows the XDG Base Directory specification and falls back to -// ~/.cache. -func defaultCacheDir() string { - if os.Getuid() == 0 { +// cacheDirForUid returns the cache directory for the given uid. +// When root (uid 0) it uses the system-wide /var/cache path. +// When non-root it follows the XDG Base Directory specification +// and falls back to ~/.cache. +func cacheDirForUid(uid int) string { + if uid == 0 { return "/var/cache/image-builder/store" } if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" { @@ -57,6 +56,11 @@ func defaultCacheDir() string { return filepath.Join(home, ".cache", "image-builder", "store") } +// defaultCacheDir returns the cache directory for the current user. +func defaultCacheDir() string { + return cacheDirForUid(os.Getuid()) +} + // basenameFor returns the basename for directory and filenames // for the given imageType. This can be user overriden via userBasename. func basenameFor(img *imagefilter.Result, userBasename string) string { diff --git a/cmd/image-builder/main_test.go b/cmd/image-builder/main_test.go index ac6e0429..10515c60 100644 --- a/cmd/image-builder/main_test.go +++ b/cmd/image-builder/main_test.go @@ -1116,28 +1116,19 @@ customizations.FIPS = true } } -func TestDefaultCacheDirAsRoot(t *testing.T) { - if os.Getuid() != 0 { - t.Skip("test requires running as root") - } - assert.Equal(t, "/var/cache/image-builder/store", main.DefaultCacheDir()) +func TestCacheDirForUidRoot(t *testing.T) { + assert.Equal(t, "/var/cache/image-builder/store", main.CacheDirForUid(0)) } -func TestDefaultCacheDirNonRootXDG(t *testing.T) { - if os.Getuid() == 0 { - t.Skip("test requires running as non-root") - } +func TestCacheDirForUidNonRootXDG(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "/tmp/test-xdg-cache") - assert.Equal(t, "/tmp/test-xdg-cache/image-builder/store", main.DefaultCacheDir()) + assert.Equal(t, "/tmp/test-xdg-cache/image-builder/store", main.CacheDirForUid(1000)) } -func TestDefaultCacheDirNonRootFallback(t *testing.T) { - if os.Getuid() == 0 { - t.Skip("test requires running as non-root") - } +func TestCacheDirForUidNonRootFallback(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") home, err := os.UserHomeDir() require.NoError(t, err) expected := filepath.Join(home, ".cache", "image-builder", "store") - assert.Equal(t, expected, main.DefaultCacheDir()) + assert.Equal(t, expected, main.CacheDirForUid(1000)) }