Skip to content

allow specifying a dockerfile to use for a target and implement "pre-build"ing #678

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

Merged
merged 1 commit into from
Jun 15, 2022
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #795 - added images for additional toolchains maintained by cross-rs.
- #792 - added `CROSS_CONTAINER_IN_CONTAINER` environment variable to replace `CROSS_DOCKER_IN_DOCKER`.
- #782 - added `build-std` config option, which builds the rust standard library from source if enabled.
- #775 - forward Cargo exit code to host
- #678 - Add optional `target.{target}.dockerfile[.file]`, `target.{target}.dockerfile.context` and `target.{target}.dockerfile.build-args` to invoke docker/podman build before using an image.
- #678 - Add `target.{target}.pre-build` config for running commands before building the image.
- #772 - added `CROSS_CONTAINER_OPTS` environment variable to replace `DOCKER_OPTS`.
- #767, #788 - added the `cross-util` and `xtask` commands.
- #745 - added `thumbv7neon-*` targets.
Expand All @@ -27,6 +28,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Changed

- #775 - forward Cargo exit code to host
- #762 - re-enabled `x86_64-unknown-dragonfly` target.
- #747 - reduced android image sizes.
- #746 - limit image permissions for android images.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_ignored = "0.1.2"
shell-words = "1.1.0"
sha1_smol = "1.0.0"

[target.'cfg(not(windows))'.dependencies]
nix = { version = "0.24", default-features = false, features = ["user"] }
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,22 @@ the default one. Normal Docker behavior applies, so:

- If only `tag` is omitted, then Docker will use the `latest` tag.

#### Dockerfiles

If you're using a custom Dockerfile, you can use `target.{{TARGET}}.dockerfile` to automatically build it

``` toml
[target.aarch64-unknown-linux-gnu.dockerfile]
dockerfile = "./path/to/where/the/Dockerfile/resides"
```

`cross` will build and use the image that was built instead of the default image.

