fix(shorebird_cli): skip missing-arch libapp.so on Android upload and patch (#3793)
This commit is contained in:
@@ -497,6 +497,14 @@ 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,
|
||||
@@ -504,6 +512,15 @@ Looked in:
|
||||
'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');
|
||||
|
||||
@@ -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('''
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user