Skip to content

Commit f6ce5b1

Browse files
authored
Added support for yarn berry (#4)
* Added support for yarn berry * Added check for .yarnrc.yaml also
1 parent 0e8404a commit f6ce5b1

2 files changed

Lines changed: 128 additions & 37 deletions

File tree

build.go

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package yarn
22

33
import (
4-
"fmt"
5-
"os"
6-
"path/filepath"
7-
"strconv"
8-
"time"
9-
10-
"github.com/paketo-buildpacks/packit/v2"
11-
"github.com/paketo-buildpacks/packit/v2/chronos"
12-
"github.com/paketo-buildpacks/packit/v2/draft"
13-
"github.com/paketo-buildpacks/packit/v2/postal"
14-
"github.com/paketo-buildpacks/packit/v2/sbom"
15-
"github.com/paketo-buildpacks/packit/v2/scribe"
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
"time"
11+
"encoding/json"
12+
13+
"github.com/paketo-buildpacks/packit/v2"
14+
"github.com/paketo-buildpacks/packit/v2/chronos"
15+
"github.com/paketo-buildpacks/packit/v2/draft"
16+
"github.com/paketo-buildpacks/packit/v2/postal"
17+
"github.com/paketo-buildpacks/packit/v2/sbom"
18+
"github.com/paketo-buildpacks/packit/v2/scribe"
1619
)
1720

1821
//go:generate faux --interface DependencyManager --output fakes/dependency_manager.go
@@ -41,13 +44,38 @@ func Build(
4144
return packit.BuildResult{}, err
4245
}
4346

44-
planner := draft.NewPlanner()
45-
entry, _ := planner.Resolve("yarn", context.Plan.Entries, nil)
46-
version, ok := entry.Metadata["version"].(string)
47-
if !ok {
48-
version = "default"
49-
}
50-
47+
planner := draft.NewPlanner()
48+
entry, _ := planner.Resolve("yarn", context.Plan.Entries, nil)
49+
version, ok := entry.Metadata["version"].(string)
50+
if !ok {
51+
version = "default"
52+
}
53+
54+
// Detect Yarn Berry (modern) by presence of .yarnrc.yml/.yarnrc.yaml
55+
isBerryYarn := false
56+
if _, statErr := os.Stat(filepath.Join(context.WorkingDir, ".yarnrc.yml")); statErr == nil {
57+
isBerryYarn = true
58+
} else if _, statErr := os.Stat(filepath.Join(context.WorkingDir, ".yarnrc.yaml")); statErr == nil {
59+
isBerryYarn = true
60+
}
61+
62+
// If package.json declares packageManager: "yarn@<ver>", prefer that ONLY for Berry projects
63+
var pmYarnVersion string
64+
if data, readErr := os.ReadFile(filepath.Join(context.WorkingDir, "package.json")); readErr == nil {
65+
var pkg struct {
66+
PackageManager string `json:"packageManager"`
67+
}
68+
if jsonErr := json.Unmarshal(data, &pkg); jsonErr == nil {
69+
pm := strings.TrimSpace(pkg.PackageManager)
70+
if strings.HasPrefix(pm, "yarn@") {
71+
v := strings.TrimPrefix(pm, "yarn@")
72+
if idx := strings.IndexAny(v, " +#"); idx != -1 {
73+
v = v[:idx]
74+
}
75+
pmYarnVersion = v
76+
}
77+
}
78+
}
5179
dependency, err := dependencyManager.Resolve(
5280
filepath.Join(context.CNBPath, "buildpack.toml"),
5381
entry.Name,
@@ -71,8 +99,15 @@ func Build(
7199
launchMetadata = packit.LaunchMetadata{BOM: bom}
72100
}
73101

74-
cachedSHA, ok := yarnLayer.Metadata[DependencyCacheKey].(string)
75-
if ok && postal.Checksum(dependency.Checksum).MatchString(cachedSHA) {
102+
// Use resolved dependency version as the install version (override with packageManager only for Berry)
103+
104+
resolvedInstallVersion := dependency.Version
105+
if isBerryYarn && pmYarnVersion != "" {
106+
resolvedInstallVersion = pmYarnVersion
107+
}
108+
109+
cachedSHA, ok := yarnLayer.Metadata[DependencyCacheKey].(string)
110+
if ok && !isBerryYarn && postal.Checksum(dependency.Checksum).MatchString(cachedSHA) {
76111
logger.Process("Reusing cached layer %s", yarnLayer.Path)
77112
logger.Break()
78113

@@ -85,25 +120,63 @@ func Build(
85120
}, nil
86121
}
87122

88-
logger.Process("Executing build process")
123+
logger.Process("Executing build process")
89124

90125
yarnLayer, err = yarnLayer.Reset()
91126
if err != nil {
92127
return packit.BuildResult{}, err
93128
}
94129

95-
yarnLayer.Launch, yarnLayer.Build, yarnLayer.Cache = launch, build, build
96-
97-
logger.Subprocess("Installing Yarn")
98-
99-
duration, err := clock.Measure(func() error {
100-
return dependencyManager.Deliver(dependency, context.CNBPath, yarnLayer.Path, context.Platform.Path)
101-
})
102-
if err != nil {
103-
return packit.BuildResult{}, err
104-
}
105-
logger.Action("Completed in %s", duration.Round(time.Millisecond))
106-
logger.Break()
130+
yarnLayer.Launch, yarnLayer.Build, yarnLayer.Cache = launch, build, build
131+
132+
logger.Subprocess("Installing Yarn %s", resolvedInstallVersion)
133+
134+
var duration time.Duration
135+
if isBerryYarn {
136+
// Persist Corepack cache under the yarn layer so the runtime doesn't re-download Yarn
137+
corepackDir := filepath.Join(yarnLayer.Path, "corepack")
138+
if mkErr := os.MkdirAll(corepackDir, 0o755); mkErr != nil {
139+
return packit.BuildResult{}, mkErr
140+
}
141+
142+
143+
duration, err = clock.Measure(func() error {
144+
steps := [][]string{
145+
{"corepack", "enable"},
146+
{"corepack", "prepare", fmt.Sprintf("yarn@%s", resolvedInstallVersion), "--activate"},
147+
}
148+
for _, args := range steps {
149+
150+
cmd := exec.Command(args[0], args[1:]...)
151+
cmd.Env = append(os.Environ(), "COREPACK_HOME="+corepackDir)
152+
cmd.Stdout = os.Stdout
153+
cmd.Stderr = os.Stderr
154+
if err := cmd.Run(); err != nil {
155+
return err
156+
}
157+
}
158+
return nil
159+
})
160+
if err != nil {
161+
return packit.BuildResult{}, err
162+
}
163+
logger.Action("Completed in %s", duration.Round(time.Millisecond))
164+
logger.Break()
165+
// Ensure COREPACK_HOME is present in build and launch images
166+
yarnLayer.Build = true
167+
yarnLayer.Launch = true
168+
yarnLayer.BuildEnv.Default("COREPACK_HOME", filepath.Join(yarnLayer.Path, "corepack"))
169+
yarnLayer.LaunchEnv.Default("COREPACK_HOME", filepath.Join(yarnLayer.Path, "corepack"))
170+
} else {
171+
duration, err = clock.Measure(func() error {
172+
return dependencyManager.Deliver(dependency, context.CNBPath, yarnLayer.Path, context.Platform.Path)
173+
})
174+
if err != nil {
175+
return packit.BuildResult{}, err
176+
}
177+
logger.Action("Completed in %s", duration.Round(time.Millisecond))
178+
logger.Break()
179+
}
107180

108181
sbomDisabled, err := checkSbomDisabled()
109182
if err != nil {
@@ -134,9 +207,13 @@ func Build(
134207
}
135208
}
136209

137-
yarnLayer.Metadata = map[string]interface{}{
138-
DependencyCacheKey: dependency.Checksum,
139-
}
210+
cacheValue := dependency.Checksum
211+
if isBerryYarn || cacheValue == "" {
212+
cacheValue = dependency.Version
213+
}
214+
yarnLayer.Metadata = map[string]interface{}{
215+
DependencyCacheKey: cacheValue,
216+
}
140217

141218
return packit.BuildResult{
142219
Layers: []packit.Layer{yarnLayer},

buildpack.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ api = "0.7"
2929
uri = "https://paketo-artifacts.s3.us-east-1.amazonaws.com/yarn/yarn_1.22.19_linux_noarch_bionic_3235ba55.tgz"
3030
version = "1.22.19"
3131

32+
[[metadata.dependencies]]
33+
checksum = "sha256:c8d3eae160a892e32837db3dcae515e843e5383fef52b8141940c8bcf8b6d59f"
34+
cpe = "cpe:2.3:a:yarnpkg:yarn:4.9.2:*:*:*:*:*:*:*"
35+
id = "yarn"
36+
licenses = ["BSD-2-Clause"]
37+
name = "Yarn"
38+
purl = "pkg:generic/yarn@4.9.2"
39+
source = "https://registry.npmjs.org/yarn/-/yarn-4.9.2.tgz"
40+
source-checksum = "sha256:c8d3eae160a892e32837db3dcae515e843e5383fef52b8141940c8bcf8b6d59f"
41+
stacks = ["io.buildpacks.stacks.bionic", "io.buildpacks.stacks.jammy", "io.buildpacks.stacks.noble", "*"]
42+
strip-components = 1
43+
uri = "https://registry.npmjs.org/yarn/-/yarn-4.9.2.tgz"
44+
version = "4.9.2"
45+
3246
[[metadata.dependencies]]
3347
checksum = "sha256:88268464199d1611fcf73ce9c0a6c4d44c7d5363682720d8506f6508addf36a0"
3448
cpe = "cpe:2.3:a:yarnpkg:yarn:1.22.22:*:*:*:*:*:*:*"

0 commit comments

Comments
 (0)