findArtifact static method

Future<String> findArtifact(
  1. String projectRoot, {
  2. String ipaOutputPath = 'build/ios/ipa',
})

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;
}