Skip to content

Commit 3e60ee1

Browse files
authored
Merge branch 'master' into rawfile-convert
2 parents 180c601 + b8fffb3 commit 3e60ee1

File tree

11 files changed

+186
-96
lines changed

11 files changed

+186
-96
lines changed

CHANGES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
6868
- CacheDir writes no longer happen within the taskmaster critical section,
6969
and therefore can run in parallel with both other CacheDir writes and the
7070
taskmaster DAG walk.
71+
- The NewParallel scheduler now only adds threads as new work requiring execution
72+
is discovered, up to the limit set by -j. This should reduce resource utilization
73+
when the achievable parallelism in the DAG is less than the -j limit.
7174

7275
From Mats Wichmann:
7376
- Add support for Python 3.13 (as of alpha 2). So far only affects
@@ -83,6 +86,11 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
8386
- Improve handling of file data that SCons itself processes - try
8487
harder to decode non-UTF-8 text. SCons.Util.to_Text now exists
8588
to convert a byte stream, such as "raw" file data. Fixes #3569, #4462.
89+
The Pseudo manpage entry was updated to provide more clarity.
90+
- The internal routine which implements the PyPackageDir function
91+
would fail with an exception if called with a module which is
92+
not found. It will now return None. Updated manpage entry and
93+
docstring..
8694

8795

8896
RELEASE 4.6.0 - Sun, 19 Nov 2023 17:22:20 -0700

RELEASE.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ FIXES
5656
but Pseudo() did not work; is now enabled.
5757
- Improve handling of file data that SCons itself processes - as in
5858
scanners - try harder to decode non-UTF-8 text.
59+
- PyPackageDir no longer fails if passed a module name which cannot be found,
60+
now returns None.
61+
5962

6063
IMPROVEMENTS
6164
------------
@@ -69,6 +72,9 @@ IMPROVEMENTS
6972
(Larger -j values)
7073
- CacheDir writes no longer happen within the taskmaster critical section, and therefore
7174
can run in parallel with both other CacheDir writes and the taskmaster DAG walk.
75+
- The NewParallel scheduler now only adds threads as new work requiring execution
76+
is discovered, up to the limit set by -j. This should reduce resource utilization
77+
when the achievable parallelism in the DAG is less than the -j limit.
7278

7379

7480
PACKAGING
@@ -82,6 +88,7 @@ DOCUMENTATION
8288
- Fixed the Scanner examples in the User Guide to be runnable and added
8389
some more explantion. Clarified discussion of the scanner function in
8490
the Scanner Objects section of the manpage.
91+
- The manpage entry for Pseudo was clarified.
8592

8693
DEVELOPMENT
8794
-----------

SCons/Environment.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2339,6 +2339,7 @@ def Local(self, *targets):
23392339
return ret
23402340

23412341
def Precious(self, *targets):
2342+
"""Mark *targets* as precious: do not delete before building."""
23422343
tlist = []
23432344
for t in targets:
23442345
tlist.extend(self.arg2nodes(t, self.fs.Entry))
@@ -2347,6 +2348,7 @@ def Precious(self, *targets):
23472348
return tlist
23482349

23492350
def Pseudo(self, *targets):
2351+
"""Mark *targets* as pseudo: must not exist."""
23502352
tlist = []
23512353
for t in targets:
23522354
tlist.extend(self.arg2nodes(t, self.fs.Entry))
@@ -2355,13 +2357,17 @@ def Pseudo(self, *targets):
23552357
return tlist
23562358

23572359
def Repository(self, *dirs, **kw) -> None:
2360+
"""Specify Repository directories to search."""
23582361
dirs = self.arg2nodes(list(dirs), self.fs.Dir)
23592362
self.fs.Repository(*dirs, **kw)
23602363

23612364
def Requires(self, target, prerequisite):
2362-
"""Specify that 'prerequisite' must be built before 'target',
2363-
(but 'target' does not actually depend on 'prerequisite'
2364-
and need not be rebuilt if it changes)."""
2365+
"""Specify that *prerequisite* must be built before *target*.
2366+
2367+
Creates an order-only relationship, not a full dependency.
2368+
*prerequisite* must exist before *target* can be built, but
2369+
a change to *prerequisite* does not trigger a rebuild of *target*.
2370+
"""
23652371
tlist = self.arg2nodes(target, self.fs.Entry)
23662372
plist = self.arg2nodes(prerequisite, self.fs.Entry)
23672373
for t in tlist:

