Skip to content
Merged
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
38 changes: 38 additions & 0 deletions packages/shorebird_cli/lib/src/code_push_client_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -497,13 +497,30 @@ Looked in:
);
}

// Track which arch paths AGP didn't produce, so we can surface them at
// the end if zero archs uploaded. AGP omits an arch directory whenever
// the project filters it out via `ndk.abiFilters`, `splits.abi`, or
// `jniLibs.excludes`. Iterating only over present files lets a filtered
// release succeed instead of crashing on the first missing arch
// (https://github.com/shorebirdtech/shorebird/issues/3388).
final missingArchPaths = <String>[];
var uploadedArchCount = 0;
for (final arch in architectures) {
final artifactPath = p.join(
archsDir.path,
arch.androidBuildPath,
'libapp.so',
);
final artifact = File(artifactPath);
if (!artifact.existsSync()) {
logger.detail(
'Skipping ${arch.arch}: no libapp.so at $artifactPath. '
'This is expected if the project filters this ABI via '
'ndk.abiFilters, splits.abi, or jniLibs.excludes.',
);
missingArchPaths.add(artifactPath);
continue;
}
final hash = sha256.convert(await artifact.readAsBytes()).toString();
logger.detail('Uploading artifact for $artifactPath');

Expand All @@ -518,7 +535,9 @@ Looked in:
canSideload: false,
podfileLockHash: null,
);
uploadedArchCount++;
} on CodePushConflictException catch (_) {
uploadedArchCount++;
// Newlines are due to how logger.info interacts with logger.progress.
logger.info('''

Expand All @@ -532,6 +551,25 @@ ${arch.arch} artifact already exists, continuing...''');
}
}

if (uploadedArchCount == 0) {
_handleErrorAndExit(
Exception('No architecture artifacts found to upload.'),
progress: createArtifactProgress,
message:
'''
No architecture artifacts found to upload.

Shorebird looked for libapp.so under ${archsDir.path} but every requested
architecture was missing:
${missingArchPaths.map((p) => ' - $p').join('\n')}

This usually means your project's ndk.abiFilters / splits.abi / jniLibs.excludes
configuration excludes every architecture Shorebird was asked to build. Either
relax those filters or pass `--target-platform=<archs>` to restrict Shorebird
to the architectures your project actually builds.''',
);
}

try {
logger.detail('Uploading artifact for $aabPath');
await codePushClient.createReleaseArtifact(
Expand Down
19 changes: 19 additions & 0 deletions packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
);
logger.detail('Creating artifact for $patchArtifactPath');
final patchArtifact = File(patchArtifactPath);
// Skip archs the build didn't produce (ndk.abiFilters / splits.abi /
// jniLibs.excludes). Without this, a release built for a subset of
// archs can't be patched without hitting PathNotFoundException
// (https://github.com/shorebirdtech/shorebird/issues/3388).
if (!patchArtifact.existsSync()) {
logger.detail(
'Skipping ${arch.arch}: no libapp.so at $patchArtifactPath. '
'This is expected if the project filters this ABI via '
'ndk.abiFilters, splits.abi, or jniLibs.excludes.',
);
continue;
}
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
final hashSignature = await signHash(hash);

Expand All @@ -212,6 +224,13 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
throw ProcessExit(ExitCode.software.code);
}
}
if (patchArtifactBundles.isEmpty) {
createDiffProgress.fail(
'No patch artifacts produced: no libapp.so found for any architecture '
'under ${patchArchsBuildDir.path}.',
);
throw ProcessExit(ExitCode.software.code);
}
createDiffProgress.complete();
return patchArtifactBundles;
}
Expand Down
123 changes: 123 additions & 0 deletions packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,129 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
verify(() => progress.complete()).called(1);
verifyNever(() => progress.fail(any()));
});

test(
'skips arches whose libapp.so is missing (abiFilters case)',
() async {
setUpProjectRoot();
// Simulate AGP filtering: remove armeabi-v7a libapp.so to mirror
// a project with ndk.abiFilters that excludes arm32.
final missingArch = Arch.arm32;
File(
p.join(
projectRoot.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
missingArch.androidBuildPath,
'libapp.so',
),
).deleteSync();

await runWithOverrides(
() async => codePushClientWrapper.createAndroidReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
platform: releasePlatform,
projectRoot: projectRoot.path,
aabPath: p.join(projectRoot.path, aabPath),
architectures: Arch.values,
),
);

// One upload per surviving arch, plus one for the aab.
final expectedUploads = Arch.values.length - 1 + 1;
verify(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).called(expectedUploads);
verifyNever(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: missingArch.arch,
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
);
verify(() => progress.complete()).called(1);
verifyNever(() => progress.fail(any()));
},
);

test(
'exits with code 70 when every requested arch is missing',
() async {
// Create the archs directory but no libapp.so files inside it.
// This mirrors AGP producing an empty strip output (all archs
// filtered out).
for (final archMetadata in Arch.values) {
Directory(
p.join(
projectRoot.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
archMetadata.androidBuildPath,
),
).createSync(recursive: true);
}
File(p.join(projectRoot.path, aabPath)).createSync(recursive: true);

await expectLater(
() async => runWithOverrides(
() async => codePushClientWrapper.createAndroidReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
platform: releasePlatform,
projectRoot: projectRoot.path,
aabPath: p.join(projectRoot.path, aabPath),
architectures: Arch.values,
),
),
exitsWithCode(ExitCode.software),
);

verify(
() => progress.fail(
any(
that: contains('No architecture artifacts found to upload'),
),
),
).called(1);
verifyNever(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
);
},
);
});

group('createWindowsReleaseArtifacts', () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,84 @@ Looked in:
});
});

group('when one arch is missing from the patch build', () {
setUp(() {
// Create every arch's libapp.so, then delete one to simulate AGP
// filtering a single ABI via ndk.abiFilters / splits.abi.
setUpProjectRootArtifacts();
patchArtifactForArch(Arch.arm32).deleteSync();

when(
() => artifactManager.createDiff(
releaseArtifactPath: any(named: 'releaseArtifactPath'),
patchArtifactPath: any(named: 'patchArtifactPath'),
),
).thenAnswer((_) async {
final tempDir = Directory.systemTemp.createTempSync();
final diffPath = p.join(tempDir.path, 'diff');
File(diffPath)
..createSync()
..writeAsStringSync('test');
return diffPath;
});
});

test('skips missing arch and returns bundles for the rest', () async {
final result = await runWithOverrides(
() => patcher.createPatchArtifacts(
appId: 'appId',
releaseId: 0,
releaseArtifact: File('release.aab'),
),
);

expect(result.keys, isNot(contains(Arch.arm32)));
expect(result, hasLength(Arch.values.length - 1));
verifyNever(() => progress.fail(any()));
});
});

group('when every arch is missing from the patch build', () {
setUp(() {
// Create the archs directory layout so androidArchsDirectory
// resolves, but leave each arch directory empty.
for (final arch in Arch.values) {
Directory(
p.join(
projectRoot.path,
'build',
'app',
'intermediates',
'stripped_native_libs',
'release',
'out',
'lib',
arch.androidBuildPath,
),
).createSync(recursive: true);
}
});

test('fails progress and exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: 'appId',
releaseId: 0,
releaseArtifact: File('release.aab'),
),
),
exitsWithCode(ExitCode.software),
);

verify(
() => progress.fail(
any(that: contains('No patch artifacts produced')),
),
).called(1);
});
});

group('when patch artifacts successfully created', () {
setUp(() {
setUpProjectRootArtifacts();
Expand Down
Loading