theartofrain
theartofrain

Reputation: 1837

A shared Makefile dependency is only run once

TL; DR

A dependency shared by multiple targets is only invoked once.

Why?

Summary

In this tiny Makefile example, I expect deploy-everywhere to deploy staging-repo code to staging, and prod-repo code to prod.

Instead, both staging and prod get staging-repo code, which is, admittedly, suboptimal.

This seems to be happening because clone-repo is skipped / pruned when it is examined 2nd time around, as part of deploy-to-prod (see -d log output below).

Why is this happening, and can I change this behavior?

$ cat Makefile

.PHONY: deploy-everywhere deploy-to-prod deploy-to-staging clone-repo

deploy-everywhere: deploy-to-staging deploy-to-prod
    @echo "deployed everywhere"

deploy-to-prod: repo="prod-repo"
deploy-to-prod: clone-repo
    @echo "deployed to prod (from $(repo))"

deploy-to-staging: repo="staging-repo"
deploy-to-staging: clone-repo
    @echo "deployed to staging (from $(repo))"

clone-repo:
    @echo "clone-repo $(repo)"

More Details (logs, version)

Here is the output from running make:

$ make deploy-everywhere
clone-repo staging-repo
deployed to staging (from staging-repo)
deployed to prod (from prod-repo) <<<<< THIS IS A LIE, WE NEVER CLONED THE PROD REPO!!
deployed everywhere <<<< RIIIIGHT.

Extra logging:

$ make -d deploy-everywhere
...
  Successfully remade target file `deploy-to-staging'.
  Considering target file `deploy-to-prod'.
   File `deploy-to-prod' does not exist.
    Pruning file `clone-repo'.          <<<< WHY? WHY?!?!?!?! WHY.
   Finished prerequisites of target file `deploy-to-prod'.
  Must remake target `deploy-to-prod'.
...

I am running GNU Make 3.81:

$ make --version
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i386-apple-darwin11.3.0

Upvotes: 3

Views: 739

Answers (2)

Vroomfondel
Vroomfondel

Reputation: 2898

To add some explanation to Beta's solution: The programmatic contract of a make rule is that it is an opaque update of a permanent entity in the filesystem, thereby satisfying a "less-than" (younger-than) relation laid down in the dependency tree. Your construction OTOH hides the dependency from make by passing it as a parameter. The problem that you tried to circumvent is that there is no notion for "less-than" in make for non-filesystem-present entities (in your case, a revision control system). The only question that I have is why you didn't simply add the clone call to each deploy target - if you really want to solve it as a "less-than" dependency (ie. cloning only if local repository non-existent or older) there is quite some make programming necessary.

EDIT after your first comment: If I got you right, your idea of PHONY is a bit off (not totally, just a bit). PHONY is not about facilitating something across different make runs, it is a way to have a short-circtuit in your dependency tree. Given a chain foo -> bar -> phony_baz (foo depending on bar depending on phony_baz) make will, in the case that foo was the main target, first look at bar to see if it is older than phony_baz which always will be the case. This is because phony_baz itself has a make-implicit property that will always yield a "younger_than" when it is compared as a prerequisite against a target (and now comes the important part:) at the first encounter in the dependency tree. All subsequent encounters of phony_baz in other branches of the tree will do the exact opposite, terminating the evaluation right there at the phony target. This is no different from the behaviour at any other node in the dependency graph - make assumes that the contract which I noted above is fulfilled and no repetitive action is justified. This is the explanation for your clone-repo not firing a second time although it is .PHONY - your setup is effectively a directed acyclic graph where two edges join at the same node (which make is completely fine with, just worth noting), not a tree . Beta's first solution is to dynamically create two entirely unrelated nodes by parametrizing the rule, thereby returning the shape to a tree.

Upvotes: 1

Beta
Beta

Reputation: 99104

This is by design. Targets in a makefile are more like nouns than like verbs.

You can achieve what you want several ways. Here is one:

deploy-to-prod: clone-prod-repo
    @echo "deployed to prod (from prod-repo)"

deploy-to-staging: clone-staging-repo
    @echo "deployed to staging (from staging-repo)"

clone-%-repo:
    @echo "clone $*-repo"

EDIT: All right, the clone operation takes multiple parameters. Here is one way to do it:

deploy-to-prod: PARAM1=ProdOne
deploy-to-prod: PARAM2=ProdTwo
deploy-to-prod: clone-prod-repo
    @echo "deployed to prod (from prod-repo)"

deploy-to-staging: PARAM1=StagingFirst
deploy-to-staging: PARAM2=StagingSecond
deploy-to-staging: clone-staging-repo
    @echo "deployed to staging (from staging-repo)"

clone-%-repo:
    @echo clone $*-repo with $(PARAM1) and $(PARAM2)

And here is another:

define clone-repo
@echo clone using $(1)
@echo and $(2) in some way
endef

deploy-to-prod:
    @echo $@
    $(call clone-repo, ProdOne, ProdTwo)

deploy-to-staging:
    @echo $@
    $(call clone-repo, StagingFirst, StagingSecond)

Upvotes: 1

Related Questions