Skip to content

Commit 210c8cd

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 210c8cd

File tree

5 files changed

+865
-37
lines changed

5 files changed

+865
-37
lines changed

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: 208 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,38 @@ func Build(pkg *Package, opts ...BuildOption) (err error) {
872887
return nil
873888
}
874889

875-
buildErr := pkg.build(ctx)
890+
// Build hard deps of weak deps first - they need to be available for extraction
891+
// when the main package builds. Use GetTransitiveWeakDependencies to get only
892+
// the actual weak deps (Go libraries), not the mixed result from collectWeakDependencies.
893+
hardDepsOfWeakDeps := collectHardDepsOfWeakDepsForBuild(pkg.GetTransitiveWeakDependencies())
894+
if len(hardDepsOfWeakDeps) > 0 {
895+
var hdGroup errgroup.Group
896+
for _, hd := range hardDepsOfWeakDeps {
897+
hardDep := hd
898+
hdGroup.Go(func() error {
899+
return hardDep.build(ctx)
900+
})
901+
}
902+
if err := hdGroup.Wait(); err != nil {
903+
return xerrors.Errorf("build failed")
904+
}
905+
}
906+
907+
// Now build weak deps and main package in parallel
908+
var buildGroup errgroup.Group
909+
910+
for _, wd := range weakDeps {
911+
weakDep := wd
912+
buildGroup.Go(func() error {
913+
return weakDep.build(ctx)
914+
})
915+
}
916+
917+
buildGroup.Go(func() error {
918+
return pkg.build(ctx)
919+
})
920+
921+
buildErr := buildGroup.Wait()
876922

877923
// Check for build errors immediately and return if there are any
878924
if buildErr != nil {
@@ -1452,6 +1498,10 @@ func (p *Package) packagesToDownload(inLocalCache map[*Package]struct{}, inRemot
14521498
// as we also need components/gitpod-protocol:gitpod-schema to be available on disk to perform the build.
14531499
case YarnPackage, GoPackage:
14541500
deps = p.GetTransitiveDependencies()
1501+
// Also include hard deps of weak deps (e.g., generic packages that Go libraries depend on)
1502+
for _, wd := range p.GetTransitiveWeakDependencies() {
1503+
deps = append(deps, wd.GetTransitiveDependencies()...)
1504+
}
14551505
// For Generic and Docker packages it is sufficient to have the direct dependencies.
14561506
case GenericPackage, DockerPackage:
14571507
deps = p.GetDependencies()
@@ -1462,6 +1512,91 @@ func (p *Package) packagesToDownload(inLocalCache map[*Package]struct{}, inRemot
14621512
}
14631513
}
14641514

1515+
// collectHardDepsOfWeakDeps returns hard dependencies of weak deps that aren't already in transdep.
1516+
// These need to be extracted as built artifacts for the weak dep sources to compile.
1517+
func collectHardDepsOfWeakDeps(weakdeps []*Package, transdep []*Package) []*Package {
1518+
existing := make(map[string]struct{})
1519+
for _, dep := range transdep {
1520+
existing[dep.FullName()] = struct{}{}
1521+
}
1522+
1523+
seen := make(map[string]struct{})
1524+
var result []*Package
1525+
1526+
for _, wd := range weakdeps {
1527+
for _, hd := range wd.GetTransitiveDependencies() {
1528+
if _, ok := existing[hd.FullName()]; ok {
1529+
continue
1530+
}
1531+
if _, ok := seen[hd.FullName()]; ok {
1532+
continue
1533+
}
1534+
seen[hd.FullName()] = struct{}{}
1535+
result = append(result, hd)
1536+
}
1537+
}
1538+
1539+
return result
1540+
}
1541+
1542+
// collectHardDepsOfWeakDepsForBuild returns hard deps of weak deps that need to be built
1543+
// before the main package can build (they need to be extracted to _deps/).
1544+
func collectHardDepsOfWeakDepsForBuild(weakDeps []*Package) []*Package {
1545+
seen := make(map[string]struct{})
1546+
var result []*Package
1547+
1548+
for _, wd := range weakDeps {
1549+
for _, hd := range wd.GetTransitiveDependencies() {
1550+
if _, ok := seen[hd.FullName()]; ok {
1551+
continue
1552+
}
1553+
seen[hd.FullName()] = struct{}{}
1554+
result = append(result, hd)
1555+
}
1556+
}
1557+
1558+
return result
1559+
}
1560+
1561+
// collectWeakDependencies collects all weak dependencies from a package and its dependency tree,
1562+
// including hard deps of weak deps (they need to be built for the weak dep to work).
1563+
func collectWeakDependencies(pkg *Package) []*Package {
1564+
seen := make(map[string]struct{})
1565+
visited := make(map[string]struct{})
1566+
var result []*Package
1567+
1568+
var collectFromPackage func(p *Package)
1569+
collectFromPackage = func(p *Package) {
1570+
if _, ok := visited[p.FullName()]; ok {
1571+
return
1572+
}
1573+
visited[p.FullName()] = struct{}{}
1574+
1575+
for _, wd := range p.GetWeakDependencies() {
1576+
if _, ok := seen[wd.FullName()]; !ok {
1577+
seen[wd.FullName()] = struct{}{}
1578+
result = append(result, wd)
1579+
}
1580+
1581+
collectFromPackage(wd)
1582+
1583+
for _, td := range wd.GetTransitiveDependencies() {
1584+
if _, ok := seen[td.FullName()]; !ok {
1585+
seen[td.FullName()] = struct{}{}
1586+
result = append(result, td)
1587+
}
1588+
}
1589+
}
1590+
1591+
for _, dep := range p.GetDependencies() {
1592+
collectFromPackage(dep)
1593+
}
1594+
}
1595+
1596+
collectFromPackage(pkg)
1597+
return result
1598+
}
1599+
14651600
// validateDependenciesAvailable checks if all required dependencies of a package are available.
14661601
// A dependency is considered available if it's in the local cache OR will be built (PackageNotBuiltYet).
14671602
// Returns true if all dependencies are available, false otherwise.
@@ -1475,6 +1610,10 @@ func validateDependenciesAvailable(p *Package, localCache cache.LocalCache, pkgs
14751610
case YarnPackage, GoPackage:
14761611
// Go and Yarn packages need all transitive dependencies
14771612
deps = p.GetTransitiveDependencies()
1613+
// Also include hard deps of weak deps (e.g., generic packages that Go libraries depend on)
1614+
for _, wd := range p.GetTransitiveWeakDependencies() {
1615+
deps = append(deps, wd.GetTransitiveDependencies()...)
1616+
}
14781617
case GenericPackage, DockerPackage:
14791618
// Generic and Docker packages only need direct dependencies
14801619
deps = p.GetDependencies()
@@ -2068,43 +2207,81 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
20682207
}
20692208

20702209
transdep := p.GetTransitiveDependencies()
2071-
if len(transdep) > 0 {
2210+
weakdeps := p.GetTransitiveWeakDependencies()
2211+
2212+
// Collect hard deps of weak deps - they need to be extracted as built artifacts
2213+
hardDepsOfWeakDeps := collectHardDepsOfWeakDeps(weakdeps, transdep)
2214+
2215+
needsDepsDir := len(transdep) > 0 || len(weakdeps) > 0 || len(hardDepsOfWeakDeps) > 0
2216+
2217+
if needsDepsDir {
20722218
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"mkdir", "_deps"})
2219+
}
20732220

