开发者

In a makefile, how to get the relative path from one absolute path to another?

An example to illustrate my q开发者_StackOverflowuestion:

Top level makefile

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

Makefile for src/libfoo

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

Another example:

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

How can this be done, while trying to be as portable as possible?


You can use the shell function, and use realpath(1) (which is part of coreutils) and the --relative-to flag.

Here is an example:

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

You can even process a whole list of files with one invocation of realpath(1) since it knows how to process many file names.

Here is an example:

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


Doing what you want does not look easy. It may be possible using a lot of combined $(if in the makefile but not portable (gmake only) and cumbersome.

IMHO, you are trying to solve a problem that you create yourself. Why don't you send the correct value of includedir as a relative path from the Top-level Makefile? It can be done very easily as follows:

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

Then you can use $(includedir) in the sub-makefiles. It is already defined as relative.


Python is portable! So, I would suggest you this simple example in your submakefile

With current_dir and destination_dir paths os.path.relpath() does the job for you so you do not have to re-invent the wheel.

submakefile.mk

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


Robust drop-in solution with pure 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

Test cases:

$(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/))

Expected results:

/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


Didier's answer is the best one, but the following might give you some ideas:

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"

Sorted!

(no-one said it had to be pretty...)


Here's a solution that only uses GNU make functions. Even though it's recursive, it ought to be more efficient than calling an external program. The idea is pretty straight forward: the relative path will be zero or more .. to go up to the most common ancestor, then a suffix to go down to the 2nd directory. The hard part is finding the longest common prefix in both paths.

# 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)

I recently translated a large set of recursive makefiles into a whole project make as it's well known that recursive make is bad due to not exposing the entire dependence graph (http://aegis.sourceforge.net/auug97.pdf). All source code and library paths are defined relative to the current makefile directory. Instead of defining a fixed number of generic % build rules, I create a set of rules for every (source code directory, output directory) pair, which avoids the ambiguity of using vpath. When creating the build rules, I need a canonical path for each source code directory. Although the absolute path can be used, it's usually too long and less portable (I happened to be using Cygwin GNU make where absolute paths have a /cygdrive prefix and aren't recognized by Windows programs). Therefore, I use this function heavily for generating canonical paths.


Perl is portable! Here's a solution using the abs2rel function from the core Perl module File::Spec (resp. File::Spec::Functions):

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)"


This is my solution based on the Norman Gray's idea:

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