pkg/nodejs/pnpm.go (80 lines of code) (raw):
package nodejs
import (
"fmt"
"os"
"path/filepath"
"github.com/GoogleCloudPlatform/buildpacks/pkg/fetch"
gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack"
"github.com/buildpacks/libcnb/v2"
)
var (
// PNPMLock is the name of the pnpm lock file.
PNPMLock = "pnpm-lock.yaml"
// pnpmDownloadURL is the template used to generate a pnpm download URL.
pnpmDownloadURL = "https://github.com/pnpm/pnpm/releases/download/v%s/pnpm-linux-x64"
// pnpmVersionKey is the metadata key used to store the pnpm version in the pnpn layer.
pnpmVersionKey = "version"
)
// InstallPNPM installs pnpm in the given layer if it is not already cached.
func InstallPNPM(ctx *gcp.Context, pnpmLayer *libcnb.Layer, pjs *PackageJSON) error {
layerName := pnpmLayer.Name
installDir := filepath.Join(pnpmLayer.Path, "bin")
version, err := detectPNPMVersion(pjs)
if err != nil {
return err
}
// Check the metadata in the cache layer to determine if we need to proceed.
metaVersion := ctx.GetMetadata(pnpmLayer, pnpmVersionKey)
if version == metaVersion {
ctx.CacheHit(layerName)
ctx.Logf("pnpm cache hit: %q, %q, skipping installation.", version, metaVersion)
} else {
ctx.CacheMiss(layerName)
if err := ctx.ClearLayer(pnpmLayer); err != nil {
return fmt.Errorf("clearing layer %q: %w", layerName, err)
}
// Download and install pnpm in layer.
ctx.Logf("Installing pnpm v%s", version)
if err := downloadPNPM(ctx, installDir, version); err != nil {
return gcp.InternalErrorf("downloading pnpm: %w", err)
}
fp := filepath.Join(installDir, "pnpm")
if err := os.Chmod(fp, 0777); err != nil {
return gcp.InternalErrorf("chmoding %s: %w", fp, err)
}
}
// Store layer flags and metadata.
ctx.SetMetadata(pnpmLayer, versionKey, version)
// We need to update the path here to ensure the version we just installed take precedence over
// anything pre-installed in the base image.
if err := ctx.Setenv("PATH", installDir+":"+os.Getenv("PATH")); err != nil {
return err
}
return nil
}
// downloadPNPM downloads a given version of pnpm into the provided directory.
func downloadPNPM(ctx *gcp.Context, dir, version string) error {
if err := ctx.MkdirAll(dir, 0755); err != nil {
return err
}
fp := filepath.Join(dir, "pnpm")
url := fmt.Sprintf(pnpmDownloadURL, version)
return fetch.File(url, fp)
}
// detectPnpmVersion determines the version of pnpm that should be installed in a Node.js project
// by examining the "engines.pnpm" and "packageManager" constraints specified in package.json and comparing them against all
// published versions in the NPM registry, if both exist "engines.pnpm" will take precedence.
// If the package.json does not include "engines.pnpm" or "packageManager" it
// returns the latest stable version available.
// TODO(b/338411091) create a shared packagejson util library and refactor out a generic detect
// package manager version function.
func detectPNPMVersion(pjs *PackageJSON) (string, error) {
if pjs == nil || (pjs.Engines.PNPM == "" && pjs.PackageManager == "") {
version, err := latestPackageVersion("pnpm")
if err != nil {
return "", gcp.InternalErrorf("fetching available pnpm versions: %w", err)
}
return version, nil
}
var requestedVersion string
if pjs.Engines.PNPM != "" {
requestedVersion = pjs.Engines.PNPM
} else {
packageManagerName, packageManagerVersion, err := parsePackageManager(pjs.PackageManager)
if err != nil {
return "", err
}
if packageManagerName != "pnpm" {
return "", gcp.UserErrorf("pnpm was detected but %s is set in the packageManager package.json field.", packageManagerName)
}
requestedVersion = packageManagerVersion
}
version, err := resolvePackageVersion("pnpm", requestedVersion)
if err != nil {
return "", gcp.UserErrorf("finding pnpm version that matched %q: %w", requestedVersion, err)
}
return version, nil
}