Repository: SCons/scons

Issue:
Add support for sources specified as pathlib.Path objects
A discord discussion suggested it would be helpful if SCons could accept `Path` objects (`os.PathLike` instances). Currently, this fails, as in this trivial example:

```py
# SConstruct
from pathlib import Path

Program(target='hello', source=[Path('hello.c')])
```
Error:
```
$ scons
scons: Reading SConscript files ...

scons: *** Do not know how to create a target from source `hello.c'
File "/home/user/testproject/SConstruct", line 3, in <module>
```

Note that in the error message it has been converted to a string (a simple `str` call is usually sufficient), but internally it didn't get recognized.

**Required information**
* Link to SCons Discord thread discussing your issue: https://discord.com/channels/571796279483564041/571796280146133047/1138124568293290084
* Version of SCons: 4.5.2 / git master (applies to earlier as well)
* Version of Python: 3.11 (applicable from 3.5 onwards)

There are undoubtedly other aspects to supporting `os.PathLike` objects, but this one is simple to grasp. We can think about other places one might specify a path to something in Python code (CLI arguments like `-f SConstruct`, `--cache-debug=file`, etc. don't count, there's no `pathlib` in effect there) as a separate topic.


Discussion:

Resolving patch:
diff --git a/SCons/Builder.py b/SCons/Builder.py
--- a/SCons/Builder.py
+++ b/SCons/Builder.py
@@ -99,7 +99,9 @@
 
 """
 
+import os
 from collections import UserDict, UserList
+from contextlib import suppress
 
 import SCons.Action
 import SCons.Debug
@@ -479,6 +481,11 @@ def _adjustixes(self, files, pre, suf, ensure_suffix: bool=False):
             files = [files]
 
         for f in files:
+            # fspath() is to catch PathLike paths. We avoid the simpler
+            # str(f) so as not to "lose" files that are already Nodes:
+            # TypeError: expected str, bytes or os.PathLike object, not File
+            with suppress(TypeError):
+                f = os.fspath(f)
             if SCons.Util.is_String(f):
                 f = SCons.Util.adjustixes(f, pre, suf, ensure_suffix)
             result.append(f)
diff --git a/SCons/BuilderTests.py b/SCons/BuilderTests.py
--- a/SCons/BuilderTests.py
+++ b/SCons/BuilderTests.py
@@ -658,6 +658,40 @@ def my_emit(env, sources):
         tgt = builder(my_env, target = None, source = 'f6.zzz')[0]
         assert tgt.get_internal_path() == 'f6.emit', tgt.get_internal_path()
 
+    def test__adjustixes(self) -> None:
+        """Test the _adjustixes() method"""
+        from pathlib import Path
+
+        builder = SCons.Builder.Builder()
+
+        # path without suffix should have both added
+        with self.subTest():
+            r = builder._adjustixes('file', 'pre-', '-suf')
+            self.assertEqual(r, ['pre-file-suf'])
+
+        # path with a suffix should not have one added
+        with self.subTest():
+            r = builder._adjustixes('file.o', 'pre-', '-suf')
+            self.assertEqual(r, ['pre-file.o'])
+
+        # unless ensure_suffix is True
+        with self.subTest():
+            r = builder._adjustixes('file.o', 'pre-', '-suf', ensure_suffix=True)
+            self.assertEqual(r, ['pre-file.o-suf'])
+
+        # a PathLike object should be modified
+        with self.subTest():
+            r = builder._adjustixes(Path('file'), 'pre-', '-suf')
+            self.assertEqual(r, ['pre-file-suf'])
+
+        # make sure a non-str, non-PathLike is just left alone
+        with self.subTest():
+            env = Environment()
+            mynode = env.fs.File('file')
+            r = builder._adjustixes(mynode, 'pre-', '-suf')
+            self.assertEqual(r, [mynode])
+            self.assertEqual(r[0].path, 'file')
+
     def test_single_source(self) -> None:
         """Test Builder with single_source flag set"""
         def func(target, source, env) -> None:


Test patch: