From a1cab7e995e059f25244d93e33be5e730b3e569e Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Thu, 8 Aug 2024 14:31:18 -0400 Subject: [PATCH 1/3] Adds a rule that can be used out of the box to use Periphery in bazel projects Using the example provided here you are able to generate periphery reports for any collection of swift based targets. To run the example run `bazel build //periphery/example:periphery_xcode_report` It is important to note the flags specified in the .bazelrc file, they are critical to periphery working via bazel --- .bazelrc | 3 + .bazelversion | 1 + .gitignore | 4 +- WORKSPACE | 40 +++++++ periphery/BUILD | 0 periphery/collect_periphery_info.bzl | 64 ++++++++++++ periphery/example/BUILD | 13 +++ periphery/example/src/BUILD | 12 +++ periphery/example/src/periphery_test.swift | 28 +++++ periphery/periphery_report.bzl | 115 +++++++++++++++++++++ 10 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 WORKSPACE create mode 100644 periphery/BUILD create mode 100644 periphery/collect_periphery_info.bzl create mode 100644 periphery/example/BUILD create mode 100644 periphery/example/src/BUILD create mode 100644 periphery/example/src/periphery_test.swift create mode 100644 periphery/periphery_report.bzl diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000000..cb3d2eb7af --- /dev/null +++ b/.bazelrc @@ -0,0 +1,3 @@ +common --noenable_bzlmod +build --features=swift.index_while_building +build --swiftcopt=-whole-module-optimization diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000000..19b860c187 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +6.4.0 diff --git a/.gitignore b/.gitignore index 4b986841a0..bc79b5f83e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ DerivedData .swiftpm # VSCode -.vscode/* \ No newline at end of file +.vscode/* + +bazel-* diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000000..00c44603d4 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,40 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "build_bazel_rules_swift", + sha256 = "9919ed1d8dae509645bfd380537ae6501528d8de971caebed6d5185b9970dc4d", + url = "https://github.com/bazelbuild/rules_swift/releases/download/2.1.1/rules_swift.2.1.1.tar.gz", +) + +load( + "@build_bazel_rules_swift//swift:repositories.bzl", + "swift_rules_dependencies", +) + +swift_rules_dependencies() + +load( + "@build_bazel_rules_swift//swift:extras.bzl", + "swift_rules_extra_dependencies", +) + +swift_rules_extra_dependencies() + +PERIPHERY_VERSION = "2.21.0" + +http_archive( + name = "com_github_peripheryapp_periphery", + build_file_content = """ +load("@bazel_skylib//rules:native_binary.bzl", "native_binary") + +native_binary( +name = "periphery_tool", +src = "periphery", +out = "periphery_tool-bazel", +visibility = ["//visibility:public"], +) + """, + sha256 = "7ea2b48e3444609c83dd642a8eeff7240316bf081d331e4ca91eeedb439c0668", + type = "zip", + url = "https://github.com/peripheryapp/periphery/releases/download/{version}/periphery-{version}.zip".format(version = PERIPHERY_VERSION), +) diff --git a/periphery/BUILD b/periphery/BUILD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/periphery/collect_periphery_info.bzl b/periphery/collect_periphery_info.bzl new file mode 100644 index 0000000000..6e3b08d7a6 --- /dev/null +++ b/periphery/collect_periphery_info.bzl @@ -0,0 +1,64 @@ +load("@build_bazel_rules_swift//swift:providers.bzl", "SwiftInfo") + +PeripheryInfo = provider( + doc = "Provides indexstore information for a target's recursive dependencies.", + fields = { + "periphery_file_target_mapping": "A File listing the sources of a target and paths to their respective indexstores.", + "periphery_indexstore": "A File representing the indexstore of a target.", + "srcs": "A list of sources for a target.", + }, +) + +def _collect_periphery_info_aspect_imp(target, ctx): + periphery_file_target_mapping = [] + periphery_indexstore = [] + srcs = [] + + # Only act on SwiftInfo targets that are not testonly, and exist in the current workspace + if SwiftInfo in target and not ctx.rule.attr.testonly and not target.label.workspace_name and hasattr(target[SwiftInfo], "direct_modules"): + for module in target[SwiftInfo].direct_modules: + if hasattr(module, "swift") and hasattr(module.swift, "indexstore") and module.swift.indexstore: + periphery_file_target_mappings = ctx.actions.declare_file("{}_periphery_file_target_mappings.json".format(module.name)) + swift_srcs = [src for src in module.compilation_context.direct_sources if src.extension == "swift" and src.is_source] + ctx.actions.write( + output = periphery_file_target_mappings, + content = json.encode(_create_file_target_info(swift_srcs, module.name)), + ) + periphery_file_target_mapping.append(periphery_file_target_mappings) + periphery_indexstore.append(module.swift.indexstore) + srcs.extend(swift_srcs) + periphery_file_target_mapping_depset = depset( + direct = periphery_file_target_mapping, + transitive = [dep[PeripheryInfo].periphery_file_target_mapping for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + periphery_indexstore_depset = depset( + direct = periphery_indexstore, + transitive = [dep[PeripheryInfo].periphery_indexstore for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + srcs_depset = depset( + direct = srcs, + transitive = [dep[PeripheryInfo].srcs for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + return [ + PeripheryInfo( + periphery_file_target_mapping = periphery_file_target_mapping_depset, + periphery_indexstore = periphery_indexstore_depset, + srcs = srcs_depset, + ), + OutputGroupInfo( + periphery_file_target_mapping = periphery_file_target_mapping_depset, + periphery_indexstore = periphery_indexstore_depset, + srcs = srcs_depset, + ), + ] + +def _create_file_target_info(srcs, module_name): + info = {} + for src in srcs: + info[src.path] = [module_name] + return {"file_targets": info} + +collect_periphery_info_aspect = aspect( + implementation = _collect_periphery_info_aspect_imp, + attr_aspects = ["deps"], +) diff --git a/periphery/example/BUILD b/periphery/example/BUILD new file mode 100644 index 0000000000..0c4b0b8fd3 --- /dev/null +++ b/periphery/example/BUILD @@ -0,0 +1,13 @@ +load( + "//periphery:periphery_report.bzl", + "periphery_report", +) + +periphery_report( + name = "periphery_xcode_report", + format = "xcode", + periphery_tool = "@com_github_peripheryapp_periphery//:periphery_tool", + deps = [ + "//periphery/example/src:ReportTest", + ], +) diff --git a/periphery/example/src/BUILD b/periphery/example/src/BUILD new file mode 100644 index 0000000000..4c4d63b0b6 --- /dev/null +++ b/periphery/example/src/BUILD @@ -0,0 +1,12 @@ +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +swift_library( + name = "ReportTest", + srcs = [ + "periphery_test.swift", + ], + visibility = ["//periphery/example:__pkg__"], +) diff --git a/periphery/example/src/periphery_test.swift b/periphery/example/src/periphery_test.swift new file mode 100644 index 0000000000..5ec2dc58a1 --- /dev/null +++ b/periphery/example/src/periphery_test.swift @@ -0,0 +1,28 @@ +import Foundation + +protocol FixtureProtocol83: AnyObject { + func protocolMethod() +} + +extension FixtureProtocol83 { + func protocolMethod() {} +} + +class FixtureClass83: FixtureProtocol83 {} + +class FixtureClass84: FixtureClass83 { + func protocolMethod() {} +} + +public class FixtureClass85 { + private let cls: FixtureClass84 + weak var delegate: FixtureProtocol83? + + init() { + cls = FixtureClass84() + } + + public func someMethod() { + delegate?.protocolMethod() + } +} diff --git a/periphery/periphery_report.bzl b/periphery/periphery_report.bzl new file mode 100644 index 0000000000..03345655bf --- /dev/null +++ b/periphery/periphery_report.bzl @@ -0,0 +1,115 @@ +load( + ":collect_periphery_info.bzl", + "PeripheryInfo", + "collect_periphery_info_aspect", +) + +PeripheryReportInfo = provider( + doc = "Provides periphery report information for usage by other targets.", + fields = { + "report": "A File containing a periphery report.", + }, +) + +def _periphery_report_impl(ctx): + periphery_file_inputs = _collect_file_inputs(ctx) + args = ctx.actions.args() + args.add_all([ + "--format=%s" % ctx.attr.format, + "--skip-build", + "--relative-results", + "--quiet", + ] + ctx.attr.periphery_additonal_args) + if ctx.attr.report_exclude_globs: + args.add("--report-exclude") + for glob in ctx.attr.report_exclude_globs: + args.add(glob) + args.add_all("--file-targets-path", periphery_file_inputs.periphery_file_target_mapping_files) + args.add_all("--index-store-path", periphery_file_inputs.periphery_indexstore_files, expand_directories = False) + extension = ctx.attr.format if ctx.attr.format == "json" else "txt" + output_file = ctx.actions.declare_file(ctx.label.name + "_periphery_report.%s" % extension) + ctx.actions.run_shell( + tools = [ + ctx.executable.periphery_tool, + ] + periphery_file_inputs.runfiles.files.to_list(), + arguments = [args], + outputs = [output_file], + command = "{executable} scan $@ > {output_path}".format( + executable = ctx.executable.periphery_tool.path, + output_path = output_file.path, + ), + mnemonic = "GeneratePeripheryReport", + ) + return [ + DefaultInfo( + files = depset([output_file]), + runfiles = periphery_file_inputs.runfiles, + ), + PeripheryReportInfo( + report = output_file, + ), + ] + +def _collect_file_inputs(ctx): + runfiles = ctx.runfiles( + files = [ + ctx.executable.periphery_tool, + ], + ) + periphery_file_target_mapping_files = [] + periphery_indexstore_files = [] + srcs_files = [] + for dep in ctx.attr.deps: + dep_runfiles = [] + periphery_file_target_mapping_runfiles = ctx.runfiles(transitive_files = dep[PeripheryInfo].periphery_file_target_mapping) + periphery_file_target_mapping_files.extend(dep[PeripheryInfo].periphery_file_target_mapping.to_list()) + dep_runfiles.append(periphery_file_target_mapping_runfiles) + periphery_indexstore_paths_depset = ctx.runfiles(transitive_files = dep[PeripheryInfo].periphery_indexstore) + periphery_indexstore_files.extend(dep[PeripheryInfo].periphery_indexstore.to_list()) + dep_runfiles.append(periphery_indexstore_paths_depset) + srcs_depset = ctx.runfiles(transitive_files = dep[PeripheryInfo].srcs) + srcs_files.extend(dep[PeripheryInfo].srcs.to_list()) + dep_runfiles.append(srcs_depset) + for dep_runfile in dep_runfiles: + runfiles = runfiles.merge(dep_runfile) + return struct( + runfiles = runfiles, + periphery_file_target_mapping_files = periphery_file_target_mapping_files, + periphery_indexstore_files = periphery_indexstore_files, + srcs_files = srcs_files, + ) + +periphery_report = rule( + implementation = _periphery_report_impl, + doc = "Creates a periphery report for the given targets.", + attrs = { + "deps": attr.label_list( + aspects = [collect_periphery_info_aspect], + doc = "The targets to generate a periphery report from.", + ), + "report_exclude_globs": attr.string_list( + default = [], + doc = "A list of file globs to exclude from the report.", + ), + "periphery_additonal_args": attr.string_list( + default = [ + "--retain-objc-accessible", + "--retain-assign-only-properties", + ], + doc = "A list additional arguments to pass to the periperhy invocation.", + ), + "format": attr.string( + default = "xcode", + values = [ + "xcode", + "json", + ], + doc = "The output format to use.", + ), + "periphery_tool": attr.label( + doc = "The periphery tool to use.", + executable = True, + cfg = "exec", + ), + }, +) From 5386896654209c1daf3abcede96df18502c47df4 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Mon, 12 Aug 2024 10:18:32 -0400 Subject: [PATCH 2/3] more feedback --- .bazelrc | 1 - periphery/periphery_report.bzl | 46 +++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.bazelrc b/.bazelrc index cb3d2eb7af..9436d1342b 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,3 +1,2 @@ common --noenable_bzlmod -build --features=swift.index_while_building build --swiftcopt=-whole-module-optimization diff --git a/periphery/periphery_report.bzl b/periphery/periphery_report.bzl index 03345655bf..b7ca6f2a48 100644 --- a/periphery/periphery_report.bzl +++ b/periphery/periphery_report.bzl @@ -4,6 +4,23 @@ load( "collect_periphery_info_aspect", ) +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", + ], +) + PeripheryReportInfo = provider( doc = "Provides periphery report information for usage by other targets.", fields = { @@ -24,13 +41,26 @@ def _periphery_report_impl(ctx): args.add("--report-exclude") for glob in ctx.attr.report_exclude_globs: args.add(glob) - args.add_all("--file-targets-path", periphery_file_inputs.periphery_file_target_mapping_files) - args.add_all("--index-store-path", periphery_file_inputs.periphery_indexstore_files, expand_directories = False) + config_file_output = ctx.actions.declare_file("periphery_config.yml") + ctx.actions.write( + output = config_file_output, + content = """ +file_targets_path: +- {file_targets_paths} +index_store_path: +- {index_store_paths} +""".format( + file_targets_paths = "\n- ".join([f.path for f in periphery_file_inputs.periphery_file_target_mapping_files]), + index_store_paths = "\n- ".join([f.path for f in periphery_file_inputs.periphery_indexstore_files]), + ), + ) + args.add_all(["--config", config_file_output.path]) extension = ctx.attr.format if ctx.attr.format == "json" else "txt" output_file = ctx.actions.declare_file(ctx.label.name + "_periphery_report.%s" % extension) ctx.actions.run_shell( tools = [ ctx.executable.periphery_tool, + config_file_output, ] + periphery_file_inputs.runfiles.files.to_list(), arguments = [args], outputs = [output_file], @@ -84,6 +114,7 @@ periphery_report = rule( doc = "Creates a periphery report for the given targets.", attrs = { "deps": attr.label_list( + cfg = _force_indexstore, aspects = [collect_periphery_info_aspect], doc = "The targets to generate a periphery report from.", ), @@ -92,10 +123,6 @@ periphery_report = rule( doc = "A list of file globs to exclude from the report.", ), "periphery_additonal_args": attr.string_list( - default = [ - "--retain-objc-accessible", - "--retain-assign-only-properties", - ], doc = "A list additional arguments to pass to the periperhy invocation.", ), "format": attr.string( @@ -103,6 +130,10 @@ periphery_report = rule( values = [ "xcode", "json", + "csv", + "checkstyle", + "codeclimate", + "github-actions", ], doc = "The output format to use.", ), @@ -111,5 +142,8 @@ periphery_report = rule( executable = True, cfg = "exec", ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), }, ) From 9e63be662ff36b05b2f169d46ec959898ba7a448 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Tue, 13 Aug 2024 11:48:15 -0400 Subject: [PATCH 3/3] collect plists as well --- periphery/collect_periphery_info.bzl | 5 +++-- periphery/example/src/BUILD | 3 +++ periphery/example/src/Info.plist | 8 ++++++++ periphery/example/src/periphery_test.swift | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 periphery/example/src/Info.plist diff --git a/periphery/collect_periphery_info.bzl b/periphery/collect_periphery_info.bzl index 6e3b08d7a6..032a9cc3f4 100644 --- a/periphery/collect_periphery_info.bzl +++ b/periphery/collect_periphery_info.bzl @@ -20,13 +20,14 @@ def _collect_periphery_info_aspect_imp(target, ctx): if hasattr(module, "swift") and hasattr(module.swift, "indexstore") and module.swift.indexstore: periphery_file_target_mappings = ctx.actions.declare_file("{}_periphery_file_target_mappings.json".format(module.name)) swift_srcs = [src for src in module.compilation_context.direct_sources if src.extension == "swift" and src.is_source] + infoplist_srcs = [file for file in ctx.rule.files.data if file.extension == "plist"] ctx.actions.write( output = periphery_file_target_mappings, - content = json.encode(_create_file_target_info(swift_srcs, module.name)), + content = json.encode(_create_file_target_info(swift_srcs + infoplist_srcs, module.name)), ) periphery_file_target_mapping.append(periphery_file_target_mappings) periphery_indexstore.append(module.swift.indexstore) - srcs.extend(swift_srcs) + srcs.extend(swift_srcs + infoplist_srcs) periphery_file_target_mapping_depset = depset( direct = periphery_file_target_mapping, transitive = [dep[PeripheryInfo].periphery_file_target_mapping for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], diff --git a/periphery/example/src/BUILD b/periphery/example/src/BUILD index 4c4d63b0b6..a429636c77 100644 --- a/periphery/example/src/BUILD +++ b/periphery/example/src/BUILD @@ -8,5 +8,8 @@ swift_library( srcs = [ "periphery_test.swift", ], + data = [ + "Info.plist", + ], visibility = ["//periphery/example:__pkg__"], ) diff --git a/periphery/example/src/Info.plist b/periphery/example/src/Info.plist new file mode 100644 index 0000000000..934563a716 --- /dev/null +++ b/periphery/example/src/Info.plist @@ -0,0 +1,8 @@ + + + + + NSPrincipalClass + FixturePrincipleClass + + diff --git a/periphery/example/src/periphery_test.swift b/periphery/example/src/periphery_test.swift index 5ec2dc58a1..b8def76daa 100644 --- a/periphery/example/src/periphery_test.swift +++ b/periphery/example/src/periphery_test.swift @@ -26,3 +26,5 @@ public class FixtureClass85 { delegate?.protocolMethod() } } + +public class FixturePrincipleClass {}