diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 999261db..103dd0af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2.6.0 - name: Install Pack CLI - uses: buildpacks/github-actions/setup-pack@v5.3.1 + uses: buildpacks/github-actions/setup-pack@v5.4.0 - name: Run integration tests # Runs only tests annotated with the `ignore` attribute (which in this repo, are the integration tests). run: cargo test -- --ignored @@ -73,4 +73,4 @@ jobs: # Uses a non-libc image to validate the static musl cross-compilation. # TODO: Switch this back to using the `alpine` tag once the stable Pack CLI release supports # image extensions (currently newer sample alpine images fail to build with stable Pack). - run: pack build example-basics --builder cnbs/sample-builder@sha256:da5ff69191919f1ff30d5e28859affff8e39f23038137c7751e24a42e919c1ab --trust-builder --buildpack target/buildpack/x86_64-unknown-linux-musl/debug/libcnb-examples_basics --path examples/ + run: pack build example-basics --builder cnbs/sample-builder@sha256:da5ff69191919f1ff30d5e28859affff8e39f23038137c7751e24a42e919c1ab --trust-builder --buildpack packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_basics --path examples/ diff --git a/.gitignore b/.gitignore index c0f3e140..6addfe54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /target/ +/packaged/ .DS_Store .idea Cargo.lock **/fixtures/*/target/ +**/fixtures/*/packaged/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b7a85c..1ef7d13c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,39 +4,56 @@ This is the new, unified, changelog that contains changes from across all libcnb separate changelogs for each crate were used. If you need to refer to these old changelogs, find them named `HISTORICAL_CHANGELOG.md` in their respective crate directories. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [Unreleased] ### Added -- `libcnb-package`: - - Add cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577)) - - Added the `output::BuildpackOutputDirectoryLocator` struct which contains information on how compiled buildpack directories are structured and provides a `.get(buildpack_id)` method which produces the output path for a buildpack. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - - Added `output::assemble_single_buildpack_directory` and `output::assemble_meta_buildpack_directory` which construct buildpack output directories with all their required files during packaging. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) +- `libcnb-package`: + - Added the `output::create_packaged_buildpack_dir_resolver` helper which contains all the information on how compiled buildpack directories are structured returns a function that can be invoked with `BuildpackId` to produce the output path for a buildpack. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) + - Added `output::assemble_single_buildpack_directory` and `output::assemble_meta_buildpack_directory` which construct buildpack output directories with all their required files during packaging. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) ### Changed -- `libcnb-test`: - - `ContainerContext::address_for_port` now returns `SocketAddr` directly instead of `Option`. ([#605](https://github.com/heroku/libcnb.rs/pull/605)) - - Docker commands are now run using the Docker CLI instead of Bollard and the Docker daemon API. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) - - `ContainerConfig::entrypoint` now accepts a String rather than a vector of strings. Any arguments to the entrypoint should be moved to `ContainerConfig::command`. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) - - `TestRunner::new` has been removed, since its only purpose was for advanced configuration that's no longer applicable. Use `TestRunner::default` instead. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) - - `LogOutput` no longer exposes `stdout_raw` and `stderr_raw`. ([#607](https://github.com/heroku/libcnb.rs/pull/607)) - - Improved wording of panic error messages. ([#619](https://github.com/heroku/libcnb.rs/pull/619) and [#620](https://github.com/heroku/libcnb.rs/pull/620)) -- `libcnb-package`: - - buildpack target directory now contains the target triple. Users that implicitly rely on the output directory need to adapt. The output of `cargo libcnb package` will refer to the new locations. ([#580](https://github.com/heroku/libcnb.rs/pull/580)) +- `libcnb-package`: - Changed `buildpack_dependency::rewrite_buildpackage_local_dependencies` to accept a `&BuildpackOutputDirectoryLocator` instead of `&HashMap<&BuildpackId, PathBuf>`. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - Moved `default_buildpack_directory_name` to `output::default_buildpack_directory_name`. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - Changed `build::build_buildpack_binaries` to drop the cargo_metadata argument since it can read that directly from the given project_path. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - Changed `build::BuildBinariesError` to include the error variant `ReadCargoMetadata(PathBuf, cargo_metadata::Error)`. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) -- `libherokubuildpack`: Switch the `flate2` decompression backend from `miniz_oxide` to `zlib`. ([#593](https://github.com/heroku/libcnb.rs/pull/593)) -- Bump minimum external dependency versions. ([#587](https://github.com/heroku/libcnb.rs/pull/587)) ### Removed - `libcnb-package`: - - `get_buildpack_target_dir` has been removed in favor of `output::BuildpackOutputDirectoryLocator` for building output paths to compiled buildpacks. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) + - `get_buildpack_package_dir` has been removed in favor of `output::create_packaged_buildpack_dir_resolver` for building output paths to compiled buildpacks. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - `assemble_buildpack_directory` has been removed in favor of `output::assemble_single_buildpack_directory` and `output::assemble_meta_buildpack_directory`. ([#632](https://github.com/heroku/libcnb.rs/pull/632)) - + +## [0.14.0] - 2023-08-18 + +### Added + +- `libcnb-package`: Added cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577)) +- `libcnb-cargo`: Added `--package-dir` command line option to control where packaged buildpacks are written. ([#583](https://github.com/heroku/libcnb.rs/pull/583)) +- `libcnb-test`: + - `LogOutput` now implements `std::fmt::Display`. ([#635](https://github.com/heroku/libcnb.rs/pull/635)) + - `ContainerConfig` now implements `Clone`. ([#636](https://github.com/heroku/libcnb.rs/pull/636)) + +### Changed + +- `libcnb-cargo`: Moved the default location for packaged buildpacks from Cargo's `target/` directory to `packaged/` in the Cargo workspace root. This simplifies the path and stops modification of the `target/` directory which previously might have caching implications when other tools didn't expect non-Cargo output in that directory. Users that implicitly rely on the output directory need to adapt. The output of `cargo libcnb package` will refer to the new locations. ([#583](https://github.com/heroku/libcnb.rs/pull/583)) +- `libcnb-package`: + - buildpack target directory now contains the target triple. Users that implicitly rely on the output directory need to adapt. The output of `cargo libcnb package` will refer to the new locations. ([#580](https://github.com/heroku/libcnb.rs/pull/580)) + - `get_buildpack_target_dir` was renamed to `get_buildpack_package_dir` ([#583](https://github.com/heroku/libcnb.rs/pull/583)) +- `libcnb-test`: + - `ContainerContext::address_for_port` will now panic for all failure modes rather than just some, and so now returns `SocketAddr` directly instead of `Option`. This reduces test boilerplate due to the caller no longer needing to `.unwrap()` and improves debugging UX when containers crash after startup. ([#605](https://github.com/heroku/libcnb.rs/pull/605) and [#636](https://github.com/heroku/libcnb.rs/pull/636)) + - Docker commands are now run using the Docker CLI instead of Bollard and the Docker daemon API. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) + - `ContainerConfig::entrypoint` now accepts a String rather than a vector of strings. Any arguments to the entrypoint should be moved to `ContainerConfig::command`. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) + - Removed `TestRunner::new` since its only purpose was for advanced configuration that's no longer applicable. Use `TestRunner::default` instead. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) + - Removed `stdout_raw` and `stderr_raw` from `LogOutput`. ([#607](https://github.com/heroku/libcnb.rs/pull/607)) + - Improved wording of panic error messages. ([#619](https://github.com/heroku/libcnb.rs/pull/619) and [#620](https://github.com/heroku/libcnb.rs/pull/620)) +- `libherokubuildpack`: Changed the `flate2` decompression backend from `miniz_oxide` to `zlib`. ([#593](https://github.com/heroku/libcnb.rs/pull/593)) + ### Fixed - `libcnb-test`: @@ -44,7 +61,7 @@ separate changelogs for each crate were used. If you need to refer to these old - `ContainerContext::expose_port` now only exposes the port to localhost. ([#610](https://github.com/heroku/libcnb.rs/pull/610)) - If a test with an expected result of `PackResult::Failure` unexpectedly succeeds, the built app image is now correctly cleaned up. ([#625](https://github.com/heroku/libcnb.rs/pull/625)) -## [0.13.0] 2023-06-21 +## [0.13.0] - 2023-06-21 The highlight of this release is the `cargo libcnb package` changes to support compilation of both buildpacks and meta-buildpacks. @@ -83,7 +100,7 @@ The highlight of this release is the `cargo libcnb package` changes to support c `dependency_graph::get_dependencies` to support dependency ordering and resolution in libcnb.rs-based Rust packages. ([#575](https://github.com/heroku/libcnb.rs/pull/575)) -## [0.12.0] 2023-04-28 +## [0.12.0] - 2023-04-28 Highlight of this release is the bump to [Buildpack API 0.9](https://github.com/buildpacks/spec/releases/tag/buildpack%2Fv0.9). This release contains breaking changes, please refer to the items below for migration advice. @@ -99,26 +116,26 @@ Highlight of this release is the bump to [Buildpack API 0.9](https://github.com/ - `Env::get_string_lossy` as a convenience method to work with environment variables directly. Getting a value out of an `Env` and treating its contents as unicode is a common case. Using this new method can simplify buildpack code. ([#565](https://github.com/heroku/libcnb.rs/pull/565)) - `Clone` implementation for `libcnb::layer_env::Scope`. ([#566](https://github.com/heroku/libcnb.rs/pull/566)) -## [0.11.5] 2023-02-07 +## [0.11.5] - 2023-02-07 ### Changed - Update `toml` to `0.7.1`. If your buildpack interacts with TOML data directly, you probably want to bump the `toml` version in your buildpack as well. ([#556](https://github.com/heroku/libcnb.rs/pull/556)) -## [0.11.4] 2023-01-11 +## [0.11.4] - 2023-01-11 ### Added - libcnb-data: Store struct now supports `clone()` and `default()`. ([#547](https://github.com/heroku/libcnb.rs/pull/547)) -## [0.11.3] 2023-01-09 +## [0.11.3] - 2023-01-09 ### Added - libcnb: Add `store` field to `BuildContext`, exposing the contents of `store.toml` if present. ([#543](https://github.com/heroku/libcnb.rs/pull/543)) -## [0.11.2] 2022-12-15 +## [0.11.2] - 2022-12-15 ### Fixed @@ -133,7 +150,7 @@ the `toml` version in your buildpack as well. ([#556](https://github.com/heroku/ - libherokubuildpack: Add `command` and `write` modules for working with `std::process::Command` output streams. ([#535](https://github.com/heroku/libcnb.rs/pull/535)) -## [0.11.1] 2022-09-29 +## [0.11.1] - 2022-09-29 ### Fixed @@ -144,7 +161,7 @@ the `toml` version in your buildpack as well. ([#556](https://github.com/heroku/ - Improve the `libherokubuildpack` root module rustdocs. ([#503](https://github.com/heroku/libcnb.rs/pull/503)) -## [0.11.0] 2022-09-23 +## [0.11.0] - 2022-09-23 ### Changed @@ -155,7 +172,7 @@ the `toml` version in your buildpack as well. ([#556](https://github.com/heroku/ - Add new crate `libherokubuildpack` with common code that can be useful when implementing buildpacks with libcnb. Originally hosted in a separate, private, repository. Code from `libherokubuildpack` might eventually find its way into libcnb.rs proper. At this point, consider it an incubator. ([#495](https://github.com/heroku/libcnb.rs/pull/495)) -## [0.10.0] 2022-08-31 +## [0.10.0] - 2022-08-31 Highlight of this release is the bump to [Buildpack API 0.8](https://github.com/buildpacks/spec/releases/tag/buildpack%2Fv0.8) which brings support for SBOM to @@ -186,3 +203,15 @@ version number. See the changelog below for other changes. ### Removed - Remove support for legacy BOM. Remove `Launch::bom`, `Build::bom`, `bom::Bom`, `bom::Entry`. ([#489](https://github.com/heroku/libcnb.rs/pull/489)) + +[unreleased]: https://github.com/heroku/libcnb.rs/compare/v0.14.0...HEAD +[0.14.0]: https://github.com/heroku/libcnb.rs/compare/v0.13.0...v0.14.0 +[0.13.0]: https://github.com/heroku/libcnb.rs/compare/v0.12.0...v0.13.0 +[0.12.0]: https://github.com/heroku/libcnb.rs/compare/v0.11.5...v0.12.0 +[0.11.5]: https://github.com/heroku/libcnb.rs/compare/v0.11.4...v0.11.5 +[0.11.4]: https://github.com/heroku/libcnb.rs/compare/v0.11.3...v0.11.4 +[0.11.3]: https://github.com/heroku/libcnb.rs/compare/v0.11.2...v0.11.3 +[0.11.2]: https://github.com/heroku/libcnb.rs/compare/v0.11.1...v0.11.2 +[0.11.1]: https://github.com/heroku/libcnb.rs/compare/v0.11.0...v0.11.1 +[0.11.0]: https://github.com/heroku/libcnb.rs/compare/v0.10.0...v0.11.0 +[0.10.0]: https://github.com/heroku/libcnb.rs/compare/libcnb/v0.9.0...v0.10.0 diff --git a/Cargo.toml b/Cargo.toml index 30049870..d168c765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,15 +17,15 @@ members = [ ] [workspace.package] -version = "0.13.0" +version = "0.14.0" rust-version = "1.64" edition = "2021" license = "BSD-3-Clause" [workspace.dependencies] -libcnb = { version = "0.13.0", path = "libcnb" } -libcnb-data = { version = "0.13.0", path = "libcnb-data" } -libcnb-package = { version = "0.13.0", path = "libcnb-package" } -libcnb-proc-macros = { version = "0.13.0", path = "libcnb-proc-macros" } -libcnb-test = { version = "0.13.0", path = "libcnb-test" } -toml = { version = "0.7.5" } +libcnb = { version = "=0.14.0", path = "libcnb" } +libcnb-data = { version = "=0.14.0", path = "libcnb-data" } +libcnb-package = { version = "=0.14.0", path = "libcnb-package" } +libcnb-proc-macros = { version = "=0.14.0", path = "libcnb-proc-macros" } +libcnb-test = { version = "=0.14.0", path = "libcnb-test" } +toml = { version = "0.7.6" } diff --git a/README.md b/README.md index 6bbf3e15..e59be3b9 100644 --- a/README.md +++ b/README.md @@ -178,16 +178,22 @@ In your project directory, run `cargo libcnb package` to start packaging: ```shell $ cargo libcnb package -INFO - Reading buildpack metadata... -INFO - Found buildpack libcnb-examples/my-buildpack with version 0.1.0. -INFO - Determining automatic cross-compile settings... -INFO - Building binaries (x86_64-unknown-linux-musl)... +šŸ” Locating buildpacks... +šŸ“¦ [1/1] Building libcnb-examples/my-buildpack +Determining automatic cross-compile settings... +Building binaries (x86_64-unknown-linux-musl)... # Omitting compilation output... - Finished dev [unoptimized] target(s) in 8.51s -INFO - Writing buildpack directory... -INFO - Successfully wrote buildpack directory: target/buildpack/debug/libcnb-examples_my-buildpack (3.03 MiB) -INFO - Packaging successfully finished! -INFO - Hint: To test your buildpack locally with pack, run: pack build my-image --buildpack target/buildpack/debug/libcnb-examples_my-buildpack --path /path/to/application + Finished dev [unoptimized] target(s) in 8.92s +Writing buildpack directory... +Successfully wrote buildpack directory: packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_my-buildpack (4.06 MiB) +āœØ Packaging successfully finished! + +šŸ’” To test your buildpack locally with pack, run: +pack build my-image-name \ + --buildpack /home/ponda.baba/my-buildpack/packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_my-buildpack \ + --path /path/to/application + +/home/ponda.baba/my-buildpack/packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_my-buildpack ``` If you get errors with hints about how to install required tools to cross-compile from your host platform to the @@ -202,10 +208,10 @@ application code at all, we just create an empty directory and use that as our a ```shell $ mkdir bogus-app -$ pack build my-image --buildpack target/buildpack/debug/libcnb-examples_my-buildpack --path bogus-app --builder heroku/builder:22 +$ pack build my-image --buildpack packaged/x86_64-unknown-linux-musl/debug/libcnb-examples_my-buildpack --path bogus-app --builder heroku/builder:22 ... ===> ANALYZING -Previous image with name "my-image" not found +Image with name "my-image" not found ===> DETECTING libcnb-examples/my-buildpack 0.1.0 ===> RESTORING @@ -213,17 +219,17 @@ libcnb-examples/my-buildpack 0.1.0 Hello World! Build runs on stack heroku-22! ===> EXPORTING -Adding layer 'launch.sbom' +Adding layer 'buildpacksio/lifecycle:launch.sbom' Adding 1/1 app layer(s) -Adding layer 'launcher' -Adding layer 'config' -Adding layer 'process-types' +Adding layer 'buildpacksio/lifecycle:launcher' +Adding layer 'buildpacksio/lifecycle:config' +Adding layer 'buildpacksio/lifecycle:process-types' Adding label 'io.buildpacks.lifecycle.metadata' Adding label 'io.buildpacks.build.metadata' Adding label 'io.buildpacks.project.metadata' Setting default process type 'web' Saving my-image... -*** Images (24eed75bb2e6): +*** Images (aa4695184718): my-image Successfully built image my-image ``` diff --git a/RELEASING.md b/RELEASING.md index 18f878a6..3397e8b0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,9 +10,12 @@ easier to gauge cross-crate compatibility. 1. In the `workspace.package` table, update `version` to the new version 2. In the `workspace.dependencies` table, update the `version` of each of the repository-local dependencies to the new version 3. Update [CHANGELOG.md](./CHANGELOG.md) - 1. Move all content under `## [Unreleased]` to a new section that follows this pattern: `## [VERSION] YYYY-MM-DD` + 1. Move all content under `## [Unreleased]` to a new section that follows this pattern: `## [VERSION] - YYYY-MM-DD` 2. If appropriate, add a high-level summary of changes at the beginning of the new section -4. Commit the changes, push them and open a PR targeting `main` + 3. Update the version compare links at the bottom of the file to both add the new version, and update the "unreleased" link's "from" version. +4. Install the latest version of [cargo-edit](https://github.com/killercup/cargo-edit): `cargo install cargo-edit` +5. Bump in-range dependency versions using: `cargo upgrade` +6. Commit the changes, push them and open a PR targeting `main` ## Release diff --git a/examples/ruby-sample/Cargo.toml b/examples/ruby-sample/Cargo.toml index d8cfbf72..3e90030b 100644 --- a/examples/ruby-sample/Cargo.toml +++ b/examples/ruby-sample/Cargo.toml @@ -6,12 +6,12 @@ rust-version.workspace = true publish = false [dependencies] -flate2 = { version = "1.0.26", default-features = false, features = ["zlib"] } +flate2 = { version = "1.0.27", default-features = false, features = ["zlib"] } libcnb.workspace = true -serde = "1.0.166" +serde = "1.0.183" sha2 = "0.10.7" -tar = { version = "0.4.38", default-features = false } -tempfile = "3.6.0" +tar = { version = "0.4.40", default-features = false } +tempfile = "3.7.1" ureq = { version = "2.7.1", default-features = false, features = ["tls"] } [dev-dependencies] diff --git a/libcnb-cargo/Cargo.toml b/libcnb-cargo/Cargo.toml index a39d1832..8132b222 100644 --- a/libcnb-cargo/Cargo.toml +++ b/libcnb-cargo/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] cargo_metadata = "0.17.0" -clap = { version = "4.3.10", default-features = false, features = [ +clap = { version = "4.3.22", default-features = false, features = [ "derive", "error-context", "help", @@ -27,9 +27,8 @@ clap = { version = "4.3.10", default-features = false, features = [ libcnb-data.workspace = true libcnb-package.workspace = true pathdiff = "0.2.1" -thiserror = "1.0.41" +thiserror = "1.0.47" toml.workspace = true [dev-dependencies] -fs_extra = "1.3.0" -tempfile = "3.6.0" +tempfile = "3.7.1" diff --git a/libcnb-cargo/src/cli.rs b/libcnb-cargo/src/cli.rs index c6f7e64c..86290a0c 100644 --- a/libcnb-cargo/src/cli.rs +++ b/libcnb-cargo/src/cli.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand}; +use std::path::PathBuf; #[derive(Parser)] #[command(bin_name = "cargo")] @@ -25,6 +26,9 @@ pub(crate) struct PackageArgs { /// Build for the target triple #[arg(long, default_value = "x86_64-unknown-linux-musl")] pub target: String, + /// Directory for packaged buildpacks, defaults to 'packaged' in Cargo workspace root + #[arg(long)] + pub package_dir: Option, } #[cfg(test)] diff --git a/libcnb-cargo/src/main.rs b/libcnb-cargo/src/main.rs index ed2b1003..82df916e 100644 --- a/libcnb-cargo/src/main.rs +++ b/libcnb-cargo/src/main.rs @@ -9,8 +9,6 @@ mod package; // Suppress warnings due to the `unused_crate_dependencies` lint not handling integration tests well. #[cfg(test)] -use fs_extra as _; -#[cfg(test)] use tempfile as _; use crate::cli::{Cli, LibcnbSubcommand}; diff --git a/libcnb-cargo/src/package/command.rs b/libcnb-cargo/src/package/command.rs index 57da74a2..ddfb2b0f 100644 --- a/libcnb-cargo/src/package/command.rs +++ b/libcnb-cargo/src/package/command.rs @@ -1,14 +1,14 @@ use crate::cli::PackageArgs; use crate::package::error::Error; use cargo_metadata::MetadataCommand; -use libcnb_data::buildpack::BuildpackDescriptor; +use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId}; use libcnb_package::build::build_buildpack_binaries; use libcnb_package::buildpack_package::{read_buildpack_package, BuildpackPackage}; use libcnb_package::cross_compile::{cross_compile_assistance, CrossCompileAssistance}; use libcnb_package::dependency_graph::{create_dependency_graph, get_dependencies}; use libcnb_package::output::{ assemble_meta_buildpack_directory, assemble_single_buildpack_directory, - BuildpackOutputDirectoryLocator, + create_packaged_buildpack_dir_resolver, }; use libcnb_package::{find_buildpack_dirs, CargoProfile}; use std::ffi::OsString; @@ -30,18 +30,27 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { let current_dir = std::env::current_dir().map_err(Error::GetCurrentDir)?; - let workspace = get_cargo_workspace_root(¤t_dir)?; + let workspace_root_path = get_cargo_workspace_root(¤t_dir)?; - let workspace_target_dir = MetadataCommand::new() - .manifest_path(&workspace.join("Cargo.toml")) + let cargo_metadata = MetadataCommand::new() + .manifest_path(&workspace_root_path.join("Cargo.toml")) .exec() - .map(|metadata| metadata.target_directory.into_std_path_buf()) - .map_err(|e| Error::ReadCargoMetadata(workspace.clone(), e))?; + .map_err(|e| Error::ReadCargoMetadata(workspace_root_path.clone(), e))?; + + let package_dir = args.package_dir.clone().unwrap_or_else(|| { + cargo_metadata + .workspace_root + .into_std_path_buf() + .join("packaged") + }); + + std::fs::create_dir_all(&package_dir) + .map_err(|e| Error::CreatePackageDirectory(package_dir.clone(), e))?; let buildpack_packages = create_dependency_graph( - find_buildpack_dirs(&workspace, &[workspace_target_dir.clone()]) + find_buildpack_dirs(&workspace_root_path, &[package_dir.clone()]) .map_err(|e| Error::FindBuildpackDirs { - path: workspace_target_dir.clone(), + path: workspace_root_path, source: e, })? .into_iter() @@ -68,17 +77,14 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { let build_order = get_dependencies(&buildpack_packages, &buildpack_packages_requested)?; - let buildpack_output_directory_locator = BuildpackOutputDirectoryLocator::new( - workspace_target_dir, - cargo_profile, - target_triple.clone(), - ); + let packaged_buildpack_dir_resolver = + create_packaged_buildpack_dir_resolver(&package_dir, cargo_profile, &target_triple); let lookup_target_dir = |buildpack_package: &BuildpackPackage| { if contains_buildpack_binaries(&buildpack_package.path) { buildpack_package.path.clone() } else { - buildpack_output_directory_locator.get(buildpack_package.buildpack_id()) + packaged_buildpack_dir_resolver(buildpack_package.buildpack_id()) } }; @@ -108,7 +114,7 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { package_meta_buildpack( buildpack_package, &target_dir, - &buildpack_output_directory_locator, + &packaged_buildpack_dir_resolver, )?; } } @@ -156,14 +162,14 @@ fn package_single_buildpack( fn package_meta_buildpack( buildpack_package: &BuildpackPackage, target_dir: &Path, - buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, + packaged_buildpack_dir_resolver: &impl Fn(&BuildpackId) -> PathBuf, ) -> Result<()> { eprintln!("Writing buildpack directory..."); clean_target_directory(target_dir)?; assemble_meta_buildpack_directory( target_dir, buildpack_package, - buildpack_output_directory_locator, + packaged_buildpack_dir_resolver, )?; eprint_compiled_buildpack_success(&buildpack_package.path, target_dir) } diff --git a/libcnb-cargo/src/package/error.rs b/libcnb-cargo/src/package/error.rs index acbee99d..5e32a7ee 100644 --- a/libcnb-cargo/src/package/error.rs +++ b/libcnb-cargo/src/package/error.rs @@ -25,6 +25,9 @@ pub(crate) enum Error { #[error("Could not read Cargo.toml metadata in `{0}`\nError: {1}")] ReadCargoMetadata(PathBuf, cargo_metadata::Error), + #[error("Could not create package directory: {0}\nError: {1}")] + CreatePackageDirectory(PathBuf, std::io::Error), + #[error("{0}")] CrossCompilationHelp(String), diff --git a/libcnb-cargo/tests/integration_test.rs b/libcnb-cargo/tests/integration_test.rs new file mode 100644 index 00000000..67c55e78 --- /dev/null +++ b/libcnb-cargo/tests/integration_test.rs @@ -0,0 +1,318 @@ +// Enable Clippy lints that are disabled by default. +// https://rust-lang.github.io/rust-clippy/stable/index.html +#![warn(clippy::pedantic)] + +use libcnb_data::buildpack::BuildpackId; +use libcnb_data::buildpack_id; +use libcnb_data::buildpackage::BuildpackageDependency; +use libcnb_package::output::create_packaged_buildpack_dir_resolver; +use libcnb_package::{read_buildpack_data, read_buildpackage_data, CargoProfile}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{env, fs}; +use tempfile::{tempdir_in, TempDir}; + +#[test] +#[ignore = "integration test"] +fn package_buildpack_in_single_buildpack_project() { + let fixture_dir = copy_fixture_to_temp_dir("single_buildpack").unwrap(); + let buildpack_id = buildpack_id!("single-buildpack"); + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(&fixture_dir) + .output() + .unwrap(); + + let packaged_buildpack_dir = create_packaged_buildpack_dir_resolver( + &fixture_dir.path().join(DEFAULT_PACKAGE_DIR_NAME), + CargoProfile::Release, + X86_64_UNKNOWN_LINUX_MUSL, + )(&buildpack_id); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("{}\n", packaged_buildpack_dir.to_string_lossy()) + ); + + validate_packaged_buildpack(&packaged_buildpack_dir, &buildpack_id); +} + +#[test] +#[ignore = "integration test"] +fn package_single_meta_buildpack_in_monorepo_buildpack_project() { + let fixture_dir = copy_fixture_to_temp_dir("multiple_buildpacks").unwrap(); + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(fixture_dir.path().join("meta-buildpacks").join("meta-one")) + .output() + .unwrap(); + + let packaged_buildpack_dir_resolver = create_packaged_buildpack_dir_resolver( + &fixture_dir.path().join(DEFAULT_PACKAGE_DIR_NAME), + CargoProfile::Release, + X86_64_UNKNOWN_LINUX_MUSL, + ); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!( + "{}\n", + packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/meta-one")) + .to_string_lossy() + ) + ); + + validate_packaged_meta_buildpack( + &packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/meta-one")), + &buildpack_id!("multiple-buildpacks/meta-one"), + &[ + BuildpackageDependency::try_from(packaged_buildpack_dir_resolver(&buildpack_id!( + "multiple-buildpacks/one" + ))), + BuildpackageDependency::try_from(packaged_buildpack_dir_resolver(&buildpack_id!( + "multiple-buildpacks/two" + ))), + BuildpackageDependency::try_from( + fixture_dir + .path() + .join("meta-buildpacks/meta-one/../../buildpacks/not_libcnb"), + ), + BuildpackageDependency::try_from("docker://docker.io/heroku/procfile-cnb:2.0.0"), + ] + .into_iter() + .collect::, _>>() + .unwrap(), + ); + + for buildpack_id in [ + buildpack_id!("multiple-buildpacks/one"), + buildpack_id!("multiple-buildpacks/two"), + ] { + validate_packaged_buildpack( + &packaged_buildpack_dir_resolver(&buildpack_id), + &buildpack_id, + ); + } +} + +#[test] +#[ignore = "integration test"] +fn package_single_buildpack_in_monorepo_buildpack_project() { + let fixture_dir = copy_fixture_to_temp_dir("multiple_buildpacks").unwrap(); + let buildpack_id = buildpack_id!("multiple-buildpacks/one"); + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(fixture_dir.path().join("buildpacks/one")) + .output() + .unwrap(); + + let packaged_buildpack_dir = create_packaged_buildpack_dir_resolver( + &fixture_dir.path().join(DEFAULT_PACKAGE_DIR_NAME), + CargoProfile::Release, + X86_64_UNKNOWN_LINUX_MUSL, + )(&buildpack_id); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!("{}\n", packaged_buildpack_dir.to_string_lossy()) + ); + + validate_packaged_buildpack(&packaged_buildpack_dir, &buildpack_id); +} + +#[test] +#[ignore = "integration test"] +fn package_all_buildpacks_in_monorepo_buildpack_project() { + let fixture_dir = copy_fixture_to_temp_dir("multiple_buildpacks").unwrap(); + + let dependent_buildpack_ids = [ + buildpack_id!("multiple-buildpacks/one"), + buildpack_id!("multiple-buildpacks/two"), + ]; + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(&fixture_dir) + .output() + .unwrap(); + + let packaged_buildpack_dir_resolver = create_packaged_buildpack_dir_resolver( + &fixture_dir.path().join(DEFAULT_PACKAGE_DIR_NAME), + CargoProfile::Release, + X86_64_UNKNOWN_LINUX_MUSL, + ); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!( + "{}\n", + [ + fixture_dir.path().join("buildpacks/not_libcnb"), + packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/meta-one")), + packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/one")), + packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/two")) + ] + .map(|path| path.to_string_lossy().into_owned()) + .join("\n") + ) + ); + + validate_packaged_meta_buildpack( + &packaged_buildpack_dir_resolver(&buildpack_id!("multiple-buildpacks/meta-one")), + &buildpack_id!("multiple-buildpacks/meta-one"), + &[ + BuildpackageDependency::try_from(packaged_buildpack_dir_resolver(&buildpack_id!( + "multiple-buildpacks/one" + ))), + BuildpackageDependency::try_from(packaged_buildpack_dir_resolver(&buildpack_id!( + "multiple-buildpacks/two" + ))), + BuildpackageDependency::try_from( + fixture_dir + .path() + .join("meta-buildpacks/meta-one/../../buildpacks/not_libcnb"), + ), + BuildpackageDependency::try_from("docker://docker.io/heroku/procfile-cnb:2.0.0"), + ] + .into_iter() + .collect::, _>>() + .unwrap(), + ); + + for buildpack_id in dependent_buildpack_ids { + validate_packaged_buildpack( + &packaged_buildpack_dir_resolver(&buildpack_id), + &buildpack_id, + ); + } +} + +#[test] +#[ignore = "integration test"] +fn package_non_libcnb_buildpack_in_meta_buildpack_project() { + let fixture_dir = copy_fixture_to_temp_dir("multiple_buildpacks").unwrap(); + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(fixture_dir.path().join("buildpacks/not_libcnb")) + .output() + .unwrap(); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!( + "{}\n", + fixture_dir + .path() + .join("buildpacks/not_libcnb") + .to_string_lossy() + ) + ); +} + +#[test] +#[ignore = "integration test"] +fn package_command_error_when_run_in_project_with_no_buildpacks() { + let fixture_dir = copy_fixture_to_temp_dir("no_buildpacks").unwrap(); + + let output = Command::new(CARGO_LIBCNB_BINARY_UNDER_TEST) + .args(["libcnb", "package", "--release"]) + .current_dir(&fixture_dir) + .output() + .unwrap(); + + assert_ne!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8_lossy(&output.stderr), + "šŸ” Locating buildpacks...\nāŒ No buildpacks found!\n" + ); +} + +fn validate_packaged_buildpack(packaged_buildpack_dir: &Path, buildpack_id: &BuildpackId) { + assert!(packaged_buildpack_dir.join("buildpack.toml").exists()); + assert!(packaged_buildpack_dir.join("package.toml").exists()); + assert!(packaged_buildpack_dir.join("bin").join("build").exists()); + assert!(packaged_buildpack_dir.join("bin").join("detect").exists()); + + assert_eq!( + &read_buildpack_data(packaged_buildpack_dir) + .unwrap() + .buildpack_descriptor + .buildpack() + .id, + buildpack_id + ); +} + +fn validate_packaged_meta_buildpack( + packaged_buildpack_dir: &Path, + buildpack_id: &BuildpackId, + expected_buildpackage_dependencies: &[BuildpackageDependency], +) { + assert!(packaged_buildpack_dir.join("buildpack.toml").exists()); + assert!(packaged_buildpack_dir.join("package.toml").exists()); + + assert_eq!( + &read_buildpack_data(packaged_buildpack_dir) + .unwrap() + .buildpack_descriptor + .buildpack() + .id, + buildpack_id + ); + + assert_eq!( + read_buildpackage_data(packaged_buildpack_dir) + .unwrap() + .unwrap() + .buildpackage_descriptor + .dependencies, + expected_buildpackage_dependencies + ); +} + +fn copy_fixture_to_temp_dir(name: &str) -> Result { + let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("fixtures") + .join(name); + + // Instead of using `tempfile::tempdir` directly, we get the temporary directory ourselves and + // canonicalize it before creating a temporary directory inside. We do this since on some + // operating systems (macOS specifically, see: https://github.com/rust-lang/rust/issues/99608), + // `env::temp_dir` will return a path with symlinks in it and `TempDir` doesn't allow + // canonicalization after the fact. + // + // Since libcnb-cargo itself also canonicalizes, we need to do the same so we can compare + // paths when they're output as strings. + env::temp_dir() + .canonicalize() + .and_then(tempdir_in) + .and_then(|temp_dir| copy_dir_recursively(&fixture_dir, temp_dir.path()).map(|_| temp_dir)) +} + +fn copy_dir_recursively(source: &Path, destination: &Path) -> std::io::Result<()> { + match fs::create_dir(destination) { + Err(io_error) if io_error.kind() == ErrorKind::AlreadyExists => Ok(()), + other => other, + }?; + + for entry in fs::read_dir(source)? { + let dir_entry = entry?; + + if dir_entry.file_type()?.is_dir() { + copy_dir_recursively(&dir_entry.path(), &destination.join(dir_entry.file_name()))?; + } else { + fs::copy(dir_entry.path(), destination.join(dir_entry.file_name()))?; + } + } + + Ok(()) +} + +const X86_64_UNKNOWN_LINUX_MUSL: &str = "x86_64-unknown-linux-musl"; +const CARGO_LIBCNB_BINARY_UNDER_TEST: &str = env!("CARGO_BIN_EXE_cargo-libcnb"); +const DEFAULT_PACKAGE_DIR_NAME: &str = "packaged"; diff --git a/libcnb-cargo/tests/test.rs b/libcnb-cargo/tests/test.rs deleted file mode 100644 index 19f43938..00000000 --- a/libcnb-cargo/tests/test.rs +++ /dev/null @@ -1,314 +0,0 @@ -use fs_extra::dir::{copy, CopyOptions}; -use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId}; -use libcnb_data::buildpack_id; -use libcnb_package::output::BuildpackOutputDirectoryLocator; -use libcnb_package::{read_buildpack_data, read_buildpackage_data, CargoProfile}; -use std::env; -use std::io::Read; -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use tempfile::{tempdir, TempDir}; - -#[test] -#[ignore = "integration test"] -fn package_buildpack_in_single_buildpack_project() { - let packaging_test = BuildpackPackagingTest::new("single_buildpack", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package(); - assert_eq!( - output.stdout.trim(), - [packaging_test.target_dir_name(buildpack_id!("single-buildpack"))].join("\n") - ); - assert_compiled_buildpack(&packaging_test, buildpack_id!("single-buildpack")); -} - -#[test] -#[ignore = "integration test"] -fn package_single_meta_buildpack_in_monorepo_buildpack_project() { - let packaging_test = - BuildpackPackagingTest::new("multiple_buildpacks", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package_from("meta-buildpacks/meta-one"); - assert_eq!( - output.stdout.trim(), - [packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/meta-one"))].join("\n") - ); - assert_compiled_buildpack(&packaging_test, buildpack_id!("multiple-buildpacks/one")); - assert_compiled_buildpack(&packaging_test, buildpack_id!("multiple-buildpacks/two")); - assert_compiled_meta_buildpack( - &packaging_test, - buildpack_id!("multiple-buildpacks/meta-one"), - vec![ - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one")), - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/two")), - packaging_test - .dir() - .join("meta-buildpacks/meta-one/../../buildpacks/not_libcnb") - .to_string_lossy() - .to_string(), - String::from("docker://docker.io/heroku/procfile-cnb:2.0.0"), - ], - ); -} - -#[test] -#[ignore = "integration test"] -fn package_single_buildpack_in_monorepo_buildpack_project() { - let packaging_test = - BuildpackPackagingTest::new("multiple_buildpacks", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package_from("buildpacks/one"); - assert_eq!( - output.stdout.trim(), - [packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one"))].join("\n") - ); - assert_compiled_buildpack(&packaging_test, buildpack_id!("multiple-buildpacks/one")); - assert!(!packaging_test - .target_dir(buildpack_id!("multiple-buildpacks/two")) - .exists()); - assert!(!packaging_test - .target_dir(buildpack_id!("multiple-buildpacks/meta-one")) - .exists()); -} - -#[test] -#[ignore = "integration test"] -fn package_all_buildpacks_in_monorepo_buildpack_project() { - let packaging_test = - BuildpackPackagingTest::new("multiple_buildpacks", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package(); - assert_eq!( - output.stdout.trim(), - [ - packaging_test - .dir() - .join("buildpacks/not_libcnb") - .to_string_lossy() - .to_string(), - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/meta-one")), - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one")), - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/two")), - ] - .join("\n") - ); - assert_compiled_buildpack(&packaging_test, buildpack_id!("multiple-buildpacks/one")); - assert_compiled_buildpack(&packaging_test, buildpack_id!("multiple-buildpacks/two")); - assert_compiled_meta_buildpack( - &packaging_test, - buildpack_id!("multiple-buildpacks/meta-one"), - vec![ - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one")), - packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/two")), - packaging_test - .dir() - .join("meta-buildpacks/meta-one/../../buildpacks/not_libcnb") - .to_string_lossy() - .to_string(), - String::from("docker://docker.io/heroku/procfile-cnb:2.0.0"), - ], - ); -} - -#[test] -#[ignore = "integration test"] -fn package_non_libcnb_buildpack_in_meta_buildpack_project() { - let packaging_test = - BuildpackPackagingTest::new("multiple_buildpacks", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package_from("buildpacks/not_libcnb"); - assert_eq!( - output.stdout.trim(), - [packaging_test - .dir() - .join("buildpacks/not_libcnb") - .to_string_lossy() - .to_string()] - .join("\n") - ); - assert!(!packaging_test - .target_dir(buildpack_id!("multiple-buildpacks/one")) - .exists()); - assert!(!packaging_test - .target_dir(buildpack_id!("multiple-buildpacks/two")) - .exists()); - assert!(!packaging_test - .target_dir(buildpack_id!("multiple-buildpacks/meta-one")) - .exists()); -} - -#[test] -#[ignore = "integration test"] -fn package_command_error_when_run_in_project_with_no_buildpacks() { - let packaging_test = BuildpackPackagingTest::new("no_buildpacks", X86_64_UNKNOWN_LINUX_MUSL); - let output = packaging_test.run_libcnb_package(); - assert_ne!(output.code, Some(0)); - assert_eq!( - output.stderr, - "šŸ” Locating buildpacks...\nāŒ No buildpacks found!\n" - ); -} - -fn assert_compiled_buildpack(packaging_test: &BuildpackPackagingTest, buildpack_id: BuildpackId) { - let buildpack_target_dir = PathBuf::from(packaging_test.target_dir_name(buildpack_id.clone())); - - assert!(buildpack_target_dir.exists()); - assert!(buildpack_target_dir.join("buildpack.toml").exists()); - assert!(buildpack_target_dir.join("package.toml").exists()); - assert!(buildpack_target_dir.join("bin").join("build").exists()); - assert!(buildpack_target_dir.join("bin").join("detect").exists()); - - let buildpack_data = read_buildpack_data(&buildpack_target_dir).unwrap(); - let id = match buildpack_data.buildpack_descriptor { - BuildpackDescriptor::Single(descriptor) => descriptor.buildpack.id, - BuildpackDescriptor::Meta(descriptor) => descriptor.buildpack.id, - }; - assert_eq!(id, buildpack_id); -} - -fn assert_compiled_meta_buildpack( - packaging_test: &BuildpackPackagingTest, - buildpack_id: BuildpackId, - dependencies: Vec, -) { - let buildpack_target_dir = PathBuf::from(packaging_test.target_dir_name(buildpack_id.clone())); - - assert!(buildpack_target_dir.exists()); - assert!(buildpack_target_dir.join("buildpack.toml").exists()); - assert!(buildpack_target_dir.join("package.toml").exists()); - - let buildpack_data = read_buildpack_data(&buildpack_target_dir).unwrap(); - let id = match buildpack_data.buildpack_descriptor { - BuildpackDescriptor::Single(descriptor) => descriptor.buildpack.id, - BuildpackDescriptor::Meta(descriptor) => descriptor.buildpack.id, - }; - assert_eq!(id, buildpack_id); - - let buildpackage_data = read_buildpackage_data(buildpack_target_dir) - .unwrap() - .unwrap(); - let compiled_uris: Vec<_> = buildpackage_data - .buildpackage_descriptor - .dependencies - .iter() - .map(|buildpackage_uri| buildpackage_uri.uri.to_string()) - .collect(); - assert_eq!(compiled_uris, dependencies); -} - -struct BuildpackPackagingTest { - fixture_name: String, - temp_dir: TempDir, - release_build: bool, - target_triple: String, -} - -struct TestOutput { - stdout: String, - stderr: String, - code: Option, -} - -impl BuildpackPackagingTest { - fn new(fixture_name: &str, target_triple: &str) -> Self { - let source_directory = env::current_dir() - .unwrap() - .join("fixtures") - .join(fixture_name); - let target_directory = tempdir().unwrap(); - let copy_options = CopyOptions::new(); - copy(source_directory, &target_directory, ©_options).unwrap(); - BuildpackPackagingTest { - fixture_name: fixture_name.to_string(), - temp_dir: target_directory, - release_build: true, - target_triple: String::from(target_triple), - } - } - - fn dir(&self) -> PathBuf { - self.temp_dir - .path() - .canonicalize() - .unwrap() - .join(&self.fixture_name) - } - - fn target_dir_name(&self, buildpack_id: BuildpackId) -> String { - self.target_dir(buildpack_id) - .canonicalize() - .unwrap() - .to_string_lossy() - .to_string() - } - - fn target_dir(&self, buildpack_id: BuildpackId) -> PathBuf { - let root_dir = self.dir().join("target"); - let cargo_profile = if self.release_build { - CargoProfile::Release - } else { - CargoProfile::Dev - }; - let locator = BuildpackOutputDirectoryLocator::new( - root_dir, - cargo_profile, - self.target_triple.clone(), - ); - locator.get(&buildpack_id) - } - - fn run_libcnb_package(&self) -> TestOutput { - self.run_libcnb_package_from(".") - } - - fn run_libcnb_package_from(&self, from_dir: &str) -> TestOutput { - // borrowed from assert_cmd - let suffix = env::consts::EXE_SUFFIX; - - let target_dir = env::current_exe() - .ok() - .map(|mut path| { - path.pop(); - if path.ends_with("deps") { - path.pop(); - } - path - }) - .unwrap(); - - let name = "cargo-libcnb"; - - let cargo_libcnb = env::var_os(format!("CARGO_BIN_EXE_{name}")) - .map(|p| p.into()) - .unwrap_or_else(|| target_dir.join(format!("{name}{suffix}"))); - - let mut cmd = Command::new(cargo_libcnb) - .args(["libcnb", "package", "--release"]) - .current_dir(self.dir().join(from_dir)) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); - - let status = cmd.wait().unwrap(); - - let mut stdout = String::new(); - cmd.stdout - .take() - .unwrap() - .read_to_string(&mut stdout) - .unwrap(); - println!("STDOUT:\n{stdout}"); - - let mut stderr = String::new(); - cmd.stderr - .take() - .unwrap() - .read_to_string(&mut stderr) - .unwrap(); - println!("STDERR:\n{stderr}"); - - TestOutput { - stdout: String::from_utf8_lossy(stdout.as_bytes()).to_string(), - stderr: String::from_utf8_lossy(stderr.as_bytes()).to_string(), - code: status.code(), - } - } -} - -const X86_64_UNKNOWN_LINUX_MUSL: &str = "x86_64-unknown-linux-musl"; diff --git a/libcnb-data/Cargo.toml b/libcnb-data/Cargo.toml index f7c9d081..28659dac 100644 --- a/libcnb-data/Cargo.toml +++ b/libcnb-data/Cargo.toml @@ -14,10 +14,10 @@ include = ["src/**/*", "LICENSE", "README.md"] [dependencies] fancy-regex = { version = "0.11.0", default-features = false } libcnb-proc-macros.workspace = true -serde = { version = "1.0.166", features = ["derive"] } -thiserror = "1.0.41" +serde = { version = "1.0.183", features = ["derive"] } +thiserror = "1.0.47" toml.workspace = true uriparse = "0.6.4" [dev-dependencies] -serde_test = "1.0.166" +serde_test = "1.0.176" diff --git a/libcnb-package/src/buildpack_dependency.rs b/libcnb-package/src/buildpack_dependency.rs index 48c9539a..b4900c69 100644 --- a/libcnb-package/src/buildpack_dependency.rs +++ b/libcnb-package/src/buildpack_dependency.rs @@ -1,4 +1,3 @@ -use crate::output::BuildpackOutputDirectoryLocator; use libcnb_data::buildpack::{BuildpackId, BuildpackIdError}; use libcnb_data::buildpackage::{Buildpackage, BuildpackageDependency}; use std::path::{Path, PathBuf}; @@ -81,7 +80,7 @@ pub fn get_local_buildpackage_dependencies( /// * the target path for a local dependency is an invalid URI pub fn rewrite_buildpackage_local_dependencies( buildpackage: &Buildpackage, - buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, + packaged_buildpack_dir_resolver: &impl Fn(&BuildpackId) -> PathBuf, ) -> Result { let local_dependency_to_target_dir = |target_dir: &PathBuf| { BuildpackageDependency::try_from(target_dir.clone()).map_err(|_| { @@ -99,7 +98,7 @@ pub fn rewrite_buildpackage_local_dependencies( Ok(buildpackage_dependency) } BuildpackDependency::Local(buildpack_id, _) => { - let output_dir = buildpack_output_directory_locator.get(&buildpack_id); + let output_dir = packaged_buildpack_dir_resolver(&buildpack_id); local_dependency_to_target_dir(&output_dir) } }) diff --git a/libcnb-package/src/output.rs b/libcnb-package/src/output.rs index 3d1f833f..37373893 100644 --- a/libcnb-package/src/output.rs +++ b/libcnb-package/src/output.rs @@ -12,28 +12,19 @@ use libcnb_data::buildpackage::Buildpackage; use std::fs; use std::path::{Path, PathBuf}; -pub struct BuildpackOutputDirectoryLocator { - root_dir: PathBuf, +/// Create a function that can construct the output location for a buildpack. +pub fn create_packaged_buildpack_dir_resolver( + package_dir: &Path, cargo_profile: CargoProfile, - target_triple: String, -} - -impl BuildpackOutputDirectoryLocator { - #[must_use] - pub fn new(root_dir: PathBuf, cargo_profile: CargoProfile, target_triple: String) -> Self { - Self { - root_dir, - cargo_profile, - target_triple, - } - } - - #[must_use] - pub fn get(&self, buildpack_id: &BuildpackId) -> PathBuf { - self.root_dir - .join("buildpack") - .join(&self.target_triple) - .join(match self.cargo_profile { + target_triple: &str, +) -> impl Fn(&BuildpackId) -> PathBuf { + let package_dir = PathBuf::from(package_dir); + let target_triple = target_triple.to_string(); + + move |buildpack_id| { + package_dir + .join(&target_triple) + .join(match cargo_profile { CargoProfile::Dev => "debug", CargoProfile::Release => "release", }) @@ -166,7 +157,7 @@ pub fn assemble_single_buildpack_directory( pub fn assemble_meta_buildpack_directory( destination_path: impl AsRef, buildpack_package: &BuildpackPackage, - buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, + packaged_buildpack_dir_resolver: &impl Fn(&BuildpackId) -> PathBuf, ) -> Result<(), AssembleBuildpackDirectoryError> { fs::create_dir_all(destination_path.as_ref()).map_err(|e| { AssembleBuildpackDirectoryError::CreateBuildpackDestinationDirectory( @@ -189,7 +180,7 @@ pub fn assemble_meta_buildpack_directory( .buildpackage_data .as_ref() .map_or(&default_buildpackage, |data| &data.buildpackage_descriptor), - buildpack_output_directory_locator, + packaged_buildpack_dir_resolver, ) .map_err(AssembleBuildpackDirectoryError::RewriteLocalDependencies) .and_then(|buildpackage| { @@ -227,36 +218,33 @@ fn create_file_symlink, Q: AsRef>( #[cfg(test)] mod tests { - use crate::output::BuildpackOutputDirectoryLocator; + use crate::output::create_packaged_buildpack_dir_resolver; use crate::CargoProfile; use libcnb_data::buildpack_id; use std::path::PathBuf; #[test] - fn test_get_buildpack_output_directory_locator() { + fn test_get_buildpack_target_dir() { let buildpack_id = buildpack_id!("some-org/with-buildpack"); + let package_dir = PathBuf::from("/package"); + let target_triple = "x86_64-unknown-linux-musl"; + + let dev_packaged_buildpack_dir_resolver = + create_packaged_buildpack_dir_resolver(&package_dir, CargoProfile::Dev, target_triple); + + let release_packaged_buildpack_dir_resolver = create_packaged_buildpack_dir_resolver( + &package_dir, + CargoProfile::Release, + target_triple, + ); assert_eq!( - BuildpackOutputDirectoryLocator { - cargo_profile: CargoProfile::Dev, - target_triple: "x86_64-unknown-linux-musl".to_string(), - root_dir: PathBuf::from("/target") - } - .get(&buildpack_id), - PathBuf::from( - "/target/buildpack/x86_64-unknown-linux-musl/debug/some-org_with-buildpack" - ) + dev_packaged_buildpack_dir_resolver(&buildpack_id), + PathBuf::from("/package/x86_64-unknown-linux-musl/debug/some-org_with-buildpack") ); assert_eq!( - BuildpackOutputDirectoryLocator { - cargo_profile: CargoProfile::Release, - target_triple: "x86_64-unknown-linux-musl".to_string(), - root_dir: PathBuf::from("/target") - } - .get(&buildpack_id), - PathBuf::from( - "/target/buildpack/x86_64-unknown-linux-musl/release/some-org_with-buildpack" - ) + release_packaged_buildpack_dir_resolver(&buildpack_id), + PathBuf::from("/package/x86_64-unknown-linux-musl/release/some-org_with-buildpack") ); } } diff --git a/libcnb-proc-macros/Cargo.toml b/libcnb-proc-macros/Cargo.toml index 9713fcce..a9bdec3f 100644 --- a/libcnb-proc-macros/Cargo.toml +++ b/libcnb-proc-macros/Cargo.toml @@ -16,5 +16,5 @@ proc-macro = true [dependencies] cargo_metadata = "0.17.0" fancy-regex = { version = "0.11.0", default-features = false } -quote = "1.0.29" -syn = { version = "2.0.23", features = ["full"] } +quote = "1.0.33" +syn = { version = "2.0.29", features = ["full"] } diff --git a/libcnb-test/Cargo.toml b/libcnb-test/Cargo.toml index a062501c..442cf89b 100644 --- a/libcnb-test/Cargo.toml +++ b/libcnb-test/Cargo.toml @@ -16,8 +16,8 @@ fastrand = "2.0.0" fs_extra = "1.3.0" libcnb-data.workspace = true libcnb-package.workspace = true -tempfile = "3.6.0" +tempfile = "3.7.1" [dev-dependencies] -indoc = "2.0.2" +indoc = "2.0.3" ureq = { version = "2.7.1", default-features = false } diff --git a/libcnb-test/src/container_config.rs b/libcnb-test/src/container_config.rs index c3828db4..f98f8b48 100644 --- a/libcnb-test/src/container_config.rs +++ b/libcnb-test/src/container_config.rs @@ -25,7 +25,7 @@ use std::collections::{HashMap, HashSet}; /// }, /// ); /// ``` -#[derive(Default)] +#[derive(Clone, Default)] pub struct ContainerConfig { pub(crate) entrypoint: Option, pub(crate) command: Option>, diff --git a/libcnb-test/src/container_context.rs b/libcnb-test/src/container_context.rs index 882968a1..5fa2146f 100644 --- a/libcnb-test/src/container_context.rs +++ b/libcnb-test/src/container_context.rs @@ -2,13 +2,15 @@ use crate::docker::{ DockerExecCommand, DockerLogsCommand, DockerPortCommand, DockerRemoveContainerCommand, }; use crate::log::LogOutput; -use crate::util; +use crate::util::CommandError; +use crate::{util, ContainerConfig}; use std::net::SocketAddr; /// Context of a launched container. pub struct ContainerContext { /// The randomly generated name of this container. pub container_name: String, + pub(crate) config: ContainerConfig, } impl ContainerContext { @@ -111,15 +113,30 @@ impl ContainerContext { /// was not exposed using [`ContainerConfig::expose_port`](crate::ContainerConfig::expose_port). #[must_use] pub fn address_for_port(&self, port: u16) -> SocketAddr { + assert!( + self.config.exposed_ports.contains(&port), + "Unknown port: Port {port} needs to be exposed first using `ContainerConfig::expose_port`" + ); + let docker_port_command = DockerPortCommand::new(&self.container_name, port); - util::run_command(docker_port_command) - .unwrap_or_else(|command_err| { - panic!("Error obtaining container port mapping:\n\n{command_err}") - }) - .stdout - .trim() - .parse() - .expect("Couldn't parse `docker port` output") + + match util::run_command(docker_port_command) { + Ok(output) => output + .stdout + .trim() + .parse() + .expect("Couldn't parse `docker port` output"), + Err(CommandError::NonZeroExitCode { log_output, .. }) => { + panic!( + "Error obtaining container port mapping:\n{}\nThis normally means that the container crashed. Container logs:\n\n{}", + log_output.stderr, + self.logs_now() + ); + } + Err(command_err) => { + panic!("Error obtaining container port mapping:\n\n{command_err}"); + } + } } /// Executes a shell command inside an already running container. diff --git a/libcnb-test/src/log.rs b/libcnb-test/src/log.rs index 519b6717..6bb93726 100644 --- a/libcnb-test/src/log.rs +++ b/libcnb-test/src/log.rs @@ -1,6 +1,15 @@ +use std::fmt::Display; + /// Log output from a command. #[derive(Debug, Default)] pub struct LogOutput { pub stdout: String, pub stderr: String, } + +impl Display for LogOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let LogOutput { stdout, stderr } = self; + write!(f, "## stderr:\n\n{stderr}\n## stdout:\n\n{stdout}\n") + } +} diff --git a/libcnb-test/src/test_context.rs b/libcnb-test/src/test_context.rs index e838516d..bc08deb6 100644 --- a/libcnb-test/src/test_context.rs +++ b/libcnb-test/src/test_context.rs @@ -109,7 +109,10 @@ impl<'a> TestContext<'a> { util::run_command(docker_run_command) .unwrap_or_else(|command_err| panic!("Error starting container:\n\n{command_err}")); - f(ContainerContext { container_name }); + f(ContainerContext { + container_name, + config: config.clone(), + }); } /// Run the provided shell command. diff --git a/libcnb-test/src/test_runner.rs b/libcnb-test/src/test_runner.rs index d88c9c2d..67284e71 100644 --- a/libcnb-test/src/test_runner.rs +++ b/libcnb-test/src/test_runner.rs @@ -1,9 +1,7 @@ use crate::docker::DockerRemoveImageCommand; use crate::pack::PackBuildCommand; use crate::util::CommandError; -use crate::{ - app, build, util, BuildConfig, BuildpackReference, LogOutput, PackResult, TestContext, -}; +use crate::{app, build, util, BuildConfig, BuildpackReference, PackResult, TestContext}; use std::borrow::Borrow; use std::env; use std::path::PathBuf; @@ -119,10 +117,10 @@ impl TestRunner { let output = match (&config.expected_pack_result, pack_result) { (PackResult::Success, Ok(output)) => output, - (PackResult::Failure, Err(CommandError::NonZeroExitCode { stdout, stderr, .. })) => { - LogOutput { stdout, stderr } + (PackResult::Failure, Err(CommandError::NonZeroExitCode { log_output, .. })) => { + log_output } - (PackResult::Failure, Ok(LogOutput { stdout, stderr })) => { + (PackResult::Failure, Ok(log_output)) => { // Ordinarily the Docker image created by `pack build` will either be cleaned up by // `TestContext::Drop` later on, or will not have been created in the first place, // if the `pack build` was not successful. However, in the case of an unexpectedly @@ -130,7 +128,7 @@ impl TestRunner { util::run_command(DockerRemoveImageCommand::new(image_name)).unwrap_or_else( |command_err| panic!("Error removing Docker image:\n\n{command_err}"), ); - panic!("The pack build was expected to fail, but did not:\n\n## stderr:\n\n{stderr}\n## stdout:\n\n{stdout}\n"); + panic!("The pack build was expected to fail, but did not:\n\n{log_output}"); } (_, Err(command_err)) => { panic!("Error performing pack build:\n\n{command_err}"); diff --git a/libcnb-test/src/util.rs b/libcnb-test/src/util.rs index e0dd8eef..5c629269 100644 --- a/libcnb-test/src/util.rs +++ b/libcnb-test/src/util.rs @@ -40,17 +40,18 @@ pub(crate) fn run_command(command: impl Into) -> Result, program: String, - stdout: String, - stderr: String, + log_output: LogOutput, }, } @@ -89,11 +89,10 @@ impl Display for CommandError { CommandError::NonZeroExitCode { program, exit_code, - stdout, - stderr, + log_output, } => write!( f, - "{program} command failed with exit code {}!\n\n## stderr:\n\n{stderr}\n## stdout:\n\n{stdout}\n", + "{program} command failed with exit code {}!\n\n{log_output}", exit_code.map_or(String::from(""), |exit_code| exit_code.to_string()) ), } diff --git a/libcnb-test/tests/integration_test.rs b/libcnb-test/tests/integration_test.rs index 36e8bc85..76bfaedc 100644 --- a/libcnb-test/tests/integration_test.rs +++ b/libcnb-test/tests/integration_test.rs @@ -475,13 +475,9 @@ fn expose_port_invalid_port() { #[test] #[ignore = "integration test"] -#[should_panic(expected = "Error obtaining container port mapping: - -docker command failed with exit code 1! - -## stderr: - -Error: No public port '12345")] +#[should_panic( + expected = "Unknown port: Port 12345 needs to be exposed first using `ContainerConfig::expose_port`" +)] fn address_for_port_when_port_not_exposed() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/procfile") @@ -496,14 +492,17 @@ fn address_for_port_when_port_not_exposed() { #[test] #[ignore = "integration test"] -// TODO: Improve the UX here: https://github.com/heroku/libcnb.rs/issues/482 -#[should_panic(expected = "Error obtaining container port mapping: - -docker command failed with exit code 1! +#[should_panic(expected = " +This normally means that the container crashed. Container logs: ## stderr: -Error: No public port '12345")] +some stderr + +## stdout: + +some stdout +")] fn address_for_port_when_container_crashed() { TestRunner::default().build( BuildConfig::new("heroku/builder:22", "test-fixtures/procfile") @@ -512,11 +511,12 @@ fn address_for_port_when_container_crashed() { context.start_container( ContainerConfig::new() .entrypoint("launcher") - .command(["exit 1"]) + .command(["echo 'some stdout'; echo 'some stderr' >&2; exit 1"]) .expose_port(TEST_PORT), |container| { + // Wait for the container to actually exit, otherwise `address_for_port()` will succeed. thread::sleep(Duration::from_secs(1)); - let _ = container.address_for_port(12345); + let _ = container.address_for_port(TEST_PORT); }, ); }, diff --git a/libcnb/Cargo.toml b/libcnb/Cargo.toml index d4f48fa5..0ffdd3cb 100644 --- a/libcnb/Cargo.toml +++ b/libcnb/Cargo.toml @@ -12,14 +12,14 @@ readme = "README.md" include = ["src/**/*", "LICENSE", "README.md"] [dependencies] -anyhow = { version = "1.0.71", optional = true } +anyhow = { version = "1.0.75", optional = true } cyclonedx-bom = { version = "0.4.0", optional = true } libcnb-data.workspace = true libcnb-proc-macros.workspace = true -serde = { version = "1.0.166", features = ["derive"] } -thiserror = "1.0.41" +serde = { version = "1.0.183", features = ["derive"] } +thiserror = "1.0.47" toml.workspace = true [dev-dependencies] fastrand = "2.0.0" -tempfile = "3.6.0" +tempfile = "3.7.1" diff --git a/libherokubuildpack/Cargo.toml b/libherokubuildpack/Cargo.toml index c4af51b3..fd88c947 100644 --- a/libherokubuildpack/Cargo.toml +++ b/libherokubuildpack/Cargo.toml @@ -33,15 +33,15 @@ crossbeam-utils = { version = "0.8.16", optional = true } # Ideally we'd use the fastest `zlib-ng` backend, however it fails to cross-compile: # https://github.com/rust-lang/libz-sys/issues/93 # As such we have to use the next best alternate backend, which is `zlib`. -flate2 = { version = "1.0.26", default-features = false, features = ["zlib"], optional = true } +flate2 = { version = "1.0.27", default-features = false, features = ["zlib"], optional = true } libcnb = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true } sha2 = { version = "0.10.7", optional = true } -tar = { version = "0.4.38", default-features = false, optional = true } +tar = { version = "0.4.40", default-features = false, optional = true } termcolor = { version = "1.2.0", optional = true } -thiserror = { version = "1.0.41", optional = true } +thiserror = { version = "1.0.47", optional = true } toml = { workspace = true, optional = true } ureq = { version = "2.7.1", default-features = false, features = ["tls"], optional = true } [dev-dependencies] -tempfile = "3.6.0" +tempfile = "3.7.1"