build: add cargo xtask pack for distributable archives
`cargo xtask pack [--target T] [--format zip|tar.gz] [--out-dir DIR] [--debug]` builds the client and dedicated server and packs them with assets, README and changelog into dist/station-<version>-<target>.(zip|tar.gz). Windows targets default to zip, others to tar.gz. Packaging uses a new `dist` profile (release + strip), which shrinks the binaries considerably without changing regular release builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
a3c55bc8b0
commit
0b258a9361
|
|
@ -0,0 +1,2 @@
|
||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
/target
|
/target
|
||||||
/saves
|
/saves
|
||||||
__pycache__/
|
__pycache__/
|
||||||
/.NOCOMMIT
|
/.NOCOMMIT
|
||||||
|
dist
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ Station 13, with singleplayer and multiplayer. It is built in Rust (edition
|
||||||
- `assets/data/`: item, recipe, building and skill definitions (`.rsn`).
|
- `assets/data/`: item, recipe, building and skill definitions (`.rsn`).
|
||||||
- `assets/scripts/`: Rune scripts (`.rn`), referenced as `"module::function"`.
|
- `assets/scripts/`: Rune scripts (`.rn`), referenced as `"module::function"`.
|
||||||
- `assets/maps/`, `assets/tilesets/`, `assets/sprites/`: Tiled maps and art.
|
- `assets/maps/`, `assets/tilesets/`, `assets/sprites/`: Tiled maps and art.
|
||||||
|
- `xtask/`: project automation (`cargo xtask pack` builds and archives the
|
||||||
|
client and server into `dist/`).
|
||||||
- `tools/gen_placeholder_assets.py`: regenerates placeholder art and the
|
- `tools/gen_placeholder_assets.py`: regenerates placeholder art and the
|
||||||
example map (generated output is committed).
|
example map (generated output is committed).
|
||||||
- `.github/workflows/`, `.gitea/workflows/`: CI for GitHub and Gitea.
|
- `.github/workflows/`, `.gitea/workflows/`: CI for GitHub and Gitea.
|
||||||
|
|
@ -89,9 +91,10 @@ cargo nextest run --workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
Run binaries with `cargo run` (client; `--host`, `--connect`) and
|
Run binaries with `cargo run` (client; `--host`, `--connect`) and
|
||||||
`cargo run -p game_server --bin game-server`. The Windows client can be
|
`cargo run -p game_server --bin game-server`. Build a distributable archive
|
||||||
cross-built with `cargo build --release -p game_client --target
|
with `cargo xtask pack` (add `--target x86_64-pc-windows-gnu` to cross-build
|
||||||
x86_64-pc-windows-gnu` (needs MinGW). Running the client requires a Vulkan or
|
for Windows; needs MinGW). Files shipped next to the binaries are listed in
|
||||||
|
`EXTRA_FILES` in `xtask/src/main.rs`. Running the client requires a Vulkan or
|
||||||
GL capable GPU or a software driver such as lavapipe.
|
GL capable GPU or a software driver such as lavapipe.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,5 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
- Crafting, construction/deconstruction, skills, hunger, health and respawning.
|
- Crafting, construction/deconstruction, skills, hunger, health and respawning.
|
||||||
- Windows client builds for GitHub and Gitea CI; release builds run without a
|
- Windows client builds for GitHub and Gitea CI; release builds run without a
|
||||||
console window.
|
console window.
|
||||||
|
- `cargo xtask pack` builds the client and server and packs them with their
|
||||||
|
assets into a distributable archive; CI publishes these archives.
|
||||||
|
|
|
||||||
|
|
@ -2638,6 +2638,16 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
|
|
@ -5428,6 +5438,16 @@ dependencies = [
|
||||||
"slotmap",
|
"slotmap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
|
@ -5686,6 +5706,12 @@ version = "2.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|
@ -6659,6 +6685,16 @@ version = "0.8.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"serde_json",
|
||||||
|
"tar",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.19"
|
version = "0.8.19"
|
||||||
|
|
@ -6782,6 +6818,20 @@ dependencies = [
|
||||||
"syn 3.0.9",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "9.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"nix",
|
||||||
|
"typed-path",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlib-rs"
|
name = "zlib-rs"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/game_core", "crates/game_server", "crates/game_client"]
|
members = ["crates/game_core", "crates/game_server", "crates/game_client", "xtask"]
|
||||||
default-members = ["crates/game_client"]
|
default-members = ["crates/game_client"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -25,6 +25,11 @@ opt-level = 1
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
# Used by `cargo xtask pack` for distributable builds.
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
strip = true
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
# Bevy system queries are naturally verbose.
|
# Bevy system queries are naturally verbose.
|
||||||
type_complexity = "allow"
|
type_complexity = "allow"
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -185,3 +185,17 @@ cargo clippy --workspace --all-targets
|
||||||
Tests run with [cargo-nextest](https://nexte.st) (`cargo install cargo-nextest --locked`);
|
Tests run with [cargo-nextest](https://nexte.st) (`cargo install cargo-nextest --locked`);
|
||||||
its configuration is in `.config/nextest.toml`. CI uses `--profile ci`, which writes a
|
its configuration is in `.config/nextest.toml`. CI uses `--profile ci`, which writes a
|
||||||
JUnit report to `target/nextest/ci/junit.xml`.
|
JUnit report to `target/nextest/ci/junit.xml`.
|
||||||
|
|
||||||
|
### Packaging
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo xtask pack # for the host platform
|
||||||
|
cargo xtask pack --target x86_64-pc-windows-gnu # cross-compile for Windows (needs MinGW)
|
||||||
|
```
|
||||||
|
|
||||||
|
`pack` builds the client (`game`) and the dedicated server (`game-server`) with the
|
||||||
|
stripped `dist` profile and writes `dist/station-<version>-<target>.zip` (Windows) or
|
||||||
|
`.tar.gz` (other platforms). The archive contains both binaries, `assets/`, the README
|
||||||
|
and the changelog. Unpack it and run the binaries from that directory. The unpacked
|
||||||
|
staging directory is kept next to the archive. Options: `--format zip|tar.gz`,
|
||||||
|
`--out-dir DIR`, `--debug`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
flate2 = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
tar = { version = "0.4", default-features = false }
|
||||||
|
zip = { version = "9", default-features = false, features = ["deflate-flate2"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
@ -0,0 +1,367 @@
|
||||||
|
//! Project automation, run with `cargo xtask <command>`.
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo xtask pack [--target TRIPLE] [--format zip|tar.gz] [--out-dir DIR] [--debug]
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
error::Error,
|
||||||
|
fs::{self, File},
|
||||||
|
io::{self, BufRead, BufReader, BufWriter, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::{Command, ExitCode, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
const PACKAGE_NAME: &str = "station";
|
||||||
|
/// `(package, binary)` pairs shipped in the archive.
|
||||||
|
const BINARIES: [(&str, &str); 2] = [("game_client", "game"), ("game_server", "game-server")];
|
||||||
|
/// Files and directories (relative to the workspace root) shipped next to the binaries.
|
||||||
|
const EXTRA_FILES: [&str; 3] = ["assets", "README.md", "CHANGELOG.md"];
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let mut args = env::args().skip(1);
|
||||||
|
let result = match args.next().as_deref() {
|
||||||
|
Some("pack") => PackOptions::parse(args).and_then(|opts| pack(&opts)),
|
||||||
|
Some("-h" | "--help") | None => {
|
||||||
|
print_help();
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
Some(other) => Err(format!("unknown command `{other}`, see `cargo xtask --help`").into()),
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("error: {err}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"usage: cargo xtask <command>\n\n\
|
||||||
|
commands:\n \
|
||||||
|
pack build the client and server and pack them with their assets\n\n\
|
||||||
|
pack options:\n \
|
||||||
|
--target TRIPLE cross-compile for TRIPLE (default: host)\n \
|
||||||
|
--format zip|tar.gz archive format (default: zip for Windows, tar.gz otherwise)\n \
|
||||||
|
--out-dir DIR output directory (default: dist)\n \
|
||||||
|
--debug pack a debug build instead of the stripped `dist` profile"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Format {
|
||||||
|
Zip,
|
||||||
|
TarGz,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format {
|
||||||
|
fn parse(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"zip" => Ok(Self::Zip),
|
||||||
|
"tar.gz" | "tgz" => Ok(Self::TarGz),
|
||||||
|
_ => Err(format!("unknown archive format `{s}` (expected zip or tar.gz)").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_for(target: &str) -> Self {
|
||||||
|
if target.contains("windows") {
|
||||||
|
Self::Zip
|
||||||
|
} else {
|
||||||
|
Self::TarGz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Zip => "zip",
|
||||||
|
Self::TarGz => "tar.gz",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct PackOptions {
|
||||||
|
target: Option<String>,
|
||||||
|
format: Option<Format>,
|
||||||
|
out_dir: Option<PathBuf>,
|
||||||
|
debug: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackOptions {
|
||||||
|
fn parse(mut args: impl Iterator<Item = String>) -> Result<Self> {
|
||||||
|
let mut opts = Self::default();
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
let mut value = |name: &str| args.next().ok_or_else(|| format!("{name} needs a value"));
|
||||||
|
match arg.as_str() {
|
||||||
|
"--target" => opts.target = Some(value("--target")?),
|
||||||
|
"--format" => opts.format = Some(Format::parse(&value("--format")?)?),
|
||||||
|
"--out-dir" => opts.out_dir = Some(value("--out-dir")?.into()),
|
||||||
|
"--debug" => opts.debug = true,
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_help();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown pack option `{other}`").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.expect("xtask lives inside the workspace")
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cargo() -> Command {
|
||||||
|
Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_target() -> Result<String> {
|
||||||
|
let output = Command::new(env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()))
|
||||||
|
.arg("-vV")
|
||||||
|
.output()?;
|
||||||
|
String::from_utf8(output.stdout)?
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("host: "))
|
||||||
|
.map(str::to_owned)
|
||||||
|
.ok_or_else(|| "could not determine the host target from `rustc -vV`".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pack(opts: &PackOptions) -> Result<()> {
|
||||||
|
let root = workspace_root();
|
||||||
|
let target = match &opts.target {
|
||||||
|
Some(target) => target.clone(),
|
||||||
|
None => host_target()?,
|
||||||
|
};
|
||||||
|
let format = opts.format.unwrap_or_else(|| Format::default_for(&target));
|
||||||
|
let out_dir = root.join(opts.out_dir.as_deref().unwrap_or(Path::new("dist")));
|
||||||
|
let name = format!("{PACKAGE_NAME}-{}-{target}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
let executables = build(&root, opts.target.as_deref(), opts.debug)?;
|
||||||
|
|
||||||
|
// The staging directory is kept so CI can upload it without double-zipping.
|
||||||
|
let stage = out_dir.join(&name);
|
||||||
|
if stage.exists() {
|
||||||
|
fs::remove_dir_all(&stage)?;
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&stage)?;
|
||||||
|
for exe in &executables {
|
||||||
|
fs::copy(
|
||||||
|
exe,
|
||||||
|
stage.join(exe.file_name().expect("executable has a file name")),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
for extra in EXTRA_FILES {
|
||||||
|
copy_recursive(&root.join(extra), &stage.join(extra))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive = out_dir.join(format!("{name}.{}", format.extension()));
|
||||||
|
write_archive(&stage, &name, &archive, format)?;
|
||||||
|
println!("packed {}", archive.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the shipped binaries and returns their paths, as reported by cargo.
|
||||||
|
fn build(root: &Path, target: Option<&str>, debug: bool) -> Result<Vec<PathBuf>> {
|
||||||
|
let mut cmd = cargo();
|
||||||
|
cmd.current_dir(root).args([
|
||||||
|
"build",
|
||||||
|
"--locked",
|
||||||
|
"--message-format=json-render-diagnostics",
|
||||||
|
]);
|
||||||
|
cmd.args(["--profile", if debug { "dev" } else { "dist" }]);
|
||||||
|
if let Some(target) = target {
|
||||||
|
cmd.args(["--target", target]);
|
||||||
|
}
|
||||||
|
for (package, bin) in BINARIES {
|
||||||
|
cmd.args(["-p", package, "--bin", bin]);
|
||||||
|
}
|
||||||
|
let mut child = cmd.stdout(Stdio::piped()).spawn()?;
|
||||||
|
|
||||||
|
let mut executables = Vec::new();
|
||||||
|
for line in BufReader::new(child.stdout.take().expect("stdout is piped")).lines() {
|
||||||
|
let msg: serde_json::Value = serde_json::from_str(&line?)?;
|
||||||
|
if msg["reason"] == "compiler-artifact"
|
||||||
|
&& let Some(exe) = msg["executable"].as_str()
|
||||||
|
&& BINARIES
|
||||||
|
.iter()
|
||||||
|
.any(|(_, bin)| msg["target"]["name"] == *bin)
|
||||||
|
{
|
||||||
|
executables.push(PathBuf::from(exe));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !child.wait()?.success() {
|
||||||
|
return Err("cargo build failed".into());
|
||||||
|
}
|
||||||
|
if executables.len() != BINARIES.len() {
|
||||||
|
return Err(format!(
|
||||||
|
"expected {} executables, cargo reported {executables:?}",
|
||||||
|
BINARIES.len()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Ok(executables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_recursive(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
|
if from.is_dir() {
|
||||||
|
fs::create_dir_all(to)?;
|
||||||
|
for entry in fs::read_dir(from)? {
|
||||||
|
let entry = entry?;
|
||||||
|
copy_recursive(&entry.path(), &to.join(entry.file_name()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
fs::copy(from, to).map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files below `dir` as `(path, archive path with '/' separators)`, sorted for reproducible archives.
|
||||||
|
fn collect_files(dir: &Path, prefix: &str) -> io::Result<Vec<(PathBuf, String)>> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<io::Result<_>>()?;
|
||||||
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
for entry in entries {
|
||||||
|
let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
files.extend(collect_files(&entry.path(), &name)?);
|
||||||
|
} else {
|
||||||
|
files.push((entry.path(), name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_executable(archive_path: &str) -> bool {
|
||||||
|
let file = archive_path.rsplit('/').next().unwrap_or(archive_path);
|
||||||
|
BINARIES
|
||||||
|
.iter()
|
||||||
|
.any(|(_, bin)| file == *bin || file == format!("{bin}.exe"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packs the contents of `dir` into `archive`, below a top-level directory `prefix`.
|
||||||
|
fn write_archive(dir: &Path, prefix: &str, archive: &Path, format: Format) -> Result<()> {
|
||||||
|
let files = collect_files(dir, prefix)?;
|
||||||
|
let out = BufWriter::new(File::create(archive)?);
|
||||||
|
match format {
|
||||||
|
Format::Zip => {
|
||||||
|
let mut zip = zip::ZipWriter::new(out);
|
||||||
|
for (path, name) in files {
|
||||||
|
let mode = if is_executable(&name) { 0o755 } else { 0o644 };
|
||||||
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
|
.compression_method(zip::CompressionMethod::Deflated)
|
||||||
|
.unix_permissions(mode);
|
||||||
|
zip.start_file(name, options)?;
|
||||||
|
io::copy(&mut File::open(path)?, &mut zip)?;
|
||||||
|
}
|
||||||
|
zip.finish()?.flush()?;
|
||||||
|
}
|
||||||
|
Format::TarGz => {
|
||||||
|
let gz = flate2::write::GzEncoder::new(out, flate2::Compression::default());
|
||||||
|
let mut tar = tar::Builder::new(gz);
|
||||||
|
for (path, name) in files {
|
||||||
|
let mut header = tar::Header::new_gnu();
|
||||||
|
header.set_size(fs::metadata(&path)?.len());
|
||||||
|
header.set_mode(if is_executable(&name) { 0o755 } else { 0o644 });
|
||||||
|
header.set_mtime(0);
|
||||||
|
header.set_cksum();
|
||||||
|
tar.append_data(&mut header, &name, File::open(&path)?)?;
|
||||||
|
}
|
||||||
|
tar.into_inner()?.finish()?.flush()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn temp_dir(name: &str) -> PathBuf {
|
||||||
|
let dir = env::temp_dir().join(format!("xtask-test-{name}-{}", std::process::id()));
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
fs::create_dir_all(dir.join("assets/data")).unwrap();
|
||||||
|
fs::write(dir.join("game"), b"binary").unwrap();
|
||||||
|
fs::write(dir.join("assets/data/items.rsn"), b"items: []").unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_format_follows_target() {
|
||||||
|
assert_eq!(Format::default_for("x86_64-pc-windows-msvc"), Format::Zip);
|
||||||
|
assert_eq!(Format::default_for("x86_64-pc-windows-gnu"), Format::Zip);
|
||||||
|
assert_eq!(
|
||||||
|
Format::default_for("x86_64-unknown-linux-gnu"),
|
||||||
|
Format::TarGz
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_pack_options() {
|
||||||
|
let args = ["--target", "x", "--format", "zip", "--debug"].map(String::from);
|
||||||
|
let opts = PackOptions::parse(args.into_iter()).unwrap();
|
||||||
|
assert_eq!(opts.target.as_deref(), Some("x"));
|
||||||
|
assert_eq!(opts.format, Some(Format::Zip));
|
||||||
|
assert!(opts.debug);
|
||||||
|
assert!(PackOptions::parse(["--target".to_owned()].into_iter()).is_err());
|
||||||
|
assert!(PackOptions::parse(["--bogus".to_owned()].into_iter()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zip_contains_prefixed_files_with_permissions() {
|
||||||
|
let dir = temp_dir("zip");
|
||||||
|
let archive = dir.with_extension("zip");
|
||||||
|
write_archive(&dir, "pkg", &archive, Format::Zip).unwrap();
|
||||||
|
|
||||||
|
let mut zip = zip::ZipArchive::new(File::open(&archive).unwrap()).unwrap();
|
||||||
|
let mut names: Vec<_> = zip.file_names().map(|n| n.unwrap().into_owned()).collect();
|
||||||
|
names.sort();
|
||||||
|
assert_eq!(names, ["pkg/assets/data/items.rsn", "pkg/game"]);
|
||||||
|
assert_eq!(
|
||||||
|
zip.by_name("pkg/game").unwrap().unix_mode().unwrap() & 0o777,
|
||||||
|
0o755
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
fs::remove_file(&archive).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tar_gz_contains_prefixed_files_with_permissions() {
|
||||||
|
let dir = temp_dir("tar");
|
||||||
|
let archive = dir.with_extension("tar.gz");
|
||||||
|
write_archive(&dir, "pkg", &archive, Format::TarGz).unwrap();
|
||||||
|
|
||||||
|
let gz = flate2::read::GzDecoder::new(File::open(&archive).unwrap());
|
||||||
|
let mut tar = tar::Archive::new(gz);
|
||||||
|
let entries: Vec<_> = tar
|
||||||
|
.entries()
|
||||||
|
.unwrap()
|
||||||
|
.map(|e| {
|
||||||
|
let e = e.unwrap();
|
||||||
|
(
|
||||||
|
e.path().unwrap().to_string_lossy().into_owned(),
|
||||||
|
e.header().mode().unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
entries,
|
||||||
|
[
|
||||||
|
("pkg/assets/data/items.rsn".to_owned(), 0o644),
|
||||||
|
("pkg/game".to_owned(), 0o755)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
|
fs::remove_file(&archive).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue