diff --git a/internal/application/application.go b/internal/application/application.go index b0ab94c..1bfed5e 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -150,7 +150,7 @@ func Run(bundled fs.FS) { if err != nil { panic(err) } - if err := assets.Provision(bundled, config.AssetsDir); err != nil { + if err := assets.ProvisionAssets(bundled, config.AssetsDir); err != nil { logger.Error("failed to provision bundled assets into %s: %v", config.AssetsDir, err) } diff --git a/internal/assets/assets.go b/internal/assets/assets.go index 6f1d68a..7a254fa 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -11,6 +11,25 @@ import ( "path/filepath" ) +// embeddedRoot is the directory in the source tree (and therefore in the +// embedded FS) where the bundled assets live. It mirrors the nix store +// layout, but consumers expect the assets directly under the assets dir, +// so this root is stripped during provisioning. +const embeddedRoot = "share/radiantwave" + +// ProvisionAssets provisions the bundled static assets (fonts, shaders, and +// terms-of-service pages) into destDir, stripping the embedded +// share/radiantwave root so the files land directly in destDir rather than +// in a nested share/radiantwave/ subdirectory. Existing files are +// overwritten so the deployed assets always match the running binary. +func ProvisionAssets(bundled fs.FS, destDir string) error { + root, err := fs.Sub(bundled, embeddedRoot) + if err != nil { + return fmt.Errorf("bundled assets missing %s root: %w", embeddedRoot, err) + } + return Provision(root, destDir) +} + // Provision copies every file from the embedded filesystem into destDir, // preserving the relative directory structure. Existing files are // overwritten so the deployed assets always match the running binary. diff --git a/internal/assets/assets_test.go b/internal/assets/assets_test.go index 7266d96..040effd 100644 --- a/internal/assets/assets_test.go +++ b/internal/assets/assets_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "testing" + "testing/fstest" ) //go:embed testdata @@ -38,6 +39,40 @@ func TestProvisionCopiesBundledFiles(t *testing.T) { } } +// TestProvisionAssetsStripsEmbeddedRoot is a regression test: provisioning +// used to preserve the embedded share/radiantwave root, so assets landed in +// /share/radiantwave/fonts instead of /fonts. ProvisionAssets +// strips that root so consumers find the assets directly under the assets dir. +func TestProvisionAssetsStripsEmbeddedRoot(t *testing.T) { + fsys := fstest.MapFS{ + "share/radiantwave/fonts/TestFont.ttf": &fstest.MapFile{Data: []byte("font")}, + "share/radiantwave/shaders/text.frag": &fstest.MapFile{Data: []byte("frag")}, + "share/radiantwave/shaders/text.vert": &fstest.MapFile{Data: []byte("vert")}, + "share/radiantwave/tos/01 - WELCOME.html": &fstest.MapFile{Data: []byte("welcome")}, + } + dest := t.TempDir() + + if err := ProvisionAssets(fsys, dest); err != nil { + t.Fatalf("ProvisionAssets returned error: %v", err) + } + + // Assets must land directly under dest, not under dest/share/radiantwave. + for _, rel := range []string{ + "fonts/TestFont.ttf", + "shaders/text.frag", + "shaders/text.vert", + "tos/01 - WELCOME.html", + } { + if _, err := os.Stat(filepath.Join(dest, filepath.FromSlash(rel))); err != nil { + t.Errorf("expected %s to be provisioned directly under dest: %v", rel, err) + } + } + + if _, err := os.Stat(filepath.Join(dest, "share")); !os.IsNotExist(err) { + t.Errorf("unexpected share/ directory created under dest: %v", err) + } +} + func TestProvisionOverwritesStaleFiles(t *testing.T) { dest := t.TempDir()