Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-parrots-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/brownfield-cli': patch
---

respect `--destination` in `package:ios` when merging XCFrameworks
10 changes: 5 additions & 5 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@
"@expo/config": "^12.0.13",
"@react-native-community/cli-config": "^20.0.0",
"@react-native-community/cli-config-android": "^20.0.0",
"@rock-js/platform-android": "^0.14.0",
"@rock-js/platform-apple-helpers": "^0.14.0",
"@rock-js/plugin-brownfield-android": "^0.14.0",
"@rock-js/plugin-brownfield-ios": "^0.14.0",
"@rock-js/tools": "^0.14.0",
"@rock-js/platform-android": "^0.15.4",
"@rock-js/platform-apple-helpers": "^0.15.4",
"@rock-js/plugin-brownfield-android": "^0.15.4",
"@rock-js/plugin-brownfield-ios": "^0.15.4",
"@rock-js/tools": "^0.15.4",
"ajv": "^8.20.0",
"commander": "^14.0.3",
"quicktype-core": "^23.2.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ describe('package:ios action --add-spm-package', () => {
productsPath: '/repo/ios/.brownfield/build/Build/Products',
configuration: 'Release',
frameworkName: 'BrownfieldLib',
sdks: ['iphoneos', 'iphonesimulator'],
});
expect(mockCreateLocalSpmPackage).toHaveBeenCalledWith({
packageDir: '/repo/ios/.brownfield/package/build',
Expand All @@ -222,11 +223,14 @@ describe('package:ios action --add-spm-package', () => {
'Release',
]);

expect(mockEmitExpoSupportXcframeworks).toHaveBeenCalledWith({
projectRoot: '/repo',
packageDir: '/repo/ios/.brownfield/package/build',
usePrebuiltExpo: false,
});
expect(mockEmitExpoSupportXcframeworks).toHaveBeenCalledWith(
expect.objectContaining({
projectRoot: '/repo',
packageDir: '/repo/ios/.brownfield/package/build',
usePrebuiltExpo: false,
usePrebuiltExpoExplicit: true,
})
);
expect(mockCreateLocalSpmPackage).toHaveBeenCalledWith({
packageDir: '/repo/ios/.brownfield/package/build',
frameworkName: 'BrownfieldLib',
Expand Down Expand Up @@ -276,6 +280,37 @@ describe('package:ios action --add-spm-package', () => {
expect(processExitMock).toHaveBeenCalledWith(1);
});

test('marks an explicit --use-prebuilt-expo true as explicit', async () => {
await invokePackageIosAction([
'--use-prebuilt-expo',
'true',
'--configuration',
'Release',
]);

expect(mockEmitExpoSupportXcframeworks).toHaveBeenCalledWith(
expect.objectContaining({
usePrebuiltExpo: true,
usePrebuiltExpoExplicit: true,
})
);
});

test('passes SPM generation the degraded value when Expo prebuilts are missing', async () => {
mockEmitExpoSupportXcframeworks.mockImplementation(
({ onDegradeToSource }: { onDegradeToSource?: () => void }) => {
onDegradeToSource?.();
return true;
}
);

await invokePackageIosAction(['--add-spm-package', '--configuration', 'Release']);

expect(mockCreateLocalSpmPackage).toHaveBeenCalledWith(
expect.objectContaining({ usePrebuiltExpo: false })
);
});

test('runs Expo framework emission before local SPM package creation for Expo SDK 56+', async () => {
mockEmitExpoSupportXcframeworks.mockReturnValue(true);

Expand All @@ -285,11 +320,15 @@ describe('package:ios action --add-spm-package', () => {
'Release',
]);

expect(mockEmitExpoSupportXcframeworks).toHaveBeenCalledWith({
projectRoot: '/repo',
packageDir: '/repo/ios/.brownfield/package/build',
usePrebuiltExpo: true,
});
// omitted flag => value is version-inferred, not explicit
expect(mockEmitExpoSupportXcframeworks).toHaveBeenCalledWith(
expect.objectContaining({
projectRoot: '/repo',
packageDir: '/repo/ios/.brownfield/package/build',
usePrebuiltExpo: true,
usePrebuiltExpoExplicit: false,
})
);
expect(
mockEmitExpoSupportXcframeworks.mock.invocationCallOrder[0]
).toBeLessThan(mockCreateLocalSpmPackage.mock.invocationCallOrder[0] ?? 0);
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/brownfield/commands/__tests__/packageIos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as rockTools from '@rock-js/tools';
import { describe, expect, test } from 'vitest';

import {
isUsePrebuiltExpoExplicit,
packageIosCommand,
parseUsePrebuiltExpoArgument,
parseUsePrebuiltRnCoreArgument,
Expand Down Expand Up @@ -120,3 +121,14 @@ describe('--add-spm-package (Commander)', () => {
);
});
});

describe('isUsePrebuiltExpoExplicit', () => {
test('is false when the value is omitted (version-inferred later)', () => {
expect(isUsePrebuiltExpoExplicit(undefined)).toBe(false);
});

test('is true for explicit true and false (CLI or brownfield.config.json)', () => {
expect(isUsePrebuiltExpoExplicit(true)).toBe(true);
expect(isUsePrebuiltExpoExplicit(false)).toBe(true);
});
});
90 changes: 50 additions & 40 deletions packages/cli/src/brownfield/commands/packageIos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
import { runBrownieCodegenIfApplicable } from '../../brownie/helpers/runBrownieCodegenIfApplicable.js';
import { runNavigationCodegenIfApplicable } from '../../navigation/helpers/runNavigationCodegenIfApplicable.js';
import { copyDebugBundleToSimulatorSlice } from '../utils/copyDebugBundleToSimulatorSlice.js';
import {
collectFrameworkPaths,
resolveDestinationSdks,
} from '../utils/destinationSdks.js';
import { resolvePackagedFrameworkName } from '../utils/resolvePackagedFrameworkName.js';
import { stripFrameworkBinary } from '../utils/stripFrameworkBinary.js';
import type { PackageIosOptions } from '../../types.js';
Expand Down Expand Up @@ -125,6 +129,11 @@ export const packageIosCommand = curryOptions(
.action(
actionRunner(async (cliOptions: PackageIosOptions) => {
const options = mergeBrownfieldConfigWithOptions(cliOptions, 'ios');
// captured before the version-aware default below, so that a value from
// brownfield.config.json counts as explicit
const usePrebuiltExpoExplicit = isUsePrebuiltExpoExplicit(
options.usePrebuiltExpo
);
const projectRoot = findProjectRoot();

await runExpoPrebuildIfNeeded({ projectRoot, platform: 'ios' });
Expand Down Expand Up @@ -197,6 +206,9 @@ export const packageIosCommand = curryOptions(
// Reference: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/scripts/cocoapods/new_architecture.rb#L171
const packageDir = path.join(dotBrownfieldDir, 'package', 'build');
const configuration = options.configuration ?? 'Debug';
// `--destination` narrows which slices Xcode emits, so every path below has to
// look at those slices only instead of assuming a device + simulator pair
const sdks = resolveDestinationSdks(options.destination);

const { hasBrownie } = await runBrownieCodegenIfApplicable(
projectRoot,
Expand Down Expand Up @@ -225,6 +237,11 @@ export const packageIosCommand = curryOptions(
projectRoot,
packageDir,
usePrebuiltExpo: options.usePrebuiltExpo,
usePrebuiltExpoExplicit,
// keep the generated SPM manifest consistent with the emitted XCFrameworks
onDegradeToSource: () => {
options.usePrebuiltExpo = false;
},
});

const productsPath = path.join(options.buildFolder, 'Build', 'Products');
Expand All @@ -233,6 +250,7 @@ export const packageIosCommand = curryOptions(
explicitScheme: options.scheme,
productsPath,
configuration,
sdks,
});

if (!frameworkName && options.addSpmPackage) {
Expand All @@ -251,24 +269,19 @@ export const packageIosCommand = curryOptions(
productsPath,
configuration,
frameworkName,
sdks,
});

if (configuration.includes('Debug')) {
// Re-merge only Debug frameworks so the simulator slice includes main.jsbundle.
await mergeFrameworks({
sourceDir: userConfig.project.ios.sourceDir,
frameworkPaths: [
path.join(
productsPath,
`${configuration}-iphoneos`,
`${frameworkName}.framework`
),
path.join(
productsPath,
`${configuration}-iphonesimulator`,
`${frameworkName}.framework`
),
],
frameworkPaths: collectFrameworkPaths({
productsPath,
configuration,
sdks,
frameworkName,
}),
outputPath: path.join(packageDir, `${frameworkName}.xcframework`),
});
}
Expand Down Expand Up @@ -300,20 +313,13 @@ export const packageIosCommand = curryOptions(

await mergeFrameworks({
sourceDir: userConfig.project.ios.sourceDir,
frameworkPaths: [
path.join(
productsPath,
`${configuration}-iphoneos`,
'Brownie',
'Brownie.framework'
),
path.join(
productsPath,
`${configuration}-iphonesimulator`,
'Brownie',
'Brownie.framework'
),
],
frameworkPaths: collectFrameworkPaths({
productsPath,
configuration,
sdks,
frameworkName: 'Brownie',
productSubDir: 'Brownie',
}),
outputPath: brownieOutputPath,
});

Expand All @@ -335,20 +341,13 @@ export const packageIosCommand = curryOptions(

await mergeFrameworks({
sourceDir: userConfig.project.ios.sourceDir,
frameworkPaths: [
path.join(
productsPath,
`${configuration}-iphoneos`,
'BrownfieldNavigation',
'BrownfieldNavigation.framework'
),
path.join(
productsPath,
`${configuration}-iphonesimulator`,
'BrownfieldNavigation',
'BrownfieldNavigation.framework'
),
],
frameworkPaths: collectFrameworkPaths({
productsPath,
configuration,
sdks,
frameworkName: 'BrownfieldNavigation',
productSubDir: 'BrownfieldNavigation',
}),
outputPath: brownfieldNavigationOutputPath,
});

Expand Down Expand Up @@ -378,9 +377,20 @@ export const packageIosCommand = curryOptions(
'In Xcode, choose File > Add Package Dependencies..., click Add Local..., and select that folder.'
);
}

// The dependency prints its own "Success" before the post-build steps above,
// so log a final line marking the true end of the run.
logger.success(
`package:ios finished. Artifacts are in ${colorLink(relativeToCwd(packageDir))}`
);
})
);

/** Whether `--use-prebuilt-expo` was chosen (CLI or brownfield.config.json) rather than inferred. */
export function isUsePrebuiltExpoExplicit(value: boolean | undefined) {
return value !== undefined;
}

export const packageIosExample = new ExampleUsage(
'package:ios --scheme BrownfieldLib --configuration Release',
"Build iOS XCFramework for 'BrownfieldLib' scheme in Release configuration"
Expand Down
Loading
Loading