diff --git a/tools/BUILD b/tools/BUILD index a0d546fde2..7c7632c011 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -4,6 +4,7 @@ load("//xcodeproj:defs.bzl", "xcodeproj", "xcschemes") load("//xcodeproj/internal:collections.bzl", "flatten", "uniq") _TOOLS = { + "calculate_output_groups": "//tools/calculate_output_groups", "files_and_groups": "//tools/generators/files_and_groups", "import_indexstores": "//tools/import_indexstores", "pbxnativetargets": "//tools/generators/pbxnativetargets", @@ -51,6 +52,38 @@ _XCSCHEME_DIAGNOSTICS = xcschemes.diagnostics( ) _XCSCHEMES = [ + xcschemes.scheme( + name = "calculate_output_groups", + profile = xcschemes.profile( + launch_target = xcschemes.launch_target( + _TOOLS["calculate_output_groups"], + ), + xcode_configuration = "Release", + ), + run = xcschemes.run( + args = [ + # colorDiagnostics + "NO", + # xcodeVersionActual + "1520", + # nonPreviewObjRoot + "/Users/brentley/Library/Developer/Xcode/DerivedData/tools-exxvdkcaoxdlhndlfnwxqvucohsr/Build/Intermediates.noindex", + # baseObjRoot + "/Users/brentley/Library/Developer/Xcode/DerivedData/tools-exxvdkcaoxdlhndlfnwxqvucohsr/Build/Intermediates.noindex", + # buildMarkerFile + "/Users/brentley/Library/Developer/Xcode/DerivedData/tools-exxvdkcaoxdlhndlfnwxqvucohsr/Build/Intermediates.noindex/build_marker", + # outputGroupPrefixes + "bc,bp,bi", + ], + build_targets = [ + _TOOLS["calculate_output_groups"], + ], + diagnostics = _XCSCHEME_DIAGNOSTICS, + launch_target = xcschemes.launch_target( + _TOOLS["calculate_output_groups"], + ), + ), + ), xcschemes.scheme( name = "files_and_groups", profile = xcschemes.profile( @@ -561,6 +594,7 @@ xcodeproj( filegroup( name = "release_files", srcs = [ + "//" + package_name() + "/calculate_output_groups:release_files", "//" + package_name() + "/extension_point_identifiers_parser:release_files", "//" + package_name() + "/generators:release_files", "//" + package_name() + "/import_indexstores:release_files", diff --git a/tools/calculate_output_groups/Arguments.swift b/tools/calculate_output_groups/Arguments.swift new file mode 100644 index 0000000000..34ec13ac1f --- /dev/null +++ b/tools/calculate_output_groups/Arguments.swift @@ -0,0 +1,41 @@ +import ArgumentParser +import Foundation + +extension OutputGroupsCalculator { + struct Arguments: ParsableArguments { + @Argument( + help: "Value of the 'XCODE_VERSION_ACTUAL' environment variable." + ) + var xcodeVersionActual: Int + + @Argument( + help: """ +Value of the 'OBJROOT' build setting when 'ENABLE_PREVIEWS = NO'. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: true) } + ) + var nonPreviewObjRoot: URL + + @Argument( + help: """ +Value of 'nonPreviewObjRoot' when 'INDEX_ENABLE_BUILD_ARENA = NO'. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: true) } + ) + var baseObjRoot: URL + + @Argument( + help: """ +Path to a file that has a ctime at or after the start of the build. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var buildMarkerFile: URL + + @Argument( + help: "Comma seperated list of output group prefixes.", + transform: { $0.split(separator: ",").map(String.init) } + ) + var outputGroupPrefixes: [String] + } +} diff --git a/tools/calculate_output_groups/BUILD b/tools/calculate_output_groups/BUILD new file mode 100644 index 0000000000..d30a3f3be6 --- /dev/null +++ b/tools/calculate_output_groups/BUILD @@ -0,0 +1,64 @@ +load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary") +load( + "@build_bazel_rules_apple//apple:macos.bzl", + "macos_command_line_application", +) +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_binary", + "swift_library", +) + +# This target exists to keep configurations the same between the generator +# and the tests, which makes the Xcode development experience better. If we used +# `swift_binary` or `apple_universal_binary` in `xcodeproj`, then the +# `macos_unit_test` transition (which is used to be able to set a minimum os +# version on the tests) will create slightly different configurations for our +# `swift_library`s. Maybe https://github.com/bazelbuild/bazel/issues/6526 will +# fix that for us. +macos_command_line_application( + name = "calculate_output_groups", + minimum_os_version = "12.0", + visibility = ["//visibility:public"], + deps = [":calculate_output_groups.library"], +) + +swift_library( + name = "calculate_output_groups.library", + srcs = glob(["*.swift"]), + module_name = "calculate_output_groups", + deps = [ + "//tools/lib/ToolCommon", + "@com_github_apple_swift_argument_parser//:ArgumentParser", + "@com_github_michaeleisel_zippyjson//:ZippyJSON", + ], +) + +swift_binary( + name = "calculate_output_groups_binary", + deps = [":calculate_output_groups.library"], +) + +apple_universal_binary( + name = "universal_calculate_output_groups", + binary = ":calculate_output_groups_binary", + forced_cpus = [ + "x86_64", + "arm64", + ], + minimum_os_version = "12.0", + platform_type = "macos", + visibility = ["//visibility:public"], +) + +# Release + +filegroup( + name = "release_files", + srcs = [ + "BUILD.release.bazel", + ":universal_calculate_output_groups", + ], + tags = ["manual"], + visibility = ["//:__subpackages__"], +) diff --git a/tools/calculate_output_groups/BUILD.release.bazel b/tools/calculate_output_groups/BUILD.release.bazel new file mode 100644 index 0000000000..a928619468 --- /dev/null +++ b/tools/calculate_output_groups/BUILD.release.bazel @@ -0,0 +1 @@ +exports_files(["universal_calculate_output_groups"]) diff --git a/tools/calculate_output_groups/CalculateOutputGroups.swift b/tools/calculate_output_groups/CalculateOutputGroups.swift new file mode 100644 index 0000000000..faa910d9aa --- /dev/null +++ b/tools/calculate_output_groups/CalculateOutputGroups.swift @@ -0,0 +1,31 @@ +import ArgumentParser +import Darwin +import ToolCommon + +@main +struct CalculateOutputGroups: AsyncParsableCommand { + @Argument( + help: "Value of the 'COLOR_DIAGNOSTICS' environment variable.", + transform: { $0 == "YES" } + ) + var colorDiagnostics: Bool + + @OptionGroup var arguments: OutputGroupsCalculator.Arguments + + func run() async throws { + let logger = DefaultLogger( + standardError: StderrOutputStream(), + standardOutput: StdoutOutputStream(), + colorize: colorDiagnostics + ) + + let calculator = OutputGroupsCalculator() + + do { + try await calculator.calculateOutputGroups(arguments: arguments) + } catch { + logger.logError(error.localizedDescription) + Darwin.exit(1) + } + } +} diff --git a/tools/calculate_output_groups/OutputGroupsCalculator.swift b/tools/calculate_output_groups/OutputGroupsCalculator.swift new file mode 100644 index 0000000000..04075ea007 --- /dev/null +++ b/tools/calculate_output_groups/OutputGroupsCalculator.swift @@ -0,0 +1,126 @@ +import Foundation +import ToolCommon +import ZippyJSON + +struct OutputGroupsCalculator { + func calculateOutputGroups(arguments: Arguments) async throws { + let pifCache = arguments.baseObjRoot + .appendingPathComponent("XCBuildData/PIFCache") + let projectCache = pifCache.appendingPathComponent("project") + let targetCache = pifCache.appendingPathComponent("target") + + let fileManager = FileManager.default + + guard fileManager.fileExists(atPath: projectCache.path) && + fileManager.fileExists(atPath: targetCache.path) + else { + throw UsageError(message: """ +error: PIFCache (\(pifCache)) doesn't exist. If you manually cleared Derived \ +Data, you need to close and re-open the project for the PIFCache to be created \ +again. Using the "Clean Build Folder" command instead (⇧ ⌘ K) won't trigger \ +this error. If this error still happens after re-opening the project, please \ +file a bug report here: \ +https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md +""") + } + + let projectURL = try Self.findProjectURL(in: projectCache) + let project = try Self.decodeProject(at: projectURL) + let targets = + try await Self.decodeTargets(project.targets, in: targetCache) + + dump(targets) + } + + static func findProjectURL(in projectCache: URL) throws -> URL { + let projectPIFsEnumerator = FileManager.default.enumerator( + at: projectCache, + includingPropertiesForKeys: [.contentModificationDateKey], + options: [ + .skipsHiddenFiles, + .skipsPackageDescendants, + .skipsSubdirectoryDescendants, + ] + )! + + var newestProjectPIF: URL? + var newestProjectPIFDate = Date.distantPast + for case let projectPIF as URL in projectPIFsEnumerator { + guard let resourceValues = try? projectPIF.resourceValues( + forKeys: [.contentModificationDateKey] + ), let modificationDate = resourceValues.contentModificationDate + else { + continue + } + + // TODO: The modification date is in the filename, should we use + // that instead? + if modificationDate > newestProjectPIFDate { + newestProjectPIF = projectPIF + newestProjectPIFDate = modificationDate + } + } + + guard let projectPIF = newestProjectPIF else { + throw UsageError(message: """ +error: Couldn't find a Project PIF at "\(projectCache)". Please file a bug \ +report here: https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md +""") + } + + return projectPIF + } + + static func decodeProject(at url: URL) throws -> ProjectPIF { + let decoder = ZippyJSONDecoder() + return try decoder.decode(ProjectPIF.self, from: Data(contentsOf: url)) + } + + static func decodeTargets( + _ targets: [String], + in targetCache: URL + ) async throws -> [TargetPIF] { + return try await withThrowingTaskGroup( + of: TargetPIF.self, + returning: [TargetPIF].self + ) { group in + for target in targets { + group.addTask { + let url = + targetCache.appendingPathComponent("\(target)-json") + let decoder = ZippyJSONDecoder() + return try decoder + .decode(TargetPIF.self, from: Data(contentsOf: url)) + } + } + + var targetPIFs: [TargetPIF] = [] + for try await target in group { + targetPIFs.append(target) + } + + return targetPIFs + } + } +} + +struct ProjectPIF: Decodable { + let targets: [String] +} + +struct TargetPIF: Decodable { + struct BuildConfiguration: Decodable { + let name: String + let buildSettings: [String: String] + } + + let guid: String + let buildConfigurations: [BuildConfiguration] +} + +struct Target { + let label: String + + // Maps Platform Name -> [Target ID] + let targetIds: [String: [String]] +}