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
|
||||
/saves
|
||||
__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/scripts/`: Rune scripts (`.rn`), referenced as `"module::function"`.
|
||||
- `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
|
||||
example map (generated output is committed).
|
||||
- `.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
|
||||
`cargo run -p game_server --bin game-server`. The Windows client can be
|
||||
cross-built with `cargo build --release -p game_client --target
|
||||
x86_64-pc-windows-gnu` (needs MinGW). Running the client requires a Vulkan or
|
||||
`cargo run -p game_server --bin game-server`. Build a distributable archive
|
||||
with `cargo xtask pack` (add `--target x86_64-pc-windows-gnu` to cross-build
|
||||
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.
|
||||
|
||||
## 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.
|
||||
- Windows client builds for GitHub and Gitea CI; release builds run without a
|
||||
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
|
|
@ -5428,6 +5438,16 @@ dependencies = [
|
|||
"slotmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
|
|
@ -5686,6 +5706,12 @@ version = "2.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
|
@ -6659,6 +6685,16 @@ version = "0.8.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
version = "0.8.19"
|
||||
|
|
@ -6782,6 +6818,20 @@ dependencies = [
|
|||
"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]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
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"]
|
||||
|
||||
[workspace.package]
|
||||
|
|
@ -25,6 +25,11 @@ opt-level = 1
|
|||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
# Used by `cargo xtask pack` for distributable builds.
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
strip = true
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Bevy system queries are naturally verbose.
|
||||
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`);
|
||||
its configuration is in `.config/nextest.toml`. CI uses `--profile ci`, which writes a
|
||||
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