Skip to content

Commit 99ccfd7

Browse files
MikeMcQuaidRylan12
andcommitted
formula_auditor: audit revision and compatibility_version
Add a new audit method to check if the `revision` and `compatibility_version` are consistent with each other. This ensures that when a formula's `compatibility_version` is increased, the `revision` of dependent formulas was also increased. Inversely, when a formula's `revision` is increased in the same PR as one of its dependencies, the `compatibility_version` of dependent formulae must be increased by 1. While we're here, DRY things up and improve some naming to me more readable/obvious. Co-authored-by: Rylan Polster <[email protected]>
1 parent f9553b6 commit 99ccfd7

File tree

2 files changed

+443
-162
lines changed

2 files changed

+443
-162
lines changed

Library/Homebrew/formula_auditor.rb

Lines changed: 162 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require "resource_auditor"
88
require "utils/shared_audits"
99
require "utils/output"
10+
require "utils/git"
1011

1112
module Homebrew
1213
# Auditor for checking common violations in {Formula}e.
@@ -39,8 +40,7 @@ def initialize(formula, options = {})
3940
@spdx_license_data = options[:spdx_license_data]
4041
@spdx_exception_data = options[:spdx_exception_data]
4142
@tap_audit = options[:tap_audit]
42-
@previous_committed = {}
43-
@newest_committed = {}
43+
@committed_version_info_cache = {}
4444
end
4545

4646
def audit_style
@@ -864,42 +864,114 @@ def audit_stable_version
864864
current_version = formula.stable.version
865865
current_version_scheme = formula.version_scheme
866866

867-
previous_committed, newest_committed = committed_version_info
867+
previous_version_info, origin_head_version_info = committed_version_info
868868

869-
if !newest_committed[:version].nil? &&
870-
current_version < newest_committed[:version] &&
871-
current_version_scheme == previous_committed[:version_scheme]
872-
problem "Stable: version should not decrease (from #{newest_committed[:version]} to #{current_version})"
869+
if (origin_head_version = origin_head_version_info[:version]) &&
870+
current_version < origin_head_version &&
871+
current_version_scheme == previous_version_info[:version_scheme]
872+
problem "Stable: version should not decrease (from #{origin_head_version} to #{current_version})"
873873
end
874874
end
875875

876876
def audit_revision
877877
new_formula_problem("New formulae should not define a revision.") if @new_formula && !formula.revision.zero?
878878

879879
return unless @git
880-
return unless formula.tap # skip formula not from core or any taps
881-
return unless formula.tap.git? # git log is required
880+
881+
tap = formula.tap
882+
return if tap.nil?
883+
return unless tap.git?
882884
return if formula.stable.blank?
883885

884886
current_version = formula.stable.version
885887
current_revision = formula.revision
886888

887-
previous_committed, newest_committed = committed_version_info
889+
previous_version_info, origin_head_version_info = committed_version_info
890+
891+
previous_version = previous_version_info[:version]
892+
previous_revision = previous_version_info[:revision]
893+
origin_head_version = origin_head_version_info[:version]
894+
origin_head_revision = origin_head_version_info[:revision]
888895

