【问题标题】:In a makefile, how to get the relative path from one absolute path to another?在makefile中,如何获取从一个绝对路径到另一个绝对路径的相对路径?
【发布时间】:2011-03-21 11:12:46
【问题描述】:

一个例子来说明我的问题:

顶级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

如何在尽可能便携的同时做到这一点?

【问题讨论】:

    标签: regex shell makefile path


    【解决方案1】:

    Perl 是可移植的!这是使用核心 Perl 模块 File::Spec (resp. 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)"
    

    【讨论】:

      【解决方案2】:

      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
      

      【讨论】:

      • 第一:很好的解决方案!我必须记住使用 python 的很棒的操作系统库.... 第二:你切换了 os.path.relpath() 的参数。应该是:os.path.relpath('$(destination_dir)', '$(current_dir)')
      • 我写了一个python脚本,它接受1-2个参数并打印结果。然后通过$(shell python relpath.py $(destination_dir)) 调用它。惊人的提示!
      【解决方案3】:

      使用纯 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
      

      【讨论】:

        【解决方案4】:

        shell 使用 realpath(1)--relative-to 标志具有此功能,因此您只需调用 shell。

        这是一个例子:

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

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

        这是一个例子:

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

        【讨论】:

        • realpath 在我的 shell 中不存在。 (BSD) 我认为它也不存在于 cygwin 下。
        • realpath(1) 是 GNU coreutils 的一部分,因此它是 Linux 上的标准。
        【解决方案5】:

        这是一个使用 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 或更高版本中不起作用,请参阅warning in release notes,请改用space = $(substr ,, )
        【解决方案6】:

        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"
        

        排序!

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

        【讨论】:

          【解决方案7】:

          做你想做的事看起来并不容易。在 makefile 中使用很多组合的$(if 可能是可能的,但不便携(仅限 gmake)并且很麻烦。

          恕我直言,您正在尝试解决您自己创建的问题。为什么不将includedir 的正确值作为顶级Makefile 的相对路径发送?它可以很容易地完成,如下所示:

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

          然后你可以在子makefiles中使用$(includedir)。它已经被定义为相对的。

          【讨论】:

          • 这个元答案是最好的——避免这个问题!另请注意,--directory 不可移植:cd $(rootdir)/src/libs/libfoo; $(MAKE) ... 会更可靠。
          • 我不认为我会走到的地步,但并非所有make 命令都支持它(例如,它不在posix 中) ,所以如果这个makefile是为了分发,那么它可能会产生问题。无论如何,我倾向于认为cd foo; $(MAKE)...表达的意图更清楚。
          猜你喜欢
          • 2011-08-11
          • 2016-04-22
          • 2010-09-21
          • 1970-01-01
          • 2022-06-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多