@@ -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 ) ]
2327pub ( 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\n rm -rf {dest}\n mkdir -p {dest}\n tar -xf {archive} -C {dest}\n rm -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\n rm -rf {destination}\n mkdir -p {destination}\n tar -xf {archive} -C {destination}\n find {destination} -name '._*' -delete\n rm -f {archive}\n " ,
529+ archive = shell:: quote_path( archive) ,
530+ )
531+ }
532+
520533fn 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+
755790pub ( 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 \x07 ATTR" )
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 \x07 ATTR" ) . 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 \x07 ATTR" ) . 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