889-
if (previous_committed[:version] != newest_committed[:version] ||
890-
current_version != newest_committed[:version]) &&
891-
!current_revision.zero? &&
892-
current_revision == newest_committed[:revision] &&
893-
current_revision == previous_committed[:revision]
896+
if (previous_version != origin_head_version || current_version != origin_head_version) &&
897+
!current_revision.zero? && current_revision == origin_head_revision && current_revision == previous_revision
894898
problem "`revision #{current_revision}` should be removed"
895-
elsif current_version == previous_committed[:version] &&
896-
!previous_committed[:revision].nil? &&
897-
current_revision < previous_committed[:revision]
898-
problem "`revision` should not decrease (from #{previous_committed[:revision]} to #{current_revision})"
899-
elsif newest_committed[:revision] &&
900-
current_revision > (newest_committed[:revision] + 1)
899+
elsif current_version == previous_version && previous_revision && current_revision < previous_revision
900+
problem "`revision` should not decrease (from #{previous_revision} to #{current_revision})"
901+
elsif origin_head_revision && current_revision > (origin_head_revision + 1)
901902
problem "`revision` should only increment by 1"
902903
end
904+
905+
revision_increment = current_revision - previous_revision.to_i
906+
return if revision_increment != 1
907+
908+
dependency_names = formula.recursive_dependencies.map(&:name)
909+
return if dependency_names.empty?
910+
911+
changed_dependency_paths = changed_formulae_paths(tap, only_names: dependency_names)
912+
return if changed_dependency_paths.empty?
913+
914+
missing_compatibility_bumps = changed_dependency_paths.filter_map do |path|
915+
changed_formula = Formulary.factory(path)
916+
# Each changed dependency must raise its compatibility_version by exactly one.
917+
_, origin_head_dependency_version_info = committed_version_info(formula: changed_formula)
918+
previous_compatibility_version = origin_head_dependency_version_info[:compatibility_version] || 0
919+
current_compatibility_version = changed_formula.compatibility_version || 0
920+
next if current_compatibility_version == previous_compatibility_version + 1
921+
922+
"#{changed_formula.name} (#{previous_compatibility_version} to #{current_compatibility_version})"
923+
end
924+
return if missing_compatibility_bumps.empty?
925+
926+
problem "`revision` increased but recursive dependencies must increase `compatibility_version` by 1: " \
927+
"#{missing_compatibility_bumps.join(", ")}."
928+
end
929+
930+
def audit_compatibility_version
931+
return unless @git
932+
933+
tap = formula.tap
934+
return if tap.nil?
935+
return unless tap.git?
936+
937+
previous_version_info, origin_head_version_info = committed_version_info
938+
return if origin_head_version_info.empty?
939+
940+
previous_compatibility_version = previous_version_info[:compatibility_version] || 0
941+
current_compatibility_version = formula.compatibility_version || previous_compatibility_version
942+
943+
if current_compatibility_version < previous_compatibility_version
944+
problem "`compatibility_version` should not decrease " \
945+
"from #{previous_compatibility_version} to #{current_compatibility_version}"
946+
return
947+
elsif current_compatibility_version > (previous_compatibility_version + 1)
948+
problem "`compatibility_version` should only increment by 1"
949+
return
950+
end
951+
952+
compatibility_increment = current_compatibility_version - previous_compatibility_version
953+
return if compatibility_increment.zero?
954+
955+
dependent_revision_bumps = changed_formulae_paths(tap).filter_map do |path|
956+
changed_formula = Formulary.factory(path)
957+
next if changed_formula.name == formula.name
958+
959+
dependencies = changed_formula.recursive_dependencies.map(&:name)
960+
# Only formulae that depend (recursively) on the audited formula can justify the bump.
961+
next unless dependencies.include?(formula.name)
962+
963+
_, origin_head_dependent_version_info = committed_version_info(formula: changed_formula)
964+
previous_revision = origin_head_dependent_version_info[:revision] || 0
965+
current_revision = changed_formula.revision
966+
next if current_revision != previous_revision + 1
967+
968+
changed_formula.name
969+
end
970+
return if dependent_revision_bumps.any?
971+
972+
problem "`compatibility_version` increased from #{previous_compatibility_version} to " \
973+
"#{current_compatibility_version} but no recursive dependent formulae increased " \
974+
"`revision` by 1 in this PR."
903975
end
904976

905977
def audit_version_scheme
@@ -910,14 +982,13 @@ def audit_version_scheme
910982

911983
current_version_scheme = formula.version_scheme
912984

913-
previous_committed, = committed_version_info
985+
previous_version_info, = committed_version_info
986+
previous_version_scheme = previous_version_info[:version_scheme]
987+
return if previous_version_scheme.nil?
914988

915-
return if previous_committed[:version_scheme].nil?
916-
917-
if current_version_scheme < previous_committed[:version_scheme]
918-
problem "`version_scheme` should not decrease (from #{previous_committed[:version_scheme]} " \
919-
"to #{current_version_scheme})"
920-
elsif current_version_scheme > (previous_committed[:version_scheme] + 1)
989+
if current_version_scheme < previous_version_scheme
990+
problem "`version_scheme` should not decrease (from #{previous_version_scheme} to #{current_version_scheme})"
991+
elsif current_version_scheme > (previous_version_scheme + 1)
921992
problem "`version_scheme` should only increment by 1"
922993
end
923994
end
@@ -932,12 +1003,13 @@ def audit_unconfirmed_checksum_change
9321003
current_checksum = formula.stable.checksum
9331004
current_url = formula.stable.url
9341005

935-
_, newest_committed = committed_version_info
1006+
_, origin_head_version_info = committed_version_info
1007+
origin_head_checksum = origin_head_version_info[:checksum]
9361008

