On Sat, Oct 27, 2018 at 6:59 PM Nils Dagsson Moskopp
<
ni...@dieweltistgarnichtso.net> wrote:
> Avery Pennarun <
apen...@gmail.com> writes:
> > I know what you mean, but I don't think the "build at most once"
> > behaviour is very surprising, if that's what a user has been led to
> > expect. Admittedly, the (my) redo documentation isn't super clear on
> > this, because I assumed it was obvious. Since you interpreted it
> > differently, clearly I was wrong in that assumption.
>
> Your documentation contains this description of redo-ifchange behaviour:
>
> > The redo-ifchange command means, "build each of my arguments. If any
> > of them or their dependencies ever change, then I need to run the
> > current script over again."
>
> Source: <
https://github.com/apenwarr/redo>
I can see how that phrase is confusing, but it is intending to
indicate that redo-ifchange does two separate things:
1) It requests redo to build the targets if needed
2) It explains to redo that when checking the *current* target in the
future, that it depends on the given targets.
If X depends on A, then ever after building X, if A has changed since
last time we checked, yes, we have to build X again. What my
documentation doesn't say is that we *will* run the current script
again *right now*. No information is lost. You're creating weird
dependency loops and expecting something useful to happen; there is no
unambiguous way to resolve a dependency loop in such a way that we can
expect the loop to ever terminate. redo isn't prolog; nobody expects
it to just run forever until the chain finally breaks.
> I just implemented that behaviour the simplest way it could possibly
> work. To make it obvious that something is built only once, the text
> should have been something along the lines of:
>
> > The redo-ifchange command means, “build each of my arguments, except
> > for those that were already built in the recent past. If any of them
> > or their dependencies ever change, then I need to run the current
> > script over again, unless it was already run in the recent past or
> > redo can not figure out why an argument or its dependency changed.”
Your text above makes it sound like redo will re-run the "current
script" again in the current run, without even being asked. That's
not even what redo-ifchange means, ever.
> As you can probably guess from that “in the recent past”, I have no idea
> how to even describe the behaviour of not rebuilding out-of-date targets
> that your redo implementation exhibits in layman's terms. I do know this
> phenomenon, of not documenting stuff, which is why I always let ordinary
> people outside of a project review the documentation for it. My own redo
> man pages were a mess in the first version, before my friends read them.
I'm not making a claim that my documentation is perfect or wouldn't be
greatly improved by having more proofreaders. Sure.
However, by your own admission, your original docs were a mess until
your friends read them. This may mean that your mental model is a
mess. Maybe you should ask your friends if "redo-ifchange only builds
a given target at most once per run" is confusing or not?
> > A human, handed a list of tasks, will not necessarily go back to you
> > after each task to see if you have a new one.
>
> A strange point; but a human, given a list of *prerequisites* for tasks
> will probably check those before each task. The redo idea is about such
> prerequisites for tasks, i.e. “if this resource is not in such-and-such
> state, do this task to rectify the situation”. And the major reason why
> one would use redo is that other systems do this in a way that does not
> capture everything. Make, for example, can not ever handle nonexistence
> dependencies – no system that needs the full dependency tree before the
> build can.
There are many ways a human might execute a task list. One obvious
way is this: imagine my job is to take out all the garbage from each
floor of a building, once per day. I go to each floor, and take out
the garbage. After I'm done, I don't revisit each floor to see if
more garbage has appeared. If someone asks me at the end of the day
whether I took out "all" the garbage, I can say with 100% certainty,
yes I did! If they ask me to go take out all the garbage again, I can
do that. I don't think this way of doing things is confusing for most
humans. Moreover, when I ask redo to do something, that's exactly
what I want it to do. Don't be fancy, just do the things I tell you.
Once. If I want you to do it more than once, I'll say so.
> > Similarly, my version of redo takes a list of high-level tasks from
> > you, plans them all out (checks all the dependencies at once), and
> > executes once. As I mentioned in my other email, this is a
> > self-consistent view of the world that I think is pretty easy to
> > explain, and as a bonus allows for lots of neat optimizations.
>
> I seriously doubt the consistency of this approach – anything involving
> redo-always and/or dependencies on resources that could change during a
> run without user intervention (the output of “curl
http://example.org”,
> for example) is guaranteed to be subtly wrong using this redo approach.
My point about "consistency" is that everything built in a given run
is built using a *single* result of, say, your "curl
http://example.org". If you have multiple .o files depending on that
input, say, then they will all be working from the same thing.
As a silly example, imagine I have a date.do that does just this:
date >$3
redo-always
redo-stamp <$3
Then I do various transformations on the date. For example,
version.h.do turns it into a version number for my nightly build, and
nightly.do produces a tarball that uses the date in a version number.
Both of those targets depend on date. If date is not identical for
*both*, and say the compile is slow or started at 11:59pm, then I
could end up with a release where version.h says yesterday but the
tarball filename is today. Oops!
If you really want to re-run curl every time its output is used, then
just run it directly. redo doesn't add anything. Despite its name,
most of the purpose of redo is to *not* redo things when not
necessary. Otherwise you'd just use a shell script.
> The *big* problem is that “redo-ifchange” is no longer atomic – what you
> attempted to make atomic is the whole build. Yet for that to make sense,
> you could only replace files at the very end of it and freeze everything
> that could be dependency-checked at the very beginning (making the cache
> the reality, in a way). If you are not root / have a networked computer,
> this is impossible.
If your external dependencies change atomically, then actually you
could construct a set of redo scripts that retrieves the atomic set of
values from the server once per run. Then downstream .do files could
pull parts of the retrieved files to generate individual targets.
That would give you an atomically correct build.
I think that would work with my implementation, but not yours, because
yours has no way to stop redo-always targets from running repeatedly
and breaking atomicity.
My proposal in an earlier email was to add a command like redo-recheck
(originally redo-inconsistent) to optionally make my version able to
act like yours, while explicitly declaring that you don't want this
atomicity.
> A dofile involving loops and an iterative process is not even possible:
> The result would even differ depending on such loop being in the dofile
> or in a shell script which invokes redo, because the latter does ensure
> that dependencies are checked.
I'm not sure what you mean here. I use dofiles with loops all the
time. But I don't "redo-ifchange" the same thing repeatedly across
loop iterations, that's true. I feel like at most you just want to
call 'redo' there instead of 'redo-ifchange'. 'redo' doesn't check
dependencies nor add dependencies.
> I can not show them to you, since I wrote them in a work context, but I
> did write dofiles that generated API documentation for a HTTP Backend …
> and this documentation included examples that were the calls to the API
> that I was documenting and the responses. This meant that each of those
> targets depended on the service being in a running state. Now, having a
> target that starts a service and outputs the state of it is technically
> racy, but it was very useful, since other processes on the same machine
> also involved starting (or killing) the service. Your solution would be
> one that would start the service once even if redo-always was involved,
> if I am not mistaken – but it would pretend that one service started is
> online forever, even if it was not in reality, as it would not rebuild.
This example is awful; you have an admittedly racy service that you
and others are starting and stopping at random times, presumably all
on the same port, hoping that you're the one who wins, and praying
that the output you get from your hopefully-still-running version is
what you wanted so that you can use it to complete the current build.
Whoa! Nothing redo can do will make your plan a good plan.
One thing you could try would be to start a copy of the service on a
randomized port number so that other people don't conflict with yours.
Then you could, yes, start it only one time instead of multiple times.
As a bonus, your build would go faster because you don't have to
constantly start/stop your service only to try to shrink the race
condition window.
> > Your view is also self-consistent, but does not allow for neat
> > optimizations. And I don't think yours is actually any easier to
> > explain.
>
> As I see it, the single downside of my approach is that the implementors
> can not take some shortcuts. It also explains why my own parallelization
> attempts went nowhere, as I tried to solve a different (harder) problem.
>
> The result is that with my approach, users could achieve everything that
> is possible with your approach. On the other hand, your approach has the
> same set of issues that make(1) has: It is possible that targets are not
> being rebuilt, even though they probably should.
Your model of build systems apparently doesn't include "run fast" as a
thing that can be achieved, because your model cannot ever achieve
that if it isn't allowed to ever cache dependency checks. This was
also one of my annoyances with the "Build systems a la carte" paper,
which was theoretically interesting but practically missed a lot of
important stuff.
In any case, my version of redo supports parallelism and very large
projects, and none of the others do. This is because I thought
through this stuff in advance. Parallelism is massively important to
all my use cases.
I also proposed a redo-recheck command that seems to enable the few
rare cases that you bring up that my system currently rejects.
> I want to again ask you for real-world projects you have been using redo
> in.
You can play with my wvbuild and wvstreams projects (on github) if you
like. wvbuild's toplevel redo scripts compile big projects like
openssl (itself using make) and then uses them as dependencies for the
wvstreams project (which is no entirely redo-ized). Beware that
currently wvstreams relies on my original redo-whichdo syntax,
although I think you've convinced me to switch to your proposed
whichdo syntax.
A much uglier project is buildroot, which I've been working to
partially switch over to redo, with great success so far. Startup
time is much shorter than with (non-recursive) make, and yet
dependency handling is clearer with redo, so there can be more
parallelism, thus also reducing build times. buildroot extracts many
full-sized open source packages and wants to know what to rebuild when
you change one of the source files in any of them, so the dependency
tree is quite a nightmare. apenwarr/redo is still annoyingly slow
when I add all the dependencies I want, and that's with all my
optimizations currently in place, so it's a great place to look for
further improvements. I haven't released my buildroot patches yet
because they're too messy, however.
> As I have written before, I wrote my redo implementation because the
> build process I had should neither have stale builds, nor do superfluous
> rebuilds. As I understand it, your implementation basically codifies the
> opinion that superfluous rebuilds are worse than stale builds – while my
> implementation basically codifies the opinion that a build system should
> do the most it can to make sure that nothing in a build is out of date …
> so ”Better fast than correct!” or vice versa. Would you agree with that?
No, that's not how I would phrase it. Both your version and my
version are correct, for different definitions of correct. My version
expects dependencies to be in the shape of a DAG and that any
dependency will not change during a run unless redo is what changed
it. Even so, if you violate these expectations (change a dependency
during a run, from outside redo) it will react in a predictable way:
it will consider the right targets to be out of date even after redo
returns. This is not incorrect, it's a well-defined model that
doesn't match yours.
> Another suggestion: Your approach works if all sources are idempotent –
> which could be approximately true for files, but mostly wrong for other
> things. My approach does not care about that property.
I assume you mean 'targets are idempotent' here, not sources. Yes, I
suppose that's an implicit assumption I've been using. It's true that
if you have a random.do that reads a bunch of bytes from /dev/urandom,
you might be in for some unexpected results.
> One more thing: Since “do” does always rebuild and has no stat cache, it
> is basically correct from my point of view. It does remove targets (that
> surely is a problem) – but it will never erroneously not build a target.
That was the intention, that it be the simplest possible thing that's
basically correct, albeit inefficient. However, even minimal/do will
only build a redo-always target once per run. Since it doesn't
remember dependencies at all, this was the only way to implement
redo-ifchange without building every dependency again every time any
target depended on it.
Have fun,
Avery