SCons/Environment.xml

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,20 +2880,41 @@ and &f-link-env-Prepend;.
28802880
</arguments>
28812881
<summary>
28822882
<para>
2883-
This returns a Directory Node similar to Dir.
2884-
The python module / package is looked up and if located
2885-
the directory is returned for the location.
2886-
<parameter>modulename</parameter>
2887-
Is a named python package / module to
2888-
lookup the directory for it's location.
2889-
</para>
2890-
<para>
2891-
If
2892-
<parameter>modulename</parameter>
2893-
is a list, SCons returns a list of Dir nodes.
2883+
Finds the location of <parameter>modulename</parameter>,
2884+
which can be a string or a sequence of strings,
2885+
each representing the name of a &Python; module.
28942886
Construction variables are expanded in
28952887
<parameter>modulename</parameter>.
2888+
Returns a Directory Node (see &f-link-Dir;),
2889+
or a list of Directory Nodes if
2890+
<parameter>modulename</parameter> is a sequence.
2891+
<literal>None</literal> is returned for any module not found.
2892+
</para>
2893+
2894+
<para>
2895+
When a Tool module which is installed as a
2896+
&Python; module is used, you need
2897+
to specify a <parameter>toolpath</parameter> argument to
2898+
&f-link-Tool;,
2899+
&f-link-Environment;
2900+
or &f-link-Clone;,
2901+
as tools outside the standard project locations
2902+
(<filename>site_scons/site_tools</filename>)
2903+
will not be found otherwise.
2904+
Using &f-PyPackageDir; allows this path to be
2905+
discovered at runtime instead of hardcoding the path.
28962906
</para>
2907+
2908+
<para>
2909+
Example:
2910+
</para>
2911+
2912+
<example_commands>
2913+
env = Environment(
2914+
tools=["default", "ExampleTool"],
2915+
toolpath=[PyPackageDir("example_tool")]
2916+
)
2917+
</example_commands>
28972918
</summary>
28982919
</scons_function>
28992920

@@ -2988,6 +3009,10 @@ but the target file(s) do not actually
29883009
depend on the prerequisites
29893010
and will not be rebuilt simply because
29903011
the prerequisite file(s) change.
3012+
<parameter>target</parameter> and
3013+
<parameter>prerequisite</parameter> may each
3014+
be a string or Node, or a list of strings or Nodes.
3015+
Returns a list of the affected target nodes.
29913016
</para>
29923017

29933018
<para>

SCons/Node/FS.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,7 @@ def get_root(self, drive):
12951295
self.Root[''] = root
12961296
return root
12971297

1298-
def _lookup(self, p, directory, fsclass, create: int=1):
1298+
def _lookup(self, p, directory, fsclass, create: bool = True):
12991299
"""
13001300
The generic entry point for Node lookup with user-supplied data.
13011301
@@ -1431,7 +1431,7 @@ def _lookup(self, p, directory, fsclass, create: int=1):
14311431

14321432
return root._lookup_abs(p, fsclass, create)
14331433

1434-
def Entry(self, name, directory = None, create: int = 1):
1434+
def Entry(self, name, directory = None, create: bool = True):
14351435
"""Look up or create a generic Entry node with the specified name.
14361436
If the name is a relative path (begins with ./, ../, or a file
14371437
name), then it is looked up relative to the supplied directory
@@ -1440,7 +1440,7 @@ def Entry(self, name, directory = None, create: int = 1):
14401440
"""
14411441
return self._lookup(name, directory, Entry, create)
14421442

1443-
def File(self, name, directory = None, create: int = 1):
1443+
def File(self, name, directory = None, create: bool = True):
14441444
"""Look up or create a File node with the specified name. If
14451445
the name is a relative path (begins with ./, ../, or a file name),
14461446
then it is looked up relative to the supplied directory node,
@@ -1487,21 +1487,24 @@ def Repository(self, *dirs) -> None:
14871487
d = self.Dir(d)
14881488
self.Top.addRepository(d)
14891489

