Add option to delete targets for failed actions simliar to make's DELETE_ON_ERROR
Describe the bug
When a command fails after writing its output file, SCons doesn't delete the target file.
Add feature like make's DELETE_ON_ERROR to allow optional deletion of targets of actions which error out.
See also StackOverFlow question
Required information
- Link to SCons Users thread discussing your issue.
https://pairlist4.pair.net/pipermail/scons-users/2025-July/009515.html
-
Version of SCons 4.8.1
-
Version of Python 3.13.1
-
Which python distribution if applicable (python.org, cygwin, anaconda, macports, brew,etc)
python.org (I think)
- How you installed SCons
pip install scons
- What Platform are you on? (Linux/Windows and which version)
macOS 15.5 Apple Silicon
- How to reproduce your issue?
Extract the three files below SConstruct, command.sh and run.sh, make command.sh and run.sh executable and run run.sh.
The command command.sh copies file1 to file2. If file1 starts with 'bad' it exits with an error code to simulate a crash after the output file was written. SCons is executed three times with file1 having the content "good", "bad", and "good" respectively. At the end of the third scons runs we expect file2 to have "good" but it contains "bad".
- File
SConstruct:
# SCons environment
env = Environment()
# Copy file1 → file2 using command.sh
# Inject an error if file1 starts with 'bad"
file2 = env.Command(
target='file2',
source='file1',
action='./command.sh $SOURCE $TARGET'
)
# Make 'file2' the default target
Default(file2)
- File
command.sh:
#!/bin/bash
# Usage: ./command input > output
# Copy source to destination
cp "$1" "$2"
# If the input file stats with 'bad' inject an error AFTER creating the output file.
first_line=$(head -n 1 "$1")
if [[ "$first_line" == bad* ]]; then
exit 1
fi
exit 0
- File
run.sh:
#!/bin/bash
# Clean up.
rm -f .sconsign.dblite
rm -f file[12]
values=('good' 'bad' 'good')
i=0
for val in "${values[@]}"; do
((i++))
echo
echo "----- Iteration $i: file1 = '$val' -----"
echo
echo "$val" > file1
scons
echo
echo "File1"
md5 file1
cat -n file1
echo
echo "File2"
md5 file2
cat -n file2
echo
echo "DBlite:"
sconsign .sconsign.dblite
done
exit
- Output log
$ ./run.sh
----- Iteration 1: file1 = 'good' -----
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
./command.sh file1 file2
scons: done building targets.
File1
MD5 (file1) = d7f986677d9f563bd1794b09d82206a3
1 good
File2
MD5 (file2) = d7f986677d9f563bd1794b09d82206a3
1 good
DBlite:
=== .:
file1: d7f986677d9f563bd1794b09d82206a3 1752702994 5
file2: d7f986677d9f563bd1794b09d82206a3 1752702995 5
file1: d7f986677d9f563bd1794b09d82206a3 1752702994 5
2dbc2dce125a753309a27b7d5157aaaa [./command.sh $SOURCE $TARGET]
----- Iteration 2: file1 = 'bad' -----
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
./command.sh file1 file2
scons: *** [file2] Error 1
scons: building terminated because of errors.
File1
MD5 (file1) = df207dc9143c6fabf60b69b9c3035103
1 bad
File2
MD5 (file2) = df207dc9143c6fabf60b69b9c3035103
1 bad
DBlite:
=== .:
file1: df207dc9143c6fabf60b69b9c3035103 1752702995 4
file2: d7f986677d9f563bd1794b09d82206a3 1752702995 5
file1: d7f986677d9f563bd1794b09d82206a3 1752702994 5
2dbc2dce125a753309a27b7d5157aaaa [./command.sh $SOURCE $TARGET]
----- Iteration 3: file1 = 'good' -----
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
scons: `file2' is up to date.
scons: done building targets.
File1
MD5 (file1) = d7f986677d9f563bd1794b09d82206a3
1 good
File2
MD5 (file2) = df207dc9143c6fabf60b69b9c3035103
1 bad
DBlite:
=== .:
file1: d7f986677d9f563bd1794b09d82206a3 1752702995 5
file2: d7f986677d9f563bd1794b09d82206a3 1752702995 4
file1: d7f986677d9f563bd1794b09d82206a3 1752702995 5
2dbc2dce125a753309a27b7d5157aaaa [./command.sh $SOURCE $TARGET]
/projects/apio-dev/repo/experimental/scons_bug$
- How you invoke scons (The command line you're using "scons --flags some_arguments")
Running scons three times via run.sh.
Files for example 4747.tgz
Hi @bdbaddog, can you explain what 'Add option to delete targets for failed actions simliar to make's DELETE_ON_ERROR mean?
I think that SCons should handle this example correctly by default, without having to have a special configuration. If this will come at a significant computational cost, the option should be to disable it, at the user's risk. That is, conservative correctness as the default.
SCons as with most build systems has an expectation that if a build command fails, no files are generated.
Make has an optional way to delete target files for build targets which fail. If SCons had such, then setting that optional behavoir would resolve your issue.
(Though I've already pointed out in email that you just need to change one script and your build will work).
So therefore I'll categorize this request as an enhancement request.
The example we provided is just an simplified example to demonstrate the issue we have with a real life command that we don't control so it's not something we can fix.
I may missed the part about DELETE_ON_FAILURE, is in an existing env var that SCons currently observes or just a feature request?
After the third invocation SCons reports that everything is build OK but there is a discrepancy between the actual MD5 of file2 and the one in the database. Is this acceptable?
Please reread my previous comment. Also don't post screenshots, just post the text.
Do you mean DELETE_ON_ERROR?
https://www.gnu.org/software/make/manual/html_node/Special-Targets.html
Yes, updated above text.
I see. Yes, this will do it. Or potentially, SCons can just mark them in the database as dirty.
Thanks for looking into it!
The "Fix Issue 4747" record above is not related to this issue.
The "Fix Issue 4747" record above is not related to this issue.
I updated the commit message, so in theory the tag above should go away?
This is an updated version of the delete-target-on-failure patch that was proposed in #2718 a long time ago. Possibly could use as a basis, adding the DELETE_ON_ERROR option equivalent to make it conditional:
diff --git a/SCons/Script/Main.py b/SCons/Script/Main.py
index 98f1f345a..861507bf5 100644
--- a/SCons/Script/Main.py
+++ b/SCons/Script/Main.py
@@ -303,6 +303,13 @@ class BuildTask(SCons.Taskmaster.OutOfDateTask):
t, e = exc_info
tb = None
+ if not issubclass(SCons.Errors.ExplicitExit, t):
+ for t in self.targets:
+ try:
+ t.fs.unlink(t.path)
+ except:
+ pass
+
# Deprecated string exceptions will have their string stored
# in the first entry of the tuple.
if e is None:
@mwichmann - should we message that we're deleting because of error?
Summary of some questions after discussion here and elsewhere. Mark these off as a final decision is made.
- [ ] Granularity of feature: per-incovation, per-environment, per-builder, per-builder-call, or some combination? Current feeling is "per-invocation" with a command-line arg and matching
SetOption. - [ ] Name of cli argument if that route is chosen. Is
--delete-on-errorok?--remove-on-errorsince we have another "remove" option? - [ ] If feature is added, is it the new default, or does it default off with a cutover ("deprecation") cycle?
- [ ] If the failed target is one of several built from the same builder-call/action, should the other targets also be removed?
- [ ] Interaction with
--ignore-errors, error-ignoring Actions,Precious(). - [ ] Interaction with cachedir.? #1332 reports an existing issue with invalid targets being cached.
- [ ] If there's an existing cachedir entry for the removed file, should the cached file be restored?
- [ ] Should there be a warning/notification in case this actually leada to a target being removed?
@mwichmann - should we message that we're deleting because of error?
I just added that question to the checklist.
Hmmm, one more nitpick to consider: the version in Make says it will delete the target ... if it has changed and its recipe exits with a nonzero exit status. Should we check for the "if it has changed" condition?
Hmmm, one more nitpick to consider: the version in Make says it will delete the target ... if it has changed and its recipe exits with a nonzero exit status. Should we check for the "if it has changed" condition?
Good point. Do we have a hash for that target?
I presume we must if it already existed.