@@ -21,17 +21,21 @@ const swiftBuildDir = path.join(buildDir, "swiftpm");
2121const localHelperPath = path . join ( buildDir , helperName ) ;
2222const localCursorHelperPath = path . join ( buildDir , cursorHelperName ) ;
2323
24- // Build a universal (arm64 + x86_64) binary by default so both the arm64 and x64 DMGs ship a helper
25- // that runs natively. Override with OPENSCREEN_MAC_HELPER_ARCHS (comma-separated) for a faster
26- // single-arch local build.
27- const archs = ( process . env . OPENSCREEN_MAC_HELPER_ARCHS ?? "arm64,x86_64" )
24+ // Build a separate single-arch binary per requested arch and place each in its own
25+ // electron/native/bin/darwin-<arch> folder (the runtime resolves that folder by the running app's
26+ // arch). No universal/fat binary. Defaults to the host arch for local builds; CI sets
27+ // OPENSCREEN_MAC_HELPER_ARCHS per matrix entry (accepts arm64, x64, or x86_64).
28+ function normalizeArch ( value ) {
29+ return value === "x64" || value === "x86_64"
30+ ? { swift : "x86_64" , tag : "darwin-x64" }
31+ : { swift : "arm64" , tag : "darwin-arm64" } ;
32+ }
33+ const hostArch = process . arch === "arm64" ? "arm64" : "x86_64" ;
34+ const archs = ( process . env . OPENSCREEN_MAC_HELPER_ARCHS ?? hostArch )
2835 . split ( "," )
2936 . map ( ( a ) => a . trim ( ) )
30- . filter ( Boolean ) ;
31- const archToTag = ( arch ) => ( arch === "x86_64" || arch === "x64" ? "darwin-x64" : "darwin-arm64" ) ;
32- // A universal binary runs on both arches, so when building both, place it in each dir the runtime
33- // might read (it resolves electron/native/bin/<darwin-arm64|darwin-x64> by the running app's arch).
34- const targetTags = archs . length > 1 ? [ "darwin-arm64" , "darwin-x64" ] : [ archToTag ( archs [ 0 ] ) ] ;
37+ . filter ( Boolean )
38+ . map ( normalizeArch ) ;
3539
3640const xcodebuildVersion = spawnSync ( "xcodebuild" , [ "-version" ] , {
3741 cwd : root ,
@@ -56,8 +60,13 @@ if (xcodebuildVersion.status !== 0) {
5660 process . exit ( 1 ) ;
5761}
5862
59- // Locate a built binary by name under a build dir (SwiftPM's exact output path varies by version).
60- function findArtifact ( dir , name ) {
63+ // SwiftPM writes a single-arch release build to <buildPath>/<swiftArch>-apple-macosx/release/<name>.
64+ // Fall back to a search that skips the identically-named file inside the .dSYM debug bundle (matching
65+ // that file and feeding it forward is what produced an unrunnable "exec format error" binary before).
66+ function findExecutable ( dir , swiftArch , name ) {
67+ const expected = path . join ( dir , `${ swiftArch } -apple-macosx` , "release" , name ) ;
68+ if ( fs . existsSync ( expected ) ) return expected ;
69+
6170 const stack = [ dir ] ;
6271 const matches = [ ] ;
6372 while ( stack . length > 0 ) {
@@ -69,35 +78,29 @@ function findArtifact(dir, name) {
6978 continue ;
7079 }
7180 for ( const entry of entries ) {
81+ if ( entry . name . endsWith ( ".dSYM" ) ) continue ;
7282 const full = path . join ( current , entry . name ) ;
7383 if ( entry . isDirectory ( ) ) stack . push ( full ) ;
74- else if ( entry . isFile ( ) && entry . name === name ) matches . push ( full ) ;
84+ else if ( entry . isFile ( ) && entry . name === name && / [ / \\ ] r e l e a s e [ / \\ ] / i. test ( full ) ) {
85+ matches . push ( full ) ;
86+ }
7587 }
7688 }
77- matches . sort ( ( a , b ) => {
78- const ra = / r e l e a s e / i. test ( a ) ? 0 : 1 ;
79- const rb = / r e l e a s e / i. test ( b ) ? 0 : 1 ;
80- if ( ra !== rb ) return ra - rb ;
81- return fs . statSync ( b ) . mtimeMs - fs . statSync ( a ) . mtimeMs ;
82- } ) ;
8389 return matches [ 0 ] ?? null ;
8490}
8591
86- // Build each arch in its own SwiftPM invocation. A single --arch uses SwiftPM's lenient build path
87- // that tolerates the helper's `@main` inside a main.swift file; combining arches in one invocation
88- // (--arch a --arch b) switches to the stricter "apple" build system that rejects it. We lipo the
89- // slices together afterwards.
90- const slicesByName = { [ helperName ] : [ ] , [ cursorHelperName ] : [ ] } ;
91- for ( const arch of archs ) {
92- const archBuildDir = path . join ( swiftBuildDir , arch ) ;
92+ fs . mkdirSync ( buildDir , { recursive : true } ) ;
93+
94+ for ( const { swift, tag } of archs ) {
95+ const archBuildDir = path . join ( swiftBuildDir , swift ) ;
9396 const result = spawnSync (
9497 "swift" ,
9598 [
9699 "build" ,
97100 "-c" ,
98101 "release" ,
99102 "--arch" ,
100- arch ,
103+ swift ,
101104 "--package-path" ,
102105 packageDir ,
103106 "--build-path" ,
@@ -109,48 +112,33 @@ for (const arch of archs) {
109112 } ,
110113 ) ;
111114 if ( result . error ) {
112- console . error ( `Failed to start Swift build (${ arch } ): ${ result . error . message } ` ) ;
115+ console . error ( `Failed to start Swift build (${ swift } ): ${ result . error . message } ` ) ;
113116 process . exit ( 1 ) ;
114117 }
115118 if ( result . status !== 0 ) {
116119 process . exit ( result . status ?? 1 ) ;
117120 }
118- for ( const name of [ helperName , cursorHelperName ] ) {
119- const artifact = findArtifact ( archBuildDir , name ) ;
120- if ( ! artifact ) {
121- console . error ( `Swift build (${ arch } ) completed but artifact was not found: ${ name } ` ) ;
122- process . exit ( 1 ) ;
123- }
124- slicesByName [ name ] . push ( artifact ) ;
125- }
126- }
127121
128- fs . mkdirSync ( buildDir , { recursive : true } ) ;
129- const targetDirs = targetTags . map ( ( tag ) => path . join ( root , "electron" , "native" , "bin" , tag ) ) ;
130- for ( const dir of targetDirs ) fs . mkdirSync ( dir , { recursive : true } ) ;
122+ const targetDir = path . join ( root , "electron" , "native" , "bin" , tag ) ;
123+ fs . mkdirSync ( targetDir , { recursive : true } ) ;
131124
132- for ( const [ name , localPath ] of [
133- [ helperName , localHelperPath ] ,
134- [ cursorHelperName , localCursorHelperPath ] ,
135- ] ) {
136- const slices = slicesByName [ name ] ;
137- let source = slices [ 0 ] ;
138- // Stitch the per-arch slices into one universal (fat) binary so the same file runs on both arches.
139- if ( slices . length > 1 ) {
140- const universal = path . join ( buildDir , ` ${ name } .universal` ) ;
141- const lipo = spawnSync ( "lipo" , [ "-create" , ... slices , "-output" , universal ] , {
142- cwd : root ,
143- stdio : "inherit" ,
144- } ) ;
145- if ( lipo . status !== 0 ) {
146- console . error ( `lipo failed to combine ${ name } ( ${ archs . join ( ", " ) } )` ) ;
147- process . exit ( lipo . status ?? 1 ) ;
125+ for ( const [ name , localPath ] of [
126+ [ helperName , localHelperPath ] ,
127+ [ cursorHelperName , localCursorHelperPath ] ,
128+ ] ) {
129+ const exe = findExecutable ( archBuildDir , swift , name ) ;
130+ if ( ! exe ) {
131+ console . error ( `Swift build ( ${ swift } ) completed but executable was not found: ${ name } ` ) ;
132+ process . exit ( 1 ) ;
133+ }
134+ // Always place it in the arch's bin folder; mirror the host-arch build into the dev build
135+ // dir so `npm run dev` (candidate path #2) can spawn it.
136+ const dests = [ path . join ( targetDir , name ) ] ;
137+ if ( swift === hostArch ) dests . push ( localPath ) ;
138+ for ( const dest of dests ) {
139+ fs . copyFileSync ( exe , dest ) ;
140+ fs . chmodSync ( dest , 0o755 ) ;
148141 }
149- source = universal ;
150- }
151- for ( const dest of [ localPath , ...targetDirs . map ( ( dir ) => path . join ( dir , name ) ) ] ) {
152- fs . copyFileSync ( source , dest ) ;
153- fs . chmodSync ( dest , 0o755 ) ;
154142 }
155- console . log ( `Built ${ name } (${ archs . join ( ", " ) } ) → ${ targetTags . join ( ", " ) } ` ) ;
143+ console . log ( `Built ${ tag } helpers (${ swift } ) ` ) ;
156144}
0 commit comments