1490-
def PyPackageDir(self, modulename):
1491-
r"""Locate the directory of a given python module name
1490+
def PyPackageDir(self, modulename) -> Optional[Dir]:
1491+
r"""Locate the directory of Python module *modulename*.
14921492
1493-
For example scons might resolve to
1494-
Windows: C:\Python27\Lib\site-packages\scons-2.5.1
1495-
Linux: /usr/lib/scons
1493+
For example 'SCons' might resolve to
1494+
Windows: C:\Python311\Lib\site-packages\SCons
1495+
Linux: /usr/lib64/python3.11/site-packages/SCons
14961496
1497-
This can be useful when we want to determine a toolpath based on a python module name"""
1497+
Can be used to determine a toolpath based on a Python module name.
14981498
1499-
dirpath = ''
1500-
1501-
# Python3 Code
1499+
This is the backend called by the public API function
1500+
:meth:`~Environment.Base.PyPackageDir`.
1501+
"""
15021502
modspec = importlib.util.find_spec(modulename)
1503-
dirpath = os.path.dirname(modspec.origin)
1504-
return self._lookup(dirpath, None, Dir, True)
1503+
if modspec:
1504+
origin = os.path.dirname(modspec.origin)
1505+
return self._lookup(origin, directory=None, fsclass=Dir, create=True)
1506+
else:
1507+
return None
15051508

15061509

15071510
def variant_dir_target_climb(self, orig, dir, tail):

SCons/Node/FSTests.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4046,6 +4046,23 @@ def test_root_lookup_equivalence(self) -> None:
40464046
os.chdir(save_cwd)
40474047

40484048

4049+
class PyPackageDir(unittest.TestCase):
4050+
def runTest(self) -> None:
4051+
"""Test calling the PyPackageDir() method.
4052+
4053+
We don't want to mock the positive case here - there's
4054+
testing for that in E2E test test/Dir/PyPackageDir.
4055+
We're only making sure we don't die in the negative case
4056+
(module not found) and instead return None.
4057+
"""
4058+
fs = SCons.Node.FS.FS('/')
4059+
try:
4060+
pkdir = fs.PyPackageDir("garglemod")
4061+
except AttributeError:
4062+
self.fail("non-existent module raised AttributeError")
4063+
self.assertIsNone(pkdir)
4064+
4065+
40494066
if __name__ == "__main__":
40504067
unittest.main()
40514068

SCons/Node/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ def set_precious(self, precious: int = 1) -> None:
12301230
self.precious = precious
12311231

12321232
def set_pseudo(self, pseudo: bool = True) -> None:
1233-
"""Set the Node's precious value."""
1233+
"""Set the Node's pseudo value."""
12341234
self.pseudo = pseudo
12351235

12361236
def set_noclean(self, noclean: int = 1) -> None:
@@ -1250,7 +1250,7 @@ def set_always_build(self, always_build: int = 1) -> None:
12501250
self.always_build = always_build
12511251

12521252
def exists(self) -> bool:
1253-
"""Does this node exists?"""
1253+
"""Reports whether node exists."""
12541254
return _exists_map[self._func_exists](self)
12551255

12561256
def rexists(self):

SCons/Script/Main.xml

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -725,13 +725,12 @@ Progress(['-\r', '\\\r', '|\r', '/\r'], interval=5)
725725
</arguments>
726726
<summary>
727727
<para>
728-
Marks each given
729-
<varname>target</varname>
730-
as precious so it is not deleted before it is rebuilt. Normally
731-
&scons;
732-
deletes a target before building it.
733-
Multiple targets can be passed in to a single call to
734-
&f-Precious;.
728+
Marks <varname>target</varname> as precious so it is not
729+
deleted before it is rebuilt.
730+
Normally &SCons; deletes a target before building it.
731+
Multiple targets can be passed in a single call,
732+
and may be strings and/or nodes.
733+
Returns a list of the affected target nodes.
735734
</para>
736735
</summary>
737736
</scons_function>
@@ -742,16 +741,24 @@ Multiple targets can be passed in to a single call to
742741
</arguments>
743742
<summary>
744743
<para>
745-
This indicates that each given
746-
<varname>target</varname>
747-
should not be created by the build rule, and if the target is created,
748-
an error will be generated. This is similar to the gnu make .PHONY
749-
target. However, in the vast majority of cases, an
750-
&f-Alias;
751-
is more appropriate.
752-
753-
Multiple targets can be passed in to a single call to
754-
&f-Pseudo;.
744+
Marks <parameter>target</parameter> as a pseudo target,
745+
not representing the production of any physical target file.
746+
If any pseudo <parameter>target</parameter> does exist,
747+
&SCons; will abort the build with an error.
748+
Multiple targets can be passed in a single call,
749+
and may be strings and/or Nodes.
750+
Returns a list of the affected target nodes.
751+
</para>
752+
753+
<para>
754+
&f-Pseudo; may be useful in conjuction with a builder
755+
call (such as &f-link-Command;) which does not create a physical target,
756+
and the behavior if the target accidentally existed would be incorrect.
757+
This is similar in concept to the GNU <application>make</application>
758+
<literal>.PHONY</literal> target.
759+
&SCons; also provides a powerful target alias capability
760+
(see &f-link-Alias;) which may provide more flexibility
761+
in many situations when defining target names that are not directly built.
755762
</para>
756763
</summary>
757764
</scons_function>