2074-
for _, dep := range transdep {
2075-
if dep.Ephemeral {
2076-
continue
2077-
}
2221+
// Combine transdep and hardDepsOfWeakDeps for extraction
2222+
allHardDeps := make(map[string]*Package)
2223+
for _, dep := range transdep {
2224+
allHardDeps[dep.FullName()] = dep
2225+
}
2226+
for _, dep := range hardDepsOfWeakDeps {
2227+
allHardDeps[dep.FullName()] = dep
2228+
}
20782229

2079-
builtpkg, ok := buildctx.LocalCache.Location(dep)
2080-
if !ok {
2081-
return nil, PkgNotBuiltErr{dep}
2082-
}
2230+
for _, dep := range allHardDeps {
2231+
if dep.Ephemeral {
2232+
continue
2233+
}
20832234

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-
}
2235+
builtpkg, ok := buildctx.LocalCache.Location(dep)
2236+
if !ok {
2237+
return nil, PkgNotBuiltErr{dep}
2238+
}
20932239

2094-
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2095-
{"mkdir", tgt},
2096-
untarCmd,
2097-
}...)
2240+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2241+
untarCmd, err := BuildUnTarCommand(
2242+
WithInputFile(builtpkg),
2243+
WithTargetDir(tgt),
2244+
WithAutoDetectCompression(true),
2245+
)
2246+
if err != nil {
2247+
return nil, err
2248+
}
20982249

2099-
if dep.Type != GoPackage {
2100-
continue
2101-
}
2250+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2251+
{"mkdir", tgt},
2252+
untarCmd,
2253+
}...)
21022254

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-
}
2255+
if dep.Type != GoPackage {
2256+
continue
2257+
}
2258+
2259+
if isGoWorkspace {
2260+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2261+
} else {
2262+
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)})
2263+
}
2264+
}
2265+
2266+
for _, dep := range weakdeps {
2267+
if dep.Type != GoPackage {
2268+
log.WithField("package", p.FullName()).WithField("weakdep", dep.FullName()).
2269+
Warn("weak dependencies are only supported for Go packages, skipping")
2270+
continue
2271+
}
2272+
2273+
tgt := filepath.Join("_deps", p.BuildLayoutLocation(dep))
2274+
srcDir := dep.C.Origin
2275+
2276+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], [][]string{
2277+
{"mkdir", "-p", tgt},
2278+
{"sh", "-c", fmt.Sprintf("cp -r %s/* %s/", srcDir, tgt)},
2279+
}...)
2280+
2281+
if isGoWorkspace {
2282+
commands[PackageBuildPhasePrep] = append(commands[PackageBuildPhasePrep], []string{"go", "work", "use", tgt})
2283+
} else {
2284+
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)})
21082285
}
21092286
}
21102287

0 commit comments

Comments
 (0)