如何在Makefile中引用脚本?

104

在 makefile 中有更好的方式从脚本中获取设置环境变量的方法吗?

FLAG ?= 0
ifeq ($(FLAG),0)
export FLAG=1
/bin/myshell -c '<source scripts here> ; $(MAKE) $@'
else
...targets...
endif

4
听起来你真正想要的是编写一个脚本,该脚本可以调用您的脚本来设置环境变量,然后运行make,而不是让make自己调用脚本。 - Chris Dodd
我在这里看到了一个很棒的答案:https://unix.stackexchange.com/a/235254/18152 和 https://blog.153.io/2016/04/18/source-a-shell-script-in-make/。 - zx1986
15个回答

160

Makefile默认的shell是/bin/sh,它不支持source命令。

将shell更改为/bin/bash可以实现:

# Makefile

SHELL := /bin/bash

rule:
    source env.sh && YourCommand

10
当然,这是非常有用的。但请注意,原帖作者要求设置变量供“make”使用,而不仅仅是在操作行中使用。这是如何实现后者,但没有直接的方法来实现前者。 - dmckee --- ex-moderator kitten
1
你可以轻松地设置 SHELL 为你自己的包装器来完成这个任务。在这里展示 https://gist.github.com/ingydotnet/99f7e259319f6b90e50a754f3053be7f - ingydotnet
5
这似乎对我没起作用? - ngood97
8
在macOS上无法运行。 - A. Gille
1
对于/bin/sh,只需使用. env.sh来完成这个任务。 - MKesper

65

回答问题:如问,你无法

基本问题在于,子进程无法更改父进程的环境。当进行“source”操作时,Shell通过不要分叉新进程,而是在当前Shell实例中运行这些命令来解决这个问题。这很好用,但是“make”不是“/bin/sh”(或您的脚本所用的任何其他shell),并且不了解该语言(除了它们共有的部分)。

Chris Dodd和Foo Bah已解决一种可能的解决方法,因此我将建议另一种方法(假设您正在运行GNU make):将Shell脚本后处理为可与make兼容的文本,并包含结果:

shell-variable-setter.make: shell-varaible-setter.sh
    postprocess.py @^

# ...
else
include shell-variable-setter.make
endif

详细内容留作练习。


4
你无法为整个Makefile提供脚本资源,但你可以为单个recipe这样做。 - Yaroslav Nikitenko
是的,source compilationEnv.sh; $(CC) -c -o $< $^ 或类似的命令行将作为操作行起作用。 - dmckee --- ex-moderator kitten
3
您可以使用 include .env(或任何键值文件)使变量在整个 Makefile 中可访问,然后仅将所需变量传递到相应的配方中。 - rypel

26

如果你的目标只是为了在Make中设置环境变量,为什么不使用Makefile语法并使用include命令呢?

include other_makefile

如果你需要调用Shell脚本,在shell命令中捕获结果:

JUST_DO_IT=$(shell source_script)

在目标之前应该运行shell命令。但是这不会设置环境变量。

如果想要在构建过程中设置环境变量,可以编写一个单独的shell脚本来源化您的环境变量并调用make。然后,在Makefile中,让目标调用新的shell脚本。

例如,如果原始的Makefile有目标a,那么您需要做类似于以下的操作:

# mysetenv.sh
#!/bin/bash
. <script to source>
export FLAG=1
make "$@" 

# Makefile
ifeq($(FLAG),0)
export FLAG=1
a: 
    ./mysetenv.sh a
else
a:
    .. do it
endif

7
我无法控制这个脚本。必须源代码化。 - brooksbp
如果环境变量没有被导出,这个命令会捕获它们吗?乍一看,似乎这会调用一个新的 shell,运行命令,然后退出并返回到旧环境...? - brooksbp
@brooksbp这个shell命令无法捕获环境变量。但是,如果你编写一个调用原始脚本的shell脚本,那么它就可以了。我会进一步解释。 - Foo Bah

22

使用GNU Make 3.81,我可以通过以下方式从make中调用一个shell脚本:

rule:
<tab>source source_script.sh && build_files.sh

build_files.sh会"获取"由source_script.sh导出的环境变量。

请注意:

rule:
<tab>source source_script.sh
<tab>build_files.sh

将不起作用。每行都在自己的子shell中运行。


1
请注意,在GNU Make 4.1(也许是4.0)中,保持source和命令在同一行上变得必要。使用3.81,我可以使用不同的行。为了可移植性,一行代码适用于这些版本。如果一行太长,调用外部脚本会更易读! - Eric Platon
你确定吗?像我在答案中提到的那样,在 3.81 中多行不起作用。Make 在其自己的子 shell 中运行每一行,因此多行在任何版本上都不应该工作。也许你有一个不常见的操作系统或 make 构建? - Samuel
2
多行代码可以使用 \\ 结尾来表示每一行的结束。 - user5359531
@user5359531 是的,没错。 - Samuel
使用特殊目标.ONESHELL:摆脱那些\ es。 - MKesper

12
这对我来说有效。将env.sh替换为要源的文件的名称。它通过在bash中引用该文件并将修改后的环境格式化输出到名为makeenv的文件中,然后由makefile进行源操作。
IGNORE := $(shell bash -c "source env.sh; env | sed 's/=/:=/' | sed 's/^/export /' > makeenv")                         
include makeenv   

