Skip to content

Commit 7416781

Browse files
iQQBotona-agent
andcommitted
feat: auto-convert Go library deps to weak dependencies
Go packages with packaging: library are now automatically treated as weak dependencies when referenced. This means: - Source files are copied to _deps/ (not built artifacts) - go.mod replace directives are added - Builds run in parallel (don't block dependent package) - Version tracking ensures cache invalidation when libraries change This improves build parallelism for Go monorepos where libraries are used for source code via go.mod replace, not for built artifacts. Co-authored-by: Ona <no-reply@ona.com>
1 parent 74b1482 commit 7416781

5 files changed

Lines changed: 684 additions & 37 deletions

File tree

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ srcs:
108108
- "glob/**/path"
109109
# Deps list dependencies to other packages which must be built prior to building this package. How these dependencies are made
110110
# available during build depends on the package type.
111+
# NOTE: Go packages with `packaging: library` are automatically treated as "weak dependencies" - their source files
112+
# are copied (not built artifacts), and they don't block the build. See "Go Library Dependencies" section below.
111113
deps:
112114
- some/other:package
113115
# Argdeps makes build arguments version relevant. I.e. if the value of a build arg listed here changes, so does the package version.
@@ -125,6 +127,8 @@ config:
125127
```YAML
126128
config:
127129
# Packaging method. See https://godoc.org/github.com/gitpod-io/leeway/pkg/leeway#GoPackaging for details. Defaults to library.
130+
# IMPORTANT: Packages with `packaging: library` are treated as "weak dependencies" when referenced by other packages.
131+
# This means their source files are copied (not built artifacts), and they build in parallel rather than blocking.
128132
packaging: library
129133
# If true leeway runs `go generate -v ./...` prior to testing/building. Defaults to false.
130134
generate: false
@@ -144,6 +148,43 @@ config:
144148
goMod: "../go.mod"
145149
```
146150
151+
#### Go Library Dependencies (Weak Dependencies)
152+
153+
When a Go package with `packaging: library` is listed as a dependency, leeway automatically treats it as a "weak dependency". This behavior is optimized for Go's module system:
154+
155+
| Aspect | Regular Dependency | Go Library (Weak) Dependency |
156+
|--------|-------------------|------------------------------|
157+
| Affects package version | ✅ | ✅ |
158+
| Must be built first | ✅ | ❌ |
159+
| What's copied to `_deps/` | Built artifact | Source files |
160+
| `go.mod replace` added | ✅ | ✅ |
161+
| Added to build queue | ✅ | ✅ |
162+
163+
**Why this matters:**
164+
- Go libraries are typically used for their source code via `go.mod replace` directives
165+
- The library's tests can run in parallel with the dependent package's build
166+
- Changes to the library still trigger rebuilds of dependent packages (version tracking)
167+
- Build times improve because packages don't wait for library builds to complete
168+
169+
**Example:**
170+
```yaml
171+
# my-lib/BUILD.yaml
172+
packages:
173+
- name: lib
174+
type: go
175+
config:
176+
packaging: library # This makes it a weak dependency when referenced
177+
178+
# my-app/BUILD.yaml
179+
packages:
180+
- name: app
181+
type: go
182+
deps:
183+
- my-lib:lib # Automatically treated as weak dep - sources copied, builds in parallel
184+
config:
185+
packaging: app
186+
```
187+
147188
### Yarn packages
148189
```YAML
149190
config:

pkg/leeway/build.go

Lines changed: 123 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,21 @@ func Build(pkg *Package, opts ...BuildOption) (err error) {
687687
requirements := pkg.GetTransitiveDependencies()
688688
allpkg := append(requirements, pkg)
689689

690+
weakDeps := collectWeakDependencies(pkg)
691+
for _, wd := range weakDeps {
692+
// Only add if not already in allpkg (avoid duplicates)
693+
found := false
694+
for _, p := range allpkg {
695+
if p.FullName() == wd.FullName() {
696+
found = true
697+
break
698+
}
699+
}
700+
if !found {
701+
allpkg = append(allpkg, wd)
702+
}
703+
}
704+
690705
pkgsInLocalCache := make(map[*Package]struct{})
691706
var pkgsToCheckRemoteCache []*Package
692707
for _, p := range allpkg {
@@ -872,7 +887,20 @@ func Build(pkg *Package, opts ...BuildOption) (err error) {
872887
return nil
873888
}
874889

875-
buildErr := pkg.build(ctx)
890+
var buildGroup errgroup.Group
891+
892+
for _, wd := range weakDeps {
893+
weakDep := wd
894+
buildGroup.Go(func() error {
895+
return weakDep.build(ctx)
896+
})
897+
}
898+
899+
buildGroup.Go(func() error {
900+
return pkg.build(ctx)
901+
})
902+
903+
buildErr := buildGroup.Wait()
876904

877905
// Check for build errors immediately and return if there are any
878906
if buildErr != nil {
@@ -1462,6 +1490,45 @@ func (p *Package) packagesToDownload(inLocalCache map[*Package]struct{}, inRemot
14621490
}
14631491
}
14641492

1493+
// collectWeakDependencies collects all weak dependencies from a package and its dependency tree,
1494+
// including hard deps of weak deps (they need to be built for the weak dep to work).
1495+
func collectWeakDependencies(pkg *Package) []*Package {
1496+
seen := make(map[string]struct{})
1497+
visited := make(map[string]struct{})
1498+
var result []*Package
1499+
1500+
var collectFromPackage func(p *Package)
1501+
collectFromPackage = func(p *Package) {
1502+
if _, ok := visited[p.FullName()]; ok {
1503+
return
1504+
}
1505+
visited[p.FullName()] = struct{}{}
1506+
1507+
for _, wd := range p.GetWeakDependencies() {
1508+
if _, ok := seen[wd.FullName()]; !ok {
1509+
seen[wd.FullName()] = struct{}{}
1510+
result = append(result, wd)
1511+
}
1512+
1513+
collectFromPackage(wd)
1514+
1515+
for _, td := range wd.GetTransitiveDependencies() {
1516+
if _, ok := seen[td.FullName()]; !ok {
1517+
seen[td.FullName()] = struct{}{}
1518+
result = append(result, td)
1519+
}
1520+
}
1521+
}
1522+
1523+
for _, dep := range p.GetDependencies() {
1524+
collectFromPackage(dep)
1525+
}
1526+
}
1527+
1528+
collectFromPackage(pkg)
1529+
return result
1530+
}
1531+
14651532
// validateDependenciesAvailable checks if all required dependencies of a package are available.
14661533
// A dependency is considered available if it's in the local cache OR will be built (PackageNotBuiltYet).
14671534
// Returns true if all dependencies are available, false otherwise.
@@ -2068,43 +2135,68 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
20682135
}
20692136

20702137
transdep := p.GetTransitiveDependencies()
2071-
if len(transdep) > 0 {
2138+
weakdeps := p.GetTransitiveWeakDependencies()
2139+
needsDepsDir := len(transdep) > 0 || len(weakdeps) > 0
2140+
2141+
if needsDepsDir {
20722142
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"mkdir", "_deps"})
2143+
}
20732144

2074-
for _, dep := range transdep {
2075-
if dep.Ephemeral {
2076-
continue
2077-
}
2145+
for _, dep := range transdep {
2146+
if dep.Ephemeral {
2147+
continue
2148+
}
20782149

2079-
builtpkg, ok := buildctx.LocalCache.Location(dep)
2080-
if !ok {
2081-
return nil, PkgNotBuiltErr{dep}
2082-
}
2150+
builtpkg, ok := buildctx.LocalCache.Location(dep)
2151+
if !ok {
2152+
return nil, PkgNotBuiltErr{dep}
2153+
}
20832154

2084-
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2085-
untarCmd, err := BuildUnTarCommand(
2086-
WithInputFile(builtpkg),
2087-
WithTargetDir(tgt),
2088-
WithAutoDetectCompression(true),
2089-
)
2090-
if err != nil {
2091-
return nil, err
2092-
}
2155+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2156+
untarCmd, err := BuildUnTarCommand(
2157+
WithInputFile(builtpkg),
2158+
WithTargetDir(tgt),
2159+
WithAutoDetectCompression(true),
2160+
)
2161+
if err != nil {
2162+
return nil, err
2163+
}
20932164

2094-
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2095-
{"mkdir", tgt},
2096-
untarCmd,
2097-
}...)
2165+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2166+
{"mkdir", tgt},
2167+
untarCmd,
2168+
}...)
20982169

2099-
if dep.Type != GoPackage {
2100-
continue
2101-
}
2170+
if dep.Type != GoPackage {
2171+
continue
2172+
}
21022173

2103-
if isGoWorkspace {
2104-
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2105-
} else {
2106-
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"sh", "-c", fmt.Sprintf("%s mod edit -replace $(cd %s; grep module go.mod | cut -d ' ' -f 2 | head -n1)=./%s", goCommand, tgt, tgt)})
2107-
}
2174+
if isGoWorkspace {
2175+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2176+
} else {
2177+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"sh", "-c", fmt.Sprintf("%s mod edit -replace $(cd %s; grep module go.mod | cut -d ' ' -f 2 | head -n1)=./%s", goCommand, tgt, tgt)})
2178+
}
2179+
}
2180+
2181+
for _, dep := range weakdeps {
2182+
if dep.Type != GoPackage {
2183+
log.WithField("package", p.FullName()).WithField("weakdep", dep.FullName()).
2184+
Warn("weak dependencies are only supported for Go packages, skipping")
2185+
continue
2186+
}
2187+
2188+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2189+
srcDir := dep.C.Origin
2190+
2191+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2192+
{"mkdir", "-p", tgt},
2193+
{"sh", "-c", fmt.Sprintf("cp -r %s/* %s/", srcDir, tgt)},
2194+
}...)
2195+
2196+
if isGoWorkspace {
2197+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2198+
} else {
2199+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"sh", "-c", fmt.Sprintf("%s mod edit -replace $(cd %s; grep module go.mod | cut -d ' ' -f 2 | head -n1)=./%s", goCommand, tgt, tgt)})
21082200
}
21092201
}
21102202

0 commit comments

Comments
 (0)