937-
if current_version == newest_committed[:version] &&
938-
current_url == newest_committed[:url] &&
939-
current_checksum != newest_committed[:checksum] &&
940-
current_checksum.present? && newest_committed[:checksum].present?
1009+
if current_version == origin_head_version_info[:version] &&
1010+
current_url == origin_head_version_info[:url] &&
1011+
current_checksum.present? && origin_head_checksum.present? &&
1012+
current_checksum != origin_head_checksum
9411013
problem(
9421014
"stable sha256 changed without the url/version also changing; " \
9431015
"please create an issue upstream to rule out malicious " \
@@ -1064,12 +1136,47 @@ def linux_only_gcc_dep?(formula)
10641136
true
10651137
end
10661138

1067-
def committed_version_info
1068-
return [] unless @git
1069-
return [] unless formula.tap # skip formula not from core or any taps
1070-
return [] unless formula.tap.git? # git log is required
1071-
return [] if formula.stable.blank?
1072-
return [@previous_committed, @newest_committed] if @previous_committed.present? || @newest_committed.present?
1139+
sig { params(tap: Tap, only_names: T::Array[String]).returns(T::Array[Pathname]) }
1140+
def changed_formulae_paths(tap, only_names: [].freeze)
1141+
return [] unless tap.git?
1142+
1143+
base_ref = "origin/HEAD"
1144+
changed_paths = Utils.safe_popen_read(Utils::Git.git, "-C", tap.path, "diff", "--name-only", base_ref)
1145+
.lines
1146+
.filter_map do |line|
1147+
relative_path = line.chomp
1148+
next unless relative_path.end_with?(".rb")
1149+
1150+
absolute_path = tap.path/relative_path
1151+
next unless absolute_path.exist?
1152+
next unless absolute_path.to_s.start_with?(tap.formula_dir.to_s)
1153+
1154+
absolute_path
1155+
end
1156+
return changed_paths if only_names.blank?
1157+
1158+
expected_paths = only_names.filter_map do |name|
1159+
relative_path = Pathname(name.to_s.delete_prefix("#{tap.name}/"))
1160+
relative_path = relative_path.sub_ext(".rb") if relative_path.extname.empty?
1161+
absolute_path = tap.formula_dir/relative_path
1162+
absolute_path.expand_path if absolute_path.exist?
1163+
end.map(&:to_s)
1164+
1165+
changed_paths.select { |path| expected_paths.include?(path.expand_path.to_s) }
1166+
end
1167+
1168+
sig { params(formula: Formula).returns([T::Hash[Symbol, T.untyped], T::Hash[Symbol, T.untyped]]) }
1169+
def committed_version_info(formula: @formula)
1170+
empty_result = [{}, {}]
1171+
return empty_result unless @git
1172+
return empty_result unless formula.tap # skip formula not from core or any taps
1173+
return empty_result unless formula.tap.git? # git log is required
1174+
return empty_result if formula.stable.blank?
1175+
1176+
return @committed_version_info_cache[formula.full_name] if @committed_version_info_cache.key?(formula.full_name)
1177+
1178+
previous_version_info = {}
1179+
origin_head_version_info = {}
10731180

10741181
current_version = formula.stable.version
10751182
current_revision = formula.revision
@@ -1081,28 +1188,30 @@ def committed_version_info
10811188
stable = f.stable
10821189
next if stable.blank?
10831190

1084-
@previous_committed[:version] = stable.version
1085-
@previous_committed[:checksum] = stable.checksum
1086-
@previous_committed[:version_scheme] = f.version_scheme
1087-
@previous_committed[:revision] = f.revision
1088-
1089-
@newest_committed[:version] ||= @previous_committed[:version]
1090-
@newest_committed[:checksum] ||= @previous_committed[:checksum]
1091-
@newest_committed[:revision] ||= @previous_committed[:revision]
1092-
@newest_committed[:url] ||= stable.url
1191+
previous_version_info[:version] = stable.version
1192+
previous_version_info[:checksum] = stable.checksum
1193+
previous_version_info[:revision] = f.revision
1194+
previous_version_info[:version_scheme] = f.version_scheme
1195+
previous_version_info[:compatibility_version] = f.compatibility_version
1196+
1197+
origin_head_version_info[:url] ||= stable.url
1198+
origin_head_version_info[:version] ||= previous_version_info[:version]
1199+
origin_head_version_info[:checksum] ||= previous_version_info[:checksum]
1200+
origin_head_version_info[:revision] ||= previous_version_info[:revision]
1201+
origin_head_version_info[:compatibility_version] ||= previous_version_info[:compatibility_version]
10931202
end
10941203
rescue MacOSVersion::Error
10951204
break
10961205
end
10971206

1098-
break if @previous_committed[:version] && current_version != @previous_committed[:version]
1099-
break if @previous_committed[:revision] && current_revision != @previous_committed[:revision]
1207+
break if previous_version_info[:version] && current_version != previous_version_info[:version]
1208+
break if previous_version_info[:revision] && current_revision != previous_version_info[:revision]
11001209
end
11011210

1102-
@previous_committed.compact!
1103-
@newest_committed.compact!
1211+
previous_version_info.compact!
1212+
origin_head_version_info.compact!
11041213

1105-
[@previous_committed, @newest_committed]
1214+
@committed_version_info_cache[formula.full_name] = [previous_version_info, origin_head_version_info]
11061215
end
11071216
end
11081217
end

0 commit comments

Comments
 (0)