On 2024-06-01 11:05, Jeenu Viswambharan wrote:
> Hi,
> 
> This is about a question I posted on Stack Overflow some time ago:
> https://stackoverflow.com/questions/76954446/typescript-workflow-using-gnu-make.
> The suggested solution works, but I've some follow-up questions, so
> I'm trying here instead.
> 
> There's little point in my repeating the question here. 

There is a lot of point! Your message references the question via a URL,
which will one day be garbage. Someone looking at the mailing list
archive won't know what your message is about.

The Makefile is:

.PHONY: bundle
bundle: .bundled

src_stems := a b c
ts_files := $(addsuffix .ts,$(addprefix src/,$(src_stems)))
js_files := $(addsuffix .js,$(addprefix js/,$(src_stems)))
pkg_files := $(addsuffix .js,$(addprefix pkg/,$(src_stems)))

.bundled: $(pkg_files)
    touch $@

pkg:
    mkdir -p $@

pkg/%.js: js/%.js | pkg
    cp $< $@

$(js_files): .compiled

# Simulate running tsc
.compiled: $(ts_files)
    mkdir -p js
    touch $(js_files)
    touch $@

.PHONY:
clean:
    rm -rf pkg js .compiled .bundled

The reported issue is:

$ touch src/b.ts # I modify a single source
$ make
mkdir -p js
touch js/a.js js/b.js js/c.js
touch .compiled
cp js/b.js pkg/b.js    <- Note a.js isn't being bundled!
cp js/c.js pkg/c.js
touch .bundled

What you have to understand is that Make is evaluating things
in left-to-right orders. The reason "A" goes missing is that it is
the leftmost item in all your orders, but you've triggered an update
to "B". So by the time Make is evaluating something about "B", it
has already decided that "A" is up-to-date with respect to the
existing .compiled time stamp.

Also, you have to understand that your dependency rule:

  $(js_files): .compiled

is equivalent to three rules:

  js/a.js: .compiled
  js/b.js: .compiled
  js/c.js: .compiled

and when you add the semicolon to it, it's like:

  js/a.js: .compiled ;
  js/b.js: .compiled ;
  js/c.js: .compiled ;

The difference here is that since we have a complete rule with
a recipe, Make will consider each target to be updated by that
rule.

In the first case without the semicolon, Make first looks at
pkg/a.js, and sees that it's up-to-date w.r.t. js/a.js.
So then it checks whether js/a.js is up-to-date w.r.t.
.compiled.  That is also true: .compiled is newer.

So then it checks whether .compiled is out of date w.r.t
any of its prerequisites. Indeed, it is not: src/b.ts is
newer. So the recipe is run, and .compiled is now up-to-date.

So, back to the js/a.js: .compiled rule. Yes, .compiled
was updated, but this rule has no body to build js/a.js.
It does not say that js/a.js must be updated in any way if
.compiled is updated. So nothing is done to js/a.js and
it continues to be older than pkg/a.js, and no copy need
to take place.

Why is c.js copied? Because the touch line in the
.compiled recipe executed, and c has not yet been evaluated!

By the time Make is asking "is pkg/c.js out of date w.r.t
js/c.js", the .compiled rule has been executed already
and so the js/{a,b,c}.js files were touched. So at that point
pkg/c.js looks out of date and will be copied to pkg/c.js.

When you add an empty recipe to the .compile dependency
rule, then js/a.js is *considered* updated (even though
there are no commands in the recipe that could actually
have touched it).

I think the documentation could be improved. It says:

     Empty recipes can also be used to avoid errors for targets that will
  be created as a side-effect of another recipe: if the target does not
  exist the empty recipe ensures that 'make' won't complain that it
  doesn't know how to build the target, and 'make' will assume the target
  is out of date.

Maybe the last sentence should be something like

  [...] and 'make' will consider the target to have been updated
  by the empty recipe, so that if it appears as a prerequisite in
  other rules, the targets of those rules are considered out-of-date.



Reply via email to