diff --git a/cmd/llar/internal/make.go b/cmd/llar/internal/make.go index 6bee407..1ec5a22 100644 --- a/cmd/llar/internal/make.go +++ b/cmd/llar/internal/make.go @@ -3,6 +3,7 @@ package internal import ( "archive/zip" "context" + "encoding/json" "fmt" stdbuild "go/build" "io" @@ -24,6 +25,11 @@ import ( var makeVerbose bool var makeOutput string +type artifactMetadata struct { + Metadata string `json:"metadata"` + Deps []string `json:"deps,omitempty"` +} + // newRemoteStore creates the remote formula store. Overridable for testing. var newRemoteStore = func() (repo.Store, error) { formulaDir, err := repo.DefaultDir() @@ -186,7 +192,7 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version, matrix fmt.Println(main.Metadata) } if makeOutput != "" { - if err := outputResult(main.OutputDir, makeOutput); err != nil { + if err := outputArtifact(main.OutputDir, makeOutput, main.Metadata, artifactDeps(mods)); err != nil { return fmt.Errorf("failed to write output: %w", err) } } @@ -257,6 +263,55 @@ func parseModuleArg(arg string) (pattern, version string, isLocal bool, err erro return } +func writeArtifactMetadata(installDir, metadata string, deps []module.Version) error { + body, err := json.MarshalIndent(artifactMetadata{ + Metadata: metadata, + Deps: artifactDepStrings(deps), + }, "", " ") + if err != nil { + return err + } + + metaDir := filepath.Join(installDir, ".llar") + if err := os.MkdirAll(metaDir, 0o755); err != nil { + return err + } + return os.WriteFile(filepath.Join(metaDir, "metadata.json"), append(body, '\n'), 0o644) +} + +func artifactDeps(mods []*modules.Module) []module.Version { + if len(mods) <= 1 { + return nil + } + deps := make([]module.Version, 0, len(mods)-1) + main := mods[0] + for _, mod := range mods[1:] { + if mod.Path == main.Path && mod.Version == main.Version { + continue + } + deps = append(deps, module.Version{Path: mod.Path, Version: mod.Version}) + } + return deps +} + +func artifactDepStrings(deps []module.Version) []string { + if len(deps) == 0 { + return nil + } + out := make([]string, 0, len(deps)) + for _, dep := range deps { + out = append(out, dep.Path+"@"+dep.Version) + } + return out +} + +func outputArtifact(srcDir, dest, metadata string, deps []module.Version) error { + if err := writeArtifactMetadata(srcDir, metadata, deps); err != nil { + return err + } + return outputResult(srcDir, dest) +} + // outputResult writes the build output to dest. // If dest ends with ".zip", creates a zip archive; otherwise copies the directory. func outputResult(srcDir, dest string) error { diff --git a/cmd/llar/internal/make_test.go b/cmd/llar/internal/make_test.go index 3bad9bc..6f59691 100644 --- a/cmd/llar/internal/make_test.go +++ b/cmd/llar/internal/make_test.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "strings" "testing" @@ -129,6 +130,117 @@ func setupTestSrcDir(t *testing.T) string { return src } +func TestWriteArtifactMetadataWritesMetadataAndDeps(t *testing.T) { + installDir := t.TempDir() + metadata := fmt.Sprintf("-I%s -L%s -lz", filepath.Join(installDir, "include"), filepath.Join(installDir, "lib")) + deps := []module.Version{{Path: "madler/zlib", Version: "v1.3.1"}} + + if err := writeArtifactMetadata(installDir, metadata, deps); err != nil { + t.Fatalf("writeArtifactMetadata: %v", err) + } + + data, err := os.ReadFile(filepath.Join(installDir, ".llar", "metadata.json")) + if err != nil { + t.Fatalf("read metadata.json: %v", err) + } + var got struct { + Metadata string `json:"metadata"` + Deps []string `json:"deps"` + } + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("metadata.json is invalid JSON: %v", err) + } + if got.Metadata != metadata { + t.Fatalf("metadata = %q, want %q", got.Metadata, metadata) + } + wantDeps := []string{"madler/zlib@v1.3.1"} + if !reflect.DeepEqual(got.Deps, wantDeps) { + t.Fatalf("deps = %+v, want %+v", got.Deps, wantDeps) + } +} + +func TestWriteArtifactMetadataOmitsEmptyDeps(t *testing.T) { + installDir := t.TempDir() + if err := writeArtifactMetadata(installDir, "-lstandalone", nil); err != nil { + t.Fatalf("writeArtifactMetadata: %v", err) + } + + data, err := os.ReadFile(filepath.Join(installDir, ".llar", "metadata.json")) + if err != nil { + t.Fatalf("read metadata.json: %v", err) + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("metadata.json is invalid JSON: %v", err) + } + if _, ok := raw["metadata"]; !ok { + t.Fatal("metadata.json missing metadata field") + } + if _, ok := raw["deps"]; ok { + t.Fatalf("metadata.json contains deps for standalone artifact: %s", data) + } +} + +func TestOutputArtifactCopiesMetadataDirectory(t *testing.T) { + src := setupTestSrcDir(t) + dest := filepath.Join(t.TempDir(), "out") + + if err := outputArtifact(src, dest, "-lfoo", nil); err != nil { + t.Fatalf("outputArtifact: %v", err) + } + + data, err := os.ReadFile(filepath.Join(dest, ".llar", "metadata.json")) + if err != nil { + t.Fatalf("read output metadata.json: %v", err) + } + var got artifactMetadata + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("metadata.json is invalid JSON: %v", err) + } + if got.Metadata != "-lfoo" { + t.Fatalf("metadata = %q, want %q", got.Metadata, "-lfoo") + } +} + +func TestOutputArtifactZipsMetadataDirectory(t *testing.T) { + src := setupTestSrcDir(t) + dest := filepath.Join(t.TempDir(), "out.zip") + + if err := outputArtifact(src, dest, "-lfoo", nil); err != nil { + t.Fatalf("outputArtifact: %v", err) + } + + r, err := zip.OpenReader(dest) + if err != nil { + t.Fatalf("open zip: %v", err) + } + defer r.Close() + + for _, f := range r.File { + if f.Name != ".llar/metadata.json" { + continue + } + rc, err := f.Open() + if err != nil { + t.Fatalf("open metadata entry: %v", err) + } + defer rc.Close() + data, err := io.ReadAll(rc) + if err != nil { + t.Fatalf("read metadata entry: %v", err) + } + var got artifactMetadata + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("metadata.json is invalid JSON: %v", err) + } + if got.Metadata != "-lfoo" { + t.Fatalf("metadata = %q, want %q", got.Metadata, "-lfoo") + } + return + } + t.Fatal("zip missing .llar/metadata.json") +} + func TestOutputResult_CopyDir(t *testing.T) { src := setupTestSrcDir(t) dest := filepath.Join(t.TempDir(), "out")