Skip to content

Commit 180b664

Browse files
committed
[Misc #22206] Support Make variable assignments
Accept `VAR=value` on the command line while keeping `--thread-model` as a discoverable shortcut. Pass `LOCAL_HDRS` from `mkmf` so dependencies already covered by manual rules are not duplicated in the autogenerated section.
1 parent e289fe1 commit 180b664

4 files changed

Lines changed: 205 additions & 40 deletions

File tree

lib/mkmf.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2721,7 +2721,9 @@ def create_makefile(target, srcprefix = nil)
27212721
"--depend-root", MakeMakefile::Depend.find_root(srcdir)
27222722
)
27232723
MakeMakefile::Depend.new(root: depend_root).update_extension(
2724-
source_depend, source_map, nmake: !!$nmake,
2724+
source_depend, source_map,
2725+
make_variables: {"LOCAL_HDRS" => $headers},
2726+
nmake: !!$nmake,
27252727
)
27262728
end
27272729

lib/mkmf/depend.rb

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# Check committed source mappings without modifying files:
66
# ruby tool/mkdepend.rb --scope=all --sources --check
77
# The command can be run from either the source or a build directory.
8+
# Known Make variables can be supplied as +VAR=value+. The
9+
# <tt>--thread-model=MODEL</tt> option is equivalent to
10+
# +THREAD_MODEL=MODEL+.
811
#
912
# Dependency files can declare inputs that do not exist in the source tree:
1013
#
@@ -257,17 +260,23 @@ def find_root(path)
257260
# Parses command-line arguments into runner options and input paths.
258261
def parse_options(argv)
259262
options = {}
263+
make_variables = {}
260264
output = nil
261265
select_mode = proc do |mode|
262266
options[:mode] = mode
263267
end
264268
parser = OptionParser.new do |opts|
265-
opts.banner = "Usage: #{File.basename($0)} [options] [files]"
269+
opts.banner =
270+
"Usage: #{File.basename($0)} [options] [VAR=value ...] [files]"
266271
opts.separator ""
267272
opts.separator "Input selection:"
268273
opts.on("--root=DIR", "source tree root") {|value| options[:root] = value}
269-
opts.on("--thread-model=MODEL", "thread model") do |value|
270-
options[:thread_model] = value
274+
opts.on(
275+
"--thread-model=MODEL",
276+
String,
277+
"thread model (same as THREAD_MODEL=MODEL)",
278+
) do |value|
279+
make_variables["THREAD_MODEL"] = value
271280
end
272281
opts.on(
273282
"--scope=SCOPE", [:all, :core, :extensions],
@@ -301,7 +310,16 @@ def parse_options(argv)
301310
options[:verbose] = true
302311
end
303312
end
304-
inputs = parser.parse(argv)
313+
inputs = []
314+
rest = parser.order(argv) do |arg|
315+
if /\A([A-Za-z_]\w*)=(.*)\z/m =~ arg
316+
make_variables[$1] = $2
317+
else
318+
inputs << arg
319+
end
320+
end
321+
inputs.concat(rest)
322+
options[:make_variables] = make_variables unless make_variables.empty?
305323
options[:output] = output if options[:mode] == :output
306324
[options, inputs]
307325
end
@@ -315,22 +333,55 @@ def execute(inputs, root: DEFAULT_ROOT, **options)
315333
# Creates a dependency updater for +root+.
316334
def initialize(root: DEFAULT_ROOT)
317335
@root = File.expand_path(root)
318-
@thread_model = nil
336+
@make_variables = {}
319337
@dependency_declarations = {}
320338
@dependency_targets = {}
321339
@dependency_contents = {}
322340
end
323341

324342
private
325343

326-
# Selects the thread model used to expand dependency declarations.
327-
def select_thread_model(thread_model)
328-
return if @thread_model == thread_model
344+
# Selects known Make variable values used during dependency generation.
345+
def select_make_variables(make_variables)
346+
make_variables = make_variables.to_h do |name, value|
347+
[name.to_s, Array(value).map(&:to_s)]
348+
end
349+
return if @make_variables == make_variables
329350

330-
@thread_model = thread_model
351+
@make_variables = make_variables
331352
@dependency_declarations.clear
332353
end
333354

355+
# Expands known Make variables embedded in +value+.
356+
def expand_make_variables(value, defaults: {})
357+
value.gsub(/\$\((\w+)\)/) do |variable|
358+
name = $1
359+
values = if @make_variables.key?(name)
360+
@make_variables[name]
361+
elsif defaults.key?(name)
362+
Array(defaults[name])
363+
else
364+
next variable
365+
end
366+
unless values.one?
367+
raise ArgumentError, "#{name} is not a scalar Make variable"
368+
end
369+
values.first.to_s
370+
end
371+
end
372+
373+
# Expands known Make variables that stand for dependency lists.
374+
def expand_dependency_variables(dependencies)
375+
dependencies.flat_map do |dependency|
376+
if /\A\$\((\w+)\)\z/ =~ dependency &&
377+
(values = @make_variables[$1])
378+
values.flat_map(&:split)
379+
else
380+
expand_make_variables(dependency)
381+
end
382+
end
383+
end
384+
334385
# Expands matching wildcards in a +scan+ declaration.
335386
#
336387
# Returns a generated-name to source-path mapping.
@@ -390,14 +441,10 @@ def parse_dependency_declarations(path, content = nil)
390441
end
391442
end
392443
declarations.scan.transform_values! do |source|
393-
source.gsub('$(THREAD_MODEL)', @thread_model || 'pthread')
444+
expand_make_variables(source, defaults: {"THREAD_MODEL" => "pthread"})
394445
end
395-
if @thread_model
396-
declarations.dependencies.transform_values! do |dependencies|
397-
dependencies.map do |dependency|
398-
dependency.gsub('$(THREAD_MODEL)', @thread_model)
399-
end
400-
end
446+
declarations.dependencies.transform_values! do |dependencies|
447+
expand_dependency_variables(dependencies)
401448
end
402449
declarations
403450
end
@@ -705,9 +752,33 @@ def compact_dependencies(rules, group: true)
705752
lines.uniq.sort.join
706753
end
707754

755+
# Returns target and dependency pairs from Make rules.
756+
def dependency_pairs(rules)
757+
rules = normalize_dependency_rules(rules)
758+
rules.each_line.each_with_object(Set.new) do |line, pairs|
759+
next unless /\A(\S+(?:\s+\S+)*):\s*(.*?)\s*\z/ =~ line
760+
761+
targets = $1.split
762+
dependencies = expand_dependency_variables($2.split).map do |dependency|
763+
normalize_dependency_rules(dependency)
764+
end
765+
targets.product(dependencies) {|pair| pairs << pair}
766+
end
767+
end
768+
769+
# Removes generated dependencies already covered by +manual_rules+.
770+
def remove_manual_dependencies(generated, manual_rules)
771+
manual = dependency_pairs(manual_rules)
772+
generated.each_line.reject do |line|
773+
normalized = normalize_dependency_rules(line)
774+
/\A(\S+):\s+(\S+)\s*\z/ =~ normalized &&
775+
manual.include?([$1, $2])
776+
end.join
777+
end
778+
708779
# Removes VPATH markers that are unnecessary in build-directory output.
709780
def normalize_dependency_rules(rules)
710-
rules.gsub('{$(VPATH)}', '')
781+
rules.gsub(/\{(?:\.;)?\$\(VPATH\)\}/, '')
711782
end
712783

713784
# Returns whether two rule sets differ only by removable VPATH markers.
@@ -826,10 +897,13 @@ def update_deps(rules, input, group: true, verbose: false)
826897

827898
# Updates the marked dependency section in extension file +input+.
828899
#
829-
# +source_map+ maps object targets to source paths. Returns +true+ when
830-
# the file changed and +false+ when its dependencies were already current.
831-
def update_extension(input, source_map, nmake: false, verbose: false)
832-
select_thread_model(nil)
900+
# +source_map+ maps object targets to source paths. +make_variables+
901+
# supplies values of Make variables used by manual dependency rules.
902+
# Returns +true+ when the file changed and +false+ when its dependencies
903+
# were already current.
904+
def update_extension(input, source_map, make_variables: {}, nmake: false,
905+
verbose: false)
906+
select_make_variables(make_variables)
833907
input = File.expand_path(input)
834908
deps = File.read(input) if File.file?(input)
835909
match = MARK_SECTION.match(deps) if deps
@@ -848,7 +922,9 @@ def update_extension(input, source_map, nmake: false, verbose: false)
848922
source, generated, target: target, input: input, project: true
849923
)
850924
end
851-
expected = compact_dependencies(generated.join, group: !nmake)
925+
manual = match.pre_match + match.post_match
926+
generated = remove_manual_dependencies(generated.join, manual)
927+
expected = compact_dependencies(generated, group: !nmake)
852928
updated = match.pre_match + expected + match.post_match
853929
return false if same_dependency_rules?(match[0], expected)
854930

