mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Launcher import: share the copy with native builds, test the checks
- native_mods::copy_base is the base-folder copy both a native build and an import start from; carried_over stays private. - is_game (platform magic and the mod API mark) and base_for (with --base) are pure and tested: a program without the mark, a --base that is not an installed release or Nightly, and a failed import that must leave nothing. - One reader for a plain file and a zip entry, refused past 1 GiB. - install_as runs the same after-install step as an import. - The Versions page draws native and imported builds with one helper. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a9a86950bd
commit
7990e63009
4 files changed
+197
-163
No files matched your search
@@ -822,8 +822,7 @@ pub(crate) fn install_as(
|
||||
fs::rename(&top, &target).map_err(|e| e.to_string())?;
|
||||
fs::write(target.join(COMPLETE), "").map_err(|e| e.to_string())?;
|
||||
let _ = fs::remove_dir_all(&partial);
|
||||
reuse_prepared(&root, tag);
|
||||
complete_assets(tag);
|
||||
reuse_prepared_for(tag);
|
||||
progress(1.0);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1163,9 +1162,8 @@ fn reuse_prepared(versions: &Path, tag: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// What an install does after its folder is in place, for a version put there another way (an
|
||||
/// imported build, `imported`): another installed version's resources if it has none, and
|
||||
/// whatever its resources lack.
|
||||
/// What an install does once its folder is in place (an imported build's too, `imported`):
|
||||
/// another installed version's resources if it has none, and whatever its resources lack.
|
||||
pub(crate) fn reuse_prepared_for(tag: &str) {
|
||||
reuse_prepared(&versions_dir(), tag);
|
||||
complete_assets(tag);
|
||||
|
||||
+120
-89
@@ -61,13 +61,29 @@ fn official(version: &str) -> bool {
|
||||
backend::is_game_tag(version) || crate::nightly::date_of(version).is_some()
|
||||
}
|
||||
|
||||
/// The installed version an import takes its game files from: `selected` when Horizon published
|
||||
/// it, else the newest installed release or Nightly (`installed` is newest first).
|
||||
pub fn base_for(installed: &[String], selected: Option<&str>) -> Option<String> {
|
||||
selected
|
||||
.filter(|s| official(s) && installed.iter().any(|t| t == s))
|
||||
.map(str::to_owned)
|
||||
.or_else(|| installed.iter().find(|t| official(t)).cloned())
|
||||
/// The installed version an import takes its game files from: `requested` (`--base`), else
|
||||
/// `selected` when Horizon published it, else the newest installed release or Nightly.
|
||||
fn base_for(
|
||||
installed: &[String],
|
||||
requested: Option<&str>,
|
||||
selected: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let usable = |v: &str| official(v) && installed.iter().any(|t| t == v);
|
||||
match requested {
|
||||
Some(b) if usable(b) => Ok(b.to_owned()),
|
||||
Some(b) => Err(format!("{b} is not an installed release or Nightly.")),
|
||||
None => {
|
||||
selected
|
||||
.filter(|s| usable(s))
|
||||
.map(str::to_owned)
|
||||
.or_else(|| native_mods::newest_installed(installed).cloned())
|
||||
.ok_or_else(|| {
|
||||
tr("Install a release or Nightly first: an imported build plays with its game \
|
||||
files.")
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs the game program at `path` (or the one inside a zip there) as a new version on the
|
||||
@@ -79,22 +95,11 @@ pub fn import(path: &Path, base: Option<&str>) -> Result<String, String> {
|
||||
|n| n.to_string_lossy().into_owned(),
|
||||
);
|
||||
let program = read_program(path, &file)?;
|
||||
if !runs_here(&program) || horizon_mods::api_level::read_mark(&program).is_none() {
|
||||
return Err(trf(
|
||||
"{file} is not a Horizon game for this computer.",
|
||||
&[("file", &file)],
|
||||
));
|
||||
if !is_game(&program) {
|
||||
return Err(not_a_game(&file));
|
||||
}
|
||||
let installed = backend::installed();
|
||||
let selected = backend::Settings::load().selected;
|
||||
let base = match base {
|
||||
Some(b) if official(b) && installed.iter().any(|t| t == b) => b.to_owned(),
|
||||
Some(b) => return Err(format!("{b} is not an installed release or Nightly.")),
|
||||
None => base_for(&installed, selected.as_deref()).ok_or_else(|| {
|
||||
tr("Install a release or Nightly first: an imported build plays with its game files.")
|
||||
.to_string()
|
||||
})?,
|
||||
};
|
||||
let base = base_for(&backend::installed(), base, selected.as_deref())?;
|
||||
let date = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let tag = install_in(&backend::version_dir(""), &base, &program, &file, &date)?;
|
||||
backend::reuse_prepared_for(&tag);
|
||||
@@ -105,42 +110,32 @@ pub fn import(path: &Path, base: Option<&str>) -> Result<String, String> {
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
fn not_a_game(file: &str) -> String {
|
||||
trf(
|
||||
"{file} is not a Horizon game for this computer.",
|
||||
&[("file", file)],
|
||||
)
|
||||
}
|
||||
|
||||
/// The program at `path`: the file itself when it is named as the game is (`horizon-client`,
|
||||
/// "horizon-client (1).exe" as a browser saves a second copy), or from a zip (a GitHub Actions
|
||||
/// artifact), its one entry named as the game is on this platform (`CLIENT`), in any folder.
|
||||
/// The launcher and horizon-ctl carry the mod API mark too: the name tells them apart.
|
||||
fn read_program(path: &Path, file: &str) -> Result<Vec<u8>, String> {
|
||||
let not_a_game = || {
|
||||
trf(
|
||||
"{file} is not a Horizon game for this computer.",
|
||||
&[("file", file)],
|
||||
)
|
||||
};
|
||||
let open = fs::File::open(path).map_err(|e| format!("{file}: {e}"))?;
|
||||
let Ok(mut zip) = zip::ZipArchive::new(open) else {
|
||||
let size = fs::metadata(path)
|
||||
.map_err(|e| format!("{file}: {e}"))?
|
||||
.len();
|
||||
if !file.to_ascii_lowercase().starts_with("horizon-client") || size > MAX_PROGRAM {
|
||||
return Err(not_a_game());
|
||||
let open = || fs::File::open(path).map_err(|e| format!("{file}: {e}"));
|
||||
let Ok(mut zip) = zip::ZipArchive::new(open()?) else {
|
||||
if !file.to_ascii_lowercase().starts_with("horizon-client") {
|
||||
return Err(not_a_game(file));
|
||||
}
|
||||
return fs::read(path).map_err(|e| format!("{file}: {e}"));
|
||||
return read_at_most(open()?, file);
|
||||
};
|
||||
for i in 0..zip.len() {
|
||||
let mut entry = zip.by_index(i).map_err(|e| format!("{file}: {e}"))?;
|
||||
let entry = zip.by_index(i).map_err(|e| format!("{file}: {e}"))?;
|
||||
let named = entry
|
||||
.enclosed_name()
|
||||
.is_some_and(|p| p.file_name() == Some(std::ffi::OsStr::new(CLIENT)));
|
||||
if named && entry.is_file() {
|
||||
let mut program = Vec::new();
|
||||
(&mut entry)
|
||||
.take(MAX_PROGRAM + 1)
|
||||
.read_to_end(&mut program)
|
||||
.map_err(|e| format!("{file}: {e}"))?;
|
||||
if program.len() as u64 > MAX_PROGRAM {
|
||||
return Err(not_a_game());
|
||||
}
|
||||
return Ok(program);
|
||||
return read_at_most(entry, file);
|
||||
}
|
||||
}
|
||||
Err(trf(
|
||||
@@ -149,9 +144,22 @@ fn read_program(path: &Path, file: &str) -> Result<Vec<u8>, String> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether `program` starts as a program of this platform does: PE on Windows, Mach-O on macOS,
|
||||
/// ELF elsewhere. A Linux build picked on Windows would only fail to start.
|
||||
fn runs_here(program: &[u8]) -> bool {
|
||||
/// All of `from`, refused past [`MAX_PROGRAM`] rather than cut off.
|
||||
fn read_at_most(from: impl Read, file: &str) -> Result<Vec<u8>, String> {
|
||||
let mut program = Vec::new();
|
||||
from.take(MAX_PROGRAM + 1)
|
||||
.read_to_end(&mut program)
|
||||
.map_err(|e| format!("{file}: {e}"))?;
|
||||
if program.len() as u64 > MAX_PROGRAM {
|
||||
return Err(not_a_game(file));
|
||||
}
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
/// Whether `program` is a Horizon game for this computer: it starts as this platform's programs
|
||||
/// do (PE on Windows, Mach-O on macOS, ELF elsewhere: a Linux build picked on Windows would
|
||||
/// only fail to start) and carries the mod API mark every game since 0.2.69 does.
|
||||
fn is_game(program: &[u8]) -> bool {
|
||||
let magic: &[&[u8]] = if cfg!(windows) {
|
||||
&[b"MZ"]
|
||||
} else if cfg!(target_os = "macos") {
|
||||
@@ -161,6 +169,7 @@ fn runs_here(program: &[u8]) -> bool {
|
||||
&[b"\x7fELF"]
|
||||
};
|
||||
magic.iter().any(|m| program.starts_with(m))
|
||||
&& horizon_mods::api_level::read_mark(program).is_some()
|
||||
}
|
||||
|
||||
/// Installs `program` in `versions` as a copy of installed version `base` with it in place of
|
||||
@@ -197,48 +206,37 @@ fn install_in(
|
||||
let dir = versions.join(&tag);
|
||||
let partial = versions.join(format!(".{tag}.partial"));
|
||||
let _ = fs::remove_dir_all(&partial);
|
||||
fs::create_dir_all(&partial).map_err(|e| e.to_string())?;
|
||||
let record = serde_json::json!({ "base": base, "file": file, "sha256": sha256 });
|
||||
let copied = fill(&versions.join(base), &partial, program, &record).and_then(|()| {
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::rename(&partial, &dir)?;
|
||||
fs::write(dir.join(".installed"), "")
|
||||
});
|
||||
if let Err(e) = copied {
|
||||
let made = fs::create_dir_all(&partial)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|()| native_mods::copy_base(&versions.join(base), &partial, Target::Client))
|
||||
.and_then(|()| finish(&partial, &dir, program, &record).map_err(|e| e.to_string()));
|
||||
if let Err(e) = made {
|
||||
let _ = fs::remove_dir_all(&partial);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
return Err(trf(
|
||||
"Could not import the build: {error}",
|
||||
&[("error", &e.to_string())],
|
||||
));
|
||||
return Err(trf("Could not import the build: {error}", &[("error", &e)]));
|
||||
}
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
/// Fills the new version's folder `to`: version folder `base`'s files but its game and its
|
||||
/// record (`native_mods::carried_over`, cloned where the disk can), `program` as the game, and
|
||||
/// what it was made from.
|
||||
fn fill(base: &Path, to: &Path, program: &[u8], record: &serde_json::Value) -> std::io::Result<()> {
|
||||
for entry in fs::read_dir(base)?.flatten() {
|
||||
let name = entry.file_name();
|
||||
if !native_mods::carried_over(&name, Target::Client) || name == RECORD {
|
||||
continue;
|
||||
}
|
||||
let target = to.join(&name);
|
||||
if entry.path().is_dir() {
|
||||
backend::copy_dir(&entry.path(), &target)?;
|
||||
} else {
|
||||
horizon_prepare::clone_or_copy(&entry.path(), &target)?;
|
||||
fs::set_permissions(&target, entry.metadata()?.permissions())?;
|
||||
}
|
||||
}
|
||||
fs::write(to.join(CLIENT), program)?;
|
||||
/// Puts `program` and its record into the copied folder `partial`, then moves it into place as
|
||||
/// installed version `dir`.
|
||||
fn finish(
|
||||
partial: &Path,
|
||||
dir: &Path,
|
||||
program: &[u8],
|
||||
record: &serde_json::Value,
|
||||
) -> std::io::Result<()> {
|
||||
fs::write(partial.join(CLIENT), program)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(to.join(CLIENT), fs::Permissions::from_mode(0o755))?;
|
||||
fs::set_permissions(partial.join(CLIENT), fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
fs::write(to.join(RECORD), record.to_string())
|
||||
fs::write(partial.join(RECORD), record.to_string())?;
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
fs::rename(partial, dir)?;
|
||||
fs::write(dir.join(".installed"), "")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -319,11 +317,36 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(versions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_import_leaves_nothing_behind() {
|
||||
let versions = dir("failed");
|
||||
// A folder from an import that never finished is not that program's version.
|
||||
let built = program(b"build");
|
||||
let unfinished = install_in(&versions, "v0.2.95", &built, CLIENT, "2026-10-01");
|
||||
assert!(unfinished.is_err(), "no base to copy");
|
||||
assert_eq!(fs::read_dir(&versions).unwrap().count(), 0);
|
||||
let short: String = Sha256::digest(&built)[..4]
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect();
|
||||
fs::create_dir_all(versions.join(format!("imported-2026-10-01-{}", &short[..7]))).unwrap();
|
||||
assert!(install_in(&versions, "v0.2.95", &built, CLIENT, "2026-10-02").is_err());
|
||||
let _ = fs::remove_dir_all(versions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_horizon_game_for_this_computer_is_imported() {
|
||||
assert!(runs_here(&program(b"")));
|
||||
let other = if cfg!(windows) { b"\x7fELF" } else { b"MZ\0\0" };
|
||||
assert!(!runs_here(other));
|
||||
assert!(is_game(&program(b"")));
|
||||
// Another program of this platform, without the game's mark.
|
||||
assert!(!is_game(&program(b"")[..program(b"").len() - 27]));
|
||||
let other = if cfg!(windows) {
|
||||
&b"\x7fELF"[..]
|
||||
} else {
|
||||
&b"MZ"[..]
|
||||
};
|
||||
assert!(!is_game(
|
||||
&[other, &horizon_mods::api_level::MARK[..]].concat()
|
||||
));
|
||||
let d = dir("zip");
|
||||
let zip_path = d.join("horizon-client-windows-x86_64.zip");
|
||||
let mut zip = zip::ZipWriter::new(fs::File::create(&zip_path).unwrap());
|
||||
@@ -362,16 +385,24 @@ mod tests {
|
||||
"native-c9d474d-0123456789ab".to_string(),
|
||||
"imported-2026-10-31-abcdef0".to_string(),
|
||||
];
|
||||
let newest = Ok("nightly-2026-11-01-c9d474d".to_string());
|
||||
assert_eq!(
|
||||
base_for(&installed, Some("v0.2.95")).as_deref(),
|
||||
Some("v0.2.95")
|
||||
base_for(&installed, None, Some("v0.2.95")),
|
||||
Ok("v0.2.95".to_string())
|
||||
);
|
||||
for selected in [None, Some("imported-2026-10-31-abcdef0"), Some("v0.1.0")] {
|
||||
assert_eq!(
|
||||
base_for(&installed, selected).as_deref(),
|
||||
Some("nightly-2026-11-01-c9d474d")
|
||||
);
|
||||
assert_eq!(base_for(&installed, None, selected), newest);
|
||||
}
|
||||
assert!(base_for(&installed[2..], None, None).is_err());
|
||||
// --base: an installed release or Nightly, never another local build.
|
||||
let asked = |b| base_for(&installed, Some(b), Some("v0.2.95"));
|
||||
assert_eq!(asked("nightly-2026-11-01-c9d474d"), newest);
|
||||
for b in [
|
||||
"v0.1.0",
|
||||
"native-c9d474d-0123456789ab",
|
||||
"imported-2026-10-31-abcdef0",
|
||||
] {
|
||||
assert!(asked(b).is_err(), "{b}");
|
||||
}
|
||||
assert_eq!(base_for(&installed[2..], None), None);
|
||||
}
|
||||
}
|
||||
+50
-52
@@ -4124,6 +4124,37 @@ impl Launcher {
|
||||
.children(error.map(|e| notice(e, &t)))
|
||||
}
|
||||
|
||||
/// The installed builds of one kind made here (`is`: native or imported) under their caption,
|
||||
/// with a notice above them; None when there are none.
|
||||
fn local_builds(
|
||||
&self,
|
||||
title: &str,
|
||||
is: fn(&str) -> bool,
|
||||
notice_: Option<&str>,
|
||||
on_disk: &dyn Fn(&String) -> Option<u64>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Div> {
|
||||
let t = self.theme;
|
||||
let tags: Vec<&String> = self
|
||||
.installed
|
||||
.iter()
|
||||
.chain(&self.removing)
|
||||
.filter(|v| is(v))
|
||||
.collect();
|
||||
(!tags.is_empty()).then(|| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(8.))
|
||||
.child(caps(tr(title)).text_color(t.muted))
|
||||
.children(notice_.map(|n| notice(n.to_string(), &t)))
|
||||
.children(
|
||||
tags.into_iter()
|
||||
.map(|tag| self.local_build_row(tag, on_disk, cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn versions_page(&mut self, cx: &mut Context<Self>) -> Div {
|
||||
let t = self.theme;
|
||||
let header = page_header(
|
||||
@@ -4189,58 +4220,25 @@ impl Launcher {
|
||||
.child(import),
|
||||
)
|
||||
.children(self.import_error.clone().map(|e| notice(e, &t)));
|
||||
// Native-mods builds are made here, not downloaded: they are listed apart from the releases.
|
||||
let native: Vec<String> = self
|
||||
.installed
|
||||
.iter()
|
||||
.chain(&self.removing)
|
||||
.filter(|v| native_mods::is_native(v))
|
||||
.cloned()
|
||||
.collect();
|
||||
let page = page.when(!native.is_empty(), |page| {
|
||||
page.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(8.))
|
||||
.child(caps(tr("Native mod builds")).text_color(t.muted))
|
||||
.children(
|
||||
native
|
||||
.iter()
|
||||
.map(|tag| self.local_build_row(tag, &on_disk, cx)),
|
||||
),
|
||||
)
|
||||
});
|
||||
// Imported builds: programs the player picked, which nothing signed.
|
||||
let imports: Vec<String> = self
|
||||
.installed
|
||||
.iter()
|
||||
.chain(&self.removing)
|
||||
.filter(|v| imported::is_imported(v))
|
||||
.cloned()
|
||||
.collect();
|
||||
let page = page.when(!imports.is_empty(), |page| {
|
||||
page.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(8.))
|
||||
.child(caps(tr("Imported builds")).text_color(t.muted))
|
||||
.child(notice(
|
||||
tr(
|
||||
"Unsigned: you picked these programs yourself, and Horizon can't \
|
||||
check where they came from. Updates never replace them.",
|
||||
)
|
||||
.to_string(),
|
||||
&t,
|
||||
))
|
||||
.children(
|
||||
imports
|
||||
.iter()
|
||||
.map(|tag| self.local_build_row(tag, &on_disk, cx)),
|
||||
),
|
||||
)
|
||||
});
|
||||
// Builds made here, not downloaded, are listed apart from the releases.
|
||||
let page = page
|
||||
.children(self.local_builds(
|
||||
"Native mod builds",
|
||||
native_mods::is_native,
|
||||
None,
|
||||
&on_disk,
|
||||
cx,
|
||||
))
|
||||
.children(self.local_builds(
|
||||
"Imported builds",
|
||||
imported::is_imported,
|
||||
Some(tr(
|
||||
"Unsigned: you picked these programs yourself, and Horizon can't \
|
||||
check where they came from. Updates never replace them.",
|
||||
)),
|
||||
&on_disk,
|
||||
cx,
|
||||
));
|
||||
let releases = match &self.releases {
|
||||
Load::Loading => return page.child(centered(tr("Loading releases…"), &t)),
|
||||
Load::Failed(e) => {
|
||||
|
||||
+24
-17
@@ -796,7 +796,7 @@ fn about_in(dir: &Path) -> Option<About> {
|
||||
}
|
||||
|
||||
/// The newest installed release or Nightly in `installed` (newest first).
|
||||
fn newest_installed(installed: &[String]) -> Option<&String> {
|
||||
pub(crate) fn newest_installed(installed: &[String]) -> Option<&String> {
|
||||
installed
|
||||
.iter()
|
||||
.find(|t| backend::is_game_tag(t) || t.starts_with("nightly-"))
|
||||
@@ -1725,21 +1725,7 @@ fn install(
|
||||
let partial = target.directory(&format!(".{version}.partial"));
|
||||
let _ = fs::remove_dir_all(&partial);
|
||||
fs::create_dir_all(&partial).map_err(|e| e.to_string())?;
|
||||
for entry in fs::read_dir(&from).map_err(|e| e.to_string())?.flatten() {
|
||||
let name = entry.file_name();
|
||||
if !carried_over(&name, target) {
|
||||
continue;
|
||||
}
|
||||
let to = partial.join(&name);
|
||||
let copied = if entry.path().is_dir() {
|
||||
backend::copy_dir(&entry.path(), &to)
|
||||
} else {
|
||||
// A clone is a new file: horizon-ctl and horizon-server keep their permissions.
|
||||
horizon_prepare::clone_or_copy(&entry.path(), &to)
|
||||
.and_then(|()| fs::set_permissions(&to, entry.metadata()?.permissions()))
|
||||
};
|
||||
copied.map_err(|e| format!("Could not copy {}: {e}", entry.path().display()))?;
|
||||
}
|
||||
copy_base(&from, &partial, target)?;
|
||||
let installed = partial.join(target.binary());
|
||||
fs::copy(binary, &installed).map_err(|e| format!("The built game: {e}"))?;
|
||||
fs::write(partial.join("native-mods.json"), about.to_string()).map_err(|e| e.to_string())?;
|
||||
@@ -1753,12 +1739,33 @@ fn install(
|
||||
fs::write(dir.join(".installed"), "").map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Copies version folder `from` into `to` but for what a new build of `target` makes its own
|
||||
/// ([`carried_over`]), cloned where the disk can. Also an imported build's start (`imported`).
|
||||
pub(crate) fn copy_base(from: &Path, to: &Path, target: Target) -> Result<(), String> {
|
||||
for entry in fs::read_dir(from).map_err(|e| e.to_string())?.flatten() {
|
||||
let name = entry.file_name();
|
||||
if !carried_over(&name, target) {
|
||||
continue;
|
||||
}
|
||||
let to = to.join(&name);
|
||||
let copied = if entry.path().is_dir() {
|
||||
backend::copy_dir(&entry.path(), &to)
|
||||
} else {
|
||||
// A clone is a new file: horizon-ctl and horizon-server keep their permissions.
|
||||
horizon_prepare::clone_or_copy(&entry.path(), &to)
|
||||
.and_then(|()| fs::set_permissions(&to, entry.metadata()?.permissions()))
|
||||
};
|
||||
copied.map_err(|e| format!("Could not copy {}: {e}", entry.path().display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether a file of the version a build starts from goes into the new build. The game (the
|
||||
/// target's own binary), its record, and the mods' post-build data (`native-data/`, which the
|
||||
/// new build's steps write again) are the new build's own: a mod turned off leaves none of its
|
||||
/// data behind. A server build is the headless engine: it keeps neither the game's own binary
|
||||
/// nor any `horizon-engine` the base shipped, which would run unmodded beside its own.
|
||||
pub(crate) fn carried_over(name: &std::ffi::OsStr, target: Target) -> bool {
|
||||
fn carried_over(name: &std::ffi::OsStr, target: Target) -> bool {
|
||||
let mut own: Vec<String> = ["Horizon.app", "native-mods.json", "native-data"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
|
||||
Reference in new issue
Block a user