SCons/Taskmaster/Job.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ def __exit__(self, *args):
474474

475475
def __init__(self, taskmaster, num, stack_size) -> None:
476476
self.taskmaster = taskmaster
477-
self.num_workers = num
477+
self.max_workers = num
478478
self.stack_size = stack_size
479479
self.interrupted = InterruptState()
480480
self.workers = []
@@ -484,7 +484,7 @@ def __init__(self, taskmaster, num, stack_size) -> None:
484484
# also protects access to our state that gets updated
485485
# concurrently. The `can_search_cv` is associated with
486486
# this mutex.
487-
self.tm_lock = (threading.Lock if self.num_workers > 1 else NewParallel.FakeLock)()
487+
self.tm_lock = (threading.Lock if self.max_workers > 1 else NewParallel.FakeLock)()
488488

489489
# Guarded under `tm_lock`.
490490
self.jobs = 0
@@ -493,11 +493,11 @@ def __init__(self, taskmaster, num, stack_size) -> None:
493493
# The `can_search_cv` is used to manage a leader /
494494
# follower pattern for access to the taskmaster, and to
495495
# awaken from stalls.
496-
self.can_search_cv = (threading.Condition if self.num_workers > 1 else NewParallel.FakeCondition)(self.tm_lock)
496+
self.can_search_cv = (threading.Condition if self.max_workers > 1 else NewParallel.FakeCondition)(self.tm_lock)
497497

498498
# The queue of tasks that have completed execution. The
499499
# next thread to obtain `tm_lock`` will retire them.
500-
self.results_queue_lock = (threading.Lock if self.num_workers > 1 else NewParallel.FakeLock)()
500+
self.results_queue_lock = (threading.Lock if self.max_workers > 1 else NewParallel.FakeLock)()
501501
self.results_queue = []
502502

503503
if self.taskmaster.trace:
@@ -516,22 +516,27 @@ def trace_message(self, message) -> None:
516516
method_name = sys._getframe(1).f_code.co_name + "():"
517517
thread_id=threading.get_ident()
518518
self.trace.debug('%s.%s [Thread:%s] %s' % (type(self).__name__, method_name, thread_id, message))
519-
# print('%-15s %s' % (method_name, message))
520519

521520
def start(self) -> None:
522-
if self.num_workers == 1:
521+
if self.max_workers == 1:
523522
self._work()
524523
else:
525-
self._start_workers()
526-
for worker in self.workers:
527-
worker.join()
528-
self.workers = []
524+
self._start_worker()
525+
while len(self.workers) > 0:
526+
self.workers[0].join()
527+
self.workers.pop(0)
529528
self.taskmaster.cleanup()
530529

531-
def _start_workers(self) -> None:
530+
def _maybe_start_worker(self) -> None:
531+
if self.max_workers > 1 and len(self.workers) < self.max_workers:
532+
if self.jobs >= len(self.workers):
533+
self._start_worker()
534+
535+
def _start_worker(self) -> None:
532536
prev_size = self._adjust_stack_size()
533-
for _ in range(self.num_workers):
534-
self.workers.append(NewParallel.Worker(self))
537+
if self.trace:
538+
self.trace_message("Starting new worker thread")
539+
self.workers.append(NewParallel.Worker(self))
535540
self._restore_stack_size(prev_size)
536541

537542
def _adjust_stack_size(self):
@@ -680,6 +685,11 @@ def _work(self):
680685
self.trace_message("Found task requiring execution")
681686
self.state = NewParallel.State.READY
682687
self.can_search_cv.notify()
688+
# This thread will be busy taking care of
689+
# `execute`ing this task. If we haven't
690+
# reached the limit, spawn a new thread to
691+
# turn the crank and find the next task.
692+
self._maybe_start_worker()
683693

684694
else:
685695
# We failed to find a task, so this thread

0 commit comments

Comments
 (0)