@@ -896,12 +972,13 @@ def report_outdated_dependencies(input, current, expected, err: $stderr)
896972

897973
# Processes dependency +inputs+ according to the selected update +mode+.
898974
#
899-
# +thread_model+ replaces <tt>$(THREAD_MODEL)</tt> in declarations.
975+
# +make_variables+ supplies values for Make variables in declarations.
976+
# +thread_model+ is a shortcut for its <tt>THREAD_MODEL</tt> entry.
900977
#
901978
# Returns +false+ only when check mode finds an outdated dependency file.
902979
def run(inputs = ARGV, out: $stdout, err: $stderr, mode: :stdout,
903-
output: nil, nmake: false, sources: false, scope: nil,
904-
thread_model: nil, verbose: false)
980+
output: nil, make_variables: {}, nmake: false, sources: false,
981+
scope: nil, thread_model: nil, verbose: false)
905982
case mode
906983
when :output
907984
raise ArgumentError, "output directory is missing" unless output
@@ -910,7 +987,13 @@ def run(inputs = ARGV, out: $stdout, err: $stderr, mode: :stdout,
910987
else
911988
raise ArgumentError, "unknown update mode: #{mode.inspect}"
912989
end
913-
select_thread_model(thread_model)
990+
unless thread_model.nil?
991+
if thread_model.empty?
992+
raise ArgumentError, "thread model must not be empty"
993+
end
994+
make_variables = make_variables.merge("THREAD_MODEL" => thread_model)
995+
end
996+
select_make_variables(make_variables)
914997
if scope
915998
inputs = dependency_files(scope).map {|file| File.join(@root, file)}
916999
end

test/mkmf/test_depend.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@ def test_update_extension_dependencies
88
source_dir = File.join(root, 'ext/example')
99
FileUtils.mkdir_p(source_dir)
1010
File.write(File.join(root, 'ext/shared.h'), '')
11+
File.write(File.join(source_dir, 'local.h'), '')
1112
File.write(File.join(source_dir, 'example.c'), <<~SOURCE)
1213
#include "../shared.h"
14+
#include "local.h"
1315
SOURCE
1416
File.write(File.join(source_dir, 'extconf.rb'), <<~RUBY)
1517
require "mkmf"
18+
$headers << "local.h"
1619
create_makefile("example")
1720
RUBY
1821
depend = File.join(source_dir, 'depend')
1922
File.write(depend, <<~DEPEND)
23+
example.o: $(LOCAL_HDRS)
24+
2025
# AUTOGENERATED DEPENDENCIES START
2126
stale.o: stale.c
2227
# AUTOGENERATED DEPENDENCIES END
@@ -32,6 +37,8 @@ def test_update_extension_dependencies
3237
)
3338
assert_predicate($?, :success?, output)
3439
assert_equal(<<~DEPEND, File.read(depend))
40+
example.o: $(LOCAL_HDRS)
41+
3542
# AUTOGENERATED DEPENDENCIES START
3643
example.o: $(srcdir)/../shared.h
3744
example.o: example.c

0 commit comments

Comments
 (0)