It's recommended to base your custom image on the default Docker image that
cross uses: `ghcr.io/cross-rs/{{TARGET}}:{{VERSION}}` (where `{{VERSION}}` is cross's version).
This way you won't have to figure out how to install a cross C toolchain in your
custom image. Example below:
custom image.


``` Dockerfile
FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:latest
Expand All @@ -125,8 +137,23 @@ RUN dpkg --add-architecture arm64 && \
apt-get install --assume-yes libfoo:arm64
```

If you want cross to provide the `FROM` instruction, you can do the following

``` Dockerfile
ARG CROSS_BASE_IMAGE
FROM $CROSS_BASE_IMAGE

RUN ...
```
$ docker build -t my/image:tag path/to/where/the/Dockerfile/resides

#### Pre-build hook

`cross` enables you to add dependencies and run other necessary commands in the image before using it.
This action will be added to the used image, so it won't be ran/built every time you use `cross`.

``` toml
[target.x86_64-unknown-linux-gnu]
pre-build = ["dpkg --add-architecture arm64 && apt-get update && apt-get install --assume-yes libfoo:arm64"]
```

### Docker in Docker
Expand Down
21 changes: 21 additions & 0 deletions docs/cross_toml.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
The `cross` configuration in the `Cross.toml` file, can contain the following elements:

# `build`

The `build` key allows you to set global variables, e.g.:

```toml
Expand All @@ -11,6 +12,7 @@ default-target = "x86_64-unknown-linux-gnu"
```

# `build.env`

With the `build.env` key you can globally set volumes that should be mounted
in the Docker container or environment variables that should be passed through.
For example:
Expand All @@ -22,17 +24,20 @@ passthrough = ["IMPORTANT_ENV_VARIABLES"]
```

# `target.TARGET`

The `target` key allows you to specify parameters for specific compilation targets.

```toml
[target.aarch64-unknown-linux-gnu]
xargo = false
build-std = false
image = "test-image"
pre-build = ["apt-get update"]
runner = "custom-runner"
```

# `target.TARGET.env`

The `target` key allows you to specify environment variables that should be used for a specific compilation target.
This is similar to `build.env`, but allows you to be more specific per target.

Expand All @@ -41,3 +46,19 @@ This is similar to `build.env`, but allows you to be more specific per target.
volumes = ["VOL1_ARG", "VOL2_ARG"]
passthrough = ["IMPORTANT_ENV_VARIABLES"]
```

# `target.TARGET.dockerfile`

```toml
[target.x86_64-unknown-linux-gnu.dockerfile]
file = "./Dockerfile" # The dockerfile to use relative to the `Cargo.toml`
context = "." # What folder to run the build script in
build-args = { ARG1 = "foo" } # https://docs.docker.com/engine/reference/builder/#arg
```

also supports

```toml
[target.x86_64-unknown-linux-gnu]
dockerfile = "./Dockerfile"
```
2 changes: 1 addition & 1 deletion src/bin/commands/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ fn remove_images(
}
command.args(images);
if execute {
command.run(verbose).map_err(Into::into)
command.run(verbose, false).map_err(Into::into)
} else {
println!("{:?}", command);
Ok(())
Expand Down
4 changes: 3 additions & 1 deletion src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ pub fn cargo_metadata_with_args(

/// Pass-through mode
pub fn run(args: &[String], verbose: bool) -> Result<ExitStatus, CommandError> {
Command::new("cargo").args(args).run_and_get_status(verbose)
Command::new("cargo")
.args(args)
.run_and_get_status(verbose, false)
}

/// run cargo and get the output, does not check the exit status
Expand Down
145 changes: 118 additions & 27 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ impl Environment {
self.get_target_var(target, "IMAGE")
}

fn dockerfile(&self, target: &Target) -> Option<String> {
let res = self.get_values_for("DOCKERFILE", target, |s| s.to_string());
res.0.or(res.1)
}

fn dockerfile_context(&self, target: &Target) -> Option<String> {
let res = self.get_values_for("DOCKERFILE_CONTEXT", target, |s| s.to_string());
res.0.or(res.1)
}

fn pre_build(&self, target: &Target) -> Option<Vec<String>> {
let res = self.get_values_for("PRE_BUILD", target, |v| {
v.split('\n').map(String::from).collect()
});
res.0.or(res.1)
}

fn runner(&self, target: &Target) -> Option<String> {
self.get_target_var(target, "RUNNER")
}
Expand Down Expand Up @@ -178,16 +195,34 @@ impl Config {
fn vec_from_config(
&self,
target: &Target,
env: impl Fn(&Environment, &Target) -> (Option<Vec<String>>, Option<Vec<String>>),
config_build: impl for<'a> Fn(&'a CrossToml) -> Option<&'a [String]>,
config_target: impl for<'a> Fn(&'a CrossToml, &Target) -> Option<&'a [String]>,
) -> Result<Vec<String>> {
env: impl for<'a> Fn(&'a Environment, &Target) -> (Option<Vec<String>>, Option<Vec<String>>),
config: impl for<'a> Fn(&'a CrossToml, &Target) -> (Option<&'a [String]>, Option<&'a [String]>),
sum: bool,
) -> Result<Option<Vec<String>>> {
let (env_build, env_target) = env(&self.env, target);

let mut collected = self.sum_of_env_toml_values(env_build, config_build)?;
collected.extend(self.sum_of_env_toml_values(env_target, |t| config_target(t, target))?);
if sum {
return self.sum_of_env_toml_values(env_target, |t| config(t, target));
} else if let Some(env_target) = env_target {
return Ok(Some(env_target));
}

let (build, target) = self
.toml
.as_ref()
.map(|t| config(t, target))
.unwrap_or_default();

// FIXME: let expression
if target.is_none() && env_build.is_some() {
return Ok(env_build);
}

Ok(collected)
if target.is_none() {
Ok(build.map(ToOwned::to_owned))
} else {
Ok(target.map(ToOwned::to_owned))
}
}

#[cfg(test)]
Expand All @@ -211,22 +246,17 @@ impl Config {
self.string_from_config(target, Environment::runner, CrossToml::runner)
}

