在一个makefile中,如何获取从一个绝对路径到另一个绝对路径的相对路径?

17
一个例子来说明我的问题:
顶层 Makefile。
rootdir = $(realpath .)
export includedir = $(rootdir)/include
default:
    @$(MAKE) --directory=$(rootdir)/src/libs/libfoo

src/libfoo的Makefile

currentdir = $(realpath .)
includedir = $(function or magic to make a relative path
               from $(currentdir) to $(includedir),
               which in this example would be ../../../include)

另一个例子:

current dir = /home/username/projects/app/trunk/src/libs/libfoo/ 
destination = /home/username/projects/app/build/libfoo/ 
relative    = ../../../../build/libfoo

如何在尽可能保持可移植性的情况下实现这一点?

8个回答

18
您可以使用shell函数,并使用realpath(1)(它是coreutils的一部分)和--relative-to标志。

以下是一个示例:

RELATIVE_FILE1_FILE2:=$(shell realpath --relative-to $(FILE1) $(FILE2))

您甚至可以使用一个调用的realpath(1)来处理整个文件列表,因为它知道如何处理许多文件名。

以下是一个示例:

RELATIVES:=$(shell realpath --relative-to $(RELATIVE) $(FILES))

我的Shell中没有realpath命令(BSD)。我认为在cygwin下也不存在它。 - dbn
4
realpath(1)是GNU Coreutils的一部分,因此它在Linux上是标准的。 - Mark Veltzer

11

做你想做的事情看起来并不容易。可能需要在Makefile中使用大量组合的$(if),但这不具有可移植性(仅限于gmake)且麻烦。

在我看来,你正试图解决自己创造的问题。为什么不将includedir的正确值作为相对路径从顶层Makefile发送?可以通过以下简单的方式完成:

rootdir = $(realpath .)
default:
    @$(MAKE) --directory=$(rootdir)/src/libs/libfoo includedir=../../../include

那么您可以在子Makefile中使用$(includedir)。它已经定义为相对路径。


这个元回答是最好的 - 避免问题!还要注意,--directory不是可移植的:cd $(rootdir)/src/libs/libfoo; $(MAKE) ...会更可靠。 - Norman Gray
1
我不认为会到达“错误”的程度,但并非所有的make命令都支持它(例如,在posix中没有),因此如果这个makefile是用于分发的,那么可能会出现问题。无论如何,我倾向于认为cd foo; $(MAKE)...更清晰地表达了意图。 - Norman Gray

6

Python是跨平台的!因此,我建议您在submakefile中使用这个简单的示例。

使用current_dirdestination_dir路径,os.path.relpath()可以为您完成工作,因此您不必重新发明轮子。

submakefile.mk

current_dir=$(CURDIR)
makefile_target:
    (echo "import os"; echo "print(os.path.relpath('$(destination_dir)', '$(current_dir)'))" )| python

2
第一条:很棒的解决方案!我得记住使用Python的强大OS库......第二条:您切换了os.path.relpath()的参数。应该是:os.path.relpath('$(destination_dir)', '$(current_dir)') - jvriesem
我写了一个 Python 脚本,它需要 1-2 个参数并打印结果。然后通过 $(shell python relpath.py $(destination_dir)) 调用它。太棒了! - Michele d'Amico

5

使用纯Make实现的强大易用的解决方案:

override define \s :=
$() $()
endef

ifndef $(\s)
override $(\s) :=
else
$(error Defined special variable '$(\s)': reserved for internal use)
endif

override define dirname
$(patsubst %/,%,$(dir $(patsubst %/,%,$1)))
endef

override define prefix_1
$(if $(or $\
$(patsubst $(abspath $3)%,,$(abspath $1)),$\
$(patsubst $(abspath $3)%,,$(abspath $2))),$\
$(strip $(call prefix_1,$1,$2,$(call dirname,$3))),$\
$(strip $(abspath $3)))
endef

override define prefix
$(call prefix_1,$1,$2,$1)
endef

override define relpath_1
$(patsubst /%,%,$(subst $(\s),/,$(patsubst %,..,$(subst /,$(\s),$\
$(patsubst $3%,%,$(abspath $2)))))$\
$(patsubst $3%,%,$(abspath $1)))
endef

