从URL列表下载文件并作为makefile中的先决条件下载列表

7
我想要一个 makefile 来编译一个项目,如果有文件(二进制文件)缺失,我需要下载它们,因为用 git 跟踪它们不是个好主意。
我有一系列 URL 和对应的文件名列表(我希望能够选择文件名保存它们,因为我无法控制 URL 的文件名,而且大多数文件名很糟糕并且难以描述)。假设这些文件始终相同或者我们不在乎它们在服务器上的变化(我们不需要重新下载它们)。
所以我的想法是在我的 makefile 中写入类似以下内容:
files = one two three four

main : $(files)
  command to do when files are present

但是如果它们不存在,我希望每个文件都能从自己的URL下载,所以我需要像$(files)元素迭代那样的东西。

我将写下我在脑海中的想法,将Python代码和Makefile知识混合在一起。我知道这很糟糕,但我不知道其他方法来做到这一点(基本上问题是如何编写它),而且我认为它很容易理解。

urls = url1 url2 url3 url4

for i in len(files):
    $(files)[i] :
        curl -Lo $(files)[i] $(urls)[i]

因此问题是:我该如何做到这一点?
我已经查阅了一段时间的文档,但没有找到适当的方法来避免多次书写文件名(我认为应该避免使用变量)。也许 canned recipes 可以解决问题,但我不知道如何使用。

文件是否会改变(在服务器上),那么您需要再次获取它们吗?如果是这种情况,您可以考虑一种依赖管理方式(例如Apache Ivy),或者可能用Gradle脚本替换您的makefile。 - pitseeker
不,它们不会改变(如果它们改变了我也不会在意)。我会编辑问题以反映这一点,谢谢! - josealberto4444
2个回答

3

有几种方法可以实现您想要的功能。警告:它们有点棘手,因为它们使用 GNU make 的高级功能:

解决方案#1:

files := one two three four
urls := url1 url2 url3 url4

main: $(files)
    @echo 'command to do when files are present'

# $(1): file name
# $(2): url
define DOWNLOAD_rule
$(1):
    @echo 'curl -Lo $(1) $(2)'
endef
$(foreach f,$(files),\
  $(eval $(call DOWNLOAD_rule,$(f),$(firstword $(urls))))\
  $(eval urls := $(wordlist 2,$(words $(urls)),$(urls)))\
)

为了更方便测试,我用echo替换了配方:

$ make -j
curl -Lo one url1
curl -Lo four url4
curl -Lo three url3
curl -Lo two url2
command to do when files are present

解释:

DOWNLOAD_rule 是一个多行变量,用于下载操作的模板规则,其中 $(1) 表示文件名,$(2) 表示相应的 URL。

您可以在 DOWNLOAD_rule 中进行 $(1)$(2) 的替换,并将结果实例化为 make 语法:

$(eval $(call DOWNLOAD_rule,one,url1))

这与您编写以下内容相同:

one:
    @echo 'curl -Lo one url1'

foreach 函数允许循环遍历单词列表。因此:

$(foreach f,$(files),$(eval $(call DOWNLOAD_rule,$(f),url1)))

将规则实例化到所有文件上...但是URL对于所有文件来说是相同的(url1),这不是你想要的。
为了获取与每个文件对应的URL,我们可以在foreach循环中放置两个不同的eval函数调用:
  • 第一个调用使用当前文件名和$(urls)中的第一个URL来实例化规则,
  • 第二个调用从$(urls)中删除第一个单词,并将结果重新分配给urls
请注意:=赋值,它们是必不可少的。默认的= (递归展开) 在这里不起作用。 $(wordlist 2,$(words $(urls)),$(urls))可能看起来很复杂,但实际上并不是:
  • $(wordlist s,e,l)会将列表l中第s到第e个单词展开(单词从1到l的长度进行编号)
  • $(words l)会将列表l中的单词数展开

因此,如果$(urls)url2 url3 url4

  • $(words $(urls))会展开为3
  • $(wordlist 2,$(words $(urls)),$(urls))会展开为url3 url4,因为它等同于$(wordlist 2,3,url2 url3 url4)

解决方案#2:

也可以将第二个eval打包在DOWNLOAD_rule变量中,但还有另一个要考虑的方面:配方在传递给shell之前由make扩展。一个目标特定的变量(url),在分析的第一次通过时扩展,解决了这个问题:

files := one two three four
urls := url1 url2 url3 url4

main: $(files)
    @echo 'command to do when files are present'

# $(1): file name
define DOWNLOAD_rule
$(1): url := $$(firstword $$(urls))
$(1):
    @echo 'curl -Lo $(1) $$(url)'
urls := $$(wordlist 2,$$(words $$(urls)),$$(urls))
endef
$(foreach f,$(files),$(eval $(call DOWNLOAD_rule,$(f))))

请注意DOWNLOAD_rule定义中的$$,它们是必需的,因为eval会扩展其参数,而make在将结果解析为常规make语法时会再次扩展。 $$是一种保护变量引用免受第一次扩展影响的方式,这样在foreach的四次迭代期间由eval实例化的内容是:
one: url := $(firstword $(urls))
one:
    @echo 'curl -Lo one $(url)'
urls := $(wordlist 2,$(words $(urls)),$(urls))

two: url := $(firstword $(urls))
two:
    @echo 'curl -Lo two $(url)'