pub fn env_passthrough(&self, target: &Target) -> Result<Vec<String>> {
pub fn env_passthrough(&self, target: &Target) -> Result<Option<Vec<String>>> {
self.vec_from_config(
target,
Environment::passthrough,
CrossToml::env_passthrough_build,
CrossToml::env_passthrough_target,
CrossToml::env_passthrough,
true,
)
}

pub fn env_volumes(&self, target: &Target) -> Result<Vec<String>> {
self.vec_from_config(
target,
Environment::volumes,
CrossToml::env_volumes_build,
CrossToml::env_volumes_target,
)
pub fn env_volumes(&self, target: &Target) -> Result<Option<Vec<String>>> {
self.vec_from_config(target, Environment::volumes, CrossToml::env_volumes, false)
}

pub fn target(&self, target_list: &TargetList) -> Option<Target> {
Expand All @@ -238,19 +268,78 @@ impl Config {
.and_then(|t| t.default_target(target_list))
}

pub fn dockerfile(&self, target: &Target) -> Result<Option<String>> {
self.string_from_config(target, Environment::dockerfile, CrossToml::dockerfile)
}

pub fn dockerfile_context(&self, target: &Target) -> Result<Option<String>> {
self.string_from_config(
target,
Environment::dockerfile_context,
CrossToml::dockerfile_context,
)
}

pub fn dockerfile_build_args(
&self,
target: &Target,
) -> Result<Option<HashMap<String, String>>> {
// This value does not support env variables
self.toml
.as_ref()
.map_or(Ok(None), |t| Ok(t.dockerfile_build_args(target)))
}

pub fn pre_build(&self, target: &Target) -> Result<Option<Vec<String>>> {
self.vec_from_config(
target,
|e, t| (None, e.pre_build(t)),
CrossToml::pre_build,
false,
)
}

fn sum_of_env_toml_values<'a>(
&'a self,
env_values: Option<Vec<String>>,
toml_getter: impl FnOnce(&'a CrossToml) -> Option<&'a [String]>,
) -> Result<Vec<String>> {
env_values: Option<impl AsRef<[String]>>,
toml_getter: impl FnOnce(&'a CrossToml) -> (Option<&'a [String]>, Option<&'a [String]>),
) -> Result<Option<Vec<String>>> {
let mut defined = false;
let mut collect = vec![];
if let Some(mut vars) = env_values {
collect.append(&mut vars);
} else if let Some(toml_values) = self.toml.as_ref().and_then(toml_getter) {
collect.extend(toml_values.iter().cloned());
if let Some(vars) = env_values {
collect.extend(vars.as_ref().iter().cloned());
defined = true;
} else if let Some((build, target)) = self.toml.as_ref().map(toml_getter) {
if let Some(build) = build {
collect.extend(build.iter().cloned());
defined = true;
}
if let Some(target) = target {
collect.extend(target.iter().cloned());
defined = true;
}
}
if !defined {
Ok(None)
} else {
Ok(Some(collect))
}
}
}

Ok(collect)
pub fn opt_merge<I, T: Extend<I> + IntoIterator<Item = I>>(
opt1: Option<T>,
opt2: Option<T>,
) -> Option<T> {
match (opt1, opt2) {
(None, None) => None,
(None, Some(opt2)) => Some(opt2),
(Some(opt1), None) => Some(opt1),
(Some(opt1), Some(opt2)) => {
let mut res = opt2;
res.extend(opt1);
Some(res)
}
}
}

Expand Down Expand Up @@ -389,7 +478,8 @@ mod tests {
let config = Config::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env);
let expected = vec!["VOLUME1".to_string(), "VOLUME2".into()];

let result = config.env_volumes(&target()).unwrap();
let result = config.env_volumes(&target()).unwrap().unwrap_or_default();
dbg!(&result);
assert!(result.len() == 2);
assert!(result.contains(&expected[0]));
assert!(result.contains(&expected[1]));
Expand All @@ -404,7 +494,8 @@ mod tests {
let config = Config::new_with(Some(toml(TOML_BUILD_VOLUMES)?), env);
let expected = vec!["VOLUME3".to_string(), "VOLUME4".into()];

let result = config.env_volumes(&target()).unwrap();
let result = config.env_volumes(&target()).unwrap().unwrap_or_default();
dbg!(&result);
assert!(result.len() == 2);
assert!(result.contains(&expected[0]));
assert!(result.contains(&expected[1]));
Expand Down
Loading