Skip to content

Commit 2d19dc8

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 2d19dc8

5 files changed

Lines changed: 716 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: 142 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,23 @@ func Build(pkg *Package, opts ...BuildOption) (err error) {
687687
requirements := pkg.GetTransitiveDependencies()
688688
allpkg := append(requirements, pkg)
689689

690+
// Add weak dependencies to the build queue (they'll be built in parallel, not as blockers)
691+
// Weak deps are version-tracked and have sources copied, but don't block the main package build
692+
weakDeps := collectWeakDependencies(pkg)
693+
for _, wd := range weakDeps {
694+
// Only add if not already in allpkg (avoid duplicates)
695+
found := false
696+
for _, p := range allpkg {
697+
if p.FullName() == wd.FullName() {
698+
found = true
699+
break
700+
}
701+
}
702+
if !found {
703+
allpkg = append(allpkg, wd)
704+
}
705+
}
706+
690707
pkgsInLocalCache := make(map[*Package]struct{})
691708
var pkgsToCheckRemoteCache []*Package
692709
for _, p := range allpkg {
@@ -872,7 +889,25 @@ func Build(pkg *Package, opts ...BuildOption) (err error) {
872889
return nil
873890
}
874891

875-
buildErr := pkg.build(ctx)
892+
// Start weak dependency builds in parallel with the main package build
893+
// Weak deps don't block the main build but should run concurrently
894+
var buildGroup errgroup.Group
895+
896+
// Build weak dependencies in parallel
897+
for _, wd := range weakDeps {
898+
weakDep := wd // capture for goroutine
899+
buildGroup.Go(func() error {
900+
return weakDep.build(ctx)
901+
})
902+
}
903+
904+
// Build the main package (and its hard dependencies)
905+
buildGroup.Go(func() error {
906+
return pkg.build(ctx)
907+
})
908+
909+
// Wait for all builds to complete
910+
buildErr := buildGroup.Wait()
876911

877912
// Check for build errors immediately and return if there are any
878913
if buildErr != nil {
@@ -1462,6 +1497,53 @@ func (p *Package) packagesToDownload(inLocalCache map[*Package]struct{}, inRemot
14621497
}
14631498
}
14641499

1500+
// collectWeakDependencies collects all weak dependencies from a package and its dependency tree.
1501+
// This includes:
1502+
// - Direct weak deps of the package
1503+
// - Weak deps of hard deps (recursively)
1504+
// - Weak deps of weak deps (recursively) - e.g., Go library depending on another Go library
1505+
// - Hard deps of weak deps (they need to be built for the weak dep to work)
1506+
func collectWeakDependencies(pkg *Package) []*Package {
1507+
seen := make(map[string]struct{}) // tracks packages we've added to result
1508+
visited := make(map[string]struct{}) // tracks packages we've visited (to avoid infinite loops)
1509+
var result []*Package
1510+
1511+
var collectFromPackage func(p *Package)
1512+
collectFromPackage = func(p *Package) {
1513+
if _, ok := visited[p.FullName()]; ok {
1514+
return
1515+
}
1516+
visited[p.FullName()] = struct{}{}
1517+
1518+
// Collect weak deps from this package
1519+
for _, wd := range p.GetWeakDependencies() {
1520+
if _, ok := seen[wd.FullName()]; !ok {
1521+
seen[wd.FullName()] = struct{}{}
1522+
result = append(result, wd)
1523+
}
1524+
1525+
// Always recurse into weak dep to collect its deps (even if we've seen it)
1526+
collectFromPackage(wd)
1527+
1528+
// Also collect hard deps of weak deps (they need to be built)
1529+
for _, td := range wd.GetTransitiveDependencies() {
1530+
if _, ok := seen[td.FullName()]; !ok {
1531+
seen[td.FullName()] = struct{}{}
1532+
result = append(result, td)
1533+
}
1534+
}
1535+
}
1536+
1537+
// Recurse into hard dependencies (they may have weak deps too)
1538+
for _, dep := range p.GetDependencies() {
1539+
collectFromPackage(dep)
1540+
}
1541+
}
1542+
1543+
collectFromPackage(pkg)
1544+
return result
1545+
}
1546+
14651547
// validateDependenciesAvailable checks if all required dependencies of a package are available.
14661548
// A dependency is considered available if it's in the local cache OR will be built (PackageNotBuiltYet).
14671549
// Returns true if all dependencies are available, false otherwise.
@@ -2068,43 +2150,72 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
20682150
}
20692151

20702152
transdep := p.GetTransitiveDependencies()
2071-
if len(transdep) > 0 {
2153+
weakdeps := p.GetTransitiveWeakDependencies() // Get ALL weak deps including nested ones
2154+
needsDepsDir := len(transdep) > 0 || len(weakdeps) > 0
2155+
2156+
if needsDepsDir {
20722157
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"mkdir", "_deps"})
2158+
}
20732159

2074-
for _, dep := range transdep {
2075-
if dep.Ephemeral {
2076-
continue
2077-
}
2160+
// Handle hard dependencies (extract built artifacts)
2161+
for _, dep := range transdep {
2162+
if dep.Ephemeral {
2163+
continue
2164+
}
20782165

2079-
builtpkg, ok := buildctx.LocalCache.Location(dep)
2080-
if !ok {
2081-
return nil, PkgNotBuiltErr{dep}
2082-
}
2166+
builtpkg, ok := buildctx.LocalCache.Location(dep)
2167+
if !ok {
2168+
return nil, PkgNotBuiltErr{dep}
2169+
}
20832170

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-
}
2171+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2172+
untarCmd, err := BuildUnTarCommand(
2173+
WithInputFile(builtpkg),
2174+
WithTargetDir(tgt),
2175+
WithAutoDetectCompression(true),
2176+
)
2177+
if err != nil {
2178+
return nil, err
2179+
}
20932180

2094-
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2095-
{"mkdir", tgt},
2096-
untarCmd,
2097-
}...)
2181+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2182+
{"mkdir", tgt},
2183+
untarCmd,
2184+
}...)
20982185

2099-
if dep.Type != GoPackage {
2100-
continue
2101-
}
2186+
if dep.Type != GoPackage {
2187+
continue
2188+
}
21022189

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-
}
2190+
if isGoWorkspace {
2191+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2192+
} else {
2193+
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)})
2194+
}
2195+
}
2196+
2197+
// Handle weak dependencies (copy source files, don't require built artifacts)
2198+
for _, dep := range weakdeps {
2199+
if dep.Type != GoPackage {
2200+
log.WithField("package", p.FullName()).WithField("weakdep", dep.FullName()).
2201+
Warn("weak dependencies are only supported for Go packages, skipping")
2202+
continue
2203+
}
2204+
2205+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2206+
srcDir := dep.C.Origin
2207+
2208+
// Copy source files from the weak dependency's component
2209+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2210+
{"mkdir", "-p", tgt},
2211+
{"sh", "-c", fmt.Sprintf("cp -r %s/* %s/", srcDir, tgt)},
2212+
}...)
2213+
2214+
// Add go.mod replace directive for the weak dependency
2215+
if isGoWorkspace {
2216+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2217+
} else {
2218+
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)})
21082219
}
21092220
}
21102221

0 commit comments

Comments
 (0)