urls := $(wordlist 2,$(words $(urls)),$(urls))

three: url := $(firstword $(urls))
three:
    @echo 'curl -Lo three $(url)'
urls := $(wordlist 2,$(words $(urls)),$(urls))

four: url := $(firstword $(urls))
four:
    @echo 'curl -Lo four $(url)'
urls := $(wordlist 2,$(words $(urls)),$(urls))

这将完全符合我们的要求。如果没有$$,它将是:

one: url := url1
one:
    @echo 'curl -Lo one '
urls := url2 url3 url4

two: url := url1
two:
    @echo 'curl -Lo two '
urls := url2 url3 url4

three: url := url1
three:
    @echo 'curl -Lo three '
urls := url2 url3 url4

four: url := url1
four:
    @echo 'curl -Lo four '
urls := url2 url3 url4

记住这一点:在使用eval时,有两个扩展符,$$经常需要用来转义第一个。
解决方案#3:
我们也可以声明每个文件(<file>-url)一个变量来存储URL,并设置这些变量和文件列表(files)的宏:
# Set file's URL and update files' list
# Syntax: $(call set_url,<file>,<url>)
set_url = $(eval $(1)-url := $(2))$(eval files := $$(files) $(1))

$(call set_url,one,url1)
$(call set_url,two,url2)
$(call set_url,three,url3)
$(call set_url,four,url4)

main: $(files)
    @echo 'command to do when files are present'

# $(1): file name
define DOWNLOAD_rule
$(1):
    @echo 'curl -Lo $(1) $$($(1)-url)'
endef
$(foreach f,$(files),$(eval $(call DOWNLOAD_rule,$(f))))

解决方案 #4:

最后,我们可以使用出色的GNU Make Standard Librarygmsl),由John Graham-Cumming和其关联数组提供的功能,执行与解决方案#3几乎相同的操作。例如,我们可以定义一个名为urls的关联数组,以文件名作为键,URL作为值:

include gmsl

$(call set,urls,one,url1)
$(call set,urls,two,url2)
$(call set,urls,three,url3)
$(call set,urls,four,url4)

files := $(call keys,urls)

main: $(files)
    @echo 'command to do when files are present'

# $(1): file name
define DOWNLOAD_rule
$(1):
    @echo 'curl -Lo $(1) $$(call get,urls,$(1))'
endef
$(foreach f,$(files),$(eval $(call DOWNLOAD_rule,$(f))))

我无法弄清楚如何在注释中正确格式化代码... 您可以将$(foreach)中的前两个$(eval)替换为DOWNLOAD_rule体中的urls := $(wordlist 2, x, $(urls))。(x > urls和files中单词的数量。 - VannTen
@VannTen 你是正确的。但我不喜欢未经验证、硬连线、上限死板(你的 x)。当容量不足时,它们往往会导致极难发现的错误。 - Renaud Pacalet
此外,第一个 eval 仍然需要从 $(urls) 中提取第一个单词。 - Renaud Pacalet
实际上,x 应该被替换为 $(words $(urls)) 并在事先进行评估。但是,由于前面的 $(eval),所需的 URL 总是位于顶部,因此您可以直接在配方中使用 $(firstword) 而无需对其进行转义。 - VannTen
@VannTen 这样好多了。我更新了我的答案,考虑了你的建议。谢谢。 - Renaud Pacalet

2
FILES := file1 file2 file3

main : $(FILES)
    command to do when files are present

file1: firstuglyurl
file2: seconduglyurl
file3: thirduglyurl

$(FILES):
    curl -Lo $@ $<

[编辑] 那个解决方案很愚蠢,我不知道当时在想什么。 试试这个:

FILES := file1 file2 file3

main : $(FILES)
    command to do when files are present

file1: URL:=firstuglyurl
file2: URL:=seconduglyurl
file3: URL:=thirduglyurl

$(FILES):
    curl -Lo $@ $(URL)

如果你的地图(URL=>文件名)在一个文件中,而且你不想手动维护它在makefile中,那么你可以让Make轻松导入它,只需告诉我们格式即可。
请注意,这不会检查存在但已过时的文件,也就是说,在URL上存在更近期的文件版本(可能可以做到,但很棘手...)。

我本以为这会抱怨缺少的先决条件。也就是说,没有构建firstuglyurl的规则。 - lockcmpxchg8b
啊..但是将.PHONY依赖于URL可以解决这个问题。 - lockcmpxchg8b
如果我们从带有日期的源中拉取数据,也许需要在curl调用中加上-R / --remote-time参数,以防万一。当然,前提是您可以相对信任时钟的同步性。 - lockcmpxchg8b
它失败了:makefile:11: *** target pattern contains no '%'. Stop.。第11行是 file1: firstuglyurl。另外,是的,如果从csv文件中获取信息会更好,因为手动维护比较容易(无论如何我都必须手动完成)。像@lockcmpxchg8b所说的添加.PHONY : firstuglyurl使得该行失败,出现相同的错误。(感谢-R建议。) - josealberto4444
可能是不喜欢 http:// 中的 :。你可以尝试将其从 URL 中删除,并在 curl 命令中加上它。例如,file1: google.comcurl -Lo $@ http://$<(而非 file1:http://google.com)。 - lockcmpxchg8b

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接