findArtifact static method
Resolves one IPA from a project-relative file or directory.
Both lexical paths and resolved symlinks must remain under projectRoot.
Implementation
static Future<String> findArtifact(
String projectRoot, {
String ipaOutputPath = 'build/ios/ipa',
}) async {
final repositoryProjectRoot = p.normalize(p.absolute(projectRoot));
final configuredPath = p.normalize(
p.absolute(repositoryProjectRoot, ipaOutputPath),
);
SmfError.check(
p.equals(configuredPath, repositoryProjectRoot) || p.isWithin(repositoryProjectRoot, configuredPath),
'The configured ipa_output_path must stay inside the Flutter app.',
SmfErrorCode.ipaPathEscape,
);
final entityType = await FileSystemEntity.type(configuredPath);
SmfError.check(
entityType != FileSystemEntityType.notFound,
'The build command did not produce an IPA at $configuredPath.',
SmfErrorCode.ipaNotFound,
);
final realProjectRoot = await Directory(
repositoryProjectRoot,
).resolveSymbolicLinks();
final realConfiguredPath = await switch (entityType) {
FileSystemEntityType.file => File(configuredPath).resolveSymbolicLinks(),
FileSystemEntityType.directory => Directory(
configuredPath,
).resolveSymbolicLinks(),
_ => throw const SmfError(
'The configured ipa_output_path must be a file or directory.',
SmfErrorCode.ipaNotFound,
),
};
SmfError.check(
p.equals(realConfiguredPath, realProjectRoot) || p.isWithin(realProjectRoot, realConfiguredPath),
'The configured ipa_output_path resolves outside the Flutter app.',
SmfErrorCode.ipaPathEscape,
);
if (entityType == FileSystemEntityType.file) {
SmfError.check(
configuredPath.toLowerCase().endsWith('.ipa'),
'The configured artifact file must have an .ipa extension.',
SmfErrorCode.ipaNotFound,
);
return configuredPath;
}
List<FileSystemEntity> entries;
try {
entries = await Directory(configuredPath).list().toList();
} on FileSystemException catch (error) {
throw SmfError(
'The build command did not produce an IPA in $configuredPath.',
SmfErrorCode.ipaNotFound,
cause: error,
);
}
final ipas = <String>[];
for (final entry in entries) {
if (entry is! File || !entry.path.toLowerCase().endsWith('.ipa')) {
continue;
}
final realFile = await entry.resolveSymbolicLinks();
SmfError.check(
p.isWithin(realProjectRoot, realFile),
'An IPA in ipa_output_path resolves outside the Flutter app.',
SmfErrorCode.ipaPathEscape,
);
ipas.add(entry.path);
}
ipas.sort();
SmfError.check(
ipas.length == 1,
'Expected exactly one IPA in $configuredPath, found ${ipas.length}.',
SmfErrorCode.ipaCount,
);
return ipas.single;
}