This Time Self-Hosted
dark mode light mode Search

For A Parallel World. Theory lesson n.3: directory dependencies

Since this is not extremely common knowledge, I wanted to write down some more notes regarding the problem that Daniel Robbins reported in Ruby 1.9 which involves parallel make install problems.

This is actually a variant of a generic parallel install failure: lots of packages in the past assumed that make install is executed on a live filesystem and didn’t create the directories where to copy the files on. This of course fails for all the staging trees install (DESTDIR-based install), which are used by all distributions to build packages, and by Gentoo to merge from ebuilds. With time, and distributions taking a major role, most of the projects updated this so that they do create their directories before merging (although there are quite a few failing this still, just look for dodir calls in the ebuilds).

The problem we have here instead is slightly different: if you just have a single install target that depends at the same time on the rules that create the directories and on those that install the files, these doesn’t specify interdependencies:

install: install-dirs install-bins

install-dirs:
        mkdir -p /usr/bin

install-bins: mybin
        install mybin /usr/bin/mybin

(Read it like it used DESTDIR properly). When using serial make, the order the rules appear on the dependency list is respected and thus the directories are created before the binaries; with no problem. When using parallel make instead, the two rules are executed in parallel and if the install command may be executed before mkdir. Which makes the build fail.

The “quick” solution that many come to is to depend on the directory:

install: /usr/bin/mybin

/usr/bin:
        mkdir -p /usr/bin

/usr/bin/mybin: mybin /usr/bin
        install mybin /usr/bin/mybin

This is the same solution that Daniel came to; unfortunately this does not work properly; the problem is that this dependency is not just ensuring that the directory exists, but it also adds a condition on the timestamp of modification (mtime) of the directory itself. And since the directory’s mtime is updated whenever the mtime of its content is updated, this can become a problem:

flame@yamato foo % mkdir foo   
flame@yamato foo % stat -c '%Y' foo
1249082013
flame@yamato foo % touch foo/bar
flame@yamato foo % stat -c '%Y' foo
1249082018

This does seem to work for most cases, and indeed a similar patch was added already to Ruby 1.9 in Portage (and I’m going to remove it as soon as I have time). Unfortunately if there are multiple files that gets installed in a similar way, it’s possible to induce a loop inside make (installing the latter binaries will update the mtime of the directory, which will then have an higher mtime than the first binary installed).

There are two ways to solve this problem, neither look extremely clean, and neither are prefectly optimal, but they do work. The first is to always call mkdir before installing the file; this might sound overkill, but using mkdir -p it really has a small overhead compared to just calling it once.

install: /usr/bin/mybin

/usr/bin/mybin: mybin /usr/bin
        mkdir -p $(dir $@)
        install mybin /usr/bin/mybin

The second is to depend on a special time-stamped rule that creates the directories:

install: /usr/bin/mybin

usr-bin-ts:
        mkdir -p /usr/bin
        touch $@

/usr/bin/mybin: mybin usr-bin-ts
        install mybin /usr/bin/mybin

Now for Ruby I’d sincerely go with the former option rather than the latter, because the latter adds a lot more complexity and for quite little advantage (it adds a serialisation point, while mkdir -p execute in parallel). Does this help you?

Comments 7
  1. I just wonder if install -D is completely non-portable so it should be avoided or not…

  2. @install -D@ is not available on non-GNU systems, not even on FreeBSD 7.2 (it might be on 8, I haven’t checked).

  3. what about moving the dependency to the right place, eg:

    install: install-binsinstall-dirs:        mkdir -p /usr/bininstall-bins: install-dirs mybin        install mybin /usr/bin/mybin
  4. Norman, that works but has two drawbacks: * @make install@ will always copy back the new files as long as an install-dirs timestamp is not present; * if somebody touches install-dirs by mistakes it breaks entirely.The timestamp file approach is almost equivalent but with extra safety; an alternative approach would use @.PHONY@ but that would also re-copy everything each time.Sincerely, I prefer just adding the @mkdir@ on the rules themselves.

  5. I once found something like this on the web:install-dirs := /usr/bininstall: install-bins $(filter-out $(wildcard $(install-dirs)), $(install-dirs))$(install-dirs): mkdir -p $^install-bins: mybin install mybin /usr/bin/mybinThe trick is that install only depends on install-dirs when the dirs don’t already exist (wildcard won’t return anything).(sorry for the lack of indenting)

  6. I just realized I still did it wrong. I think this should work, it’s based on Daniel’s version, but it should now prevent the dependency on the dir’s mtimeinstall: /usr/bin/mybin/usr/bin: mkdir -p /usr/bin/usr/bin/mybin: mybin $(filter-out $(wildcard /usr/bin), /usr/bin) install mybin /usr/bin/mybin

  7. Xavier: the problem is that the syntax you’re using is not make, it’s GNU make. And Ruby won’t accept patches based on GNU make syntax (and the change to get that working would be more extensive than it has to be).In GNU make you can also use a _much_ simpler syntax for the whole thing by making use of order-only prerequisites (Section 4.3 in the GNU make manual):<typo:code>/usr/bin/mybin : mybin | /usr/bin install mybin /usr/bin/mybin</typo:code>It only requires that /usr/bin is present, but it ignores its mtime. This is a possible alternative for the Gentoo patching since it’s _very_ quick, the problem I have with this is that it should be a GNU make extension and I know if any other make support it.If it was supported by NetBSD’s pmake it would be feasible to just use this syntax.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.