Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

switch to fixed directory for twoliter tools #102

Merged
merged 5 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 0 additions & 28 deletions tools/buildsys/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,6 @@ lazy_static! {

static DOCKER_BUILD_MAX_ATTEMPTS: NonZeroU16 = nonzero!(10u16);

/// The list of tools.
const TOOLS: &[&str] = &[
"docker-go",
"partyplanner",
"rpm2img",
"rpm2kmodkit",
"rpm2migrations",
];

pub(crate) struct PackageBuilder;

impl PackageBuilder {
Expand Down Expand Up @@ -133,11 +124,6 @@ impl PackageBuilder {
args.build_arg(target_env_var, src_env_val);
}

// Add the tools directory as a secret since it is not in the context directory.
args.extend(tools_secret_args(&PathBuf::from(
env::var(TOOLS_DIR).context(error::EnvironmentSnafu { var: TOOLS_DIR })?,
)));

let tag = format!(
"buildsys-pkg-{package}-{arch}",
package = package,
Expand Down Expand Up @@ -236,11 +222,6 @@ impl VariantBuilder {
// Add known secrets to the build argments.
add_secrets(&mut args)?;

// Add the tools directory as a secret since it is not in the context directory.
args.extend(tools_secret_args(&PathBuf::from(
env::var(TOOLS_DIR).context(error::EnvironmentSnafu { var: TOOLS_DIR })?,
)));

// Always rebuild variants since they are located in a different workspace,
// and don't directly track changes in the underlying packages.
getenv("BUILDSYS_TIMESTAMP")?;
Expand Down Expand Up @@ -676,12 +657,3 @@ where
self.as_ref().split(' ').map(String::from).collect()
}
}

fn tools_secret_args(tools_dir: &Path) -> impl Iterator<Item = String> {
let mut secrets = Vec::new();
for &tool in TOOLS {
let id = format!("tools-{}", tool.to_lowercase().replace('.', "-"));
secrets.build_secret("file", &id, &tools_dir.join(tool).display().to_string())
}
secrets.into_iter()
}
1 change: 1 addition & 0 deletions twoliter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ anyhow = "1"
async-recursion = "1"
clap = { version = "4", features = ["derive", "env", "std"] }
env_logger = "0.10"
filetime = "0.2"
flate2 = "1"
hex = "0.4"
log = "0.4"
Expand Down
2 changes: 2 additions & 0 deletions twoliter/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ fn main() {
let paths = Paths::new();
println!("cargo:rerun-if-changed={}", paths.data_input_dir.display());

let _ = fs::remove_dir_all(&paths.prep_dir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can avoid updating build.rs, that would be great. The entire file will be changed in an upcoming pr.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can avoid updating build.rs, that would be great. The entire file will be changed in an upcoming pr.

The addition of a file appears to be necessary for this PR, that is Dockerfile.dockerignore, so at least one change in build.rs is not optional. Anyway, the changes here should be easy to replay. Delete the prep dir and add a file.

fs::create_dir_all(&paths.prep_dir).expect(&format!(
"Unable to create directory '{}'",
paths.prep_dir.display()
));

paths.copy_file("Dockerfile");
paths.copy_file("Dockerfile.dockerignore");
paths.copy_file("Makefile.toml");
paths.copy_file("docker-go");
paths.copy_file("partyplanner");
Expand Down
24 changes: 7 additions & 17 deletions twoliter/embedded/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ ARG ARCH
ARG GOARCH

FROM ${SDK} as sdk
USER root
RUN \
--mount=type=secret,id=tools-docker-go,mode=755,target=/tmp/tools/docker-go \
--mount=type=secret,id=tools-partyplanner,mode=755,target=/tmp/tools/partyplanner \
--mount=type=secret,id=tools-rpm2img,mode=755,target=/tmp/tools/rpm2img \
--mount=type=secret,id=tools-rpm2kmodkit,mode=755,target=/tmp/tools/rpm2kmodkit \
--mount=type=secret,id=tools-rpm2migrations,mode=755,target=/tmp/tools/rpm2migrations \
cp -a /tmp/tools/ /tools/
USER builder

FROM --platform=linux/${GOARCH} ${TOOLCHAIN}-${ARCH} as toolchain

############################################################################################
Expand All @@ -41,7 +31,7 @@ ARG TOKEN
# We can't create directories via RUN in a scratch container, so take an existing one.
COPY --chown=1000:1000 --from=sdk /tmp /cache
# Ensure the ARG variables are used in the layer to prevent reuse by other builds.
COPY --chown=1000:1000 .dockerignore /cache/.${PACKAGE}.${ARCH}.${TOKEN}
COPY --chown=1000:1000 .gitignore /cache/.${PACKAGE}.${ARCH}.${TOKEN}

# =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
# Some builds need to modify files in the source directory, for example Rust software using
Expand All @@ -62,7 +52,7 @@ ARG TOKEN
# We can't create directories via RUN in a scratch container, so take an existing one.
COPY --chown=1000:1000 --from=sdk /tmp /variantcache
# Ensure the ARG variables are used in the layer to prevent reuse by other builds.
COPY --chown=1000:1000 .dockerignore /variantcache/.${PACKAGE}.${ARCH}.${VARIANT}.${TOKEN}
COPY --chown=1000:1000 .gitignore /variantcache/.${PACKAGE}.${ARCH}.${VARIANT}.${TOKEN}

# =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
# Builds an RPM package from a spec file.
Expand Down Expand Up @@ -135,7 +125,7 @@ RUN --mount=target=/host \
--repofrompath repo,./rpmbuild/RPMS \
--enablerepo 'repo' \
--nogpgcheck \
--forcearch "${ARCH}" \
--forcearch "${ARCH}" \
builddep rpmbuild/SPECS/${PACKAGE}.spec

# We use the "nocache" writable space to generate code where necessary, like the variant-
Expand Down Expand Up @@ -186,7 +176,7 @@ RUN --mount=target=/host \
--nogpgcheck \
--downloadonly \
--downloaddir . \
--forcearch "${ARCH}" \
--forcearch "${ARCH}" \
install $(printf "bottlerocket-%s\n" ${PACKAGES}) \
&& mv *.rpm /local/rpms \
&& createrepo_c /local/rpms \
Expand Down Expand Up @@ -232,7 +222,7 @@ RUN --mount=target=/host \
--mount=type=secret,id=aws-access-key-id.env,target=/root/.aws/aws-access-key-id.env \
--mount=type=secret,id=aws-secret-access-key.env,target=/root/.aws/aws-secret-access-key.env \
--mount=type=secret,id=aws-session-token.env,target=/root/.aws/aws-session-token.env \
/tools/rpm2img \
/host/build/tools/rpm2img \
--package-dir=/local/rpms \
--output-dir=/local/output \
--output-fmt="${IMAGE_FORMAT}" \
Expand Down Expand Up @@ -265,7 +255,7 @@ RUN --mount=target=/host \
-name "bottlerocket-migrations-*.rpm" \
-not -iname '*debuginfo*' \
-exec cp '{}' '/local/migrations/' ';' \
&& /tools/rpm2migrations \
&& /host/build/tools/rpm2migrations \
--package-dir=/local/migrations \
--output-dir=/local/output \
&& echo ${NOCACHE}
Expand All @@ -291,7 +281,7 @@ RUN --mount=target=/host \
&& find /host/build/rpms/ -maxdepth 1 -type f \
-name "bottlerocket-${KERNEL}-archive-*.rpm" \
-exec cp '{}' '/local/archives/' ';' \
&& /tools/rpm2kmodkit \
&& /host/build/tools/rpm2kmodkit \
--archive-dir=/local/archives \
--toolchain-dir=/local/toolchain \
--output-dir=/local/output \
Expand Down
12 changes: 12 additions & 0 deletions twoliter/embedded/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/.git
/.gomodcache
/build/*
!/build/rpms/
/build/rpms/*
!/build/rpms/*.rpm
/build/rpms/*-debuginfo-*.rpm
/build/rpms/*-debugsource-*.rpm
!/build/tools/*
**/target/*
/sbkeys
/tests
11 changes: 11 additions & 0 deletions twoliter/embedded/Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,7 @@ dependencies = [
"clean-images",
"clean-repos",
"clean-state",
"clean-tools",
]

[tasks.clean-sources]
Expand Down Expand Up @@ -1545,6 +1546,16 @@ rm -rf ${BUILDSYS_STATE_DIR}
'''
]

[tasks.clean-tools]
script_runner = "bash"
script = [
'''
if [ -n "${TWOLITER_TOOLS_DIR}" ] ; then
rm -rf "${TWOLITER_TOOLS_DIR}"
fi
'''
]

# Deletes cached code used for Bottlerocket builds
[tasks.purge-cache]
dependencies = [
Expand Down
12 changes: 7 additions & 5 deletions twoliter/src/cmd/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::cargo_make::CargoMake;
use crate::docker::DockerContainer;
use crate::project;
use crate::tools::{install_tools, tools_tempdir};
use crate::tools::install_tools;
use anyhow::{Context, Result};
use clap::Parser;
use log::debug;
Expand Down Expand Up @@ -42,9 +42,11 @@ impl BuildVariant {
pub(super) async fn run(&self) -> Result<()> {
let project = project::load_or_find_project(self.project_path.clone()).await?;
let token = project.token();
let tempdir = tools_tempdir()?;
install_tools(&tempdir).await?;
let makefile_path = tempdir.path().join("Makefile.toml");
let toolsdir = project.project_dir().join("build/tools");
tokio::fs::remove_dir_all(&toolsdir).await?;
tokio::fs::create_dir_all(&toolsdir).await?;
install_tools(&toolsdir).await?;
let makefile_path = toolsdir.join("Makefile.toml");
// A temporary directory in the `build` directory
let build_temp_dir = TempDir::new_in(project.project_dir())
.context("Unable to create a tempdir for Twoliter's build")?;
Expand Down Expand Up @@ -103,7 +105,7 @@ impl BuildVariant {

// Hold the result of the cargo make call so we can clean up the project directory first.
let res = CargoMake::new(&project, &self.arch)?
.env("TWOLITER_TOOLS_DIR", tempdir.path().display().to_string())
.env("TWOLITER_TOOLS_DIR", toolsdir.display().to_string())
.env("BUILDSYS_ARCH", &self.arch)
.env("BUILDSYS_VARIANT", &self.variant)
.env("BUILDSYS_SBKEYS_DIR", sbkeys_dir.display().to_string())
Expand Down
14 changes: 8 additions & 6 deletions twoliter/src/cmd/make.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::cargo_make::CargoMake;
use crate::project;
use crate::tools::{install_tools, tools_tempdir};
use crate::project::{self};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use crate::project::{self};
use crate::project;

use crate::tools::install_tools;
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tokio::fs;

/// Run a cargo make command in Twoliter's build environment. Known Makefile.toml environment
/// variables will be passed-through to the cargo make invocation.
Expand Down Expand Up @@ -33,12 +34,13 @@ pub(crate) struct Make {
impl Make {
pub(super) async fn run(&self) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ecpullen these are changes to the code that you are moving.

let project = project::load_or_find_project(self.project_path.clone()).await?;
let tempdir = tools_tempdir()?;
install_tools(&tempdir).await?;
let makefile_path = tempdir.path().join("Makefile.toml");
let toolsdir = project.project_dir().join("build/tools");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the project object should provide such things (build_dir, tools_dir, etc)

Suggested change
let toolsdir = project.project_dir().join("build/tools");
let toolsdir = project.tools_dir();

fs::remove_dir_all(&toolsdir).await?;
install_tools(&toolsdir).await?;
let makefile_path = toolsdir.join("Makefile.toml");
CargoMake::new(&project, &self.arch)?
.env("CARGO_HOME", self.cargo_home.display().to_string())
.env("TWOLITER_TOOLS_DIR", tempdir.path().display().to_string())
.env("TWOLITER_TOOLS_DIR", toolsdir.display().to_string())
.env("BUILDSYS_VERSION_IMAGE", project.release_version())
.makefile(makefile_path)
.project_dir(project.project_dir())
Expand Down
53 changes: 35 additions & 18 deletions twoliter/src/tools.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::{Context, Result};
use filetime::{set_file_handle_times, set_file_mtime, FileTime};
use flate2::read::ZlibDecoder;
use log::debug;
use std::path::Path;
use tar::Archive;
use tempfile::TempDir;
use tokio::fs;
use tokio::io::AsyncWriteExt;

Expand All @@ -16,36 +16,38 @@ const PUBSYS_SETUP: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_PUBSYS_SETUP"));
const TESTSYS: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_TESTSYS"));
const TUFTOOL: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_TUFTOOL"));

/// Create a `TempDir` object and provide a tools-centric error message if it fails. Make sure you
/// hang on to the `TempDir` for as long as you need it. It will be deleted when it goes out of
/// scope.
pub(crate) fn tools_tempdir() -> Result<TempDir> {
TempDir::new().context("Unable to create a tempdir for Twoliter's tools")
}

/// Install tools into the given `tools_dir`. If you use a `TempDir` object, make sure to pass it by
/// reference and hold on to it until you no longer need the tools to still be installed (it will
/// auto delete when it goes out of scope).
pub(crate) async fn install_tools(tools_dir: impl AsRef<Path>) -> Result<()> {
let dir = tools_dir.as_ref();
debug!("Installing tools to '{}'", dir.display());

write_bin("bottlerocket-variant", BOTTLEROCKET_VARIANT, &dir).await?;
write_bin("buildsys", BUILDSYS, &dir).await?;
write_bin("pubsys", PUBSYS, &dir).await?;
write_bin("pubsys-setup", PUBSYS_SETUP, &dir).await?;
write_bin("testsys", TESTSYS, &dir).await?;
write_bin("tuftool", TUFTOOL, &dir).await?;

// Write out the embedded tools and scripts.
unpack_tarball(&dir)
.await
.context("Unable to install tools")?;

// Pick one of the embedded files for use as the canonical mtime.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a canonical mtime? Is this preventing docker rebuilds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a canonical mtime? Is this preventing docker rebuilds?

I'll add a comment to the code. It's a minor optimization to make the build context more efficient by not always copying the tools through.

For example on a local build with a 180 MiB file:

❯ docker build --progress=plain .
...
#10 [internal] load build context
#10 sha256:8fe1ebd2168d7202972f3fed513e650e6bc14c1252ef426d770005b1b769b70b
#10 transferring context: 183.54MB 0.5s done
#10 DONE 0.6s

❯ docker build --progress=plain .
...
#10 [internal] load build context
#10 sha256:942722aa20dadaa1f21a60c65ffa04d801e48ac344603e42b368fda45457366a
#10 transferring context: 291B done
#10 DONE 0.0s

❯ touch -r Dockerfile NVIDIA-Linux-aarch64-470.82.01.run

❯ docker build --progress=plain .
...
#10 [internal] load build context
#10 sha256:8fe1ebd2168d7202972f3fed513e650e6bc14c1252ef426d770005b1b769b70b
#10 transferring context: 183.54MB 0.5s done
#10 DONE 0.6s

❯ docker build --progress=plain .
...
#10 [internal] load build context
#10 sha256:ec46d1475e765e302cc924f8682c05c897c1a56df7a3b356bae7b1b866eb4c74
#10 transferring context: 291B done
#10 DONE 0.0s

The build context is more intelligent than a Docker layer in the sense that modifying a single file results in only that file being transferred, not the entire build context. But it still seems worth avoiding unnecessary copies.

The direction I'm exploring with passing file descriptors could let us get rid of all the changes to the build context. /packages/*/*.tar.*, /build/rpms, /.gomodcache, and /.cargo are the current offenders. I'd like to avoid others.

let metadata = fs::metadata(dir.join("Dockerfile"))
.await
.context("Unable to get Dockerfile metadata")?;
let mtime = FileTime::from_last_modification_time(&metadata);

write_bin("bottlerocket-variant", BOTTLEROCKET_VARIANT, &dir, mtime).await?;
write_bin("buildsys", BUILDSYS, &dir, mtime).await?;
write_bin("pubsys", PUBSYS, &dir, mtime).await?;
write_bin("pubsys-setup", PUBSYS_SETUP, &dir, mtime).await?;
write_bin("testsys", TESTSYS, &dir, mtime).await?;
write_bin("tuftool", TUFTOOL, &dir, mtime).await?;

// Apply the mtime to the directory now that the writes are done.
set_file_mtime(dir, mtime).context(format!("Unable to set mtime for '{}'", dir.display()))?;

Ok(())
}

async fn write_bin(name: &str, data: &[u8], dir: impl AsRef<Path>) -> Result<()> {
async fn write_bin(name: &str, data: &[u8], dir: impl AsRef<Path>, mtime: FileTime) -> Result<()> {
let path = dir.as_ref().join(name);
let mut f = fs::OpenOptions::new()
.create(true)
Expand All @@ -60,7 +62,12 @@ async fn write_bin(name: &str, data: &[u8], dir: impl AsRef<Path>) -> Result<()>
.context(format!("Unable to write to '{}'", path.display()))?;
f.flush()
.await
.context(format!("Unable to finalize '{}'", path.display()))
.context(format!("Unable to finalize '{}'", path.display()))?;

let f = f.into_std().await;
set_file_handle_times(&f, None, Some(mtime))
.context(format!("Unable to set mtime for '{}'", path.display()))?;
Ok(())
}

async fn unpack_tarball(tools_dir: impl AsRef<Path>) -> Result<()> {
Expand All @@ -77,7 +84,7 @@ async fn unpack_tarball(tools_dir: impl AsRef<Path>) -> Result<()> {

#[tokio::test]
async fn test_install_tools() {
let tempdir = tools_tempdir().unwrap();
let tempdir = tempfile::TempDir::new().unwrap();
install_tools(&tempdir).await.unwrap();

// Assert that the expected files exist in the tools directory.
Expand All @@ -98,4 +105,14 @@ async fn test_install_tools() {
assert!(tempdir.path().join("pubsys-setup").is_file());
assert!(tempdir.path().join("testsys").is_file());
assert!(tempdir.path().join("tuftool").is_file());

// Check that the mtimes match.
let dockerfile_metadata = fs::metadata(tempdir.path().join("Dockerfile"))
.await
.unwrap();
let buildsys_metadata = fs::metadata(tempdir.path().join("buildsys")).await.unwrap();
let dockerfile_mtime = FileTime::from_last_modification_time(&dockerfile_metadata);
let buildsys_mtime = FileTime::from_last_modification_time(&buildsys_metadata);

assert_eq!(dockerfile_mtime, buildsys_mtime);
}