feat(release): 支持自定义打包产物目录
release 现在会在设置 EMOD_ARTIFACTS_DIR 时将 ZIP 输出到按项目名隔离的 artifacts 子目录,未设置时保持写入项目目录。 新增单元测试覆盖默认输出和环境变量输出两条路径,避免发布产物污染源项目或路径回退失效。
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
@@ -14,6 +15,8 @@ use crate::error::{CliError, Result};
|
||||
use crate::utils::file;
|
||||
use crate::{entity, entity::project::ReleaseInfo};
|
||||
|
||||
const ARTIFACTS_DIR_ENV: &str = "EMOD_ARTIFACTS_DIR";
|
||||
|
||||
pub fn execute(args: &ReleaseArgs) {
|
||||
if let Err(e) = run_release(args) {
|
||||
eprintln!("❌ 组件打包失败: {}", e);
|
||||
@@ -34,11 +37,7 @@ fn run_release(args: &ReleaseArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(
|
||||
args: &ReleaseArgs,
|
||||
project_dir: &PathBuf,
|
||||
release_info: &ReleaseInfo,
|
||||
) -> Result<()> {
|
||||
fn release(args: &ReleaseArgs, project_dir: &PathBuf, release_info: &ReleaseInfo) -> Result<()> {
|
||||
let new_version = calculate_version(&args.ver, args.pin, &release_info.behavior_version)?;
|
||||
let version_value = Value::Array(new_version.iter().map(|v| Value::from(*v)).collect());
|
||||
|
||||
@@ -50,16 +49,12 @@ fn release(
|
||||
|
||||
let version_str = format!("{}.{}.{}", new_version[0], new_version[1], new_version[2]);
|
||||
let output_path = package_project(&project_dir, &pack_dirs, &version_str)?;
|
||||
|
||||
println!("📦 打包完成: {}", output_path.replace("\\", "/"));
|
||||
let display_path = output_path.display().to_string().replace("\\", "/");
|
||||
println!("📦 打包完成: {}", display_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_version(
|
||||
version: &Option<String>,
|
||||
pin: bool,
|
||||
current: &[u32],
|
||||
) -> Result<Vec<u32>> {
|
||||
fn calculate_version(version: &Option<String>, pin: bool, current: &[u32]) -> Result<Vec<u32>> {
|
||||
if let Some(ver_str) = version {
|
||||
ver_str
|
||||
.split(".")
|
||||
@@ -116,11 +111,7 @@ fn ensure_pack_ready(label: &str, pack_dir: &Path, identifier: &str) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_versions(
|
||||
project_dir: &PathBuf,
|
||||
version: &Value,
|
||||
pack_dirs: &PackDirs,
|
||||
) -> Result<()> {
|
||||
fn update_versions(project_dir: &PathBuf, version: &Value, pack_dirs: &PackDirs) -> Result<()> {
|
||||
update_pack_json(&project_dir, &version)?;
|
||||
update_manifest_json(&version, pack_dirs)?;
|
||||
Ok(())
|
||||
@@ -155,11 +146,7 @@ fn update_manifest_json(version: &Value, pack_dirs: &PackDirs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package_project(
|
||||
project_dir: &PathBuf,
|
||||
pack_dirs: &PackDirs,
|
||||
version: &str,
|
||||
) -> Result<String> {
|
||||
fn package_project(project_dir: &PathBuf, pack_dirs: &PackDirs, version: &str) -> Result<PathBuf> {
|
||||
let canonical_project_dir = project_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| file::io_error("解析项目目录", project_dir, e))?;
|
||||
@@ -178,15 +165,25 @@ fn package_project(
|
||||
canonical_project_dir.display()
|
||||
))
|
||||
})?;
|
||||
let output_path = format!(
|
||||
"{}/{}_release_{}.zip",
|
||||
project_dir.display(),
|
||||
project_name,
|
||||
version
|
||||
|
||||
let output_dir =
|
||||
match env::var_os(ARTIFACTS_DIR_ENV).filter(|value| !value.as_os_str().is_empty()) {
|
||||
Some(artifacts_dir) => {
|
||||
let output_dir = PathBuf::from(artifacts_dir).join(project_name);
|
||||
fs::create_dir_all(&output_dir)
|
||||
.map_err(|e| file::io_error("创建打包输出目录", &output_dir, e))?;
|
||||
println!(
|
||||
"📦 使用 {} 指定的打包输出目录: {}",
|
||||
ARTIFACTS_DIR_ENV,
|
||||
output_dir.display().to_string().replace("\\", "/")
|
||||
);
|
||||
let output_pathbuf = PathBuf::from(&output_path);
|
||||
output_dir
|
||||
}
|
||||
None => project_dir.clone(),
|
||||
};
|
||||
let output_path = output_dir.join(format!("{project_name}_release_{version}.zip"));
|
||||
let file = fs::File::create(&output_path)
|
||||
.map_err(|e| file::io_error("创建打包文件", &output_pathbuf, e))?;
|
||||
.map_err(|e| file::io_error("创建打包文件", &output_path, e))?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
let ignore = EmodIgnore::load(project_dir)?;
|
||||
@@ -276,8 +273,7 @@ fn add_world_pack_files(
|
||||
}
|
||||
|
||||
zip.start_file(name, options)?;
|
||||
let mut f =
|
||||
File::open(&path).map_err(|e| file::io_error("打开打包文件", &path, e))?;
|
||||
let mut f = File::open(&path).map_err(|e| file::io_error("打开打包文件", &path, e))?;
|
||||
f.read_to_end(&mut buffer)
|
||||
.map_err(|e| file::io_error("读取打包文件", &path, e))?;
|
||||
zip.write_all(&buffer)?;
|
||||
@@ -342,8 +338,7 @@ fn add_with_package(
|
||||
continue;
|
||||
}
|
||||
zip.start_file(&path_str, options)?;
|
||||
let mut f =
|
||||
File::open(path).map_err(|e| file::io_error("打开打包文件", path, e))?;
|
||||
let mut f = File::open(path).map_err(|e| file::io_error("打开打包文件", path, e))?;
|
||||
f.read_to_end(&mut buffer)
|
||||
.map_err(|e| file::io_error("读取打包文件", path, e))?;
|
||||
zip.write_all(&buffer)?;
|
||||
@@ -554,10 +549,7 @@ fn normalize_path(path: &Path) -> String {
|
||||
/// review tooling rejects as "中文或特殊符号". Always normalize to '/'.
|
||||
fn zip_entry_path(relative_path: &Path) -> Result<String> {
|
||||
let s = relative_path.to_str().ok_or_else(|| {
|
||||
crate::error::CliError::InvalidData(format!(
|
||||
"{:?} 不是有效的 UTF-8 路径",
|
||||
relative_path
|
||||
))
|
||||
crate::error::CliError::InvalidData(format!("{:?} 不是有效的 UTF-8 路径", relative_path))
|
||||
})?;
|
||||
Ok(s.replace('\\', "/"))
|
||||
}
|
||||
@@ -614,9 +606,12 @@ mod tests {
|
||||
use super::*;
|
||||
use std::{
|
||||
fs,
|
||||
sync::Mutex,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
static ARTIFACTS_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn preflight_pack_dirs_reports_missing_behavior_pack() {
|
||||
let project_dir = temp_project_dir();
|
||||
@@ -675,6 +670,66 @@ mod tests {
|
||||
assert_eq!(v, vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_writes_to_project_dir_when_env_unset() {
|
||||
let project_dir = temp_project_dir();
|
||||
let release_info = release_info();
|
||||
|
||||
write_file(
|
||||
&project_dir.join("behavior_pack_abcdefgh/scripts/main.py"),
|
||||
"main",
|
||||
);
|
||||
write_file(
|
||||
&project_dir.join("resource_pack_ijklmnop/textures/icon.png"),
|
||||
"icon",
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.2.3").unwrap();
|
||||
let project_name = project_dir.file_name().unwrap().to_string_lossy();
|
||||
let expected_path = project_dir.join(format!("{project_name}_release_1.2.3.zip"));
|
||||
|
||||
assert_eq!(zip_path, expected_path);
|
||||
assert!(zip_path.is_file());
|
||||
|
||||
fs::remove_dir_all(project_dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_writes_to_artifacts_subdir_when_env_set() {
|
||||
let project_dir = temp_project_dir();
|
||||
let artifacts_dir = temp_project_dir();
|
||||
let release_info = release_info();
|
||||
|
||||
write_file(
|
||||
&project_dir.join("behavior_pack_abcdefgh/scripts/main.py"),
|
||||
"main",
|
||||
);
|
||||
write_file(
|
||||
&project_dir.join("resource_pack_ijklmnop/textures/icon.png"),
|
||||
"icon",
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env(&project_dir, &pack_dirs, "1.0.0", &artifacts_dir)
|
||||
.unwrap();
|
||||
let project_name = project_dir.file_name().unwrap().to_string_lossy();
|
||||
let expected_path = artifacts_dir
|
||||
.join(project_name.as_ref())
|
||||
.join(format!("{project_name}_release_1.0.0.zip"));
|
||||
let project_dir_zip = project_dir.join(format!("{project_name}_release_1.0.0.zip"));
|
||||
|
||||
assert_eq!(zip_path, expected_path);
|
||||
assert!(zip_path.is_file());
|
||||
assert!(expected_path.parent().unwrap().is_dir());
|
||||
assert!(!project_dir_zip.exists());
|
||||
|
||||
fs::remove_dir_all(project_dir).unwrap();
|
||||
fs::remove_dir_all(artifacts_dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_project_skips_files_listed_in_emod_ignore() {
|
||||
let project_dir = temp_project_dir();
|
||||
@@ -702,7 +757,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"behavior_pack_abcdefgh/scripts/main.py".to_string()));
|
||||
@@ -736,7 +792,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"behavior_pack_abcdefgh/live/main.py".to_string()));
|
||||
@@ -763,7 +820,8 @@ mod tests {
|
||||
write_file(&project_dir.join("world_resource_packs.json"), "[]");
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"world_behavior_packs.json".to_string()));
|
||||
@@ -796,7 +854,8 @@ mod tests {
|
||||
write_file(&project_dir.join("misc/notes.md"), "notes");
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"behavior_pack_abcdefgh/scripts/main.py".to_string()));
|
||||
@@ -831,7 +890,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"behavior_pack_abcdefgh/scripts/main.py".to_string()));
|
||||
@@ -846,7 +906,10 @@ mod tests {
|
||||
let release_info = release_info();
|
||||
|
||||
write_file(&project_dir.join(".emod-package"), "**/*.json\n");
|
||||
write_file(&project_dir.join("behavior_pack_abcdefgh/manifest.json"), "{}");
|
||||
write_file(
|
||||
&project_dir.join("behavior_pack_abcdefgh/manifest.json"),
|
||||
"{}",
|
||||
);
|
||||
write_file(
|
||||
&project_dir.join("behavior_pack_abcdefgh/scripts/main.py"),
|
||||
"main",
|
||||
@@ -854,7 +917,8 @@ mod tests {
|
||||
write_file(&project_dir.join("world_behavior_packs.json"), "[]");
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
assert!(entries.contains(&"behavior_pack_abcdefgh/manifest.json".to_string()));
|
||||
@@ -885,7 +949,8 @@ mod tests {
|
||||
write_file(&project_dir.join("world_resource_packs.json"), "[]");
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
for entry in &entries {
|
||||
@@ -904,7 +969,10 @@ mod tests {
|
||||
let project_dir = temp_project_dir();
|
||||
let release_info = release_info();
|
||||
|
||||
write_file(&project_dir.join(".emod-package"), "behavior_pack_abcdefgh/\n");
|
||||
write_file(
|
||||
&project_dir.join(".emod-package"),
|
||||
"behavior_pack_abcdefgh/\n",
|
||||
);
|
||||
write_file(
|
||||
&project_dir.join("behavior_pack_abcdefgh/BoxData/data.json"),
|
||||
"{}",
|
||||
@@ -915,7 +983,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let pack_dirs = pack_dirs(&project_dir, &release_info);
|
||||
let zip_path = package_project(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let zip_path =
|
||||
package_project_with_artifacts_env_unset(&project_dir, &pack_dirs, "1.0.0").unwrap();
|
||||
let entries = zip_entries(&zip_path);
|
||||
|
||||
for entry in &entries {
|
||||
@@ -964,7 +1033,48 @@ mod tests {
|
||||
fs::write(path, content).unwrap();
|
||||
}
|
||||
|
||||
fn zip_entries(path: &str) -> Vec<String> {
|
||||
fn package_project_with_artifacts_env_unset(
|
||||
project_dir: &PathBuf,
|
||||
pack_dirs: &PackDirs,
|
||||
version: &str,
|
||||
) -> Result<PathBuf> {
|
||||
let _guard = ARTIFACTS_ENV_LOCK.lock().unwrap();
|
||||
remove_artifacts_dir_env();
|
||||
let result = package_project(project_dir, pack_dirs, version);
|
||||
remove_artifacts_dir_env();
|
||||
result
|
||||
}
|
||||
|
||||
fn package_project_with_artifacts_env(
|
||||
project_dir: &PathBuf,
|
||||
pack_dirs: &PackDirs,
|
||||
version: &str,
|
||||
artifacts_dir: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
let _guard = ARTIFACTS_ENV_LOCK.lock().unwrap();
|
||||
set_artifacts_dir_env(artifacts_dir);
|
||||
let result = package_project(project_dir, pack_dirs, version);
|
||||
remove_artifacts_dir_env();
|
||||
result
|
||||
}
|
||||
|
||||
fn set_artifacts_dir_env(path: &Path) {
|
||||
// SAFETY: tests serialize every EMOD_ARTIFACTS_DIR mutation and every
|
||||
// package_project call through ARTIFACTS_ENV_LOCK.
|
||||
unsafe {
|
||||
env::set_var(ARTIFACTS_DIR_ENV, path.as_os_str());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_artifacts_dir_env() {
|
||||
// SAFETY: tests serialize every EMOD_ARTIFACTS_DIR mutation and every
|
||||
// package_project call through ARTIFACTS_ENV_LOCK.
|
||||
unsafe {
|
||||
env::remove_var(ARTIFACTS_DIR_ENV);
|
||||
}
|
||||
}
|
||||
|
||||
fn zip_entries(path: &Path) -> Vec<String> {
|
||||
let file = File::open(path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(file).unwrap();
|
||||
(0..archive.len())
|
||||
|
||||
Reference in New Issue
Block a user