From 470682e55cfa6691e740aa70f9d9b6250a7b9fd7 Mon Sep 17 00:00:00 2001 From: Ian Leitch Date: Sun, 11 Aug 2024 10:35:30 +0200 Subject: [PATCH] Bazel support --- .gitignore | 6 +- MODULE.bazel | 11 ++ MODULE.bazel.lock | 75 +++++++++ Package.swift | 5 +- Sources/Frontend/Commands/ScanCommand.swift | 4 + Sources/Frontend/Project.swift | 4 + Sources/Frontend/Scan.swift | 4 +- Sources/Frontend/main.swift | 12 +- Sources/PeripheryKit/Bazel/Bazel.swift | 8 + .../Bazel/BazelProjectDriver.swift | 120 ++++++++++++++ .../Generic/GenericProjectDriver.swift | 14 +- Sources/PeripheryKit/ProjectDriver.swift | 8 + .../PeripheryKit/SPM/SPMProjectDriver.swift | 1 + Sources/Shared/Configuration.swift | 13 +- Sources/Shared/Logger.swift | 9 +- Sources/Shared/PeripheryError.swift | 8 +- Sources/Shared/ProjectKind.swift | 1 + Sources/Shared/Shell.swift | 136 ++++++++-------- bazel/BUILD.bazel | 13 ++ bazel/extensions.bzl | 22 +++ bazel/internal/BUILD.bazel | 3 + bazel/internal/scan.bzl | 152 ++++++++++++++++++ bazel/internal/scan_template.sh | 2 + 23 files changed, 536 insertions(+), 95 deletions(-) create mode 100644 MODULE.bazel create mode 100644 MODULE.bazel.lock create mode 100644 Sources/PeripheryKit/Bazel/Bazel.swift create mode 100644 Sources/PeripheryKit/Bazel/BazelProjectDriver.swift create mode 100644 bazel/BUILD.bazel create mode 100644 bazel/extensions.bzl create mode 100644 bazel/internal/BUILD.bazel create mode 100644 bazel/internal/scan.bzl create mode 100644 bazel/internal/scan_template.sh diff --git a/.gitignore b/.gitignore index 55c803316..2235ba614 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,8 @@ DerivedData Tests/Fixtures/.build/ # VSCode -.vscode/* \ No newline at end of file +.vscode/* + +# Bazel +bazel-* +/MODULE.bazel.lock \ No newline at end of file diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000..4dbf83b3c --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,11 @@ +module( + name = "periphery", + version = "0.0.0", + compatibility_level = 1, +) + +bazel_dep(name = "rules_swift", version = "2.1.1") +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +generated = use_extension("//bazel:extensions.bzl", "generated") +use_repo(generated, "periphery_generated") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock new file mode 100644 index 000000000..951137685 --- /dev/null +++ b/MODULE.bazel.lock @@ -0,0 +1,75 @@ +{ + "lockFileVersion": 11, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/source.json": "7e3a9adf473e9af076ae485ed649d5641ad50ec5c11718103f34de03170d94ad", + "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", + "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", + "https://bcr.bazel.build/modules/apple_support/1.15.1/source.json": "517f2b77430084c541bc9be2db63fdcbb7102938c5f64c17ee60ffda2e5cf07b", + "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015", + "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.11.0/source.json": "c73d9ef4268c91bd0c1cd88f1f9dfa08e814b1dbe89b5f594a9f08ba0244d206", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/21.7/source.json": "bbe500720421e582ff2d18b0802464205138c06056f443184de39fbb8187b09b", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", + "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/source.json": "a075731e1b46bc8425098512d038d416e966ab19684a10a34f4741295642fc35", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", + "https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", + "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", + "https://bcr.bazel.build/modules/rules_swift/2.1.1/source.json": "40fc69dfaac64deddbb75bd99cdac55f4427d9ca0afbe408576a65428427a186", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.1/source.json": "a96f95e02123320aa015b956f29c00cb818fa891ef823d55148e1a362caacf29", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/source.json": "f1ef7d3f9e0e26d4b23d1c39b5f5de71f584dd7d1b4ef83d9bbba6ec7a6a6459", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d" + }, + "selectedYankedVersions": {}, + "moduleExtensions": {} +} diff --git a/Package.swift b/Package.swift index 1f5c5742d..2da9af4a2 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ var targets: [PackageDescription.Target] = [ dependencies: [ .target(name: "SyntaxAnalysis"), .target(name: "Shared"), - .product(name: "SwiftIndexStore", package: "swift-indexstore") + .product(name: "SwiftIndexStore", package: "swift-indexstore"), ] ), .target( @@ -64,7 +64,8 @@ var targets: [PackageDescription.Target] = [ dependencies: [ .target(name: "SourceGraph"), .target(name: "Shared"), - .product(name: "SwiftSyntax", package: "swift-syntax") + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), ] ), .target( diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index 2f2ee1d68..4916b5de7 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -126,6 +126,9 @@ struct ScanCommand: FrontendCommand { @Option(help: "Project configuration for non-Apple build systems") var genericProjectConfig: FilePath? + @Flag(help: "Enable Bazel project mode") + var bazel: Bool = defaultConfiguration.$bazel.defaultValue + private static let defaultConfiguration = Configuration() func run() throws { @@ -174,6 +177,7 @@ struct ScanCommand: FrontendCommand { configuration.apply(\.$baseline, baseline) configuration.apply(\.$writeBaseline, writeBaseline) configuration.apply(\.$genericProjectConfig, genericProjectConfig) + configuration.apply(\.$bazel, bazel) try scanBehavior.main { project in try Scan().perform(project: project) diff --git a/Sources/Frontend/Project.swift b/Sources/Frontend/Project.swift index 12f00df83..21e1c3f50 100644 --- a/Sources/Frontend/Project.swift +++ b/Sources/Frontend/Project.swift @@ -15,6 +15,8 @@ final class Project { return self.init(kind: .xcode(projectPath: path)) } else if let path = configuration.genericProjectConfig { return self.init(kind: .generic(genericProjectConfig: path)) + } else if Bazel.isSupported && configuration.bazel { + return self.init(kind: .bazel) } else if SPM.isSupported { return self.init(kind: .spm) } @@ -38,6 +40,8 @@ final class Project { #endif case .spm: return try SPMProjectDriver.build() + case .bazel: + return try BazelProjectDriver.build() case .generic(let genericProjectConfig): return try GenericProjectDriver.build(genericProjectConfig: genericProjectConfig) } diff --git a/Sources/Frontend/Scan.swift b/Sources/Frontend/Scan.swift index c5452b00a..8747d42a6 100644 --- a/Sources/Frontend/Scan.swift +++ b/Sources/Frontend/Scan.swift @@ -24,12 +24,14 @@ final class Scan { } } + let driver = try setup(project) + + // Output config after project setup as the driver may alter the configuration. if configuration.verbose { let configYaml = try configuration.asYaml() logger.debug("[configuration:begin]\n\(configYaml.trimmed)\n[configuration:end]") } - let driver = try setup(project) try build(driver) try index(driver) try analyze() diff --git a/Sources/Frontend/main.swift b/Sources/Frontend/main.swift index 91289fe80..28daafdeb 100644 --- a/Sources/Frontend/main.swift +++ b/Sources/Frontend/main.swift @@ -7,13 +7,21 @@ Logger.configureBuffering() struct PeripheryCommand: FrontendCommand { static let configuration = CommandConfiguration( commandName: "periphery", - subcommands: [ScanCommand.self, CheckUpdateCommand.self, ClearCacheCommand.self, VersionCommand.self] + subcommands: [ + ScanCommand.self, + CheckUpdateCommand.self, + ClearCacheCommand.self, + VersionCommand.self + ] ) } signal(SIGINT) { _ in let logger = Logger() - logger.warn("Termination can result in a corrupt index. Try the '--clean-build' flag if you get erroneous results, such as false-positives and incorrect source file locations.") + logger.warn( + "Termination can result in a corrupt index. Try the '--clean-build' flag if you get erroneous results, such as false-positives and incorrect source file locations.", + newlinePrefix: true // Print a newline after ^C + ) Shell.shared.interruptRunning() exit(0) } diff --git a/Sources/PeripheryKit/Bazel/Bazel.swift b/Sources/PeripheryKit/Bazel/Bazel.swift new file mode 100644 index 000000000..a04c09c8c --- /dev/null +++ b/Sources/PeripheryKit/Bazel/Bazel.swift @@ -0,0 +1,8 @@ +import Foundation +import SystemPackage + +public struct Bazel { + public static var isSupported: Bool { + FilePath("MODULE.bazel").exists || FilePath("WORKSPACE").exists + } +} diff --git a/Sources/PeripheryKit/Bazel/BazelProjectDriver.swift b/Sources/PeripheryKit/Bazel/BazelProjectDriver.swift new file mode 100644 index 000000000..c928b1dd0 --- /dev/null +++ b/Sources/PeripheryKit/Bazel/BazelProjectDriver.swift @@ -0,0 +1,120 @@ +import Foundation +import Shared +import Indexer +import SystemPackage + +public class BazelProjectDriver: ProjectDriver { + public static func build() throws -> Self { + let configuration = Configuration.shared + configuration.bazel = false // Generic project mode is used for the actual scan. + configuration.reportExclude.append("**/bazel-out/**/*") + return self.init(configuration: configuration) + } + + // TODO: Others? tvos, watchos, etc + private static let kinds = [ + "apple_framework_packaging", + "ios_unit_test", + "ios_ui_test", + "ios_application", +// "swift_binary", +// "swift_test" + ] + + private let configuration: Configuration + private let shell: Shell + private let logger: Logger + private let fileManager: FileManager + + private let outputPath = FilePath("/var/tmp/periphery_bazel") + + private lazy var contextLogger: ContextualLogger = { + logger.contextualized(with: "bazel") + }() + + required init( + configuration: Configuration = .shared, + shell: Shell = .shared, + logger: Logger = .init(), + fileManager: FileManager = .default + ) { + self.configuration = configuration + self.shell = shell + self.logger = logger + self.fileManager = fileManager + } + + public func build() throws { + guard let executablePath = Bundle.main.executablePath else { + fatalError("Expected executable path.") + } + + try fileManager.createDirectory(at: outputPath.url, withIntermediateDirectories: true) + + let configPath = outputPath.appending("periphery.yml") + try configuration.save(to: configPath) + contextLogger.debug("Configuration written to \(configPath)") + + let buildPath = outputPath.appending("BUILD.bazel") + let deps = try queryTargets().joined(separator: ",\n") + let buildFileContents = """ + load("@periphery//bazel/internal:scan.bzl", "scan") + + scan( + name = "scan", + testonly = True, + config = "\(configPath)", + periphery_binary = "\(executablePath)", + visibility = [ + "@periphery//bazel:package_group" + ], + deps = [ + \(deps) + ], + ) + """ + + try buildFileContents.write(to: buildPath.url, atomically: true, encoding: .utf8) + contextLogger.debug("Build file written to \(buildPath)") + + if configuration.outputFormat.supportsAuxiliaryOutput { + let asterisk = colorize("*", .boldGreen) + logger.info("\(asterisk) Building...") + } + + let status = try shell.execStatus([ + "bazel", + "run", + "--ui_event_filters=-info,-debug,-warning", + "@periphery//bazel:scan"] + ) + + // The actual scan is performed by Bazel. + exit(status) + } + + // MARK: - Private + + private func queryTargets() throws -> [String] { + try shell + .exec([ + "bazel", + "query", + "--noshow_progress", + "--ui_event_filters=-info,-debug,-warning", + query + ]) + .split(separator: "\n") + .map { "\"@@\($0)\"" } + } + + private var query: String { + // TODO: Make configurable. + // TODO: Add option to filter labels. + let target = ["//..."] + let depsExpr = target.map { "deps(\($0))" }.joined(separator: " union ") + let kindsExpr = "kind('(\(Self.kinds.joined(separator: "|"))) rule', \(depsExpr))" + let filterExpr = "filter('^//.*', \(kindsExpr))" + return filterExpr + } +} diff --git a/Sources/PeripheryKit/Generic/GenericProjectDriver.swift b/Sources/PeripheryKit/Generic/GenericProjectDriver.swift index e6242489b..662417656 100644 --- a/Sources/PeripheryKit/Generic/GenericProjectDriver.swift +++ b/Sources/PeripheryKit/Generic/GenericProjectDriver.swift @@ -6,7 +6,8 @@ import SystemPackage public final class GenericProjectDriver { struct GenericConfig: Decodable { - let plistPaths: Set + let indexstores: Set + let plists: Set let testTargets: Set } @@ -19,24 +20,29 @@ public final class GenericProjectDriver { decoder.keyDecodingStrategy = .convertFromSnakeCase let data = try Data(contentsOf: genericProjectConfig.url) let config = try decoder.decode(GenericConfig.self, from: data) - let plistPaths = config.plistPaths.mapSet { FilePath.makeAbsolute($0) } + let plistPaths = config.plists.mapSet { FilePath.makeAbsolute($0) } + let indexstorePaths = config.indexstores.mapSet { FilePath.makeAbsolute($0) } return self.init( + indexstorePaths: indexstorePaths, plistPaths: plistPaths, testTargets: config.testTargets, configuration: .shared ) } + private let indexstorePaths: Set private let plistPaths: Set private let testTargets: Set private let configuration: Configuration private init( + indexstorePaths: Set, plistPaths: Set, testTargets: Set, configuration: Configuration ) { + self.indexstorePaths = indexstorePaths self.plistPaths = plistPaths self.testTargets = testTargets self.configuration = configuration @@ -44,12 +50,10 @@ public final class GenericProjectDriver { } extension GenericProjectDriver: ProjectDriver { - public func build() throws {} - public func plan(logger: ContextualLogger) throws -> IndexPlan { let excludedTestTargets = configuration.excludeTests ? testTargets : [] let collector = SourceFileCollector( - indexStorePaths: Set(configuration.indexStorePath), + indexStorePaths: Set(configuration.indexStorePath).union(indexstorePaths), excludedTestTargets: excludedTestTargets, logger: logger ) diff --git a/Sources/PeripheryKit/ProjectDriver.swift b/Sources/PeripheryKit/ProjectDriver.swift index 0b457b127..51ffab763 100644 --- a/Sources/PeripheryKit/ProjectDriver.swift +++ b/Sources/PeripheryKit/ProjectDriver.swift @@ -7,3 +7,11 @@ public protocol ProjectDriver { func build() throws func plan(logger: ContextualLogger) throws -> IndexPlan } + +extension ProjectDriver { + public func build() throws {} + + public func plan(logger: ContextualLogger) throws -> IndexPlan { + IndexPlan(sourceFiles: [:]) + } +} diff --git a/Sources/PeripheryKit/SPM/SPMProjectDriver.swift b/Sources/PeripheryKit/SPM/SPMProjectDriver.swift index 77e2e5222..21d752e7a 100644 --- a/Sources/PeripheryKit/SPM/SPMProjectDriver.swift +++ b/Sources/PeripheryKit/SPM/SPMProjectDriver.swift @@ -3,6 +3,7 @@ import Indexer import Shared import SwiftIndexStore import SystemPackage + public final class SPMProjectDriver { public static func build() throws -> Self { let configuration = Configuration.shared diff --git a/Sources/Shared/Configuration.swift b/Sources/Shared/Configuration.swift index 19146ce92..5c957a6a8 100644 --- a/Sources/Shared/Configuration.swift +++ b/Sources/Shared/Configuration.swift @@ -4,7 +4,7 @@ import SystemPackage import Yams public final class Configuration { - public static var defaultConfigurationFile = ".periphery.yml" + public static var defaultConfigurationFile = FilePath(".periphery.yml") public static let shared = Configuration() public init(logger: BaseLogger = .shared) { @@ -125,6 +125,9 @@ public final class Configuration { @Setting(key: "generic_project_config", defaultValue: nil, setter: filePathSetter) public var genericProjectConfig: FilePath? + @Setting(key: "bazel", defaultValue: false) + public var bazel: Bool + // Non user facing. public var guidedSetup: Bool = false @@ -147,9 +150,9 @@ public final class Configuration { return try Yams.dump(object: config) } - public func save() throws { + public func save(to path: FilePath = defaultConfigurationFile) throws { let data = try asYaml().data(using: .utf8) - FileManager.default.createFile(atPath: Self.defaultConfigurationFile, contents: data) + FileManager.default.createFile(atPath: path.string, contents: data) } public func load(from path: FilePath?) throws { @@ -220,7 +223,7 @@ public final class Configuration { // MARK: - Private - lazy var settings: [any AbstractSetting] = [$project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat, $retainPublic, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible, $retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis, $disableUnusedImportAnalysis, $externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $disableUpdateCheck, $strict, $indexStorePath, $skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults, $jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline, $genericProjectConfig] + lazy var settings: [any AbstractSetting] = [$project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat, $retainPublic, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible, $retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis, $disableUnusedImportAnalysis, $externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $disableUpdateCheck, $strict, $indexStorePath, $skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults, $jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline, $genericProjectConfig, $bazel] private func buildFilenameMatchers(with patterns: [String]) -> [FilenameMatcher] { // TODO: respect filesystem case sensitivity. @@ -237,7 +240,7 @@ public final class Configuration { return path } - return [FilePath(Self.defaultConfigurationFile), FilePath(".periphery.yaml")].first { $0.exists } + return [Self.defaultConfigurationFile, FilePath(".periphery.yaml")].first { $0.exists } } } diff --git a/Sources/Shared/Logger.swift b/Sources/Shared/Logger.swift index 240086e36..c32f0476c 100644 --- a/Sources/Shared/Logger.swift +++ b/Sources/Shared/Logger.swift @@ -57,7 +57,10 @@ public final class BaseLogger { } @inlinable - func warn(_ text: String) { + func warn(_ text: String, newlinePrefix: Bool = false) { + if newlinePrefix { + log("", output: stderr) + } let text = colorize("warning: ", .boldYellow) + text log(text, output: stderr) } @@ -118,9 +121,9 @@ public final class Logger { } @inlinable - public func warn(_ text: String) { + public func warn(_ text: String, newlinePrefix: Bool = false) { guard !configuration.quiet else { return } - baseLogger.warn(text) + baseLogger.warn(text, newlinePrefix: newlinePrefix) } @inlinable diff --git a/Sources/Shared/PeripheryError.swift b/Sources/Shared/PeripheryError.swift index 4f2dcc592..7f0a4c256 100644 --- a/Sources/Shared/PeripheryError.swift +++ b/Sources/Shared/PeripheryError.swift @@ -2,7 +2,7 @@ import Foundation import SystemPackage public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { - case shellCommandFailed(cmd: String, args: [String], status: Int32, output: String) + case shellCommandFailed(cmd: [String], status: Int32, output: String) case shellOutputEncodingFailed(cmd: String, args: [String], encoding: String.Encoding) case usageError(String) case underlyingError(Error) @@ -21,9 +21,9 @@ public enum PeripheryError: Error, LocalizedError, CustomStringConvertible { public var errorDescription: String? { switch self { - case let .shellCommandFailed(cmd, args, status, output): - let joinedArgs = args.joined(separator: " ") - return "Shell command '\(cmd) \(joinedArgs)' returned exit status '\(status)':\n\(output)" + case let .shellCommandFailed(cmd, status, output): + let joinedCmd = cmd.joined(separator: " ") + return "Shell command '\(joinedCmd)' returned exit status '\(status)':\n\(output)" case let .shellOutputEncodingFailed(cmd, args, encoding): let joinedArgs = args.joined(separator: " ") return "Shell command '\(cmd) \(joinedArgs)' output encoding to \(encoding) failed." diff --git a/Sources/Shared/ProjectKind.swift b/Sources/Shared/ProjectKind.swift index bf4df0007..cabb6d456 100644 --- a/Sources/Shared/ProjectKind.swift +++ b/Sources/Shared/ProjectKind.swift @@ -4,5 +4,6 @@ import SystemPackage public enum ProjectKind { case xcode(projectPath: FilePath) case spm + case bazel case generic(genericProjectConfig: FilePath) } diff --git a/Sources/Shared/Shell.swift b/Sources/Shared/Shell.swift index 778439e06..058f2b256 100644 --- a/Sources/Shared/Shell.swift +++ b/Sources/Shared/Shell.swift @@ -1,49 +1,5 @@ import Foundation -final class ReadableStream { - private let cmd: String - private let args: [String] - private let fileHandle: FileHandle - private let task: Process - private let encoding: String.Encoding - private var output = "" - private var didExit = false - - init(cmd: String, args: [String], fileHandle: FileHandle, task: Process, encoding: String.Encoding = .utf8) { - self.cmd = cmd - self.args = args - self.fileHandle = fileHandle - self.task = task - self.encoding = encoding - } - - func terminationStatus() throws -> Int32 { - try waitUntilExit() - return task.terminationStatus - } - - func allOutput() throws -> String { - try waitUntilExit() - return output - } - - // MARK: - Private - - private func waitUntilExit() throws { - guard !didExit else { return } - - let data = fileHandle.readDataToEndOfFile() - task.waitUntilExit() - - guard let result = String(data: data, encoding: encoding) else { - throw PeripheryError.shellOutputEncodingFailed(cmd: cmd, args: args, encoding: encoding) - } - - output += result - didExit = true - } -} - open class Shell { public static let shared: Shell = { Shell(environment: ProcessInfo.processInfo.environment, logger: Logger()) @@ -61,12 +17,17 @@ open class Shell { } public func interruptRunning() { - tasksQueue.sync { tasks.forEach { $0.interrupt() } } + tasksQueue.sync { + tasks.forEach { + $0.interrupt() + $0.waitUntilExit() + } + } } lazy var pristineEnvironment: [String: String] = { let shell = environment["SHELL"] ?? "/bin/bash" - guard let pristineEnv = try? exec([shell, "-lc", "env"], stderr: false, environment: [:]) else { + guard let pristineEnv = try? exec([shell, "-lc", "env"], environment: [:]).1 else { return environment } @@ -79,7 +40,7 @@ open class Shell { result[pair.0] = pair.1 } - let preservedKeys = ["PATH", "DEVELOPER_DIR"] + let preservedKeys = ["TERM", "PATH", "DEVELOPER_DIR", "SSH_AUTH_SOCK"] preservedKeys.forEach { key in if let value = environment[key] { newEnv[key] = value @@ -90,14 +51,42 @@ open class Shell { }() @discardableResult - open func exec(_ args: [String], stderr: Bool = true) throws -> String { + open func exec( + _ args: [String], + stderr: Bool = true + ) throws -> String { let env = pristineEnvironment - return try exec(args, stderr: stderr, environment: env) + let (status, output) = try exec(args, stderr: stderr, environment: env) + + if status == 0 { + return output + } + + throw PeripheryError.shellCommandFailed( + cmd: args, + status: status, + output: output + ) } + @discardableResult + open func execStatus( + _ args: [String], + stderr: Bool = true + ) throws -> Int32 { + let env = pristineEnvironment + let (status, _) = try exec(args, stderr: stderr, captureOutput: false, environment: env) + return status + } + // MARK: - Private - private func exec(_ args: [String], stderr: Bool, environment: [String: String]) throws -> String { + private func exec( + _ args: [String], + stderr: Bool = false, + captureOutput: Bool = true, + environment: [String: String] + ) throws -> (Int32, String) { let launchPath: String let newArgs: [String] @@ -113,35 +102,38 @@ open class Shell { task.launchPath = launchPath task.environment = environment task.arguments = newArgs - - let pipe = Pipe() - task.standardOutput = pipe - task.standardError = stderr ? pipe : nil - + logger.debug("\(launchPath) \(newArgs.joined(separator: " "))") tasksQueue.sync { _ = tasks.insert(task) } - task.launch() + var outputPipe: Pipe? - let readable = ReadableStream( - cmd: launchPath, - args: newArgs, - fileHandle: pipe.fileHandleForReading, - task: task) - - let status = try readable.terminationStatus() - let output = try readable.allOutput() + if captureOutput { + outputPipe = Pipe() + task.standardOutput = outputPipe + task.standardError = stderr ? outputPipe : nil + } - tasksQueue.sync { _ = tasks.remove(task) } + task.launch() + task.waitUntilExit() - if status == 0 { - return output + let status = task.terminationStatus + var output: String = "" + + if let outputPipe { + let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + guard let str = String(data: outputData, encoding: .utf8) else { + tasksQueue.sync { _ = tasks.remove(task) } + throw PeripheryError.shellOutputEncodingFailed( + cmd: launchPath, + args: newArgs, + encoding: .utf8 + ) + } + output = str } - throw PeripheryError.shellCommandFailed( - cmd: launchPath, - args: newArgs, - status: status, - output: output) + tasksQueue.sync { _ = tasks.remove(task) } + return (status, output) } } diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel new file mode 100644 index 000000000..8b4fc0750 --- /dev/null +++ b/bazel/BUILD.bazel @@ -0,0 +1,13 @@ +package_group( + name = "generated", + includes = [ + "@periphery_generated//:package_group" + ], +) + +package_group( + name = "package_group", + packages = ["//..."], +) + +alias(actual = "@periphery_generated//rule:scan", name = "scan") diff --git a/bazel/extensions.bzl b/bazel/extensions.bzl new file mode 100644 index 000000000..ce171b36f --- /dev/null +++ b/bazel/extensions.bzl @@ -0,0 +1,22 @@ +def _generated_repo_impl(repository_ctx): + repository_ctx.file( + "BUILD.bazel", + content = """ +package_group( + name = "package_group", + packages = ["//..."], +) +""", + ) + + # TODO: Scope by project? + repository_ctx.symlink( + "/var/tmp/periphery_bazel/BUILD.bazel", + "rule/BUILD.bazel", + ) + +generated_repo = repository_rule( + implementation = _generated_repo_impl, +) + +generated = module_extension(implementation = lambda _: generated_repo(name = "periphery_generated")) \ No newline at end of file diff --git a/bazel/internal/BUILD.bazel b/bazel/internal/BUILD.bazel new file mode 100644 index 000000000..d77caa605 --- /dev/null +++ b/bazel/internal/BUILD.bazel @@ -0,0 +1,3 @@ +exports_files([ + "scan_template.sh" +]) \ No newline at end of file diff --git a/bazel/internal/scan.bzl b/bazel/internal/scan.bzl new file mode 100644 index 000000000..e229f81ac --- /dev/null +++ b/bazel/internal/scan.bzl @@ -0,0 +1,152 @@ +load("@bazel_skylib//lib:sets.bzl", "sets") +load("@rules_swift//swift:providers.bzl", "SwiftInfo") + +PeripheryInfo = provider( + doc = "TODO", + fields = { + "indexstores": "TODO", + "plists": "TODO", + "test_targets": "TODO", + }, +) + +def _force_indexstore_impl(settings, _attr): + return { + "//command_line_option:features": settings["//command_line_option:features"] + [ + "swift.index_while_building", + ], + } + +_force_indexstore = transition( + implementation = _force_indexstore_impl, + inputs = [ + "//command_line_option:features", + ], + outputs = [ + "//command_line_option:features", + ], +) + +def _get_template_substitutions(*, periphery_binary, config_path, generic_project_config_path): + """Returns the template substitutions for this executable.""" + subs = { + "periphery_binary": periphery_binary, + "config_path": config_path, + "generic_project_config_path": generic_project_config_path, + } + return {"%(" + k + ")s": subs[k] for k in subs} + +def _scan_inputs_aspect_impl(target, ctx): + direct_indexstores = [] + test_targets = [] + plists = [] + + if hasattr(ctx.rule.files, "data"): + # TODO: Generated plists. + plists.extend([file for file in ctx.rule.files.data if file.extension == "plist"]) + + # and not target.label.workspace_name + if SwiftInfo in target and hasattr(target[SwiftInfo], "direct_modules"): + for module in target[SwiftInfo].direct_modules: + if hasattr(module, "swift"): + if ctx.rule.attr.testonly: + test_targets.append(module.name) + + if hasattr(module.swift, "indexstore") and module.swift.indexstore: + direct_indexstores.append(module.swift.indexstore) + + deps = getattr(ctx.rule.attr, "deps", []) + + indexstores_depset = depset( + direct = direct_indexstores, + transitive = [dep[PeripheryInfo].indexstores for dep in deps], + ) + test_targets_depset = depset( + direct = test_targets, + transitive = [dep[PeripheryInfo].test_targets for dep in deps], + ) + plists_depset = depset( + direct = plists, + transitive = [dep[PeripheryInfo].plists for dep in deps], + ) + + return [ + PeripheryInfo( + indexstores = indexstores_depset, + plists = plists_depset, + test_targets = test_targets_depset, + ), + ] + +def _scan_impl(ctx): + indexstores_set = sets.make() + plists_set = sets.make() + test_targets_set = sets.make() + + for dep in ctx.attr.deps: + indexstores_set = sets.union(indexstores_set, sets.make(dep[PeripheryInfo].indexstores.to_list())) + plists_set = sets.union(plists_set, sets.make(dep[PeripheryInfo].plists.to_list())) + test_targets_set = sets.union(test_targets_set, sets.make(dep[PeripheryInfo].test_targets.to_list())) + + indexstores = sets.to_list(indexstores_set) + plists = sets.to_list(plists_set) + test_targets = sets.to_list(test_targets_set) + + generic_project_config_struct = struct( + indexstores = [store.path for store in indexstores], + plists = [plist.path for plist in plists], + test_targets = test_targets, + ) + + generic_project_config_struct_json = json.encode_indent(generic_project_config_struct) + generic_project_config_struct_file = ctx.actions.declare_file("generic_project_config.json") + ctx.actions.write(generic_project_config_struct_file, generic_project_config_struct_json) + + ctx.actions.expand_template( + template = ctx.file._template, + output = ctx.outputs.scan, + substitutions = _get_template_substitutions( + periphery_binary = ctx.attr.periphery_binary, + config_path = ctx.attr.config, + generic_project_config_path = generic_project_config_struct_file.path + ), + ) + + runfiles = ctx.runfiles( + files = [generic_project_config_struct_file] + indexstores + plists, + ) + + return DefaultInfo( + executable = ctx.outputs.scan, + files = depset( + [ctx.outputs.scan, generic_project_config_struct_file], + ), + runfiles = runfiles + ) + +scan_inputs_aspect = aspect( + _scan_inputs_aspect_impl, + attr_aspects = ["deps"], +) + +scan = rule( + attrs = { + "deps": attr.label_list( + cfg = _force_indexstore, + mandatory = True, + aspects = [scan_inputs_aspect] + ), + "config": attr.string(), + "periphery_binary": attr.string(), + "_template": attr.label( + allow_single_file = True, + default = "@periphery//bazel/internal:scan_template.sh", + ), + }, + outputs = { + "generic_project_config": "generic_project_config.json", + "scan": "scan.sh", + }, + implementation = _scan_impl, + executable = True, +) diff --git a/bazel/internal/scan_template.sh b/bazel/internal/scan_template.sh new file mode 100644 index 000000000..a9d420692 --- /dev/null +++ b/bazel/internal/scan_template.sh @@ -0,0 +1,2 @@ +cd $BUILD_WORKSPACE_DIRECTORY +%(periphery_binary)s scan --config "%(config_path)s" --generic-project-config "%(generic_project_config_path)s"