@@ -122,7 +122,12 @@ pub async fn build_update_from(
122122 state. artifact_paths . workspace_dir = Some ( workspace. workspace_dir . clone ( ) ) ;
123123 state. save ( & paths. state_file ) ?;
124124
125- copy_builder_bundle ( bundle_source, & workspace. bundle_dir ) ?;
125+ let feature_config = crate :: config:: effective_feature_config_path ( config) ;
126+ copy_builder_bundle (
127+ bundle_source,
128+ & workspace. bundle_dir ,
129+ feature_config. as_deref ( ) ,
130+ ) ?;
126131
127132 state. status = UpdateStatus :: PatchingApp ;
128133 state. save ( & paths. state_file ) ?;
@@ -146,7 +151,7 @@ pub async fn build_update_from(
146151 // writes it to a stable per-user path) so the rebuild stages exactly those
147152 // features. Only set it when the file actually exists; an absent path would
148153 // make linux-features.js see an empty enabled set and stage nothing.
149- if let Some ( feature_config) = crate :: config :: effective_feature_config_path ( config ) {
154+ if let Some ( feature_config) = feature_config {
150155 install. env ( "CODEX_LINUX_FEATURES_CONFIG" , & feature_config) ;
151156 }
152157 run_and_log ( & mut install, & workspace. install_log )
@@ -246,7 +251,11 @@ fn package_build_script(bundle_dir: &Path) -> PathBuf {
246251 }
247252}
248253
249- fn copy_builder_bundle ( source_root : & Path , destination_root : & Path ) -> Result < ( ) > {
254+ fn copy_builder_bundle (
255+ source_root : & Path ,
256+ destination_root : & Path ,
257+ feature_config : Option < & Path > ,
258+ ) -> Result < ( ) > {
250259 let manifest_path = source_root. join ( UPDATE_BUILDER_MANIFEST ) ;
251260 if manifest_path. exists ( ) {
252261 return copy_builder_bundle_from_manifest ( source_root, destination_root, & manifest_path) ;
@@ -268,9 +277,34 @@ fn copy_builder_bundle(source_root: &Path, destination_root: &Path) -> Result<()
268277 ) ?;
269278 }
270279
280+ if linux_feature_enabled_in_config ( feature_config, "global-dictation" ) {
281+ copy_entry (
282+ & source_root. join ( "global-dictation-linux" ) ,
283+ & destination_root. join ( "global-dictation-linux" ) ,
284+ false ,
285+ ) ?;
286+ }
287+
271288 Ok ( ( ) )
272289}
273290
291+ fn linux_feature_enabled_in_config ( feature_config : Option < & Path > , feature_id : & str ) -> bool {
292+ let Some ( feature_config) = feature_config else {
293+ return false ;
294+ } ;
295+ let Ok ( contents) = fs:: read_to_string ( feature_config) else {
296+ return false ;
297+ } ;
298+ let Ok ( config) = serde_json:: from_str :: < serde_json:: Value > ( & contents) else {
299+ return false ;
300+ } ;
301+
302+ config
303+ . get ( "enabled" )
304+ . and_then ( serde_json:: Value :: as_array)
305+ . is_some_and ( |enabled| enabled. iter ( ) . any ( |id| id. as_str ( ) == Some ( feature_id) ) )
306+ }
307+
274308fn copy_builder_bundle_from_manifest (
275309 source_root : & Path ,
276310 destination_root : & Path ,
950984 Ok ( ( ) )
951985 }
952986
953- #[ test]
954- fn bundle_copy_skips_missing_optional_package_scripts ( ) -> Result < ( ) > {
955- let temp = tempdir ( ) ?;
956- let source_root = temp. path ( ) . join ( "source" ) ;
957- let destination_root = temp. path ( ) . join ( "destination" ) ;
958-
987+ fn write_fake_fallback_bundle ( source_root : & Path ) -> Result < ( ) > {
959988 fs:: create_dir_all ( source_root. join ( "scripts/lib" ) ) ?;
960989 fs:: create_dir_all ( source_root. join ( "launcher" ) ) ?;
961990 fs:: create_dir_all ( source_root. join ( "packaging/linux" ) ) ?;
@@ -1003,8 +1032,18 @@ fi
10031032 ) ?;
10041033 fs:: write ( source_root. join ( "assets/codex.png" ) , b"png" ) ?;
10051034 fs:: write ( source_root. join ( "assets/codex-linux.png" ) , b"linux png" ) ?;
1035+ Ok ( ( ) )
1036+ }
10061037
1007- copy_builder_bundle ( & source_root, & destination_root) ?;
1038+ #[ test]
1039+ fn bundle_copy_skips_missing_optional_package_scripts ( ) -> Result < ( ) > {
1040+ let temp = tempdir ( ) ?;
1041+ let source_root = temp. path ( ) . join ( "source" ) ;
1042+ let destination_root = temp. path ( ) . join ( "destination" ) ;
1043+
1044+ write_fake_fallback_bundle ( & source_root) ?;
1045+
1046+ copy_builder_bundle ( & source_root, & destination_root, None ) ?;
10081047
10091048 assert ! ( destination_root. join( "scripts/build-deb.sh" ) . exists( ) ) ;
10101049 assert ! ( destination_root
@@ -1041,6 +1080,36 @@ fi
10411080 Ok ( ( ) )
10421081 }
10431082
1083+ #[ test]
1084+ fn fallback_bundle_copies_global_dictation_source_when_enabled ( ) -> Result < ( ) > {
1085+ let temp = tempdir ( ) ?;
1086+ let source_root = temp. path ( ) . join ( "source" ) ;
1087+ let destination_root = temp. path ( ) . join ( "destination" ) ;
1088+ let feature_config = temp. path ( ) . join ( "linux-features.json" ) ;
1089+
1090+ write_fake_fallback_bundle ( & source_root) ?;
1091+ fs:: create_dir_all ( source_root. join ( "global-dictation-linux/src" ) ) ?;
1092+ fs:: write (
1093+ source_root. join ( "global-dictation-linux/Cargo.toml" ) ,
1094+ b"[package]\n name = \" codex-global-dictation-linux\" \n version = \" 0.1.0\" \n edition = \" 2021\" \n " ,
1095+ ) ?;
1096+ fs:: write (
1097+ source_root. join ( "global-dictation-linux/src/main.rs" ) ,
1098+ b"fn main() {}\n " ,
1099+ ) ?;
1100+ fs:: write ( & feature_config, b"{\" enabled\" :[\" global-dictation\" ]}\n " ) ?;
1101+
1102+ copy_builder_bundle ( & source_root, & destination_root, Some ( & feature_config) ) ?;
1103+
1104+ assert ! ( destination_root
1105+ . join( "global-dictation-linux/Cargo.toml" )
1106+ . exists( ) ) ;
1107+ assert ! ( destination_root
1108+ . join( "global-dictation-linux/src/main.rs" )
1109+ . exists( ) ) ;
1110+ Ok ( ( ) )
1111+ }
1112+
10441113 #[ test]
10451114 fn bundle_copy_prefers_packaged_update_builder_manifest ( ) -> Result < ( ) > {
10461115 let temp = tempdir ( ) ?;
10621131 b"# generated\n assets/codex-linux.png\n record-replay-linux/Cargo.toml\n " ,
10631132 ) ?;
10641133
1065- copy_builder_bundle ( & source_root, & destination_root) ?;
1134+ copy_builder_bundle ( & source_root, & destination_root, None ) ?;
10661135
10671136 assert ! ( destination_root. join( "assets/codex-linux.png" ) . exists( ) ) ;
10681137 assert ! ( destination_root
10821151 fs:: create_dir_all ( source_root. join ( ".codex-linux" ) ) ?;
10831152 fs:: write ( source_root. join ( UPDATE_BUILDER_MANIFEST ) , b"../escape\n " ) ?;
10841153
1085- let error = copy_builder_bundle ( & source_root, & destination_root)
1154+ let error = copy_builder_bundle ( & source_root, & destination_root, None )
10861155 . expect_err ( "manifest parent path should be rejected" ) ;
10871156 assert ! ( error
10881157 . to_string( )
10991168 fs:: create_dir_all ( source_root. join ( ".codex-linux" ) ) ?;
11001169 fs:: write ( source_root. join ( UPDATE_BUILDER_MANIFEST ) , b"/tmp/escape\n " ) ?;
11011170
1102- let error = copy_builder_bundle ( & source_root, & destination_root)
1171+ let error = copy_builder_bundle ( & source_root, & destination_root, None )
11031172 . expect_err ( "manifest absolute path should be rejected" ) ;
11041173 assert ! ( error
11051174 . to_string( )
0 commit comments