Skip to content

Commit 404d8fe

Browse files
committed
config: skip builtin packs reachable from imports
1 parent a576dec commit 404d8fe

2 files changed

Lines changed: 110 additions & 23 deletions

File tree

internal/config/compose.go

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,10 @@ func LoadWithIncludes(fs fsys.FS, path string, extraIncludes ...string) (*City,
237237
// Inject system pack includes into Workspace.Includes. These are
238238
// appended AFTER user includes so user packs override system pack
239239
// fallbacks via the normal dedup/fallback resolution.
240-
// Skip packs already reachable from user includes (avoids duplicate
241-
// agent errors when a user pack transitively includes a system pack).
242-
existingPacks := resolvedPackNames(root.Workspace.Includes, fs, cityRoot)
240+
// Skip packs already reachable from user includes or top-level imports
241+
// (avoids duplicate agent errors when a user pack transitively includes
242+
// a system pack).
243+
existingPacks := resolvedPackNames(root.Workspace.Includes, root.Imports, fs, cityRoot)
243244
for _, inc := range packIncludes {
244245
name := readPackNameFromDir(inc)
245246
if name != "" && existingPacks[name] {
@@ -831,23 +832,30 @@ func trackWorkspace(prov *Provenance, meta toml.MetaData, source string) {
831832
}
832833

833834
// resolvedPackNames collects pack names that are reachable from a set of
834-
// include paths (including transitive includes in pack.toml). Used to
835-
// skip system pack injection when a pack is already included by the user.
836-
func resolvedPackNames(includes []string, sysFS fsys.FS, cityRoot string) map[string]bool {
837-
names := make(map[string]bool, len(includes))
838-
var visit func(ref string)
839-
visit = func(ref string) {
840-
dir := resolveConfigPath(ref, cityRoot, cityRoot)
841-
// Try resolving as a pack directory.
842-
packPath := filepath.Join(dir, packFile)
843-
data, err := sysFS.ReadFile(packPath)
835+
// top-level include paths and imports. It walks both legacy [pack].includes
836+
// and V2 [imports] transitively so builtin system-pack injection can be
837+
// skipped when a user pack already brings the same pack into the city
838+
// closure.
839+
func resolvedPackNames(includes []string, imports map[string]Import, sysFS fsys.FS, cityRoot string) map[string]bool {
840+
names := make(map[string]bool, len(includes)+len(imports))
841+
seenDirs := make(map[string]bool)
842+
843+
var visit func(ref, declDir string)
844+
visit = func(ref, declDir string) {
845+
dir, err := resolvePackRef(ref, declDir, cityRoot)
844846
if err != nil {
845-
// Maybe it's a remote ref.
846-
if resolved, rErr := resolvePackRef(ref, cityRoot, cityRoot); rErr == nil {
847-
dir = resolved
848-
data, err = sysFS.ReadFile(filepath.Join(dir, packFile))
849-
}
847+
return
848+
}
849+
absDir, absErr := filepath.Abs(dir)
850+
if absErr != nil {
851+
absDir = dir
850852
}
853+
if seenDirs[absDir] {
854+
return
855+
}
856+
seenDirs[absDir] = true
857+
858+
data, err := sysFS.ReadFile(filepath.Join(dir, packFile))
851859
if err != nil {
852860
return
853861
}
@@ -856,20 +864,26 @@ func resolvedPackNames(includes []string, sysFS fsys.FS, cityRoot string) map[st
856864
Name string `toml:"name"`
857865
Includes []string `toml:"includes"`
858866
} `toml:"pack"`
867+
Imports map[string]Import `toml:"imports"`
859868
}
860869
if _, decErr := toml.Decode(string(data), &pc); decErr != nil || pc.Pack.Name == "" {
861870
return
862871
}
863-
if names[pc.Pack.Name] {
864-
return
865-
}
872+
866873
names[pc.Pack.Name] = true
867874
for _, sub := range pc.Pack.Includes {
868-
visit(resolveConfigPath(sub, dir, cityRoot))
875+
visit(sub, dir)
876+
}
877+
for _, imp := range pc.Imports {
878+
visit(imp.Source, dir)
869879
}
870880
}
881+
871882
for _, inc := range includes {
872-
visit(inc)
883+
visit(inc, cityRoot)
884+
}
885+
for _, imp := range imports {
886+
visit(imp.Source, cityRoot)
873887
}
874888
return names
875889
}

internal/config/compose_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,79 @@ name = "mayor"
4545
}
4646
}
4747

48+
func TestLoadWithIncludes_SkipsSystemPackWhenReachableFromRootImport(t *testing.T) {
49+
dir := t.TempDir()
50+
write := func(rel, data string) {
51+
path := filepath.Join(dir, rel)
52+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
53+
t.Fatalf("MkdirAll(%s): %v", path, err)
54+
}
55+
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
56+
t.Fatalf("WriteFile(%s): %v", path, err)
57+
}
58+
}
59+
60+
write("city.toml", `
61+
[workspace]
62+
name = "test"
63+
`)
64+
write("pack.toml", `
65+
[pack]
66+
name = "test"
67+
schema = 2
68+
69+
[imports.gs]
70+
source = "./packs/gastown"
71+
`)
72+
write("packs/gastown/pack.toml", `
73+
[pack]
74+
name = "gastown"
75+
schema = 2
76+
includes = ["../maintenance"]
77+
78+
[[agent]]
79+
name = "mayor"
80+
scope = "city"
81+
`)
82+
write("packs/maintenance/pack.toml", `
83+
[pack]
84+
name = "maintenance"
85+
schema = 2
86+
87+
[[agent]]
88+
name = "dog"
89+
scope = "city"
90+
`)
91+
write("system/maintenance/pack.toml", `
92+
[pack]
93+
name = "maintenance"
94+
schema = 2
95+
96+
[[agent]]
97+
name = "dog"
98+
scope = "city"
99+
`)
100+
101+
cfg, _, err := LoadWithIncludes(fsys.OSFS{}, filepath.Join(dir, "city.toml"), filepath.Join(dir, "system", "maintenance"))
102+
if err != nil {
103+
t.Fatalf("LoadWithIncludes: %v", err)
104+
}
105+
106+
found := map[string]bool{}
107+
for _, a := range explicitAgents(cfg.Agents) {
108+
found[a.QualifiedName()] = true
109+
}
110+
if !found["gs.mayor"] {
111+
t.Fatalf("missing gs.mayor: %v", found)
112+
}
113+
if !found["gs.dog"] {
114+
t.Fatalf("missing gs.dog from imported maintenance closure: %v", found)
115+
}
116+
if found["dog"] {
117+
t.Fatalf("system maintenance should have been skipped when root import already reaches maintenance: %v", found)
118+
}
119+
}
120+
48121
func TestLoadWithIncludes_CityPackSchema2(t *testing.T) {
49122
fs := fsys.NewFake()
50123
fs.Files["/city/city.toml"] = []byte(`

0 commit comments

Comments
 (0)