1
#ifdef _intel64onosx<br/> GCC=/opt/intel/bin/icc<br/> CFLAGS= -m64 -g -std=c99<br/> IGNORE :=$(shell bash -c "source /opt/intel/bin/iccvars.sh intel64; env | sed 's/=/:=/' | sed 's/^/export /' > makeenv")<br/> include makeenv - revher
设置正确的编译器环境非常好(这里是64位)。谢谢。 - revher
执行此操作的语法是:“make IGNORE”吗? - John
@John,不需要。只需“make”。 - gdw2
如果您的 env.sh 文件中的变量值包含特殊的 Make 字符,例如 #$,那么当您执行 include makeenv 时,这些值将会混乱不堪。 - donhector

8

shellGNU Make中,有些结构是相同的。

var=1234
text="Some text"

你可以修改你的shell脚本来引用它所需的定义。它们必须全部是简单的name=value类型。
例如:
[script.sh]
. ./vars.sh

[Makefile]

include vars.sh

那么shell脚本和Makefile可以共享相同的信息源。我找到这个问题是因为我正在寻找一个通用语法清单,可用于GNU Make和shell脚本(不关心使用哪种shell)。

编辑: Shell和make都理解${var}。这意味着您可以连接等操作。 var="一个字符串" var=${var} "第二个字符串"


错误,像上面那样的shell脚本不会像被接受的答案中那样创建任何混乱的细节。它最终会变得非常像一个kbuild文件。 - artless noise
1
shell variables and make variables are not always transparently interchangeable. When you include them in your Makefile, they must comply with make syntax, which imposes certain characters to be escaped. For example a shell variable value like SECRET='12#34$56' in your .vars.sh will cause you trouble when go use it after include vars.sh - donhector
1
我认为有两种用例; OP无法控制脚本。所以你的观点非常好,因为你很可能会得到需要转义的数据。这些是“%$#”。如果用例是在使用环境变量和make的工具之间共享变量,则此解决方案可以工作。它具有仅一个信息源和没有额外进程的优点。如果您无法控制shell脚本,则此答案不太有用。此外,为什么要在变量名中使用“$%#”也是值得怀疑的。如果您计算项目,则需要遵循“编辑:”部分。 - artless noise

3

我非常喜欢Foo Bah的答案,其中调用脚本并回调到make。为了扩展这个答案,我做了以下操作:

# Makefile
.DEFAULT_GOAL := all 

ifndef SOME_DIR

%:
<tab>. ./setenv.sh $(MAKE) $@

else

all:
<tab>...

clean:
<tab>...

endif

--

# setenv.sh
export SOME_DIR=$PWD/path/to/some/dir

if [ -n "$1" ]; then
    # The first argument is set, call back into make.
    $1 $2
fi

这样做的额外优点是使用$(MAKE),以防有人使用独特的make程序,并且还可以处理在命令行上指定的任何规则,而无需在SOME_DIR未定义的情况下重复每个规则的名称。


如果您使用的是旧版本的make,而且不支持.DEFAULT_GOAL,则ifndef的第一个分支将失败并显示“make: *** No targets. Stop."。您可以通过将"all"添加到隐式规则中来解决此问题,例如:"all %:"。请注意,这将导致在较新版本的make中出现“*** mixed implicit and normal rules: deprecated syntax”警告。 - Samuel

3

如果你想将变量传递给子进程,使它们成为环境变量,那么可以使用bash的set -aset +a。前者的意思是,“当我设置一个变量时,也同时设置相应的环境变量。”所以这对我很有效:

check:
    bash -c "set -a && source .env.test && set +a && cargo test"

这将把.env.test中的所有内容作为环境变量传递给cargo test

请注意,这将允许您将环境传递给子命令,但它不会让您设置Makefile变量(它们是不同的东西)。如果您需要后者,请尝试这里的其他建议。


2
我提供的解决方案是:(假设您拥有bash,对于例如tcsh的语法,$@的语法不同)
创建一个名为sourceThenExec.sh的脚本,如下所示:
#!/bin/bash
source whatever.sh
$@

然后,在您的makefile中,在您的目标之前加上bash sourceThenExec.sh,例如:

ExampleTarget:
    bash sourceThenExec.sh gcc ExampleTarget.C

当然,您可以在makefile顶部放置类似于STE=bash sourceThenExec.sh的内容,并缩短以下内容:
ExampleTarget:
    $(STE) gcc ExampleTarget.C

所有这些都能够运行是因为sourceThenExec.sh打开了一个子shell,但是命令在同一个子shell中运行。
这种方法的缺点是每个目标都会对文件进行源操作,这可能是不希望看到的。

1

根据您的Make版本和封闭shell,您可以通过evalcat和使用&&链接调用来实现一个不错的解决方案:

ENVFILE=envfile

source-via-eval:
  @echo "FOO: $${FOO}"
  @echo "FOO=AMAZING!" > $(ENVFILE)
  @eval `cat $(ENVFILE)` && echo "FOO: $${FOO}"

还有一个快速测试:

> make source-via-eval
FOO:
FOO: AMAZING!

这在我的Ubuntu 20.4上有效。谢谢! - jrosell

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