override define relpath
$(call relpath_1,$1,$2,$(call prefix,$1,$2))
endef

测试用例:

$(info $(call prefix,/home/user,/home/user))
$(info $(call prefix,/home/user,/home/user/))
$(info $(call prefix,/home/user/,/home/user))
$(info $(call prefix,/home/user/,/home/user/))

$(info $(call relpath,/home/user,/home/user))
$(info $(call relpath,/home/user,/home/user/))
$(info $(call relpath,/home/user/,/home/user))
$(info $(call relpath,/home/user/,/home/user/))

$(info ----------------------------------------------------------------------)

$(info $(call prefix,/home/user,/home/user/.local/share))
$(info $(call prefix,/home/user,/home/user/.local/share/))
$(info $(call prefix,/home/user/,/home/user/.local/share))
$(info $(call prefix,/home/user/,/home/user/.local/share/))

$(info $(call relpath,/home/user,/home/user/.local/share))
$(info $(call relpath,/home/user,/home/user/.local/share/))
$(info $(call relpath,/home/user/,/home/user/.local/share))
$(info $(call relpath,/home/user/,/home/user/.local/share/))

$(info ----------------------------------------------------------------------)

$(info $(call prefix,/home/user/.config,/home/user/.local/share))
$(info $(call prefix,/home/user/.config,/home/user/.local/share/))
$(info $(call prefix,/home/user/.config/,/home/user/.local/share))
$(info $(call prefix,/home/user/.config/,/home/user/.local/share/))

$(info $(call relpath,/home/user/.config,/home/user/.local/share))
$(info $(call relpath,/home/user/.config,/home/user/.local/share/))
$(info $(call relpath,/home/user/.config/,/home/user/.local/share))
$(info $(call relpath,/home/user/.config/,/home/user/.local/share/))

$(info ----------------------------------------------------------------------)

$(info $(call prefix,/home/user/.local/share,/home/user))
$(info $(call prefix,/home/user/.local/share,/home/user/))
$(info $(call prefix,/home/user/.local/share/,/home/user))
$(info $(call prefix,/home/user/.local/share/,/home/user/))

$(info $(call relpath,/home/user/.local/share,/home/user))
$(info $(call relpath,/home/user/.local/share,/home/user/))
$(info $(call relpath,/home/user/.local/share/,/home/user))
$(info $(call relpath,/home/user/.local/share/,/home/user/))

$(info ----------------------------------------------------------------------)

$(info $(call prefix,/home/user/.local/share,/home/user/.config))
$(info $(call prefix,/home/user/.local/share,/home/user/.config/))
$(info $(call prefix,/home/user/.local/share/,/home/user/.config))
$(info $(call prefix,/home/user/.local/share/,/home/user/.config/))

$(info $(call relpath,/home/user/.local/share,/home/user/.config))
$(info $(call relpath,/home/user/.local/share,/home/user/.config/))
$(info $(call relpath,/home/user/.local/share/,/home/user/.config))
$(info $(call relpath,/home/user/.local/share/,/home/user/.config/))

$(info ----------------------------------------------------------------------)

$(info $(call prefix,/root,/home/user))
$(info $(call prefix,/root,/home/user/))
$(info $(call prefix,/root/,/home/user))
$(info $(call prefix,/root/,/home/user/))

$(info $(call relpath,/root,/home/user))
$(info $(call relpath,/root,/home/user/))
$(info $(call relpath,/root/,/home/user))
$(info $(call relpath,/root/,/home/user/))

期望结果:

/home/user
/home/user
/home/user
/home/user




----------------------------------------------------------------------
/home/user
/home/user
/home/user
/home/user
../..
../..
../..
../..
----------------------------------------------------------------------
/home/user
/home/user
/home/user
/home/user
../../.config
../../.config
../../.config
../../.config
----------------------------------------------------------------------
/home/user
/home/user
/home/user
/home/user
.local/share
.local/share
.local/share
.local/share
----------------------------------------------------------------------
/home/user
/home/user
/home/user
/home/user
../.local/share
../.local/share
../.local/share
../.local/share
----------------------------------------------------------------------




../../root
../../root
../../root
../../root

这真的很令人印象深刻。 - shadowtalker
请注意,这在我的 Mac 上附带的 Make 版本(3.81)上不起作用,但可以使用 GNU Make 4.3。 - shadowtalker

