Skip to content

Commit 2e994a9

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 4321334 commit 2e994a9

File tree

2 files changed

+440
-162
lines changed

2 files changed

+440
-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
@@ -858,42 +858,114 @@ def audit_stable_version
858858
current_version = formula.stable.version
859859
current_version_scheme = formula.version_scheme
860860

861-
previous_committed, newest_committed = committed_version_info
861+
previous_version_info, origin_head_version_info = committed_version_info
862862

863-
if !newest_committed[:version].nil? &&
864-
current_version < newest_committed[:version] &&
865-
current_version_scheme == previous_committed[:version_scheme]
866-
problem "Stable: version should not decrease (from #{newest_committed[:version]} to #{current_version})"
863+
if (origin_head_version = origin_head_version_info[:version]) &&
864+
current_version < origin_head_version &&
865+
current_version_scheme == previous_version_info[:version_scheme]
866+
problem "Stable: version should not decrease (from #{origin_head_version} to #{current_version})"
867867
end
868868
end
869869

870870
def audit_revision
871871
new_formula_problem("New formulae should not define a revision.") if @new_formula && !formula.revision.zero?
872872

873873
return unless @git
874-
return unless formula.tap # skip formula not from core or any taps
875-
return unless formula.tap.git? # git log is required
874+
875+
tap = formula.tap
876+
return if tap.nil?
877+
return unless tap.git?
876878
return if formula.stable.blank?
877879

878880
current_version = formula.stable.version
879881
current_revision = formula.revision
880882

881-
previous_committed, newest_committed = committed_version_info
883+
previous_version_info, origin_head_version_info = committed_version_info
884+
885+
previous_version = previous_version_info[:version]
886+
previous_revision = previous_version_info[:revision]
887+
origin_head_version = origin_head_version_info[:version]
888+
origin_head_revision = origin_head_version_info[:revision]
882889

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

899971
def audit_version_scheme
@@ -904,14 +976,13 @@ def audit_version_scheme
904976

905977
current_version_scheme = formula.version_scheme
906978

907-
previous_committed, = committed_version_info
979+
_, origin_head_version_info = committed_version_info
980+
previous_version_scheme = origin_head_version_info[:version_scheme]
981+
return if previous_version_scheme.nil?
908982

909-
return if previous_committed[:version_scheme].nil?
910-
911-
if current_version_scheme < previous_committed[:version_scheme]
912-
problem "`version_scheme` should not decrease (from #{previous_committed[:version_scheme]} " \
913-
"to #{current_version_scheme})"
914-
elsif current_version_scheme > (previous_committed[:version_scheme] + 1)
983+
if current_version_scheme < previous_version_scheme
984+
problem "`version_scheme` should not decrease (from #{previous_version_scheme} to #{current_version_scheme})"
985+
elsif current_version_scheme > (previous_version_scheme + 1)
915986
problem "`version_scheme` should only increment by 1"
916987
end
917988
end
@@ -926,12 +997,13 @@ def audit_unconfirmed_checksum_change
926997
current_checksum = formula.stable.checksum
927998
current_url = formula.stable.url
928999

929-
_, newest_committed = committed_version_info
1000+
_, origin_head_version_info = committed_version_info
1001+
origin_head_checksum = origin_head_version_info[:checksum]
9301002

931-
if current_version == newest_committed[:version] &&
932-
current_url == newest_committed[:url] &&
933-
current_checksum != newest_committed[:checksum] &&
934-
current_checksum.present? && newest_committed[:checksum].present?
1003+
if current_version == origin_head_version_info[:version] &&
1004+
current_url == origin_head_version_info[:url] &&
1005+
current_checksum.present? && origin_head_checksum.present? &&
1006+
current_checksum != origin_head_checksum
9351007
problem(
9361008
"stable sha256 changed without the url/version also changing; " \
9371009
"please create an issue upstream to rule out malicious " \
@@ -1058,12 +1130,47 @@ def linux_only_gcc_dep?(formula)
10581130
true
10591131
end
10601132

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

10681175
current_version = formula.stable.version
10691176
current_revision = formula.revision
@@ -1075,28 +1182,30 @@ def committed_version_info
10751182
stable = f.stable
10761183
next if stable.blank?
10771184

1078-
@previous_committed[:version] = stable.version
1079-
@previous_committed[:checksum] = stable.checksum
1080-
@previous_committed[:version_scheme] = f.version_scheme
1081-
@previous_committed[:revision] = f.revision
1082-
1083-
@newest_committed[:version] ||= @previous_committed[:version]
1084-
@newest_committed[:checksum] ||= @previous_committed[:checksum]
1085-
@newest_committed[:revision] ||= @previous_committed[:revision]
1086-
@newest_committed[:url] ||= stable.url
1185+
previous_version_info[:version] = stable.version
1186+
previous_version_info[:checksum] = stable.checksum
1187+
previous_version_info[:revision] = f.revision
1188+
previous_version_info[:version_scheme] = f.version_scheme
1189+
previous_version_info[:compatibility_version] = f.compatibility_version
1190+
1191+
origin_head_version_info[:url] ||= stable.url
1192+
origin_head_version_info[:version] ||= previous_version_info[:version]
1193+
origin_head_version_info[:checksum] ||= previous_version_info[:checksum]
1194+
origin_head_version_info[:revision] ||= previous_version_info[:revision]
1195+
origin_head_version_info[:compatibility_version] ||= previous_version_info[:compatibility_version]
10871196
end
10881197
rescue MacOSVersion::Error
10891198
break
10901199
end
10911200

1092-
break if @previous_committed[:version] && current_version != @previous_committed[:version]
1093-
break if @previous_committed[:revision] && current_revision != @previous_committed[:revision]
1201+
break if previous_version_info[:version] && current_version != previous_version_info[:version]
1202+
break if previous_version_info[:revision] && current_revision != previous_version_info[:revision]
10941203
end
10951204

1096-
@previous_committed.compact!
1097-
@newest_committed.compact!
1205+
previous_version_info.compact!
1206+
origin_head_version_info.compact!
10981207

1099-
[@previous_committed, @newest_committed]
1208+
@committed_version_info_cache[formula.full_name] = [previous_version_info, origin_head_version_info]
11001209
end
11011210
end
11021211
end

0 commit comments

Comments
 (0)