fix(release): 保留空目录到 ZIP 产物

移除 count_files 和 dir_has_included_files 两个预检函数,不再在打包时
过滤空目录。所有遍历到的子目录(包括空目录和仅含 .gitkeep 的目录)
现在都会写入 ZIP 条目,确保项目目录结构完整保留。

新增三个测试覆盖:空子目录、仅 .gitkeep 目录、emod-package 匹配的
空目录。
This commit is contained in:
2026-05-10 02:08:01 +08:00
parent 02e72fc9d8
commit 55e92c4b4f

View File

@@ -214,10 +214,6 @@ fn add_directory_to_zip(
))); )));
} }
if count_files(src_dir, project_dir, ignore)? == 0 {
return Ok(());
}
let options = SimpleFileOptions::default(); let options = SimpleFileOptions::default();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
@@ -243,9 +239,7 @@ fn add_directory_to_zip(
f.read_to_end(&mut buffer)?; f.read_to_end(&mut buffer)?;
zip.write_all(&buffer)?; zip.write_all(&buffer)?;
buffer.clear(); buffer.clear();
} else if !relative_path.as_os_str().is_empty() } else if !relative_path.as_os_str().is_empty() {
&& count_files(path, project_dir, ignore)? > 0
{
zip.add_directory(&path_str, options)?; zip.add_directory(&path_str, options)?;
} }
} }
@@ -329,7 +323,7 @@ fn add_with_package(
if !is_dir { if !is_dir {
continue; continue;
} }
// 当前目录未命中且无被包含的子孙时,无需写出目录条目 // 当前目录未命中不写出目录条目WalkDir 仍会继续遍历子孙
continue; continue;
} }
@@ -343,9 +337,7 @@ fn add_with_package(
.map_err(|e| file::io_error("读取打包文件", path, e))?; .map_err(|e| file::io_error("读取打包文件", path, e))?;
zip.write_all(&buffer)?; zip.write_all(&buffer)?;
buffer.clear(); buffer.clear();
} else if !relative_path.as_os_str().is_empty() } else if !relative_path.as_os_str().is_empty() {
&& dir_has_included_files(path, project_dir, package, ignore)?
{
zip.add_directory(&path_str, options)?; zip.add_directory(&path_str, options)?;
} }
} }
@@ -353,57 +345,6 @@ fn add_with_package(
Ok(()) Ok(())
} }
fn dir_has_included_files(
dir: &Path,
project_dir: &Path,
package: &EmodPackage,
ignore: &EmodIgnore,
) -> Result<bool> {
for entry in walkdir::WalkDir::new(dir) {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.display().to_string().ends_with(".gitkeep") {
continue;
}
let relative_path = path
.strip_prefix(project_dir)
.map_err(|e| crate::error::CliError::InvalidData(e.to_string()))?;
if ignore.is_ignored(relative_path, false) {
continue;
}
if !package.matches(relative_path, false) {
continue;
}
return Ok(true);
}
Ok(false)
}
fn count_files(dir: &Path, project_dir: &Path, ignore: &EmodIgnore) -> Result<usize> {
let mut count = 0;
for entry in walkdir::WalkDir::new(dir) {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let relative_path = path
.strip_prefix(project_dir)
.map_err(|e| crate::error::CliError::InvalidData(e.to_string()))?;
if !path.display().to_string().ends_with(".gitkeep")
&& !ignore.is_ignored(relative_path, false)
{
count += 1;
}
}
Ok(count)
}
struct EmodIgnore { struct EmodIgnore {
rules: Vec<IgnoreRule>, rules: Vec<IgnoreRule>,
} }
@@ -803,6 +744,68 @@ mod tests {
fs::remove_dir_all(project_dir).unwrap(); fs::remove_dir_all(project_dir).unwrap();
} }
#[test]
fn package_project_keeps_empty_subdirectory() {
let project_dir = temp_project_dir();
let release_info = release_info();
fs::create_dir_all(project_dir.join("behavior_pack_abcdefgh/empty_dir")).unwrap();
fs::create_dir_all(project_dir.join("resource_pack_ijklmnop")).unwrap();
let pack_dirs = pack_dirs(&project_dir, &release_info);
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/empty_dir/".to_string()));
fs::remove_dir_all(project_dir).unwrap();
}
#[test]
fn package_project_keeps_dir_with_only_gitkeep() {
let project_dir = temp_project_dir();
let release_info = release_info();
write_file(
&project_dir.join("behavior_pack_abcdefgh/only_gitkeep/.gitkeep"),
"",
);
fs::create_dir_all(project_dir.join("resource_pack_ijklmnop")).unwrap();
let pack_dirs = pack_dirs(&project_dir, &release_info);
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/only_gitkeep/".to_string()));
assert!(!entries.contains(&"behavior_pack_abcdefgh/only_gitkeep/.gitkeep".to_string()));
fs::remove_dir_all(project_dir).unwrap();
}
#[test]
fn emod_package_keeps_matched_empty_subdirectory() {
let project_dir = temp_project_dir();
let release_info = release_info();
write_file(
&project_dir.join(".emod-package"),
"behavior_pack_abcdefgh/empty_dir/\n",
);
fs::create_dir_all(project_dir.join("behavior_pack_abcdefgh/empty_dir")).unwrap();
fs::create_dir_all(project_dir.join("resource_pack_ijklmnop")).unwrap();
let pack_dirs = pack_dirs(&project_dir, &release_info);
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/empty_dir/".to_string()));
fs::remove_dir_all(project_dir).unwrap();
}
#[test] #[test]
fn package_project_includes_world_pack_json_in_default_mode() { fn package_project_includes_world_pack_json_in_default_mode() {
let project_dir = temp_project_dir(); let project_dir = temp_project_dir();