-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix infinite loop when --dry-running autoconf projects. #500
base: main
Are you sure you want to change the base?
Conversation
@drok, this is very cool. Even if you don't finish the PR, this is a great pointer for us to continue. I wasn't aware of this possible solution to the infinite loop problem which I considered by design and I thought only removing always-make could somewhat help (with other negative consequences in some scenarios). Regarding passing the flags to the sub-makes. I will test soon. Did I understand right from the recursion link you provided, that we can define MAKEFLAGS in the environment of the main process and so all the sub-make process will see it? The extension can inject this variable into the environment before we spawn the first dry-run. So maybe we don't need AM_MAKEFLAGS at all? Regarding target "all". Why is that needed? How about we let the default target or whichever target the user defines via the makefile UI? Just let me know what would happen to the command line if "all" would be missing or replaced by some other "build1" for example target. Regarding the makefile specified as target and as assume_old. Sometimes the user points to a makefile somewhere else (or even named differently) via "makefile.makefilePath". Would work the same if instead of the simple most common case of "Makefile" we would have "dir1/dir2/MyMakefile"? I'll test but you can also answer if you happen to know or it's quick to try. |
Hi Andrea, thank you for taking up this issue; I will answer your questions below:
|
@drok , thank you for the additional info, very useful and we will consider it all. Lucky that indeed the most common cases can be addressed with the simpler approach. I will open a work item for the cases that we will not be able to address, including an eventual fork of make or even a contribution into the main one, if it will be desired. Brainstorm: I am not familiar yet with automake to write myself or play much with it other than just run the usual commands on an existing framework. Can you see a solution where the Makefile Tools extension would use the original makefile of the project as source-input, we call automake ourselves to generate a new makefile which will be identical in content but at least can inherit the variables we need? Then the dry-run will be invoked on that newly generated makefile. I'll be thinking and maybe you can also add on top of this idea or let me know if you see this wouldn't help at all. |
I would recommend Hello World(automake) as a good place to start.
Fortunately, this behaviour is built in. See the crafted search below, of the automake source code, which shows that in the Makefile templates that Maybe I misunderstood the idea. How, specifically, would the alternate makefile you envision, be different than the makefile that automake generates by default?
This would be very error prone. Complex projects often include makefiles into makefiles to avoid re-writing common rules or definitions multiple times, in a similar way to how C source code uses #include statements. String manipulation of a C source code file, without rigorous expansion of the inclusions and preprocessor definitions can only end in tears. It's the same with Makefiles. I do believe that the Makefiles that are normally generated by default have all the features needed to solve this dry-run problem (namely, the |
That idea was for projects that don't use automake. You listed this case as one of the future problems to solve. I thought we could invoke automake ourselves when it's not part of the process, but without changing any file belonging to the user (that is why I proposed we generate a different new makefile - even if identical - the user doesn't need to worry about and we point the extension towards that and invoking which will also have all the flags). And yes, no string preprocessing to inject flags for sub-make calls. |
Generating a makefile from an existing makefile makes no sense to me, so I feel I need to describe what What is
|
562ea62
to
e03fcd9
Compare
'all' is a well-known name The default target must be explicitly named on the dry-run command line, because of [Remaking Makefiles](https://www.gnu.org/software/make/manual/html_node/Remaking-Makefiles.html) rule of GNU Make. In order to avoid remaking makefiles during a dry-run, the makefile must also be given as an explicit target. If the makefile were given as the only target, the dry-run would not mean 'dry-run the default target', but 'dry-run nothing'. Thus, if the makefile must be given as a target, the default target must be made explicit, and thus is must be defined. Also, previously 'all' was added to the make invokation for listing targets. This would prevent the .DEFAULT_GOAL to be correctly set as the Makefile declared, and instead be set to the value given on the command line. An explicit target is no longer given on the --print-data-base invocation, so the output .DEFAULT_GOAL will be the genuine default goal. This commit makes currentTarget always defined, with the default value 'all'; this is a reasonable, well-known default.
The top makefile in projects that use autotools and automake contains a rule to remake the makefile itself when the configuration changes (configure.ac). Even when dry-running, GNU make regenerates the makefile, in a bid to generate a 'correct' dry-run output. VScode needs to add --always-make in order to get a complete view of the dry-run. Without it, it would only get the commands needed for outdated targets. These two behaviours combined cause a naive 'make --dry-run --always-make' to continuously remake the Makefile. In order to avoid this infinite loop, make must be instructed as in the "Remaking Makefiles" man page, to avoid remaking the makefile. This is done by adding two options: --assume-old=Makefile, and Makefile (ie, target). Make requires the Makefile to be explicitly specified as target, otherwise it ignores the --assume-old=Makefile option. Furthermore, Makefiles generated by automake cause the top invocation to be a recursive sub-make invocation. On recursive makes, make itself calls submake without passing the --assume-old option, thus breaking the combo required for the sub-make to avoid remaking the makefile. As a result, automake Makefiles need one more workaround. The --assume-old option must be manually passed to sub-make via the AM_MAKEFLAGS, which is always appended to sub-make's command line. This commit implements the above incantation to enable automake Makefiles to be dry-run without an infinite loop. Additionally, the makefilePath, makeDirectory, updatedMakefilePath and updatedMakeDirectory variables are made to store the respective setting, rather then re-purposing them to store the corresponding resolved, absolute paths. makefilePath cannot be undefined because for dry-running the name of the makefile must always be supplied on the make commandline.
e03fcd9
to
ba69555
Compare
@andreeis, I have (force-pushed) updated this fork with a complete implementation of the infinite loop fix. Compared with my original implementation (562ea62), the following improvements were made:
The current PR is also tested on a project that uses recursive make, and contains one nested auto-tools sub-project. Regarding the original reasons for marking it WIP:
Thus, I am now removing the draft marker from this PR. It is ready. Future work: I plan to fork GNU make and include it as the dry-running tool, as I describe above - Is there a better way?, eventually, and with no particular rush. I currently am not affected by the non-automake recursive dry-running corner case in any of my projects. @microsoft-github-policy-service agree |
Thank you for the last long explanation. Very useful. I see now :). |
let currentTarget: string | undefined; | ||
export function getCurrentTarget(): string | undefined { return currentTarget; } | ||
export function setCurrentTarget(target: string | undefined): void { currentTarget = target; } | ||
// Assume the well-known "all" target for the default goal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. Before this change, if the user does not specify a build target, we invoke "make" (eventually with other args, coming via other switches) but no target mentioned which will result in "make" looking only at the first (if I understand correctly). Now for such scenario suddenly "make" would build "all" even if the user did not specify "all".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is that in order to avoid the infinite loop, you must add Makefile
as a target, no matter what. Giving no other target is not an option, because make would not attempt to build anything, there would be no compilers invoked, and thus no useful information to be parsed (defines and includes).
The target "all" is assumed all over the code, so that's why I thought it's already baked in. If you want to do it more correctly, you'd have to call make twice. The first time with --print-data-base --dry-run --assume-old=Makefile Makefile
, from which you'd extract the .DEFAULT_GOAL
variable, and the second time with --dry-run --assume-old=Makefile Makefile ${.DEFAULT_GOAL}
- then no assumptions are made about the default target, and you get useful tool invocations from the 2nd --dry-run.
In fact, the output of the first run would be useful to extract the targets in the current directory. (so you'd get the targets first, then the tool invocations)
I see the common parts with the other PRs. I mentioned some questions for the common code in the other PRs. Did not test yet but I'll get back to this soon. It's possible you'll have a chance to reply and look at test failures by then. |
the existing tests fail to be meaningful, and reviewing failures is not a good use of time; likewise, putting any faith in "passing" tests is unwise. I have taken this as the drok#1 issue with this project. The lack of meaningful testing discourages contribution from the community. |
When --dry-running autoconf projects, the default make invocation flags, along causes the dry-run process to enter an infinite loop where the Makefile is updated, by running config.status, which requires reconfiguration.
The problem
The infinite loop when dry-running autoconf projects is due to
GNU Make
's documented behaviour. It's not a bug, or version-specific behaviour:GNU Make remakes files makefiles unconditionally (defined as files it reads rules from, eg, given a statement "include blah.inc", the file
blah.inc
is a "makefile"). Remaking Makefiles says "--dry-run
does not prevent updating of makefiles, because an out-of-date makefile would result in the wrong output for other targets"Automake creates rules for updating the Makefile when
Makefile.am
changes, and whenconfigure.ac
changes.In recursive builds, when make spawns a sub-make process, it does not pass "
--assume-old
" arguments to child make via theMAKEFLAGS
mechanism. This is described in Communicating Options to a Sub-makeTogether, these three behaviours guarantee that an autoconf+automake project will end up in an infinite loop when "
--dry-run
" as the vscode "Makefile Tools" extension does by default when projects are loaded.Solution
The solution is to change the command line options as follows (
settings.json
excerpt):If using the GUI, each of these strings must appear on one line. Specifically,
AM_MAKEFLAGS=--assume-old=Makefile Makefile
is one argument. In a bash command line, this would be quoted likeAM_MAKEFLAGS="--assume-old=Makefile Makefile"
Explanation
The
--assume-old=Makefile
means the file named 'Makefile', which is also a target, will not be considered for remake, in spite of--always-make
, per Avoiding Compilation.Since the file
Makefile
is also a file (ie, used to read rules from), it would be re-made in spite of "--dry-run". This behaviour is overriden by specifyingMakefile
as a target, in addition toall
, as documented in Remaking Makefiles: "on occasion you might actually wish to prevent updating of even the makefiles. You can do this by specifying the makefiles as goals in the command line as well as specifying them as makefiles. When the makefile name is specified explicitly as a goal, the options ‘-t’ and so on do apply to them."Since on recursive builds the
--assume-old
argument would not be passed to the sub-make via make's ownMAKEFLAGS
environment variable mechanism, theAM_MAKEFLAGS
variable is used to perform override this behaviour. This works because automake projects add the value of this variable on every make invocation.Note 1. If your project defines the
AM_MAKEFLAGS
variable in Makefile.am, you'll need to adjust it to include the needed--assume-old
incantation. One way would be:Then, use use
DRYRUN_MAKEFLAGS="--assume-old=Makefile Makefile"
insettings.json
.Note 2. Which of the
settings.json
files you put the "makefile.dryrunSwitches" settings matters. If you work on autoconf as well as cmake projects, you might want it in each project'ssettings.json
. I have it set at the user level, as I work on very few cmake projects, and then I can override in at the project level.Marked WIP because of these issues:
--assume-old
to sub-makes should be found.The problem was reported at StackOverflow, and this is my solution to that question.
This commit changes the default
makefile.dryrunSwitches
flags as documented above.