3

这里有一个解决方案,仅使用GNU make函数。尽管它是递归的,但应该比调用外部程序更有效。思路非常简单:相对路径将是零个或多个..以到达最常见的祖先,然后是一个后缀以到达第二个目录。难点在于找到两个路径中最长的公共前缀。

# DOES not work if path has spaces
OneDirectoryUp=$(patsubst %/$(lastword $(subst /, ,$(1))),%,$(1))

# FindParentDir2(dir0, dir1, prefix)  returns prefix if dir0 and dir1
# start with prefix, otherwise returns
# FindParentDir2(dir0, dir1, OneDirectoryUp(prefix))
FindParentDir2=
$(if
  $(or
    $(patsubst $(3)/%,,$(1)),
    $(patsubst $(3)/%,,$(2))
   ),
   $(call FindParentDir2,$(1),$(2),$(call OneDirectoryUp,$(3))),
   $(3)
 )

FindParentDir=$(call FindParentDir2,$(1),$(2),$(1))

# how to make a variable with a space, courtesy of John Graham-Cumming 
# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
space:= 
space+=

# dir1 relative to dir2 (dir1 and dir2 must be absolute paths)
RelativePath=$(subst
               $(space),
               ,
               $(patsubst
                 %,
                 ../,
                 $(subst
                   /,
                   ,
                   $(patsubst
                     $(call FindParentDir,$(1),$(2))/%,
                     %,
                     $(2)
                    )
                  )
                )
              )
             $(patsubst
               $(call FindParentDir,$(1),$(2))/%,
               %,
               $(1)
              )

# example of how to use (will give ..)
$(call RelativePath,/home/yale,/home/yale/workspace)

我最近将一组大型递归Makefile翻译成一个整个项目的Make,因为众所周知递归Make存在问题,因为它没有暴露整个依赖关系图(http://aegis.sourceforge.net/auug97.pdf)。所有源代码和库路径都是相对于当前Makefile目录定义的。我没有定义固定数量的通用%构建规则,而是为每个(源代码目录,输出目录)对创建了一组规则,这避免了使用vpath时的歧义。在创建构建规则时,我需要为每个源代码目录提供一个规范路径。虽然可以使用绝对路径,但通常太长且不太可移植(我恰好正在使用Cygwin GNU make,其中绝对路径具有/cygdrive前缀,并且不被Windows程序识别)。因此,我在生成规范路径时广泛使用此功能。


这个空格变量技巧在Make 4.3或更高版本中不起作用,请参见发布说明中的警告,改用space = $(substr ,, ) - Caleb

3

Didier的回答是最好的,但以下内容可能会给您一些启示:

includedir=/a/b/c/d
currentdir=/a/b/e/f/g
up=; while ! expr $includedir : $currentdir >/dev/null; do up=../$up; currentdir=`dirname $currentdir`; done; relative=$up`expr $includedir : $currentdir'/*\(.*\)'`
echo "up=$up  currentdir=$currentdir, relative=$relative"

已排序!

(虽然没有人说它必须很漂亮...)


0
这是基于Norman Gray的想法的我的解决方案:
define relative
CURDIR=$(1); UP=; while ! expr $(2) : $${CURDIR} > /dev/null; do UP=../$${UP}; CURDIR=$$(dirname $${CURDIR}); done; echo $${UP}$$(expr $(2) : $${CURDIR}'/*\(.*\)')
endef

includedir1=/a/b/c/d
includedir2=/a/b/e/f/g/h/j
currentdir=/a/b/e/f/g

relative1:=$(shell $(call relative,$(currentdir),$(includedir1)))

makefile_target:
    @echo "Relative1: $(relative1)"
    @echo "Relative2: $(shell $(call relative,$(currentdir),$(includedir2)))"

0

Perl是可移植的!这里提供了一个解决方案,使用核心Perl模块File::Spec(或File::Spec::Functions)中的abs2rel函数:

current_dir= /home/username/projects/app/trunk/src/libs/libfoo/ 
destination= /home/username/projects/app/build/libfoo/ 
relative=    $(shell perl -MFile::Spec::Functions=abs2rel -E 'say abs2rel(shift, shift)' $(destination) $(current_dir))

makefile_target:
    @echo "Use now $(relative)"

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