buildIOS function

Future<void> buildIOS(
  1. ExecContext context, {
  2. required String gh,
  3. required String appName,
  4. required int defaultIncrementNumber,
})

Gibhut Actions build for IOS.

IOS用のGibhut Actionsのビルド。

Implementation

Future<void> buildIOS(
  ExecContext context, {
  required String gh,
  required String appName,
  required int defaultIncrementNumber,
}) async {
  final github = context.yaml.getAsMap("github");
  final action = github.getAsMap("action");
  final ios = action.getAsMap("ios");
  final issuerId = ios.get("issuer_id", "");
  final teamId = ios.get("team_id", "");
  final secretGithub = context.secrets.getAsMap("github");
  final slack = secretGithub.getAsMap("slack");
  final slackIncomingWebhookUrl = slack.get("incoming_webhook_url", "");
  if (issuerId.isEmpty) {
    error(
      "The item [github]->[action]->[ios]->[issuer_id] is missing. Copy the Issuer ID listed on the page at https://appstoreconnect.apple.com/access/api.",
    );
    return;
  }
  if (teamId.isEmpty) {
    error(
      "The item [github]->[action]->[ios]->[team_id] is missing. Copy and include your team ID from https://developer.apple.com/account.",
    );
    return;
  }
  final p12File = await find(
    Directory("ios"),
    RegExp(r".p12$"),
  );
  if (p12File == null) {
    error(
      "Cannot find the `distribution/development.p12` file, download the cer file from AppleDeveloperProgram and create a p12 file with `katana app p12`. `distribution/development.p12`ファイルが見つかりません。cerファイルをAppleDeveloperProgramからダウンロードし、`katana app p12`でp12ファイルを作成してください。",
    );
    return;
  }
  final mobileProvisionFile = await find(
    Directory("ios"),
    RegExp("([^/.]+).mobileprovision"),
  );
  final passwordFile = File("ios/ios_certificate_password.key");
  if (!passwordFile.existsSync()) {
    error(
      "Cannot find password file for Certificate. Please create a p12 file with `katana app p12`. Certificate用のパスワードファイルが見つかりません。`katana app p12`でp12ファイルを作成してください。",
    );
    return;
  }
  final p8File = await find(
    Directory("ios"),
    RegExp(r"AuthKey_([a-zA-Z0-9]+).p8$"),
  );
  if (p8File == null) {
    error(
      "Cannot find the `AuthKey` file, please download the file from AppStoreConnect and place it under the IOS folder. `AuthKey`ファイルが見つかりません。AppStoreConnectからファイルをダウンロードしiosフォルダ以下に配置してください。",
    );
    return;
  }
  label("Edit project.pbxproj");
  final xcode = XCode();
  await xcode.load();
  for (final settings in xcode.pbxBuildConfiguration) {
    settings.buildSettings.removeWhere((e) => e.key == "DEVELOPMENT_TEAM");
  }
  await xcode.save();
  label("Edit Release.xcconfig");
  final xcconfigFile = File("ios/Flutter/Release.xcconfig");
  if (!xcconfigFile.existsSync()) {
    error(
      "Cannot find `ios/Flutter/Release.xcconfig`. Project is broken.",
    );
    return;
  }
  final xcconfig = await xcconfigFile.readAsLines();
  if (!xcconfig.any((e) => e.startsWith("DEVELOPMENT_TEAM="))) {
    xcconfig.add("DEVELOPMENT_TEAM=$teamId");
  }
  await xcconfigFile.writeAsString(xcconfig.join("\n"));
  label("Create ExportOptions.plist");
  await const ExportOptionsCliCode().generateFile("ExportOptions.plist");
  final p8 = base64.encode(await p8File.readAsBytes());
  final p8Key =
      RegExp(r"AuthKey_([a-zA-Z0-9]+).p8$").firstMatch(p8File.path)!.group(1)!;
  final p12 = base64.encode(await p12File.readAsBytes());
  final password = await passwordFile.readAsString();
  if (mobileProvisionFile != null) {
    final mobileProvision =
        base64.encode(await mobileProvisionFile.readAsBytes());
    await command(
      "Store `${mobileProvisionFile.path.last()}` in `secrets.IOS_PROVISIONING_PROFILE_${appName.toUpperCase()}`.",
      [
        gh,
        "secret",
        "set",
        "IOS_PROVISIONING_PROFILE_${appName.toUpperCase()}",
        "--body",
        mobileProvision,
      ],
    );
  }
  await command(
    "Store `${p12File.path.last()}` in `secrets.IOS_CERTIFICATES_P12_${appName.toUpperCase()}`.",
    [
      gh,
      "secret",
      "set",
      "IOS_CERTIFICATES_P12_${appName.toUpperCase()}",
      "--body",
      p12,
    ],
  );
  await command(
    "Store `ios_certificate_password.key` in `secrets.IOS_CERTIFICATE_PASSWORD_${appName.toUpperCase()}`.",
    [
      gh,
      "secret",
      "set",
      "IOS_CERTIFICATE_PASSWORD_${appName.toUpperCase()}",
      "--body",
      password,
    ],
  );
  await command(
    "Store API key id in `secrets.IOS_API_KEY_ID_${appName.toUpperCase()}`.",
    [
      gh,
      "secret",
      "set",
      "IOS_API_KEY_ID_${appName.toUpperCase()}",
      "--body",
      p8Key,
    ],
  );
  await command(
    "Store `${p8File.path}` in `secrets.IOS_API_AUTHKEY_P8_${appName.toUpperCase()}`.",
    [
      gh,
      "secret",
      "set",
      "IOS_API_AUTHKEY_P8_${appName.toUpperCase()}",
      "--body",
      p8,
    ],
  );
  await command(
    "Store Issuer ID in `secrets.IOS_API_ISSUER_ID_${appName.toUpperCase()}`.",
    [
      gh,
      "secret",
      "set",
      "IOS_API_ISSUER_ID_${appName.toUpperCase()}",
      "--body",
      issuerId,
    ],
  );
  final gitDir = await findGitDirectory(Directory.current);
  final iosCode = GithubActionsIOSCliCode(
    workingDirectory: gitDir,
    defaultIncrementNumber: defaultIncrementNumber,
  );
  await iosCode.generateFile(
    "build_ios_${appName.toLowerCase()}.yaml",
    filter: (value) {
      return iosCode._additionalSlackFilter(
        value.replaceAll(
          "#### REPLACE_APP_NAME ####",
          appName.toUpperCase(),
        ),
        slackIncomingWebhookUrl,
      );
    },
  );
  label("Edit Info.plist.");
  final plist = File("ios/Runner/Info.plist");
  final document = XmlDocument.parse(await plist.readAsString());
  final dict = document.findAllElements("dict").firstOrNull;
  if (dict == null) {
    throw Exception(
      "Could not find `dict` element in `ios/Runner/Info.plist`. File is corrupt.",
    );
  }
  final nodeITSEncryptionExportComplianceCode =
      dict.children.firstWhereOrNull((p0) {
    return p0 is XmlElement &&
        p0.name.toString() == "key" &&
        p0.innerText == "ITSEncryptionExportComplianceCode";
  });
  if (nodeITSEncryptionExportComplianceCode == null) {
    dict.children.addAll(
      [
        XmlElement(
          XmlName("key"),
          [],
          [XmlText("ITSEncryptionExportComplianceCode")],
        ),
        XmlElement(
          XmlName("false"),
        ),
      ],
    );
  }
  final nodeITSAppUsesNonExemptEncryption =
      dict.children.firstWhereOrNull((p0) {
    return p0 is XmlElement &&
        p0.name.toString() == "key" &&
        p0.innerText == "ITSAppUsesNonExemptEncryption";
  });
  if (nodeITSAppUsesNonExemptEncryption == null) {
    dict.children.addAll(
      [
        XmlElement(
          XmlName("key"),
          [],
          [XmlText("ITSAppUsesNonExemptEncryption")],
        ),
        XmlElement(
          XmlName("false"),
        ),
      ],
    );
  }
  await plist.writeAsString(
    document.toXmlString(pretty: true, indent: "\t", newLine: "\n"),
  );
  label("Rewrite `.gitignore`.");
  final gitignore = File("ios/.gitignore");
  if (!gitignore.existsSync()) {
    error("Cannot find `ios/.gitignore`. Project is broken.");
    return;
  }
  final gitignores = await gitignore.readAsLines();
  if (context.yaml.getAsMap("git").get("ignore_secure_file", true)) {
    if (!gitignores.any((e) => e.startsWith("**/*.p12"))) {
      gitignores.add("**/*.p12");
    }
    if (!gitignores.any((e) => e.startsWith("**/*.p8"))) {
      gitignores.add("**/*.p8");
    }
    if (!gitignores.any((e) => e.startsWith("**/*.mobileprovision"))) {
      gitignores.add("**/*.mobileprovision");
    }
    if (!gitignores.any((e) => e.startsWith("**/*.pem"))) {
      gitignores.add("**/*.pem");
    }
    if (!gitignores.any((e) => e.startsWith("**/*.cer"))) {
      gitignores.add("**/*.cer");
    }
    if (!gitignores.any((e) => e.startsWith("**/*.certSigningRequest"))) {
      gitignores.add("**/*.certSigningRequest");
    }
    if (!gitignores
        .any((e) => e.startsWith("**/ios_certificate_password.key"))) {
      gitignores.add("**/ios_certificate_password.key");
    }
    if (!gitignores.any((e) => e.startsWith("**/ios_enterprise.key"))) {
      gitignores.add("**/ios_enterprise.key");
    }
  } else {
    gitignores.removeWhere((e) => e.startsWith("**/*.p12"));
    gitignores.removeWhere((e) => e.startsWith("**/*.p8"));
    gitignores.removeWhere((e) => e.startsWith("**/*.mobileprovision"));
    gitignores.removeWhere((e) => e.startsWith("**/*.pem"));
    gitignores.removeWhere((e) => e.startsWith("**/*.cer"));
    gitignores.removeWhere((e) => e.startsWith("**/*.certSigningRequest"));
    gitignores
        .removeWhere((e) => e.startsWith("**/ios_certificate_password.key"));
    gitignores.removeWhere((e) => e.startsWith("**/ios_enterprise.key"));
  }
  await gitignore.writeAsString(gitignores.join("\n"));
}