问题
I'm trying to create a Makefile using a monorepo and am struggling a bit with dynamic output directories based on the source files' directories.
My project is laid out like so:
% tree .
.
├── Makefile
└── packages
├── bar
│ └── src
│ └── index.js
│ └── other-file.js
└── foo
└── src
└── index.js
5 directories, 4 files
I want to process each *.js file inside of each package (e.g., ./packages/foo/src/index.js) and emit the output into another directory (specifically, ./packages/foo/lib/index.js).
For example make bar would:
- Process
./packages/bar/src/index.js->./packages/bar/lib/index.js - Process
./packages/bar/src/other-file.js->./packages/bar/lib/other-file.js
I would also potentially like to use this pattern for other sorts of files in ./src such as processing .less files.
Makefile:
PACKAGES_ROOT = ./packages
packages := $(shell ls $(PACKAGES_ROOT))
package_source_dir := $(addprefix packages/,$(addsuffix /src/,$(packages)))
package_dest_dir := $(subst src,lib,$(package_source_dir))
package_source := $(foreach sdir, $(package_source_dir), $(wildcard $(sdir)*.js*))
package_dest := $(subst src,lib,$(package_source))
.PHONY: all checkdirs clean
all: checkdirs
checkdirs: $(package_dest_dir)
$(package_dest_dir):
mkdir -p $@
$(packages):
@echo "$@"
clean:
rm -rf $(package_dest_dir)
I'm not sure if I need to use VPATH or what ... help?
Note: The Makefile currently is just enough to make and remove all destination directories.
回答1:
Something like this should do the job:
# Directories
PKGS_ROOT := packages
PKGS_SRCDIR := src
PKGS_OUTDIR := lib
# Expands to the source directory for the specified package
pkg-srcdir = $(PKGS_ROOT)/$1/$(PKGS_SRCDIR)
# Expands to the output directory for the specified package
pkg-libdir = $(PKGS_ROOT)/$1/$(PKGS_OUTDIR)
# Expands to all output targets for the specified package
pkg-libs = $(addprefix $(call pkg-libdir,$1)/,$(notdir $(wildcard $(call pkg-srcdir,$1)/*.js)))
# Defines the following rules for the specified package:
# - build rule for .js files
# - rule to create the output directory if missing
# - package rule to build all outputs
# - clean-package rule to remove the output directory
# Adds the following prerequisites:
# - package target to 'all' rule
# - clean-package target to 'clean' rule
define pkg-rules
$(call pkg-libdir,$1)/%.js: $(call pkg-srcdir,$1)/%.js | $(call pkg-libdir,$1)
@echo Making $$@ from $$^
@cp $$^ $$@
$(call pkg-libdir,$1):
@mkdir $$@
$1: $(call pkg-libs,$1)
clean-$1:
rm -rf $(call pkg-libdir,$1)
all: $1
clean: clean-$1
.PHONY: $1 clean-$1
endef
# Creates rules for the specified package
add-pkg = $(eval $(call pkg-rules,$1))
# Create rules for all packages
PKGS := $(notdir $(wildcard $(PKGS_ROOT)/*))
$(foreach p,$(PKGS),$(call add-pkg,$p))
# Will be filled in by pkg-rules
.PHONY: all clean
.DEFAULT_GOAL := all
The individual functions/helpers/manipulation shouldn't be hard to understand. I abstracted directories into the first three helpers to avoid having to change a ton of occurrences should the need arise. Let's dive in pkg-rules where the actual dynamic rules are defined.
Say pkg-rules gets passed foo. It will create the following rules:
packages/foo/lib/%.js: packages/foo/src/%.js | packages/foo/lib: this is the pattern rule that builds the output.jsfile. It also has the output directory as an order-only prerequisite (which has nothing to do with execution order - it means the directory target will be built only if it doesn't exist, without looking at the timestamp).packages/foo/lib: this rule creates the output directory. You don't have nested directories and the recipe won't be executed if the directory exists, so-pis not needed. By the way, it's possible to have nested directories and replicate the source tree, but it requires some more trickery (likely secondary expansion,$(@D), using a more cleverpatsubstin place ofnotdir, using the shell for recursive wildcards, extracting dirs, etc).foo: packages/foo/lib/index.js: in the comments I call this the "package target/rule". It's an empty phony rule which has all the package outputs as prerequisites.clean-foo: in the comments I call this "clean-package target/rule". It's a phony rule which removes the package output directory.
It will also add the package and clean-package targets as prerequisites to the generic all and clean rules.
Targets like foo, clean-bar, all and clean will behave exactly like you expect.
回答2:
You could generate a temporary makefile:
packages:= $(shell ls $(PACKAGES_ROOT))
packages.mk : Makefile $(abspath .)
for package in $(packages); do \
echo "packages/$(package)/lib/%.js:packages/$(package)/src/%.js" >> $@ ; \
echo " echo recipe for $(dir)" >> $@; \
echo >> $@; \
done
-include "packages.mk"
This assumes that the directories are not dynamically created by other rules, as $(packages) will only contain the directories which were present when the makefile was first read.
来源:https://stackoverflow.com/questions/39584137/makefile-with-dynamic-output-directory-based-on-source-file-directory