diff --git a/CHANGELOG.md b/CHANGELOG.md index e723386a4..5c14a94a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Cleanup of non-deterministic files in Node.js, npm, and pnpm layers to prevent unnecessary invalidation of runtime layers for export. ([#1274](https://github.com/heroku/buildpacks-nodejs/pull/1274)) + ### Changed - Automated sync of `@yarnpkg/plugin-prune-dev-dependencies.js` from https://github.com/heroku/heroku-buildpack-nodejs/commit/ab3aa2a99c9cc926366d62f8b56342f623b0acaa diff --git a/Cargo.lock b/Cargo.lock index c1b779353..7e65021ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,6 +2348,7 @@ dependencies = [ "toml", "toml_edit", "tracing", + "walkdir", "wiremock", "yaml-rust2", ] diff --git a/Cargo.toml b/Cargo.toml index 2db9bc3d7..98463d6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ toml = "0.9" toml_edit = "0.23" tracing = "0.1" yaml-rust2 = "0.10" +walkdir = "2.5.0" [dev-dependencies] insta = "1" diff --git a/crates/test_support/src/snapshot_filters.rs b/crates/test_support/src/snapshot_filters.rs index 8503f8eb1..8b53704eb 100644 --- a/crates/test_support/src/snapshot_filters.rs +++ b/crates/test_support/src/snapshot_filters.rs @@ -326,13 +326,6 @@ pub(super) fn create_snapshot_filters() -> Vec<(String, String)> { "${1}", )); - // TODO: investigate why the `...native_modules_are_recompiled...`-based tests (mostly the pnpm version) - // occassionally switch from Adding layer 'heroku/nodejs:xyz' to Reusing layer 'heroku/nodejs:xyz' - filters.push(( - r"(?:Adding|Reusing) layer 'heroku/nodejs:virtual", - " layer 'heroku/nodejs:virtual", - )); - filters .into_iter() .map(|(matcher, replacement)| (matcher.to_string(), replacement.to_string())) diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 000000000..1fb2fe841 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,36 @@ +use crate::NodeJsBuildpack; +use crate::layer_cleanup::LayerCleanupTarget; +use std::cell::RefCell; +use std::ops::Deref; + +/// Wrapper around libcnb `BuildContext` that tracks layers needing cleanup +/// of non-deterministic build artifacts (Python bytecode, Makefiles) +pub(crate) struct NodeJsBuildContext { + inner: libcnb::build::BuildContext, + cleanup_registry: RefCell>, +} + +impl NodeJsBuildContext { + pub(crate) fn new(inner: libcnb::build::BuildContext) -> Self { + Self { + inner, + cleanup_registry: RefCell::new(Vec::new()), + } + } + + pub(crate) fn register_layer_for_cleanup(&self, target: LayerCleanupTarget) { + self.cleanup_registry.borrow_mut().push(target); + } + + pub(crate) fn layers_to_cleanup(&self) -> Vec { + self.cleanup_registry.borrow().iter().cloned().collect() + } +} + +impl Deref for NodeJsBuildContext { + type Target = libcnb::build::BuildContext; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/src/layer_cleanup.rs b/src/layer_cleanup.rs new file mode 100644 index 000000000..e4f03a56e --- /dev/null +++ b/src/layer_cleanup.rs @@ -0,0 +1,84 @@ +use bullet_stream::global::print; +use std::fs; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +#[derive(Debug, Clone)] +pub(crate) enum LayerKind { + /// pnpm virtual store layer (contains native module builds with Makefiles) + Virtual, +} + +#[derive(Debug, Clone)] +pub(crate) struct LayerCleanupTarget { + pub(crate) path: PathBuf, + pub(crate) kind: LayerKind, +} + +/// Remove Makefile files from native module build directories +/// These files have non-deterministic dependency ordering causing layer invalidation +fn remove_build_makefiles(base_path: &Path) -> std::io::Result { + let makefile_dir_entries = WalkDir::new(base_path) + .into_iter() + .filter_map(Result::ok) + .filter(|dir_entry| { + dir_entry.file_type().is_file() && dir_entry.path().ends_with("build/Makefile") + }); + + let mut removed_count = 0; + for entry in makefile_dir_entries { + fs::remove_file(entry.path())?; + removed_count += 1; + } + + Ok(removed_count) +} + +/// Clean up non-deterministic build artifacts from a layer +pub(crate) fn cleanup_layer(target: &LayerCleanupTarget) -> Result<(), std::io::Error> { + let path = &target.path; + + if !path.exists() { + // Layer doesn't exist, nothing to clean + return Ok(()); + } + + match target.kind { + LayerKind::Virtual => { + // pnpm virtual store: contains symlinked packages with native module builds + // Clean Makefiles from: virtual/store/*/node_modules/*/build/ + print::bullet("Cleaning up pnpm virtual store layer"); + let removed = remove_build_makefiles(path)?; + if removed > 0 { + print::sub_bullet(format!("Removed {removed} Makefile artifacts")); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_remove_build_makefiles() { + let temp = TempDir::new().unwrap(); + let base = temp.path(); + + // Create build directory with Makefile + let build_dir = base.join("node_modules/some-package/build"); + fs::create_dir_all(&build_dir).unwrap(); + fs::write(build_dir.join("Makefile"), b"makefile content").unwrap(); + fs::write(build_dir.join("output.o"), b"binary").unwrap(); // Should not be removed + + let removed = remove_build_makefiles(base).unwrap(); + + assert_eq!(removed, 1); // Makefile + assert!(!build_dir.join("Makefile").exists()); + assert!(build_dir.join("output.o").exists()); // Not a makefile + } +} diff --git a/src/main.rs b/src/main.rs index c64a3fd4f..7dc87d229 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ use crate::buildpack_config::{ConfigValue, ConfigValueSource}; +use crate::context::NodeJsBuildContext; +use crate::layer_cleanup::cleanup_layer; use crate::o11y::*; use crate::package_manager::InstalledPackageManager; use crate::utils::error_handling::{ErrorMessage, on_framework_error}; @@ -18,6 +20,8 @@ use libcnb_test as _; use toml::Table; mod buildpack_config; +mod context; +mod layer_cleanup; mod o11y; mod package_json; mod package_manager; @@ -27,7 +31,7 @@ mod runtimes; mod utils; type BuildpackDetectContext = libcnb::detect::DetectContext; -type BuildpackBuildContext = libcnb::build::BuildContext; +type BuildpackBuildContext = NodeJsBuildContext; type BuildpackError = libcnb::Error; type BuildpackResult = Result; @@ -70,8 +74,11 @@ impl libcnb::Buildpack for NodeJsBuildpack { #[allow(clippy::too_many_lines)] fn build( &self, - context: BuildpackBuildContext, + context: libcnb::build::BuildContext, ) -> libcnb::Result { + // Wrap the context to track layers needing cleanup + let context = NodeJsBuildContext::new(context); + let buildpack_start = print::buildpack( context .buildpack_descriptor @@ -236,6 +243,16 @@ impl libcnb::Buildpack for NodeJsBuildpack { ), )?; + // Clean up non-deterministic build artifacts from registered layers + let layers_to_cleanup = context.layers_to_cleanup(); + if !layers_to_cleanup.is_empty() { + for layer_to_cleanup in layers_to_cleanup { + if let Err(e) = cleanup_layer(&layer_to_cleanup) { + print::sub_bullet(format!("- Error during cleanup: {e}")); + } + } + } + print::all_done(&Some(buildpack_start)); build_result_builder.store(store).build() diff --git a/src/package_managers/npm.rs b/src/package_managers/npm.rs index f27efd130..f9ed6cebc 100644 --- a/src/package_managers/npm.rs +++ b/src/package_managers/npm.rs @@ -1,3 +1,4 @@ +use crate::utils::build_env::node_gyp_env; use crate::utils::error_handling::ErrorType::Internal; use crate::utils::error_handling::{ ErrorMessage, ErrorType, SuggestRetryBuild, SuggestSubmitIssue, error_message, @@ -75,8 +76,8 @@ pub(crate) fn install_npm( env, npm_packument, node_version, - ) - .map_err(Into::into) + )?; + Ok(()) } pub(crate) fn install_npm_dependencies( @@ -105,8 +106,13 @@ pub(crate) fn install_npm_dependencies( .named_output() .map_err(|e| create_set_npm_cache_directory_command_error(&e))?; - print::sub_stream_cmd(Command::new("npm").args(["ci"]).envs(env)) - .map_err(|e| create_npm_install_error(&e))?; + print::sub_stream_cmd( + Command::new("npm") + .args(["ci"]) + .envs(env) + .envs(node_gyp_env()), + ) + .map_err(|e| create_npm_install_error(&e))?; Ok(()) } diff --git a/src/package_managers/pnpm.rs b/src/package_managers/pnpm.rs index 27908a33c..7522855f6 100644 --- a/src/package_managers/pnpm.rs +++ b/src/package_managers/pnpm.rs @@ -1,4 +1,6 @@ +use crate::layer_cleanup::{LayerCleanupTarget, LayerKind}; use crate::package_json::PackageJson; +use crate::utils::build_env::node_gyp_env; use crate::utils::error_handling::{ ErrorMessage, ErrorType, SuggestRetryBuild, SuggestSubmitIssue, error_message, file_value, }; @@ -45,8 +47,8 @@ pub(crate) fn install_pnpm( env, pnpm_packument, node_version, - ) - .map_err(Into::into) + )?; + Ok(()) } pub(crate) fn install_dependencies( @@ -67,7 +69,8 @@ pub(crate) fn install_dependencies( print::sub_stream_cmd( Command::new("pnpm") .args(["install", "--frozen-lockfile"]) - .envs(env), + .envs(env) + .envs(node_gyp_env()), ) .map_err(|e| create_pnpm_install_command_error(&e))?; @@ -183,6 +186,12 @@ fn create_virtual_store_directory(context: &BuildpackBuildContext) -> BuildpackR Err(create_node_modules_symlink_error(&error))?; } + // Register virtual layer for cleanup of non-deterministic Makefiles + context.register_layer_for_cleanup(LayerCleanupTarget { + path: pnpm_virtual_store_layer.path().clone(), + kind: LayerKind::Virtual, + }); + Ok(virtual_store_dir) } diff --git a/src/package_managers/yarn.rs b/src/package_managers/yarn.rs index 2af27aa80..e19b8f74d 100644 --- a/src/package_managers/yarn.rs +++ b/src/package_managers/yarn.rs @@ -1,4 +1,5 @@ use crate::o11y::*; +use crate::utils::build_env::node_gyp_env; use crate::utils::error_handling::{ ErrorMessage, ErrorType, SuggestRetryBuild, SuggestSubmitIssue, error_message, file_value, }; @@ -57,14 +58,15 @@ pub(crate) fn install_yarn( yarn_packument: &PackagePackument, node_version: &Version, ) -> BuildpackResult<()> { + // Note: yarn layer path is returned but not used for cleanup registration utils::npm_registry::install_package_layer( layer_name!("yarn"), context, env, yarn_packument, node_version, - ) - .map_err(Into::into) + )?; + Ok(()) } #[instrument(skip_all)] @@ -283,6 +285,7 @@ pub(crate) fn install_dependencies( print::bullet("Installing dependencies"); let mut yarn_install_command = Command::new("yarn"); yarn_install_command.envs(env); + yarn_install_command.envs(node_gyp_env()); yarn_install_command.arg("install"); if version.major() == 1 { yarn_install_command.args(["--production=false", "--frozen-lockfile"]); diff --git a/src/utils/build_env.rs b/src/utils/build_env.rs index d5ea75f11..5b6f96c01 100644 --- a/src/utils/build_env.rs +++ b/src/utils/build_env.rs @@ -34,3 +34,14 @@ pub(crate) fn set_default_env_var( Ok(()) } + +pub(crate) fn node_gyp_env() -> Vec<(String, String)> { + vec![ + // If this is set to a non-empty string, Python won’t try to write .pyc files on the import of source modules. + // see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE + // + // This is used to prevent node-gyp from generating files which invalidate runtime layers whenever + // native modules are compiled as `node-gyp` uses Python as part of the compilation process. + ("PYTHONDONTWRITEBYTECODE".to_string(), "1".to_string()), + ] +} diff --git a/src/utils/npm_registry.rs b/src/utils/npm_registry.rs index 8cf4ba857..3c80624f7 100644 --- a/src/utils/npm_registry.rs +++ b/src/utils/npm_registry.rs @@ -20,7 +20,7 @@ use libcnb::layer_env::Scope; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::{fs, io}; const NPMJS_ORG_HOST: &str = "https://registry.npmjs.org"; @@ -335,7 +335,7 @@ pub(crate) fn install_package_layer( env: &mut Env, package_packument: &PackagePackument, node_version: &Version, -) -> Result<(), InstallPackageLayerError> { +) -> Result { let package_name = &package_packument.name; let package_version = &package_packument.version; @@ -433,7 +433,7 @@ pub(crate) fn install_package_layer( env.clone_from(&layer_env.apply(Scope::Build, env)); - Ok(()) + Ok(install_package_layer.path().clone()) } pub(crate) enum InstallPackageLayerError { diff --git a/tests/npm_integration_test.rs b/tests/npm_integration_test.rs index b050e2696..6553fc7ae 100644 --- a/tests/npm_integration_test.rs +++ b/tests/npm_integration_test.rs @@ -240,6 +240,27 @@ fn test_npm_native_modules_are_recompiled_even_on_cache_restore() { ); } +#[test] +#[ignore = "integration test"] +fn test_npm_engine_native_modules_are_recompiled_even_on_cache_restore() { + nodejs_integration_test_with_config( + "./fixtures/npm-project-with-native-module", + |config| { + config.env("npm_config_foreground-scripts", "true"); + config.app_dir_preprocessor(|app_dir| { + set_npm_engine(&app_dir, "11.x"); + }); + }, + |ctx| { + let build_snapshot = create_build_snapshot(&ctx.pack_stdout); + let config = ctx.config.clone(); + ctx.rebuild(config, |ctx| { + build_snapshot.rebuild_output(&ctx.pack_stdout).assert(); + }); + }, + ); +} + #[test] #[ignore = "integration test"] fn test_npm_skip_build_scripts_from_buildplan() { diff --git a/tests/snapshots/pnpm_7_pnp.snap b/tests/snapshots/pnpm_7_pnp.snap index bc7d1aa48..e87e3113c 100644 --- a/tests/snapshots/pnpm_7_pnp.snap +++ b/tests/snapshots/pnpm_7_pnp.snap @@ -64,12 +64,13 @@ heroku/nodejs devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/pnpm_8_hoist.snap b/tests/snapshots/pnpm_8_hoist.snap index 0cf95c823..d24b4f430 100644 --- a/tests/snapshots/pnpm_8_hoist.snap +++ b/tests/snapshots/pnpm_8_hoist.snap @@ -67,12 +67,13 @@ heroku/nodejs devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/pnpm_8_nuxt.snap b/tests/snapshots/pnpm_8_nuxt.snap index 34e507e03..6f60c508f 100644 --- a/tests/snapshots/pnpm_8_nuxt.snap +++ b/tests/snapshots/pnpm_8_nuxt.snap @@ -91,12 +91,13 @@ heroku/nodejs ! ! Since pruning can't be done safely for your build, it will be skipped. To fix this you must upgrade your version of pnpm to 8.15.6 or higher. +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/pnpm_install_engine.snap b/tests/snapshots/pnpm_install_engine.snap index cf54260bf..85cc3806f 100644 --- a/tests/snapshots/pnpm_install_engine.snap +++ b/tests/snapshots/pnpm_install_engine.snap @@ -68,6 +68,7 @@ test/print-build-env 0.0.0 devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer - Done (finished in ) HEROKU_AVAILABLE_PARALLELISM= PATH=/workspace/node_modules/.bin:/layers/heroku_nodejs/pnpm/bin:/layers/heroku_nodejs/dist/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin @@ -75,7 +76,7 @@ PATH=/workspace/node_modules/.bin:/layers/heroku_nodejs/pnpm/bin:/layers/heroku_ Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/pnpm_install_package_manager.snap b/tests/snapshots/pnpm_install_package_manager.snap index e7027c754..9da60807b 100644 --- a/tests/snapshots/pnpm_install_package_manager.snap +++ b/tests/snapshots/pnpm_install_package_manager.snap @@ -67,12 +67,13 @@ heroku/nodejs devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_npm_engine_native_modules_are_recompiled_even_on_cache_restore.snap b/tests/snapshots/test_npm_engine_native_modules_are_recompiled_even_on_cache_restore.snap new file mode 100644 index 000000000..0211b2867 --- /dev/null +++ b/tests/snapshots/test_npm_engine_native_modules_are_recompiled_even_on_cache_restore.snap @@ -0,0 +1,167 @@ +--- +source: crates/test_support/src/lib.rs +--- +===> ANALYZING +Image with name "" not found +===> DETECTING +heroku/nodejs +===> RESTORING + +===> BUILDING + +## Heroku Node.js + +- Checking Node.js version + - Detected Node.js version range: `^20.0` + - Resolved Node.js version: `20.20.0` +- Installing Node.js distribution + - GET https://nodejs.org/download/release/v20.20.0/node-v20.20.0-.tar.gz ... () + - Validating ... () + - Extracting ... () + - Verifying checksum + - Extracting Node.js `20.20.0 ()` + - Installing Node.js `20.20.0 ()` ... () +- Determining npm package information + - Found `engines.npm` version `11.x` declared in `package.json` + - GET https://registry.npmjs.org/npm ... () + - Resolved npm version `11.x` to `11.10.1` +- Installing npm + - GET https://registry.npmjs.org/npm/-/npm-11.10.1.tgz ... () + - Extracting ... () + - Successfully installed `npm@11.10.1` +- Installing node modules + - Using npm version `11.10.1` + - Creating npm cache + - Configuring npm cache directory + - Running `npm ci` + + + > dtrace-provider@0.8.8 install + > node-gyp rebuild || node suppress-error.js + + + + added packages, and audited packages in + + found 0 vulnerabilities + + - Done () +- Running scripts + - No build scripts found +- Pruning dev dependencies + - Running `npm prune` + + + up to date, audited packages in + + found 0 vulnerabilities + + - Done () +- Configuring default processes + - Skipping default web process (no start script defined) +- Done (finished in ) +===> EXPORTING +Adding layer 'heroku/nodejs:available_parallelism' +Adding layer 'heroku/nodejs:dist' +Adding layer 'heroku/nodejs:npm_engine' +Adding layer 'heroku/nodejs:npm_runtime_config' +Adding layer 'heroku/nodejs:web_env' +Adding layer 'heroku/nodejs:z_node_module_bins' +Adding layer 'buildpacksio/lifecycle:launch.sbom' +Added 1/1 app layer(s) +Adding layer 'buildpacksio/lifecycle:launcher' +Adding layer 'buildpacksio/lifecycle:config' +Adding label 'io.buildpacks.lifecycle.metadata' +Adding label 'io.buildpacks.build.metadata' +Adding label 'io.buildpacks.project.metadata' +no default process type +Saving ... +*** Images (): + +Adding cache layer 'heroku/nodejs:dist' +Adding cache layer 'heroku/nodejs:npm_cache' +Adding cache layer 'heroku/nodejs:npm_engine' +Adding cache layer 'heroku/nodejs:npm_packument' +Successfully built image '' + +--------------------------------------------- REBUILD --------------------------------------------- +===> ANALYZING +Restoring data for SBOM from previous image +===> DETECTING +heroku/nodejs +===> RESTORING + +===> BUILDING + +## Heroku Node.js + +- Checking Node.js version + - Detected Node.js version range: `^20.0` + - Resolved Node.js version: `20.20.0` +- Installing Node.js distribution + - Reusing Node.js 20.20.0 () +- Determining npm package information + - Found `engines.npm` version `11.x` declared in `package.json` + - GET https://registry.npmjs.org/npm ... (Not Modified) + - Using cached packument for npm + - Resolved npm version `11.x` to `11.10.1` +- Installing npm + - Using cached version of npm + - Successfully installed `npm@11.10.1` +- Installing node modules + - Using npm version `11.10.1` + - Restoring npm cache + - Configuring npm cache directory + - Running `npm ci` + + + > dtrace-provider@0.8.8 install + > node-gyp rebuild || node suppress-error.js + + + + added packages, and audited packages in + + found 0 vulnerabilities + + - Done () +- Running scripts + - No build scripts found +- Pruning dev dependencies + - Running `npm prune` + + + up to date, audited packages in + + found 0 vulnerabilities + + - Done () +- Configuring default processes + - Skipping default web process (no start script defined) +- Done (finished in ) +===> EXPORTING +Reusing layer 'heroku/nodejs:available_parallelism' +Reusing layer 'heroku/nodejs:dist' +Reusing layer 'heroku/nodejs:npm_engine' +Reusing layer 'heroku/nodejs:npm_runtime_config' +Reusing layer 'heroku/nodejs:web_env' +Reusing layer 'heroku/nodejs:z_node_module_bins' +Reusing layer 'buildpacksio/lifecycle:launch.sbom' +Reused 1/1 app layer(s) +Reusing layer 'buildpacksio/lifecycle:launcher' +Reusing layer 'buildpacksio/lifecycle:config' +Adding label 'io.buildpacks.lifecycle.metadata' +Adding label 'io.buildpacks.build.metadata' +Adding label 'io.buildpacks.project.metadata' +no default process type +Saving ... +*** Images (): + +Reusing cache layer 'heroku/nodejs:dist' +Adding cache layer 'heroku/nodejs:dist' +Adding cache layer 'heroku/nodejs:npm_cache' +Reusing cache layer 'heroku/nodejs:npm_engine' +Adding cache layer 'heroku/nodejs:npm_engine' +Reusing cache layer 'heroku/nodejs:npm_packument' +Adding cache layer 'heroku/nodejs:npm_packument' +Successfully built image '' diff --git a/tests/snapshots/test_npm_native_modules_are_recompiled_even_on_cache_restore.snap b/tests/snapshots/test_npm_native_modules_are_recompiled_even_on_cache_restore.snap index 88164b36c..87cb03f92 100644 --- a/tests/snapshots/test_npm_native_modules_are_recompiled_even_on_cache_restore.snap +++ b/tests/snapshots/test_npm_native_modules_are_recompiled_even_on_cache_restore.snap @@ -122,7 +122,7 @@ heroku/nodejs - Done (finished in ) ===> EXPORTING Reusing layer 'heroku/nodejs:available_parallelism' -Adding layer 'heroku/nodejs:dist' +Reusing layer 'heroku/nodejs:dist' Reusing layer 'heroku/nodejs:npm_runtime_config' Reusing layer 'heroku/nodejs:web_env' Reusing layer 'heroku/nodejs:z_node_module_bins' @@ -137,6 +137,7 @@ no default process type Saving ... *** Images (): +Reusing cache layer 'heroku/nodejs:dist' Adding cache layer 'heroku/nodejs:dist' Adding cache layer 'heroku/nodejs:npm_cache' Successfully built image '' diff --git a/tests/snapshots/test_pnpm_10_workspace.snap b/tests/snapshots/test_pnpm_10_workspace.snap index 1a377f402..83744a24e 100644 --- a/tests/snapshots/test_pnpm_10_workspace.snap +++ b/tests/snapshots/test_pnpm_10_workspace.snap @@ -69,12 +69,13 @@ heroku/nodejs Done in - Done () +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_pnpm_default_web_process_registration_is_skipped_if_procfile_exists.snap b/tests/snapshots/test_pnpm_default_web_process_registration_is_skipped_if_procfile_exists.snap index f0854d2d8..a51c2bcf7 100644 --- a/tests/snapshots/test_pnpm_default_web_process_registration_is_skipped_if_procfile_exists.snap +++ b/tests/snapshots/test_pnpm_default_web_process_registration_is_skipped_if_procfile_exists.snap @@ -70,12 +70,13 @@ heroku/nodejs - Done () - Skipping default web process (Procfile detected) +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_pnpm_native_modules_are_recompiled_even_on_cache_restore.snap b/tests/snapshots/test_pnpm_native_modules_are_recompiled_even_on_cache_restore.snap index cc5af0620..36ffd81a3 100644 --- a/tests/snapshots/test_pnpm_native_modules_are_recompiled_even_on_cache_restore.snap +++ b/tests/snapshots/test_pnpm_native_modules_are_recompiled_even_on_cache_restore.snap @@ -67,12 +67,14 @@ heroku/nodejs devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer + - Removed 1 Makefile artifacts - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' @@ -146,12 +148,14 @@ heroku/nodejs devDependencies: skipped - Done () +- Cleaning up pnpm virtual store layer + - Removed 1 Makefile artifacts - Done (finished in ) ===> EXPORTING Reusing layer 'heroku/nodejs:available_parallelism' Reusing layer 'heroku/nodejs:dist' -Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Reusing layer 'heroku/nodejs:pnpm' +Reusing layer 'heroku/nodejs:virtual' Reusing layer 'heroku/nodejs:web_env' Reusing layer 'heroku/nodejs:z_node_module_bins' Reusing layer 'buildpacksio/lifecycle:launch.sbom' @@ -168,6 +172,7 @@ Saving ... Adding cache layer 'heroku/nodejs:addressable' Reusing cache layer 'heroku/nodejs:dist' Adding cache layer 'heroku/nodejs:dist' +Reusing cache layer 'heroku/nodejs:pnpm' Adding cache layer 'heroku/nodejs:pnpm' Reusing cache layer 'heroku/nodejs:pnpm_packument' Adding cache layer 'heroku/nodejs:pnpm_packument' diff --git a/tests/snapshots/test_pnpm_prune_dev_dependencies_config.snap b/tests/snapshots/test_pnpm_prune_dev_dependencies_config.snap index 69af27c6f..e400f34a8 100644 --- a/tests/snapshots/test_pnpm_prune_dev_dependencies_config.snap +++ b/tests/snapshots/test_pnpm_prune_dev_dependencies_config.snap @@ -64,12 +64,13 @@ heroku/nodejs ! Warning: Experimental configuration `com.heroku.buildpacks.nodejs.actions.prune_dev_dependencies` found in `project.toml`. This feature may change unexpectedly in the future. +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_pnpm_skip_build_scripts_from_buildplan.snap b/tests/snapshots/test_pnpm_skip_build_scripts_from_buildplan.snap index 6e8dd5762..91c6c4a3d 100644 --- a/tests/snapshots/test_pnpm_skip_build_scripts_from_buildplan.snap +++ b/tests/snapshots/test_pnpm_skip_build_scripts_from_buildplan.snap @@ -66,12 +66,13 @@ test/skip-build-scripts 0.0.0 ! Warning: Experimental configuration `node_build_scripts.metadata.skip_pruning` was added to the buildplan by a later buildpack. This feature may change unexpectedly in the future. +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_root_project.snap b/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_root_project.snap index 03407b962..c15ee9276 100644 --- a/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_root_project.snap +++ b/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_root_project.snap @@ -72,12 +72,13 @@ heroku/nodejs ! ! Since pruning can't be done safely for your build, it will be skipped. +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom' diff --git a/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_workspace_project.snap b/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_workspace_project.snap index bcb85d171..866dd8638 100644 --- a/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_workspace_project.snap +++ b/tests/snapshots/test_pnpm_workspace_prune_skipped_if_lifecycle_scripts_are_present_in_workspace_project.snap @@ -72,12 +72,13 @@ heroku/nodejs ! ! Since pruning can't be done safely for your build, it will be skipped. +- Cleaning up pnpm virtual store layer - Done (finished in ) ===> EXPORTING Adding layer 'heroku/nodejs:available_parallelism' Adding layer 'heroku/nodejs:dist' Adding layer 'heroku/nodejs:pnpm' - layer 'heroku/nodejs:virtual' +Adding layer 'heroku/nodejs:virtual' Adding layer 'heroku/nodejs:web_env' Adding layer 'heroku/nodejs:z_node_module_bins' Adding layer 'buildpacksio/lifecycle:launch.sbom'