Skip to content

Commit b0c7754

Browse files
authored
fix(lab): strip AppleDouble extension metadata (#10555)
1 parent c100743 commit b0c7754

1 file changed

Lines changed: 141 additions & 12 deletions

File tree

crates/homeboy-lab-runner/src/extension_materialization.rs

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ use super::{
1919
copy_snapshot_to_directory, exec, Runner, RunnerExecOptions, RunnerFileTransfer, RunnerKind,
2020
};
2121

22+
fn extension_snapshot_excludes() -> Vec<String> {
23+
vec!["._*".to_string(), "**/._*".to_string()]
24+
}
25+
2226
#[derive(Debug, Clone, Serialize)]
2327
pub(crate) struct RunnerExtensionMaterializationRequest {
2428
pub(crate) id: String,
@@ -443,12 +447,13 @@ fn sync_controller_snapshot(
443447
plan: &RunnerExtensionMaterializationPlan,
444448
) -> Result<()> {
445449
let synced_source_path = plan.synced_source_path.trim_end_matches('/');
450+
let excludes = extension_snapshot_excludes();
446451
match runner.kind {
447-
RunnerKind::Local => copy_snapshot_to_directory(
448-
Path::new(&plan.source_path),
449-
Path::new(synced_source_path),
450-
&[],
451-
),
452+
RunnerKind::Local => {
453+
let destination = Path::new(synced_source_path);
454+
copy_snapshot_to_directory(Path::new(&plan.source_path), destination, &excludes)?;
455+
remove_appledouble_files(destination)
456+
}
452457
RunnerKind::Ssh => {
453458
let transfer = RunnerFileTransfer::for_runner(runner, None)?;
454459
upload_snapshot(runner, plan, &transfer)
@@ -465,10 +470,14 @@ fn upload_snapshot(
465470
Error::internal_io(err.to_string(), Some("stage extension overlay".to_string()))
466471
})?;
467472
let staged = tempdir.path().join("source");
468-
copy_snapshot_to_directory(Path::new(&plan.source_path), &staged, &[])?;
473+
let excludes = extension_snapshot_excludes();
474+
copy_snapshot_to_directory(Path::new(&plan.source_path), &staged, &excludes)?;
475+
remove_appledouble_files(&staged)?;
469476
let archive = tempdir.path().join("source.tar");
470477
let status = Command::new("tar")
478+
.env("COPYFILE_DISABLE", "1")
471479
.args([
480+
"--no-xattrs",
472481
"-C",
473482
&staged.display().to_string(),
474483
"-cf",
@@ -497,11 +506,7 @@ fn upload_snapshot(
497506
})?;
498507
transfer.ensure_directory(remote_parent)?;
499508
transfer.upload_file(&archive.display().to_string(), &remote_archive)?;
500-
let extract = format!(
501-
"set -e\nrm -rf {dest}\nmkdir -p {dest}\ntar -xf {archive} -C {dest}\nrm -f {archive}\n",
502-
dest = shell::quote_path(&plan.synced_source_path),
503-
archive = shell::quote_path(&remote_archive),
504-
);
509+
let extract = extension_snapshot_extract_command(&plan.synced_source_path, &remote_archive);
505510
let (_output, exit_code) = exec(&runner.id, RunnerExecOptions::diagnostic_raw_shell(extract))?;
506511
if exit_code != 0 {
507512
return Err(Error::validation_invalid_argument(
@@ -517,6 +522,14 @@ fn upload_snapshot(
517522
Ok(())
518523
}
519524

525+
fn extension_snapshot_extract_command(destination: &str, archive: &str) -> String {
526+
let destination = shell::quote_path(destination);
527+
format!(
528+
"set -e\nrm -rf {destination}\nmkdir -p {destination}\ntar -xf {archive} -C {destination}\nfind {destination} -name '._*' -delete\nrm -f {archive}\n",
529+
archive = shell::quote_path(archive),
530+
)
531+
}
532+
520533
fn run_materialization_command(
521534
runner: &Runner,
522535
env: Option<&HashMap<String, String>>,
@@ -733,7 +746,9 @@ fn collect_hash_entries(
733746
children.sort_by_key(|entry| entry.path());
734747
for entry in children {
735748
let path = entry.path();
736-
if entry.file_name().to_string_lossy() == ".git" {
749+
let file_name = entry.file_name();
750+
let file_name = file_name.to_string_lossy();
751+
if file_name == ".git" || file_name.starts_with("._") {
737752
continue;
738753
}
739754
let metadata = fs::symlink_metadata(&path)
@@ -752,6 +767,26 @@ fn collect_hash_entries(
752767
Ok(())
753768
}
754769

770+
fn remove_appledouble_files(path: &Path) -> Result<()> {
771+
let children = fs::read_dir(path)
772+
.map_err(|err| Error::internal_io(err.to_string(), Some(path.display().to_string())))?
773+
.collect::<std::result::Result<Vec<_>, _>>()
774+
.map_err(|err| Error::internal_io(err.to_string(), Some(path.display().to_string())))?;
775+
for entry in children {
776+
let path = entry.path();
777+
let metadata = fs::symlink_metadata(&path)
778+
.map_err(|err| Error::internal_io(err.to_string(), Some(path.display().to_string())))?;
779+
if metadata.is_dir() {
780+
remove_appledouble_files(&path)?;
781+
} else if entry.file_name().to_string_lossy().starts_with("._") {
782+
fs::remove_file(&path).map_err(|err| {
783+
Error::internal_io(err.to_string(), Some(path.display().to_string()))
784+
})?;
785+
}
786+
}
787+
Ok(())
788+
}
789+
755790
pub(crate) fn dev_extension_lifecycle(
756791
runner_id: &str,
757792
remote_path: &str,
@@ -1059,6 +1094,100 @@ mod tests {
10591094
)));
10601095
}
10611096

1097+
#[test]
1098+
fn controller_snapshot_ignores_appledouble_metadata() {
1099+
let workspace = tempfile::tempdir().expect("runner workspace");
1100+
let mut runner = runner();
1101+
runner.workspace_root = Some(workspace.path().to_string_lossy().to_string());
1102+
let source = tempfile::tempdir().expect("extension source");
1103+
let rules = source
1104+
.path()
1105+
.join("node_modules/eslint-plugin-jest/lib/rules");
1106+
fs::create_dir_all(&rules).expect("rules directory");
1107+
fs::write(source.path().join("nodejs.json"), r#"{"id":"nodejs"}"#).expect("manifest");
1108+
fs::write(rules.join("consistent-test-it.js"), "module.exports = {};")
1109+
.expect("JavaScript rule");
1110+
let hash_without_metadata =
1111+
extension_source_content_hash(source.path()).expect("source hash");
1112+
fs::write(rules.join("._consistent-test-it.js"), b"\0\x05\x16\x07ATTR")
1113+
.expect("AppleDouble metadata");
1114+
let hash_with_metadata = extension_source_content_hash(source.path()).expect("source hash");
1115+
1116+
assert_eq!(hash_with_metadata, hash_without_metadata);
1117+
1118+
let request = RunnerExtensionMaterializationRequest {
1119+
id: "nodejs".to_string(),
1120+
revision: "abc123".to_string(),
1121+
source: RunnerExtensionMaterializationSource::ControllerSnapshot {
1122+
local_path: source.path().to_path_buf(),
1123+
},
1124+
};
1125+
let provenance = materialize_runner_extension_with_exec(
1126+
&runner,
1127+
"homeboy",
1128+
None,
1129+
&request,
1130+
&mut |_runner_id, _options| Ok((output(), 0)),
1131+
)
1132+
.expect("materializes");
1133+
let synced_rules = Path::new(&provenance.synced_source_path)
1134+
.join("node_modules/eslint-plugin-jest/lib/rules");
1135+
1136+
assert!(synced_rules.join("consistent-test-it.js").exists());
1137+
assert!(!synced_rules.join("._consistent-test-it.js").exists());
1138+
}
1139+
1140+
#[test]
1141+
fn staged_extension_cleanup_removes_nested_appledouble_metadata() {
1142+
let staged = tempfile::tempdir().expect("staged extension");
1143+
let rules = staged.path().join("node_modules/plugin/rules");
1144+
fs::create_dir_all(&rules).expect("rules directory");
1145+
fs::write(rules.join("rule.js"), "module.exports = {};").expect("JavaScript rule");
1146+
fs::write(rules.join("._rule.js"), b"\0\x05\x16\x07ATTR").expect("AppleDouble metadata");
1147+
1148+
remove_appledouble_files(staged.path()).expect("clean staged extension");
1149+
1150+
assert!(rules.join("rule.js").exists());
1151+
assert!(!rules.join("._rule.js").exists());
1152+
}
1153+
1154+
#[test]
1155+
fn runner_extraction_removes_appledouble_metadata() {
1156+
let fixture = tempfile::tempdir().expect("extension fixture");
1157+
let source = fixture.path().join("source");
1158+
let destination = fixture.path().join("destination");
1159+
let archive = fixture.path().join("source.tar");
1160+
fs::create_dir_all(&source).expect("source directory");
1161+
fs::write(source.join("rule.js"), "module.exports = {};").expect("JavaScript rule");
1162+
fs::write(source.join("._rule.js"), b"\0\x05\x16\x07ATTR").expect("AppleDouble metadata");
1163+
let archive_status = Command::new("tar")
1164+
.args([
1165+
"-C",
1166+
&source.display().to_string(),
1167+
"-cf",
1168+
&archive.display().to_string(),
1169+
".",
1170+
])
1171+
.status()
1172+
.expect("archive fixture");
1173+
assert!(archive_status.success());
1174+
1175+
let extract_status = Command::new("sh")
1176+
.args([
1177+
"-c",
1178+
&extension_snapshot_extract_command(
1179+
&destination.display().to_string(),
1180+
&archive.display().to_string(),
1181+
),
1182+
])
1183+
.status()
1184+
.expect("extract fixture");
1185+
1186+
assert!(extract_status.success());
1187+
assert!(destination.join("rule.js").exists());
1188+
assert!(!destination.join("._rule.js").exists());
1189+
}
1190+
10621191
#[test]
10631192
fn controller_snapshot_plan_uses_content_hash_slot() {
10641193
let dir = tempfile::tempdir().expect("extension source");

0 commit comments

Comments
 (0)