Skip to content

Commit c6395ec

Browse files
cipolleschifacebook-github-bot
authored andcommitted
add function hard links for headers from ReactCodegen (#53640)
Summary: Pull Request resolved: #53640 ## Context One of the quirk of SwiftPM is that the packages has to have access to the headers they need. Usually this is solved by properly setting the header_search_path. However, in SwiftPM, we are not allowed to use headers search path that escape the package itself (basically, header search path can't start with `../`). To work around this limitation we are recreating the correct Header structure by using hardlinks to the actual headers. ## Changed In this change we are adding an helper function to create hard links to Codegen Headers so that other libraries can access them. ## Changelog: [Internal] - Reviewed By: cortinico Differential Revision: D81778450 fbshipit-source-id: e9f62f1c81ce8a635cc880007f495de342342a25
1 parent 515c735 commit c6395ec

2 files changed

Lines changed: 226 additions & 1 deletion

File tree

packages/react-native/scripts/swiftpm/__tests__/headers-utils-test.js

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'use strict';
1212

1313
const {
14+
symlinkCodegenHeaders,
1415
symlinkHeadersFromPath,
1516
symlinkReactAppleHeaders,
1617
symlinkReactCommonHeaders,
@@ -44,7 +45,8 @@ describe('symlinkHeadersFromPath', () => {
4445
mockPath.dirname.mockImplementation(filePath => {
4546
const parts = filePath.split('/');
4647
parts.pop();
47-
return parts.join('/');
48+
const result = parts.join('/');
49+
return result === '' ? '.' : result;
4850
});
4951
mockPath.basename.mockImplementation(filePath => {
5052
return filePath.split('/').pop();
@@ -295,6 +297,169 @@ describe('symlinkHeadersFromPath', () => {
295297
});
296298
});
297299

300+
describe('symlinkCodegenHeaders', () => {
301+
let mockUtils;
302+
let mockFs;
303+
let mockPath;
304+
let originalConsoleWarn;
305+
let originalConsoleLog;
306+
307+
beforeEach(() => {
308+
// Setup mocks
309+
mockUtils = require('../utils');
310+
mockFs = require('fs');
311+
mockPath = require('path');
312+
313+
// Mock path functions
314+
mockPath.relative.mockImplementation((from, to) => {
315+
return to.replace(from + '/', '');
316+
});
317+
mockPath.join.mockImplementation((...args) => args.join('/'));
318+
mockPath.dirname.mockImplementation(filePath => {
319+
const parts = filePath.split('/');
320+
parts.pop();
321+
const result = parts.join('/');
322+
return result === '' ? '.' : result;
323+
});
324+
mockPath.basename.mockImplementation(filePath => {
325+
return filePath.split('/').pop();
326+
});
327+
328+
// Mock console methods to prevent test output noise
329+
originalConsoleWarn = console.warn;
330+
originalConsoleLog = console.log;
331+
console.warn = jest.fn();
332+
console.log = jest.fn();
333+
334+
// Reset all mocks
335+
jest.clearAllMocks();
336+
});
337+
338+
afterEach(() => {
339+
// Restore console methods
340+
console.warn = originalConsoleWarn;
341+
console.log = originalConsoleLog;
342+
});
343+
344+
it('should create symlinks for codegen headers with conditional directory structure', () => {
345+
// Setup
346+
const reactNativePath = '/path/to/react-native';
347+
const iosAppPath = '/path/to/ios-app';
348+
const outputFolder = '/output/folder';
349+
const reactCodegenPath =
350+
'/path/to/ios-app/build/generated/ios/ReactCodegen';
351+
const headerFiles = [
352+
`${reactCodegenPath}/ComponentDescriptors.h`,
353+
`${reactCodegenPath}/ModuleProvider.h`,
354+
`${reactCodegenPath}/react/renderer/components/MyComponent/ComponentDescriptors.h`,
355+
`${reactCodegenPath}/react/renderer/components/MyComponent/EventEmitter.h`,
356+
];
357+
358+
mockFs.existsSync.mockImplementation(filePath => {
359+
if (filePath === reactCodegenPath) return true;
360+
if (filePath.includes('ReactCodegen') && filePath.endsWith('.h'))
361+
return true;
362+
return false;
363+
});
364+
mockUtils.listHeadersInFolder.mockReturnValue(headerFiles);
365+
mockUtils.setupSymlink.mockImplementation(() => {});
366+
367+
// Execute
368+
const result = symlinkCodegenHeaders(
369+
reactNativePath,
370+
iosAppPath,
371+
outputFolder,
372+
);
373+
374+
// Assert
375+
expect(mockFs.existsSync).toHaveBeenCalledWith(reactCodegenPath);
376+
expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith(
377+
reactCodegenPath,
378+
['headers', 'tests'],
379+
);
380+
// Files with no subpath go to ReactCodegen folder
381+
expect(mockUtils.setupSymlink).toHaveBeenCalledWith(
382+
`${reactCodegenPath}/ComponentDescriptors.h`,
383+
'/output/folder/headers/ReactCodegen/ComponentDescriptors.h',
384+
);
385+
expect(mockUtils.setupSymlink).toHaveBeenCalledWith(
386+
`${reactCodegenPath}/ModuleProvider.h`,
387+
'/output/folder/headers/ReactCodegen/ModuleProvider.h',
388+
);
389+
// Files with subpaths preserve structure under headers/
390+
expect(mockUtils.setupSymlink).toHaveBeenCalledWith(
391+
`${reactCodegenPath}/react/renderer/components/MyComponent/ComponentDescriptors.h`,
392+
'/output/folder/headers/react/renderer/components/MyComponent/ComponentDescriptors.h',
393+
);
394+
expect(mockUtils.setupSymlink).toHaveBeenCalledWith(
395+
`${reactCodegenPath}/react/renderer/components/MyComponent/EventEmitter.h`,
396+
'/output/folder/headers/react/renderer/components/MyComponent/EventEmitter.h',
397+
);
398+
expect(result).toBe(4);
399+
});
400+
401+
it('should warn and return 0 if ReactCodegen path does not exist', () => {
402+
// Setup
403+
const reactNativePath = '/path/to/react-native';
404+
const iosAppPath = '/path/to/ios-app';
405+
const outputFolder = '/output/folder';
406+
const reactCodegenPath =
407+
'/path/to/ios-app/build/generated/ios/ReactCodegen';
408+
409+
mockFs.existsSync.mockImplementation(filePath => {
410+
if (filePath === reactCodegenPath) return false; // ReactCodegen path doesn't exist
411+
return false;
412+
});
413+
414+
// Execute
415+
const result = symlinkCodegenHeaders(
416+
reactNativePath,
417+
iosAppPath,
418+
outputFolder,
419+
);
420+
421+
// Assert
422+
expect(console.warn).toHaveBeenCalledWith(
423+
`ReactCodegen path does not exist: ${reactCodegenPath}`,
424+
);
425+
expect(mockUtils.listHeadersInFolder).not.toHaveBeenCalled();
426+
expect(result).toBe(0);
427+
});
428+
429+
it('should return 0 if no header files exist', () => {
430+
// Setup
431+
const reactNativePath = '/path/to/react-native';
432+
const iosAppPath = '/path/to/ios-app';
433+
const outputFolder = '/output/folder';
434+
const reactCodegenPath =
435+
'/path/to/ios-app/build/generated/ios/ReactCodegen';
436+
437+
mockFs.existsSync.mockImplementation(filePath => {
438+
if (filePath === reactCodegenPath) return true;
439+
return false;
440+
});
441+
mockUtils.listHeadersInFolder.mockReturnValue([]);
442+
443+
// Execute
444+
const result = symlinkCodegenHeaders(
445+
reactNativePath,
446+
iosAppPath,
447+
outputFolder,
448+
);
449+
450+
// Assert
451+
expect(mockUtils.listHeadersInFolder).toHaveBeenCalledWith(
452+
reactCodegenPath,
453+
['headers', 'tests'],
454+
);
455+
expect(mockUtils.setupSymlink).not.toHaveBeenCalled();
456+
expect(console.log).toHaveBeenCalledWith(
457+
'Created symlinks for 0 Codegen headers with conditional directory structure',
458+
);
459+
expect(result).toBe(0);
460+
});
461+
});
462+
298463
describe('symlinkThirdPartyDependenciesHeaders', () => {
299464
let mockUtils;
300465
let mockFs;

packages/react-native/scripts/swiftpm/headers-utils.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,69 @@ function symlinkThirdPartyDependenciesHeaders(
214214
return linkedCount;
215215
}
216216

217+
/**
218+
* Create symlinks for Codegen headers in the output folder
219+
*/
220+
function symlinkCodegenHeaders(
221+
reactNativePath /*: string */,
222+
iosAppPath /*: string */,
223+
outputFolder /*: string */,
224+
) /*: number */ {
225+
console.log('Creating symlinks for Codegen headers...');
226+
227+
// Look for ReactCodegen folder specifically
228+
const reactCodegenPath = path.join(
229+
iosAppPath,
230+
'build',
231+
'generated',
232+
'ios',
233+
'ReactCodegen',
234+
);
235+
236+
if (!fs.existsSync(reactCodegenPath)) {
237+
console.warn(`ReactCodegen path does not exist: ${reactCodegenPath}`);
238+
return 0;
239+
}
240+
241+
const headersOutput = path.join(outputFolder, 'headers');
242+
const reactCodegenHeadersOutput = path.join(headersOutput, 'ReactCodegen');
243+
let linkedCount = 0;
244+
245+
// No custom mappings needed for codegen headers
246+
const headerFiles = listHeadersInFolder(reactCodegenPath, [
247+
'headers',
248+
'tests',
249+
]);
250+
headerFiles.forEach(sourcePath => {
251+
if (fs.existsSync(sourcePath)) {
252+
// Calculate relative path from ReactCodegen base
253+
const relativePath = path.relative(reactCodegenPath, sourcePath);
254+
255+
let destPath /*: string */ = '';
256+
257+
// If relative path contains no subpath (just a filename), put it in ReactCodegen folder
258+
if (path.dirname(relativePath) === '.') {
259+
destPath = path.join(reactCodegenHeadersOutput, relativePath);
260+
} else {
261+
// Otherwise, preserve the structure under headers/
262+
destPath = path.join(headersOutput, relativePath);
263+
}
264+
265+
setupSymlink(sourcePath, destPath);
266+
linkedCount++;
267+
}
268+
});
269+
270+
console.log(
271+
`Created symlinks for ${linkedCount} Codegen headers with conditional directory structure`,
272+
);
273+
return linkedCount;
274+
}
275+
217276
module.exports = {
218277
symlinkHeadersFromPath,
219278
symlinkReactAppleHeaders,
220279
symlinkReactCommonHeaders,
221280
symlinkThirdPartyDependenciesHeaders,
281+
symlinkCodegenHeaders,
222282
};

0 commit comments

Comments
 (0)