diff --git a/.appveyor.yml b/.appveyor.yml index 07b6fe1826..0ad936fb59 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -35,40 +35,37 @@ environment: SCONS_CACHE_MSVC_CONFIG: "true" matrix: # Test oldest and newest supported Pythons, and a subset in between. - # Skipping 3.7 and 3.9 at this time - - WINPYTHON: "Python312" - - - WINPYTHON: "Python310" - - - WINPYTHON: "Python38" - - - WINPYTHON: "Python36" + # Skipping 3.8, 3.10, 3.12 at this time + - WINPYTHON: "Python313" + - WINPYTHON: "Python311" + - WINPYTHON: "Python39" + - WINPYTHON: "Python37" # remove sets of build jobs based on criteria below # to fine tune the number and platforms tested matrix: exclude: - # test python 3.6 on Visual Studio 2017 image + # test python 3.7 on Visual Studio 2017 image - image: Visual Studio 2017 - WINPYTHON: "Python312" + WINPYTHON: "Python313" - image: Visual Studio 2017 - WINPYTHON: "Python310" + WINPYTHON: "Python311" - image: Visual Studio 2017 - WINPYTHON: "Python38" + WINPYTHON: "Python39" - # test python 3.8 on Visual Studio 2019 image + # test python 3.9 on Visual Studio 2019 image - image: Visual Studio 2019 - WINPYTHON: "Python312" + WINPYTHON: "Python313" - image: Visual Studio 2019 - WINPYTHON: "Python310" + WINPYTHON: "Python311" - image: Visual Studio 2019 - WINPYTHON: "Python36" + WINPYTHON: "Python37" - # test python 3.10 and 3.11 on Visual Studio 2022 image + # test python 3.11, 3.13 on Visual Studio 2022 image - image: Visual Studio 2022 - WINPYTHON: "Python36" + WINPYTHON: "Python39" - image: Visual Studio 2022 - WINPYTHON: "Python38" + WINPYTHON: "Python37" # Remove some binaries we don't want to be found # Note this is no longer needed, git-windows bin/ is quite minimal now. diff --git a/.editorconfig b/.editorconfig index 1889885a7d..f6fbafbbe5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,17 +10,17 @@ trim_trailing_whitespace = true end_of_line = lf charset = utf-8 -[*.py] -max_line_length = 78 +[{*.py,SConstruct,SConscript}] +max_line_length = 88 ensure_newline_before_comments = true include_trailing_comma = true use_parentheses = true -[*.xml] +[*.{xml,yml,yaml}] indent_size = 2 [*.rst] indent_size = 3 -[*.bat] +[*.{bat,cmd,ps1}] end_of_line = crlf diff --git a/.flake8 b/.flake8 deleted file mode 100644 index d10d72d2ab..0000000000 --- a/.flake8 +++ /dev/null @@ -1,22 +0,0 @@ -[flake8] -show-source = True -# don't complain about work black has done -max-line-length = 88 -extend-exclude = - bin, - bench, - doc, - src, - template, - testing, - test, - timings, - SCons/Tool/docbook/docbook-xsl-1.76.1, - bootstrap.py, - runtest.py -extend-ignore = - E302, - E305 -per-file-ignores = - # module symbols made available for compat - ignore "unused" warns - SCons/Util/__init__.py: F401 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 538cb23c92..caf616ab92 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,6 @@ # files reformatted from DOS line-endings 1277d8e5ab6457ed18d291100539f31d1bdb2d7c +# enforced .editorconfig eol settings +fbb026ef1145fe29e0ec3c1b66a3e99cac51e18d +# Black/Ruff'd test framework code. +c9d9fa58b796532320a2248ddc5be07b7280adf3 diff --git a/.gitattributes b/.gitattributes index 8b807dfb94..dcf034f78c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,10 @@ +* text=auto eol=lf +*.bat eol=crlf +*.cmd eol=crlf +*.ps1 eol=crlf + doc/* linguist-documentation SCons/Tool/docbook/docbook-xsl-1.76.1 linguist-vendored *.xml linguist-documentation *.xsl linguist-documentation *.gen linguist-documentation - -*.in eol=lf diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4c2d467669..423ba3f7b3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,5 +7,5 @@ https://scons.org/guidelines.html ## Contributor Checklist: * [ ] I have created a new test or updated the unit tests to cover the new/changed functionality. -* [ ] I have updated `CHANGES.txt` (and read the `README.rst`) +* [ ] I have updated `CHANGES.txt` and `RELEASE.txt` (and read the `README.rst`). * [ ] I have updated the appropriate documentation diff --git a/.github/workflows/experimental_tests.yml b/.github/workflows/experimental_tests.yml index 03f233c4f4..ffd2df12f8 100644 --- a/.github/workflows/experimental_tests.yml +++ b/.github/workflows/experimental_tests.yml @@ -13,47 +13,76 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +env: + # For use by the Windows runner (ignored by the others): + SCONS_CACHE_MSVC_CONFIG: 1 + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" + # This workflow contains a single job called "experimental" experimental: strategy: fail-fast: false matrix: - # note: in the 2nd half of 2022 the setup-mingw was often failing on - # windows-latest. revisit someday (perhaps when there's an @v3) - #os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] - os: ['ubuntu-latest', 'windows-2019', 'macos-latest'] + # In the 2nd half of 2022 the setup-mingw was often failing on + # windows-latest, when 2022 became the latest. Now 2025 is out, + # and 2019 is being retired, we need some kind of a solution. + cfg: [ + {os: 'ubuntu-latest', args: ''}, + {os: 'windows-latest', args: ''}, + {os: 'macos-latest', args: '--exclude-list=testing/ci/macos_ci_skip.txt'}, + ] # The type of runner that the job will run on - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.cfg.os }} # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4 # experiment: maybe don't need this? # update: looks like we do: with this commented out, the build hung - - name: Set up MinGW - uses: egor-tensin/setup-mingw@v2.2.0 - if: matrix.os == 'windows-2019' + # - name: Set up MinGW + # uses: egor-tensin/setup-mingw@v2.2.0 + # if: matrix.os == 'windows-2019' + # with: + # platform: x64 + # static: 0 + + - name: Set up MSYS2 ${{ matrix.cfg.os }} + uses: msys2/setup-msys2@v2 + if: startsWith(matrix.cfg.os, 'windows') with: - platform: x64 - static: 0 + msystem: UCRT64 + update: false + install: git mingw-w64-ucrt-x86_64-gcc - - name: Set up Python 3.11 ${{ matrix.os }} - uses: actions/setup-python@v5.1.0 + - name: Set up Python 3.11 ${{ matrix.cfg.os }} + uses: actions/setup-python@v5 with: python-version: '3.11' - - name: Install dependencies including ninja ${{ matrix.os }} + - name: Install dependencies including ninja ${{ matrix.cfg.os }} run: | - python -m pip install --upgrade pip setuptools wheel - python -m pip install ninja psutil + python -m pip install --progress-bar off --upgrade pip + python -m pip install --progress-bar off ninja psutil # sudo apt-get update - - name: Test experimental packages ${{ matrix.os }} + - name: Populate MSVC cache ${{ matrix.cfg.os }} + if: startsWith(matrix.cfg.os, 'windows') + run: | + python testing/ci/windows_msvc_cache.py + + - name: Test experimental packages ${{ matrix.cfg.os }} run: | - python runtest.py test/import.py test/ninja + python runtest.py --time ${{ matrix.cfg.args }} test/import.py test/ninja + + - name: Archive Failed tests ${{ matrix.cfg.os }} + uses: actions/upload-artifact@v4 + if: failure() + with: + name: ${{ matrix.cfg.os }}-failed-tests + path: | + failed_tests.log diff --git a/.github/workflows/framework_tests.yml b/.github/workflows/framework_tests.yml index bf5ab05c26..48a5fda88a 100644 --- a/.github/workflows/framework_tests.yml +++ b/.github/workflows/framework_tests.yml @@ -29,10 +29,10 @@ jobs: steps: # Checkouut repository under $GITHUB_WORKSPACE - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4 - name: Set up Python 3.11 ${{ matrix.os }} - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5 with: python-version: '3.11' diff --git a/.github/workflows/runtest-win-msvc.yml b/.github/workflows/runtest-win-msvc.yml new file mode 100644 index 0000000000..f22ff90370 --- /dev/null +++ b/.github/workflows/runtest-win-msvc.yml @@ -0,0 +1,62 @@ +# This is a basic workflow to help you get started with Actions + +name: Test MSVC Optional Environment + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + SCONS_CACHE_MSVC_CONFIG: 1 + SCONS_CACHE_MSVC_FORCE_DEFAULTS: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + PSDisableModuleAnalysisCacheCleanup: 1 + VCPKG_DISABLE_METRICS: 1 + VSCMD_SKIP_SENDTELEMETRY: 1 + +jobs: + runtest-msvc: + strategy: + fail-fast: false + matrix: + os: ['windows-latest'] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 ${{ matrix.os }} + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install Python dependencies ${{ matrix.os }} + run: | + python -m pip install --progress-bar off --upgrade pip + python -m pip install --progress-bar off -r requirements-dev.txt + + - name: runtest ${{ matrix.os }} + run: | + python runtest.py --file=testing/ci/msvc_ci_run.txt --time + + - name: Display MSVC cache ${{ matrix.os }} + if: always() + run: | + python testing/ci/windows_msvc_cache.py --skip-populate + + - name: Archive Failed tests ${{ matrix.os }} + uses: actions/upload-artifact@v4 + if: failure() + with: + name: ${{ matrix.os }}-failed-tests + path: | + failed_tests.log diff --git a/.github/workflows/runtest-win.yml b/.github/workflows/runtest-win.yml index ffbe071156..5a185aa85a 100644 --- a/.github/workflows/runtest-win.yml +++ b/.github/workflows/runtest-win.yml @@ -13,29 +13,48 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +env: + SCONS_CACHE_MSVC_CONFIG: 1 + jobs: runtest-win32: - runs-on: windows-latest + strategy: + fail-fast: false + matrix: + os: ['windows-latest'] + + runs-on: ${{ matrix.os }} + steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5.1.0 + - name: Set up Python 3.12 ${{ matrix.os }} + uses: actions/setup-python@v5 with: python-version: '3.12' + cache: 'pip' + + - name: Install Python dependencies ${{ matrix.os }} + run: | + python -m pip install --progress-bar off --upgrade pip + python -m pip install --progress-bar off -r requirements-dev.txt + + - name: Install Chocolatey packages ${{ matrix.os }} + run: | + choco install --yes --no-progress dmd winflexbison3 - - name: Install dependencies including ninja + - name: Populate MSVC cache ${{ matrix.os }} run: | - python -m pip install --upgrade pip - python -m pip install -r requirements-dev.txt + python testing/ci/windows_msvc_cache.py - - name: runtest + - name: runtest ${{ matrix.os }} run: | - python runtest.py --all --exclude-list=windows_ci_skip.txt --time --jobs=4 + python runtest.py --all --exclude-list=testing/ci/windows_ci_skip.txt --time --jobs=4 - - name: Archive Failed tests - uses: actions/upload-artifact@v4.3.3 + - name: Archive Failed tests ${{ matrix.os }} + uses: actions/upload-artifact@v4 + if: failure() with: - name: windows-failed-tests + name: ${{ matrix.os }}-failed-tests path: | failed_tests.log diff --git a/.github/workflows/runtest.yml b/.github/workflows/runtest.yml index 40ad3970ac..1b36854303 100644 --- a/.github/workflows/runtest.yml +++ b/.github/workflows/runtest.yml @@ -20,37 +20,42 @@ jobs: strategy: fail-fast: false matrix: - os: ['ubuntu-22.04', 'ubuntu-24.04'] + cfg: [ + {os: 'ubuntu-22.04', py: '3.7'}, + {os: 'ubuntu-24.04', py: '3.13'}, + ] - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.cfg.os }} steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4 - - name: Install Ubuntu packages $${matrix.os}} + - name: Install Ubuntu packages ${{ matrix.cfg.os }} run: | sudo apt-get update sudo apt-get install libtirpc-dev - - name: Set up Python 3.12 ${{ matrix.os }} - uses: actions/setup-python@v5.1.0 + - name: Set up Python ${{ matrix.cfg.py }} ${{ matrix.cfg.os }} + uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: ${{ matrix.cfg.py }} + cache: 'pip' - - name: Install dependencies including ninja ${{ matrix.os }} + - name: Install Python ${{ matrix.cfg.py }} dependencies ${{ matrix.cfg.os }} run: | - python -m pip install --upgrade pip setuptools wheel - python -m pip install -r requirements-dev.txt + python -m pip install --progress-bar off --upgrade pip + python -m pip install --progress-bar off -r requirements-dev.txt # sudo apt-get update - - name: runtest ${{ matrix.os }} + - name: runtest ${{ matrix.cfg.os }} ${{ matrix.cfg.py }} run: | python runtest.py --all --time --jobs=4 - - name: Archive Failed tests ${{ matrix.os }} - uses: actions/upload-artifact@v4.3.3 + - name: Archive Failed tests ${{ matrix.cfg.os }} ${{ matrix.cfg.py }} + uses: actions/upload-artifact@v4 + if: failure() with: - name: ${{ matrix.os }}-failed-tests + name: ${{ matrix.cfg.os }}-${{ matrix.cfg.py }}-failed-tests path: | failed_tests.log diff --git a/.github/workflows/scons-package.yml b/.github/workflows/scons-package.yml index 69526aa041..66ac04f742 100644 --- a/.github/workflows/scons-package.yml +++ b/.github/workflows/scons-package.yml @@ -14,18 +14,20 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4 + # We can't build with > 3.12 for now: no usable lxml wheel from 3.13 on - name: Set up Python 3.12 - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5 with: python-version: '3.12' + cache: 'pip' - name: Install dependencies run: | - python -m pip install --upgrade pip setuptools wheel build + python -m pip install --progress-bar off --upgrade pip setuptools wheel #python -m pip install flake8 pytest - if [ -f requirements-pkg.txt ]; then pip install -r requirements-pkg.txt; elif [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-pkg.txt ]; then pip install --progress-bar off -r requirements-pkg.txt; elif [ -f requirements.txt ]; then pip install --progress-bar off -r requirements.txt; fi sudo apt-get update sudo apt-get -y install docbook-xml docbook-xsl xsltproc fop docbook-xsl-doc-pdf # try to keep the texlive install as small as we can to save some time/space diff --git a/CHANGES.txt b/CHANGES.txt index 3fe676ab14..3fd6506983 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,14 +4,511 @@ Change Log -NOTE: The 4.0.0 Release of SCons dropped Python 2.7 Support -NOTE: 4.3.0 now requires Python 3.6.0 and above. Python 3.5.x is no longer supported +NOTE: The 4.0.0 release of SCons dropped Python 2.7 support. Use 3.1.2 if + Python 2.7 support is required (but note old SCons releases are unsupported). +NOTE: Since SCons 4.3.0, Python 3.6.0 or above is required. +NOTE: Since SCons 4.9.0, Python 3.7.0 or above is required. + RELEASE VERSION/DATE TO BE FILLED IN LATER + From John Doe: + - Whatever John Doe did. + + From Joseph Brill: + - MSVC: A significant delay was experienced in the Github Actions windows + 2022 and 2025 runners due to the environment used by SCons to initialize + MSVC when the Visual Studio vcpkg component is installed. The Visual + Studio vcpkg component is not installed in the Github Actions windows + 2019 runner. + The Visual Studio vcpkg component invokes a powershell script when the + MSVC batch files are called. The significant delay in the Github + Actions windows 2022 and 2025 runners appears due to the environment + used by SCons to initialize MSVC not including the pwsh executable on + the system path, not including the powershell module analysis cache + location, and not including the powershell module path. + Adding the pwsh and powershell executable paths in the order discovered + on the shell environment path, passing the powershell module analysis + cache location, and adding a subset of the powershell module path to the + environment used by SCons to initialize MSVC appears to have eliminated + the significant delays in the Github Actions windows 2022 and 2025 + runners. + In the Github Actions windows 2022 and 2025 runners, any one of the + three additions appears to eliminate the significant delays. It is hoped + that the combination of all three additions will guard against + significant delays in other environment configurations as well. + - MSVC: The following shell environment variables are now included in the + environment used by SCons to initialize MSVC when defined: + VCPKG_DISABLE_METRICS, VCPKG_ROOT, POWERSHELL_TELEMETRY_OPTOUT, + PSDisableModuleAnalysisCacheCleanup, and PSModuleAnalysisCachePath. A + subset of the shell environment PSModulePath is included in the + environment used by SCons to initialize MSVC when defined. None of + these variables and values are propagated to the user's SCons + environment after running the MSVC batch files. + - GitHub workflow changes: + * Add directory testing/ci for test runner files. Move the existing + windows ci skip file from the SCons root to the ci testing directory. + * Remove the interactive tests from the windows ci skip file. + * Add an exclude test file for MacOS that skips the two ninja tests that + consistently fail. + * Add a test file containing one test for the MSVC optional environment + workflow. + * Modify the experimental workflow to pass the exclude test file as an + argument to the test runner for macos. + * Add a workflow file to test MSVC with the optional environment + variables. + * Add a script in the testing/ci directory to populate the MSVC + cache before running the test suite in select windows workflow files. + The experimental test suite and the full test suite workflow files + populate the MSVC cache before running the test suite on windows. The + script is also run after the test suite in the optional MSVC + environment variables workflow file to display the MSVC cache. + * Upload the failed_tests.log artifact on failure in select workflow + files. Prior to this change, the failed test log was uploaded only + when none of the tests failed. + * Modify the runtest workflow file to pair an OS version with a python + version. + - Ninja: Increase the number of generated source files in the ninja + iterative speedup test from 200 to 250. Increasing the workload should + reduce the likelihood that the ninja tests are slower. + - Testing: Increase the default timeout from 20 seconds to 60 + seconds in the testing framework wait_for method. At present, the + wait_for method is only used for the interactive tests. + - GitHub: Remove the packaging tar xz test from the windows ci skip file. + - Testing: Update the packaging tar bz2 and xz tests on on Windows. + Detect if the tar bz2 and xz formats are supported for the windows + system tar executable using the reported version string. The packaging + tar bz2 and xz tests should be skipped on Windows 10 and GitHub + windows-2022. The packaging tar bz2 and xz tests should be run on + Windows 11 and GitHub windows-2025. + + From William Deegan: + - Fix Issue #4746. TEMPFILE's are written with utf-8 encoding, In case + of decoding errors, TEMPFILEENCODING can now be specified to give + more explicit instructions to SCons. + + From Edward Peek: + - Fix the variant dir component being missing from generated source file + paths with CompilationDatabase() builder (Fixes #4003). + + From Bill Prendergast: + - Fixed SCons.Variables.PackageVariable to correctly test the default + setting against both enable & disable strings. (Fixes #4702) + - Extended unittests (crudely) to test for correct/expected response + when default setting is a boolean string. + + From Keith Prussing: + - Fixed LaTeX SCanner to mimic LaTeX's search behavior for included files. + + From Mats Wichmann: + - Clean up C and C++ FLAGS tests. Tests which use a real compiler + are now more clearly distinguished (-live.py suffix and docstring). + - runtest.py once again finds "external" tests, such as the tests for + tools in scons-contrib. An earlier rework had broken this. Fixes #4699. + - Clarify how pre/post actions on an alias work. + - Rearrange Fortran e2e tests - "live" tests now live in a separate + test file and don't share with flags-only mocked tests. + - Tweak the two doc-generation modules. The significant change is + turning the introductory comment in bin/SConsExamples into a docstring + that can be rendered by Sphinx, and updating that text. The rest is + minor fiddling like switching to f-strings small doc changes. + - Fix a couple of unit tests to not fail with Python 3.14. These involve + bytecode strings and error message contents; there was no problem with + SCons itself using 3.14. Since 3.14 is now in Beta, there should + be no further changes. + - Replace use of old conditional expression idioms with the official + one from PEP 308 introduced in Python 2.5 (2006). The idiom being + replaced (using and/or) is regarded as error prone. + - Improve the description of PackageVariable. + - Tweak the "API Docs" build (Sphinx) configuration a bit. Some + one-file modules were reported as duplicated, this is fixed. + SConsDoc and SConsExample are now included - their API is + interesting to developers working on SCons (needed to write docs), + even if not part of "The SCons API" itself. + Reworded the API Docs intro sectios a bit. + - Include the roff (.1) manpages in the scons-doc tarball as a better + long-term home than in the sdist. + - Virtualenv support module modernized: previously looked first for an + obsolete mechanism the external virtualenv tool used to use, now checks + first for the official approach introduced in PEP 405. + - Add missing manpage entry for Virtualenv(). Return type is documented + as an empty string in case of a negative result (rather than None), + and the code adjusted. All internal usage, including tests, + was dont boolean-style anyway ("if Virtualenv():"). + - Add internal routines _Remove_Targets and _Remove_Arguments to + allow taking away values placed the public attributes BUILD_TARGETS, + COMMAND_LINE_TARGETS, ARGUMENTS and ARGLIST. This is a step towards + fixing the handling of option-arguments specified with a space + separator (multiple issues, harvested from PR #3799 created by Dillan Mills). + These interfaces are not part of the public API. + - Minor test tweaks to clean up and add DefaultEnvironment call. + - Ninja tool generate_command() fixed to call subst() with correct + arguments in ListAction case. Unit tests added for generate_command. + Fixes #4580. + - Ninja tool now quotes targets (if necessary) when calling back to + SCons - both in the POST request constructed to contact the + daemon, and in the command eventually issued from the deamon. + Initial suggestion from Julien Pommier. Fixes #4730. + - Ninja tool is adjusted to recognize and emit the right rule in + the case of special actions that the tool recognizes, like Copy. + This was working in the case of single commands, but not when part + of a list of actions. Recognition only happens if the special + action is first in the list. Initial suggestion from Julien Pommier. + Fixes #4731. + - Fix a test problem on Windows where UNC tests failed due to incorrect + path munging if a non-default %TEMP% was defined (as in moving to + a Dev Drive). Also some cleanup. + - Improve the wording of Configure methods. + - Add unit test cases for AppendUnique, PrependUnique - verify behavior + if existing value already contained more than one of added value. + + +RELEASE 4.9.1 - Thu, 27 Mar 2025 11:40:20 -0700 + + + From Adam Simpkins: + - Fixed a hang in `wait_for_process_to_die()` on Windows, affecting + clean-up of the SCons daemon used for Ninja builds. + + From Thaddeus Crews: + - Nodes are now treated as PathLike objects. + + From Mats Wichmann: + - Fix typos in CCFLAGS test. Didn't affect the test itself, but + didn't correctly apply the DefaultEnvironment speedup. + + - New CacheDir initialization code failed on Python 3.7 for unknown + reason (worked on 3.8+). Adjusted the approach a bit. Fixes #4694. + - Try to fix Windows fails on Docbook tests in case xsltproc is found. + It's still not certain why this started failing. On both GitHub + Actions and AppVeyor, it's found as part of StrawberryPerl, part of + the default install - maybe this wasn't the case before? The xsltproc + from choco install is considerably older and may have been more lenient? + Anyway, intermittent fails seem caused by something network related, + so propagate two xsltproc flags that avoid loading the dtd that were + present in three of the 11 "live" tests, but not the other eight. + Also, all 11 now pass the test-discovered xslt processor the same + way, which was not the case previously. + - Update progress printing on three tools for SCons developers - + the test runner and two of the doc generators. + + +RELEASE 4.9.0 - Sun, 02 Mar 2025 17:22:20 -0700 + + From Ruben Di Battista: + - Expose `extra_libs` kwarg in Configure checks `CheckLibWithHeader` + and 'CheckLib' and forward it downstream to `CheckLib` + + From Joseph Brill: + - Added error handling when creating MSVC detection debug log file specified by + SCONS_MSCOMMON_DEBUG. + - MSVS: Added default HOST_ARCH values to sconstruct/sconscript environment for + select msvs test scripts to allow tests to run on platforms not recognized by + the msvs/msvc tool implementations. Fixes #4608. + - MSVS: Fix early exit after the first msvc version loop execution in select msvs + test scripts. Select msvs test scripts were being invoked for msvc version 8.0 + only. Fixes #4609. + - MSVS: Additional minor select msvs test script fixes as a result of the msvs + tests being invoked for all msvc versions: fix vs version number for vc version + 14.3, fix expected platform toolset version, add and use a default known + project GUID for some select tests, add AdditionalOptions Condition to expected + vcx project file. + - MSVS: Additional minor changes to the msvs tool as a result of the msvs tests + being invoked for all msvc versions: use environment MSVS_PROJECT_GUID when + generating project files information, fix the visual studio string for VS2015, + add .vcxproj as an expected suffix for assigning the name to the file basename. + - MSVS: Add additional msvs tests for multi-project and solution builds. + - MSVS: Check for variant directory build of MSVSSolution and adjust the source + node similar to the handling for MSVSProject. The solution was generated in + the build directory instead of the source directory. The placeholder solution + file is not generated in the build directory and the solution file is generated + in the source directory similar to the handling for project files. + Fixes #4612. + - MSVS: Add project dsp nodes to the dsw source node list in the msvs tool. This + appears to always cause the project files to be generated before the solution + files which is necessary to retrieve the project GUIDs for use in the solution + file. This does change the behavior of clean for a project generated with + auto_build_solution disabled and explicit solution generation: when the + solution files are cleaned, the project files are also cleaned. The tests for + vs 6.0-7.1 were changed accordingly. + - MSVS: Add an optional keyword argument, auto_filter_projects, to MSVSSolution. + Accepted values for auto_filter_projects are: + - None [default]: raise an exception when solution file names or nodes are + detected in the projects argument list. + - True or evaluates True: automatically remove solution file names and nodes + from the project argument list. + - False or evaluates False: leave solution file names and nodes in the project + argument list. An exception is not raised. + Solution file names and/or nodes in the project argument list cause erroneous + Project records to be produced in the generated solution file. As a + convenience, an end-user may elect to ignore solution file names and nodes in + the projects argument list rather than manually removing solution file names + and nodes from the MSVSProject return values. Resolves #4613. + - MSVS: Remove the platform specification (i.e., platform = 'win32') from select + test script environments. The platform specification appears superfluous. + - SCons C preprocessor changes: + - Preserve literals that contain valid integer substring specifications. + Previously, the integer suffix could be stripped from a symbol that contained + an integer and suffix substring. + - Update the optional integer suffixes to include the z|Z and wb|WB suffixes. + - Update the optional integer suffixes to include support for alternate + orderings of unsigned with long or long long as defined in the c/cpp + grammar. + - Update the optional integer suffixes for case insensitive specifications as + defined in the c/cpp grammar. + - Add support for binary integer constants. + - Add support for octal integer constants. Previously, octal integers were + evaluated as decimal integers. A literal zero (0) is treated as an octal + number. + - Change the attempted conversion of a define expansion from using int() to + a constant expression evaluation. + + From Thaddeus Crews: + - Removed Python 3.6 support. + - Ruff/Mypy: Excluded items now synced. + - Ruff: Linter includes new rules - `FA`, `UP006`, `UP007`, and `UP037` - to + detect and upgrade legacy type-hint syntax. + - Removed "SCons.Util.sctyping.py", as the functionality can now be substituted + via top-level `from __future__ import annotations`. + - Implemented type hints for Nodes. + + From William Deegan: + - Update ninja tool to use ninja.BIN_DIR to find pypi packaged ninja binary. + python ninja package version 1.11.1.2 changed the location and previous + logic no longer worked. + - Added TestSCons.NINJA_BINARY to TestSCons to centralize logic to find ninja binary + - Refactored SCons.Tool.ninja -> SCons.Tool.ninja_tool, and added alias so + env.Tool('ninja') will still work. This avoids conflicting with the pypi module ninja. + - Renamed env.Help() & Help()'s argument `keep_local` to `local_only`, previously the documentation + specified `local_only`, but the code and tests were using `keep_local`. The functionality + more closely matches local only. NOTE: It doesn't seem like any code in the wild was using + local_only as we'd not received any reports of such until PR #4606 from hedger. + - Fix Issue #2281, AddPreAction() & AddPostAction() were being ignored if no action + was specified when the Alias was initially created. + + From Alex James: + - On Darwin, PermissionErrors are now handled while trying to access + /etc/paths.d. This may occur if SCons is invoked in a sandboxed + environment (such as Nix). + + From Prabhu S. Khalsa: + - Fix typo in man page + + From Dillan Mills: + - Fix support for short options (`-x`). + + From Keith F Prussing: + - Added support for tracking beamer themes in the LaTeX scanner. + + From Adam Scott: + - Changed Ninja's TEMPLATE rule pool to use `install_pool` instead of + `local_pool`, hoping it will fix a race condition that can occurs when + Ninja defers to SCons to build. + + From Alex Thiessen: + - Many grammatical and spelling fixes in the documentation. + + From Mats Wichmann: + - PackageVariable now does what the documentation always said it does + if the variable is used on the command line with one of the enabling + string as the value: the variable's default value is produced (previously + it always produced True in this case). + - Minor updates to test framework. The functional change is that + test.must_exist() and test.must_exist_one_of() now take an optional + 'message' keyword argument which is passed on to fail_test() if + the test fails. The rest is cleanup and type annotations. Be more + careful that the returns from stderr() and stdout(), which *can* + return None, are not used without checking. + - The optparse add_option method supports an additional calling style + that is not directly described in SCons docs, but is included + by reference ("see the optparse documentation for details"): + a single arg consisting of a premade option object. Because optparse + detects that case based on seeing zero kwargs and we always + added at least one (default=) that would fail for AddOption. Fix + for consistency, but don't advertise it further - not added to + manpage synopsis/description. + - Temporary files created by TempFileMunge() are now cleaned up on + scons exit, instead of at the time they're used. Fixes #4595. + - Override environments, created when giving construction environment + keyword arguments to Builder calls (or manually, through the undocumented + Override method), were modified not to "leak" on item deletion. The item + will now not be deleted from the base environment. Override Environments + now also pretend to have a _dict attribute so that regular environment + methods don't have a problem if passed an OE instance. + - Fix a problem with compilation_db component initialization - the + entries for assembler files were not being set up correctly. + - Add clang and clang++ to the default tool search orders for POSIX + and Windows platforms. These will be searched for after gcc and g++, + respectively. Does not affect explicitly requested tool lists. + Note: on Windows, SCons currently only has builtin support for + clang, not for clang-cl, the version of the frontend that uses + cl.exe-compatible command line switches. + - Some manpage cleanup for the gettext and pdf/ps builders. + - Some clarifications in the User Guide "Environments" chapter. + - Fix nasm test for missing include file, cleanup. + - Change long-standing irritant in Environment tests - instead of using + a while loop to pull test info from a list of tests and then delete + the test, structure the test data as a list of tuples and iterate it. + - Skip running a few validation tests if the user is root and the test is + not designed to work for the root user. + - Clarify documentation of Repository() in manpage and user guide. + - Add a tag to each CacheDir to let systems ignore backing it up + (per https://bford.info/cachedir/). Update the way a CacheDir + is created, since it now has to create two files. + - The Dictionary method now has an as_dict flag. If true, Dictionary + always returns a dict. The default remains to return different + types depending on whether zero, one, or multiple construction + variable names are given. + - Update Clean and NoClean documentation. + - Make sure unknown variables from a Variables file are recognized + as such. Previously only unknowns from the command line were + recognized (issue #4645). + - A Variables object now makes available a "defaulted" attribute, + a list of variable names that were set in the environment with + their values taken from the default in the variable description + (if a variable was set to the same value as the default in one + of the input sources, it is not included in this list). + - If a build Variable is created with no aliases, the name of the + Variable is no longer listed in its aliases. Internally, the name + and aliases are considered together anyway so this should not have + any effect except for being visible to custom help text formatters. + - A build Variable is now a dataclass, with initialization moving to + the automatically provided method; the Variables class no longer + writes directly to a Variable (makes static checkers happier). + - Improved Variables documentation. + - The (optional) C Conditional Scanner now does limited macro + replacement on the contents of CPPDEFINES, to improve finding deps + that are conditionally included. Previously replacement was only + done on macro definitions found in the file being scanned. + Only object-like macros are replaced (not function-like), and + only on a whole-word basis; recursion is limited to five levels + and does not error out if that limit is reached (issue #4523). + - Minor modernization: make use of stat object's st_mode, st_mtime + and other attributes rather than indexing into stat return. + - The update-release-info test is adapted to accept changed help output + introduced in Python 3.12.8/3.13.1. + - Update the User Guide Command() example which now shows a target name + being created from '${SOURCE.base}.out' to use a valid special + attribute and to explain what's being done in the example. + - Test framework reformatted using settings from pyproject.toml. + Includes code embedded in docstrings. + - Handle case of "memoizer" as one member of a comma-separated + --debug string - this was previously missed. + - test YACC/live.py fixed - finally started failing on an "old-style" + (K&R flavor) function declaration, updated. + - Test framework - add recognizing list-of-path-components for + the destination of fixtures too (matches docstrings now). + + +RELEASE 4.8.1 - Tue, 03 Sep 2024 17:22:20 -0700 + + From Thaddeus Crews: + - Add explicit return types to sctypes `is_*` functions. For Python <=3.9, + the return type is simply `bool`, same as before. Python 3.10 and later + will benefit from `TypeGuard`/`TypeIs`, to produce intellisense similar + to using `isinstance` directly. + + From Anthony Siegrist; + - On win32 platform, handle piped process output more robustly. Output encoding + now uses 'oem' which should be the systems default encoding for the shell where + the process is being spawned. + + From Mats Wichmann: + - env.Dump() now considers the "key" positional argument to be a varargs + type (zero, one or many). However called, it returns a serialized + result that looks like a dict. Previously, only a single "key" was + accepted, and unlike the zero-args case, it was serialized to a + string containing just the value (without the key). For example, if + "print(repr(env.Dump('CC'))" previously returned "'gcc'", it will now + return "{'CC': 'gcc'}". + - Add a timeout to test/ninja/default_targets.py - it's gotten stuck on + the GitHub Windows action and taken the run to the full six hour timeout. + Usually runs in a few second, so set the timeout to 3min (120). + - SCons 4.8.0 added an `__all__` specifier at the top of the Variables + module (`Variables/__init__.py`) to control what is made available in + a star import. However, there was existing usage of doing + `from SCons.Variables import *` which expected the variable *types* + to be available. While we never advertised this usage, there's no + real reason it shouldn't keep working - add to `__all__`. + - Switch SCons build to use setuptools' supported version fetcher from + the old homegrown one. + - Improve wording of manpage "Functions and Environment Methods" section. + Make doc function signature style more consistent - tweaks to AddOption, + DefaultEnvironment and Tool,. + - Fix a problem with AppendUnique and PrependUnique where a value could + be erroneously removed due to a substring match. + - Fix handling of ListVariable when supplying a quoted choice containing + a space character (issue #4585). + + +RELEASE 4.8.0 - Sun, 07 Jul 2024 17:22:20 -0700 + + From Joseph Brill: + - For msvc version specifications without an 'Exp' suffix, an express installation + is used when no other edition is detected for the msvc version. Similarly, an + express installation of the IDE binary is used when no other IDE edition is + detected. + - VS2015 Express (14.0Exp) does not support the sdk version argument. VS2015 Express + does not support the store argument for target architectures other than x86. + Script argument validation now takes into account these restrictions. + - VS2015 BuildTools (14.0) does not support the sdk version argument and does not + support the store argument. Script argument validation now takes into account + these restrictions. + - The Windows SDK for Windows 7 and .NET Framework 4" (SDK 7.1) populates the + registry keys in a manner in which the msvc detection would report that VS2010 + (10.0) is installed when only the SDK was installed. The installed files are + intended to be used via the sdk batch file setenv.cmd. The installed msvc + batch files will fail. The msvc detection logic now ignores SDK-only VS2010 + installations. Similar protection is implemented for the sdk-only installs that + populate the installation folder and registry keys for VS2008 (9.0), if necessary. + - For VS2005 (8.0) to VS2015 (14.0), vsvarsall.bat is employed to dispatch to a + dependent batch file when configuring the msvc environment. Previously, only the + existence of the compiler executable was verified. In certain installations, the + dependent batch file (e.g., vcvars64.bat) may not exist while the compiler + executable does exist resulting in build failures. The existence of vcvarsall.bat, + the dependent batch file, and the compiler executable are now validated. + - MSVC configuration data specific to versions VS2005 (8.0) to VS2008 (9.0) was added + as the dependent batch files have different names than the batch file names used + for VS2010 (10.0) and later. The VS2008 (9.0) Visual C++ For Python installation + is handled as a special case as the dependent batch files are: (a) not used and (b) + in different locations. + - When VS2008 (9.0) Visual C++ For Python is installed using the ALLUSERS=1 option + (i.e., msiexec /i VCForPython27.msi ALLUSERS=1), the registry keys are written to + HKEY_LOCAL_MACHINE rather than HKEY_CURRENT_USER. An entry was added to query the + Visual C++ For Python keys in HKLM following the HKCU query, if necessary. + - For VS2008, a full development edition (e.g., Professional) is now selected before + a Visual C++ For Python edition. Prior to this change, Visual C++ For Python was + selected before a full development edition when both editions are installed. + - The registry detection of VS2015 (14.0), and earlier, is now cached at runtime and + is only evaluated once for each msvc version. + - The vswhere executable is frozen upon initial detection. Specifying a different + vswhere executable via the construction variable VSWHERE after the initial + detection now results in an exception. Multiple bugs in the implementation of + specifying a vswhere executable via the construction variable VSWHERE have been + fixed. Previously, when a user specified vswhere executable detects new msvc + installations after the initial detection, the internal msvc installation cache + and the default msvc version based on the initial detection are no longer valid. + For example, when no vswhere executable is found for the initial detection + and then later an environment is constructed with a user specified vswhere + executable that detects new msvc installations. + - The vswhere detection of VS2017 (14.1), and later, is now cached at runtime and is + only evaluated once using a single vswhere invocation for all msvc versions. + Previously, the vswhere executable was invoked for each supported msvc version. + - The vswhere executable locations for the WinGet and Scoop package managers were + added to the default vswhere executable search list after the Chocolatey + installation location. + - Fix issue #4543: add support for msvc toolset versions 14.4X installed as the + latest msvc toolset versions for msvc buildtools v143. The v143 msvc buildtools + may contain msvc toolset versions from 14.30 to 14.4X. + From Thaddeus Crews: - GetSConsVersion() to grab the latest SCons version without needing to access SCons internals. + - Migrate setup.cfg logic to pyproject.toml; remove setup.cfg. + - Update .gitattributes to match .editorconfig; enforce eol settings. + - Replace black/flake8 with ruff for more efficient formatting & linting. + - When debugging (--debug=pdb), the filename SCsub is now recognized when + manipulating breakpoints. From Raymond Li: - Fix issue #3935: OSErrors are now no longer hidden during execution of @@ -20,7 +517,7 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER consistent behavior. NOTE: With this change, user created Actions should now catch and handle expected exceptions (whereas previously many of these were silently - caught and suppressed by the SCons Action exection code). + caught and suppressed by the SCons Action execution code). From Ryan Carsten Schmidt: - Teach ParseFlags to put a --stdlib=libname argument into CXXFLAGS. @@ -71,6 +568,48 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER tweaked the Util package doc build so it's structured more like the other packages (a missed part of the transition when it was split). - Updated manpage description of Command "builder" and function. + - Framework for scons-time tests adjusted so a path with a long username + Windows has squashed doesn't get re-expanded. Fixes a problem seen + on GitHub Windows runner which uses a name "runneradmin". + - SCons.Environment.is_valid_construction_var() now returns a boolean to + match the convention that functions beginning with "is" have yes/no + answers (previously returned either None or an re.match object). + Now matches the annotation and docstring (which were prematurely + updated in 4.6). All SCons usage except unit test was already fully + consistent with a bool. + - When a variable is added to a Variables object, it can now be flagged + as "don't perform substitution" by setting the argument subst. + This allows variables to contain characters which would otherwise + cause expansion. Fixes #4241. + - The test runner now recognizes the unittest module's return code of 5, + which means no tests were run. SCons/Script/MainTests.py currently + has no tests, so this particular error code is expected - should not + cause runtest to give up with an "unknown error code". + - Updated the notes about reproducible builds with SCons and the example. + - The Clone() method now respects the variables argument (fixes #3590) + - is_valid_construction_var() (not part of the public API) moved from + SCons.Environment to SCons.Util to avoid the chance of import loops. Variables + and Environment both use the routine and Environment() uses a Variables() + object so better to move to a safer location. + - Performance tweak: the __setitem__ method of an Environment, used for + setting construction variables, now uses the string method isidentifier + to validate the name (updated from microbenchmark results). + - AddOption and the internal add_local_option which AddOption calls now + recognize a "settable" keyword argument to indicate a project-added + option can also be modified using SetOption. Fixes #3983. + NOTE: If you were using ninja and using SetOption() for ninja options + in your SConscripts prior to loading the ninja tool, you will now + see an error. The fix is to move the SetOption() to after you've loaded + the ninja tool. + - ListVariable now has a separate validator, with the functionality + that was previously part of the converter. The main effect is to + allow a developer to supply a custom validator, which previously + could be inhibited by the converter failing before the validator + is reached. + - Regularized header (copyright, licens) at top of documentation files + using SPDX. + - Updated introductory section of manual page. + - Minor cleanups in tests - drop unused "% locals()" stanzas. RELEASE 4.7.0 - Sun, 17 Mar 2024 17:22:20 -0700 @@ -1444,7 +1983,7 @@ RELEASE 3.1.2 - Mon, 17 Dec 2019 02:06:27 +0000 in the case where a child process is spawned while a Python action has a file open. Original author: Ryan Beasley. - Added memoization support for calls to Environment.Value() in order to - improve performance of repeated calls. + improve performance of repeated calls. From Jason Kenny diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f255e00a1e..1a02e74f37 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -6,20 +6,22 @@ Introduction Thanks for taking the time to contribute to SCons! -This will give a brief overview of the development process, +This is a brief overview of the development process, and about the SCons tree (right here, if you're reading this -in Github, or in a cloned repository). +in Github, or in a repository clone). There are lots of places we could use help - please don't think we're only interested in contributions to code. If you're going to contribute, we'd love to get to know you -a bit and understand and what problems you're looking to solve, +a bit and understand what problems you're looking to solve, or what you are intending to improve, whether that's documentation, code, examples, tutorials, etc. A great way to introduce yourself is to to hop onto the `SCons Discord Server `_ -to chat. You don't have to be a regular Discord user, -there is a web interface. +to chat. You don't have to use the Discord app, +as there is a web interface (does require an account). +You can also use the +`SCons Users Mailing List `_. Resources ========= @@ -38,15 +40,20 @@ Reporting Bugs One of the easiest ways to contribute is by filing bugs. The SCons project welcomes bug reports and feature requests, -but we *do* have a preference for having talked about them first - -we request you send an email to the -`SCons Users Mailing List `_ -or hop on the Discord channel (see link above), and if so -instructed, then proceed to an issue report. +but unless they're really trivial (like doc typos), +we *do* have a preference for having talked about them first. +This step helps identify possible collaborators and reviewers, +and pre-screens issues that may already have solutions, +be in progress, or perhaps are the result of a misunderstanding. +You can either ask on the Discord channel (see link above), +or send am email to the mailing list. +You can also use +`GitHub Discussions `_. +If so instructed, please proceed to an issue report. You can explore the list of existing bugs on GitHub. Sometimes there's work in progress which may include temporary -workarounds for the problem you've run into:: +workarounds for the problem you are running into:: https://github.com/SCons/scons/issues @@ -58,12 +65,12 @@ This tree contains a lot more than just the SCons engine itself. Some of it has to do with packaging it in a couple of forms: a Python-installable package (source distribution and installable wheel file, which get uploaded to the Python -Package Index), a portable zip (or tar) distribution +Package Index, PyPI), a portable zip (or tar) distribution called "scons-local", and a full source bundle. You usually don't need to worry about the packaging parts when working on a source or doc contribution - unless you're adding an entirely -new file, then the packaging bits may need to know about it. The -project maintainers can usually help with that part. +new file, then the packaging bits may need to know about it. +The project maintainers can help with that part. There are also tests and tools in the tree. The *full* development cycle is not just to test code changes directly, @@ -111,7 +118,7 @@ on the documentation process at the Documentation Toolchain page: https://github.com/SCons/scons/blob/master/doc/overview.rst -You can execute SCons directly from this repository. For Linux or UNIX:: +You can execute SCons directly from this repository. For Linux/UNIX/MacOS:: $ python scripts/scons.py [arguments] diff --git a/README-SF.rst b/README-SF.rst index 854524afbc..5c18c99505 100755 --- a/README-SF.rst +++ b/README-SF.rst @@ -47,8 +47,10 @@ version at the SCons download page: Execution Requirements ====================== -Running SCons requires Python 3.6 or higher. There should be no other +Running SCons requires Python 3.7 or higher. There should be no other dependencies or requirements to run standard SCons. + +The last release to support Python 3.6 was 4.8.1. The last release to support Python 3.5 was 4.2.0. The default SCons configuration assumes use of the Microsoft Visual C++ @@ -614,5 +616,5 @@ many contributors, including but not at all limited to: \... and many others. -Copyright (c) 2001 - 2023 The SCons Foundation +Copyright (c) 2001 - 2024 The SCons Foundation diff --git a/README-local b/README-local index 002af2d54e..b7929acc7c 100644 --- a/README-local +++ b/README-local @@ -43,8 +43,10 @@ scons-local package, or any SCons package, at the SCons download page: EXECUTION REQUIREMENTS ====================== -Running SCons requires Python 3.6 or higher. There should be no other +Running SCons requires Python 3.7 or higher. There should be no other dependencies or requirements to run standard SCons. + +The last release to support Python 3.6 was 4.8.1. The last release to support Python 3.5 was 4.2.0. The default SCons configuration assumes use of the Microsoft Visual C++ diff --git a/README-package.rst b/README-package.rst index 45eb273e43..706d62467d 100755 --- a/README-package.rst +++ b/README-package.rst @@ -80,8 +80,10 @@ http://www.scons.org/documentation.html. Execution Requirements ====================== -Running SCons requires Python 3.6 or higher. There should be no other +Running SCons requires Python 3.7 or higher. There should be no other dependencies or requirements to run standard SCons. + +The last release to support Python 3.6 was 4.8.1. The last release to support Python 3.5 was 4.2.0. Some experimental features may require additional Python packages diff --git a/README.rst b/README.rst index ae3c0f4a28..61217cfb18 100755 --- a/README.rst +++ b/README.rst @@ -88,8 +88,10 @@ is the latest version at the Execution Requirements ====================== -Running SCons requires Python 3.6 or higher. There should be no other +Running SCons requires Python 3.7 or higher. There should be no other dependencies or requirements to run standard SCons. + +The last release to support Python 3.6 was 4.8.1. The last release to support Python 3.5 was 4.2.0. Some experimental features may require additional Python packages @@ -249,6 +251,19 @@ notifications and other GitHub events (``#github-update``), if those are of interest. See the website for more contact information: https://scons.org/contact.html. +Reproducible Builds +=================== +SCons itself is set up to do "reproducible builds" +(see (https://reproducible-builds.org/specs/source-date-epoch/) +if environment variables ``SOURCE_DATE_EPOCH`` is set - that is, +fields in the package which could change each time the package is +constructed are forced to constant values. + +To support other projects which wish to do the same, a sample script +is provided which can be placed in a site directory, which imports +``SOURCE_DATE_EPOCH`` and sets it in the execution environment of +every created construction envirionment. There's also an installer +script (POSIX shell only). See packaging/etc/README.txt for more details. Donations ========= @@ -258,15 +273,6 @@ software, or hardware) to support continued work on the project. Information is available at https://www.scons.org/donate.html or the GitHub Sponsors button on https://github.com/scons/scons. -Reproducible Builds -=================== -In order to suppor those users who which to produce reproducible builds -(https://reproducible-builds.org/specs/source-date-epoch/) we're now including -logic to force SCons to propagate SOURCE_DATE_EPOCH from your shell environment for -all SCons builds to support reproducible builds we're now providing an example -site_init.py and a script to install it in your ~/.scons. See packaging/etc/README.txt -for more info - For More Information ==================== diff --git a/RELEASE.txt b/RELEASE.txt index 92e31ab72d..a808d9a8e5 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -6,91 +6,187 @@ Past official release announcements appear at: ================================================================== -A new SCons release, 4.7.1, is now available on the SCons download page: +A new SCons release, X.Y.Z, is now available on the SCons download page: https://scons.org/pages/download.html +Here is a summary of the changes since 4.9.1: -Here is a summary of the changes since 4.7.0: NEW FUNCTIONALITY ----------------- -- GetSConsVersion() added to retrieve the SCons version. +- List new features (presumably why a checkpoint is being released) DEPRECATED FUNCTIONALITY ------------------------ -- Mark Python 3.6 support as deprecated. Use --warn=no-python-version - to quiet the warning. +- List anything that's been deprecated since the last release CHANGED/ENHANCED EXISTING FUNCTIONALITY --------------------------------------- -- Dump() with json format selected now recognizes additional compound types - (UserDict and UserList), which improves the detail of the display. - json output is also sorted, to match the default display. -- Python 3.13 changes the behavior of isabs() on Windows. Adjust SCons - usage of this in NodeInfo classes to avoid test problems. -- Drop duplicated __getstate__ and __setstate__ methods in AliasNodeInfo, - FileNodeInfo and ValueNodeInfo classes, as they are identical to the - ones in parent NodeInfoBase and can just be inherited. -- All exceptions during the execution of an Action are now returned by value - rather than by raising an exception, for more consistent behavior. - NOTE: With this change, user created Actions should now catch and handle - expected exceptions (whereas previously many of these were silently caught - and suppressed by the SCons Action exection code). -- ParseFlags now sorts a --stdlib=libname argument into CXXFLAGS instead - of CCFLAGS; the latter variable could cause a compiler warning. -- The implementation of Variables was slightly refactored, there should - not be user-visible changes. +- List modifications to existing features, where the previous behavior + wouldn't actually be considered a bug + +- Nodes are now treated as PathLike objects. + +- Replace use of old conditional expression idioms with the official + one from PEP 308 introduced in Python 2.5 (2006). The idiom being + replaced (using and/or) is regarded as error prone. + +- MSVC: The following shell environment variables are now included in + the environment used by SCons to initialize MSVC when defined: + VCPKG_DISABLE_METRICS, VCPKG_ROOT, POWERSHELL_TELEMETRY_OPTOUT, + PSDisableModuleAnalysisCacheCleanup, and PSModuleAnalysisCachePath. + A subset of the shell environment PSModulePath is included in the + environment used by SCons to initialize MSVC when defined. None of + these variables and values are propagated to the user's SCons + environment after running the MSVC batch files. FIXES ----- -- OSErrors are now no longer hidden during the execution of Actions. -- Improved the conversion of a "foreign" exception from an action - into BuildError by making sure our defaults get applied even in - corner cases. Fixes Issue #4530 +- Fixed SCons.Variables.PackageVariable to correctly test the default + setting against both enable & disable strings. (Fixes #4702) + +- MSVC: Fixed a significant delay experienced in the Github Actions + windows 2022 and 2025 runners due to the environment used by SCons + to initialize MSVC when the Visual Studio vcpkg component is + installed. The Github Actions windows 2019 runner was not affected. + +- Fix the variant dir component being missing from generated source file + paths with CompilationDatabase() builder (Fixes #4003). + +- Ninja tool generate_command() fixed to call subst() with correct + arguments in ListAction case. Unit tests added for generate_command. + +- Fix the SCons.Scanner.LaTeX to mimic LaTeX's search method. + +- Ninja tool now quotes targets (if necessary) when calling back to + SCons - both in the POST request constructed to contact the + daemon, and in the command eventually issued from the deamon. + +- Ninja tool is adjusted to recognize and emit the right rule in + the case of special actions that the tool recognizes, like Copy. + This was working in the case of single commands, but not when part + of a list of actions. Recognition only happens if the special + action is first in the list. + +- Fix a test problem on Windows where UNC tests failed due to incorrect path + munging if a non-default %TEMP% was defined (as in moving to a Dev Drive). + +- Fix Issue #4746. The TEMPFILE is written in utf-8 encoding by default. + If the tempfile contents cannot be decoded by the command the + tempfile is passed to, (new) TEMPPFILEENCODING can be used to speficy a + different encoding to use. On Windows, the username may be a cause of this, + as the default path for temporary files includes the username. Setting + (existing) TEMPFILEDIR may also help in this case. + IMPROVEMENTS ------------ -- Make the testing framework a little more resilient: the temporary - directory for tests now includes a component named "scons" which can - be given to antivirus software to exclude. +- List improvements that wouldn't be visible to the user in the + documentation: performance improvements (describe the circumstances + under which they would be observed), or major code cleanups + +- Virtualenv support module modernized: previously looked first for an + unofficial approach from before venv support was made part of Python + in 3.3; now looks for the official approach first. This in an internal + detail, the API is unchanged. + +- Add internal routines to maniplutate publicly visible argument and + target lists. These interfaces are not part of the public API. PACKAGING --------- - List changes in the way SCons is packaged and/or released +- The generated roff (.1) manpages are now included in the + scons-doc tarball that is built at part of the release process, + in addition to the html and txt versions. For distribution + packaging, the manpages can be extracted from here (downloadable + from https://scons.org/doc/ using a a version-specific URL, + e.g. https://scons.org/doc/4.9.1/scons-doc-4.9.1.tar.gz). DOCUMENTATION ------------- -- Updated Value Node docs. -- Update manpage for Tools, and for the TOOL variable. -- Update manpage and user guide for Variables usage. -- Restructured API Docs build so main package contents are listed - before contents of package submodules. -- Updated manpage description of Command "builder" and function. +- List any significant changes to the documentation (not individual + typo fixes, even if they're mentioned in src/CHANGES.txt to give + the contributor credit) + +- Clarify how pre/post actions on an alias work. + +- Improve the description of PackageVariable. +- The "API Docs" build (Sphinx) configuration is improved, and + SConsDoc and SConsExample are now included - their API is + interesting to developers working on SCons (needed to write docs), + even if not part of "The SCons API" itself. +- Missing documentation for the Virtualenv() function is added. + Note that the User Guide previously described a negative outcome + as returning None. It is now explicit that the path is returned if + running in a virtualenv, and an empty (falsy) string if not. + +- Improve the wording of Configure methods. DEVELOPMENT ----------- -- Documentation build now properly passes through skipping the PDF - (and EPUB) builds of manpage and user guide; this can also be done - manually if directly calling doc/man/SConstruct and doc/user/SConstruct - by adding SKIP_PDF=1. This should help with distro packaging of SCons, - which now does not need "fop" and other tools to be set up in order to - build pdf versions which are then ignored. +- List visible changes in the way SCons is developed + +- runtest.py once again finds "external" tests, such as the tests for + tools in scons-contrib. An earlier rework had broken this. Fixes #4699. + +- Clean up C and C++ FLAGS tests. Tests which use a real compiler + are now more clearly distinguished (-live.py suffix and docstring). + +- Fix a couple of unit tests to not fail with Python 3.14. These involve + expectations for bytecode strings and error message contents; there was + no problem with SCons itself using 3.14. + +- Move the GitHub workflow test runner files from the SCons root to + the ci subdirectory of the testing directory. + +- GitHub: Enable the interactive tests on windows. + +- GitHub: Enable the packaging tar xz test on windows. + +- GitHub: Exclude two ninja tests that consistently fail on MacOS in + the experimental tests workflow. +- GitHub: Add a workflow file to test MSVC with optional environment + variables. + +- GitHub: Populate the MSVC cache before running the test suite for + select windows workflow files. + +- GitHub: Upload the failed_tests.log artifact on failure in select + workflow files. + +- GitHub: Change the runtest workflow to allow the python version to + be specified with the OS version. + +- Testing: Increase the default timeout from 20 seconds to 60 seconds + in the testing framework wait_for method. The timeout was increased + during isolated experiments of the interactive tests on windows. + +- Testing: Update the packaging tar bz2 and xz tests on on Windows. + The packaging tar bz2 and xz tests should be skipped on Windows 10 + and GitHub windows-2022. The packaging tar bz2 and xz tests should + be run on Windows 11 and GitHub windows-2025. + +- Ninja: Increase the number of generated source files in the + iterative speedup test from 200 to 250. Increasing the workload + should reduce the likelihood that the ninja tests are slower. Thanks to the following contributors listed below for their contributions to this release. ========================================================================================== .. code-block:: text - git shortlog --no-merges -ns 4.0.1..HEAD + + git shortlog --no-merges -ns 4.9.1..HEAD diff --git a/ReleaseConfig b/ReleaseConfig index 52fb6aaf7e..9ab93e1a6c 100755 --- a/ReleaseConfig +++ b/ReleaseConfig @@ -31,14 +31,14 @@ # If the release type is not 'final', the patchlevel is set to the # release date. This value is mandatory and must be present in this file. #version_tuple = (2, 2, 0, 'final', 0) -version_tuple = (4, 6, 2, 'a', 0) +version_tuple = (4, 9, 2, 'a', 0) # Python versions prior to unsupported_python_version cause a fatal error # when that version is used. Python versions prior to deprecate_python_version # cause a warning to be issued (assuming it's not disabled). These values are # mandatory and must be present in the configuration file. -unsupported_python_version = (3, 6, 0) -deprecated_python_version = (3, 6, 0) +unsupported_python_version = (3, 7, 0) +deprecated_python_version = (3, 7, 0) # If release_date is (yyyy, mm, dd, hh, mm, ss), that is used as the release # date and time. If release_date is (yyyy, mm, dd), it is used for the @@ -50,7 +50,7 @@ deprecated_python_version = (3, 6, 0) #month_year = 'December 2012' # If copyright years is not given, the release year is used as the end. -copyright_years = '2001 - 2023' +copyright_years = '2001 - 2025' # Local Variables: # tab-width:4 diff --git a/SCons/Action.py b/SCons/Action.py index 567f66cef4..18a488a78d 100644 --- a/SCons/Action.py +++ b/SCons/Action.py @@ -100,6 +100,8 @@ """ +from __future__ import annotations + import inspect import os import pickle @@ -109,7 +111,7 @@ from abc import ABC, abstractmethod from collections import OrderedDict from subprocess import DEVNULL, PIPE -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING import SCons.Debug import SCons.Errors @@ -120,7 +122,9 @@ from SCons.Debug import logInstanceCreation from SCons.Subst import SUBST_CMD, SUBST_RAW, SUBST_SIG from SCons.Util import is_String, is_List -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor class _null: pass @@ -481,9 +485,7 @@ def _do_create_action(act, kw): return None -# TODO: from __future__ import annotations once we get to Python 3.7 base, -# to avoid quoting the defined-later classname -def _do_create_list_action(act, kw) -> "ListAction": +def _do_create_list_action(act, kw) -> ListAction: """A factory for list actions. Convert the input list *act* into Actions and then wrap them in a @@ -529,7 +531,7 @@ def __call__( show=_null, execute=_null, chdir=_null, - executor: Optional[ExecutorType] = None, + executor: Executor | None = None, ): raise NotImplementedError @@ -541,15 +543,15 @@ def no_batch_key(self, env, target, source): batch_key = no_batch_key - def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str: + def genstring(self, target, source, env, executor: Executor | None = None) -> str: return str(self) @abstractmethod - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): raise NotImplementedError @abstractmethod - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): raise NotImplementedError def get_contents(self, target, source, env): @@ -601,10 +603,10 @@ def presub_lines(self, env): self.presub_env = None # don't need this any more return lines - def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_varlist(self, target, source, env, executor: Executor | None = None): return self.varlist - def get_targets(self, env, executor: Optional[ExecutorType]): + def get_targets(self, env, executor: Executor | None): """ Returns the type of targets ($TARGETS, $CHANGED_TARGETS) used by this action. @@ -658,7 +660,7 @@ def __call__(self, target, source, env, show=_null, execute=_null, chdir=_null, - executor: Optional[ExecutorType] = None): + executor: Executor | None = None): if not is_List(target): target = [target] if not is_List(source): @@ -742,10 +744,10 @@ def __call__(self, target, source, env, # an ABC like parent ActionBase, but things reach in and use it. It's # not just unittests or we could fix it up with a concrete subclass there. - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): raise NotImplementedError - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): raise NotImplementedError @@ -891,15 +893,6 @@ def scons_subproc_run(scons_env, *args, **kwargs) -> subprocess.CompletedProcess del kwargs['error'] kwargs['check'] = check - # TODO: Python version-compat stuff: remap/remove too-new args if needed - if 'text' in kwargs and sys.version_info < (3, 7): - kwargs['universal_newlines'] = kwargs.pop('text') - - if 'capture_output' in kwargs and sys.version_info < (3, 7): - capture_output = kwargs.pop('capture_output') - if capture_output: - kwargs['stdout'] = kwargs['stderr'] = PIPE - # Most SCons tools/tests expect not to fail on things like missing files. # check=True (or error="raise") means we're okay to take an exception; # else we catch the likely exception and construct a dummy @@ -1010,7 +1003,7 @@ def __str__(self) -> str: return str(self.cmd_list) - def process(self, target, source, env, executor=None, overrides: Optional[dict] = None) -> Tuple[List, bool, bool]: + def process(self, target, source, env, executor: Executor | None = None, overrides: dict | None = None) -> tuple[list, bool, bool]: if executor: result = env.subst_list(self.cmd_list, SUBST_CMD, executor=executor, overrides=overrides) else: @@ -1031,7 +1024,7 @@ def process(self, target, source, env, executor=None, overrides: Optional[dict] pass return result, ignore, silent - def strfunction(self, target, source, env, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None) -> str: + def strfunction(self, target, source, env, executor: Executor | None = None, overrides: dict | None = None) -> str: if self.cmdstr is None: return None if self.cmdstr is not _null: @@ -1046,7 +1039,7 @@ def strfunction(self, target, source, env, executor: Optional[ExecutorType] = No return '' return _string_from_cmd_list(cmd_list[0]) - def execute(self, target, source, env, executor: Optional[ExecutorType] = None): + def execute(self, target, source, env, executor: Executor | None = None): """Execute a command action. This will handle lists of commands as well as individual commands, @@ -1108,7 +1101,7 @@ def execute(self, target, source, env, executor: Optional[ExecutorType] = None): command=cmd_line) return 0 - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): """Return the signature contents of this action's command line. This strips $(-$) and everything in between the string, @@ -1123,7 +1116,7 @@ def get_presig(self, target, source, env, executor: Optional[ExecutorType] = Non return env.subst_target_source(cmd, SUBST_SIG, executor=executor) return env.subst_target_source(cmd, SUBST_SIG, target, source) - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): """Return the implicit dependencies of this action's command line.""" icd = env.get('IMPLICIT_COMMAND_DEPENDENCIES', True) if is_String(icd) and icd[:1] == '$': @@ -1145,7 +1138,7 @@ def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType # lightweight dependency scanning. return self._get_implicit_deps_lightweight(target, source, env, executor) - def _get_implicit_deps_lightweight(self, target, source, env, executor: Optional[ExecutorType]): + def _get_implicit_deps_lightweight(self, target, source, env, executor: Executor | None): """ Lightweight dependency scanning involves only scanning the first entry in an action string, even if it contains &&. @@ -1166,7 +1159,7 @@ def _get_implicit_deps_lightweight(self, target, source, env, executor: Optional res.append(env.fs.File(d)) return res - def _get_implicit_deps_heavyweight(self, target, source, env, executor: Optional[ExecutorType], + def _get_implicit_deps_heavyweight(self, target, source, env, executor: Executor | None, icd_int): """ Heavyweight dependency scanning involves scanning more than just the @@ -1234,7 +1227,7 @@ def __init__(self, generator, kw) -> None: self.varlist = kw.get('varlist', ()) self.targets = kw.get('targets', '$TARGETS') - def _generate(self, target, source, env, for_signature, executor: Optional[ExecutorType] = None): + def _generate(self, target, source, env, for_signature, executor: Executor | None = None): # ensure that target is a list, to make it easier to write # generator functions: if not is_List(target): @@ -1265,11 +1258,11 @@ def __str__(self) -> str: def batch_key(self, env, target, source): return self._generate(target, source, env, 1).batch_key(env, target, source) - def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str: + def genstring(self, target, source, env, executor: Executor | None = None) -> str: return self._generate(target, source, env, 1, executor).genstring(target, source, env) def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, - show=_null, execute=_null, chdir=_null, executor: Optional[ExecutorType] = None): + show=_null, execute=_null, chdir=_null, executor: Executor | None = None): act = self._generate(target, source, env, 0, executor) if act is None: raise SCons.Errors.UserError( @@ -1281,7 +1274,7 @@ def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, target, source, env, exitstatfunc, presub, show, execute, chdir, executor ) - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): """Return the signature contents of this action's command line. This strips $(-$) and everything in between the string, @@ -1289,13 +1282,13 @@ def get_presig(self, target, source, env, executor: Optional[ExecutorType] = Non """ return self._generate(target, source, env, 1, executor).get_presig(target, source, env) - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): return self._generate(target, source, env, 1, executor).get_implicit_deps(target, source, env) - def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_varlist(self, target, source, env, executor: Executor | None = None): return self._generate(target, source, env, 1, executor).get_varlist(target, source, env, executor) - def get_targets(self, env, executor: Optional[ExecutorType]): + def get_targets(self, env, executor: Executor | None): return self._generate(None, None, env, 1, executor).get_targets(env, executor) @@ -1341,22 +1334,22 @@ def _generate_cache(self, env): raise SCons.Errors.UserError("$%s value %s cannot be used to create an Action." % (self.var, repr(c))) return gen_cmd - def _generate(self, target, source, env, for_signature, executor: Optional[ExecutorType] = None): + def _generate(self, target, source, env, for_signature, executor: Executor | None = None): return self._generate_cache(env) def __call__(self, target, source, env, *args, **kw): c = self.get_parent_class(env) return c.__call__(self, target, source, env, *args, **kw) - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): c = self.get_parent_class(env) return c.get_presig(self, target, source, env) - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): c = self.get_parent_class(env) return c.get_implicit_deps(self, target, source, env) - def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_varlist(self, target, source, env, executor: Executor | None = None): c = self.get_parent_class(env) return c.get_varlist(self, target, source, env, executor) @@ -1389,7 +1382,7 @@ def function_name(self): except AttributeError: return "unknown_python_function" - def strfunction(self, target, source, env, executor: Optional[ExecutorType] = None): + def strfunction(self, target, source, env, executor: Executor | None = None): if self.cmdstr is None: return None if self.cmdstr is not _null: @@ -1430,7 +1423,7 @@ def __str__(self) -> str: return str(self.execfunction) return "%s(target, source, env)" % name - def execute(self, target, source, env, executor: Optional[ExecutorType] = None): + def execute(self, target, source, env, executor: Executor | None = None): exc_info = (None,None,None) try: if executor: @@ -1461,14 +1454,14 @@ def execute(self, target, source, env, executor: Optional[ExecutorType] = None): # more information about this issue. del exc_info - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): """Return the signature contents of this callable action.""" try: return self.gc(target, source, env) except AttributeError: return self.funccontents - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): return [] class ListAction(ActionBase): @@ -1485,7 +1478,7 @@ def list_of_actions(x): self.varlist = () self.targets = '$TARGETS' - def genstring(self, target, source, env, executor: Optional[ExecutorType] = None) -> str: + def genstring(self, target, source, env, executor: Executor | None = None) -> str: return '\n'.join([a.genstring(target, source, env) for a in self.list]) def __str__(self) -> str: @@ -1495,7 +1488,7 @@ def presub_lines(self, env): return SCons.Util.flatten_sequence( [a.presub_lines(env) for a in self.list]) - def get_presig(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_presig(self, target, source, env, executor: Executor | None = None): """Return the signature contents of this action list. Simple concatenation of the signatures of the elements. @@ -1503,7 +1496,7 @@ def get_presig(self, target, source, env, executor: Optional[ExecutorType] = Non return b"".join([bytes(x.get_contents(target, source, env)) for x in self.list]) def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, - show=_null, execute=_null, chdir=_null, executor: Optional[ExecutorType] = None): + show=_null, execute=_null, chdir=_null, executor: Executor | None = None): if executor: target = executor.get_all_targets() source = executor.get_all_sources() @@ -1514,13 +1507,13 @@ def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, return stat return 0 - def get_implicit_deps(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_implicit_deps(self, target, source, env, executor: Executor | None = None): result = [] for act in self.list: result.extend(act.get_implicit_deps(target, source, env)) return result - def get_varlist(self, target, source, env, executor: Optional[ExecutorType] = None): + def get_varlist(self, target, source, env, executor: Executor | None = None): result = OrderedDict() for act in self.list: for var in act.get_varlist(target, source, env, executor): @@ -1586,7 +1579,7 @@ def subst_kw(self, target, source, env): kw[key] = self.subst(self.kw[key], target, source, env) return kw - def __call__(self, target, source, env, executor: Optional[ExecutorType] = None): + def __call__(self, target, source, env, executor: Executor | None = None): args = self.subst_args(target, source, env) kw = self.subst_kw(target, source, env) return self.parent.actfunc(*args, **kw) diff --git a/SCons/Action.xml b/SCons/Action.xml index becd30f792..906324cfcb 100644 --- a/SCons/Action.xml +++ b/SCons/Action.xml @@ -1,9 +1,10 @@ Action strings can be segmented by the use of an AND operator, &&. -In a segemented string, each segment is a separate +In a segmented string, each segment is a separate command line, these are run -sequentially until one fails or the entire +sequentially until one fails, or the entire sequence has been executed. If an action string is segmented, then the selected behavior of &cv-IMPLICIT_COMMAND_DEPENDENCIES; diff --git a/SCons/ActionTests.py b/SCons/ActionTests.py index 39798809e9..2250c6f263 100644 --- a/SCons/ActionTests.py +++ b/SCons/ActionTests.py @@ -27,6 +27,8 @@ # contents, so try to minimize changes by defining them here, before we # even import anything. +from __future__ import annotations + def GlobalFunc() -> None: pass @@ -43,13 +45,19 @@ def __call__(self) -> None: import unittest from unittest import mock from subprocess import PIPE -from typing import Optional +from typing import TYPE_CHECKING + +# If assertEqual truncates strings so it's hard to see the diff, enable this: +# if 'unittest.util' in __import__('sys').modules: +# __import__('sys').modules['unittest.util']._MAX_LENGTH = 99999999 import SCons.Action import SCons.Environment import SCons.Errors from SCons.Action import scons_subproc_run -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor import TestCmd @@ -1541,8 +1549,6 @@ def LocalFunc() -> None: # Since the python bytecode has per version differences, we need different expected results per version func_matches = { - (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'), - (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), @@ -1550,6 +1556,7 @@ def LocalFunc() -> None: (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), (3, 12): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00y\x00),(),()'), (3, 13): bytearray(b'0, 0, 0, 0,(),(),(\x95\x00g\x00),(),()'), + (3, 14): bytearray(b'0, 0, 0, 0,(),(),(\x80\x00R\x00#\x00),(),()'), } meth_matches = [ @@ -1701,11 +1708,11 @@ def __call__(self, target, source, env) -> int: c = test.read(outfile, 'r') assert c == "class1b\n", c - def build_it(target, source, env, executor: Optional[ExecutorType] = None, self=self) -> int: + def build_it(target, source, env, executor: Executor | None = None, self=self) -> int: self.build_it = 1 return 0 - def string_it(target, source, env, executor: Optional[ExecutorType] = None, self=self): + def string_it(target, source, env, executor: Executor | None = None, self=self): self.string_it = 1 return None @@ -1723,8 +1730,6 @@ def LocalFunc() -> None: pass func_matches = { - (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'), - (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), @@ -1732,12 +1737,11 @@ def LocalFunc() -> None: (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), (3, 12): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00y\x00),(),()'), (3, 13): bytearray(b'0, 0, 0, 0,(),(),(\x95\x00g\x00),(),()'), + (3, 14): bytearray(b'0, 0, 0, 0,(),(),(\x80\x00R\x00#\x00),(),()'), } meth_matches = { - (3, 5): bytearray(b'1, 1, 0, 0,(),(),(d\x00\x00S),(),()'), - (3, 6): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 7): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 8): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'), @@ -1745,6 +1749,7 @@ def LocalFunc() -> None: (3, 11): bytearray(b'1, 1, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), (3, 12): bytearray(b'1, 1, 0, 0,(),(),(\x97\x00y\x00),(),()'), (3, 13): bytearray(b'1, 1, 0, 0,(),(),(\x95\x00g\x00),(),()'), + (3, 14): bytearray(b'1, 1, 0, 0,(),(),(\x80\x00R\x00#\x00),(),()'), } def factory(act, **kw): @@ -1978,8 +1983,6 @@ def LocalFunc() -> None: pass func_matches = { - (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'), - (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'), @@ -1987,6 +1990,7 @@ def LocalFunc() -> None: (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'), (3, 12): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00y\x00),(),()'), (3, 13): bytearray(b'0, 0, 0, 0,(),(),(\x95\x00g\x00),(),()'), + (3, 14): bytearray(b'0, 0, 0, 0,(),(),(\x80\x00R\x00#\x00),(),()'), } meth_matches = [ @@ -2042,8 +2046,6 @@ def LocalFunc() -> None: pass matches = { - (3, 5): b'd\x00\x00S', - (3, 6): b'd\x00S\x00', (3, 7): b'd\x00S\x00', (3, 8): b'd\x00S\x00', (3, 9): b'd\x00S\x00', @@ -2051,6 +2053,7 @@ def LocalFunc() -> None: (3, 11): b'\x97\x00d\x00S\x00', (3, 12): b'\x97\x00y\x00', (3, 13): b'\x95\x00g\x00', + (3, 14): b'\x80\x00R\x00#\x00', } with self.subTest(): @@ -2246,18 +2249,17 @@ def func1(a, b, c): # we need different expected results per version # Note unlike the others, this result is a tuple, use assertIn expected = { - (3, 5): (bytearray(b'3, 3, 0, 0,(),(),(|\x00\x00S),(),()'),), - (3, 6): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'),), - (3, 7): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'),), - (3, 8): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'),), - (3, 9): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'),), + (3, 7): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), + (3, 8): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), + (3, 9): (bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"),), (3, 10): ( # 3.10.1, 3.10.2 - bytearray(b'3, 3, 0, 0,(N.),(),(|\x00S\x00),(),()'), - bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'), + bytearray(b"3, 3, 0, 0,(N.),(),(|\x00S\x00),(),()"), + bytearray(b"3, 3, 0, 0,(),(),(|\x00S\x00),(),()"), ), - (3, 11): (bytearray(b'3, 3, 0, 0,(),(),(\x97\x00|\x00S\x00),(),()'),), - (3, 12): (bytearray(b'3, 3, 0, 0,(),(),(\x97\x00|\x00S\x00),(),()'),), - (3, 13): (bytearray(b'3, 3, 0, 0,(),(),(\x95\x00U\x00$\x00),(),()'),), + (3, 11): (bytearray(b"3, 3, 0, 0,(),(),(\x97\x00|\x00S\x00),(),()"),), + (3, 12): (bytearray(b"3, 3, 0, 0,(),(),(\x97\x00|\x00S\x00),(),()"),), + (3, 13): (bytearray(b"3, 3, 0, 0,(),(),(\x95\x00U\x00$\x00),(),()"),), + (3, 14): (bytearray(b"3, 3, 0, 0,(),(),(\x80\x00V\x00#\x00),(),()"),), } c = SCons.Action._function_contents(func1) @@ -2275,35 +2277,31 @@ def test_object_contents(self) -> None: # Since the python bytecode has per version differences, # we need different expected results per version expected = { - (3, 5): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01\x00|\x00\x00_\x00\x00d\x02\x00|\x00\x00_\x01\x00d\x00\x00S),(),(),2, 2, 0, 0,(),(),(d\x00\x00S),(),()}}{{{a=a,b=b}}}" - ), - (3, 6): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" - ), (3, 7): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), (3, 8): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), (3, 9): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), (3, 10): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), (3, 11): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x97\x00d\x01|\x00_\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x02|\x00_\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x00S\x00),(),(),2, 2, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x97\x00d\x01|\x00_\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x02|\x00_\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x00S\x00),(),(),2, 2, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()}}{{{a=a,b=b}}}" ), (3, 12): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x97\x00d\x01|\x00_\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x02|\x00_\x01\x00\x00\x00\x00\x00\x00\x00\x00y\x00),(),(),2, 2, 0, 0,(),(),(\x97\x00y\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x97\x00d\x01|\x00_\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x02|\x00_\x01\x00\x00\x00\x00\x00\x00\x00\x00y\x00),(),(),2, 2, 0, 0,(),(),(\x97\x00y\x00),(),()}}{{{a=a,b=b}}}" ), (3, 13): bytearray( - b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x95\x00S\x01U\x00l\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x02U\x00l\x01\x00\x00\x00\x00\x00\x00\x00\x00g\x00),(),(),2, 2, 0, 0,(),(),(\x95\x00g\x00),(),()}}{{{a=a,b=b}}}" + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x95\x00S\x01U\x00l\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x02U\x00l\x01\x00\x00\x00\x00\x00\x00\x00\x00g\x00),(),(),2, 2, 0, 0,(),(),(\x95\x00g\x00),(),()}}{{{a=a,b=b}}}" + ), + (3, 14): bytearray( + b"{TestClass:__main__}[[[(, ()), [(, (,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x80\x00R\x00V\x00n\x00\x00\x00\x00\x00\x00\x00\x00\x00R\x01V\x00n\x01\x00\x00\x00\x00\x00\x00\x00\x00R\x02#\x00),(),(),2, 2, 0, 0,(),(),(\x80\x00R\x00#\x00),(),()}}{{{a=a,b=b}}}" ), } - self.assertEqual(c, expected[sys.version_info[:2]]) def test_code_contents(self) -> None: @@ -2314,33 +2312,30 @@ def test_code_contents(self) -> None: # Since the python bytecode has per version differences, we need different expected results per version expected = { - (3, 5): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00\x00d\x00\x00\x83\x01\x00\x01d\x01\x00S)' - ), - (3, 6): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)' - ), (3, 7): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)' + b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" ), (3, 8): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)' + b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" ), (3, 9): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)' + b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" ), (3, 10): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)' + b"0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)" ), (3, 11): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x01S\x00)' + b"0, 0, 0, 0,(Hello, World!),(print),(\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x01S\x00)" ), (3, 12): bytearray( - b'0, 0, 0, 0,(Hello, World!),(print),(\x97\x00\x02\x00e\x00d\x00\xab\x01\x00\x00\x00\x00\x00\x00\x01\x00y\x01)' + b"0, 0, 0, 0,(Hello, World!),(print),(\x97\x00\x02\x00e\x00d\x00\xab\x01\x00\x00\x00\x00\x00\x00\x01\x00y\x01)" ), (3, 13): bytearray( b'0, 0, 0, 0,(Hello, World!),(print),(\x95\x00\\\x00"\x00S\x005\x01\x00\x00\x00\x00\x00\x00 \x00g\x01)' ), + (3, 14): bytearray( + b"0, 0, 0, 0,(Hello, World!),(print),(\x80\x00]\x00!\x00R\x004\x01\x00\x00\x00\x00\x00\x00\x1f\x00R\x01#\x00)" + ), } self.assertEqual(c, expected[sys.version_info[:2]]) @@ -2357,7 +2352,6 @@ def test_uncaught_exception_bubbles(self): raise Exception("expected a non-EnvironmentError exception") - def mock_subprocess_run(*args, **kwargs): """Replacement subprocess.run: return kwargs for checking.""" kwargs.pop("env") # the value of env isn't interesting here @@ -2387,17 +2381,17 @@ def test_scons_subproc_run(self): {"text": True, "check": False}, ) - # 3.6: - sys.version_info = (3, 6, 2) + # 3.7: + sys.version_info = (3, 7, 2) with self.subTest(): self.assertEqual( scons_subproc_run(env, capture_output=True), - {"check": False, "stdout": PIPE, "stderr": PIPE}, + {"capture_output": True, "check": False}, ) with self.subTest(): self.assertEqual( scons_subproc_run(env, text=True), - {"check": False, "universal_newlines": True}, + {"check": False, "text": True}, ) with self.subTest(): self.assertEqual( diff --git a/SCons/Builder.py b/SCons/Builder.py index 3efcc8271d..0e8d337521 100644 --- a/SCons/Builder.py +++ b/SCons/Builder.py @@ -99,10 +99,11 @@ """ +from __future__ import annotations + import os from collections import UserDict, UserList from contextlib import suppress -from typing import Optional import SCons.Action import SCons.Debug @@ -112,7 +113,8 @@ import SCons.Warnings from SCons.Debug import logInstanceCreation from SCons.Errors import InternalError, UserError -from SCons.Util.sctyping import ExecutorType +from SCons.Executor import Executor +from SCons.Node import Node class _Null: pass @@ -486,10 +488,11 @@ def _adjustixes(self, files, pre, suf, ensure_suffix: bool=False): # 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) + if not isinstance(f, Node): + 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) return result @@ -591,7 +594,7 @@ def _execute(self, env, target, source, overwarn={}, executor_kw={}): # build this particular list of targets from this particular list of # sources. - executor: Optional[ExecutorType] = None + executor: Executor | None = None key = None if self.multi: @@ -698,7 +701,7 @@ def set_src_suffix(self, src_suffix) -> None: src_suffix = [] elif not SCons.Util.is_List(src_suffix): src_suffix = [ src_suffix ] - self.src_suffix = [callable(suf) and suf or self.adjust_suffix(suf) for suf in src_suffix] + self.src_suffix = [suf if callable(suf) else self.adjust_suffix(suf) for suf in src_suffix] def get_src_suffix(self, env): """Get the first src_suffix in the list of src_suffixes.""" diff --git a/SCons/BuilderTests.py b/SCons/BuilderTests.py index b66f52439e..fff26171e9 100644 --- a/SCons/BuilderTests.py +++ b/SCons/BuilderTests.py @@ -21,6 +21,8 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations + import SCons.compat # Define a null function for use as a builder action. @@ -31,6 +33,7 @@ def Func() -> None: pass from collections import UserList +from typing import TYPE_CHECKING import io import os.path import re @@ -45,7 +48,9 @@ def Func() -> None: import SCons.Errors import SCons.Subst import SCons.Util -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor sys.stdout = io.StringIO() @@ -185,9 +190,9 @@ def generate_build_env(self, env): return env def get_build_env(self): return self.executor.get_build_env() - def set_executor(self, executor: ExecutorType) -> None: + def set_executor(self, executor: Executor) -> None: self.executor = executor - def get_executor(self, create: int=1) -> ExecutorType: + def get_executor(self, create: int=1) -> Executor: return self.executor class MyNode(MyNode_without_target_from_source): @@ -697,7 +702,7 @@ def test_single_source(self) -> None: """Test Builder with single_source flag set""" def func(target, source, env) -> None: """create the file""" - with open(str(target[0]), "w"): + with open(target[0], "w"): pass if len(source) == 1 and len(target) == 1: env['CNT'][0] = env['CNT'][0] + 1 @@ -754,7 +759,7 @@ def test_lists(self) -> None: """Testing handling lists of targets and source""" def function2(target, source, env, tlist = [outfile, outfile2], **kw) -> int: for t in target: - with open(str(t), 'w') as f: + with open(t, 'w') as f: f.write("function2\n") for t in tlist: if t not in list(map(str, target)): @@ -785,7 +790,7 @@ def function2(target, source, env, tlist = [outfile, outfile2], **kw) -> int: def function3(target, source, env, tlist = [sub1_out, sub2_out]) -> int: for t in target: - with open(str(t), 'w') as f: + with open(t, 'w') as f: f.write("function3\n") for t in tlist: if t not in list(map(str, target)): diff --git a/SCons/CacheDir.py b/SCons/CacheDir.py index 0174793df5..a5184b6e9c 100644 --- a/SCons/CacheDir.py +++ b/SCons/CacheDir.py @@ -27,8 +27,10 @@ import atexit import json import os +import shutil import stat import sys +import tempfile import uuid import SCons.Action @@ -36,6 +38,12 @@ import SCons.Warnings import SCons.Util +CACHE_PREFIX_LEN = 2 # first two characters used as subdirectory name +CACHE_TAG = ( + b"Signature: 8a477f597d28d172789f06886806bc55\n" + b"# SCons cache directory - see https://bford.info/cachedir/\n" +) + cache_enabled = True cache_debug = False cache_force = False @@ -64,23 +72,22 @@ def CacheRetrieveFunc(target, source, env) -> int: except OSError: pass st = fs.stat(cachefile) - fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) + fs.chmod(t.get_internal_path(), stat.S_IMODE(st.st_mode) | stat.S_IWRITE) return 0 -def CacheRetrieveString(target, source, env) -> None: +def CacheRetrieveString(target, source, env) -> str: t = target[0] - fs = t.fs cd = env.get_CacheDir() cachedir, cachefile = cd.cachepath(t) if t.fs.exists(cachefile): return "Retrieved `%s' from cache" % t.get_internal_path() - return None + return "" CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString) CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None) -def CachePushFunc(target, source, env): +def CachePushFunc(target, source, env) -> None: if cache_readonly: return @@ -134,8 +141,7 @@ def CachePushFunc(target, source, env): class CacheDir: def __init__(self, path) -> None: - """ - Initialize a CacheDir object. + """Initialize a CacheDir object. The cache configuration is stored in the object. It is read from the config file in the supplied path if @@ -147,53 +153,120 @@ def __init__(self, path) -> None: self.path = path self.current_cache_debug = None self.debugFP = None - self.config = dict() - if path is None: - return - - self._readconfig(path) + self.config = {} + if path is not None: + self._readconfig(path) + + def _add_config(self, path: str) -> None: + """Create the cache config file in *path*. + + Locking isn't necessary in the normal case - when the cachedir is + being created - because it's written to a unique directory first, + before the directory is renamed. But it is legal to call CacheDir + with an existing directory, which may be missing the config file, + and in that case we do need locking. Simpler to always lock. + """ + config_file = os.path.join(path, 'config') + # TODO: this breaks the "unserializable config object" test which + # does some crazy stuff, so for now don't use setdefault. It does + # seem like it would be better to preserve an exisiting value. + # self.config.setdefault('prefix_len', CACHE_PREFIX_LEN) + self.config['prefix_len'] = CACHE_PREFIX_LEN + with SCons.Util.FileLock(config_file, timeout=5, writer=True), open( + config_file, "x" + ) as config: + try: + json.dump(self.config, config) + except Exception: + msg = "Failed to write cache configuration for " + path + raise SCons.Errors.SConsEnvironmentError(msg) + # Add the tag file "carelessly" - the contents are not used by SCons + # so we don't care about the chance of concurrent writes. + try: + tagfile = os.path.join(path, "CACHEDIR.TAG") + with open(tagfile, 'xb') as cachedir_tag: + cachedir_tag.write(CACHE_TAG) + except FileExistsError: + pass - def _readconfig(self, path): - """ - Read the cache config. + def _mkdir_atomic(self, path: str) -> bool: + """Create cache directory at *path*. - If directory or config file do not exist, create. Take advantage - of Py3 capability in os.makedirs() and in file open(): just try - the operation and handle failure appropriately. + Uses directory renaming to avoid races. If we are actually + creating the dir, populate it with the metadata files at the + same time as that's the safest way. But it's not illegal to point + CacheDir at an existing directory that wasn't a cache previously, + so we may have to do that elsewhere, too. - Omit the check for old cache format, assume that's old enough - there will be none of those left to worry about. + Returns: + ``True`` if it we created the dir, ``False`` if already existed, - :param path: path to the cache directory + Raises: + SConsEnvironmentError: if we tried and failed to create the cache. """ - config_file = os.path.join(path, 'config') + directory = os.path.abspath(path) + if os.path.exists(directory): + return False + try: - # still use a try block even with exist_ok, might have other fails - os.makedirs(path, exist_ok=True) - except OSError: + # TODO: Python 3.7. See comment below. + # tempdir = tempfile.TemporaryDirectory(dir=os.path.dirname(directory)) + tempdir = tempfile.mkdtemp(dir=os.path.dirname(directory)) + except OSError as e: msg = "Failed to create cache directory " + path - raise SCons.Errors.SConsEnvironmentError(msg) + raise SCons.Errors.SConsEnvironmentError(msg) from e + + # TODO: Python 3.7: the context manager raises exception on cleanup + # if the temporary was moved successfully (File Not Found). + # Fixed in 3.8+. In the replacement below we manually clean up if + # the move failed as mkdtemp() does not. TemporaryDirectory's + # cleanup is more sophisitcated so prefer when we can use it. + # self._add_config(tempdir.name) + # with tempdir: + # try: + # os.replace(tempdir.name, directory) + # return True + # except OSError as e: + # # did someone else get there first? + # if os.path.isdir(directory): + # return False # context manager cleans up + # msg = "Failed to create cache directory " + path + # raise SCons.Errors.SConsEnvironmentError(msg) from e + + self._add_config(tempdir) + try: + os.replace(tempdir, directory) + return True + except OSError as e: + # did someone else get there first? attempt cleanup. + if os.path.isdir(directory): + try: + shutil.rmtree(tempdir) + except Exception: # we tried, don't worry about it + pass + return False + msg = "Failed to create cache directory " + path + raise SCons.Errors.SConsEnvironmentError(msg) from e + + def _readconfig(self, path: str) -> None: + """Read the cache config from *path*. + If directory or config file do not exist, create and populate. + """ + config_file = os.path.join(path, 'config') + created = self._mkdir_atomic(path) + if not created and not os.path.isfile(config_file): + # Could have been passed an empty directory + self._add_config(path) try: - with SCons.Util.FileLock(config_file, timeout=5, writer=True), open( - config_file, "x" + with SCons.Util.FileLock(config_file, timeout=5, writer=False), open( + config_file ) as config: - self.config['prefix_len'] = 2 - try: - json.dump(self.config, config) - except Exception: - msg = "Failed to write cache configuration for " + path - raise SCons.Errors.SConsEnvironmentError(msg) - except FileExistsError: - try: - with SCons.Util.FileLock(config_file, timeout=5, writer=False), open( - config_file - ) as config: - self.config = json.load(config) - except (ValueError, json.decoder.JSONDecodeError): - msg = "Failed to read cache configuration for " + path - raise SCons.Errors.SConsEnvironmentError(msg) + self.config = json.load(config) + except (ValueError, json.decoder.JSONDecodeError): + msg = "Failed to read cache configuration for " + path + raise SCons.Errors.SConsEnvironmentError(msg) def CacheDebug(self, fmt, target, cachefile) -> None: if cache_debug != self.current_cache_debug: @@ -252,7 +325,7 @@ def is_enabled(self) -> bool: def is_readonly(self) -> bool: return cache_readonly - def get_cachedir_csig(self, node): + def get_cachedir_csig(self, node) -> str: cachedir, cachefile = self.cachepath(node) if cachefile and os.path.exists(cachefile): return SCons.Util.hash_file_signature(cachefile, SCons.Node.FS.File.hash_chunksize) diff --git a/SCons/CacheDirTests.py b/SCons/CacheDirTests.py index 6ec9e84abb..0ecd502a13 100644 --- a/SCons/CacheDirTests.py +++ b/SCons/CacheDirTests.py @@ -28,9 +28,10 @@ import tempfile import stat -from TestCmd import TestCmd +from TestCmd import TestCmd, IS_WINDOWS, IS_ROOT import SCons.CacheDir +import SCons.Node.FS built_it = None @@ -62,15 +63,11 @@ def get_CacheDir(self): return self.cachedir class BaseTestCase(unittest.TestCase): - """ - Base fixtures common to our other unittest classes. - """ + """Base fixtures common to our other unittest classes.""" + def setUp(self) -> None: self.test = TestCmd(workdir='') - - import SCons.Node.FS self.fs = SCons.Node.FS.FS() - self._CacheDir = SCons.CacheDir.CacheDir('cache') def File(self, name, bsig=None, action=Action()): @@ -83,14 +80,11 @@ def File(self, name, bsig=None, action=Action()): return node def tearDown(self) -> None: - os.remove(os.path.join(self._CacheDir.path, 'config')) - os.rmdir(self._CacheDir.path) - # Should that be shutil.rmtree? + shutil.rmtree(self._CacheDir.path) class CacheDirTestCase(BaseTestCase): - """ - Test calling CacheDir code directly. - """ + """Test calling CacheDir code directly.""" + def test_cachepath(self) -> None: """Test the cachepath() method""" @@ -98,6 +92,7 @@ def test_cachepath(self) -> None: # of the file in cache. def my_collect(list, hash_format=None): return list[0] + save_collect = SCons.Util.hash_collect SCons.Util.hash_collect = my_collect @@ -112,6 +107,21 @@ def my_collect(list, hash_format=None): finally: SCons.Util.hash_collect = save_collect +class CacheDirExistsTestCase(unittest.TestCase): + """Test passing an existing but not setup cache directory.""" + + def setUp(self) -> None: + self.test = TestCmd(workdir='') + self.test.subdir('ex-cache') # force an empty dir + cache = self.test.workpath('ex-cache') + self.fs = SCons.Node.FS.FS() + self._CacheDir = SCons.CacheDir.CacheDir(cache) + + def test_existing_cachedir(self) -> None: + """Test the setup happened even though cache already existed.""" + assert os.path.exists(self.test.workpath('ex-cache', 'config')) + assert os.path.exists(self.test.workpath('ex-cache', 'CACHEDIR.TAG')) + class ExceptionTestCase(unittest.TestCase): """Test that the correct exceptions are thrown by CacheDir.""" @@ -124,28 +134,38 @@ def setUp(self) -> None: def tearDown(self) -> None: shutil.rmtree(self.tmpdir) - @unittest.skipIf(sys.platform.startswith("win"), "This fixture will not trigger an OSError on Windows") + @unittest.skipIf( + IS_WINDOWS, + "Skip privileged CacheDir test on Windows, cannot change directory rights", + ) + @unittest.skipIf( + IS_ROOT, + "Skip privileged CacheDir test if running as root.", + ) def test_throws_correct_on_OSError(self) -> None: - """Test that the correct error is thrown when cache directory cannot be created.""" + """Test for correct error when cache directory cannot be created.""" + test = TestCmd() privileged_dir = os.path.join(self.tmpdir, "privileged") - try: - os.mkdir(privileged_dir) - os.chmod(privileged_dir, stat.S_IREAD) - cd = SCons.CacheDir.CacheDir(os.path.join(privileged_dir, "cache")) - assert False, "Should have raised exception and did not" - except SCons.Errors.SConsEnvironmentError as e: - assert str(e) == "Failed to create cache directory {}".format(os.path.join(privileged_dir, "cache")) - finally: - os.chmod(privileged_dir, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD) - shutil.rmtree(privileged_dir) - + cachedir_path = os.path.join(privileged_dir, "cache") + os.makedirs(privileged_dir, exist_ok=True) + test.writable(privileged_dir, False) + with self.assertRaises(SCons.Errors.SConsEnvironmentError) as cm: + cd = SCons.CacheDir.CacheDir(cachedir_path) + self.assertEqual( + str(cm.exception), + "Failed to create cache directory " + cachedir_path + ) + test.writable(privileged_dir, True) + shutil.rmtree(privileged_dir) def test_throws_correct_when_failed_to_write_configfile(self) -> None: + """Test for correct error if cache config file cannot be created.""" + class Unserializable: - """A class which the JSON should not be able to serialize""" + """A class which the JSON module should not be able to serialize.""" def __init__(self, oldconfig) -> None: - self.something = 1 # Make the object unserializable + self.something = 1 # Make the object unserializable # Pretend to be the old config just enough self.__dict__["prefix_len"] = oldconfig["prefix_len"] @@ -160,16 +180,17 @@ def __setitem__(self, name, value) -> None: oldconfig = self._CacheDir.config self._CacheDir.config = Unserializable(oldconfig) + # Remove the config file that got created on object creation # so that _readconfig* will try to rewrite it old_config = os.path.join(self._CacheDir.path, "config") os.remove(old_config) - - try: + with self.assertRaises(SCons.Errors.SConsEnvironmentError) as cm: self._CacheDir._readconfig(self._CacheDir.path) - assert False, "Should have raised exception and did not" - except SCons.Errors.SConsEnvironmentError as e: - assert str(e) == "Failed to write cache configuration for {}".format(self._CacheDir.path) + self.assertEqual( + str(cm.exception), + "Failed to write cache configuration for " + self._CacheDir.path, + ) def test_raise_environment_error_on_invalid_json(self) -> None: config_file = os.path.join(self._CacheDir.path, "config") @@ -180,17 +201,16 @@ def test_raise_environment_error_on_invalid_json(self) -> None: with open(config_file, "w") as cfg: cfg.write(content) - try: - # Construct a new cache dir that will try to read the invalid config + with self.assertRaises(SCons.Errors.SConsEnvironmentError) as cm: + # Construct a new cachedir that will try to read the invalid config new_cache_dir = SCons.CacheDir.CacheDir(self._CacheDir.path) - assert False, "Should have raised exception and did not" - except SCons.Errors.SConsEnvironmentError as e: - assert str(e) == "Failed to read cache configuration for {}".format(self._CacheDir.path) + self.assertEqual( + str(cm.exception), + "Failed to read cache configuration for " + self._CacheDir.path, + ) class FileTestCase(BaseTestCase): - """ - Test calling CacheDir code through Node.FS.File interfaces. - """ + """Test calling CacheDir code through Node.FS.File interfaces.""" # These tests were originally in Nodes/FSTests.py and got moved # when the CacheDir support was refactored into its own module. # Look in the history for Node/FSTests.py if any of this needs @@ -266,9 +286,7 @@ def test_CacheRetrieveSilent(self) -> None: def test_CachePush(self) -> None: """Test the CachePush() function""" - save_CachePush = SCons.CacheDir.CachePush - SCons.CacheDir.CachePush = self.push try: @@ -301,7 +319,6 @@ def test_CachePush(self) -> None: def test_warning(self) -> None: """Test raising a warning if we can't copy a file to cache.""" - test = TestCmd(workdir='') save_copy2 = shutil.copy2 @@ -329,7 +346,6 @@ def mkdir(dir, mode: int=0) -> None: def test_no_strfunction(self) -> None: """Test handling no strfunction() for an action.""" - save_CacheRetrieveSilent = SCons.CacheDir.CacheRetrieveSilent f8 = self.File("cd.f8", 'f8_bsig') diff --git a/SCons/Defaults.py b/SCons/Defaults.py index a5d49fc76f..d971d060f6 100644 --- a/SCons/Defaults.py +++ b/SCons/Defaults.py @@ -31,12 +31,14 @@ from distutils.msvccompiler. """ +from __future__ import annotations + import os import shutil import stat import sys import time -from typing import List, Callable +from typing import Callable import SCons.Action import SCons.Builder @@ -467,8 +469,8 @@ def _stripixes( prefix: str, items, suffix: str, - stripprefixes: List[str], - stripsuffixes: List[str], + stripprefixes: list[str], + stripsuffixes: list[str], env, literal_prefix: str = "", c: Callable[[list], list] = None, @@ -547,7 +549,7 @@ def _stripixes( return c(prefix, stripped, suffix, env) -def processDefines(defs) -> List[str]: +def processDefines(defs) -> list[str]: """Return list of strings for preprocessor defines from *defs*. Resolves the different forms ``CPPDEFINES`` can be assembled in: diff --git a/SCons/Defaults.xml b/SCons/Defaults.xml index c7b86437c6..4385390ca1 100644 --- a/SCons/Defaults.xml +++ b/SCons/Defaults.xml @@ -1,9 +1,10 @@ None, the name and definition are combined into a single name=definition item -before the preending/appending. +before the prepending/appending. @@ -260,7 +261,7 @@ to each directory in &cv-link-CPPPATH;. The list of directories that the C preprocessor will search for include directories. The C/C++ implicit dependency scanner will search these directories for include files. -In general it's not advised to put include directory directives +In general, it's not advised to put include directory directives directly into &cv-link-CCFLAGS; or &cv-link-CXXFLAGS; as the result will be non-portable and the directories will not be searched by the dependency scanner. @@ -275,7 +276,7 @@ directory names in &cv-CPPPATH; will be looked-up relative to the directory of the SConscript file when they are used in a command. To force &scons; -to look-up a directory relative to the root of the source tree use +to lookup a directory relative to the root of the source tree, use the # prefix: @@ -284,7 +285,7 @@ env = Environment(CPPPATH='#/include') -The directory look-up can also be forced using the +The directory lookup can also be forced using the &f-link-Dir; function: @@ -547,7 +548,7 @@ directory names in &cv-LIBPATH; will be looked-up relative to the directory of the SConscript file when they are used in a command. To force &scons; -to look-up a directory relative to the root of the source tree use +to lookup a directory relative to the root of the source tree, use the # prefix: @@ -556,7 +557,7 @@ env = Environment(LIBPATH='#/libs') -The directory look-up can also be forced using the +The directory lookup can also be forced using the &f-link-Dir; function: @@ -617,7 +618,7 @@ If &cv-link-LIBLITERALPREFIX; is set to a non-empty string, then a string-valued &cv-LIBS; entry that starts with &cv-link-LIBLITERALPREFIX; will cause the rest of the entry -to be searched for for unmodified, +to be searched for unmodified, but respecting normal library search paths (this is an exception to the guideline above about leaving off the prefix/suffix from the library name). @@ -648,12 +649,12 @@ env.Append(LIBS=File('/tmp/mylib.so')) For each &Builder; call that causes linking with libraries, &SCons; will add the libraries in the setting of &cv-LIBS; -in effect at that moment to the dependecy graph +in effect at that moment to the dependency graph as dependencies of the target being generated. -The library list will transformed to command line +The library list will be transformed to command-line arguments through the automatically-generated &cv-link-_LIBFLAGS; &consvar; which is constructed by @@ -728,43 +729,53 @@ searching the repositories. -([**kwargs]) +([key=value, ...]) Instantiates and returns the global &consenv; object. -This environment is used internally by SCons -when it executes many of the global functions listed in this section -(that is, those not called as methods of a specific &consenv;). -The &defenv; is a singleton: -the keyword arguments are used only on the first call; -on subsequent calls the already-constructed object is returned +The &DefEnv; is used internally by &SCons; +when executing a global function +or the global form of a Builder method +that requires access to a &consenv;. + + + +On the first call, +arguments are interpreted as for the &f-link-Environment; function. +The &DefEnv; is a singleton; +subsequent calls to &f-DefaultEnvironment; return +the already-constructed object, and any keyword arguments are silently ignored. -The &defenv; can still be modified after instantiation -in the same way as any other &consenv;. -The &defenv; is independent: -modifying it has no effect on any other &consenv; -constructed by an &f-link-Environment; or &f-link-Clone; call. - - - -It is not mandatory to call &f-DefaultEnvironment;: -the &defenv; is instantiated automatically when the -build phase begins if this function has not been called; -however calling it explicitly gives the opportunity to -affect and examine the contents of the &defenv;. -Instantiation happens even if no build instructions -appar to use it, as there are internal uses. -If there are no uses in the project &SConscript; files, -a small performance gain may be seen by calling -&f-DefaultEnvironment; with an empty tools list, -thus avoiding that part of the initialization cost. -This is mainly of interest in testing when &scons; is -launched repeatedly in a short time period: - -DefaultEnvironment(tools=[]) - + + +The &DefEnv; can be modified after instantiation, +similar to other &consenvs;, +although some &consenv; methods may be unavailable. +Modifying the &DefEnv; has no effect on any other &consenv;, +either existing or newly constructed. + + + +It is not necessary to explicitly call &f-DefaultEnvironment;. +&SCons; instantiates the &defenv; automatically when the +build phase begins, if has not already been done. +However, calling it explicitly provides the opportunity to +affect and examine its contents. +Instantiation occurs even if nothing in the build system +appears to use it, due to internal uses. + + + +If the project &SConscript; files do not use global functions or Builders, +a small performance gain may be achieved by calling +&f-DefaultEnvironment; with an empty tools list +(DefaultEnvironment(tools=[])). +This avoids the tool initialization cost for the &DefEnv;, +which is mainly of interest in the test suite +where &scons; is launched repeatedly in a short time period. + diff --git a/SCons/Environment.py b/SCons/Environment.py index 5bf763d91a..ff9309a6fe 100644 --- a/SCons/Environment.py +++ b/SCons/Environment.py @@ -30,6 +30,8 @@ are construction variables used to initialize the Environment. """ +from __future__ import annotations + import copy import os import sys @@ -37,7 +39,7 @@ import shlex from collections import UserDict, UserList, deque from subprocess import PIPE, DEVNULL -from typing import Callable, Collection, Optional, Sequence, Union +from typing import TYPE_CHECKING, Callable, Collection, Sequence import SCons.Action import SCons.Builder @@ -76,7 +78,9 @@ to_String_for_subst, uniquer_hashables, ) -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor class _Null: pass @@ -510,13 +514,6 @@ def update(self, mapping) -> None: self.__setitem__(i, v) -_is_valid_var = re.compile(r'[_a-zA-Z]\w*$') - -def is_valid_construction_var(varstr) -> bool: - """Return True if *varstr* is a legitimate construction variable.""" - return _is_valid_var.match(varstr) - - class SubstitutionEnvironment: """Base class for different flavors of construction environments. @@ -541,6 +538,11 @@ class SubstitutionEnvironment: Environment.Base to create their own flavors of construction environment, we'll save that for a future refactoring when this class actually becomes useful.) + + Special note: methods here and in actual child classes might be called + via proxy from an :class:`OverrideEnvironment`, which isn't in the + class inheritance chain. Take care that methods called with a *self* + that's really an ``OverrideEnvironment`` don't make bad assumptions. """ def __init__(self, **kw) -> None: @@ -574,6 +576,20 @@ def _init_special(self) -> None: self._special_set_keys = list(self._special_set.keys()) def __eq__(self, other): + """Compare two environments. + + This is used by checks in Builder to determine if duplicate + targets have environments that would cause the same result. + The more reliable way (respecting the admonition to avoid poking + at :attr:`_dict` directly) would be to use ``Dictionary`` so this + is sure to work even if one or both are are instances of + :class:`OverrideEnvironment`. However an actual + ``SubstitutionEnvironment`` doesn't have a ``Dictionary`` method + That causes problems for unit tests written to excercise + ``SubsitutionEnvironment`` directly, although nobody else seems + to ever instantiate one. We count on :class:`OverrideEnvironment` + to fake the :attr:`_dict` to make things work. + """ return self._dict == other._dict def __delitem__(self, key) -> None: @@ -587,26 +603,20 @@ def __getitem__(self, key): return self._dict[key] def __setitem__(self, key, value): - # This is heavily used. This implementation is the best we have - # according to the timings in bench/env.__setitem__.py. - # - # The "key in self._special_set_keys" test here seems to perform - # pretty well for the number of keys we have. A hard-coded - # list worked a little better in Python 2.5, but that has the - # disadvantage of maybe getting out of sync if we ever add more - # variable names. - # So right now it seems like a good trade-off, but feel free to - # revisit this with bench/env.__setitem__.py as needed (and - # as newer versions of Python come out). if key in self._special_set_keys: self._special_set[key](self, key, value) else: - # If we already have the entry, then it's obviously a valid - # key and we don't need to check. If we do check, using a - # global, pre-compiled regular expression directly is more - # efficient than calling another function or a method. - if key not in self._dict and not _is_valid_var.match(key): - raise UserError("Illegal construction variable `%s'" % key) + # Performance: since this is heavily used, try to avoid checking + # if the variable is valid unless necessary. bench/__setitem__.py + # times a bunch of different approaches. Based the most recent + # run, against Python 3.6-3.13(beta), the best we can do across + # different combinations of actions is to use a membership test + # to see if we already have the variable, if so it must already + # have been checked, so skip; if we do check, "isidentifier()" + # (new in Python 3 so wasn't in benchmark until recently) + # on the key is the best. + if key not in self._dict and not key.isidentifier(): + raise UserError(f"Illegal construction variable {key!r}") self._dict[key] = value def get(self, key, default=None): @@ -639,7 +649,7 @@ def arg2nodes(self, args, node_factory=_null, lookup_list=_null, **kw): args - filename strings or nodes to convert; nodes are just added to the list without further processing. node_factory - optional factory to create the nodes; if not - specified, will use this environment's ``fs.File method. + specified, will use this environment's ``fs.File`` method. lookup_list - optional list of lookup functions to call to attempt to find the file referenced by each *args*. kw - keyword arguments that represent additional nodes to add. @@ -692,7 +702,7 @@ def gvars(self): def lvars(self): return {} - def subst(self, string, raw: int=0, target=None, source=None, conv=None, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None): + def subst(self, string, raw: int=0, target=None, source=None, conv=None, executor: Executor | None = None, overrides: dict | None = None): """Recursively interpolates construction variables from the Environment into the specified string, returning the expanded result. Construction variables are specified by a $ prefix @@ -718,7 +728,7 @@ def subst_kw(self, kw, raw: int=0, target=None, source=None): nkw[k] = v return nkw - def subst_list(self, string, raw: int=0, target=None, source=None, conv=None, executor: Optional[ExecutorType] = None, overrides: Optional[dict] = None): + def subst_list(self, string, raw: int=0, target=None, source=None, conv=None, executor: Executor | None = None, overrides: dict | None = None): """Calls through to SCons.Subst.scons_subst_list(). See the documentation for that function. @@ -824,16 +834,30 @@ def RemoveMethod(self, function) -> None: self.added_methods = [dm for dm in self.added_methods if dm.method is not function] def Override(self, overrides): - """ - Produce a modified environment whose variables are overridden by - the overrides dictionaries. "overrides" is a dictionary that - will override the variables of this environment. + """Create an override environment from the current environment. - This function is much more efficient than Clone() or creating - a new Environment because it doesn't copy the construction + Produces a modified environment where the current variables are + overridden by any same-named variables from the *overrides* dict. + + An override is much more efficient than doing :meth:`~Base.Clone` + or creating a new Environment because it doesn't copy the construction environment dictionary, it just wraps the underlying construction environment, and doesn't even create a wrapper object if there are no overrides. + + Using this method is preferred over directly instantiating an + :class:`OverrideEnvirionment` because extra checks are performed, + substitution takes place, and there is special handling for a + *parse_flags* keyword argument. + + This method is not currently exposed as part of the public API, + but is invoked internally when things like builder calls have + keyword arguments, which are then passed as *overrides* here. + Some tools also call this explicitly. + + Returns: + A proxy environment of type :class:`OverrideEnvironment`. + or the current environment if *overrides* is empty. """ if not overrides: return self o = copy_non_reserved_keywords(overrides) @@ -881,7 +905,7 @@ def ParseFlags(self, *flags) -> dict: 'RPATH' : [], } - def do_parse(arg: Union[str, Sequence]) -> None: + def do_parse(arg: str | Sequence) -> None: if not arg: return @@ -959,7 +983,7 @@ def append_define(name, mapping=mapping) -> None: else: mapping[append_next_arg_to].append(arg) append_next_arg_to = None - elif not arg[0] in ['-', '+']: + elif arg[0] not in ['-', '+']: mapping['LIBS'].append(self.fs.File(arg)) elif arg == '-dylib_file': mapping['LINKFLAGS'].append(arg) @@ -1427,7 +1451,6 @@ def Append(self, **kw) -> None: The variable is created if it is not already present. """ - kw = copy_non_reserved_keywords(kw) for key, val in kw.items(): if key == 'CPPDEFINES': @@ -1525,11 +1548,17 @@ def AppendENVPath(self, name, newpath, envname: str='ENV', self._dict[envname][name] = nv - def AppendUnique(self, delete_existing: bool=False, **kw) -> None: - """Append values to existing construction variables - in an Environment, if they're not already there. - If delete_existing is True, removes existing values first, so - values move to end. + def AppendUnique(self, delete_existing: bool = False, **kw) -> None: + """Append values uniquely to existing construction variables. + + Similar to :meth:`Append`, but the result may not contain duplicates + of any values passed for each given key (construction variable), + so an existing list may need to be pruned first, however it may still + contain other duplicates. + + If *delete_existing* is true, removes existing values first, so values + move to the end; otherwise (the default) values are skipped if + already present. """ kw = copy_non_reserved_keywords(kw) for key, val in kw.items(): @@ -1552,12 +1581,11 @@ def AppendUnique(self, delete_existing: bool=False, **kw) -> None: val = [x for x in val if x not in dk] self._dict[key] = dk + val else: + # val is not a list, so presumably a scalar (likely str). dk = self._dict[key] if is_List(dk): - # By elimination, val is not a list. Since dk is a - # list, wrap val in a list first. if delete_existing: - dk = list(filter(lambda x, val=val: x not in val, dk)) + dk = [x for x in dk if x != val] self._dict[key] = dk + [val] else: if val not in dk: @@ -1568,16 +1596,28 @@ def AppendUnique(self, delete_existing: bool=False, **kw) -> None: self._dict[key] = dk + val self.scanner_map_delete(kw) - def Clone(self, tools=[], toolpath=None, parse_flags = None, **kw): + def Clone(self, tools=[], toolpath=None, variables=None, parse_flags=None, **kw): """Return a copy of a construction Environment. - The copy is like a Python "deep copy"--that is, independent - copies are made recursively of each objects--except that - a reference is copied when an object is not deep-copyable - (like a function). There are no references to any mutable - objects in the original Environment. - """ + The copy is like a Python "deep copy": independent copies are made + recursively of each object, except that a reference is copied when + an object is not deep-copyable (like a function). There are no + references to any mutable objects in the original environment. + + Unrecognized keyword arguments are taken as construction variable + assignments. + Arguments: + tools: list of tools to initialize. + toolpath: list of paths to search for tools. + variables: a :class:`~SCons.Variables.Variables` object to + use to populate construction variables from command-line + variables. + parse_flags: option strings to parse into construction variables. + + .. versionadded:: 4.8.0 + The optional *variables* parameter was added. + """ builders = self._dict.get('BUILDERS', {}) clone = copy.copy(self) @@ -1603,6 +1643,8 @@ def Clone(self, tools=[], toolpath=None, parse_flags = None, **kw): for key, value in kw.items(): new[key] = SCons.Subst.scons_subst_once(value, self, key) clone.Replace(**new) + if variables: + variables.Update(clone) apply_tools(clone, tools, toolpath) @@ -1670,49 +1712,64 @@ def Detect(self, progs): return None - def Dictionary(self, *args): - r"""Return construction variables from an environment. + def Dictionary(self, *args: str, as_dict: bool = False): + """Return construction variables from an environment. Args: - \*args (optional): variable names to look up + args (optional): construction variable names to select. + If omitted, all variables are selected and returned + as a dict. + as_dict: if true, and *args* is supplied, return the + variables and their values in a dict. If false + (the default), return a single value as a scalar, + or multiple values in a list. Returns: - If `args` omitted, the dictionary of all construction variables. - If one arg, the corresponding value is returned. - If more than one arg, a list of values is returned. + A dictionary of construction variables, or a single value + or list of values. Raises: - KeyError: if any of `args` is not in the construction environment. + KeyError: if any of *args* is not in the construction environment. + .. versionchanged:: 4.9.0 + Added the *as_dict* keyword arg to specify always returning a dict. """ if not args: return self._dict - dlist = [self._dict[x] for x in args] + if as_dict: + return {key: self._dict[key] for key in args} + dlist = [self._dict[key] for key in args] if len(dlist) == 1: - dlist = dlist[0] + return dlist[0] return dlist - def Dump(self, key: Optional[str] = None, format: str = 'pretty') -> str: - """ Returns a dump of serialized construction variables. + def Dump(self, *key: str, format: str = 'pretty') -> str: + """Return string of serialized construction variables. - The display formats are intended for humaan readers when - debugging - none of the supported formats produce a result that - SCons itself can directly make use of. Objects that cannot - directly be represented get a placeholder like - ```` or ``<>``. + Produces a "pretty" output of a dictionary of selected + construction variables, or all of them. The display *format* is + selectable. The result is intended for human consumption (e.g, + to print), mainly when debugging. Objects that cannot directly be + represented get a placeholder like ```` + (pretty-print) or ``<>`` (JSON). Args: - key: if ``None``, format the whole dict of variables, - else format just the value of *key*. + key: variables to format together with their values. + If omitted, format the whole dict of variables, format: specify the format to serialize to. ``"pretty"`` generates a pretty-printed string, ``"json"`` a JSON-formatted string. Raises: ValueError: *format* is not a recognized serialization format. + + .. versionchanged:: 4.9.0 + *key* is no longer limited to a single construction variable name. + If *key* is supplied, a formatted dictionary is generated like the + no-arg case - previously a single *key* displayed just the value. """ - if key: - cvars = self.Dictionary(key) + if len(key): + cvars = self.Dictionary(*key, as_dict=True) else: cvars = self.Dictionary() @@ -1734,17 +1791,18 @@ def Dump(self, key: Optional[str] = None, format: str = 'pretty') -> str: class DumpEncoder(json.JSONEncoder): """SCons special json Dump formatter.""" + def default(self, obj): if isinstance(obj, (UserList, UserDict)): return obj.data return f'<>' return json.dumps(cvars, indent=4, cls=DumpEncoder, sort_keys=True) - else: - raise ValueError("Unsupported serialization format: %s." % fmt) + raise ValueError("Unsupported serialization format: %s." % fmt) - def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> Optional[str]: + + def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> str | None: """Search *paths* for a path that has *prefix* and *suffix*. Returns on first match. @@ -1841,7 +1899,6 @@ def Prepend(self, **kw) -> None: The variable is created if it is not already present. """ - kw = copy_non_reserved_keywords(kw) for key, val in kw.items(): if key == 'CPPDEFINES': @@ -1928,11 +1985,17 @@ def PrependENVPath(self, name, newpath, envname: str='ENV', self._dict[envname][name] = nv - def PrependUnique(self, delete_existing: bool=False, **kw) -> None: - """Prepend values to existing construction variables - in an Environment, if they're not already there. - If delete_existing is True, removes existing values first, so - values move to front. + def PrependUnique(self, delete_existing: bool = False, **kw) -> None: + """Prepend values uniquely to existing construction variables. + + Similar to :meth:`Prepend`, but the result may not contain duplicates + of any values passed for each given key (construction variable), + so an existing list may need to be pruned first, however it may still + contain other duplicates. + + If *delete_existing* is true, removes existing values first, so values + move to the front; otherwise (the default) values are skipped if + already present. """ kw = copy_non_reserved_keywords(kw) for key, val in kw.items(): @@ -1955,12 +2018,11 @@ def PrependUnique(self, delete_existing: bool=False, **kw) -> None: val = [x for x in val if x not in dk] self._dict[key] = val + dk else: + # val is not a list, so presumably a scalar (likely str). dk = self._dict[key] if is_List(dk): - # By elimination, val is not a list. Since dk is a - # list, wrap val in a list first. if delete_existing: - dk = [x for x in dk if x not in val] + dk = [x for x in dk if x != val] self._dict[key] = [val] + dk else: if val not in dk: @@ -2021,7 +2083,7 @@ def _find_toolpath_dir(self, tp): return self.fs.Dir(self.subst(tp)).srcnode().get_abspath() def Tool( - self, tool: Union[str, Callable], toolpath: Optional[Collection[str]] = None, **kwargs + self, tool: str | Callable, toolpath: Collection[str] | None = None, **kwargs ) -> Callable: """Find and run tool module *tool*. @@ -2186,6 +2248,16 @@ def CacheDir(self, path, custom_class=None) -> None: self.get_CacheDir() def Clean(self, targets, files) -> None: + """Mark additional files for cleaning. + + *files* will be removed if any of *targets* are selected + for cleaning - that is, the combination of target selection + and -c clean mode. + + Args: + targets (files or nodes): targets to associate *files* with. + files (files or nodes): items to remove if *targets* are selected. + """ global CleanTargets tlist = self.arg2nodes(targets, self.fs.Entry) flist = self.arg2nodes(files, self.fs.Entry) @@ -2272,8 +2344,8 @@ def PyPackageDir(self, modulename): return result return self.fs.PyPackageDir(s) - def NoClean(self, *targets): - """Tag target(s) so that it will not be cleaned by -c.""" + def NoClean(self, *targets) -> list: + """Tag *targets* to not be removed in clean mode.""" tlist = [] for t in targets: tlist.extend(self.arg2nodes(t, self.fs.Entry)) @@ -2516,45 +2588,75 @@ def FindInstalledFiles(self): class OverrideEnvironment(Base): - """A proxy that overrides variables in a wrapped construction - environment by returning values from an overrides dictionary in - preference to values from the underlying subject environment. - - This is a lightweight (I hope) proxy that passes through most use of - attributes to the underlying Environment.Base class, but has just - enough additional methods defined to act like a real construction - environment with overridden values. It can wrap either a Base - construction environment, or another OverrideEnvironment, which - can in turn nest arbitrary OverrideEnvironments... - - Note that we do *not* call the underlying base class - (SubsitutionEnvironment) initialization, because we get most of those - from proxying the attributes of the subject construction environment. - But because we subclass SubstitutionEnvironment, this class also - has inherited arg2nodes() and subst*() methods; those methods can't - be proxied because they need *this* object's methods to fetch the - values from the overrides dictionary. + """A proxy that implements override environments. + + Returns attributes/methods and construction variables from the + base environment *subject*, except that same-named construction + variables from *overrides* are returned on read access; assignment + to a construction variable creates an override entry - *subject* is + not modified. This is a much lighter weight approach for limited-use + setups than cloning an environment, for example to handle a builder + call with keyword arguments that make a temporary change to the + current environment:: + + env.Program(target="foo", source=sources, DEBUG=True) + + While the majority of methods are proxied from the underlying environment + class, enough plumbing is defined in this class for it to behave + like an ordinary Environment without the caller needing to know it is + "special" in some way. We don't call the initializer of the class + we're proxying, rather depend on it already being properly set up. + + Deletion is handled specially, if a variable was explicitly deleted, + it should no longer appear to be in the env, but we also don't want to + modify the subject environment. + + :class:`OverrideEnvironment` can nest arbitratily, *subject* + can be an existing instance. Although instances can be + instantiated directly, the expected use is to call the + :meth:`~SubstitutionEnvironment.Override` method as a factory. + + Note Python does not give us a way to assure the subject environment + is not modified. Assigning to a variable creates a new entry in + the override, but moditying a variable will first fetch the one + from the subject, and if mutable, it will just be modified in place. + For example: ``over_env.Append(CPPDEFINES="-O")``, where ``CPPDEFINES`` + is an existing list or :class:`~SCons.Util.CLVar`, will successfully + append to ``CPPDEFINES`` in the subject env. To avoid such leakage, + clients such as Scanners, Emitters and Action functions called by a + Builder using override syntax must take care if modifying an env + (which is not advised anyway) in case they were passed an + ``OverrideEnvironment``. """ - def __init__(self, subject, overrides=None) -> None: + def __init__(self, subject, overrides: dict | None = None) -> None: if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.OverrideEnvironment') + overrides = {} if overrides is None else overrides + # set these directly via __dict__ to avoid trapping by __setattr__ self.__dict__['__subject'] = subject - if overrides is None: - self.__dict__['overrides'] = {} - else: - self.__dict__['overrides'] = overrides + self.__dict__['overrides'] = overrides + self.__dict__['__deleted'] = [] # Methods that make this class act like a proxy. + def __getattr__(self, name): + # Proxied environment methods don't know (nor should they have to) that + # they could be called with an OverrideEnvironment as 'self' and may + # access the _dict construction variable dict directly, so we need to + # pretend to have one, and not serve up the one from the subject, or it + # will miss the overridden values (and possibly modify the base). Use + # ourselves and hope the dict-like methods below are sufficient. + if name == '_dict': + return self + attr = getattr(self.__dict__['__subject'], name) - # Here we check if attr is one of the Wrapper classes. For - # example when a pseudo-builder is being called from an - # OverrideEnvironment. - # - # These wrappers when they're constructed capture the - # Environment they are being constructed with and so will not - # have access to overrided values. So we rebuild them with the - # OverrideEnvironment so they have access to overrided values. + + # Check first if attr is one of the Wrapper classes, for example + # when a pseudo-builder is being called from an OverrideEnvironment. + # These wrappers, when they're constructed, capture the Environment + # they are being constructed with and so will not have access to + # overridden values. So we rebuild them with the OverrideEnvironment + # so they have access to overridden values. if isinstance(attr, MethodWrapper): return attr.clone(self) else: @@ -2564,68 +2666,116 @@ def __setattr__(self, name, value) -> None: setattr(self.__dict__['__subject'], name, value) # Methods that make this class act like a dictionary. + def __getitem__(self, key): + """Return the visible value of *key*. + + Backfills from the subject env if *key* doesn't have an entry in + the override, and is not explicity deleted. + """ try: return self.__dict__['overrides'][key] except KeyError: + if key in self.__dict__['__deleted']: + raise return self.__dict__['__subject'].__getitem__(key) - def __setitem__(self, key, value): - if not is_valid_construction_var(key): - raise UserError("Illegal construction variable `%s'" % key) + def __setitem__(self, key, value) -> None: + # This doesn't have the same performance equation as a "real" + # environment: in an override you're basically just writing + # new stuff; it's not a common case to be changing values already + # set in the override dict, so don't spend time checking for existance. + if not key.isidentifier(): + raise UserError(f"Illegal construction variable {key!r}") self.__dict__['overrides'][key] = value + if key in self.__dict__['__deleted']: + # it's no longer "deleted" if we set it + self.__dict__['__deleted'].remove(key) - def __delitem__(self, key): + def __delitem__(self, key) -> None: + """Delete *key* from override. + + Makes *key* not visible in the override. Previously implemented + by deleting from ``overrides`` and from ``__subject``, which + keeps :meth:`__getitem__` from filling it back in next time. + However, that approach was a form of leak, as the subject + environment was modified. So instead we log that it's deleted + and use that to make decisions elsewhere. + """ try: del self.__dict__['overrides'][key] except KeyError: - deleted = 0 + deleted = False else: - deleted = 1 - try: - result = self.__dict__['__subject'].__delitem__(key) - except KeyError: - if not deleted: - raise - result = None - return result + deleted = True + if not deleted and key not in self.__dict__['__subject']: + raise KeyError(key) + self.__dict__['__deleted'].append(key) def get(self, key, default=None): - """Emulates the get() method of dictionaries.""" + """Emulates the ``get`` method of dictionaries. + + Backfills from the subject environment if *key* is not in the override + and not deleted. + """ try: return self.__dict__['overrides'][key] except KeyError: + if key in self.__dict__['__deleted']: + return default return self.__dict__['__subject'].get(key, default) def __contains__(self, key) -> bool: + """Emulates the ``contains`` method of dictionaries. + + Backfills from the subject environment if *key* is not in the override + and not deleted. + """ if key in self.__dict__['overrides']: return True + if key in self.__dict__['__deleted']: + return False return key in self.__dict__['__subject'] - def Dictionary(self, *args): - d = self.__dict__['__subject'].Dictionary().copy() + def Dictionary(self, *args, as_dict: bool = False): + """Return construction variables from an environment. + + Behavior is as described for :class:`SubstitutionEnvironment.Dictionary` + but understanda about the override. + + Raises: + KeyError: if any of *args* is not in the construction environment. + + .. versionchanged: 4.9.0 + Added the *as_dict* keyword arg to always return a dict. + """ + d = {} + d.update(self.__dict__['__subject']) d.update(self.__dict__['overrides']) + d = {k: v for k, v in d.items() if k not in self.__dict__['__deleted']} if not args: return d - dlist = [d[x] for x in args] + if as_dict: + return {key: d[key] for key in args} + dlist = [d[key] for key in args] if len(dlist) == 1: - dlist = dlist[0] + return dlist[0] return dlist def items(self): - """Emulates the items() method of dictionaries.""" + """Emulates the ``items`` method of dictionaries.""" return self.Dictionary().items() def keys(self): - """Emulates the keys() method of dictionaries.""" + """Emulates the ``keys`` method of dictionaries.""" return self.Dictionary().keys() def values(self): - """Emulates the values() method of dictionaries.""" + """Emulates the ``values`` method of dictionaries.""" return self.Dictionary().values() def setdefault(self, key, default=None): - """Emulates the setdefault() method of dictionaries.""" + """Emulates the ``setdefault`` method of dictionaries.""" try: return self.__getitem__(key) except KeyError: @@ -2633,6 +2783,7 @@ def setdefault(self, key, default=None): return default # Overridden private construction environment methods. + def _update(self, other) -> None: self.__dict__['overrides'].update(other) @@ -2655,6 +2806,7 @@ def lvars(self): return lvars # Overridden public construction environment methods. + def Replace(self, **kw) -> None: kw = copy_non_reserved_keywords(kw) self.__dict__['overrides'].update(semi_deepcopy(kw)) diff --git a/SCons/Environment.xml b/SCons/Environment.xml index 5f152f22ee..296a890b8a 100644 --- a/SCons/Environment.xml +++ b/SCons/Environment.xml @@ -1,9 +1,10 @@ A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -165,7 +166,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -176,7 +177,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -187,7 +188,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -198,7 +199,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -209,7 +210,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -220,7 +221,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -231,7 +232,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -278,13 +279,12 @@ for a complete explanation of the arguments and behavior. Note that the &f-env-Action; form of the invocation will expand -construction variables in any argument strings, +&consvars; in any argument strings, including the action argument, at the time it is called -using the construction variables in the -env -construction environment through which +using the &consvars; in the +&consenv; through which &f-env-Action; was called. The &f-Action; global function form delays all variable expansion @@ -348,7 +348,7 @@ env.other_method_name('another arg') -Arranges for the specified +Arrange for the specified action to be performed after the specified @@ -374,6 +374,12 @@ foo = Program('foo.c') AddPostAction(foo, Chmod('$TARGET', "a-x")) + +If a target is an &f-Alias;, +action is associated with the +action of the alias, if specified. + + @@ -383,7 +389,7 @@ AddPostAction(foo, Chmod('$TARGET', "a-x")) -Arranges for the specified +Arrange for the specified action to be performed before the specified @@ -406,31 +412,39 @@ one or more targets in the list. Note that if any of the targets are built in multiple steps, the action will be invoked just -before the "final" action that specifically +before the action step that specifically generates the specified target(s). -For example, when building an executable program -from a specified source -.c -file via an intermediate object file: +It may not always be obvious +if the process is multi-step - for example, +if you use the &Program; builder to +construct an executable program from a +.c source file, +&scons; builds an intermediate object file first; +the pre-action is invoked after this step +and just before the link command to +generate the executable program binary. +Example: foo = Program('foo.c') -AddPreAction(foo, 'pre_action') +AddPreAction(foo, 'echo "Running pre-action"') + +$ scons -Q +gcc -o foo.o -c foo.c +echo "Running pre-action" +Running pre-action +gcc -o foo foo.o + + -The specified -pre_action -would be executed before -&scons; -calls the link command that actually -generates the executable program binary -foo, -not before compiling the -foo.c -file into an object file. +If a target is an &f-Alias;, +action is associated with the +action of the alias, if specified. + @@ -440,7 +454,7 @@ file into an object file. -Creates an alias target that +Create an Alias node that can be used as a reference to zero or more other targets, specified by the optional source parameter. Aliases provide a way to give a shorter or more descriptive @@ -513,7 +527,7 @@ env.Alias('update', ['file1', 'file2'], "update_database $SOURCES") Marks each given target -so that it is always assumed to be out of date, +so that it is always assumed to be out-of-date, and will always be rebuilt if needed. Note, however, that &f-AlwaysBuild; @@ -572,15 +586,15 @@ named key, then key is simply stored with a value of val. Otherwise, val is -combinined with the existing value, +combined with the existing value, possibly converting into an appropriate type which can hold the expanded contents. There are a few special cases to be aware of. Normally, when two strings are combined, the result is a new string containing their concatenation (and you are responsible for supplying any needed separation); -however, the contents of &cv-link-CPPDEFINES; will -will be postprocessed by adding a prefix and/or suffix +however, the contents of &cv-link-CPPDEFINES; +will be post-processed by adding a prefix and/or suffix to each entry when the command line is produced, so &SCons; keeps them separate - appending a string will result in a separate string entry, @@ -696,7 +710,7 @@ scons: `.' is up to date. Changed in version 4.5: -clarifined the use of tuples vs. other types, +clarified the use of tuples vs. other types, handling is now consistent across the four functions. @@ -723,7 +737,7 @@ See &cv-link-CPPDEFINES; for more details. Appending a string val -to a dictonary-typed &consvar; enters +to a dictionary-typed &consvar; enters val as the key in the dictionary, and None as its value. Using a tuple type to supply a key-value pair @@ -861,14 +875,14 @@ for a complete explanation of the arguments and behavior. Note that the env.Builder() form of the invocation will expand -construction variables in any arguments strings, +&consvars; in any arguments strings, including the action argument, at the time it is called -using the construction variables in the +using the &consvars; in the env -construction environment through which +&consenv; through which &f-env-Builder; was called. The &f-Builder; @@ -902,12 +916,12 @@ disables derived file caching. Calling the environment method &f-link-env-CacheDir; limits the effect to targets built -through the specified construction environment. +through the specified &consenv;. Calling the global function &f-link-CacheDir; sets a global default that will be used by all targets built -through construction environments +through &consenvs; that do not set up environment-specific caching by calling &f-env-CacheDir;. @@ -1010,45 +1024,34 @@ to arrange for cache pruning, expiry, access control, etc. if needed. -(targets, files_or_dirs) +(targets, files) -This specifies a list of files or directories which should be removed -whenever the targets are specified with the - -command line option. -The specified targets may be a list -or an individual target. -Multiple calls to -&f-Clean; -are legal, -and create new targets or add files and directories to the -clean list for the specified targets. - - - -Multiple files or directories should be specified -either as separate arguments to the -&f-Clean; -method, or as a list. -&f-Clean; -will also accept the return value of any of the construction environment -Builder methods. -Examples: +Set additional files +for removal when any of +targets are selected +for cleaning +( +command line option). +targets and +files +can each be a single filename or node, +or a list of filenames or nodes. +These can refer to files or directories. +Calling this method repeatedly +has an additive effect. The related &f-link-NoClean; -function overrides calling -&f-Clean; -for the same target, -and any targets passed to both functions will -not -be removed by the - -option. +method has higher priority: +any target specified to +&f-NoClean; +will not be cleaned even if also given as +a files +parameter to &f-Clean;. @@ -1062,10 +1065,13 @@ Clean(['foo', 'bar'], 'something_else_to_clean') +&SCons; does not directly track directories as targets - +they are created if needed and not normally removed +in clean mode. In this example, installing the project creates a subdirectory for the documentation. -This statement causes the subdirectory to be removed -if the project is deinstalled. +The &f-Clean; call ensures that the subdirectory is removed +if the project is uninstalled. Clean(docdir, os.path.join(docdir, projectname)) @@ -1079,11 +1085,12 @@ Clean(docdir, os.path.join(docdir, projectname)) -Returns a separate copy of a construction environment. -If there are any keyword arguments specified, -they are added to the returned copy, +Returns an independent copy of a &consenv;. +If there are any unrecognized keyword arguments specified, +they are added as &consvars; in the copy, overwriting any existing values -for the keywords. +for those keywords. +See the manpage section "Construction Environments" for more details. @@ -1096,8 +1103,9 @@ env3 = env.Clone(CCFLAGS='-g') -Additionally, a list of tools and a toolpath may be specified, as in -the &f-link-Environment; constructor: +A list of tools +and a toolpath may be specified, +as in the &f-link-Environment; constructor: @@ -1110,7 +1118,7 @@ env4 = env.Clone(tools=['msvc', MyTool]) The parse_flags -keyword argument is also recognized to allow merging command-line +keyword argument is also recognized, to allow merging command-line style arguments into the appropriate construction variables (see &f-link-env-MergeFlags;). @@ -1119,6 +1127,17 @@ variables (see &f-link-env-MergeFlags;). # create an environment for compiling programs that use wxWidgets wx_env = env.Clone(parse_flags='!wx-config --cflags --cxxflags') + + +The variables +keyword argument is also recognized, to allow (re)initializing +&consvars; from a Variables object. + + + +Changed in version 4.8.0: +the variables parameter was added. + @@ -1133,7 +1152,7 @@ This is useful for "one-off" builds where a full Builder is not needed. Since the anonymous Builder is never hooked into the standard Builder framework, -an Action must always be specfied. +an Action must always be specified. See the &f-link-Command; function description for the calling syntax and details. @@ -1243,7 +1262,7 @@ env.Command( import os def rename(env, target, source): - os.rename('.tmp', str(target[0])) + os.rename('.tmp', target[0]) env.Command( @@ -1313,13 +1332,11 @@ for a complete explanation of the arguments and behavior. Specifies that all up-to-date decisions for -targets built through this construction environment -will be handled by the specified -function. +targets built through this &consenv; +will be handled by function. function can be the name of a function or one of the following strings -that specify the predefined decision function -that will be applied: +that specify a predefined decider function: @@ -1328,7 +1345,7 @@ that will be applied: "content" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's content has changed since the last time the target was built, as determined by performing a checksum @@ -1350,7 +1367,7 @@ can still be used as a synonym, but is deprecated. "content-timestamp" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's content has changed since the last time the target was built, except that dependencies with a timestamp that matches @@ -1387,7 +1404,7 @@ can still be used as a synonym, but is deprecated. "timestamp-newer" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's timestamp is newer than the target file's timestamp. This is the behavior of the classic Make utility, and @@ -1401,7 +1418,7 @@ can be used a synonym for "timestamp-match" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's timestamp is different than the timestamp recorded the last time the target was built. This provides behavior very similar to the classic Make utility @@ -1426,7 +1443,7 @@ Examples: Decider('timestamp-match') # Use hash content signatures for any targets built -# with the attached construction environment. +# with the attached &consenv;. env.Decider('content') @@ -1447,7 +1464,7 @@ The Node (file) which should cause the target to be rebuilt -if it has "changed" since the last tme +if it has "changed" since the last time target was built. @@ -1522,7 +1539,7 @@ otherwise not be rebuilt). Note that the decision can be made -using whatever criteria are appopriate. +using whatever criteria are appropriate. Ignoring some or all of the function arguments is perfectly normal. @@ -1613,16 +1630,22 @@ but will not include any such extension in the return value. -([vars]) +([var, ...], [as_dict=]) -Returns a dictionary object -containing the &consvars; in the &consenv;. -If there are any arguments specified, -the values of the specified &consvars; -are returned as a string (if one -argument) or as a list of strings. +Return an object containing &consvars; from +env. +If var is omitted, +all the &consvars; with their values +are returned in a dict. +If var is specified, +and as_dict is true, +the specified &consvars; are returned in a dict; +otherwise (the default, for backwards compatibility), +values only are returned, +as a scalar if one var is given, +or as a list if multiples. @@ -1633,6 +1656,24 @@ Example: cvars = env.Dictionary() cc_values = env.Dictionary('CC', 'CCFLAGS', 'CCCOM') + + +The object returned by &f-link-env-Dictionary; should be treated +as a read-only view into the &consvars;. +Some &consvars; require special internal handling, +and modifying them through the &f-env-Dictionary; object can bypass +that handling and cause data inconsistencies. +The primary use of &f-env-Dictionary; is for diagnostic purposes - +it is used widely by test cases specifically because +it bypasses the special handling so that behavior +can be verified. + + + +Changed in 4.9.0: +as_dict added. + + @@ -1678,21 +1719,30 @@ for more information. -([key], [format]) +([var, ...], [format=TYPE]) -Serializes &consvars; to a string. -The method supports the following formats specified by -format: +Serialize &consvars; from env to a string. +If var is omitted, +all the &consvars; are serialized. +If one or more var values are supplied, +only those variables and their values are serialized. + + + +The optional format string +selects the serialization format: + pretty -Returns a pretty printed representation of the environment (if -format -is not specified, this is the default). +Returns a pretty-printed representation +of the &consvars; - the result will look like a +&Python; dict +(this is the default). @@ -1700,38 +1750,47 @@ is not specified, this is the default). json -Returns a JSON-formatted string representation of the environment. +Returns a JSON-formatted representation of the variables. +The variables will be presented as a JSON object literal, +the JSON equivalent of a &Python; dict.. -If key is -None (the default) the entire -dictionary of &consvars; is serialized. -If supplied, it is taken as the name of a &consvar; -whose value is serialized. + +Changed in 4.9.0: +More than one key can be specified. +The returned string always looks like a dict +(or equivalent in other formats); +previously a single key serialized only the value, +not the key with the value. -This SConstruct: +Examples: this &SConstruct; -env=Environment() +env = Environment() print(env.Dump('CCCOM')) +print(env.Dump('CC', 'CCFLAGS', format='json')) -will print: +will print something like: -'$CC -c -o $TARGET $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $SOURCES' +{'CCCOM': '$CC -o $TARGET -c $CFLAGS $CCFLAGS $_CCCOMCOM $SOURCES'} +{ + "CC": "gcc", + "CCFLAGS": [] +} -While this SConstruct: +While this &SConstruct;: @@ -1740,7 +1799,7 @@ print(env.Dump()) -will print: +will print something like: { 'AR': 'ar', @@ -1751,6 +1810,7 @@ will print: 'ASFLAGS': [], ... + @@ -1760,7 +1820,7 @@ will print: -Return a new construction environment +Return a new &consenv; initialized with the specified key=value pairs. @@ -1770,7 +1830,8 @@ The keyword arguments toolpath, tools and variables -are also specially recognized. +are specially recognized and do not lead to +&consvar; creation. See the manpage section "Construction Environments" for more details. @@ -1978,7 +2039,7 @@ FindSourceFiles('src') -As you can see build support files (SConstruct in the above example) +As you can see, build support files (&SConstruct; in the above example) will also be returned by this function. @@ -2062,8 +2123,8 @@ or (most commonly) relative to the directory of the current &f-Glob; matches both files stored on disk and Nodes which &SCons; already knows about, even if any corresponding file is not currently stored on disk. -The evironment method form (&f-env-Glob;) -performs string substition on +The environment method form (&f-env-Glob;) +performs string substitution on pattern and returns whatever matches the resulting expanded pattern. The results are sorted, unlike for the similar &Python; @@ -2154,7 +2215,7 @@ If the optional source argument evaluates true, and the local directory is a variant directory, -then &f-Glob; returnes Nodes from +then &f-Glob; returns Nodes from the corresponding source directory, rather than the local directory. @@ -2188,7 +2249,7 @@ directory.) The optional exclude argument may be set to a pattern or a list of patterns -descibing files or directories +describing files or directories to filter out of the match list. Elements matching a least one specified pattern will be excluded. These patterns use the same syntax as for @@ -2222,14 +2283,14 @@ is non-zero, adds the names of the default builders (Program, Library, etc.) to the global name space -so they can be called without an explicit construction environment. +so they can be called without an explicit &consenv;. (This is the default.) When flag is zero, the names of the default builders are removed from the global name space -so that an explicit construction environment is required +so that an explicit &consenv; is required to call all builders. @@ -2287,7 +2348,7 @@ env.Ignore('bar', 'bar/foobar.obj') The specified string will be preserved as-is -and not have construction variables expanded. +and not have &consvars; expanded. @@ -2393,7 +2454,7 @@ In case of duplication, any &consvar; names that end in PATH keep the left-most value so the -path searcb order is not altered. +path search order is not altered. All other &consvars; keep the right-most value. If unique is false, @@ -2446,7 +2507,7 @@ either as separate arguments to the &f-NoCache; method, or as a list. &f-NoCache; -will also accept the return value of any of the construction environment +will also accept the return value of any of the &consenv; Builder methods. @@ -2470,35 +2531,21 @@ NoCache(env.Program('hello', 'hello.c')) -(target, ...) +(targets, ...) -Specifies a list of files or directories which should -not -be removed whenever the targets (or their dependencies) -are specified with the - -command line option. -The specified targets may be a list -or an individual target. -Multiple calls to -&f-NoClean; -are legal, -and prevent each specified target -from being removed by calls to the - -option. - - - -Multiple files or directories should be specified -either as separate arguments to the -&f-NoClean; -method, or as a list. -&f-NoClean; -will also accept the return value of any of the construction environment -Builder methods. +Specifies files or directories which should not +be removed whenever a specified +target (or its dependencies) +is selected and clean mode is active +( +command line option). +targets +may be one or more file or directory names +or nodes, +and/or lists of names or nodes. +&f-NoClean; can be called multiple times. @@ -2507,11 +2554,9 @@ Calling for a target overrides calling &f-link-Clean; for the same target, -and any targets passed to both functions will +so any targets passed to both functions will not -be removed by the - -option. +be removed in clean mode. @@ -2542,7 +2587,7 @@ is omitted or None, &f-link-env-MergeFlags; is used. By default, duplicate values are not -added to any construction variables; +added to any &consvars;; you can specify unique=False to allow duplicate values to be added. @@ -2565,11 +2610,11 @@ the output produced by command in order to distribute it to appropriate &consvars;. &f-env-MergeFlags; uses a separate function to do that processing - -see &f-link-env-ParseFlags; for the details, including a -a table of options and corresponding construction variables. +see &f-link-env-ParseFlags; for the details, including +a table of options and corresponding &consvars;. To provide alternative processing of the output of command, -you can suppply a custom +you can supply a custom function, which must accept three arguments: the &consenv; to modify, @@ -2649,12 +2694,12 @@ function. Parses one or more strings containing typical command-line flags for GCC-style tool chains and returns a dictionary with the flag values -separated into the appropriate SCons construction variables. +separated into the appropriate SCons &consvars;. Intended as a companion to the &f-link-env-MergeFlags; method, but allows for the values in the returned dictionary to be modified, if necessary, -before merging them into the construction environment. +before merging them into the &consenv;. (Note that &f-env-MergeFlags; will call this method if its argument is not a dictionary, @@ -2683,7 +2728,7 @@ See &f-link-ParseConfig; for more details. Flag values are translated according to the prefix found, -and added to the following construction variables: +and added to the following &consvars;: @@ -2723,8 +2768,7 @@ and added to the following construction variables: Any other strings not associated with options are assumed to be the names of libraries and added to the -&cv-LIBS; -construction variable. +&cv-LIBS; &consvar;. @@ -2755,7 +2799,7 @@ selected by plat (defaults to the detected platform for the current system) that can be used to initialize -a construction environment by passing it as the +a &consenv; by passing it as the platform keyword argument to the &f-link-Environment; function. @@ -2792,7 +2836,7 @@ See the manpage section "Construction Environments" for more details. Prepend values to &consvars; in the current &consenv;, -Works like &f-link-env-Append; (see for details), +works like &f-link-env-Append; (see for details), except that values are added to the front, rather than the end, of any existing value of the &consvar; @@ -2952,7 +2996,7 @@ env = Environment( -Replaces construction variables in the Environment +Replaces &consvars; in the Environment with the specified keyword arguments. @@ -2972,50 +3016,43 @@ env.Replace(CCFLAGS='-g', FOO='foo.xxx') -Specifies that +Sets directory -is a repository to be searched for files. +as a repository to be searched for files contributing to the build. Multiple calls to &f-Repository; -are legal, -and each one adds to the list of -repositories that will be searched. +are allowed, +with repositories searched in the given order. +Repositories specified via command-line option +have higher priority. -To +In &scons;, -a repository is a copy of the source tree, -from the top-level directory on down, -which may contain -both source files and derived files +a repository is partial or complete copy of the source tree, +from the top-level directory down, +containing source files that can be used to build targets in -the local source tree. -The canonical example would be an -official source tree maintained by an integrator. -If the repository contains derived files, -then the derived files should have been built using -&scons;, -so that the repository contains the necessary -signature information to allow -&scons; -to figure out when it is appropriate to -use the repository copy of a derived file, -instead of building one locally. +the current worktree. +Repositories can also contain derived files. +An example might be an official source tree maintained by an integrator. +If a repository contains derived files, +they should be the result of building with &SCons;, +so a signature database (sconsign) is present +in the repository, +allowing better decisions on whether they are +up-to-date or not. Note that if an up-to-date derived file already exists in a repository, -&scons; -will +&scons; will not make a copy in the local directory tree. -In order to guarantee that a local copy -will be made, -use the -&f-link-Local; -method. +If you need a local copy to be made, +use the &f-link-Local; method. @@ -3178,7 +3215,7 @@ in a separate .sconsign file in each directory, not in a single combined database file. -This is a backwards-compatibility meaure to support +This is a backwards-compatibility measure to support what was the default behavior prior to &SCons; 0.97 (i.e. before 2008). Use of this mode is discouraged and may be @@ -3220,7 +3257,7 @@ SConsignFile(dbm_module=dbm.gnu) -Sets construction variables to default values specified with the keyword +Sets &consvars; to default values specified with the keyword arguments if (and only if) the variables are not already set. The following statements are equivalent: @@ -3455,7 +3492,7 @@ source_nodes = env.subst('$EXPAND_TO_NODELIST', conv=lambda x: x) -(name, [toolpath, **kwargs]) +(name, [toolpath, key=value, ...]) @@ -3555,7 +3592,7 @@ Returns a Node object representing the specified &Python; Value Nodes can be used as dependencies of targets. If the string representation of the Value Node changes between &SCons; runs, it is considered -out of date and any targets depending it will be rebuilt. +out-of-date and any targets depending on it will be rebuilt. Since Value Nodes have no filesystem representation, timestamps are not used; the timestamp deciders perform the same content-based up to date check. @@ -3603,7 +3640,7 @@ def create(target, source, env): Writes 'prefix=$SOURCE' into the file name given as $TARGET. """ - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(b'prefix=' + source[0].get_contents() + b'\n') # Fetch the prefix= argument, if any, from the command line. diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py index 9d1229c44d..6330665f59 100644 --- a/SCons/EnvironmentTests.py +++ b/SCons/EnvironmentTests.py @@ -38,7 +38,6 @@ NoSubstitutionProxy, OverrideEnvironment, SubstitutionEnvironment, - is_valid_construction_var, ) from SCons.Util import CLVar from SCons.SConsign import current_sconsign_filename @@ -181,16 +180,19 @@ def test___cmp__(self) -> None: env3 = SubstitutionEnvironment(XXX = 'xxx') env4 = SubstitutionEnvironment(XXX = 'x', YYY = 'x') - assert env1 == env2 - assert env1 != env3 - assert env1 != env4 + with self.subTest(): + self.assertEqual(env1, env2) + with self.subTest(): + self.assertNotEqual(env1, env3) + with self.subTest(): + self.assertNotEqual(env1, env4) def test___delitem__(self) -> None: """Test deleting a variable from a SubstitutionEnvironment.""" env1 = SubstitutionEnvironment(XXX = 'x', YYY = 'y') env2 = SubstitutionEnvironment(XXX = 'x') del env1['YYY'] - assert env1 == env2 + self.assertEqual(env1, env2) def test___getitem__(self) -> None: """Test fetching a variable from a SubstitutionEnvironment.""" @@ -202,7 +204,7 @@ def test___setitem__(self) -> None: env1 = SubstitutionEnvironment(XXX = 'x') env2 = SubstitutionEnvironment(XXX = 'x', YYY = 'y') env1['YYY'] = 'y' - assert env1 == env2 + self.assertEqual(env1, env2) def test_get(self) -> None: """Test the SubstitutionEnvironment get() method.""" @@ -1035,7 +1037,8 @@ def test_BuilderWrapper_attributes(self) -> None: # underlying method it tests (Environment.BuilderWrapper.execute()) # is necessary, but we're leaving the code here for now in case # that's mistaken. - def _DO_NOT_test_Builder_execs(self) -> None: + @unittest.skip("BuilderWrapper.execute method not needed") + def test_Builder_execs(self) -> None: """Test Builder execution through different environments One environment is initialized with a single @@ -1194,18 +1197,6 @@ def test_ENV(self) -> None: def test_ReservedVariables(self) -> None: """Test warning generation when reserved variable names are set""" - - reserved_variables = [ - 'CHANGED_SOURCES', - 'CHANGED_TARGETS', - 'SOURCE', - 'SOURCES', - 'TARGET', - 'TARGETS', - 'UNCHANGED_SOURCES', - 'UNCHANGED_TARGETS', - ] - warning = SCons.Warnings.ReservedVariableWarning SCons.Warnings.enableWarningClass(warning) old = SCons.Warnings.warningAsException(1) @@ -1301,10 +1292,14 @@ def RDirs(pathlist, fs=env.fs): ] assert flags == expect, flags - env.Replace(F77PATH = [ 'foo', '$FOO/bar', blat ], - INCPREFIX = 'foo ', - INCSUFFIX = 'bar', - FOO = 'baz') + # do a Replace using the dict form + newvalues = { + "F77PATH": ['foo', '$FOO/bar', blat], + "INCPREFIX": 'foo ', + "INCSUFFIX": 'bar', + "FOO": 'baz', + } + env.Replace(**newvalues) flags = env.subst_list('$_F77INCFLAGS', 1)[0] expect = [ '$(', normalize_path('foo'), @@ -1569,7 +1564,7 @@ def test__stripixes(self) -> None: def test_gvars(self) -> None: - """Test the Environment gvars() method""" + """Test the Environment gvars() method.""" env = self.TestEnvironment(XXX = 'x', YYY = 'y', ZZZ = 'z') gvars = env.gvars() assert gvars['XXX'] == 'x', gvars['XXX'] @@ -1577,7 +1572,7 @@ def test_gvars(self) -> None: assert gvars['ZZZ'] == 'z', gvars['ZZZ'] def test__update(self) -> None: - """Test the _update() method""" + """Test the _update() method.""" env = self.TestEnvironment(X = 'x', Y = 'y', Z = 'z') assert env['X'] == 'x', env['X'] assert env['Y'] == 'y', env['Y'] @@ -1597,106 +1592,103 @@ def test__update(self) -> None: assert env['SOURCES'] == 'sss', env['SOURCES'] def test_Append(self) -> None: - """Test appending to construction variables in an Environment - """ - + """Test appending to construction variables in an Environment.""" b1 = Environment()['BUILDERS'] b2 = Environment()['BUILDERS'] assert b1 == b2, diff_dict(b1, b2) cases = [ - 'a1', 'A1', 'a1A1', - 'a2', ['A2'], ['a2', 'A2'], - 'a3', UL(['A3']), UL(['a', '3', 'A3']), - 'a4', '', 'a4', - 'a5', [], ['a5'], - 'a6', UL([]), UL(['a', '6']), - 'a7', [''], ['a7', ''], - 'a8', UL(['']), UL(['a', '8', '']), - - ['e1'], 'E1', ['e1', 'E1'], - ['e2'], ['E2'], ['e2', 'E2'], - ['e3'], UL(['E3']), UL(['e3', 'E3']), - ['e4'], '', ['e4'], - ['e5'], [], ['e5'], - ['e6'], UL([]), UL(['e6']), - ['e7'], [''], ['e7', ''], - ['e8'], UL(['']), UL(['e8', '']), - - UL(['i1']), 'I1', UL(['i1', 'I', '1']), - UL(['i2']), ['I2'], UL(['i2', 'I2']), - UL(['i3']), UL(['I3']), UL(['i3', 'I3']), - UL(['i4']), '', UL(['i4']), - UL(['i5']), [], UL(['i5']), - UL(['i6']), UL([]), UL(['i6']), - UL(['i7']), [''], UL(['i7', '']), - UL(['i8']), UL(['']), UL(['i8', '']), - - {'d1':1}, 'D1', {'d1':1, 'D1':None}, - {'d2':1}, ['D2'], {'d2':1, 'D2':None}, - {'d3':1}, UL(['D3']), {'d3':1, 'D3':None}, - {'d4':1}, {'D4':1}, {'d4':1, 'D4':1}, - {'d5':1}, UD({'D5':1}), UD({'d5':1, 'D5':1}), - - UD({'u1':1}), 'U1', UD({'u1':1, 'U1':None}), - UD({'u2':1}), ['U2'], UD({'u2':1, 'U2':None}), - UD({'u3':1}), UL(['U3']), UD({'u3':1, 'U3':None}), - UD({'u4':1}), {'U4':1}, UD({'u4':1, 'U4':1}), - UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1}), - - '', 'M1', 'M1', - '', ['M2'], ['M2'], - '', UL(['M3']), UL(['M3']), - '', '', '', - '', [], [], - '', UL([]), UL([]), - '', [''], [''], - '', UL(['']), UL(['']), - - [], 'N1', ['N1'], - [], ['N2'], ['N2'], - [], UL(['N3']), UL(['N3']), - [], '', [], - [], [], [], - [], UL([]), UL([]), - [], [''], [''], - [], UL(['']), UL(['']), - - UL([]), 'O1', ['O', '1'], - UL([]), ['O2'], ['O2'], - UL([]), UL(['O3']), UL(['O3']), - UL([]), '', UL([]), - UL([]), [], UL([]), - UL([]), UL([]), UL([]), - UL([]), [''], UL(['']), - UL([]), UL(['']), UL(['']), - - [''], 'P1', ['', 'P1'], - [''], ['P2'], ['', 'P2'], - [''], UL(['P3']), UL(['', 'P3']), - [''], '', [''], - [''], [], [''], - [''], UL([]), UL(['']), - [''], [''], ['', ''], - [''], UL(['']), UL(['', '']), - - UL(['']), 'Q1', ['', 'Q', '1'], - UL(['']), ['Q2'], ['', 'Q2'], - UL(['']), UL(['Q3']), UL(['', 'Q3']), - UL(['']), '', UL(['']), - UL(['']), [], UL(['']), - UL(['']), UL([]), UL(['']), - UL(['']), [''], UL(['', '']), - UL(['']), UL(['']), UL(['', '']), + ('a1', 'A1', 'a1A1'), + ('a2', ['A2'], ['a2', 'A2']), + ('a3', UL(['A3']), UL(['a', '3', 'A3'])), + ('a4', '', 'a4'), + ('a5', [], ['a5']), + ('a6', UL([]), UL(['a', '6'])), + ('a7', [''], ['a7', '']), + ('a8', UL(['']), UL(['a', '8', ''])), + + (['e1'], 'E1', ['e1', 'E1']), + (['e2'], ['E2'], ['e2', 'E2']), + (['e3'], UL(['E3']), UL(['e3', 'E3'])), + (['e4'], '', ['e4']), + (['e5'], [], ['e5']), + (['e6'], UL([]), UL(['e6'])), + (['e7'], [''], ['e7', '']), + (['e8'], UL(['']), UL(['e8', ''])), + + (UL(['i1']), 'I1', UL(['i1', 'I', '1'])), + (UL(['i2']), ['I2'], UL(['i2', 'I2'])), + (UL(['i3']), UL(['I3']), UL(['i3', 'I3'])), + (UL(['i4']), '', UL(['i4'])), + (UL(['i5']), [], UL(['i5'])), + (UL(['i6']), UL([]), UL(['i6'])), + (UL(['i7']), [''], UL(['i7', ''])), + (UL(['i8']), UL(['']), UL(['i8', ''])), + + ({'d1':1}, 'D1', {'d1':1, 'D1':None}), + ({'d2':1}, ['D2'], {'d2':1, 'D2':None}), + ({'d3':1}, UL(['D3']), {'d3':1, 'D3':None}), + ({'d4':1}, {'D4':1}, {'d4':1, 'D4':1}), + ({'d5':1}, UD({'D5':1}), UD({'d5':1, 'D5':1})), + + (UD({'u1':1}), 'U1', UD({'u1':1, 'U1':None})), + (UD({'u2':1}), ['U2'], UD({'u2':1, 'U2':None})), + (UD({'u3':1}), UL(['U3']), UD({'u3':1, 'U3':None})), + (UD({'u4':1}), {'U4':1}, UD({'u4':1, 'U4':1})), + ((UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1}))), + + ('', 'M1', 'M1'), + ('', ['M2'], ['M2']), + ('', UL(['M3']), UL(['M3'])), + ('', '', ''), + ('', [], []), + ('', UL([]), UL([])), + ('', [''], ['']), + ('', UL(['']), UL([''])), + + ([], 'N1', ['N1']), + ([], ['N2'], ['N2']), + ([], UL(['N3']), UL(['N3'])), + ([], '', []), + ([], [], []), + ([], UL([]), UL([])), + ([], [''], ['']), + ([], UL(['']), UL([''])), + + (UL([]), 'O1', ['O', '1']), + (UL([]), ['O2'], ['O2']), + (UL([]), UL(['O3']), UL(['O3'])), + (UL([]), '', UL([])), + (UL([]), [], UL([])), + (UL([]), UL([]), UL([])), + (UL([]), [''], UL([''])), + (UL([]), UL(['']), UL([''])), + + ([''], 'P1', ['', 'P1']), + ([''], ['P2'], ['', 'P2']), + ([''], UL(['P3']), UL(['', 'P3'])), + ([''], '', ['']), + ([''], [], ['']), + ([''], UL([]), UL([''])), + ([''], [''], ['', '']), + ([''], UL(['']), UL(['', ''])), + + (UL(['']), 'Q1', ['', 'Q', '1']), + (UL(['']), ['Q2'], ['', 'Q2']), + (UL(['']), UL(['Q3']), UL(['', 'Q3'])), + (UL(['']), '', UL([''])), + (UL(['']), [], UL([''])), + (UL(['']), UL([]), UL([''])), + (UL(['']), [''], UL(['', ''])), + (UL(['']), UL(['']), UL(['', ''])), ] env = Environment() failed = 0 - while cases: - input, append, expect = cases[:3] + for input, append, expect in cases: env['XXX'] = copy.copy(input) try: - env.Append(XXX = append) + env.Append(XXX=append) except Exception as e: if failed == 0: print() print(" %s Append %s exception: %s" % \ @@ -1708,8 +1700,7 @@ def test_Append(self) -> None: if failed == 0: print() print(" %s Append %s => %s did not match %s" % \ (repr(input), repr(append), repr(result), repr(expect))) - failed = failed + 1 - del cases[:3] + failed += 1 assert failed == 0, "%d Append() cases failed" % failed env['UL'] = UL(['foo']) @@ -1760,19 +1751,30 @@ def test_AppendENVPath(self) -> None: ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'}, MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'}, ) + # have to include the pathsep here so that the test will work on UNIX too. env1.AppendENVPath('PATH', r'C:\dir\num\two', sep=';') env1.AppendENVPath('PATH', r'C:\dir\num\three', sep=';') - env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';') assert ( env1['ENV']['PATH'] == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three' ), env1['ENV']['PATH'] + # add nonexisting - at end env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';') + assert ( + env1['MYENV']['MYPATH'] == r'C:\mydir\num\one;C:\mydir\num\two;C:\mydir\num\three' + ), env1['MYENV']['MYPATH'] + + # add existing with delete_existing true - moves to the end env1.AppendENVPath( - 'MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';', delete_existing=1 + 'MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';', delete_existing=True ) - # this should do nothing since delete_existing is 0 + assert ( + env1['MYENV']['MYPATH'] == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one' + ), env1['MYENV']['MYPATH'] + + # this should do nothing since delete_existing is false + env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';') assert ( env1['MYENV']['MYPATH'] == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one' ), env1['MYENV']['MYPATH'] @@ -1784,11 +1786,12 @@ def test_AppendENVPath(self) -> None: env1.AppendENVPath('PATH', env1.fs.Dir('sub2'), sep=';') assert env1['ENV']['PATH'] == p + ';sub1;sub2', env1['ENV']['PATH'] + def test_AppendUnique(self) -> None: """Test appending to unique values to construction variables - This strips values that are already present when lists are - involved.""" + This strips values that are already present when lists are involved. + """ env = self.TestEnvironment(AAA1 = 'a1', AAA2 = 'a2', AAA3 = 'a3', @@ -1801,7 +1804,8 @@ def test_AppendUnique(self) -> None: BBB5 = ['b5'], CCC1 = '', CCC2 = '', - DDD1 = ['a', 'b', 'c']) + DDD1 = ['a', 'b', 'c'], + DDD2 = ['a', 'a', 'b']) env['LL1'] = [env.Literal('a literal'), env.Literal('b literal')] env['LL2'] = [env.Literal('c literal'), env.Literal('b literal')] env.AppendUnique(AAA1 = 'a1', @@ -1817,6 +1821,7 @@ def test_AppendUnique(self) -> None: CCC1 = 'c1', CCC2 = ['c2'], DDD1 = 'b', + DDD2 = 'a', LL1 = env.Literal('a literal'), LL2 = env.Literal('a literal')) @@ -1833,34 +1838,52 @@ def test_AppendUnique(self) -> None: assert env['CCC1'] == 'c1', env['CCC1'] assert env['CCC2'] == ['c2'], env['CCC2'] assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1'] - assert env['LL1'] == [env.Literal('a literal'), env.Literal('b literal')], env['LL1'] - assert env['LL2'] == [env.Literal('c literal'), env.Literal('b literal'), env.Literal('a literal')], [str(x) for x in env['LL2']] + assert env['DDD2'] == ['a', 'a', 'b'], env['DDD2'] # keep existing dup + assert env['LL1'] == [ + env.Literal('a literal'), + env.Literal('b literal'), + ], env['LL1'] + assert env['LL2'] == [ + env.Literal('c literal'), + env.Literal('b literal'), + env.Literal('a literal'), + ], [str(x) for x in env['LL2']] + + env.AppendUnique(DDD1='b', delete_existing=True) + assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # b moves to end + + env.AppendUnique(DDD1=['a', 'b'], delete_existing=True) + assert env['DDD1'] == ['c', 'a', 'b'], env['DDD1'] # a & b move to end + + env.AppendUnique(DDD1=['e', 'f', 'e'], delete_existing=True) + assert env['DDD1'] == ['c', 'a', 'b', 'f', 'e'], env['DDD1'] # add last - env.AppendUnique(DDD1 = 'b', delete_existing=1) - assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # b moves to end - env.AppendUnique(DDD1 = ['a','b'], delete_existing=1) - assert env['DDD1'] == ['c', 'a', 'b'], env['DDD1'] # a & b move to end - env.AppendUnique(DDD1 = ['e','f', 'e'], delete_existing=1) - assert env['DDD1'] == ['c', 'a', 'b', 'f', 'e'], env['DDD1'] # add last + env.AppendUnique(DDD2=['a'], delete_existing=True) + assert env['DDD2'] == ['b', 'a'], env['DDD2'] # all existing instances deleted + + # issue regression: substrings should not be deleted + env.AppendUnique(BBB4='b4.newer', delete_existing=True) + assert env['BBB4'] == ['b4', 'b4.new', 'b4.newer'], env['BBB4'] env['CLVar'] = CLVar([]) - env.AppendUnique(CLVar = 'bar') + env.AppendUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['bar'], result env['CLVar'] = CLVar(['abc']) - env.AppendUnique(CLVar = 'bar') + env.AppendUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['abc', 'bar'], result env['CLVar'] = CLVar(['bar']) - env.AppendUnique(CLVar = 'bar') + env.AppendUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['bar'], result + def test_Clone(self) -> None: """Test construction environment cloning. @@ -2124,6 +2147,13 @@ def test_Dictionary(self) -> None: xxx, zzz = env.Dictionary('XXX', 'ZZZ') assert xxx == 'x' assert zzz == 'z' + # added in 4.9.0: as_dict flag + with self.subTest(): + expect = {'XXX': 'x'} + self.assertEqual(env.Dictionary('XXX', as_dict=True), expect) + with self.subTest(): + expect = {'XXX': 'x', 'YYY': 'y'} + self.assertEqual(env.Dictionary('XXX', 'YYY', as_dict=True), expect) assert 'BUILDERS' in env.Dictionary() assert 'CC' in env.Dictionary() assert 'CCFLAGS' in env.Dictionary() @@ -2306,7 +2336,7 @@ def my_depends(target, dependency, tlist=tlist, dlist=dlist) -> None: exc_caught = None try: - env.ParseDepends(test.workpath('does_not_exist'), must_exist=1) + env.ParseDepends(test.workpath('does_not_exist'), must_exist=True) except IOError: exc_caught = 1 assert exc_caught, "did not catch expected IOError" @@ -2314,7 +2344,7 @@ def my_depends(target, dependency, tlist=tlist, dlist=dlist) -> None: del tlist[:] del dlist[:] - env.ParseDepends('$SINGLE', only_one=1) + env.ParseDepends('$SINGLE', only_one=True) t = list(map(str, tlist)) d = list(map(str, dlist)) assert t == ['f0'], t @@ -2331,7 +2361,7 @@ def my_depends(target, dependency, tlist=tlist, dlist=dlist) -> None: exc_caught = None try: - env.ParseDepends(test.workpath('multiple'), only_one=1) + env.ParseDepends(test.workpath('multiple'), only_one=True) except SCons.Errors.UserError: exc_caught = 1 assert exc_caught, "did not catch expected UserError" @@ -2361,101 +2391,99 @@ def test_Platform(self) -> None: assert env['OBJSUFFIX'] == '.obj', env['OBJSUFFIX'] def test_Prepend(self) -> None: - """Test prepending to construction variables in an Environment - """ + """Test prepending to construction variables in an Environment.""" cases = [ - 'a1', 'A1', 'A1a1', - 'a2', ['A2'], ['A2', 'a2'], - 'a3', UL(['A3']), UL(['A3', 'a', '3']), - 'a4', '', 'a4', - 'a5', [], ['a5'], - 'a6', UL([]), UL(['a', '6']), - 'a7', [''], ['', 'a7'], - 'a8', UL(['']), UL(['', 'a', '8']), - - ['e1'], 'E1', ['E1', 'e1'], - ['e2'], ['E2'], ['E2', 'e2'], - ['e3'], UL(['E3']), UL(['E3', 'e3']), - ['e4'], '', ['e4'], - ['e5'], [], ['e5'], - ['e6'], UL([]), UL(['e6']), - ['e7'], [''], ['', 'e7'], - ['e8'], UL(['']), UL(['', 'e8']), - - UL(['i1']), 'I1', UL(['I', '1', 'i1']), - UL(['i2']), ['I2'], UL(['I2', 'i2']), - UL(['i3']), UL(['I3']), UL(['I3', 'i3']), - UL(['i4']), '', UL(['i4']), - UL(['i5']), [], UL(['i5']), - UL(['i6']), UL([]), UL(['i6']), - UL(['i7']), [''], UL(['', 'i7']), - UL(['i8']), UL(['']), UL(['', 'i8']), - - {'d1':1}, 'D1', {'d1':1, 'D1':None}, - {'d2':1}, ['D2'], {'d2':1, 'D2':None}, - {'d3':1}, UL(['D3']), {'d3':1, 'D3':None}, - {'d4':1}, {'D4':1}, {'d4':1, 'D4':1}, - {'d5':1}, UD({'D5':1}), UD({'d5':1, 'D5':1}), - - UD({'u1':1}), 'U1', UD({'u1':1, 'U1':None}), - UD({'u2':1}), ['U2'], UD({'u2':1, 'U2':None}), - UD({'u3':1}), UL(['U3']), UD({'u3':1, 'U3':None}), - UD({'u4':1}), {'U4':1}, UD({'u4':1, 'U4':1}), - UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1}), - - '', 'M1', 'M1', - '', ['M2'], ['M2'], - '', UL(['M3']), UL(['M3']), - '', '', '', - '', [], [], - '', UL([]), UL([]), - '', [''], [''], - '', UL(['']), UL(['']), - - [], 'N1', ['N1'], - [], ['N2'], ['N2'], - [], UL(['N3']), UL(['N3']), - [], '', [], - [], [], [], - [], UL([]), UL([]), - [], [''], [''], - [], UL(['']), UL(['']), - - UL([]), 'O1', UL(['O', '1']), - UL([]), ['O2'], UL(['O2']), - UL([]), UL(['O3']), UL(['O3']), - UL([]), '', UL([]), - UL([]), [], UL([]), - UL([]), UL([]), UL([]), - UL([]), [''], UL(['']), - UL([]), UL(['']), UL(['']), - - [''], 'P1', ['P1', ''], - [''], ['P2'], ['P2', ''], - [''], UL(['P3']), UL(['P3', '']), - [''], '', [''], - [''], [], [''], - [''], UL([]), UL(['']), - [''], [''], ['', ''], - [''], UL(['']), UL(['', '']), - - UL(['']), 'Q1', UL(['Q', '1', '']), - UL(['']), ['Q2'], UL(['Q2', '']), - UL(['']), UL(['Q3']), UL(['Q3', '']), - UL(['']), '', UL(['']), - UL(['']), [], UL(['']), - UL(['']), UL([]), UL(['']), - UL(['']), [''], UL(['', '']), - UL(['']), UL(['']), UL(['', '']), + ('a1', 'A1', 'A1a1'), + ('a2', ['A2'], ['A2', 'a2']), + ('a3', UL(['A3']), UL(['A3', 'a', '3'])), + ('a4', '', 'a4'), + ('a5', [], ['a5']), + ('a6', UL([]), UL(['a', '6'])), + ('a7', [''], ['', 'a7']), + ('a8', UL(['']), UL(['', 'a', '8'])), + + (['e1'], 'E1', ['E1', 'e1']), + (['e2'], ['E2'], ['E2', 'e2']), + (['e3'], UL(['E3']), UL(['E3', 'e3'])), + (['e4'], '', ['e4']), + (['e5'], [], ['e5']), + (['e6'], UL([]), UL(['e6'])), + (['e7'], [''], ['', 'e7']), + (['e8'], UL(['']), UL(['', 'e8'])), + + (UL(['i1']), 'I1', UL(['I', '1', 'i1'])), + (UL(['i2']), ['I2'], UL(['I2', 'i2'])), + (UL(['i3']), UL(['I3']), UL(['I3', 'i3'])), + (UL(['i4']), '', UL(['i4'])), + (UL(['i5']), [], UL(['i5'])), + (UL(['i6']), UL([]), UL(['i6'])), + (UL(['i7']), [''], UL(['', 'i7'])), + (UL(['i8']), UL(['']), UL(['', 'i8'])), + + ({'d1':1}, 'D1', {'d1':1, 'D1':None}), + ({'d2':1}, ['D2'], {'d2':1, 'D2':None}), + ({'d3':1}, UL(['D3']), {'d3':1, 'D3':None}), + ({'d4':1}, {'D4':1}, {'d4':1, 'D4':1}), + ({'d5':1}, UD({'D5':1}), UD({'d5':1, 'D5':1})), + + (UD({'u1':1}), 'U1', UD({'u1':1, 'U1':None})), + (UD({'u2':1}), ['U2'], UD({'u2':1, 'U2':None})), + (UD({'u3':1}), UL(['U3']), UD({'u3':1, 'U3':None})), + (UD({'u4':1}), {'U4':1}, UD({'u4':1, 'U4':1})), + (UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1})), + + ('', 'M1', 'M1'), + ('', ['M2'], ['M2']), + ('', UL(['M3']), UL(['M3'])), + ('', '', ''), + ('', [], []), + ('', UL([]), UL([])), + ('', [''], ['']), + ('', UL(['']), UL([''])), + + ([], 'N1', ['N1']), + ([], ['N2'], ['N2']), + ([], UL(['N3']), UL(['N3'])), + ([], '', []), + ([], [], []), + ([], UL([]), UL([])), + ([], [''], ['']), + ([], UL(['']), UL([''])), + + (UL([]), 'O1', UL(['O', '1'])), + (UL([]), ['O2'], UL(['O2'])), + (UL([]), UL(['O3']), UL(['O3'])), + (UL([]), '', UL([])), + (UL([]), [], UL([])), + (UL([]), UL([]), UL([])), + (UL([]), [''], UL([''])), + (UL([]), UL(['']), UL([''])), + + ([''], 'P1', ['P1', '']), + ([''], ['P2'], ['P2', '']), + ([''], UL(['P3']), UL(['P3', ''])), + ([''], '', ['']), + ([''], [], ['']), + ([''], UL([]), UL([''])), + ([''], [''], ['', '']), + ([''], UL(['']), UL(['', ''])), + + (UL(['']), 'Q1', UL(['Q', '1', ''])), + (UL(['']), ['Q2'], UL(['Q2', ''])), + (UL(['']), UL(['Q3']), UL(['Q3', ''])), + (UL(['']), '', UL([''])), + (UL(['']), [], UL([''])), + (UL(['']), UL([]), UL([''])), + (UL(['']), [''], UL(['', ''])), + (UL(['']), UL(['']), UL(['', ''])), ] env = Environment() failed = 0 - while cases: - input, prepend, expect = cases[:3] + for input, prepend, expect in cases: env['XXX'] = copy.copy(input) try: - env.Prepend(XXX = prepend) + env.Prepend(XXX=prepend) except Exception as e: if failed == 0: print() print(" %s Prepend %s exception: %s" % \ @@ -2467,8 +2495,7 @@ def test_Prepend(self) -> None: if failed == 0: print() print(" %s Prepend %s => %s did not match %s" % \ (repr(input), repr(prepend), repr(result), repr(expect))) - failed = failed + 1 - del cases[:3] + failed += 1 assert failed == 0, "%d Prepend() cases failed" % failed env['UL'] = UL(['foo']) @@ -2502,6 +2529,7 @@ def test_PrependENVPath(self) -> None: ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'}, MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'}, ) + # have to include the pathsep here so that the test will work on UNIX too. env1.PrependENVPath('PATH', r'C:\dir\num\two', sep=';') env1.PrependENVPath('PATH', r'C:\dir\num\three', sep=';') @@ -2509,11 +2537,18 @@ def test_PrependENVPath(self) -> None: env1['ENV']['PATH'] == r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one' ), env1['ENV']['PATH'] + + # add nonexisting - at front env1.PrependENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';') + assert ( + env1['MYENV']['MYPATH'] == r'C:\mydir\num\three;C:\mydir\num\one;C:\mydir\num\two' + ), env1['MYENV']['MYPATH'] + + # add existing - moves to the front env1.PrependENVPath('MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';') - # this should do nothing since delete_existing is 0 + # this should do nothing since delete_existing is false env1.PrependENVPath( - 'MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';', delete_existing=0 + 'MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';', delete_existing=False ) assert ( env1['MYENV']['MYPATH'] == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two' @@ -2526,6 +2561,7 @@ def test_PrependENVPath(self) -> None: env1.PrependENVPath('PATH', env1.fs.Dir('sub2'), sep=';') assert env1['ENV']['PATH'] == 'sub2;sub1;' + p, env1['ENV']['PATH'] + def test_PrependUnique(self) -> None: """Test prepending unique values to construction variables @@ -2543,7 +2579,8 @@ def test_PrependUnique(self) -> None: BBB5 = ['b5'], CCC1 = '', CCC2 = '', - DDD1 = ['a', 'b', 'c']) + DDD1 = ['a', 'b', 'c'], + DDD2 = ['b', 'a', 'a']) env.PrependUnique(AAA1 = 'a1', AAA2 = ['a2'], AAA3 = ['a3', 'b', 'c', 'b', 'a3'], # ignore dups @@ -2556,7 +2593,8 @@ def test_PrependUnique(self) -> None: BBB5 = ['b5.new'], CCC1 = 'c1', CCC2 = ['c2'], - DDD1 = 'b') + DDD1 = 'b', + DDD2 = 'a') assert env['AAA1'] == 'a1a1', env['AAA1'] assert env['AAA2'] == ['a2'], env['AAA2'] assert env['AAA3'] == ['c', 'b', 'a3'], env['AAA3'] @@ -2570,33 +2608,43 @@ def test_PrependUnique(self) -> None: assert env['CCC1'] == 'c1', env['CCC1'] assert env['CCC2'] == ['c2'], env['CCC2'] assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1'] + assert env['DDD2'] == ['b', 'a', 'a'], env['DDD2'] # keep existing dup + + env.PrependUnique(DDD1='b', delete_existing=True) + assert env['DDD1'] == ['b', 'a', 'c'], env['DDD1'] # b moves to front - env.PrependUnique(DDD1 = 'b', delete_existing=1) - assert env['DDD1'] == ['b', 'a', 'c'], env['DDD1'] # b moves to front - env.PrependUnique(DDD1 = ['a','c'], delete_existing=1) - assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # a & c move to front - env.PrependUnique(DDD1 = ['d','e','d'], delete_existing=1) + env.PrependUnique(DDD1=['a', 'c'], delete_existing=True) + assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # a & c move to front + + env.PrependUnique(DDD1=['d', 'e', 'd'], delete_existing=True) assert env['DDD1'] == ['d', 'e', 'a', 'c', 'b'], env['DDD1'] + env.PrependUnique(DDD2=['a'], delete_existing=True) + assert env['DDD2'] == ['a', 'b'], env['DDD2'] # all existing instances deleted + + # issue regression: substrings should not be deleted + env.PrependUnique(BBB4='b4.newer', delete_existing=True) + assert env['BBB4'] == ['b4.newer', 'b4.new', 'b4'], env['BBB4'] env['CLVar'] = CLVar([]) - env.PrependUnique(CLVar = 'bar') + env.PrependUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['bar'], result env['CLVar'] = CLVar(['abc']) - env.PrependUnique(CLVar = 'bar') + env.PrependUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['bar', 'abc'], result env['CLVar'] = CLVar(['bar']) - env.PrependUnique(CLVar = 'bar') + env.PrependUnique(CLVar='bar') result = env['CLVar'] assert isinstance(result, CLVar), repr(result) assert result == ['bar'], result + def test_Replace(self) -> None: """Test replacing construction variables in an Environment @@ -3177,24 +3225,42 @@ def test_NoClean(self) -> None: def test_Dump(self) -> None: """Test the Dump() method""" - env = self.TestEnvironment(FOO='foo', FOOFLAGS=CLVar('--bar --baz')) - assert env.Dump('FOO') == "'foo'", env.Dump('FOO') - assert len(env.Dump()) > 200, env.Dump() # no args version - assert env.Dump('FOO', 'json') == '"foo"' # JSON key version - expect = """[\n "--bar",\n "--baz"\n]""" - self.assertEqual(env.Dump('FOOFLAGS', 'json'), expect) - import json - env_dict = json.loads(env.Dump(format = 'json')) - assert env_dict['FOO'] == 'foo' # full JSON version + # changed in 4.9.0: single arg now displays as a dict, + # not a bare value; more than one arg is allowed. + with self.subTest(): # one-arg version + self.assertEqual(env.Dump('FOO'), "{'FOO': 'foo'}") + + with self.subTest(): # multi-arg version + expect = "{'FOO': 'foo', 'FOOFLAGS': ['--bar', '--baz']}" + self.assertEqual(env.Dump('FOO', 'FOOFLAGS'), expect) + + with self.subTest(): # no-arg version + self.assertGreater(len(env.Dump()), 200) + + with self.subTest(): # one-arg JSON version, simple value + expect = '{\n "FOO": "foo"\n}' + self.assertEqual(env.Dump('FOO', format='json'), expect) + + with self.subTest(): # one-arg JSON version, list value + expect = '{\n "FOOFLAGS": [\n "--bar",\n "--baz"\n ]\n}' + self.assertEqual(env.Dump('FOOFLAGS', format='json'), expect) + + with self.subTest(): # multi--arg JSON version, list value + expect = '{\n "FOO": "foo",\n "FOOFLAGS": [\n "--bar",\n "--baz"\n ]\n}' + self.assertEqual(env.Dump('FOO', 'FOOFLAGS', format='json'), expect) + + with self.subTest(): # full JSON version + import json + env_dict = json.loads(env.Dump(format='json')) + self.assertEqual(env_dict['FOO'], 'foo') + + with self.subTest(): # unsupported format type + with self.assertRaises(ValueError) as cm: + env.Dump(format='markdown') + self.assertEqual(str(cm.exception), "Unsupported serialization format: markdown.") - try: - env.Dump(format = 'markdown') - except ValueError as e: - assert str(e) == "Unsupported serialization format: markdown." - else: - self.fail("Did not catch expected ValueError.") def test_Environment(self) -> None: """Test the Environment() method""" @@ -3748,13 +3814,17 @@ class OverrideEnvironmentTestCase(unittest.TestCase,TestEnvironmentFixture): def setUp(self) -> None: env = Environment() env._dict = {'XXX' : 'x', 'YYY' : 'y'} + def verify_value(env, key, value, *args, **kwargs) -> None: """Verifies that key is value on the env this is called with.""" - assert env[key] == value + self.assertEqual(env[key], value) + env.AddMethod(verify_value) + # env2 does not overrride 'YYY' to test passthrough env2 = OverrideEnvironment(env, {'XXX' : 'x2'}) + # env3 overrides both, plus sets a new var 'ZZZ' env3 = OverrideEnvironment(env2, {'XXX' : 'x3', 'YYY' : 'y3', 'ZZZ' : 'z3'}) - self.envs = [ env, env2, env3 ] + self.envs = [env, env2, env3] def checkpath(self, node, expect): return str(node) == os.path.normpath(expect) @@ -3762,31 +3832,79 @@ def checkpath(self, node, expect): def test___init__(self) -> None: """Test OverrideEnvironment initialization""" env, env2, env3 = self.envs - assert env['XXX'] == 'x', env['XXX'] - assert env2['XXX'] == 'x2', env2['XXX'] - assert env3['XXX'] == 'x3', env3['XXX'] - assert env['YYY'] == 'y', env['YYY'] - assert env2['YYY'] == 'y', env2['YYY'] - assert env3['YYY'] == 'y3', env3['YYY'] + + with self.subTest(): + self.assertEqual(env['XXX'], 'x') + with self.subTest(): + self.assertEqual(env2['XXX'], 'x2') + with self.subTest(): + self.assertEqual(env3['XXX'], 'x3') + with self.subTest(): + self.assertEqual(env['YYY'], 'y') + with self.subTest(): + self.assertEqual(env2['YYY'], 'y') + with self.subTest(): + self.assertEqual(env3['YYY'], 'y3') + with self.subTest(): + self.assertNotIn('ZZZ', env) + with self.subTest(): + self.assertNotIn('ZZZ', env2) + with self.subTest(): + self.assertEqual(env3['ZZZ'], 'z3') + + def test___setitem__(self) -> None: + """Test setting a variable does not leak through.""" + env, env2, env3 = self.envs + env3['QQQ'] = 'q' + with self.subTest(): + self.assertEqual(env3['QQQ'], 'q') + with self.subTest(): + self.assertNotIn('QQQ', env2) + with self.subTest(): + self.assertNotIn('QQQ', env) def test___delitem__(self) -> None: """Test deleting variables from an OverrideEnvironment""" env, env2, env3 = self.envs + # changed in 4.9.0: delete does not cascade to underlying envs + # XXX is in all three, del from env3 should affect only it del env3['XXX'] - assert 'XXX' not in env, "env has XXX?" - assert 'XXX' not in env2, "env2 has XXX?" - assert 'XXX' not in env3, "env3 has XXX?" + with self.subTest(): + self.assertIn('XXX', env) + with self.subTest(): + self.assertIn('XXX', env2) + with self.subTest(): + self.assertNotIn('XXX', env3) + # YYY unique in env and env3, shadowed in env2: env2 should still work. del env3['YYY'] - assert 'YYY' not in env, "env has YYY?" - assert 'YYY' not in env2, "env2 has YYY?" - assert 'YYY' not in env3, "env3 has YYY?" + with self.subTest(): + self.assertIn('YYY', env) + with self.subTest(): + self.assertEqual(env2['YYY'], 'y') + with self.subTest(): + self.assertIn('YYY', env2) + with self.subTest(): + self.assertNotIn('YYY', env3) + # ZZZ is only in env3, none should have it del env3['ZZZ'] - assert 'ZZZ' not in env, "env has ZZZ?" - assert 'ZZZ' not in env2, "env2 has ZZZ?" - assert 'ZZZ' not in env3, "env3 has ZZZ?" + with self.subTest(): + self.assertNotIn('ZZZ', env) + with self.subTest(): + self.assertNotIn('ZZZ', env2) + with self.subTest(): + self.assertNotIn('ZZZ', env3) + + # make sure we can write back after deletion + env3['XXX'] = 'x4' + with self.subTest(): + self.assertEqual(env3['XXX'], 'x4') + with self.subTest(): + self.assertEqual(env2['XXX'], 'x2') + with self.subTest(): + self.assertEqual(env['XXX'], 'x') def test_get(self) -> None: """Test the OverrideEnvironment get() method""" @@ -3820,21 +3938,35 @@ def test_Dictionary(self) -> None: # nothing overrriden items = env.Dictionary() assert items == {'XXX' : 'x', 'YYY' : 'y'}, items + # env2 overrides XXX, YYY unchanged items = env2.Dictionary() assert items == {'XXX' : 'x2', 'YYY' : 'y'}, items + # env3 overrides XXX, YYY, adds ZZZ items = env3.Dictionary() assert items == {'XXX' : 'x3', 'YYY' : 'y3', 'ZZZ' : 'z3'}, items + # test one-arg and multi-arg Dictionary assert env3.Dictionary('XXX') == 'x3', env3.Dictionary('XXX') xxx, yyy = env2.Dictionary('XXX', 'YYY') assert xxx == 'x2', xxx assert yyy == 'y', yyy + # added in NEXT_VERSION: as_dict flag + with self.subTest(): + expect = {'XXX': 'x3'} + self.assertEqual(env3.Dictionary('XXX', as_dict=True), expect) + with self.subTest(): + expect = {'XXX': 'x2', 'YYY': 'y'} + self.assertEqual(env2.Dictionary('XXX', 'YYY', as_dict=True), expect) + + # test deletion in top override del env3['XXX'] - assert 'XXX' not in env3.Dictionary() - assert 'XXX' not in env2.Dictionary() - assert 'XXX' not in env.Dictionary() + self.assertRaises(KeyError, env3.Dictionary, 'XXX') + # changed in 4.9.0: *not* deleted from underlying envs + assert 'XXX' in env2.Dictionary() + assert 'XXX' in env.Dictionary() + def test_items(self) -> None: """Test the OverrideEnvironment items() method""" @@ -3899,15 +4031,9 @@ def test_lvars(self) -> None: def test_Replace(self) -> None: """Test the OverrideEnvironment Replace() method""" env, env2, env3 = self.envs - assert env['XXX'] == 'x', env['XXX'] - assert env2['XXX'] == 'x2', env2['XXX'] - assert env3['XXX'] == 'x3', env3['XXX'] - assert env['YYY'] == 'y', env['YYY'] - assert env2['YYY'] == 'y', env2['YYY'] - assert env3['YYY'] == 'y3', env3['YYY'] - - env.Replace(YYY = 'y4') + # initial state already proven by test___init__ + env.Replace(YYY='y4') assert env['XXX'] == 'x', env['XXX'] assert env2['XXX'] == 'x2', env2['XXX'] assert env3['XXX'] == 'x3', env3['XXX'] @@ -3915,8 +4041,68 @@ def test_Replace(self) -> None: assert env2['YYY'] == 'y4', env2['YYY'] assert env3['YYY'] == 'y3', env3['YYY'] + + def test_Override_Leakage(self) -> None: + """Test OverrideEnvironment modifying a variable for leakage.""" + env, env2, env3 = self.envs + # initial state already proven by test___init__ + + # string appending should be additive - only in the override + env.Append(WWW='w') + self.assertEqual(env2['WWW'], 'w') + with self.subTest(): + env2.Append(WWW='w2') + self.assertEqual(env2['WWW'], 'ww2') + # did it leak? + self.assertEqual(env['WWW'], 'w', "leak error") + + # append to a string already in the override + self.assertEqual(env['XXX'], 'x') + self.assertEqual(env2['XXX'], 'x2') + with self.subTest(): + env2.Append(XXX='x4') + self.assertEqual(env2['XXX'], 'x2x4') + # did it leak? + self.assertEqual(env['XXX'], 'x', "leak error") + + # add a new mutable key to base env, but copy it before modifying + # This isn't a terribly interesting test, just shows that if you + # "behave carefully", things don't leak. + env.Append(QQQ=deque(['q1', 'q2', 'q3'])) + self.assertEqual(env2['QQQ'], deque(['q1', 'q2', 'q3'])) + with self.subTest(): + env2['QQQ'] = env2['QQQ'].copy() + env2.Append(QQQ='q4') + self.assertEqual(env2['QQQ'], deque(['q1', 'q2', 'q3', 'q4'])) + # did it leak? + self.assertNotIn('q4', env['QQQ'], "leak error") + + + @unittest.expectedFailure + def test_Override_Mutable_Leakage(self) -> None: + """Test OverrideEnvironment modifying a mutable variable for leakage. + + This is factored out from test_Override_Leakage as currently + there is no way to prevent the leakage when updating a mutable + element such as a list - thus it's marked as an xfail. This + gives us something to come back to if we ever invent some sort + of isolation via object copying, etc. + """ + env, env2, env3 = self.envs + # initial state already proven by test___init__ + + # add a new key to base env, with a mutable value + env.Append(QQQ=deque(['q1', 'q2', 'q3'])) + self.assertEqual(env2['QQQ'], deque(['q1', 'q2', 'q3'])) + with self.subTest(): + env2.Append(QQQ='q4') + self.assertEqual(env2['QQQ'], deque(['q1', 'q2', 'q3', 'q4'])) + # did it leak? + self.assertNotIn('q4', env['QQQ'], "leak error") + + # Tests a number of Base methods through an OverrideEnvironment to - # make sure they handle overridden constructionv variables properly. + # make sure they handle overridden construction variables properly. # # The following Base methods also call self.subst(), and so could # theoretically be subject to problems with evaluating overridden @@ -4142,40 +4328,6 @@ def test_subst_target_source(self) -> None: x = proxy.subst_target_source(*args, **kw) assert x == ' ttt sss ', x -class EnvironmentVariableTestCase(unittest.TestCase): - - def test_is_valid_construction_var(self) -> None: - """Testing is_valid_construction_var()""" - r = is_valid_construction_var("_a") - assert r is not None, r - r = is_valid_construction_var("z_") - assert r is not None, r - r = is_valid_construction_var("X_") - assert r is not None, r - r = is_valid_construction_var("2a") - assert r is None, r - r = is_valid_construction_var("a2_") - assert r is not None, r - r = is_valid_construction_var("/") - assert r is None, r - r = is_valid_construction_var("_/") - assert r is None, r - r = is_valid_construction_var("a/") - assert r is None, r - r = is_valid_construction_var(".b") - assert r is None, r - r = is_valid_construction_var("_.b") - assert r is None, r - r = is_valid_construction_var("b1._") - assert r is None, r - r = is_valid_construction_var("-b") - assert r is None, r - r = is_valid_construction_var("_-b") - assert r is None, r - r = is_valid_construction_var("b1-_") - assert r is None, r - - if __name__ == "__main__": unittest.main() diff --git a/SCons/Errors.py b/SCons/Errors.py index a2efc97088..af77971032 100644 --- a/SCons/Errors.py +++ b/SCons/Errors.py @@ -26,11 +26,15 @@ Used to handle internal and user errors in SCons. """ +from __future__ import annotations + import shutil -from typing import Optional +from typing import TYPE_CHECKING from SCons.Util.sctypes import to_String, is_String -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor # Note that not all Errors are defined here, some are at the point of use @@ -75,7 +79,7 @@ class BuildError(Exception): def __init__(self, node=None, errstr: str="Unknown error", status: int=2, exitstatus: int=2, - filename=None, executor: Optional[ExecutorType] = None, action=None, command=None, + filename=None, executor: Executor | None = None, action=None, command=None, exc_info=(None, None, None)) -> None: # py3: errstr should be string and not bytes. diff --git a/SCons/Executor.py b/SCons/Executor.py index 1b054b4ba8..53eb5cbb2f 100644 --- a/SCons/Executor.py +++ b/SCons/Executor.py @@ -23,8 +23,9 @@ """Execute actions with specific lists of target and source Nodes.""" +from __future__ import annotations + import collections -from typing import Dict import SCons.Errors import SCons.Memoize @@ -32,7 +33,6 @@ from SCons.compat import NoSlotsPyPy import SCons.Debug from SCons.Debug import logInstanceCreation -from SCons.Util.sctyping import ExecutorType class Batch: """Remembers exact association between targets @@ -550,12 +550,12 @@ def get_implicit_deps(self): -_batch_executors: Dict[str, ExecutorType] = {} +_batch_executors: dict[str, Executor] = {} -def GetBatchExecutor(key: str) -> ExecutorType: +def GetBatchExecutor(key: str) -> Executor: return _batch_executors[key] -def AddBatchExecutor(key: str, executor: ExecutorType) -> None: +def AddBatchExecutor(key: str, executor: Executor) -> None: assert key not in _batch_executors _batch_executors[key] = executor diff --git a/SCons/Node/Alias.py b/SCons/Node/Alias.py index 4de222999b..6041581781 100644 --- a/SCons/Node/Alias.py +++ b/SCons/Node/Alias.py @@ -104,9 +104,15 @@ def sconsign(self) -> None: # # - def build(self) -> None: + def build(self, **kw) -> None: """A "builder" for aliases.""" - pass + if len(self.executor.post_actions) + len(self.executor.pre_actions) > 0: + # Only actually call Node's build() if there are any + # pre or post actions. + # Alias nodes will get 1 action and Alias.build() + # This fixes GH Issue #2281 + return self.really_build(**kw) + def convert(self) -> None: try: del self.builder diff --git a/SCons/Node/FS.py b/SCons/Node/FS.py index 3cd7720c0f..275a7be8e9 100644 --- a/SCons/Node/FS.py +++ b/SCons/Node/FS.py @@ -30,6 +30,8 @@ that can be used by scripts or modules looking for the canonical default. """ +from __future__ import annotations + import codecs import fnmatch import importlib.util @@ -40,7 +42,6 @@ import sys import time from itertools import chain -from typing import Optional import SCons.Action import SCons.Debug @@ -144,17 +145,29 @@ def initialize_do_splitdrive() -> None: do_splitdrive = bool(os.path.splitdrive('X:/foo')[0]) if do_splitdrive: + # Note there's a little dilemma here - we should be able to use the + # Python os.path.splitdrive, which is well debugged over the years. + # For normal usage it should work, but we also want to as much as + # possible run the full testsuite on whichever platform, even if it's + # "wrong" for some feature. POSIX splitdrive doesn't do what we want + # when running the drive and UNC path tests, and the NT one isn't + # designed for use on non-win32. So for now stuck on our private one. + # _my_splitdrive = os.path.splitdrive + def _my_splitdrive(p): if p[1:2] == ':': return p[:2], p[2:] if p[0:2] == '//': - # Note that we leave a leading slash in the path - # because UNC paths are always absolute. + # We leave a leading slash in the path because UNC paths + # are always absolute. + # + # TODO: returning "//" for the drive part is actually + # completely wrong. UNC paths must have minimum three + # slashes (if using '//server/share/path' style), or five + # ('//?/UNC/server/share/path)' and we should return the + # part up to but not including the next slash. return '//', p[1:] return '', p - # TODO: the os routine should work and be better debugged than ours, - # but unit test test_unc_path fails on POSIX platforms. Resolve someday. - # _my_splitdrive = os.path.splitdrive # Keep some commonly used values in global variables to skip to # module look-up costs. @@ -761,6 +774,8 @@ def getmtime(self): st = self.stat() if st: + # TODO: switch to st.st_mtime, however this changes granularity + # (ST_MTIME is an int for backwards compat, st_mtime is float) return st[stat.ST_MTIME] else: return None @@ -1008,7 +1023,7 @@ def __init__(self, name, directory, fs) -> None: def diskcheck_match(self) -> None: pass - def disambiguate(self, must_exist=None): + def disambiguate(self, must_exist=False): """ """ if self.isfile(): @@ -1066,7 +1081,7 @@ def get_text_contents(self) -> str: system, we check to see into what sort of subclass we should morph this Entry.""" try: - self = self.disambiguate(must_exist=1) + self = self.disambiguate(must_exist=True) except SCons.Errors.UserError: # There was nothing on disk with which to disambiguate # this entry. Leave it as an Entry, but return a null @@ -1492,7 +1507,7 @@ def Repository(self, *dirs) -> None: d = self.Dir(d) self.Top.addRepository(d) - def PyPackageDir(self, modulename) -> Optional[Dir]: + def PyPackageDir(self, modulename) -> Dir | None: r"""Locate the directory of Python module *modulename*. For example 'SCons' might resolve to @@ -3190,7 +3205,7 @@ def exists(self): # SIGNATURE SUBSYSTEM # - def get_max_drift_csig(self) -> Optional[str]: + def get_max_drift_csig(self) -> str | None: """ Returns the content signature currently stored for this node if it's been unmodified longer than the max_drift value, or the diff --git a/SCons/Node/FSTests.py b/SCons/Node/FSTests.py index 2036f9266b..30fa1c1f6f 100644 --- a/SCons/Node/FSTests.py +++ b/SCons/Node/FSTests.py @@ -21,6 +21,8 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations + import SCons.compat import os import os.path @@ -29,22 +31,23 @@ import unittest import shutil import stat -from typing import Optional +from typing import TYPE_CHECKING -from TestCmd import TestCmd, IS_WINDOWS +from TestCmd import TestCmd, IS_WINDOWS, IS_ROOT import SCons.Errors import SCons.Node.FS import SCons.Util import SCons.Warnings import SCons.Environment -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor built_it = None scanner_count = 0 - class Scanner: def __init__(self, node=None) -> None: global scanner_count @@ -321,7 +324,7 @@ class MkdirAction(Action): def __init__(self, dir_made) -> None: self.dir_made = dir_made - def __call__(self, target, source, env, executor: Optional[ExecutorType] = None) -> None: + def __call__(self, target, source, env, executor: Executor | None = None) -> None: if executor: target = executor.get_all_targets() source = executor.get_all_sources() @@ -513,7 +516,7 @@ def copy(self, src, dest) -> None: # Disable symlink and link for now in win32. # We don't have a consistant plan to make these work as yet # They are only supported with PY3 - if sys.platform == 'win32': + if IS_WINDOWS: real_symlink = None real_link = None @@ -706,7 +709,7 @@ def test_isfile(self) -> None: nonexistent = fs.Entry('nonexistent') assert not nonexistent.isfile() - @unittest.skipUnless(sys.platform != 'win32' and hasattr(os, 'symlink'), + @unittest.skipIf(IS_WINDOWS or not hasattr(os, 'symlink'), "symlink is not used on Windows") def test_islink(self) -> None: """Test the Base.islink() method""" @@ -771,6 +774,7 @@ def test_update(self) -> None: ni.update(fff) + # TODO: flip this to st.st_mtime when Node/FS.py does mtime = st[stat.ST_MTIME] assert ni.timestamp == mtime, (ni.timestamp, mtime) size = st.st_size @@ -783,6 +787,7 @@ def test_update(self) -> None: st = os.stat('fff') + # TODO: flip this to st.st_mtime when Node/FS.py does mtime = st[stat.ST_MTIME] assert ni.timestamp != mtime, (ni.timestamp, mtime) size = st.st_size @@ -958,6 +963,8 @@ def test_runTest(self): This test case handles all of the file system node tests in one environment, so we don't have to set up a complicated directory structure for each test individually. + This isn't ideal: normally you want to separate tests a bit + more to make it easier to debug and not fail too fast. """ test = self.test @@ -1046,7 +1053,7 @@ def test_runTest(self): seps = [os.sep] if os.sep != '/': - seps = seps + ['/'] + seps.append('/') drive, path = os.path.splitdrive(os.getcwd()) @@ -1082,21 +1089,16 @@ def strip_slash(p, drive=drive): else: dir_up_path = dir.up().get_internal_path() - assert dir.name == name, \ - "dir.name %s != expected name %s" % \ - (dir.name, name) - assert dir.get_internal_path() == path, \ - "dir.path %s != expected path %s" % \ - (dir.get_internal_path(), path) - assert str(dir) == path, \ - "str(dir) %s != expected path %s" % \ - (str(dir), path) - assert dir.get_abspath() == abspath, \ - "dir.abspath %s != expected absolute path %s" % \ - (dir.get_abspath(), abspath) - assert dir_up_path == up_path, \ - "dir.up().path %s != expected parent path %s" % \ - (dir_up_path, up_path) + with self.subTest(): + self.assertEqual(dir.name, name) + with self.subTest(): + self.assertEqual(dir.get_internal_path(), path) + with self.subTest(): + self.assertEqual(str(dir), path) + with self.subTest(): + self.assertEqual(dir.get_abspath(), abspath) + with self.subTest(): + self.assertEqual(dir_up_path, up_path) for sep in seps: @@ -1449,7 +1451,7 @@ def nonexistent(method, s): except SyntaxError: assert c == "" - if sys.platform != 'win32' and hasattr(os, 'symlink'): + if not IS_WINDOWS and hasattr(os, 'symlink'): os.symlink('nonexistent', test.workpath('dangling_symlink')) e = fs.Entry('dangling_symlink') c = e.get_contents() @@ -1541,7 +1543,7 @@ def nonexistent(method, s): assert r, r assert not os.path.exists(test.workpath('exists')), "exists was not removed" - if sys.platform != 'win32' and hasattr(os, 'symlink'): + if not IS_WINDOWS and hasattr(os, 'symlink'): symlink = test.workpath('symlink') os.symlink(test.workpath('does_not_exist'), symlink) assert os.path.islink(symlink) @@ -1550,67 +1552,66 @@ def nonexistent(method, s): assert r, r assert not os.path.islink(symlink), "symlink was not removed" - test.write('can_not_remove', "can_not_remove\n") - test.writable(test.workpath('.'), 0) - fp = open(test.workpath('can_not_remove')) - - f = fs.File('can_not_remove') - exc_caught = 0 - try: - r = f.remove() - except OSError: - exc_caught = 1 - - fp.close() - - assert exc_caught, "Should have caught an OSError, r = " + str(r) - f = fs.Entry('foo/bar/baz') assert f.for_signature() == 'baz', f.for_signature() assert f.get_string(0) == os.path.normpath('foo/bar/baz'), \ f.get_string(0) assert f.get_string(1) == 'baz', f.get_string(1) - def test_drive_letters(self) -> None: - """Test drive-letter look-ups""" + @unittest.skipIf(IS_ROOT, "Skip file removal in protected dir if running as root.") + def test_remove_fail(self) -> None: + """Test failure when removing a file where permissions don't allow. + + Split from :math:`test_runTest` to be able to skip on root. + We want to be able to skip only this one testcase and run the rest. + """ test = self.test + fs = SCons.Node.FS.FS() + test.write('can_not_remove', "can_not_remove\n") + test.writable(test.workpath('.'), False) + with open(test.workpath('can_not_remove')): + f = fs.File('can_not_remove') + with self.assertRaises(OSError, msg="Should have caught an OSError"): + r = f.remove() + + def test_drive_letters(self) -> None: + """Test drive-letter look-ups""" + test = self.test test.subdir('sub', ['sub', 'dir']) + seps = [os.sep] + if os.sep != '/': + seps.append('/') + def drive_workpath(dirs, test=test): x = test.workpath(*dirs) drive, path = os.path.splitdrive(x) return 'X:' + path wp = drive_workpath(['']) - - if wp[-1] in (os.sep, '/'): + if wp[-1] in seps: tmp = os.path.split(wp[:-1])[0] else: tmp = os.path.split(wp)[0] - parent_tmp = os.path.split(tmp)[0] if parent_tmp == 'X:': parent_tmp = 'X:' + os.sep - tmp_foo = os.path.join(tmp, 'foo') - foo = drive_workpath(['foo']) foo_bar = drive_workpath(['foo', 'bar']) - sub = drive_workpath(['sub', '']) - sub_dir = drive_workpath(['sub', 'dir', '']) - sub_dir_foo = drive_workpath(['sub', 'dir', 'foo', '']) - sub_dir_foo_bar = drive_workpath(['sub', 'dir', 'foo', 'bar', '']) - sub_foo = drive_workpath(['sub', 'foo', '']) + # The following are not used, but kept for reference. + # sub = drive_workpath(['sub', '']) + # sub_dir = drive_workpath(['sub', 'dir', '']) + # sub_dir_foo = drive_workpath(['sub', 'dir', 'foo', '']) + # sub_dir_foo_bar = drive_workpath(['sub', 'dir', 'foo', 'bar', '']) + # sub_foo = drive_workpath(['sub', 'foo', '']) fs = SCons.Node.FS.FS() - seps = [os.sep] - if os.sep != '/': - seps = seps + ['/'] - def _do_Dir_test(lpath, path_, up_path_, sep, fileSys=fs) -> None: + """Test the Dir constructor with a given path and expected values.""" dir = fileSys.Dir(lpath.replace('/', sep)) if os.sep != '/': @@ -1626,18 +1627,14 @@ def strip_slash(p): up_path = strip_slash(up_path_) name = path.split(os.sep)[-1] - assert dir.name == name, \ - "dir.name %s != expected name %s" % \ - (dir.name, name) - assert dir.get_internal_path() == path, \ - "dir.path %s != expected path %s" % \ - (dir.get_internal_path(), path) - assert str(dir) == path, \ - "str(dir) %s != expected path %s" % \ - (str(dir), path) - assert dir.up().get_internal_path() == up_path, \ - "dir.up().path %s != expected parent path %s" % \ - (dir.up().get_internal_path(), up_path) + with self.subTest(): + self.assertEqual(dir.name, name) + with self.subTest(): + self.assertEqual(dir.get_internal_path(), path) + with self.subTest(): + self.assertEqual(str(dir), path) + with self.subTest(): + self.assertEqual(dir.up().get_internal_path(), up_path) save_os_path = os.path save_os_sep = os.sep @@ -1648,6 +1645,7 @@ def strip_slash(p): SCons.Node.FS.initialize_do_splitdrive() for sep in seps: + def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test): return func(lpath, path_, up_path_, sep) @@ -1674,59 +1672,73 @@ def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test): def test_unc_path(self) -> None: """Test UNC path look-ups""" - test = self.test - test.subdir('sub', ['sub', 'dir']) - def strip_slash(p): - if p[-1] == os.sep and len(p) > 3: - p = p[:-1] - return p + seps = [os.sep] + if os.sep != '/': + seps.append('/') def unc_workpath(dirs, test=test): + """Return a UNC-style path for the given directory. + + *dirs* is a list of directory names, which will be joined + together to derive the base of the path - any "drive letter" + bits split off. Makes no effort to create an actual *valid* + UNC path - does not add a server or share name, so it will not + actually work in real life, just for string testing. + + So that tests will work on non-Windows systems, we use the ntpath + module to perform the splitdrive, as that's otherwise a no-op. + """ import ntpath + + # This duplicates the definition in _do_Dir_test below. + # It depends on the monkeypatching of os.sep so we just have two. + def strip_slash(p): + if p[-1] in seps and len(p) > 3: + p = p[:-1] + return p + x = test.workpath(*dirs) drive, path = ntpath.splitdrive(x) - try: - unc, path = ntpath.splitunc(path) - except AttributeError: - # could be python 3.7 or newer, make sure splitdrive can do UNC - assert ntpath.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive' path = strip_slash(path) return '//' + path[1:] wp = unc_workpath(['']) - - if wp[-1] in (os.sep, '/'): - tmp = os.path.split(wp[:-1])[0] - else: - tmp = os.path.split(wp)[0] - - parent_tmp = os.path.split(tmp)[0] - + tmp = os.path.split(wp)[0] + # parent_tmp = os.path.split(tmp)[0] tmp_foo = os.path.join(tmp, 'foo') - foo = unc_workpath(['foo']) foo_bar = unc_workpath(['foo', 'bar']) - sub = unc_workpath(['sub', '']) - sub_dir = unc_workpath(['sub', 'dir', '']) - sub_dir_foo = unc_workpath(['sub', 'dir', 'foo', '']) - sub_dir_foo_bar = unc_workpath(['sub', 'dir', 'foo', 'bar', '']) - sub_foo = unc_workpath(['sub', 'foo', '']) + # The following are not used, but kept for reference. + # sub = unc_workpath(['sub', '']) + # sub_dir = unc_workpath(['sub', 'dir', '']) + # sub_dir_foo = unc_workpath(['sub', 'dir', 'foo', '']) + # sub_dir_foo_bar = unc_workpath(['sub', 'dir', 'foo', 'bar', '']) + # sub_foo = unc_workpath(['sub', 'foo', '']) fs = SCons.Node.FS.FS() - seps = [os.sep] - if os.sep != '/': - seps = seps + ['/'] - - def _do_Dir_test(lpath, path, up_path, sep, fileSys=fs) -> None: + def _do_Dir_test(lpath, path_, up_path_, sep, fileSys=fs) -> None: + """Test the Dir constructor with a given path and expected values.""" + # before = time.perf_counter() dir = fileSys.Dir(lpath.replace('/', sep)) + # after = time.perf_counter() + # print(f"TIME: dir creation for {lpath!r} (as {dir.path!r}) took {after - before:.6f} seconds") + # convert to os-native paths if os.sep != '/': - path = path.replace('/', os.sep) - up_path = up_path.replace('/', os.sep) + path_ = path_.replace('/', os.sep) + up_path_ = up_path_.replace('/', os.sep) + + def strip_slash(p): + if p[-1] in os.sep and len(p) > 3: + p = p[:-1] + return p + + path = strip_slash(path_) + up_path = strip_slash(up_path_) if path == os.sep + os.sep: name = os.sep + os.sep @@ -1738,23 +1750,23 @@ def _do_Dir_test(lpath, path, up_path, sep, fileSys=fs) -> None: else: dir_up_path = dir.up().get_internal_path() - assert dir.name == name, \ - "dir.name %s != expected name %s" % \ - (dir.name, name) - assert dir.get_internal_path() == path, \ - "dir.path %s != expected path %s" % \ - (dir.get_internal_path(), path) - assert str(dir) == path, \ - "str(dir) %s != expected path %s" % \ - (str(dir), path) - assert dir_up_path == up_path, \ - "dir.up().path %s != expected parent path %s" % \ - (dir.up().get_internal_path(), up_path) + with self.subTest(): + self.assertEqual(dir.name, name) + with self.subTest(): + self.assertEqual(dir.get_internal_path(), path) + with self.subTest(): + self.assertEqual(str(dir), path) + with self.subTest(): + self.assertEqual(dir_up_path, up_path) save_os_path = os.path save_os_sep = os.sep try: import ntpath + + # monkeypatch some things to look like Windows and force the + # splitdrive stuff in the Node package to re-init; we'll put + # it back later if we happened to be on a non-Windows platform. os.path = ntpath os.sep = '\\' SCons.Node.FS.initialize_do_splitdrive() @@ -1763,6 +1775,16 @@ def _do_Dir_test(lpath, path, up_path, sep, fileSys=fs) -> None: def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test): return func(lpath, path_, up_path_, sep) + # Note that if any of the UNC paths eventually makes it to + # Windows, and looks syntactically valid (\\server\share), the + # mkdir takes a *long* time to fail with "network path not + # found". If it's not valid syntax, it fails immediately. + # Thus, some of these tests can be very slow on the first pass. + # Second pass they all "hit" on the Node lookup cache, + # so there's no further slowdown. Tried doing this passing + # create=False to the Dir() function, but that causes a hard + # fail if the dir doesn't already exist, so not right either. + Dir_test('//foo', '//foo', '//') Dir_test('//foo/bar', '//foo/bar', '//foo') Dir_test('//', '//', '//') @@ -1847,7 +1869,7 @@ def test_lookup_abs(self) -> None: d = root._lookup_abs('/tmp/foo-nonexistent/nonexistent-dir', SCons.Node.FS.Dir) assert d.__class__ == SCons.Node.FS.Dir, str(d.__class__) - @unittest.skipUnless(sys.platform == "win32", "requires Windows") + @unittest.skipUnless(IS_WINDOWS, "requires Windows") def test_lookup_uncpath(self) -> None: """Testing looking up a UNC path on Windows""" test = self.test @@ -1859,13 +1881,13 @@ def test_lookup_uncpath(self) -> None: assert str(f) == r'\\servername\C$\foo', \ 'UNC path %s got looked up as %s' % (path, f) - @unittest.skipUnless(sys.platform.startswith == "win32", "requires Windows") + @unittest.skipUnless(IS_WINDOWS, "requires Windows") def test_unc_drive_letter(self) -> None: """Test drive-letter lookup for windows UNC-style directories""" share = self.fs.Dir(r'\\SERVER\SHARE\Directory') assert str(share) == r'\\SERVER\SHARE\Directory', str(share) - @unittest.skipUnless(sys.platform == "win32", "requires Windows") + @unittest.skipUnless(IS_WINDOWS, "requires Windows") def test_UNC_dirs_2689(self) -> None: """Test some UNC dirs that printed incorrectly and/or caused infinite recursion errors prior to r5180 (SCons 2.1).""" @@ -1928,7 +1950,7 @@ def test_rel_path(self) -> None: d1_d2_f, d3_d4_f, '../../d3/d4/f', ] - if sys.platform in ('win32',): + if IS_WINDOWS: x_d1 = fs.Dir(r'X:\d1') x_d1_d2 = x_d1.Dir('d2') y_d1 = fs.Dir(r'Y:\d1') @@ -2487,7 +2509,7 @@ def collect(self, args): result += a return result - def signature(self, executor: ExecutorType): + def signature(self, executor: Executor): return self.val + 222 self.module = M(val) @@ -3578,7 +3600,7 @@ class MkdirAction(Action): def __init__(self, dir_made) -> None: self.dir_made = dir_made - def __call__(self, target, source, env, executor: Optional[ExecutorType] = None) -> None: + def __call__(self, target, source, env, executor: Executor | None = None) -> None: if executor: target = executor.get_all_targets() source = executor.get_all_sources() diff --git a/SCons/Node/NodeTests.py b/SCons/Node/NodeTests.py index 70c8551b13..d8288f4c9a 100644 --- a/SCons/Node/NodeTests.py +++ b/SCons/Node/NodeTests.py @@ -21,17 +21,21 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations + import SCons.compat import collections import re import unittest -from typing import Optional +from typing import TYPE_CHECKING import SCons.Errors import SCons.Node import SCons.Util -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor built_it = None @@ -64,7 +68,7 @@ class MyAction(MyActionBase): def __init__(self) -> None: self.order = 0 - def __call__(self, target, source, env, executor: Optional[ExecutorType] = None) -> int: + def __call__(self, target, source, env, executor: Executor | None = None) -> int: global built_it, built_target, built_source, built_args, built_order if executor: target = executor.get_all_targets() @@ -457,13 +461,13 @@ def test_push_to_cache(self) -> None: """Test the base push_to_cache() method""" n = SCons.Node.Node() r = n.push_to_cache() - assert r is None, r + assert not r, r def test_retrieve_from_cache(self) -> None: """Test the base retrieve_from_cache() method""" n = SCons.Node.Node() r = n.retrieve_from_cache() - assert r == 0, r + assert not r, r def test_visited(self) -> None: """Test the base visited() method @@ -707,21 +711,21 @@ def test_set_always_build(self) -> None: node = SCons.Node.Node() node.set_always_build() assert node.always_build - node.set_always_build(3) - assert node.always_build == 3 + node.set_always_build(3) # type: ignore[arg-type] + assert node.always_build def test_set_noclean(self) -> None: """Test setting a Node's noclean value """ node = SCons.Node.Node() node.set_noclean() - assert node.noclean == 1, node.noclean - node.set_noclean(7) - assert node.noclean == 1, node.noclean - node.set_noclean(0) - assert node.noclean == 0, node.noclean - node.set_noclean(None) - assert node.noclean == 0, node.noclean + assert node.noclean, node.noclean + node.set_noclean(7) # type: ignore[arg-type] + assert node.noclean, node.noclean + node.set_noclean(0) # type: ignore[arg-type] + assert not node.noclean, node.noclean + node.set_noclean(None) # type: ignore[arg-type] + assert not node.noclean, node.noclean def test_set_precious(self) -> None: """Test setting a Node's precious value @@ -729,8 +733,8 @@ def test_set_precious(self) -> None: node = SCons.Node.Node() node.set_precious() assert node.precious - node.set_precious(7) - assert node.precious == 7 + node.set_precious(7) # type: ignore[arg-type] + assert node.precious def test_set_pseudo(self) -> None: """Test setting a Node's pseudo value @@ -746,14 +750,14 @@ def test_exists(self) -> None: """ node = SCons.Node.Node() e = node.exists() - assert e == 1, e + assert e, e def test_exists_repo(self) -> None: """Test evaluating whether a Node exists locally or in a repository. """ node = SCons.Node.Node() e = node.rexists() - assert e == 1, e + assert e, e class MyNode(SCons.Node.Node): def exists(self) -> str: diff --git a/SCons/Node/__init__.py b/SCons/Node/__init__.py index 00bf4ac3b1..fb880ec75b 100644 --- a/SCons/Node/__init__.py +++ b/SCons/Node/__init__.py @@ -40,18 +40,26 @@ """ +from __future__ import annotations + import collections import copy from itertools import chain, zip_longest -from typing import Optional +from typing import Any, Callable, TYPE_CHECKING import SCons.Debug import SCons.Executor import SCons.Memoize from SCons.compat import NoSlotsPyPy from SCons.Debug import logInstanceCreation, Trace +from SCons.Executor import Executor from SCons.Util import hash_signature, is_List, UniqueList, render_tree -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Builder import BuilderBase + from SCons.Environment import Base as Environment + from SCons.Scanner import ScannerBase + from SCons.SConsign import SConsignEntry print_duplicate = 0 @@ -101,7 +109,7 @@ def do_nothing_node(node) -> None: pass Annotate = do_nothing_node # global set for recording all processed SContruct/SConscript nodes -SConscriptNodes = set() +SConscriptNodes: set[Node] = set() # Gets set to 'True' if we're running in interactive mode. Is # currently used to release parts of a target's info during @@ -188,7 +196,7 @@ def get_contents_entry(node): """Fetch the contents of the entry. Returns the exact binary contents of the file.""" try: - node = node.disambiguate(must_exist=1) + node = node.disambiguate(must_exist=True) except SCons.Errors.UserError: # There was nothing on disk with which to disambiguate # this entry. Leave it as an Entry, but return a null @@ -355,7 +363,7 @@ class NodeInfoBase: __slots__ = ('__weakref__',) current_version_id = 2 - def update(self, node) -> None: + def update(self, node: Node) -> None: try: field_list = self.field_list except AttributeError: @@ -375,7 +383,7 @@ def update(self, node) -> None: def convert(self, node, val) -> None: pass - def merge(self, other) -> None: + def merge(self, other: NodeInfoBase) -> None: """ Merge the fields of another object into this object. Already existing information is overwritten by the other instance's data. @@ -385,7 +393,7 @@ def merge(self, other) -> None: state = other.__getstate__() self.__setstate__(state) - def format(self, field_list=None, names: int=0): + def format(self, field_list: list[str] | None = None, names: bool = False): if field_list is None: try: field_list = self.field_list @@ -408,7 +416,7 @@ def format(self, field_list=None, names: int=0): fields.append(f) return fields - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: """ Return all fields that shall be pickled. Walk the slots in the class hierarchy and add those to the state dictionary. If a '__dict__' slot is @@ -428,7 +436,7 @@ def __getstate__(self): pass return state - def __setstate__(self, state) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: """ Restore the attributes from a pickled state. The version is discarded. """ @@ -457,12 +465,12 @@ class BuildInfoBase: def __init__(self) -> None: # Create an object attribute from the class attribute so it ends up # in the pickled data in the .sconsign file. - self.bsourcesigs = [] - self.bdependsigs = [] - self.bimplicitsigs = [] - self.bactsig = None + self.bsourcesigs: list[BuildInfoBase] = [] + self.bdependsigs: list[BuildInfoBase] = [] + self.bimplicitsigs: list[BuildInfoBase] = [] + self.bactsig: str | None = None - def merge(self, other) -> None: + def merge(self, other: BuildInfoBase) -> None: """ Merge the fields of another object into this object. Already existing information is overwritten by the other instance's data. @@ -472,7 +480,7 @@ def merge(self, other) -> None: state = other.__getstate__() self.__setstate__(state) - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: """ Return all fields that shall be pickled. Walk the slots in the class hierarchy and add those to the state dictionary. If a '__dict__' slot is @@ -492,7 +500,7 @@ def __getstate__(self): pass return state - def __setstate__(self, state) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: """ Restore the attributes from a pickled state. """ @@ -552,6 +560,12 @@ class Node(metaclass=NoSlotsPyPy): '_func_target_from_source'] class Attrs: + """A generic place to store extra information about the Node. + + Defines ``__slots__`` for performance, but different consumers + define their own attributes, so to avoid having to collect them + all here, we add a ``__dict__`` slot to get dynamic attributes. + """ __slots__ = ('shared', '__dict__') @@ -570,42 +584,42 @@ def __init__(self) -> None: # this way, instead of wrapping up each list+dictionary pair in # a class. (Of course, we could always still do that in the # future if we had a good reason to...). - self.sources = [] # source files used to build node - self.sources_set = set() + self.sources: list[Node] = [] # source files used to build node + self.sources_set: set[Node] = set() self._specific_sources = False - self.depends = [] # explicit dependencies (from Depends) - self.depends_set = set() - self.ignore = [] # dependencies to ignore - self.ignore_set = set() - self.prerequisites = None - self.implicit = None # implicit (scanned) dependencies (None means not scanned yet) - self.waiting_parents = set() - self.waiting_s_e = set() + self.depends: list[Node] = [] # explicit dependencies (from Depends) + self.depends_set: set[Node] = set() + self.ignore: list[Node] = [] # dependencies to ignore + self.ignore_set: set[Node] = set() + self.prerequisites: UniqueList | None = None + self.implicit: list[Node] | None = None # implicit (scanned) dependencies (None means not scanned yet) + self.waiting_parents: set[Node] = set() + self.waiting_s_e: set[Node] = set() self.ref_count = 0 - self.wkids = None # Kids yet to walk, when it's an array + self.wkids: list[Node] | None = None # Kids yet to walk, when it's an array - self.env = None + self.env: Environment | None = None self.state = no_state - self.precious = None + self.precious = False self.pseudo = False - self.noclean = 0 - self.nocache = 0 - self.cached = 0 # is this node pulled from cache? - self.always_build = None - self.includes = None + self.noclean = False + self.nocache = False + self.cached = False # is this node pulled from cache? + self.always_build = False + self.includes: list[str] | None = None self.attributes = self.Attrs() # Generic place to stick information about the Node. - self.side_effect = 0 # true iff this node is a side effect - self.side_effects = [] # the side effects of building this target - self.linked = 0 # is this node linked to the variant directory? - self.changed_since_last_build = 0 - self.store_info = 0 - self._tags = None - self._func_is_derived = 1 - self._func_exists = 1 - self._func_rexists = 1 - self._func_get_contents = 0 - self._func_target_from_source = 0 - self.ninfo = None + self.side_effect = False # true iff this node is a side effect + self.side_effects: list[Node] = [] # the side effects of building this target + self.linked = False # is this node linked to the variant directory? + self.changed_since_last_build = 0 # Index for "_decider_map". + self.store_info = 0 # Index for "store_info_map". + self._tags: dict[str, Any] | None = None + self._func_is_derived = 1 # Index for "_is_derived_map". + self._func_exists = 1 # Index for "_exists_map" + self._func_rexists = 1 # Index for "_rexists_map" + self._func_get_contents = 0 # Index for "_get_contents_map" + self._func_target_from_source = 0 # Index for "_target_from_source_map" + self.ninfo: NodeInfoBase | None = None self.clear_memoized_values() @@ -614,14 +628,17 @@ def __init__(self) -> None: # what line in what file created the node, for example). Annotate(self) - def disambiguate(self, must_exist=None): + def __fspath__(self) -> str: + return str(self) + + def disambiguate(self, must_exist: bool = False): return self def get_suffix(self) -> str: return '' @SCons.Memoize.CountMethodCall - def get_build_env(self): + def get_build_env(self) -> Environment: """Fetch the appropriate Environment to build this node. """ try: @@ -632,15 +649,15 @@ def get_build_env(self): self._memo['get_build_env'] = result return result - def get_build_scanner_path(self, scanner): + def get_build_scanner_path(self, scanner: ScannerBase): """Fetch the appropriate scanner path for this node.""" return self.get_executor().get_build_scanner_path(scanner) - def set_executor(self, executor: ExecutorType) -> None: + def set_executor(self, executor: Executor) -> None: """Set the action executor for this node.""" self.executor = executor - def get_executor(self, create: int=1) -> ExecutorType: + def get_executor(self, create: bool = True) -> Executor: """Fetch the action executor for this node. Create one if there isn't already one, and requested to do so.""" try: @@ -651,7 +668,7 @@ def get_executor(self, create: int=1) -> ExecutorType: try: act = self.builder.action except AttributeError: - executor = SCons.Executor.Null(targets=[self]) # type: ignore + executor = SCons.Executor.Null(targets=[self]) # type: ignore[assignment] else: executor = SCons.Executor.Executor(act, self.env or self.builder.env, @@ -664,7 +681,7 @@ def get_executor(self, create: int=1) -> ExecutorType: def executor_cleanup(self) -> None: """Let the executor clean up any cached information.""" try: - executor = self.get_executor(create=None) + executor = self.get_executor(create=False) except AttributeError: pass else: @@ -681,7 +698,7 @@ def reset_executor(self) -> None: def push_to_cache(self) -> bool: """Try to push a node into a cache """ - pass + return False def retrieve_from_cache(self) -> bool: """Try to retrieve the node's content from a cache @@ -708,7 +725,7 @@ def make_ready(self) -> None: """ pass - def prepare(self): + def prepare(self) -> None: """Prepare for this Node to be built. This is called after the Taskmaster has decided that the Node @@ -741,7 +758,7 @@ def prepare(self): raise SCons.Errors.StopError(msg % (i, self)) self.binfo = self.get_binfo() - def build(self, **kw): + def build(self, **kw) -> None: """Actually build the node. This is called by the Taskmaster after it's decided that the @@ -826,10 +843,10 @@ def release_target_info(self) -> None: """ pass - def add_to_waiting_s_e(self, node) -> None: + def add_to_waiting_s_e(self, node: Node) -> None: self.waiting_s_e.add(node) - def add_to_waiting_parents(self, node) -> int: + def add_to_waiting_parents(self, node: Node) -> int: """ Returns the number of nodes added to our waiting parents list: 1 if we add a unique waiting parent, 0 if not. (Note that the @@ -866,13 +883,13 @@ def clear(self) -> None: delattr(self, attr) except AttributeError: pass - self.cached = 0 + self.cached = False self.includes = None def clear_memoized_values(self) -> None: self._memo = {} - def builder_set(self, builder) -> None: + def builder_set(self, builder: BuilderBase | None) -> None: self.builder = builder try: del self.executor @@ -898,7 +915,7 @@ def has_builder(self) -> bool: b = self.builder = None return b is not None - def set_explicit(self, is_explicit) -> None: + def set_explicit(self, is_explicit: bool) -> None: self.is_explicit = is_explicit def has_explicit_builder(self) -> bool: @@ -914,7 +931,7 @@ def has_explicit_builder(self) -> bool: self.is_explicit = False return False - def get_builder(self, default_builder=None): + def get_builder(self, default_builder: BuilderBase | None = None) -> BuilderBase | None: """Return the set builder, or a specified default value""" try: return self.builder @@ -947,7 +964,7 @@ def is_conftest(self) -> bool: return False return True - def check_attributes(self, name): + def check_attributes(self, name: str) -> Any | None: """ Simple API to check if the node.attributes for name has been set""" return getattr(getattr(self, "attributes", None), name, None) @@ -957,7 +974,7 @@ def alter_targets(self): """ return [], None - def get_found_includes(self, env, scanner, path): + def get_found_includes(self, env: Environment, scanner: ScannerBase | None, path) -> list[Node]: """Return the scanned include lines (implicit dependencies) found in this node. @@ -967,7 +984,7 @@ def get_found_includes(self, env, scanner, path): """ return [] - def get_implicit_deps(self, env, initial_scanner, path_func, kw = {}): + def get_implicit_deps(self, env: Environment, initial_scanner: ScannerBase | None, path_func, kw = {}) -> list[Node]: """Return a list of implicit dependencies for this node. This method exists to handle recursive invocation of the scanner @@ -1002,7 +1019,7 @@ def get_implicit_deps(self, env, initial_scanner, path_func, kw = {}): return dependencies - def _get_scanner(self, env, initial_scanner, root_node_scanner, kw): + def _get_scanner(self, env: Environment, initial_scanner: ScannerBase | None, root_node_scanner: ScannerBase | None, kw: dict[str, Any] | None) -> ScannerBase | None: if initial_scanner: # handle explicit scanner case scanner = initial_scanner.select(self) @@ -1019,13 +1036,13 @@ def _get_scanner(self, env, initial_scanner, root_node_scanner, kw): return scanner - def get_env_scanner(self, env, kw={}): + def get_env_scanner(self, env: Environment, kw: dict[str, Any] | None = {}) -> ScannerBase | None: return env.get_scanner(self.scanner_key()) - def get_target_scanner(self): + def get_target_scanner(self) -> ScannerBase | None: return self.builder.target_scanner - def get_source_scanner(self, node): + def get_source_scanner(self, node: Node) -> ScannerBase | None: """Fetch the source scanner for the specified node NOTE: "self" is the target being built, "node" is @@ -1051,10 +1068,10 @@ def get_source_scanner(self, node): scanner = scanner.select(node) return scanner - def add_to_implicit(self, deps) -> None: + def add_to_implicit(self, deps: list[Node]) -> None: if not hasattr(self, 'implicit') or self.implicit is None: self.implicit = [] - self.implicit_set = set() + self.implicit_set: set[Node] = set() self._children_reset() self._add_child(self.implicit, self.implicit_set, deps) @@ -1113,10 +1130,10 @@ def print_nodelist(n): if scanner: executor.scan_targets(scanner) - def scanner_key(self): + def scanner_key(self) -> str | None: return None - def select_scanner(self, scanner): + def select_scanner(self, scanner: ScannerBase) -> ScannerBase | None: """Selects a scanner for this Node. This is a separate method so it can be overridden by Node @@ -1126,7 +1143,7 @@ def select_scanner(self, scanner): """ return scanner.select(self) - def env_set(self, env, safe: bool=False) -> None: + def env_set(self, env: Environment, safe: bool = False) -> None: if safe and self.env: return self.env = env @@ -1138,21 +1155,21 @@ def env_set(self, env, safe: bool=False) -> None: NodeInfo = NodeInfoBase BuildInfo = BuildInfoBase - def new_ninfo(self): + def new_ninfo(self) -> NodeInfoBase: ninfo = self.NodeInfo() return ninfo - def get_ninfo(self): + def get_ninfo(self) -> NodeInfoBase: if self.ninfo is not None: return self.ninfo self.ninfo = self.new_ninfo() return self.ninfo - def new_binfo(self): + def new_binfo(self) -> BuildInfoBase: binfo = self.BuildInfo() return binfo - def get_binfo(self): + def get_binfo(self) -> BuildInfoBase: """ Fetch a node's build information. @@ -1211,7 +1228,7 @@ def del_binfo(self) -> None: except AttributeError: pass - def get_csig(self): + def get_csig(self) -> str: try: return self.ninfo.csig except AttributeError: @@ -1219,13 +1236,13 @@ def get_csig(self): ninfo.csig = hash_signature(self.get_contents()) return self.ninfo.csig - def get_cachedir_csig(self): + def get_cachedir_csig(self) -> str: return self.get_csig() - def get_stored_info(self): + def get_stored_info(self) -> SConsignEntry | None: return None - def get_stored_implicit(self): + def get_stored_implicit(self) -> list[Node] | None: """Fetch the stored implicit dependencies""" return None @@ -1233,7 +1250,7 @@ def get_stored_implicit(self): # # - def set_precious(self, precious: int = 1) -> None: + def set_precious(self, precious: bool = True) -> None: """Set the Node's precious value.""" self.precious = precious @@ -1241,19 +1258,15 @@ def set_pseudo(self, pseudo: bool = True) -> None: """Set the Node's pseudo value.""" self.pseudo = pseudo - def set_noclean(self, noclean: int = 1) -> None: + def set_noclean(self, noclean: bool = True) -> None: """Set the Node's noclean value.""" - # Make sure noclean is an integer so the --debug=stree - # output in Util.py can use it as an index. - self.noclean = noclean and 1 or 0 + self.noclean = noclean - def set_nocache(self, nocache: int = 1) -> None: + def set_nocache(self, nocache: bool = True) -> None: """Set the Node's nocache value.""" - # Make sure nocache is an integer so the --debug=stree - # output in Util.py can use it as an index. - self.nocache = nocache and 1 or 0 + self.nocache = nocache - def set_always_build(self, always_build: int = 1) -> None: + def set_always_build(self, always_build: bool = True) -> None: """Set the Node's always_build value.""" self.always_build = always_build @@ -1261,12 +1274,12 @@ def exists(self) -> bool: """Reports whether node exists.""" return _exists_map[self._func_exists](self) - def rexists(self): + def rexists(self) -> bool: """Does this node exist locally or in a repository?""" # There are no repositories by default: return _rexists_map[self._func_rexists](self) - def get_contents(self): + def get_contents(self) -> bytes | str: """Fetch the contents of the entry.""" return _get_contents_map[self._func_get_contents](self) @@ -1275,11 +1288,11 @@ def missing(self) -> bool: not self.linked and \ not self.rexists() - def remove(self): + def remove(self) -> None: """Remove this Node: no-op by default.""" return None - def add_dependency(self, depend): + def add_dependency(self, depend: list[Node]) -> None: """Adds dependencies.""" try: self._add_child(self.depends, self.depends_set, depend) @@ -1291,14 +1304,14 @@ def add_dependency(self, depend): s = str(e) raise SCons.Errors.UserError("attempted to add a non-Node dependency to %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e))) - def add_prerequisite(self, prerequisite) -> None: + def add_prerequisite(self, prerequisite: list[Node]) -> None: """Adds prerequisites""" if self.prerequisites is None: self.prerequisites = UniqueList() self.prerequisites.extend(prerequisite) self._children_reset() - def add_ignore(self, depend): + def add_ignore(self, depend: list[Node]) -> None: """Adds dependencies to ignore.""" try: self._add_child(self.ignore, self.ignore_set, depend) @@ -1310,7 +1323,7 @@ def add_ignore(self, depend): s = str(e) raise SCons.Errors.UserError("attempted to ignore a non-Node dependency of %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e))) - def add_source(self, source): + def add_source(self, source: list[Node]) -> None: """Adds sources.""" if self._specific_sources: return @@ -1324,7 +1337,7 @@ def add_source(self, source): s = str(e) raise SCons.Errors.UserError("attempted to add a non-Node as source of %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e))) - def _add_child(self, collection, set, child) -> None: + def _add_child(self, collection: list[Node], set: set[Node], child: list[Node]) -> None: """Adds 'child' to 'collection', first checking 'set' to see if it's already present.""" added = None @@ -1336,11 +1349,11 @@ def _add_child(self, collection, set, child) -> None: if added: self._children_reset() - def set_specific_source(self, source) -> None: + def set_specific_source(self, source: list[Node]) -> None: self.add_source(source) self._specific_sources = True - def add_wkid(self, wkid) -> None: + def add_wkid(self, wkid: Node) -> None: """Add a node to the list of kids waiting to be evaluated""" if self.wkids is not None: self.wkids.append(wkid) @@ -1352,7 +1365,7 @@ def _children_reset(self) -> None: self.executor_cleanup() @SCons.Memoize.CountMethodCall - def _children_get(self): + def _children_get(self) -> list[Node]: try: return self._memo['_children_get'] except KeyError: @@ -1383,12 +1396,12 @@ def _children_get(self): if i not in self.ignore_set: children.append(i) else: - children = self.all_children(scan=0) + children = self.all_children(scan=False) self._memo['_children_get'] = children return children - def all_children(self, scan: int=1): + def all_children(self, scan: bool = True) -> list[Node]: """Return a list of all the node's direct children.""" if scan: self.scan() @@ -1412,27 +1425,27 @@ def all_children(self, scan: int=1): # internally anyway...) return list(chain.from_iterable([_f for _f in [self.sources, self.depends, self.implicit] if _f])) - def children(self, scan: int=1): + def children(self, scan: bool = True) -> list[Node]: """Return a list of the node's direct children, minus those that are ignored by this node.""" if scan: self.scan() return self._children_get() - def set_state(self, state) -> None: + def set_state(self, state: int) -> None: self.state = state - def get_state(self): + def get_state(self) -> int: return self.state - def get_env(self): + def get_env(self) -> Environment: env = self.env if not env: import SCons.Defaults env = SCons.Defaults.DefaultEnvironment() return env - def Decider(self, function) -> None: + def Decider(self, function: Callable[[Node, Node, NodeInfoBase, Node | None], bool]) -> None: foundkey = None for k, v in _decider_map.items(): if v == function: @@ -1443,19 +1456,19 @@ def Decider(self, function) -> None: _decider_map[foundkey] = function self.changed_since_last_build = foundkey - def Tag(self, key, value) -> None: + def Tag(self, key: str, value: Any | None) -> None: """ Add a user-defined tag. """ if not self._tags: self._tags = {} self._tags[key] = value - def GetTag(self, key): + def GetTag(self, key: str) -> Any | None: """ Return a user-defined tag. """ if not self._tags: return None return self._tags.get(key, None) - def changed(self, node=None, allowcache: bool=False): + def changed(self, node: Node | None = None, allowcache: bool = False) -> bool: """ Returns if the node is up-to-date with respect to the BuildInfo stored last time it was built. The default behavior is to compare @@ -1534,7 +1547,7 @@ def children_are_up_to_date(self) -> bool: if self.always_build: return False state = 0 - for kid in self.children(None): + for kid in self.children(False): s = kid.get_state() if s and (not state or s > state): state = s @@ -1559,13 +1572,13 @@ def render_include_tree(self): path = self.get_build_scanner_path(scanner) else: path = None - def f(node, env=env, scanner=scanner, path=path): + def f(node: Node, env: Environment = env, scanner: ScannerBase = scanner, path=path): return node.get_found_includes(env, scanner, path) return render_tree(s, f, 1) else: return None - def get_abspath(self): + def get_abspath(self) -> str: """ Return an absolute path to the Node. This will return simply str(Node) by default, but for Node types that have a concept of @@ -1573,7 +1586,7 @@ def get_abspath(self): """ return str(self) - def for_signature(self): + def for_signature(self) -> str: """ Return a string representation of the Node that will always be the same for this particular Node, no matter what. This @@ -1588,7 +1601,7 @@ def for_signature(self): """ return str(self) - def get_string(self, for_signature): + def get_string(self, for_signature: bool) -> str: """This is a convenience function designed primarily to be used in command generators (i.e., CommandGeneratorActions or Environment variables that are callable), which are called @@ -1719,9 +1732,9 @@ class NodeList(collections.UserList): def __str__(self) -> str: return str(list(map(str, self.data))) -def get_children(node, parent): return node.children() -def ignore_cycle(node, stack) -> None: pass -def do_nothing(node, parent) -> None: pass +def get_children(node: Node, parent: Node | None) -> list[Node]: return node.children() +def ignore_cycle(node: Node, stack: list[Node]) -> None: pass +def do_nothing(node: Node, parent: Node | None) -> None: pass class Walker: """An iterator for walking a Node tree. @@ -1736,15 +1749,19 @@ class Walker: This class does not get caught in node cycles caused, for example, by C header file include loops. """ - def __init__(self, node, kids_func=get_children, - cycle_func=ignore_cycle, - eval_func=do_nothing) -> None: + def __init__( + self, + node: Node, + kids_func: Callable[[Node, Node | None], list[Node]] = get_children, + cycle_func: Callable[[Node, list[Node]], None] = ignore_cycle, + eval_func: Callable[[Node, Node | None], None] = do_nothing, + ) -> None: self.kids_func = kids_func self.cycle_func = cycle_func self.eval_func = eval_func node.wkids = copy.copy(kids_func(node, None)) self.stack = [node] - self.history = {} # used to efficiently detect and avoid cycles + self.history: dict[Node, Any | None] = {} # used to efficiently detect and avoid cycles self.history[node] = None def get_next(self): diff --git a/SCons/Platform/Platform.xml b/SCons/Platform/Platform.xml index 5d31f6bb0c..d4d98bfbf2 100644 --- a/SCons/Platform/Platform.xml +++ b/SCons/Platform/Platform.xml @@ -1,9 +1,10 @@ -A callable object used to handle overly long command line strings, -since operations which call out to a shell will fail -if the line is longer than the shell can accept. -This tends to particularly impact linking. -The tempfile object stores the command line in a temporary -file in the appropriate format, and returns -an alternate command line so the invoked tool will make -use of the contents of the temporary file. -If you need to replace the default tempfile object, -the callable should take into account the settings of +Holds a callable object which will be invoked to transform long +command lines (string or list) into an alternate form. +Length limits on various operating systems +may cause long command lines to fail when calling out to +a shell to run the command. +Most often affects linking, when there are many object files and/or +libraries to be linked, but may also affect other +compilation steps which have many arguments. +&cv-TEMPFILE; is not called directly, +but rather is typically embedded in another +&consvar;, to be expanded when used. Example: + + + +env["TEMPFILE"] = TempFileMunge +env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES', '$LINKCOMSTR')}" + + + +The SCons default value for &cv-TEMPFILE;, +TempFileMunge, +performs command substitution on the passed command line, +calculates whether modification is needed, +then puts all but the first word (assumed to be the command name) +of the resulting list into a temporary file +(sometimes called a response file or command file), +and returns a new command line consisting of the +the command name and an appropriately formatted reference +to the temporary file. + + + +A replacement for the default tempfile object would need +to do fundamentally the same thing, including taking into account +the values of &cv-link-MAXLINELENGTH;, &cv-link-TEMPFILEPREFIX;, &cv-link-TEMPFILESUFFIX;, @@ -314,6 +340,11 @@ the callable should take into account the settings of &cv-link-TEMPFILEDIR; and &cv-link-TEMPFILEARGESCFUNC;. +If a particular use case requires a different transformation +than the default, it is recommended to copy the mechanism and +define a new &consvar; and rewrite the relevant *COM +variable(s) to use it, to avoid possibly disrupting existing uses +of &cv-TEMPFILE;. @@ -323,6 +354,8 @@ and The prefix for the name of the temporary file used to store command lines exceeding &cv-link-MAXLINELENGTH;. +The prefix must include the compiler syntax to actually +include and process the file. The default prefix is '@', which works for the &MSVC; and GNU toolchains on Windows. Set this appropriately for other toolchains, @@ -337,7 +370,7 @@ or '-via' for ARM toolchain. The suffix for the name of the temporary file used to store command lines exceeding &cv-link-MAXLINELENGTH;. -The suffix should include the dot ('.') if one is wanted as +The suffix should include the dot ('.') if one is needed as it will not be added automatically. The default is .lnk. @@ -362,6 +395,18 @@ Note this value is used literally and not expanded by the subst logic. The directory to create the long-lines temporary file in. +If unset, the &Python; +tempfile module chooses the directory +based on the TMPDIR, +TEMP +or TMP environment variables. +If the default path causes processing errors, +set &cv-TEMPFILEDIR; to a safer alternative. +For example, on Windows, +the default temporary file path contains the username. +If the username contains non-7-bit-ASCII characters, +there may decoding errors opening the path to the temporary file. +See also &cv-link-TEMPFILEENCODING;. @@ -375,8 +420,8 @@ If you need to apply extra operations on a command argument (to fix Windows slashes, normalize paths, etc.) before writing to the temporary file, you can set the &cv-TEMPFILEARGESCFUNC; variable to a custom function. -Such a function takes a single string argument and returns -a new string with any modifications applied. +The function must accept a single string argument and +and return a new string with any modifications applied. Example: @@ -399,4 +444,23 @@ env["TEMPFILEARGESCFUNC"] = tempfile_arg_esc_func + + + +By default, the long-lines temporary file (aka "response file") created by the +&cv-link-TEMPFILE; function will be encoded in the &Python; +default encoding, UTF-8. +If the external command which reads the response file +encounters decoding errors +(usually, if that command depends on Windows legacy code pages, +and a pathname in the response file +or the response file path itself cannot +be represented in the 7-bit ASCII characer set), +set this variable to the appropriate codec. +See also &cv-link-TEMPFILEDIR;. + +New in version NEXT_RELEASE + + + diff --git a/SCons/Platform/PlatformTests.py b/SCons/Platform/PlatformTests.py index c786aa7d8d..691aed2598 100644 --- a/SCons/Platform/PlatformTests.py +++ b/SCons/Platform/PlatformTests.py @@ -207,9 +207,7 @@ def test_MAXLINELENGTH(self) -> None: assert cmd != defined_cmd, cmd def test_TEMPFILEARGJOINBYTE(self) -> None: - """ - Test argument join byte TEMPFILEARGJOINBYTE - """ + """Test argument join byte TEMPFILEARGJOINBYTE.""" # Init class with cmd, such that the fully expanded # string reads "a test command line". @@ -232,19 +230,24 @@ def test_TEMPFILEARGJOINBYTE(self) -> None: SCons.Action.print_actions = 0 env['MAXLINELENGTH'] = len(expanded_cmd)-1 cmd = t(None, None, env, 0) - # print("CMD is:%s"%cmd) + # print(f"[CMD is: {cmd}]") - with open(cmd[-1],'rb') as f: + if cmd[-1].startswith('@'): + tempfile = cmd[-1][1:] + else: + tempfile = cmd[-1] + with open(tempfile, 'rb') as f: file_content = f.read() - # print("Content is:[%s]"%file_content) + # print(f"[Content of {tempfile} is:{file_content}]") # ...and restoring its setting. SCons.Action.print_actions = old_actions - assert file_content != env['TEMPFILEARGJOINBYTE'].join(['test','command','line']) + assert file_content != bytearray( + env['TEMPFILEARGJOINBYTE'].join(['test', 'command', 'line']), + encoding='utf-8', + ) def test_TEMPFILEARGESCFUNC(self) -> None: - """ - Test a custom TEMPFILEARGESCFUNC - """ + """Test a custom TEMPFILEARGESCFUNC.""" def _tempfile_arg_esc_func(arg): return str(arg).replace("line", "newarg") @@ -262,12 +265,16 @@ def _tempfile_arg_esc_func(arg): SCons.Action.print_actions = 0 env['TEMPFILEARGESCFUNC'] = _tempfile_arg_esc_func cmd = t(None, None, env, 0) - # print("CMD is: %s"%cmd) + # print(f"[CMD is: {cmd}]") - with open(cmd[-1], 'rb') as f: + if cmd[-1].startswith('@'): + tempfile = cmd[-1][1:] + else: + tempfile = cmd[-1] + with open(tempfile, 'rb') as f: file_content = f.read() - # print("Content is:[%s]"%file_content) - # # ...and restoring its setting. + # print(f"[Content of {tempfile} is:{file_content}]") + # ...and restoring its setting. SCons.Action.print_actions = old_actions assert b"newarg" in file_content diff --git a/SCons/Platform/__init__.py b/SCons/Platform/__init__.py index 77eea5c099..34d8cbfd02 100644 --- a/SCons/Platform/__init__.py +++ b/SCons/Platform/__init__.py @@ -42,14 +42,18 @@ import SCons.compat +import atexit import importlib import os import sys import tempfile +import SCons.Action import SCons.Errors +import SCons.Platform import SCons.Subst import SCons.Tool +import SCons.Util def platform_default(): @@ -150,7 +154,7 @@ class TempFileMunge: the length of command lines. Example:: env["TEMPFILE"] = TempFileMunge - env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES','$LINKCOMSTR')}" + env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES', '$LINKCOMSTR')}" By default, the name of the temporary file used begins with a prefix of '@'. This may be configured for other tool chains by @@ -255,34 +259,32 @@ def __call__(self, target, source, env, for_signature): else: tempfile_dir = None - fd, tmp = tempfile.mkstemp(suffix, dir=tempfile_dir, text=True) + # default is binary - encode the tempfile contents later + fd, tmp = tempfile.mkstemp(suffix, dir=tempfile_dir) native_tmp = SCons.Util.get_native_path(tmp) + # arrange for cleanup on exit: + + def tmpfile_cleanup(file) -> None: + os.remove(file) + + atexit.register(tmpfile_cleanup, tmp) + if env.get('SHELL', None) == 'sh': # The sh shell will try to escape the backslashes in the # path, so unescape them. native_tmp = native_tmp.replace('\\', r'\\\\') - # In Cygwin, we want to use rm to delete the temporary - # file, because del does not exist in the sh shell. - rm = env.Detect('rm') or 'del' - else: - # Don't use 'rm' if the shell is not sh, because rm won't - # work with the Windows shells (cmd.exe or command.com) or - # Windows path names. - rm = 'del' if 'TEMPFILEPREFIX' in env: prefix = env.subst('$TEMPFILEPREFIX') else: - prefix = '@' + prefix = "@" tempfile_esc_func = env.get('TEMPFILEARGESCFUNC', SCons.Subst.quote_spaces) - args = [ - tempfile_esc_func(arg) - for arg in cmd[1:] - ] + args = [tempfile_esc_func(arg) for arg in cmd[1:]] join_char = env.get('TEMPFILEARGJOIN', ' ') - os.write(fd, bytearray(join_char.join(args) + "\n", 'utf-8')) + encoding = env.get('TEMPFILEENCODING', 'utf-8') + os.write(fd, bytes(join_char.join(args) + "\n", encoding=encoding)) os.close(fd) # XXX Using the SCons.Action.print_actions value directly @@ -301,15 +303,20 @@ def __call__(self, target, source, env, for_signature): # purity get in the way of just being helpful, so we'll # reach into SCons.Action directly. if SCons.Action.print_actions: - cmdstr = env.subst(self.cmdstr, SCons.Subst.SUBST_RAW, target, - source) if self.cmdstr is not None else '' + cmdstr = ( + env.subst(self.cmdstr, SCons.Subst.SUBST_RAW, target, source) + if self.cmdstr is not None + else '' + ) # Print our message only if XXXCOMSTR returns an empty string - if len(cmdstr) == 0 : - cmdstr = ("Using tempfile "+native_tmp+" for command line:\n"+ - str(cmd[0]) + " " + " ".join(args)) + if not cmdstr: + cmdstr = ( + f"Using tempfile {native_tmp} for command line:\n" + f'{cmd[0]} {" ".join(args)}' + ) self._print_cmd_str(target, source, env, cmdstr) - cmdlist = [cmd[0], prefix + native_tmp + '\n' + rm, native_tmp] + cmdlist = [cmd[0], prefix + native_tmp] # Store the temporary file command list into the target Node.attributes # to avoid creating two temporary files one for print and one for execute. diff --git a/SCons/Platform/darwin.py b/SCons/Platform/darwin.py index 381b54249d..4ab7466c60 100644 --- a/SCons/Platform/darwin.py +++ b/SCons/Platform/darwin.py @@ -46,7 +46,7 @@ def generate(env) -> None: # make sure this works on Macs with Tiger or earlier try: dirlist = os.listdir('/etc/paths.d') - except FileNotFoundError: + except (FileNotFoundError, PermissionError): dirlist = [] for file in dirlist: diff --git a/SCons/Platform/posix.xml b/SCons/Platform/posix.xml index e90e1595d0..dcf05e0903 100644 --- a/SCons/Platform/posix.xml +++ b/SCons/Platform/posix.xml @@ -1,9 +1,10 @@ bool: - """Returns true if **path** is located under the **base** directory.""" - if not path or not base: # empty path may happen, base too +# Variables to export: +# - Python docs: +# When a virtual environment has been activated, the VIRTUAL_ENV environment +# variable is set to the path of the environment. Since explicitly +# activating a virtual environment is not required to use it, VIRTUAL_ENV +# cannot be relied upon to determine whether a virtual environment is being +# used. +# - pipenv: shell sets PIPENV_ACTIVE, cannot find it documented. +# Any others we should include? +VIRTUALENV_VARIABLES = ['VIRTUAL_ENV', 'PIPENV_ACTIVE'] + + +def _running_in_virtualenv() -> bool: + """Check whether scons is running in a virtualenv.""" + # TODO: the virtualenv command used to inject a sys.real_prefix before + # Python started officially tracking virtualenvs with the venv module. + # All Pythons since 3.3 use sys.base_prefix for tracking (PEP 405); + # virtualenv has retired their old behavior and now only makes + # venv-style virtualenvs. We're now using the detection suggested in + # PEP 668, and should be able to drop the real_prefix check soon. + return sys.base_prefix != sys.prefix or hasattr(sys, 'real_prefix') + + +def _is_path_in(path: str, base: str) -> bool: + """Check if *path* is located under the *base* directory.""" + if not path or not base: # empty path or base are possible return False rp = os.path.relpath(path, base) return (not rp.startswith(os.path.pardir)) and (not rp == os.path.curdir) def _inject_venv_variables(env) -> None: + """Copy any set virtualenv variables from ``os.environ`` to *env*.""" if 'ENV' not in env: env['ENV'] = {} ENV = env['ENV'] - for name in virtualenv_variables: + for name in VIRTUALENV_VARIABLES: try: ENV[name] = os.environ[name] except KeyError: pass def _inject_venv_path(env, path_list=None) -> None: - """Modify environment such that SCons will take into account its virtualenv - when running external tools.""" + """Insert virtualenv-related paths from ``os.environe`` to *env*.""" if path_list is None: path_list = os.getenv('PATH') env.PrependENVPath('PATH', select_paths_in_venv(path_list)) -def select_paths_in_venv(path_list): - """Returns a list of paths from **path_list** which are under virtualenv's - home directory.""" +def select_paths_in_venv(path_list: str | list[str]) -> list[str]: + """Filter *path_list*, returning values under the virtualenv.""" if SCons.Util.is_String(path_list): path_list = path_list.split(os.path.pathsep) - # Find in path_list the paths under the virtualenv's home return [path for path in path_list if IsInVirtualenv(path)] def ImportVirtualenv(env) -> None: - """Copies virtualenv-related environment variables from OS environment - to ``env['ENV']`` and prepends virtualenv's PATH to ``env['ENV']['PATH']``. - """ + """Add virtualenv information to *env*.""" _inject_venv_variables(env) _inject_venv_path(env) -def Virtualenv(): - """Returns path to the virtualenv home if scons is executing within a - virtualenv or None, if not.""" +def Virtualenv() -> str: + """Return whether operating in a virtualenv. + + Returns the path to the virtualenv home if scons is executing + within a virtualenv, else and empty string. + """ if _running_in_virtualenv(): return sys.prefix - return None + return "" -def IsInVirtualenv(path): - """Returns True, if **path** is under virtualenv's home directory. If not, - or if we don't use virtualenv, returns False.""" +def IsInVirtualenv(path: str) -> bool: + """Check whether *path* is under the virtualenv's directory. + + Returns ``False`` if not using a virtualenv. + """ return _is_path_in(path, Virtualenv()) diff --git a/SCons/Platform/virtualenv.xml b/SCons/Platform/virtualenv.xml new file mode 100644 index 0000000000..0e4a50a53a --- /dev/null +++ b/SCons/Platform/virtualenv.xml @@ -0,0 +1,40 @@ + + + + +%scons; + +%builders-mod; + +%functions-mod; + +%tools-mod; + +%variables-mod; +]> + + + + +() + + +If the &SCons; process is running inside a &Python; +virtual environment, +return the path to the directory where that environment is stored, +else an empty string. +The result can be treated as a boolean value if the path is unneeded. + + + + + diff --git a/SCons/Platform/virtualenvTests.py b/SCons/Platform/virtualenvTests.py index e2565205ee..cc8565db6e 100644 --- a/SCons/Platform/virtualenvTests.py +++ b/SCons/Platform/virtualenvTests.py @@ -27,6 +27,7 @@ # This happens in this unittest, since it's the script path. Remove # it before the stdlib imports. Better way to handle this problem? import sys + if 'Platform' in sys.path[0]: platpath = sys.path.pop(0) # pylint: disable=wrong-import-position @@ -40,13 +41,19 @@ import SCons.Util # pylint: enable=wrong-import-position +# TODO: this test does a lot of fiddling with obsolete sys.real_prefix. +# The 'virtualenv' tool used to force this in, but now uses venv-style +# to follow Python since 3.3. Simplify once we think it's safe enough. + class Environment(collections.UserDict): + """Mock environment for testing.""" + def Detect(self, cmd): return cmd def AppendENVPath(self, key, value) -> None: if SCons.Util.is_List(value): - value = os.path.pathsep.join(value) + value = os.path.pathsep.join(value) if 'ENV' not in self: self['ENV'] = {} current = self['ENV'].get(key) @@ -57,7 +64,7 @@ def AppendENVPath(self, key, value) -> None: def PrependENVPath(self, key, value) -> None: if SCons.Util.is_List(value): - value = os.path.pathsep.join(value) + value = os.path.pathsep.join(value) if 'ENV' not in self: self['ENV'] = {} current = self['ENV'].get(key) @@ -66,8 +73,10 @@ def PrependENVPath(self, key, value) -> None: else: self['ENV'][key] = os.path.pathsep.join([value, current]) + class SysPrefixes: - """Used to temporarily mock sys.prefix, sys.real_prefix and sys.base_prefix""" + """Context manager to mock/restore sys.{prefix,real_prefix.base_prefix}.""" + def __init__(self, prefix, real_prefix=None, base_prefix=None) -> None: self._prefix = prefix self._real_prefix = real_prefix @@ -83,7 +92,10 @@ def start(self) -> None: sys.real_prefix = self._real_prefix if self._base_prefix is None: if hasattr(sys, 'base_prefix'): - del sys.base_prefix + # Since 3.3, python always sets base_prefix. We used to + # delete it, but now we need it to behave like Python: + # if not pretending to be in a venv, should match sys.prefix. + sys.base_prefix = sys.prefix else: sys.base_prefix = self._base_prefix @@ -117,77 +129,104 @@ def _restore(self) -> None: sys.prefix = s['prefix'] del self._stored + def _p(p): - """Converts path string **p** from posix format to os-specific format.""" + """Convert path string *p* from posix format to os-specific format.""" drive = [] if p.startswith('/') and sys.platform == 'win32': - drive = ['C:'] + drive = ['C:'] pieces = p.split('/') return os.path.sep.join(drive + pieces) class _is_path_in_TestCase(unittest.TestCase): def test_false(self) -> None: - for args in [ ('',''), - ('', _p('/foo/bar')), - (_p('/foo/bar'), ''), - (_p('/foo/bar'), _p('/foo/bar')), - (_p('/foo/bar'), _p('/foo/bar/geez')), - (_p('/'), _p('/foo')), - (_p('foo'), _p('foo/bar')) ]: - assert SCons.Platform.virtualenv._is_path_in(*args) is False, "_is_path_in(%r, %r) should be False" % args + for args in [ + ('', ''), + ('', _p('/foo/bar')), + (_p('/foo/bar'), ''), + (_p('/foo/bar'), _p('/foo/bar')), + (_p('/foo/bar'), _p('/foo/bar/geez')), + (_p('/'), _p('/foo')), + (_p('foo'), _p('foo/bar')), + ]: + with self.subTest(): + assert SCons.Platform.virtualenv._is_path_in(*args) is False, ( + "_is_path_in(%r, %r) should be False" % args + ) def test__true(self) -> None: - for args in [ (_p('/foo'), _p('/')), - (_p('/foo/bar'), _p('/foo')), - (_p('/foo/bar/geez'), _p('/foo/bar')), - (_p('/foo//bar//geez'), _p('/foo/bar')), - (_p('/foo/bar/geez'), _p('/foo//bar')), - (_p('/foo/bar/geez'), _p('//foo//bar')) ]: - assert SCons.Platform.virtualenv._is_path_in(*args) is True, "_is_path_in(%r, %r) should be True" % args + for args in [ + (_p('/foo'), _p('/')), + (_p('/foo/bar'), _p('/foo')), + (_p('/foo/bar/geez'), _p('/foo/bar')), + (_p('/foo//bar//geez'), _p('/foo/bar')), + (_p('/foo/bar/geez'), _p('/foo//bar')), + (_p('/foo/bar/geez'), _p('//foo//bar')), + ]: + with self.subTest(): + assert SCons.Platform.virtualenv._is_path_in(*args) is True, ( + "_is_path_in(%r, %r) should be True" % args + ) + class IsInVirtualenvTestCase(unittest.TestCase): def test_false(self) -> None: - # "without wirtualenv" - always false + # "without virtualenv" - always false with SysPrefixes(_p('/prefix')): - for p in [ _p(''), - _p('/foo'), - _p('/prefix'), - _p('/prefix/foo') ]: - assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p + for p in [_p(''), _p('/foo'), _p('/prefix'), _p('/prefix/foo')]: + with self.subTest(): + assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, ( + f"IsInVirtualenv({p!r}) should be False" + ) # "with virtualenv" with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')): - for p in [ _p(''), - _p('/real/prefix/foo'), - _p('/virtualenv/prefix'), - _p('/virtualenv/prefix/bar/..'), - _p('/virtualenv/prefix/bar/../../bleah'), - _p('/virtualenv/bleah') ]: - assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p + for p in [ + _p(''), + _p('/real/prefix/foo'), + _p('/virtualenv/prefix'), + _p('/virtualenv/prefix/bar/..'), + _p('/virtualenv/prefix/bar/../../bleah'), + _p('/virtualenv/bleah'), + ]: + with self.subTest(): + assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, ( + f"IsInVirtualenv({p!r}) should be False" + ) # "with venv" with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')): - for p in [ _p(''), - _p('/base/prefix/foo'), - _p('/virtualenv/prefix'), - _p('/virtualenv/prefix/bar/..'), - _p('/virtualenv/prefix/bar/../../bleah'), - _p('/virtualenv/bleah') ]: - assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, "IsInVirtualenv(%r) should be False" % p + for p in [ + _p(''), + _p('/base/prefix/foo'), + _p('/virtualenv/prefix'), + _p('/virtualenv/prefix/bar/..'), + _p('/virtualenv/prefix/bar/../../bleah'), + _p('/virtualenv/bleah'), + ]: + with self.subTest(): + assert SCons.Platform.virtualenv.IsInVirtualenv(p) is False, ( + f"IsInVirtualenv({p!r}) should be False" + ) def test_true(self) -> None: # "with virtualenv" with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')): - for p in [ _p('/virtualenv/prefix/foo'), - _p('/virtualenv/prefix/foo/bar') ]: - assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, "IsInVirtualenv(%r) should be True" % p + for p in [_p('/virtualenv/prefix/foo'), _p('/virtualenv/prefix/foo/bar')]: + with self.subTest(): + assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, ( + f"IsInVirtualenv({p!r}) should be True" + ) # "with venv" with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')): - for p in [ _p('/virtualenv/prefix/foo'), - _p('/virtualenv/prefix/foo/bar') ]: - assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, "IsInVirtualenv(%r) should be True" % p + for p in [_p('/virtualenv/prefix/foo'), _p('/virtualenv/prefix/foo/bar')]: + with self.subTest(): + assert SCons.Platform.virtualenv.IsInVirtualenv(p) is True, ( + f"IsInVirtualenv({p!r}) should be True" + ) + class _inject_venv_pathTestCase(unittest.TestCase): def path_list(self): @@ -197,46 +236,67 @@ def path_list(self): _p('/virtualenv/prefix/../bar'), _p('/home/user/.local/bin'), _p('/usr/bin'), - _p('/opt/bin') + _p('/opt/bin'), ] + def test_with_path_string(self) -> None: env = Environment() path_string = os.path.pathsep.join(self.path_list()) - with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')): + with self.subTest(), SysPrefixes( + _p('/virtualenv/prefix'), real_prefix=_p('/real/prefix') + ): SCons.Platform.virtualenv._inject_venv_path(env, path_string) - assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV']['PATH'] + assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV'][ + 'PATH' + ] def test_with_path_list(self) -> None: env = Environment() - with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')): + with self.subTest(), SysPrefixes( + _p('/virtualenv/prefix'), real_prefix=_p('/real/prefix') + ): SCons.Platform.virtualenv._inject_venv_path(env, self.path_list()) - assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV']['PATH'] + assert env['ENV']['PATH'] == _p('/virtualenv/prefix/bin'), env['ENV'][ + 'PATH' + ] + class VirtualenvTestCase(unittest.TestCase): - def test_none(self) -> None: + """Test the Virtualenv() function.""" + + def test_no_venv(self) -> None: def _msg(given) -> str: - return "Virtualenv() should be None, not %s" % repr(given) + return f"Virtualenv() should be empty, not {given!r}" - with SysPrefixes(_p('/prefix')): + with self.subTest(), SysPrefixes(_p('/prefix')): ve = SCons.Platform.virtualenv.Virtualenv() - assert ve is None , _msg(ve) - with SysPrefixes(_p('/base/prefix'), base_prefix=_p('/base/prefix')): + self.assertEqual(ve, "", msg=_msg(ve)) + + with self.subTest(), SysPrefixes( + _p('/base/prefix'), base_prefix=_p('/base/prefix') + ): ve = SCons.Platform.virtualenv.Virtualenv() - assert ve is None, _msg(ve) + self.assertEqual(ve, "", msg=_msg(ve)) - def test_not_none(self) -> None: + def test_virtualenv(self) -> None: def _msg(expected, given) -> str: - return "Virtualenv() should == %r, not %s" % (_p(expected), repr(given)) + return f"Virtualenv() should == {_p(expected)!r}, not {given!r}" - with SysPrefixes(_p('/virtualenv/prefix'), real_prefix=_p('/real/prefix')): + with self.subTest(), SysPrefixes( + _p('/virtualenv/prefix'), real_prefix=_p('/real/prefix') + ): ve = SCons.Platform.virtualenv.Virtualenv() assert ve == _p('/virtualenv/prefix'), _msg('/virtualenv/prefix', ve) - with SysPrefixes(_p('/same/prefix'), real_prefix=_p('/same/prefix')): + + with self.subTest(), SysPrefixes( + _p('/same/prefix'), real_prefix=_p('/same/prefix') + ): ve = SCons.Platform.virtualenv.Virtualenv() - assert ve == _p('/same/prefix'), _msg('/same/prefix', ve) + assert ve == _p('/same/prefix'), _msg('/same/prefix', ve) + with SysPrefixes(_p('/virtualenv/prefix'), base_prefix=_p('/base/prefix')): ve = SCons.Platform.virtualenv.Virtualenv() - assert ve == _p('/virtualenv/prefix'), _msg('/virtualenv/prefix', ve) + assert ve == _p('/virtualenv/prefix'), _msg('/virtualenv/prefix', ve) if __name__ == "__main__": diff --git a/SCons/Platform/win32.py b/SCons/Platform/win32.py index 1779b03649..f1659f5594 100644 --- a/SCons/Platform/win32.py +++ b/SCons/Platform/win32.py @@ -167,7 +167,7 @@ def piped_spawn(sh, escape, cmd, args, env, stdout, stderr): try: with open(tmpFileStdoutName, "rb") as tmpFileStdout: output = tmpFileStdout.read() - stdout.write(output.decode(stdout.encoding, "replace")) + stdout.write(output.decode('oem', "replace").replace("\r\n", "\n")) os.remove(tmpFileStdoutName) except OSError: pass @@ -176,7 +176,7 @@ def piped_spawn(sh, escape, cmd, args, env, stdout, stderr): try: with open(tmpFileStderrName, "rb") as tmpFileStderr: errors = tmpFileStderr.read() - stderr.write(errors.decode(stderr.encoding, "replace")) + stderr.write(errors.decode('oem', "replace").replace("\r\n", "\n")) os.remove(tmpFileStderrName) except OSError: pass diff --git a/SCons/Platform/win32.xml b/SCons/Platform/win32.xml index 3000e332f6..41c64eeaa2 100644 --- a/SCons/Platform/win32.xml +++ b/SCons/Platform/win32.xml @@ -1,9 +1,10 @@ Tuple[bool, bool, bool]: + def collect_node_states(self) -> tuple[bool, bool, bool]: # returns (is_up_to_date, cached_error, cachable) # where is_up_to_date is True if the node(s) are up_to_date # cached_error is True if the node(s) are up_to_date, but the @@ -1100,12 +1101,17 @@ def CheckCXXHeader(context, header, include_quotes: str = '""'): def CheckLib(context, library = None, symbol: str = "main", - header = None, language = None, autoadd: bool=True, - append: bool=True, unique: bool=False) -> bool: + header = None, language = None, extra_libs = None, + autoadd: bool=True, append: bool=True, unique: bool=False) -> bool: """ - A test for a library. See also CheckLibWithHeader. + A test for a library. See also :func:`CheckLibWithHeader`. Note that library may also be None to test whether the given symbol compiles without flags. + + .. versionchanged:: 4.9.0 + Added the *extra_libs* keyword parameter. The actual implementation + is in :func:`SCons.Conftest.CheckLib` which already accepted this + parameter, so this is only exposing existing functionality. """ if not library: @@ -1115,9 +1121,9 @@ def CheckLib(context, library = None, symbol: str = "main", library = [library] # ToDo: accept path for the library - res = SCons.Conftest.CheckLib(context, library, symbol, header = header, - language = language, autoadd = autoadd, - append=append, unique=unique) + res = SCons.Conftest.CheckLib(context, library, symbol, header=header, + language=language, extra_libs=extra_libs, + autoadd=autoadd, append=append, unique=unique) context.did_show_result = True return not res @@ -1125,15 +1131,21 @@ def CheckLib(context, library = None, symbol: str = "main", # Bram: Can only include one header and can't use #ifdef HAVE_HEADER_H. def CheckLibWithHeader(context, libs, header, language, - call = None, autoadd: bool=True, append: bool=True, unique: bool=False) -> bool: - # ToDo: accept path for library. Support system header files. + extra_libs = None, call = None, autoadd: bool=True, + append: bool=True, unique: bool=False) -> bool: """ Another (more sophisticated) test for a library. Checks, if library and header is available for language (may be 'C' or 'CXX'). Call maybe be a valid expression _with_ a trailing ';'. - As in CheckLib, we support library=None, to test if the call compiles + As in :func:`CheckLib`, we support library=None, to test if the call compiles without extra link flags. + + .. versionchanged:: 4.9.0 + Added the *extra_libs* keyword parameter. The actual implementation + is in :func:`SCons.Conftest.CheckLib` which already accepted this + parameter, so this is only exposing existing functionality. """ + # ToDo: accept path for library. Support system header files. prog_prefix, dummy = createIncludesFromHeaders(header, 0) if not libs: libs = [None] @@ -1142,8 +1154,8 @@ def CheckLibWithHeader(context, libs, header, language, libs = [libs] res = SCons.Conftest.CheckLib(context, libs, None, prog_prefix, - call = call, language = language, autoadd=autoadd, - append=append, unique=unique) + extra_libs = extra_libs, call = call, language = language, + autoadd=autoadd, append=append, unique=unique) context.did_show_result = 1 return not res diff --git a/SCons/SConfTests.py b/SCons/SConfTests.py index 520b02ff09..4a3c8b25dd 100644 --- a/SCons/SConfTests.py +++ b/SCons/SConfTests.py @@ -295,7 +295,7 @@ def test_TryAction(self) -> None: """Test SConf.TryAction """ def actionOK(target, source, env): - with open(str(target[0]), "w") as f: + with open(target[0], "w") as f: f.write("RUN OK\n") return None def actionFAIL(target, source, env) -> int: diff --git a/SCons/Scanner/C.py b/SCons/Scanner/C.py index aafe0d9a56..bfd897dcb1 100644 --- a/SCons/Scanner/C.py +++ b/SCons/Scanner/C.py @@ -28,6 +28,8 @@ add_scanner() for each affected suffix. """ +from typing import Dict + import SCons.Node.FS import SCons.cpp import SCons.Util @@ -65,32 +67,85 @@ def read_file(self, file) -> str: self.missing.append((file, self.current_file)) return '' -def dictify_CPPDEFINES(env) -> dict: - """Returns CPPDEFINES converted to a dict. - - This should be similar to :func:`~SCons.Defaults.processDefines`. - Unfortunately, we can't do the simple thing of calling that routine and - passing the result to the dict() constructor, because it turns the defines - into a list of "name=value" pairs, which the dict constructor won't - consume correctly. Also cannot just call dict on CPPDEFINES itself - it's - fine if it's stored in the converted form (currently deque of tuples), but - CPPDEFINES could be in other formats too. - - So we have to do all the work here - keep concepts in sync with - ``processDefines``. +def dictify_CPPDEFINES(env, replace: bool = False) -> dict: + """Return CPPDEFINES converted to a dict for preprocessor emulation. + + The concept is similar to :func:`~SCons.Defaults.processDefines`: + turn the values stored in an internal form in ``env['CPPDEFINES']`` + into one needed for a specific context - in this case the cpp-like + work the C/C++ scanner will do. We can't reuse ``processDefines`` + output as that's a list of strings for the command line. We also can't + pass the ``CPPDEFINES`` variable directly to the ``dict`` constructor, + as SCons allows it to be stored in several different ways - it's only + after ``Append`` and relatives has been called we know for sure it will + be a deque of tuples. + + If requested (*replace* is true), simulate some of the macro + replacement that would take place if an actual preprocessor ran, + to avoid some conditional inclusions comeing out wrong. A bit + of an edge case, but does happen (GH #4623). See 6.10.5 in the C + standard and 15.6 in the C++ standard). + + Args: + replace: if true, simulate macro replacement + + .. versionchanged:: 4.9.0 + Simple macro replacement added, and *replace* arg to enable it. """ + def _replace(mapping: Dict) -> Dict: + """Simplistic macro replacer for dictify_CPPDEFINES. + + Scan *mapping* for a value that is the same as a key in the dict, + and replace with the value of that key; the process is repeated a few + times, but not forever in case someone left a case that can't be + fully resolved. This is a cheap approximation of the preprocessor's + macro replacement rules with no smarts - it doesn't "look inside" + the values, so only triggers on object-like macros, not on + function-like macros, and will not work on complex values, e.g. + a value like ``(1UL << PR_MTE_TCF_SHIFT)`` would not have + ``PR_MTE_TCF_SHIFT`` replaced if that was also a key in ``CPPDEFINES``. + + Args: + mapping: a dictionary representing macro names and replacements. + + Returns: + a dictionary with replacements made. + """ + old_ns = mapping + loops = 0 + while loops < 5: # don't recurse forever in case there's circular data + # this was originally written as a dict comprehension, but unrolling + # lets us add a finer-grained check for whether another loop is + # needed, rather than comparing two dicts to see if one changed. + again = False + ns = {} + for k, v in old_ns.items(): + if v in old_ns: + ns[k] = old_ns[v] + if not again and ns[k] != v: + again = True + else: + ns[k] = v + if not again: + break + old_ns = ns + loops += 1 + return ns + cppdefines = env.get('CPPDEFINES', {}) - result = {} - if cppdefines is None: - return result + if not cppdefines: + return {} if SCons.Util.is_Tuple(cppdefines): + # single macro defined in a tuple try: return {cppdefines[0]: cppdefines[1]} except IndexError: return {cppdefines[0]: None} if SCons.Util.is_Sequence(cppdefines): + # multiple (presumably) macro defines in a deque, list, etc. + result = {} for c in cppdefines: if SCons.Util.is_Sequence(c): try: @@ -107,9 +162,12 @@ def dictify_CPPDEFINES(env) -> dict: else: # don't really know what to do here result[c] = None - return result + if replace: + return _replace(result) + return(result) if SCons.Util.is_String(cppdefines): + # single macro define in a string try: name, value = cppdefines.split('=') return {name: value} @@ -117,6 +175,9 @@ def dictify_CPPDEFINES(env) -> dict: return {cppdefines: None} if SCons.Util.is_Dict(cppdefines): + # already in the desired form + if replace: + return _replace(cppdefines) return cppdefines return {cppdefines: None} @@ -136,7 +197,9 @@ def __init__(self, name, variable) -> None: def __call__(self, node, env, path=()): cpp = SConsCPPScanner( - current=node.get_dir(), cpppath=path, dict=dictify_CPPDEFINES(env) + current=node.get_dir(), + cpppath=path, + dict=dictify_CPPDEFINES(env, replace=True), ) result = cpp(node) for included, includer in cpp.missing: @@ -149,6 +212,7 @@ def __call__(self, node, env, path=()): def recurse_nodes(self, nodes): return nodes + def select(self, node): return self diff --git a/SCons/Scanner/CTests.py b/SCons/Scanner/CTests.py index 6860a10cef..b0fdb566e2 100644 --- a/SCons/Scanner/CTests.py +++ b/SCons/Scanner/CTests.py @@ -572,6 +572,18 @@ def runTest(self) -> None: expect = {"STRING": "VALUE", "UNVALUED": None} self.assertEqual(d, expect) + with self.subTest("CPPDEFINES with macro replacement"): + env = DummyEnvironment( + CPPDEFINES=[ + ("STRING", "VALUE"), + ("REPLACEABLE", "RVALUE"), + ("RVALUE", "AVALUE"), + ] + ) + d = SCons.Scanner.C.dictify_CPPDEFINES(env, replace=True) + expect = {"STRING": "VALUE", "REPLACEABLE": "AVALUE", "RVALUE": "AVALUE"} + self.assertEqual(d, expect) + if __name__ == "__main__": unittest.main() diff --git a/SCons/Scanner/LaTeX.py b/SCons/Scanner/LaTeX.py index 4412aee64a..77331606c1 100644 --- a/SCons/Scanner/LaTeX.py +++ b/SCons/Scanner/LaTeX.py @@ -169,6 +169,11 @@ class LaTeX(ScannerBase): 'addsectionbib': 'BIBINPUTS', 'makeindex': 'INDEXSTYLE', 'usepackage': 'TEXINPUTS', + 'usetheme': 'TEXINPUTS', + 'usecolortheme': 'TEXINPUTS', + 'usefonttheme': 'TEXINPUTS', + 'useinnertheme': 'TEXINPUTS', + 'useoutertheme': 'TEXINPUTS', 'lstinputlisting': 'TEXINPUTS'} env_variables = SCons.Util.unique(list(keyword_paths.values())) two_arg_commands = ['import', 'subimport', @@ -193,6 +198,7 @@ def __init__(self, name, suffixes, graphics_extensions, *args, **kwargs) -> None | addglobalbib | addsectionbib | usepackage + | use(?:|color|font|inner|outer)theme(?:\s*\[[^\]]+\])? ) \s*{([^}]*)} # first arg (?: \s*{([^}]*)} )? # maybe another arg @@ -309,7 +315,8 @@ def find_include(self, include, source_dir, path): for n in try_names: for search_path in search_paths: - paths = tuple([d.Dir(inc_subdir) for d in search_path]) + paths = tuple([d.Dir(inc_subdir) for d in search_path] + + list(search_path)) i = SCons.Node.FS.find_file(n, paths) if i: return i, include @@ -362,6 +369,9 @@ def scan(self, node, subdir: str='.'): if inc_type in self.two_arg_commands: inc_subdir = os.path.join(subdir, include[1]) inc_list = include[2].split(',') + elif re.match('use(|color|font|inner|outer)theme', inc_type): + inc_list = [re.sub('use', 'beamer', inc_type) + _ + '.sty' for _ in + include[1].split(',')] else: inc_list = include[1].split(',') for inc in inc_list: @@ -398,20 +408,20 @@ def scan_recurse(self, node, path=()): include = queue.pop() inc_type, inc_subdir, inc_filename = include - try: - if seen[inc_filename]: - continue - except KeyError: - seen[inc_filename] = True - # # Handle multiple filenames in include[1] # n, i = self.find_include(include, source_dir, path_dict) + try: + if seen[str(n)]: + continue + except KeyError: + seen[str(n)] = True + if n is None: # Do not bother with 'usepackage' warnings, as they most # likely refer to system-level files - if inc_type != 'usepackage': + if inc_type != 'usepackage' or re.match("use(|color|font|inner|outer)theme", inc_type): SCons.Warnings.warn( SCons.Warnings.DependencyWarning, "No dependency generated for file: %s " @@ -423,7 +433,9 @@ def scan_recurse(self, node, path=()): # recurse down queue.extend(self.scan(n, inc_subdir)) - return [pair[1] for pair in sorted(nodes)] + # Don't sort on a tuple where the second element is an object, just + # use the first element of the tuple which is the "sort_key" value + return [pair[1] for pair in sorted(nodes, key=lambda n: n[0])] # Local Variables: # tab-width:4 diff --git a/SCons/Scanner/LaTeXTests.py b/SCons/Scanner/LaTeXTests.py index ae3ae6659c..2cf47dc3a2 100644 --- a/SCons/Scanner/LaTeXTests.py +++ b/SCons/Scanner/LaTeXTests.py @@ -23,6 +23,7 @@ import collections import os +import sys import unittest import TestCmd @@ -63,6 +64,24 @@ \only<2>{\includegraphics{inc7.png}} """) +test.write('test5.latex',r""" +\usetheme{scons} +""") +test.write('beamerthemescons.sty',r""" +\usecolortheme[option]{scons} +\usefonttheme{scons} +\useinnertheme{scons} +\useoutertheme{scons} +""") +for theme in ('color', 'font', 'inner', 'outer'): + test.write('beamer' + theme + 'themescons.sty', "\n") + +test.write('test6.latex',r""" +\include{inc1} +\subimport{subdir}{inc3} +\subimport{subdir2}{inc3} +""") + test.subdir('subdir') test.write('inc1.tex',"\n") @@ -78,6 +97,10 @@ test.write('inc7.png', "\n") test.write('incNO.tex', "\n") +test.subdir('subdir2') + +test.write(['subdir2', 'inc3.tex'], "\\input{inc2}\n") + # define some helpers: # copied from CTest.py class DummyEnvironment(collections.UserDict): @@ -167,6 +190,33 @@ def runTest(self) -> None: files = ['inc1.tex', 'inc2.tex', 'inc5.xyz', 'inc7.png'] deps_match(self, deps, files) +class LaTeXScannerTestCase5(unittest.TestCase): + def runTest(self) -> None: + env = DummyEnvironment(TEXINPUTS=[test.workpath("subdir")],LATEXSUFFIXES = [".tex", ".ltx", ".latex"]) + s = SCons.Scanner.LaTeX.LaTeXScanner() + path = s.path(env) + deps = s(env.File('test5.latex'), env, path) + files = ['beamer' + _ + 'themescons.sty' for _ in + ('color', 'font', 'inner', 'outer', '')] + deps_match(self, deps, files) + +class LaTeXScannerTestCase5(unittest.TestCase): + def runTest(self) -> None: + env = DummyEnvironment(TEXINPUTS=[test.workpath("subdir")],LATEXSUFFIXES = [".tex", ".ltx", ".latex"]) + s = SCons.Scanner.LaTeX.LaTeXScanner() + path = s.path(env) + deps = s(env.File('test6.latex'), env, path) + files = ['inc1.tex', 'inc2.tex', 'subdir/inc3.tex', 'subdir2/inc3.tex'] + + # on windows the paths used to sort are all caps and use backslash + # due to this only on windows subdir2 < subdir\, so this is the expected order + files_win = ['inc1.tex', 'inc2.tex', 'subdir2/inc3.tex', 'subdir/inc3.tex'] + + if sys.platform == 'win32': + deps_match(self, deps, files_win) + else: + deps_match(self, deps, files) + if __name__ == "__main__": unittest.main() diff --git a/SCons/Scanner/Scanner.xml b/SCons/Scanner/Scanner.xml index c9b7f3215f..e3dffe526b 100644 --- a/SCons/Scanner/Scanner.xml +++ b/SCons/Scanner/Scanner.xml @@ -1,9 +1,10 @@ None: ProgressObject = SCons.Util.Null() def Progress(*args, **kw) -> None: + """Show progress during building - Public API.""" global ProgressObject ProgressObject = Progressor(*args, **kw) @@ -457,19 +465,28 @@ def __init__(self, derived: bool=False, prune: bool=False, status: bool=False, s self.prune = prune self.status = status self.sLineDraw = sLineDraw + def get_all_children(self, node): return node.all_children() + def get_derived_children(self, node): children = node.all_children(None) return [x for x in children if x.has_builder()] + def display(self, t) -> None: if self.derived: func = self.get_derived_children else: func = self.get_all_children - s = self.status and 2 or 0 - SCons.Util.print_tree(t, func, prune=self.prune, showtags=s, lastChild=True, singleLineDraw=self.sLineDraw) - + s = 2 if self.status else 0 + SCons.Util.print_tree( + t, + func, + prune=self.prune, + showtags=s, + lastChild=True, + singleLineDraw=self.sLineDraw, + ) def python_version_string(): return sys.version.split()[0] @@ -501,29 +518,59 @@ def __getattr__(self, attr): # TODO: to quiet checkers, FakeOptionParser should also define # raise_exception_on_error, preserve_unknown_options, largs and parse_args - def add_local_option(self, *args, **kw) -> None: + def add_local_option(self, *args, **kw) -> SConsOption: pass OptionsParser = FakeOptionParser() -def AddOption(*args, **kw): +def AddOption(*args, **kw) -> SConsOption: + """Add a local option to the option parser - Public API. + + If the SCons-specific *settable* kwarg is true (default ``False``), + the option will allow calling :func:`SetOption`. + + .. versionchanged:: 4.8.0 + The *settable* parameter added to allow including the new option + in the table of options eligible to use :func:`SetOption`. + """ + settable = kw.get('settable', False) + if len(args) == 1 and isinstance(args[0], SConsOption): + # If they passed an SConsOption object, ignore kw - the underlying + # add_option method relies on seeing zero kwargs to recognize this. + # Since we don't support an omitted default, overrwrite optparse's + # marker to get the same effect as setting it in kw otherwise. + optobj = args[0] + if optobj.default is optparse.NO_DEFAULT: + optobj.default = None + # make sure settable attribute exists; positive setting wins + attr_settable = getattr(optobj, "settable") + if attr_settable is None or settable > attr_settable: + optobj.settable = settable + return OptionsParser.add_local_option(*args) + if 'default' not in kw: kw['default'] = None - result = OptionsParser.add_local_option(*args, **kw) - return result + kw['settable'] = settable # just to make sure it gets set + return OptionsParser.add_local_option(*args, **kw) -def GetOption(name): +def GetOption(name: str): + """Get the value from an option - Public API.""" return getattr(OptionsParser.values, name) -def SetOption(name, value): +def SetOption(name: str, value): + """Set the value of an option - Public API.""" return OptionsParser.values.set_option(name, value) -def DebugOptions(json=None): - """ - API to allow specifying options to SCons debug logic - Currently only json is supported which changes the - json file written by --debug=json from the default +def DebugOptions(json: str | None = None) -> None: + """Specify options to SCons debug logic - Public API. + + Currently only *json* is supported, which changes the JSON file + written to if the ``--debug=json`` command-line option is specified + to the value supplied. + + .. versionadded:: 4.6.0 + """ if json is not None: json_node = SCons.Defaults.DefaultEnvironment().arg2nodes(json) @@ -540,7 +587,7 @@ def DebugOptions(json=None): raise SCons.Errors.UserError(f"Unable to create directory for JSON debug output file: {SCons.Util.stats.JSON_OUTPUT_FILE}") -def ValidateOptions(throw_exception: bool=False) -> None: +def ValidateOptions(throw_exception: bool = False) -> None: """Validate options passed to SCons on the command line. Checks that all options given on the command line are known to this @@ -644,8 +691,8 @@ def _scons_internal_error() -> None: sys.exit(2) def _SConstruct_exists( - dirname: str, repositories: List[str], filelist: List[str] -) -> Optional[str]: + dirname: str, repositories: list[str], filelist: list[str] +) -> str | None: """Check that an SConstruct file exists in a directory. Arguments: @@ -1319,12 +1366,6 @@ def order(dependencies): # various print_* settings, tree_printer list, etc. BuildTask.options = options - is_pypy = platform.python_implementation() == 'PyPy' - # As of 3.7, python removed support for threadless platforms. - # See https://www.python.org/dev/peps/pep-0011/ - is_37_or_later = sys.version_info >= (3, 7) - # python_has_threads = sysconfig.get_config_var('WITH_THREAD') or is_pypy or is_37_or_later - # As of python 3.4 threading has a dummy_threading module for use when there is no threading # it's get_ident() will allways return -1, while real threading modules get_ident() will # always return a positive integer @@ -1387,7 +1428,7 @@ def _exec_main(parser, values) -> None: class SConsPdb(pdb.Pdb): """Specialization of Pdb to help find SConscript files.""" - def lookupmodule(self, filename: str) -> Optional[str]: + def lookupmodule(self, filename: str) -> str | None: """Helper function for break/clear parsing -- SCons version. Translates (possibly incomplete) file or module name @@ -1398,6 +1439,10 @@ def lookupmodule(self, filename: str) -> Optional[str]: sconscript files that don't have the suffix. .. versionadded:: 4.6.0 + + .. versionchanged:: 4.8.0 + The additional name ``SCsub`` (with spelling variants) + is also recognized - Godot uses this name. """ if os.path.isabs(filename) and os.path.exists(filename): return filename diff --git a/SCons/Script/Main.xml b/SCons/Script/Main.xml index 36e7d305c5..810e8aae50 100644 --- a/SCons/Script/Main.xml +++ b/SCons/Script/Main.xml @@ -1,9 +1,10 @@ -(arguments) +(opt_str, ..., attr=value, ...) Adds a local (project-specific) command-line option. -arguments -are the same as those supported by the add_option -method in the standard Python library module optparse, -with a few additional capabilities noted below. -See the documentation for -optparse -for a thorough discussion of its option-processing capabities. +One or more opt_str values are +the strings representing how the option can be called, +while the keyword arguments define attributes of the option. +For the most part these are the same as for the +OptionParser.add_option +method in the standard Python library module +optparse, +but with a few additional capabilities noted below. +See the + +optparse documentation +for a thorough discussion of its option-processing capabilities. +All options added through &f-AddOption; are placed +in a special "Local Options" option group. @@ -48,10 +56,9 @@ method, &f-AddOption; allows setting the nargs keyword value to -a string consisting of a question mark -('?') +a string '?' (question mark) to indicate that the option argument for -that option string is optional. +that option string may be omitted. If the option string is present on the command line but has no matching option argument, the value of the @@ -93,21 +100,25 @@ the option value may be accessed using &f-link-GetOption; or &f-link-env-GetOption;. -&f-link-SetOption; is not currently supported for -options added with &f-AddOption;. - +override a value set in an SConscript file. + + + +Changed in 4.8.0: added the +settable keyword argument +to enable an added option to be settable via &SetOption;. @@ -189,13 +200,14 @@ Future versions of &SCons; will likely forbid such usage. -Allows setting options for SCons debug options. Currently the only supported value is - json which sets the path to the json file created when +Allows setting options for SCons debug options. Currently, the only supported value is + json which sets the path to the JSON file created when --debug=json is set. DebugOptions(json='#/build/output/scons_stats.json') +New in version 4.6.0. @@ -334,9 +346,10 @@ atexit.register(print_build_failures) Query the value of settable options which may have been set -on the command line, or by using the &f-link-SetOption; function. +on the command line, via option defaults, +or by using the &f-link-SetOption; function. The value of the option is returned in a type matching how the -option was declared - see the documentation for the +option was declared - see the documentation of the corresponding command line option for information about each specific option. @@ -687,7 +700,7 @@ If the string contains the verbatim substring it will be replaced with the Node. Note that, for performance reasons, this is not -a regular SCons variable substition, +a regular SCons variable substitution, so you can not use other variables or use curly braces. The following example will print the name of @@ -801,6 +814,16 @@ are not settable using &f-SetOption; since those files must be read in order to find the &f-SetOption; call in the first place. + +For project-specific options (sometimes called +local options) +added via an &f-link-AddOption; call, +&f-SetOption; is available only after the +&f-AddOption; call has completed successfully, +and only if that call included the +settable=True argument. + + The settable variables with their associated command-line options are: diff --git a/SCons/Script/SConsOptions.py b/SCons/Script/SConsOptions.py index e8e5cbf4e9..2690a086c9 100644 --- a/SCons/Script/SConsOptions.py +++ b/SCons/Script/SConsOptions.py @@ -21,6 +21,8 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations + import gettext import optparse import re @@ -66,17 +68,14 @@ def diskcheck_convert(value): class SConsValues(optparse.Values): - """ - Holder class for uniform access to SCons options, regardless - of whether they can be set on the command line or in the - SConscript files (using the SetOption() function). + """Holder class for uniform access to SCons options. A SCons option value can originate three different ways: - 1) set on the command line; - 2) set in an SConscript file; - 3) the default setting (from the the op.add_option() - calls in the Parser() function, below). + 1. set on the command line. + 2. set in an SConscript file via :func:`~SCons.Script.Main.SetOption`. + 3. the default setting (from the the ``op.add_option()`` + calls in the :func:`Parser` function. The command line always overrides a value set in a SConscript file, which in turn always overrides default settings. Because we want @@ -87,15 +86,15 @@ class SConsValues(optparse.Values): The solution implemented in this class is to keep these different sets of settings separate (command line, SConscript file, and default) - and to override the __getattr__() method to check them in turn. - This should allow the rest of the code to just fetch values as - attributes of an instance of this class, without having to worry - about where they came from. + and to override the :meth:`__getattr__` method to check them in turn. + This allows the rest of the code to just fetch values as attributes of + an instance of this class, without having to worry about where they + came from (the scheme is similar to a ``ChainMap``). Note that not all command line options are settable from SConscript files, and the ones that are must be explicitly added to the - "settable" list in this class, and optionally validated and coerced - in the set_option() method. + :attr:`settable` list in this class, and optionally validated and coerced + in the :meth:`set_option` method. """ def __init__(self, defaults) -> None: @@ -103,28 +102,24 @@ def __init__(self, defaults) -> None: self.__SConscript_settings__ = {} def __getattr__(self, attr): - """ - Fetches an options value, checking first for explicit settings - from the command line (which are direct attributes), then the - SConscript file settings, then the default values. + """Fetch an options value, respecting priority rules. + + This is a little tricky: since we're answering questions + about outselves, we have avoid lookups that would send us into + into infinite recursion, thus the ``__dict__`` stuff. """ try: - return self.__dict__[attr] + return self.__dict__[attr] except KeyError: try: return self.__dict__['__SConscript_settings__'][attr] except KeyError: try: return getattr(self.__dict__['__defaults__'], attr) - except KeyError: - # Added because with py3 this is a new class, - # not a classic class, and due to the way - # In that case it will create an object without - # __defaults__, and then query for __setstate__ - # which will throw an exception of KeyError - # deepcopy() is expecting AttributeError if __setstate__ - # is not available. - raise AttributeError(attr) + except KeyError as exc: + # Need to respond with AttributeError because + # deepcopy expects that if __setstate__ is not available. + raise AttributeError(attr) from exc # keep this list in sync with the SetOption doc in SCons/Script/Main.xml # search for UPDATE_SETOPTION_DOCS there. @@ -148,26 +143,24 @@ def __getattr__(self, attr): 'silent', 'stack_size', 'warn', - - # TODO: Remove these once we update the AddOption() API to allow setting - # added flag as settable. - # Requested settable flag in : https://github.com/SCons/scons/issues/3983 - # From experimental ninja - 'disable_execute_ninja', - 'disable_ninja', - 'skip_ninja_regen' ] - def set_option(self, name, value): - """Sets an option from an SConscript file. + def set_option(self, name: str, value) -> None: + """Set an option value from a :func:`~SCons.Script.Main.SetOption` call. + + Validation steps for settable options (those defined in SCons + itself) are in-line here. Duplicates the logic for the matching + command-line options in :func:`Parse` - these need to be kept + in sync. Cannot provide validation for options added via + :func:`~SCons.Script.Main.AddOption` since we don't know about those + ahead of time - it is up to the developer to figure that out. Raises: - UserError: invalid or malformed option ("error in your script") + UserError: the option is not settable. """ - if name not in self.settable: raise SCons.Errors.UserError( - "This option is not settable from a SConscript file: %s" % name + f"This option is not settable from an SConscript file: {name!r}" ) # the following are for options that need some extra processing @@ -240,14 +233,37 @@ def set_option(self, name, value): class SConsOption(optparse.Option): - def convert_value(self, opt, value): + """SCons added option. + + Changes :attr:`CHECK_METHODS` and :attr:`CONST_ACTIONS` settings from + :class:`optparse.Option` base class to tune for our usage. + + New function :meth:`_check_nargs_optional` implements the ``nargs=?`` + syntax from :mod:`argparse`, and is added to the ``CHECK_METHODS`` list. + Overridden :meth:`convert_value` supports this usage. + + .. versionchanged:: 4.9.0 + The *settable* attribute is added to ``ATTRS``, allowing it to be + set in the option. A parameter to mark the option settable was added + in 4.8.0, but was not initially made part of the option object itself. + """ + # can uncomment to have a place to trap SConsOption creation for debugging: + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + + def convert_value(self, opt: str, value): + """SCons override: recognize nargs="?".""" if value is not None: if self.nargs in (1, '?'): return self.check_value(opt, value) return tuple([self.check_value(opt, v) for v in value]) def process(self, opt, value, values, parser): + """Process a value. + Direct copy of optparse version including the comments - + we don't change anything so this could just be dropped. + """ # First, convert the value(s) to the right type. Howl if any # value(s) are bogus. value = self.convert_value(opt, value) @@ -258,15 +274,17 @@ def process(self, opt, value, values, parser): return self.take_action( self.action, self.dest, opt, value, values, parser) - def _check_nargs_optional(self): + def _check_nargs_optional(self) -> None: + """SCons added: deal with optional option-arguments.""" if self.nargs == '?' and self._short_opts: fmt = "option %s: nargs='?' is incompatible with short options" raise SCons.Errors.UserError(fmt % self._short_opts[0]) + ATTRS = optparse.Option.ATTRS + ['settable'] # added for SCons CHECK_METHODS = optparse.Option.CHECK_METHODS if CHECK_METHODS is None: CHECK_METHODS = [] - CHECK_METHODS = CHECK_METHODS + [_check_nargs_optional] + CHECK_METHODS += [_check_nargs_optional] # added for SCons CONST_ACTIONS = optparse.Option.CONST_ACTIONS + optparse.Option.TYPED_ACTIONS @@ -278,8 +296,8 @@ class SConsOptionGroup(optparse.OptionGroup): lined up with the normal "SCons Options". """ - def format_help(self, formatter): - """ Format an option group's help text. + def format_help(self, formatter) -> str: + """SCons-specific formatting of an option group's help text. The title is dedented so it's flush with the "SCons Options" title we print at the top. @@ -293,14 +311,15 @@ def format_help(self, formatter): class SConsBadOptionError(optparse.BadOptionError): - """Exception used to indicate that invalid command line options were specified - - :ivar str opt_str: The offending option specified on command line which is not recognized - :ivar OptionParser parser: The active argument parser + """Raised if an invalid option value is encountered on the command line. + Attributes: + opt_str: The unrecognized command-line option. + parser: The active argument parser. """ + # TODO why is 'parser' needed? Not called in current code base. - def __init__(self, opt_str, parser=None) -> None: + def __init__(self, opt_str: str, parser: SConsOptionParser | None = None) -> None: self.opt_str = opt_str self.parser = parser @@ -312,10 +331,8 @@ class SConsOptionParser(optparse.OptionParser): preserve_unknown_options = False raise_exception_on_error = False - def error(self, msg): - """ - overridden OptionValueError exception handler - """ + def error(self, msg: str) -> None: + """SCons-specific handling of option errors.""" if self.raise_exception_on_error: raise SConsBadOptionError(msg, self) else: @@ -323,15 +340,16 @@ def error(self, msg): sys.stderr.write("SCons Error: %s\n" % msg) sys.exit(2) - def _process_long_opt(self, rargs, values): - """ SCons-specific processing of long options. + def _process_long_opt(self, rargs, values) -> None: + """SCons-specific processing of long options. - This is copied directly from the normal - ``optparse._process_long_opt()`` method, except that, if configured - to do so, we catch the exception thrown when an unknown option - is encountered and just stick it back on the "leftover" arguments - for later (re-)processing. This is because we may see the option - definition later, while processing SConscript files. + This is copied directly from the normal Optparse + :meth:`~optparse.OptionParser._process_long_opt` method, except + that, if configured to do so, we catch the exception thrown + when an unknown option is encountered and just stick it back + on the "leftover" arguments for later (re-)processing. This is + because we may see the option definition later, while processing + SConscript files. """ arg = rargs.pop(0) @@ -351,9 +369,9 @@ def _process_long_opt(self, rargs, values): % (opt, self._match_long_opt(opt)) ) except optparse.BadOptionError: + # SCons addition: if requested, add unknown options to + # the "leftover arguments" list for later processing. if self.preserve_unknown_options: - # SCons-specific: if requested, add unknown options to - # the "leftover arguments" list for later processing. self.largs.append(arg) if had_explicit_value: # The unknown option will be re-processed later, @@ -365,6 +383,7 @@ def _process_long_opt(self, rargs, values): option = self._long_opt[opt] if option.takes_value(): nargs = option.nargs + # SCons addition: recognize '?' for nargs if nargs == '?': if had_explicit_value: value = rargs.pop(0) @@ -372,6 +391,7 @@ def _process_long_opt(self, rargs, values): value = option.const elif len(rargs) < nargs: if nargs == 1: + # SCons addition: nicer msg if option had choices if not option.choices: self.error(_("%s option requires an argument") % opt) else: @@ -396,47 +416,105 @@ def _process_long_opt(self, rargs, values): option.process(opt, value, values, self) + + def _process_short_opts(self, rargs, values) -> None: + """SCons-specific processing of short options. + + This is copied directly from the normal Optparse + :meth:`~optparse.OptionParser._process_short_opts` method, except + that, if configured to do so, we catch the exception thrown + when an unknown option is encountered and just stick it back + on the "leftover" arguments for later (re-)processing. This is + because we may see the option definition later, while processing + SConscript files. + """ + arg = rargs.pop(0) + stop = False + i = 1 + for ch in arg[1:]: + opt = "-" + ch + option = self._short_opt.get(opt) + i += 1 # we have consumed a character + + try: + if not option: + raise optparse.BadOptionError(opt) + except optparse.BadOptionError: + # SCons addition: if requested, add unknown options to + # the "leftover arguments" list for later processing. + if self.preserve_unknown_options: + self.largs.append(arg) + return + raise + + if option.takes_value(): + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + rargs.insert(0, arg[i:]) + stop = True + + nargs = option.nargs + if len(rargs) < nargs: + if nargs == 1: + self.error(_("%s option requires an argument") % opt) + else: + self.error(_("%s option requires %d arguments") + % (opt, nargs)) + elif nargs == 1: + value = rargs.pop(0) + else: + value = tuple(rargs[0:nargs]) + del rargs[0:nargs] + + else: # option doesn't take a value + value = None + + option.process(opt, value, values, self) + + if stop: + break + + def reparse_local_options(self) -> None: - """ Re-parse the leftover command-line options. + """Re-parse the leftover command-line options. - Parse options stored in `self.largs`, so that any value + Leftover options are stored in ``self.largs``, so that any value overridden on the command line is immediately available - if the user turns around and does a :func:`GetOption` right away. + if the user turns around and does a :func:`~SCons.Script.Main.GetOption` + right away. We mimic the processing of the single args in the original OptionParser :func:`_process_args`, but here we allow exact matches for long-opts only (no partial argument names!). - Otherwise there could be problems in :func:`add_local_option` + Otherwise there could be problems in :meth:`add_local_option` below. When called from there, we try to reparse the - command-line arguments that - - 1. haven't been processed so far (`self.largs`), but - 2. are possibly not added to the list of options yet. + command-line arguments that haven't been processed so far + (``self.largs``), but are possibly not added to the options list yet. - So, when we only have a value for "--myargument" so far, - a command-line argument of "--myarg=test" would set it, + So, when we only have a value for ``--myargument`` so far, + a command-line argument of ``--myarg=test`` would set it, per the behaviour of :func:`_match_long_opt`, which allows for partial matches of the option name, as long as the common prefix appears to be unique. This would lead to further confusion, because we might want - to add another option "--myarg" later on (see issue #2929). - + to add another option ``--myarg`` later on (see issue #2929). """ rargs = [] largs_restore = [] # Loop over all remaining arguments skip = False - for l in self.largs: + for larg in self.largs: if skip: # Accept all remaining arguments as they are - largs_restore.append(l) + largs_restore.append(larg) else: - if len(l) > 2 and l[0:2] == "--": + if len(larg) > 2 and larg[0:2] == "--": # Check long option - lopt = (l,) - if "=" in l: + lopt = [larg] + if "=" in larg: # Split into option and value - lopt = l.split("=", 1) + lopt = larg.split("=", 1) if lopt[0] in self._long_opt: # Argument is already known @@ -445,34 +523,46 @@ def reparse_local_options(self) -> None: # Not known yet, so reject for now largs_restore.append('='.join(lopt)) else: - if l == "--" or l == "-": + if larg in("--", "-"): # Stop normal processing and don't # process the rest of the command-line opts - largs_restore.append(l) + largs_restore.append(larg) skip = True else: - rargs.append(l) + rargs.append(larg) # Parse the filtered list self.parse_args(rargs, self.values) - # Restore the list of remaining arguments for the + # Restore the list of leftover arguments for the # next call of AddOption/add_local_option... self.largs = self.largs + largs_restore - def add_local_option(self, *args, **kw): - """ Adds a local option to the parser. + def add_local_option(self, *args, **kw) -> SConsOption: + """Add a local option to the parser. + + This is the implementation of :func:`~SCons.Script.Main.AddOption`, + to add a project-defined command-line option. Local options + are added to a separate option group, which is created if necessary. - This is initiated by an :func:`AddOption` call to add a user-defined - command-line option. We add the option to a separate option - group for the local options, creating the group if necessary. + The keyword argument *settable* is recognized specially (and + removed from *kw*). If true, the option is marked as modifiable; + by default "local" (project-added) options are not eligible for + :func:`~SCons.Script.Main.SetOption` calls. + + .. versionchanged:: NEXT_VERSION + If the option's *settable* attribute is true, it is added to + the :attr:`SConsValues.settable` list. *settable* handling was + added in 4.8.0, but was not made an option attribute at the time. """ + group: SConsOptionGroup try: group = self.local_option_group except AttributeError: group = SConsOptionGroup(self, 'Local Options') - group = self.add_option_group(group) + self.add_option_group(group) self.local_option_group = group + # this gives us an SConsOption due to the setting of self.option_class result = group.add_option(*args, **kw) if result: # The option was added successfully. We now have to add the @@ -483,13 +573,16 @@ def add_local_option(self, *args, **kw): # any value overridden on the command line is immediately # available if the user turns around and does a GetOption() # right away. + # TODO: what if dest is None? setattr(self.values.__defaults__, result.dest, result.default) self.reparse_local_options() + if result.settable: + SConsValues.settable.append(result.dest) return result def format_local_option_help(self, formatter=None, file=None): - """Return the help for the project-level ("local") options. + """Return the help for the project-level ("local") SCons options. .. versionadded:: 4.6.0 """ @@ -512,7 +605,7 @@ def format_local_option_help(self, formatter=None, file=None): return local_help def print_local_option_help(self, file=None): - """Print help for just project-defined options. + """Print help for just local SCons options. Writes to *file* (default stdout). @@ -525,11 +618,11 @@ def print_local_option_help(self, file=None): class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter): def format_usage(self, usage) -> str: - """ Formats the usage message. """ + """Format the usage message for SCons.""" return "usage: %s\n" % usage def format_heading(self, heading): - """ Translates heading to "SCons Options" + """Translate heading to "SCons Options" Heading of "Options" changed to "SCons Options." Unfortunately, we have to do this here, because those titles @@ -540,11 +633,10 @@ def format_heading(self, heading): return super().format_heading(heading) def format_option(self, option): - """ Customized option formatter. + """SCons-specific option formatter. - A copy of the normal ``optparse.IndentedHelpFormatter.format_option()`` - method. This has been snarfed so we can modify text wrapping to - our liking: + A copy of the :meth:`optparse.IndentedHelpFormatter.format_option` + method. Overridden so we can modify text wrapping to our liking: * add our own regular expression that doesn't break on hyphens (so things like ``--no-print-directory`` don't get broken). @@ -554,21 +646,25 @@ def format_option(self, option): The help for each option consists of two parts: - * the opt strings and metavars e.g. ("-x", or - "-fFILENAME, --file=FILENAME") + * the opt strings and metavars e.g. (``-x``, or + ``-fFILENAME, --file=FILENAME``) * the user-supplied help string e.g. - ("turn on expert mode", "read data from FILENAME") + (``turn on expert mode``, ``read data from FILENAME``) If possible, we write both of these on the same line:: -x turn on expert mode - But if the opt string list is too long, we put the help + If the opt string list is too long, we put the help string on a second line, indented to the same column it would start in if it fit on the first line:: -fFILENAME, --file=FILENAME read data from FILENAME + + Help strings are wrapped for terminal width and do not preserve + any hand-made formatting that may have been used in the ``AddOption`` + call, so don't attempt prettying up a list of choices (for example). """ result = [] opts = self.option_strings[option] @@ -614,7 +710,8 @@ def store_local_option_strings(self, parser, group): """Local-only version of store_option_strings. We need to replicate this so the formatter will be set up - properly if we didn't go through the "normal" store_option_strings + properly if we didn't go through the "normal" + :math:`~optparse.HelpFormatter.store_option_strings`. .. versionadded:: 4.6.0 """ @@ -633,15 +730,18 @@ def Parser(version): """Returns a parser object initialized with the standard SCons options. Add options in the order we want them to show up in the ``-H`` help - text, basically alphabetical. Each ``op.add_option()`` call - should have a consistent format:: - - op.add_option("-L", "--long-option-name", - nargs=1, type="string", - dest="long_option_name", default='foo', - action="callback", callback=opt_long_option, - help="help text goes here", - metavar="VAR") + text, basically alphabetical. For readability, Each + :meth:`~optparse.OptionContainer.add_option` call should have a + consistent format:: + + op.add_option( + "-L", "--long-option-name", + nargs=1, type="string", + dest="long_option_name", default='foo', + action="callback", callback=opt_long_option, + help="help text goes here", + metavar="VAR" + ) Even though the :mod:`optparse` module constructs reasonable default destination names from the long option names, we're going to be diff --git a/SCons/Script/SConscript.py b/SCons/Script/SConscript.py index a2ef3b9d57..f98cf3b21a 100644 --- a/SCons/Script/SConscript.py +++ b/SCons/Script/SConscript.py @@ -23,6 +23,8 @@ """This module defines the Python API provided to SConscript files.""" +from __future__ import annotations + import SCons import SCons.Action import SCons.Builder @@ -45,7 +47,6 @@ import sys import traceback import time -from typing import Tuple class SConscriptReturn(Exception): pass @@ -386,7 +387,7 @@ class SConsEnvironment(SCons.Environment.Base): # Private methods of an SConsEnvironment. # @staticmethod - def _get_major_minor_revision(version_string: str) -> Tuple[int, int, int]: + def _get_major_minor_revision(version_string: str) -> tuple[int, int, int]: """Split a version string into major, minor and (optionally) revision parts. @@ -485,7 +486,7 @@ def Default(self, *targets) -> None: SCons.Script._Set_Default_Targets(self, targets) @staticmethod - def GetSConsVersion() -> Tuple[int, int, int]: + def GetSConsVersion() -> tuple[int, int, int]: """Return the current SCons version. .. versionadded:: 4.8.0 @@ -535,25 +536,27 @@ def GetOption(self, name): name = self.subst(name) return SCons.Script.Main.GetOption(name) - def Help(self, text, append: bool = False, keep_local: bool = False) -> None: + def Help(self, text, append: bool = False, local_only: bool = False) -> None: """Update the help text. The previous help text has *text* appended to it, except on the - first call. On first call, the values of *append* and *keep_local* + first call. On first call, the values of *append* and *local_only* are considered to determine what is appended to. Arguments: text: string to add to the help text. append: on first call, if true, keep the existing help text (default False). - keep_local: on first call, if true and *append* is also true, + local_only: on first call, if true and *append* is also true, keep only the help text from AddOption calls. .. versionchanged:: 4.6.0 The *keep_local* parameter was added. + .. versionchanged:: 4.9.0 + The *keep_local* parameter was renamed *local_only* to match manpage """ text = self.subst(text, raw=1) - SCons.Script.HelpFunction(text, append=append, keep_local=keep_local) + SCons.Script.HelpFunction(text, append=append, local_only=local_only) def Import(self, *vars): try: diff --git a/SCons/Script/SConscript.xml b/SCons/Script/SConscript.xml index a201c969e0..7ae0159dee 100644 --- a/SCons/Script/SConscript.xml +++ b/SCons/Script/SConscript.xml @@ -1,9 +1,10 @@ Returns the current SCons version in the form of a Tuple[int, int, int], representing the major, minor, and revision values respectively. -Added in 4.7.1. +Added in 4.8.0. @@ -363,7 +364,7 @@ Import("*") Return to the calling SConscript, optionally returning the values of variables named in vars. -Multiple strings contaning variable names may be passed to +Multiple strings containing variable names may be passed to &f-Return;. A string containing white space is split into individual variable names. Returns the value if one variable is specified, diff --git a/SCons/Script/ScriptTests.py b/SCons/Script/ScriptTests.py new file mode 100644 index 0000000000..334279115d --- /dev/null +++ b/SCons/Script/ScriptTests.py @@ -0,0 +1,181 @@ +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Unit tests of functionality from SCons.Script._init__.py. +# +# Most of the tests of this functionality are actually end-to-end scripts +# in the test/ hierarchy. +# +# This module is for specific bits of functionality that seem worth +# testing here - particularly if there's private data involved. + +import unittest + +from SCons.Script import ( + _Add_Arguments, + _Add_Targets, + _Remove_Argument, + _Remove_Target, + ARGLIST, + ARGUMENTS, + BUILD_TARGETS, + COMMAND_LINE_TARGETS, + _build_plus_default, +) + + +class TestScriptFunctions(unittest.TestCase): + def setUp(self): + # Clear global state before each test + ARGUMENTS.clear() + ARGLIST.clear() + del COMMAND_LINE_TARGETS[:] + del BUILD_TARGETS[:] + del _build_plus_default[:] + + def test_Add_Arguments(self): + test_args = ['foo=bar', 'spam=eggs'] + + _Add_Arguments(test_args) + self.assertEqual(ARGUMENTS, {'foo': 'bar', 'spam': 'eggs'}) + self.assertEqual(ARGLIST, [('foo', 'bar'), ('spam', 'eggs')]) + + def test_Add_Arguments_empty(self): + # Adding am empty argument is a no-op, with no error + _Add_Arguments([]) + self.assertEqual(ARGUMENTS, {}) + self.assertEqual(ARGLIST, []) + + def test_Add_Targets(self): + test_targets = ['target1', 'target2'] + _Add_Targets(test_targets) + + self.assertEqual(COMMAND_LINE_TARGETS, ['target1', 'target2']) + self.assertEqual(BUILD_TARGETS, ['target1', 'target2']) + self.assertEqual(_build_plus_default, ['target1', 'target2']) + + # Test that methods were replaced + self.assertEqual(BUILD_TARGETS._add_Default, BUILD_TARGETS._do_nothing) + self.assertEqual(BUILD_TARGETS._clear, BUILD_TARGETS._do_nothing) + self.assertEqual( + _build_plus_default._add_Default, _build_plus_default._do_nothing + ) + self.assertEqual( + _build_plus_default._clear, _build_plus_default._do_nothing + ) + + def test_Add_Targets_empty(self): + # Adding am empty argument is a no-op, with no error + _Add_Targets([]) + self.assertEqual(COMMAND_LINE_TARGETS, []) + self.assertEqual(BUILD_TARGETS, []) + self.assertEqual(_build_plus_default, []) + + def test_Remove_Argument(self): + ARGLIST.extend([ + ('key1', 'value1'), + ('key2', 'value2') + ]) + ARGUMENTS.update({'key1': 'value1', 'key2': 'value2'}) + + _Remove_Argument('key1=value1') + self.assertEqual(ARGUMENTS, {'key2': 'value2'}) + self.assertEqual(ARGLIST, [('key2', 'value2')]) + + def test_Remove_Argument_key_with_multiple_values(self): + ARGLIST.extend([ + ('key1', 'value1'), + ('key1', 'value2') + ]) + ARGUMENTS['key1'] = 'value2' # ARGUMENTS only keeps last, emulate + + _Remove_Argument('key1=value1') + self.assertEqual(ARGLIST, [('key1', 'value2')]) + # ARGUMENTS must be reconstructed + self.assertEqual(ARGUMENTS, {'key1': 'value2'}) + + def test_Remove_Argument_nonexistent(self): + # Removing a nonexistent argument is a no-op with no error + ARGUMENTS['key1'] = 'value1' + ARGLIST.append(('key1', 'value1')) + + _Remove_Argument('nonexistent=value') + self.assertEqual(ARGUMENTS, {'key1': 'value1'}) + self.assertEqual(ARGLIST, [('key1', 'value1')]) + + def test_Remove_Argument_empty(self): + # Removing an empty argument is also a no-op with no error + ARGUMENTS['key1'] = 'value1' + ARGLIST.append(('key1', 'value1')) + + _Remove_Argument('') + self.assertEqual(ARGUMENTS, {'key1': 'value1'}) + self.assertEqual(ARGLIST, [('key1', 'value1')]) + + # XXX where does TARGETS come in? + def test_Remove_Target(self): + BUILD_TARGETS.extend(['target1', 'target2', 'target3']) + COMMAND_LINE_TARGETS.extend(['target1', 'target2', 'target3']) + + _Remove_Target('target2') + self.assertEqual(BUILD_TARGETS, ['target1', 'target3']) + self.assertEqual(COMMAND_LINE_TARGETS, ['target1', 'target3']) + + def test_Remove_Target_duplicated(self): + # Targets can be duplicated, only one should be removed + # There is not a good way to determine which instance was added + # "in error" so all we can do is check *something* was removed. + BUILD_TARGETS.extend(['target1', 'target1']) + COMMAND_LINE_TARGETS.extend(['target1', 'target1']) + + _Remove_Target('target1') + self.assertEqual(BUILD_TARGETS, ['target1']) + self.assertEqual(COMMAND_LINE_TARGETS, ['target1']) + + def test_Remove_Target_nonexistent(self): + # Asking to remove a nonexistent argument is a no-op with no error + BUILD_TARGETS.append('target1') + COMMAND_LINE_TARGETS.append('target1') + + _Remove_Target('nonexistent') + self.assertEqual(BUILD_TARGETS, ['target1']) + self.assertEqual(COMMAND_LINE_TARGETS, ['target1']) + + def test_Remove_Target_empty(self): + # Asking to remove an empty argument is also a no-op with no error + BUILD_TARGETS.append('target1') + COMMAND_LINE_TARGETS.append('target1') + + _Remove_Target('') + self.assertEqual(BUILD_TARGETS, ['target1']) + self.assertEqual(COMMAND_LINE_TARGETS, ['target1']) + + +if __name__ == '__main__': + unittest.main() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/SCons/Script/__init__.py b/SCons/Script/__init__.py index ce0105573c..73b0f923ae 100644 --- a/SCons/Script/__init__.py +++ b/SCons/Script/__init__.py @@ -30,15 +30,16 @@ some other module. If it's specific to the "scons" script invocation, it goes here. """ - -import time -start_time = time.time() +from __future__ import annotations import collections +import itertools import os +import sys +import time from io import StringIO -import sys +start_time = time.time() # Special chicken-and-egg handling of the "--debug=memoizer" flag: # @@ -53,9 +54,17 @@ # to not add the shims. So we use a special-case, up-front check for # the "--debug=memoizer" flag and enable Memoizer before we import any # of the other modules that use it. - -_args = sys.argv + os.environ.get('SCONSFLAGS', '').split() -if "--debug=memoizer" in _args: +# Update: this breaks if the option isn't exactly "--debug=memoizer", +# like if there is more than one debug option as a csv. Do a bit more work. + +_args = sys.argv + os.environ.get("SCONSFLAGS", "").split() +_args = ( + arg[len("--debug=") :].split(",") + for arg in _args + if arg.startswith("--debug=") +) +_args = list(itertools.chain.from_iterable(_args)) +if "memoizer" in _args: import SCons.Memoize import SCons.Warnings try: @@ -126,7 +135,7 @@ #profiling = Main.profiling #repositories = Main.repositories -from . import SConscript as _SConscript +from . import SConscript as _SConscript # pylint: disable=import-outside-toplevel call_stack = _SConscript.call_stack @@ -199,13 +208,15 @@ def _clear(self) -> None: # own targets to BUILD_TARGETS. _build_plus_default = TargetList() -def _Add_Arguments(alist) -> None: +def _Add_Arguments(alist: list[str]) -> None: + """Add value(s) to ``ARGLIST`` and ``ARGUMENTS``.""" for arg in alist: a, b = arg.split('=', 1) ARGUMENTS[a] = b ARGLIST.append((a, b)) -def _Add_Targets(tlist) -> None: +def _Add_Targets(tlist: list[str]) -> None: + """Add value(s) to ``COMMAND_LINE_TARGETS`` and ``BUILD_TARGETS``.""" if tlist: COMMAND_LINE_TARGETS.extend(tlist) BUILD_TARGETS.extend(tlist) @@ -215,6 +226,52 @@ def _Add_Targets(tlist) -> None: _build_plus_default._add_Default = _build_plus_default._do_nothing _build_plus_default._clear = _build_plus_default._do_nothing +def _Remove_Argument(aarg: str) -> None: + """Remove *aarg* from ``ARGLIST`` and ``ARGUMENTS``. + + Used to remove a variables-style argument that is no longer valid. + This can happpen because the command line is processed once early, + before we see any :func:`SCons.Script.Main.AddOption` calls, so we + could not recognize it belongs to an option and is not a standalone + variable=value argument. + + .. versionadded:: NEXT_RELEASE + + """ + if aarg: + a, b = aarg.split('=', 1) + if (a, b) in ARGLIST: + ARGLIST.remove((a, b)) + ARGUMENTS.pop(a, None) + # ARGLIST might have had multiple values for 'a'. If there + # are any left, put that in ARGUMENTS, keeping the last one + # (retaining cmdline order) + for item in ARGLIST: + if item[0] == a: + ARGUMENTS[a] = item[1] + +def _Remove_Target(targ: str) -> None: + """Remove *targ* from ``BUILD_TARGETS`` and ``COMMAND_LINE_TARGETS``. + + Used to remove a target that is no longer valid. This can happpen + because the command line is processed once early, before we see any + :func:`SCons.Script.Main.AddOption` calls, so we could not recognize + it belongs to an option and is not a standalone target argument. + + Since we are "correcting an error", we also have to fix up the internal + :data:`_build_plus_default` list. + + .. versionadded:: NEXT_RELEASE + + """ + if targ: + if targ in COMMAND_LINE_TARGETS: + COMMAND_LINE_TARGETS.remove(targ) + if targ in BUILD_TARGETS: + BUILD_TARGETS.remove(targ) + if targ in _build_plus_default: + _build_plus_default.remove(targ) + def _Set_Default_Targets_Has_Been_Called(d, fs): return DEFAULT_TARGETS @@ -251,19 +308,21 @@ def _Set_Default_Targets(env, tlist) -> None: help_text = None -def HelpFunction(text, append: bool = False, keep_local: bool = False) -> None: +def HelpFunction(text, append: bool = False, local_only: bool = False) -> None: """The implementaion of the the ``Help`` method. See :meth:`~SCons.Script.SConscript.Help`. .. versionchanged:: 4.6.0 The *keep_local* parameter was added. + .. versionchanged:: 4.9.0 + The *keep_local* parameter was renamed *local_only* to match manpage """ global help_text if help_text is None: if append: with StringIO() as s: - PrintHelp(s, local_only=keep_local) + PrintHelp(s, local_only=local_only) help_text = s.getvalue() else: help_text = "" diff --git a/SCons/Subst.py b/SCons/Subst.py index b04ebe50cd..4d6b249c6c 100644 --- a/SCons/Subst.py +++ b/SCons/Subst.py @@ -23,10 +23,11 @@ """SCons string substitution.""" +from __future__ import annotations + import collections import re from inspect import signature, Parameter -from typing import Optional import SCons.Errors from SCons.Util import is_String, is_Sequence @@ -807,7 +808,7 @@ def _remove_list(list): _space_sep = re.compile(r'[\t ]+(?![^{]*})') -def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: Optional[dict] = None): +def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: dict | None = None): """Expand a string or list containing construction variable substitutions. @@ -889,7 +890,7 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ return result -def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: Optional[dict] = None): +def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides: dict | None = None): """Substitute construction variables in a string (or list or other object) and separate the arguments into a command list. diff --git a/SCons/Subst.xml b/SCons/Subst.xml index 0d50bf2fd4..71aab2fc00 100644 --- a/SCons/Subst.xml +++ b/SCons/Subst.xml @@ -1,9 +1,10 @@ -Specifies the exceptions that will be allowed -when expanding construction variables. +Specifies the exceptions that will be ignored +when expanding &consvars;. By default, -any construction variable expansions that generate a -NameError +any &consvar; expansions that generate a +&NameError; or -IndexError +&IndexError; exception will expand to a '' -(an empty string) and not cause scons to fail. +(an empty string) and not cause &scons; to fail. All exceptions not in the specified list will generate an error message and terminate processing. @@ -50,7 +51,8 @@ If &f-AllowSubstExceptions; is called multiple times, each call completely overwrites the previous list -of allowed exceptions. +of ignored exceptions. +Calling it with no arguments means no exceptions will be ignored. @@ -64,7 +66,7 @@ Example: AllowSubstExceptions() # Also allow a string containing a zero-division expansion -# like '${1 / 0}' to evalute to ''. +# like '${1 / 0}' to evaluate to ''. AllowSubstExceptions(IndexError, NameError, ZeroDivisionError) diff --git a/SCons/Taskmaster/TaskmasterTests.py b/SCons/Taskmaster/TaskmasterTests.py index f279f05eb4..9e52557c28 100644 --- a/SCons/Taskmaster/TaskmasterTests.py +++ b/SCons/Taskmaster/TaskmasterTests.py @@ -1161,6 +1161,7 @@ def test_exception(self) -> None: "integer division or modulo", "integer division or modulo by zero", "integer division by zero", # PyPy2 + "division by zero", # Python 3.14+ ] assert str(exc_value) in exception_values, exc_value diff --git a/SCons/Tool/386asm.xml b/SCons/Tool/386asm.xml index 1661a87534..21caf49d9d 100644 --- a/SCons/Tool/386asm.xml +++ b/SCons/Tool/386asm.xml @@ -1,9 +1,10 @@ The name of the compiler to use when compiling D source -destined to be in a shared objects. +destined to be in a shared object. See also &cv-link-DC; for compiling to static objects. diff --git a/SCons/Tool/FortranCommon.py b/SCons/Tool/FortranCommon.py index f221d15ec7..dc9dcddf6e 100644 --- a/SCons/Tool/FortranCommon.py +++ b/SCons/Tool/FortranCommon.py @@ -23,9 +23,10 @@ """Routines for setting up Fortran, common to all dialects.""" +from __future__ import annotations + import re import os.path -from typing import Tuple, List import SCons.Scanner.Fortran import SCons.Tool @@ -96,7 +97,7 @@ def ShFortranEmitter(target, source, env) -> Tuple: return SharedObjectEmitter(target, source, env) -def ComputeFortranSuffixes(suffixes: List[str], ppsuffixes: List[str]) -> None: +def ComputeFortranSuffixes(suffixes: list[str], ppsuffixes: list[str]) -> None: """Update the suffix lists to reflect the platform requirements. If upper-cased suffixes can be distinguished from lower, those are @@ -119,7 +120,7 @@ def ComputeFortranSuffixes(suffixes: List[str], ppsuffixes: List[str]) -> None: def CreateDialectActions( dialect: str, -) -> Tuple[CommandAction, CommandAction, CommandAction, CommandAction]: +) -> tuple[CommandAction, CommandAction, CommandAction, CommandAction]: """Create dialect specific actions.""" CompAction = Action(f'${dialect}COM ', cmdstr=f'${dialect}COMSTR') CompPPAction = Action(f'${dialect}PPCOM ', cmdstr=f'${dialect}PPCOMSTR') @@ -131,8 +132,8 @@ def CreateDialectActions( def DialectAddToEnv( env, dialect: str, - suffixes: List[str], - ppsuffixes: List[str], + suffixes: list[str], + ppsuffixes: list[str], support_mods: bool = False, ) -> None: """Add dialect specific construction variables. diff --git a/SCons/Tool/JavaCommon.py b/SCons/Tool/JavaCommon.py index c7e62b88ce..0bcb0eaa5e 100644 --- a/SCons/Tool/JavaCommon.py +++ b/SCons/Tool/JavaCommon.py @@ -23,11 +23,12 @@ """Common routines for processing Java. """ +from __future__ import annotations + import os import re import glob from pathlib import Path -from typing import List import SCons.Util @@ -491,7 +492,7 @@ def parse_java_file(fn, version=default_java_version): return os.path.split(fn) -def get_java_install_dirs(platform, version=None) -> List[str]: +def get_java_install_dirs(platform, version=None) -> list[str]: """ Find possible java jdk installation directories. Returns a list for use as `default_paths` when looking up actual @@ -540,7 +541,7 @@ def win32getvnum(java): return [] -def get_java_include_paths(env, javac, version) -> List[str]: +def get_java_include_paths(env, javac, version) -> list[str]: """Find java include paths for JNI building. Cannot be called in isolation - `javac` refers to an already detected diff --git a/SCons/Tool/MSCommon/MSVC/Config.py b/SCons/Tool/MSCommon/MSVC/Config.py index 7c0f1fe6ff..d0ccb6d0ba 100644 --- a/SCons/Tool/MSCommon/MSVC/Config.py +++ b/SCons/Tool/MSCommon/MSVC/Config.py @@ -118,15 +118,67 @@ for vc_runtime_alias in vc_runtime_alias_list: MSVC_RUNTIME_EXTERNAL[vc_runtime_alias] = vc_runtime_def -MSVC_BUILDTOOLS_DEFINITION = namedtuple('MSVCBuildtools', [ - 'vc_buildtools', - 'vc_buildtools_numeric', +MSVC_BUILDSERIES_DEFINITION = namedtuple('MSVCBuildSeries', [ + 'vc_buildseries', + 'vc_buildseries_numeric', 'vc_version', 'vc_version_numeric', 'cl_version', 'cl_version_numeric', +]) + +MSVC_BUILDSERIES_DEFINITION_LIST = [] + +MSVC_BUILDSERIES_INTERNAL = {} +MSVC_BUILDSERIES_EXTERNAL = {} + +VC_BUILDTOOLS_MAP = {} + +VC_VERSION_MAP = {} +CL_VERSION_MAP = {} + +for (vc_buildseries, vc_version, cl_version) in [ + ('144', '14.4', '19.4'), + ('143', '14.3', '19.3'), + ('142', '14.2', '19.2'), + ('141', '14.1', '19.1'), + ('140', '14.0', '19.0'), + ('120', '12.0', '18.0'), + ('110', '11.0', '17.0'), + ('100', '10.0', '16.0'), + ('90', '9.0', '15.0'), + ('80', '8.0', '14.0'), + ('71', '7.1', '13.1'), + ('70', '7.0', '13.0'), + ('60', '6.0', '12.0'), +]: + + vc_buildseries_def = MSVC_BUILDSERIES_DEFINITION( + vc_buildseries=vc_buildseries, + vc_buildseries_numeric=int(vc_buildseries), + vc_version=vc_version, + vc_version_numeric=float(vc_version), + cl_version=cl_version, + cl_version_numeric=float(cl_version), + ) + + MSVC_BUILDSERIES_DEFINITION_LIST.append(vc_buildseries_def) + + MSVC_BUILDSERIES_INTERNAL[vc_buildseries] = vc_buildseries_def + MSVC_BUILDSERIES_EXTERNAL[vc_buildseries] = vc_buildseries_def + MSVC_BUILDSERIES_EXTERNAL[vc_version] = vc_buildseries_def + + VC_VERSION_MAP[vc_version] = vc_buildseries_def + CL_VERSION_MAP[cl_version] = vc_buildseries_def + +MSVC_BUILDTOOLS_DEFINITION = namedtuple('MSVCBuildtools', [ + 'vc_buildtools', + 'vc_buildtools_numeric', + 'vc_buildseries_list', 'vc_runtime_def', 'vc_istoolset', + 'msvc_version', + 'msvc_version_numeric', ]) MSVC_BUILDTOOLS_DEFINITION_LIST = [] @@ -134,43 +186,58 @@ MSVC_BUILDTOOLS_INTERNAL = {} MSVC_BUILDTOOLS_EXTERNAL = {} -VC_VERSION_MAP = {} - -for vc_buildtools, vc_version, cl_version, vc_runtime, vc_istoolset in [ - ('v143', '14.3', '19.3', '140', True), - ('v142', '14.2', '19.2', '140', True), - ('v141', '14.1', '19.1', '140', True), - ('v140', '14.0', '19.0', '140', True), - ('v120', '12.0', '18.0', '120', False), - ('v110', '11.0', '17.0', '110', False), - ('v100', '10.0', '16.0', '100', False), - ('v90', '9.0', '15.0', '90', False), - ('v80', '8.0', '14.0', '80', False), - ('v71', '7.1', '13.1', '71', False), - ('v70', '7.0', '13.0', '70', False), - ('v60', '6.0', '12.0', '60', False), +MSVC_VERSION_NEWEST = None +MSVC_VERSION_NEWEST_NUMERIC = 0.0 + +for vc_buildtools, vc_buildseries_list, vc_runtime, vc_istoolset in [ + ('v143', ['144', '143'], '140', True), + ('v142', ['142'], '140', True), + ('v141', ['141'], '140', True), + ('v140', ['140'], '140', True), + ('v120', ['120'], '120', False), + ('v110', ['110'], '110', False), + ('v100', ['100'], '100', False), + ('v90', ['90'], '90', False), + ('v80', ['80'], '80', False), + ('v71', ['71'], '71', False), + ('v70', ['70'], '70', False), + ('v60', ['60'], '60', False), ]: vc_runtime_def = MSVC_RUNTIME_INTERNAL[vc_runtime] + vc_buildseries_list = tuple( + MSVC_BUILDSERIES_INTERNAL[vc_buildseries] + for vc_buildseries in vc_buildseries_list + ) + + vc_buildtools_numstr = vc_buildtools[1:] + + msvc_version = vc_buildtools_numstr[:-1] + '.' + vc_buildtools_numstr[-1] + msvc_version_numeric = float(msvc_version) + vc_buildtools_def = MSVC_BUILDTOOLS_DEFINITION( vc_buildtools = vc_buildtools, vc_buildtools_numeric = int(vc_buildtools[1:]), - vc_version = vc_version, - vc_version_numeric = float(vc_version), - cl_version = cl_version, - cl_version_numeric = float(cl_version), + vc_buildseries_list = vc_buildseries_list, vc_runtime_def = vc_runtime_def, vc_istoolset = vc_istoolset, + msvc_version = msvc_version, + msvc_version_numeric = msvc_version_numeric, ) MSVC_BUILDTOOLS_DEFINITION_LIST.append(vc_buildtools_def) MSVC_BUILDTOOLS_INTERNAL[vc_buildtools] = vc_buildtools_def MSVC_BUILDTOOLS_EXTERNAL[vc_buildtools] = vc_buildtools_def - MSVC_BUILDTOOLS_EXTERNAL[vc_version] = vc_buildtools_def + MSVC_BUILDTOOLS_EXTERNAL[msvc_version] = vc_buildtools_def + + for vc_buildseries_def in vc_buildseries_list: + VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries] = vc_buildtools_def - VC_VERSION_MAP[vc_version] = vc_buildtools_def + if vc_buildtools_def.msvc_version_numeric > MSVC_VERSION_NEWEST_NUMERIC: + MSVC_VERSION_NEWEST_NUMERIC = vc_buildtools_def.msvc_version_numeric + MSVC_VERSION_NEWEST = vc_buildtools_def.msvc_version MSVS_VERSION_INTERNAL = {} MSVS_VERSION_EXTERNAL = {} @@ -181,8 +248,6 @@ MSVS_VERSION_MAJOR_MAP = {} -CL_VERSION_MAP = {} - MSVC_SDK_VERSIONS = set() VISUALSTUDIO_DEFINITION = namedtuple('VisualStudioDefinition', [ @@ -247,15 +312,15 @@ vc_buildtools_def.vc_runtime_def.vc_runtime_vsdef_list.append(vs_def) - vc_version = vc_buildtools_def.vc_version + msvc_version = vc_buildtools_def.msvc_version MSVS_VERSION_INTERNAL[vs_product] = vs_def MSVS_VERSION_EXTERNAL[vs_product] = vs_def MSVS_VERSION_EXTERNAL[vs_version] = vs_def - MSVC_VERSION_INTERNAL[vc_version] = vs_def + MSVC_VERSION_INTERNAL[msvc_version] = vs_def MSVC_VERSION_EXTERNAL[vs_product] = vs_def - MSVC_VERSION_EXTERNAL[vc_version] = vs_def + MSVC_VERSION_EXTERNAL[msvc_version] = vs_def MSVC_VERSION_EXTERNAL[vc_buildtools_def.vc_buildtools] = vs_def if vs_product in VS_PRODUCT_ALIAS: @@ -264,14 +329,12 @@ MSVS_VERSION_EXTERNAL[vs_product_alias] = vs_def MSVC_VERSION_EXTERNAL[vs_product_alias] = vs_def - MSVC_VERSION_SUFFIX[vc_version] = vs_def + MSVC_VERSION_SUFFIX[msvc_version] = vs_def if vs_express: - MSVC_VERSION_SUFFIX[vc_version + 'Exp'] = vs_def + MSVC_VERSION_SUFFIX[msvc_version + 'Exp'] = vs_def MSVS_VERSION_MAJOR_MAP[vs_version_major] = vs_def - CL_VERSION_MAP[vc_buildtools_def.cl_version] = vs_def - if vc_sdk: MSVC_SDK_VERSIONS.update(vc_sdk) @@ -292,7 +355,7 @@ for vs_def in VISUALSTUDIO_DEFINITION_LIST: if not vs_def.vc_buildtools_def.vc_istoolset: continue - version_key = vs_def.vc_buildtools_def.vc_version + version_key = vs_def.vc_buildtools_def.msvc_version MSVC_VERSION_TOOLSET_DEFAULTS_MAP[version_key] = [version_key] MSVC_VERSION_TOOLSET_SEARCH_MAP[version_key] = [] if vs_def.vs_express: @@ -305,11 +368,11 @@ for vs_def in VISUALSTUDIO_DEFINITION_LIST: if not vs_def.vc_buildtools_def.vc_istoolset: continue - version_key = vs_def.vc_buildtools_def.vc_version + version_key = vs_def.vc_buildtools_def.msvc_version for vc_buildtools in vs_def.vc_buildtools_all: toolset_buildtools_def = MSVC_BUILDTOOLS_INTERNAL[vc_buildtools] - toolset_vs_def = MSVC_VERSION_INTERNAL[toolset_buildtools_def.vc_version] - buildtools_key = toolset_buildtools_def.vc_version + toolset_vs_def = MSVC_VERSION_INTERNAL[toolset_buildtools_def.msvc_version] + buildtools_key = toolset_buildtools_def.msvc_version MSVC_VERSION_TOOLSET_SEARCH_MAP[buildtools_key].extend(MSVC_VERSION_TOOLSET_DEFAULTS_MAP[version_key]) # convert string version set to string version list ranked in descending order diff --git a/SCons/Tool/MSCommon/MSVC/Kind.py b/SCons/Tool/MSCommon/MSVC/Kind.py new file mode 100644 index 0000000000..47d4a25d9f --- /dev/null +++ b/SCons/Tool/MSCommon/MSVC/Kind.py @@ -0,0 +1,668 @@ +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Version kind categorization for Microsoft Visual C/C++. +""" + +import os +import re + +from collections import ( + namedtuple, +) + +from ..common import ( + debug, +) + +from . import Registry +from . import Util + +from . import Dispatcher +Dispatcher.register_modulename(__name__) + + +# use express install for non-express msvc_version if no other installations found +USE_EXPRESS_FOR_NONEXPRESS = True + +# productdir kind + +VCVER_KIND_UNKNOWN = 0 # undefined +VCVER_KIND_DEVELOP = 1 # devenv binary +VCVER_KIND_EXPRESS = 2 # express binary +VCVER_KIND_BTDISPATCH = 3 # no ide binaries (buildtools dispatch folder) +VCVER_KIND_VCFORPYTHON = 4 # no ide binaries (2008/9.0) +VCVER_KIND_EXPRESS_WIN = 5 # express for windows binary (VSWinExpress) +VCVER_KIND_EXPRESS_WEB = 6 # express for web binary (VWDExpress) +VCVER_KIND_SDK = 7 # no ide binaries +VCVER_KIND_CMDLINE = 8 # no ide binaries + +VCVER_KIND_STR = { + VCVER_KIND_UNKNOWN: '', + VCVER_KIND_DEVELOP: 'Develop', + VCVER_KIND_EXPRESS: 'Express', + VCVER_KIND_BTDISPATCH: 'BTDispatch', + VCVER_KIND_VCFORPYTHON: 'VCForPython', + VCVER_KIND_EXPRESS_WIN: 'Express-Win', + VCVER_KIND_EXPRESS_WEB: 'Express-Web', + VCVER_KIND_SDK: 'SDK', + VCVER_KIND_CMDLINE: 'CmdLine', +} + +BITFIELD_KIND_DEVELOP = 0b_1000 +BITFIELD_KIND_EXPRESS = 0b_0100 +BITFIELD_KIND_EXPRESS_WIN = 0b_0010 +BITFIELD_KIND_EXPRESS_WEB = 0b_0001 + +VCVER_KIND_PROGRAM = namedtuple("VCVerKindProgram", [ + 'kind', # relpath from pdir to vsroot + 'program', # ide binaries + 'bitfield', +]) + +# + +IDE_PROGRAM_DEVENV_COM = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_DEVELOP, + program='devenv.com', + bitfield=BITFIELD_KIND_DEVELOP, +) + +IDE_PROGRAM_MSDEV_COM = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_DEVELOP, + program='msdev.com', + bitfield=BITFIELD_KIND_DEVELOP, +) + +IDE_PROGRAM_WDEXPRESS_EXE = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_EXPRESS, + program='WDExpress.exe', + bitfield=BITFIELD_KIND_EXPRESS, +) + +IDE_PROGRAM_VCEXPRESS_EXE = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_EXPRESS, + program='VCExpress.exe', + bitfield=BITFIELD_KIND_EXPRESS, +) + +IDE_PROGRAM_VSWINEXPRESS_EXE = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_EXPRESS_WIN, + program='VSWinExpress.exe', + bitfield=BITFIELD_KIND_EXPRESS_WIN, +) + +IDE_PROGRAM_VWDEXPRESS_EXE = VCVER_KIND_PROGRAM( + kind=VCVER_KIND_EXPRESS_WEB, + program='VWDExpress.exe', + bitfield=BITFIELD_KIND_EXPRESS_WEB, +) + +# detection configuration + +VCVER_KIND_DETECT = namedtuple("VCVerKindDetect", [ + 'root', # relpath from pdir to vsroot + 'path', # vsroot to ide dir + 'programs', # ide binaries +]) + +# detected binaries + +VCVER_DETECT_BINARIES = namedtuple("VCVerDetectBinaries", [ + 'bitfields', # detect values + 'have_dev', # develop ide binary + 'have_exp', # express ide binary + 'have_exp_win', # express windows ide binary + 'have_exp_web', # express web ide binary +]) + + +VCVER_DETECT_KIND = namedtuple("VCVerDetectKind", [ + 'skip', # skip vs root + 'save', # save in case no other kind found + 'kind', # internal kind + 'binaries_t', + 'extended', +]) + +# unknown value + +_VCVER_DETECT_KIND_UNKNOWN = VCVER_DETECT_KIND( + skip=True, + save=False, + kind=VCVER_KIND_UNKNOWN, + binaries_t=VCVER_DETECT_BINARIES( + bitfields=0b0, + have_dev=False, + have_exp=False, + have_exp_win=False, + have_exp_web=False, + ), + extended={}, +) + +# + +_msvc_pdir_func = None + +def register_msvc_version_pdir_func(func): + global _msvc_pdir_func + if func: + _msvc_pdir_func = func + +_cache_vcver_kind_map = {} + +def msvc_version_register_kind(msvc_version, kind_t) -> None: + global _cache_vcver_kind_map + if kind_t is None: + kind_t = _VCVER_DETECT_KIND_UNKNOWN + debug('msvc_version=%s, kind=%s', repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind])) + _cache_vcver_kind_map[msvc_version] = kind_t + +def _msvc_version_kind_lookup(msvc_version, env=None): + global _cache_vcver_kind_map + global _msvc_pdir_func + if msvc_version not in _cache_vcver_kind_map: + _msvc_pdir_func(msvc_version, env) + kind_t = _cache_vcver_kind_map.get(msvc_version, _VCVER_DETECT_KIND_UNKNOWN) + debug( + 'kind=%s, dev=%s, exp=%s, msvc_version=%s', + repr(VCVER_KIND_STR[kind_t.kind]), + kind_t.binaries_t.have_dev, kind_t.binaries_t.have_exp, + repr(msvc_version) + ) + return kind_t + +def msvc_version_is_btdispatch(msvc_version, env=None): + kind_t = _msvc_version_kind_lookup(msvc_version, env) + is_btdispatch = bool(kind_t.kind == VCVER_KIND_BTDISPATCH) + debug( + 'is_btdispatch=%s, kind:%s, msvc_version=%s', + repr(is_btdispatch), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version) + ) + return is_btdispatch + +def msvc_version_is_express(msvc_version, env=None): + kind_t = _msvc_version_kind_lookup(msvc_version, env) + is_express = bool(kind_t.kind == VCVER_KIND_EXPRESS) + debug( + 'is_express=%s, kind:%s, msvc_version=%s', + repr(is_express), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version) + ) + return is_express + +def msvc_version_is_vcforpython(msvc_version, env=None): + kind_t = _msvc_version_kind_lookup(msvc_version, env) + is_vcforpython = bool(kind_t.kind == VCVER_KIND_VCFORPYTHON) + debug( + 'is_vcforpython=%s, kind:%s, msvc_version=%s', + repr(is_vcforpython), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version) + ) + return is_vcforpython + +def msvc_version_skip_uwp_target(env, msvc_version): + + vernum = float(Util.get_msvc_version_prefix(msvc_version)) + vernum_int = int(vernum * 10) + + if vernum_int != 140: + return False + + kind_t = _msvc_version_kind_lookup(msvc_version, env) + if kind_t.kind != VCVER_KIND_EXPRESS: + return False + + target_arch = env.get('TARGET_ARCH') + + uwp_is_supported = kind_t.extended.get('uwp_is_supported', {}) + is_supported = uwp_is_supported.get(target_arch, True) + + if is_supported: + return False + + return True + +def _pdir_detect_binaries(pdir, detect): + + vs_root = os.path.join(pdir, detect.root) + ide_path = os.path.join(vs_root, detect.path) + + bitfields = 0b_0000 + for ide_program in detect.programs: + prog = os.path.join(ide_path, ide_program.program) + if not os.path.exists(prog): + continue + bitfields |= ide_program.bitfield + + have_dev = bool(bitfields & BITFIELD_KIND_DEVELOP) + have_exp = bool(bitfields & BITFIELD_KIND_EXPRESS) + have_exp_win = bool(bitfields & BITFIELD_KIND_EXPRESS_WIN) + have_exp_web = bool(bitfields & BITFIELD_KIND_EXPRESS_WEB) + + binaries_t = VCVER_DETECT_BINARIES( + bitfields=bitfields, + have_dev=have_dev, + have_exp=have_exp, + have_exp_win=have_exp_win, + have_exp_web=have_exp_web, + ) + + debug( + 'vs_root=%s, dev=%s, exp=%s, exp_win=%s, exp_web=%s, pdir=%s', + repr(vs_root), + binaries_t.have_dev, binaries_t.have_exp, + binaries_t.have_exp_win, binaries_t.have_exp_web, + repr(pdir) + ) + + return vs_root, binaries_t + +_cache_pdir_vswhere_kind = {} + +def msvc_version_pdir_vswhere_kind(msvc_version, pdir, detect_t): + global _cache_pdir_vswhere_kind + + vc_dir = os.path.normcase(os.path.normpath(pdir)) + cache_key = (msvc_version, vc_dir) + + rval = _cache_pdir_vswhere_kind.get(cache_key) + if rval is not None: + debug('cache=%s', repr(rval)) + return rval + + extended = {} + + prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version) + + vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t) + + if binaries_t.have_dev: + kind = VCVER_KIND_DEVELOP + elif binaries_t.have_exp: + kind = VCVER_KIND_EXPRESS + else: + kind = VCVER_KIND_CMDLINE + + skip = False + save = False + + if suffix != 'Exp' and kind == VCVER_KIND_EXPRESS: + skip = True + save = USE_EXPRESS_FOR_NONEXPRESS + elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS: + skip = True + + kind_t = VCVER_DETECT_KIND( + skip=skip, + save=save, + kind=kind, + binaries_t=binaries_t, + extended=extended, + ) + + debug( + 'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s', + kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]), + repr(msvc_version), repr(pdir) + ) + + _cache_pdir_vswhere_kind[cache_key] = kind_t + + return kind_t + +# VS2015 buildtools batch file call detection +# vs2015 buildtools do not support sdk_version or UWP arguments + +_VS2015BT_PATH = r'..\Microsoft Visual C++ Build Tools\vcbuildtools.bat' + +_VS2015BT_REGEX_STR = ''.join([ + r'^\s*if\s+exist\s+', + re.escape(fr'"%~dp0..\{_VS2015BT_PATH}"'), + r'\s+goto\s+setup_buildsku\s*$', +]) + +_VS2015BT_VCVARS_BUILDTOOLS = re.compile(_VS2015BT_REGEX_STR, re.IGNORECASE) +_VS2015BT_VCVARS_STOP = re.compile(r'^\s*[:]Setup_VS\s*$', re.IGNORECASE) + +def _vs_buildtools_2015_vcvars(vcvars_file): + have_buildtools_vcvars = False + with open(vcvars_file) as fh: + for line in fh: + if _VS2015BT_VCVARS_BUILDTOOLS.match(line): + have_buildtools_vcvars = True + break + if _VS2015BT_VCVARS_STOP.match(line): + break + return have_buildtools_vcvars + +def _vs_buildtools_2015(vs_root, vc_dir): + + is_btdispatch = False + + do_once = True + while do_once: + do_once = False + + buildtools_file = os.path.join(vs_root, _VS2015BT_PATH) + have_buildtools = os.path.exists(buildtools_file) + debug('have_buildtools=%s', have_buildtools) + if not have_buildtools: + break + + vcvars_file = os.path.join(vc_dir, 'vcvarsall.bat') + have_vcvars = os.path.exists(vcvars_file) + debug('have_vcvars=%s', have_vcvars) + if not have_vcvars: + break + + have_buildtools_vcvars = _vs_buildtools_2015_vcvars(vcvars_file) + debug('have_buildtools_vcvars=%s', have_buildtools_vcvars) + if not have_buildtools_vcvars: + break + + is_btdispatch = True + + debug('is_btdispatch=%s', is_btdispatch) + return is_btdispatch + +_VS2015EXP_VCVARS_LIBPATH = re.compile( + ''.join([ + r'^\s*\@if\s+exist\s+\"\%VCINSTALLDIR\%LIB\\store\\(amd64|arm)"\s+', + r'set (LIB|LIBPATH)=\%VCINSTALLDIR\%LIB\\store\\(amd64|arm);.*\%(LIB|LIBPATH)\%\s*$' + ]), + re.IGNORECASE +) + +_VS2015EXP_VCVARS_STOP = re.compile(r'^\s*[:]GetVSCommonToolsDir\s*$', re.IGNORECASE) + +def _vs_express_2015_vcvars(vcvars_file): + n_libpath = 0 + with open(vcvars_file) as fh: + for line in fh: + if _VS2015EXP_VCVARS_LIBPATH.match(line): + n_libpath += 1 + elif _VS2015EXP_VCVARS_STOP.match(line): + break + have_uwp_fix = n_libpath >= 2 + return have_uwp_fix + +def _vs_express_2015(pdir): + + have_uwp_amd64 = False + have_uwp_arm = False + + vcvars_file = os.path.join(pdir, r'vcvarsall.bat') + if os.path.exists(vcvars_file): + + vcvars_file = os.path.join(pdir, r'bin\x86_amd64\vcvarsx86_amd64.bat') + if os.path.exists(vcvars_file): + have_uwp_fix = _vs_express_2015_vcvars(vcvars_file) + if have_uwp_fix: + have_uwp_amd64 = True + + vcvars_file = os.path.join(pdir, r'bin\x86_arm\vcvarsx86_arm.bat') + if os.path.exists(vcvars_file): + have_uwp_fix = _vs_express_2015_vcvars(vcvars_file) + if have_uwp_fix: + have_uwp_arm = True + + debug('have_uwp_amd64=%s, have_uwp_arm=%s', have_uwp_amd64, have_uwp_arm) + return have_uwp_amd64, have_uwp_arm + +# winsdk installed 2010 [7.1], 2008 [7.0, 6.1] folders + +_REGISTRY_WINSDK_VERSIONS = {'10.0', '9.0'} + +_cache_pdir_registry_winsdk = {} + +def _msvc_version_pdir_registry_winsdk(msvc_version, pdir): + global _cache_pdir_registry_winsdk + + # detect winsdk-only installations + # + # registry keys: + # [prefix]\VisualStudio\SxS\VS7\10.0 + # [prefix]\VisualStudio\SxS\VC7\10.0 product directory + # [prefix]\VisualStudio\SxS\VS7\9.0 + # [prefix]\VisualStudio\SxS\VC7\9.0 product directory + # + # winsdk notes: + # - winsdk installs do not define the common tools env var + # - the product dir is detected but the vcvars batch files will fail + # - regular installations populate the VS7 registry keys + # + + vc_dir = os.path.normcase(os.path.normpath(pdir)) + cache_key = (msvc_version, vc_dir) + + rval = _cache_pdir_registry_winsdk.get(cache_key) + if rval is not None: + debug('cache=%s', repr(rval)) + return rval + + if msvc_version not in _REGISTRY_WINSDK_VERSIONS: + + is_sdk = False + + debug('is_sdk=%s, msvc_version=%s', is_sdk, repr(msvc_version)) + + else: + + vc_dir = os.path.normcase(os.path.normpath(pdir)) + + vc_suffix = Registry.vstudio_sxs_vc7(msvc_version) + vc_qresults = [record[0] for record in Registry.microsoft_query_paths(vc_suffix)] + vc_root = os.path.normcase(os.path.normpath(vc_qresults[0])) if vc_qresults else None + + if vc_dir != vc_root: + # registry vc path is not the current pdir + + is_sdk = False + + debug( + 'is_sdk=%s, msvc_version=%s, pdir=%s, vc_root=%s', + is_sdk, repr(msvc_version), repr(vc_dir), repr(vc_root) + ) + + else: + # registry vc path is the current pdir + + vs_suffix = Registry.vstudio_sxs_vs7(msvc_version) + vs_qresults = [record[0] for record in Registry.microsoft_query_paths(vs_suffix)] + vs_root = vs_qresults[0] if vs_qresults else None + + is_sdk = bool(not vs_root and vc_root) + + debug( + 'is_sdk=%s, msvc_version=%s, vs_root=%s, vc_root=%s', + is_sdk, repr(msvc_version), repr(vs_root), repr(vc_root) + ) + + _cache_pdir_registry_winsdk[cache_key] = is_sdk + + return is_sdk + +_cache_pdir_registry_kind = {} + +def msvc_version_pdir_registry_kind(msvc_version, pdir, detect_t, is_vcforpython=False): + global _cache_pdir_registry_kind + + vc_dir = os.path.normcase(os.path.normpath(pdir)) + cache_key = (msvc_version, vc_dir) + + rval = _cache_pdir_registry_kind.get(cache_key) + if rval is not None: + debug('cache=%s', repr(rval)) + return rval + + extended = {} + + prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version) + + vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t) + + if binaries_t.have_dev: + kind = VCVER_KIND_DEVELOP + elif binaries_t.have_exp: + kind = VCVER_KIND_EXPRESS + elif msvc_version == '14.0' and _vs_buildtools_2015(vs_root, pdir): + kind = VCVER_KIND_BTDISPATCH + elif msvc_version == '9.0' and is_vcforpython: + kind = VCVER_KIND_VCFORPYTHON + elif binaries_t.have_exp_win: + kind = VCVER_KIND_EXPRESS_WIN + elif binaries_t.have_exp_web: + kind = VCVER_KIND_EXPRESS_WEB + elif _msvc_version_pdir_registry_winsdk(msvc_version, pdir): + kind = VCVER_KIND_SDK + else: + kind = VCVER_KIND_CMDLINE + + skip = False + save = False + + if kind in (VCVER_KIND_EXPRESS_WIN, VCVER_KIND_EXPRESS_WEB, VCVER_KIND_SDK): + skip = True + elif suffix != 'Exp' and kind == VCVER_KIND_EXPRESS: + skip = True + save = USE_EXPRESS_FOR_NONEXPRESS + elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS: + skip = True + + if prefix == '14.0' and kind == VCVER_KIND_EXPRESS: + have_uwp_amd64, have_uwp_arm = _vs_express_2015(pdir) + uwp_is_supported = { + 'x86': True, + 'amd64': have_uwp_amd64, + 'arm': have_uwp_arm, + } + extended['uwp_is_supported'] = uwp_is_supported + + kind_t = VCVER_DETECT_KIND( + skip=skip, + save=save, + kind=kind, + binaries_t=binaries_t, + extended=extended, + ) + + debug( + 'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s', + kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]), + repr(msvc_version), repr(pdir) + ) + + _cache_pdir_registry_kind[cache_key] = kind_t + + return kind_t + +# queries + +def get_msvc_version_kind(msvc_version, env=None): + kind_t = _msvc_version_kind_lookup(msvc_version, env) + kind_str = VCVER_KIND_STR[kind_t.kind] + debug( + 'kind=%s, kind_str=%s, msvc_version=%s', + repr(kind_t.kind), repr(kind_str), repr(msvc_version) + ) + return (kind_t.kind, kind_str) + +def msvc_version_sdk_version_is_supported(msvc_version, env=None): + + vernum = float(Util.get_msvc_version_prefix(msvc_version)) + vernum_int = int(vernum * 10) + + kind_t = _msvc_version_kind_lookup(msvc_version, env) + + if vernum_int >= 141: + # VS2017 and later + is_supported = True + elif vernum_int == 140: + # VS2015: + # True: Develop, CmdLine + # False: Express, BTDispatch + is_supported = True + if kind_t.kind == VCVER_KIND_EXPRESS: + is_supported = False + elif kind_t.kind == VCVER_KIND_BTDISPATCH: + is_supported = False + else: + # VS2013 and earlier + is_supported = False + + debug( + 'is_supported=%s, msvc_version=%s, kind=%s', + is_supported, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]) + ) + return is_supported + +def msvc_version_uwp_is_supported(msvc_version, target_arch=None, env=None): + + vernum = float(Util.get_msvc_version_prefix(msvc_version)) + vernum_int = int(vernum * 10) + + kind_t = _msvc_version_kind_lookup(msvc_version, env) + + is_target = False + + if vernum_int >= 141: + # VS2017 and later + is_supported = True + elif vernum_int == 140: + # VS2015: + # True: Develop, CmdLine + # Maybe: Express + # False: BTDispatch + is_supported = True + if kind_t.kind == VCVER_KIND_EXPRESS: + uwp_is_supported = kind_t.extended.get('uwp_is_supported', {}) + is_supported = uwp_is_supported.get(target_arch, True) + is_target = True + elif kind_t.kind == VCVER_KIND_BTDISPATCH: + is_supported = False + else: + # VS2013 and earlier + is_supported = False + + debug( + 'is_supported=%s, is_target=%s, msvc_version=%s, kind=%s, target_arch=%s', + is_supported, is_target, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]), repr(target_arch) + ) + + return is_supported, is_target + +# reset cache + +def reset() -> None: + global _cache_vcver_kind_map + global _cache_pdir_vswhere_kind + global _cache_pdir_registry_kind + global _cache_pdir_registry_winsdk + + debug('') + + _cache_vcver_kind_map = {} + _cache_pdir_vswhere_kind = {} + _cache_pdir_registry_kind = {} + _cache_pdir_registry_winsdk = {} diff --git a/SCons/Tool/MSCommon/MSVC/Policy.py b/SCons/Tool/MSCommon/MSVC/Policy.py index fe8da3156b..7e11a63ba5 100644 --- a/SCons/Tool/MSCommon/MSVC/Policy.py +++ b/SCons/Tool/MSCommon/MSVC/Policy.py @@ -38,6 +38,10 @@ namedtuple, ) +from contextlib import ( + contextmanager, +) + import SCons.Warnings from ..common import ( @@ -215,6 +219,18 @@ def msvc_notfound_handler(env, msg): else: SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, msg) +@contextmanager +def msvc_notfound_policy_contextmanager(MSVC_NOTFOUND_POLICY=None): + """ Temporarily change the MSVC not found policy within a context. + + Args: + MSVC_NOTFOUND_POLICY: + string representing the policy behavior + when MSVC is not found or None + """ + prev_policy = msvc_set_notfound_policy(MSVC_NOTFOUND_POLICY) + yield + msvc_set_notfound_policy(prev_policy) def _msvc_scripterror_policy_lookup(symbol): @@ -299,3 +315,16 @@ def msvc_scripterror_handler(env, msg): else: SCons.Warnings.warn(MSVCScriptExecutionWarning, msg) +@contextmanager +def msvc_scripterror_policy_contextmanager(MSVC_SCRIPTERROR_POLICY=None): + """ Temporarily change the msvc batch execution errors policy within a context. + + Args: + MSVC_SCRIPTERROR_POLICY: + string representing the policy behavior + when msvc batch file execution errors are detected or None + """ + prev_policy = msvc_set_scripterror_policy(MSVC_SCRIPTERROR_POLICY) + yield + msvc_set_scripterror_policy(prev_policy) + diff --git a/SCons/Tool/MSCommon/MSVC/Registry.py b/SCons/Tool/MSCommon/MSVC/Registry.py index f9e544c6fc..b5b72c42fc 100644 --- a/SCons/Tool/MSCommon/MSVC/Registry.py +++ b/SCons/Tool/MSCommon/MSVC/Registry.py @@ -110,6 +110,9 @@ def windows_kit_query_paths(version): q = windows_kits(version) return microsoft_query_paths(q) +def vstudio_sxs_vs7(version): + return '\\'.join([r'VisualStudio\SxS\VS7', version]) + def vstudio_sxs_vc7(version): return '\\'.join([r'VisualStudio\SxS\VC7', version]) diff --git a/SCons/Tool/MSCommon/MSVC/ScriptArguments.py b/SCons/Tool/MSCommon/MSVC/ScriptArguments.py index 8848095cf9..2c766fed4e 100644 --- a/SCons/Tool/MSCommon/MSVC/ScriptArguments.py +++ b/SCons/Tool/MSCommon/MSVC/ScriptArguments.py @@ -42,6 +42,7 @@ from . import Config from . import Registry from . import WinSDK +from . import Kind from .Exceptions import ( MSVCInternalError, @@ -172,6 +173,12 @@ class SortOrder(enum.IntEnum): 'vs_def', ]) +TOOLSET_VERSION_ARGS_DEFINITION = namedtuple('ToolsetVersionArgsDefinition', [ + 'version', # full version (e.g., '14.1Exp', '14.32.31326') + 'vc_buildtools_def', + 'is_user', +]) + def _msvc_version(version): verstr = Util.get_msvc_version_prefix(version) @@ -184,19 +191,22 @@ def _msvc_version(version): return version_args -def _toolset_version(version): +def _toolset_version(version, is_user=False): - verstr = Util.get_msvc_version_prefix(version) - vs_def = Config.MSVC_VERSION_INTERNAL[verstr] + vc_series = Util.get_msvc_version_prefix(version) - version_args = MSVC_VERSION_ARGS_DEFINITION( + vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL[vc_series] + vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries] + + version_args = TOOLSET_VERSION_ARGS_DEFINITION( version = version, - vs_def = vs_def, + vc_buildtools_def = vc_buildtools_def, + is_user = is_user, ) return version_args -def _msvc_script_argument_uwp(env, msvc, arglist): +def _msvc_script_argument_uwp(env, msvc, arglist, target_arch): uwp_app = env['MSVC_UWP_APP'] debug('MSVC_VERSION=%s, MSVC_UWP_APP=%s', repr(msvc.version), repr(uwp_app)) @@ -207,17 +217,34 @@ def _msvc_script_argument_uwp(env, msvc, arglist): if uwp_app not in _ARGUMENT_BOOLEAN_TRUE_LEGACY: return None - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: msvc version constraint: %s < %s VS2015', - repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2015.vc_buildtools_def.vc_version_numeric) + repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric), + repr(VS2015.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_UWP_APP ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( - repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) + repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version) ) raise MSVCArgumentError(err_msg) + is_supported, is_target = Kind.msvc_version_uwp_is_supported(msvc.version, target_arch, env) + if not is_supported: + _, kind_str = Kind.get_msvc_version_kind(msvc.version) + debug( + 'invalid: msvc_version constraint: %s %s %s', + repr(msvc.version), repr(kind_str), repr(target_arch) + ) + if is_target and target_arch: + err_msg = "MSVC_UWP_APP ({}) TARGET_ARCH ({}) is not supported for MSVC_VERSION {} ({})".format( + repr(uwp_app), repr(target_arch), repr(msvc.version), repr(kind_str) + ) + else: + err_msg = "MSVC_UWP_APP ({}) is not supported for MSVC_VERSION {} ({})".format( + repr(uwp_app), repr(msvc.version), repr(kind_str) + ) + raise MSVCArgumentError(err_msg) + # VS2017+ rewrites uwp => store for 14.0 toolset uwp_arg = msvc.vs_def.vc_uwp @@ -250,16 +277,24 @@ def _user_script_argument_uwp(env, uwp, user_argstr) -> bool: raise MSVCArgumentError(err_msg) -def _msvc_script_argument_sdk_constraints(msvc, sdk_version): +def _msvc_script_argument_sdk_constraints(msvc, sdk_version, env): - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: msvc_version constraint: %s < %s VS2015', - repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2015.vc_buildtools_def.vc_version_numeric) + repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric), + repr(VS2015.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_SDK_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( - repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) + repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version) + ) + return err_msg + + if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env): + _, kind_str = Kind.get_msvc_version_kind(msvc.version) + debug('invalid: msvc_version constraint: %s %s', repr(msvc.version), repr(kind_str)) + err_msg = "MSVC_SDK_VERSION ({}) is not supported for MSVC_VERSION {} ({})".format( + repr(sdk_version), repr(msvc.version), repr(kind_str) ) return err_msg @@ -277,23 +312,23 @@ def _msvc_script_argument_sdk_platform_constraints(msvc, toolset, sdk_version, p if sdk_version == '8.1' and platform_def.is_uwp: - vs_def = toolset.vs_def if toolset else msvc.vs_def + vc_buildtools_def = toolset.vc_buildtools_def if toolset else msvc.vs_def.vc_buildtools_def - if vs_def.vc_buildtools_def.vc_version_numeric > VS2015.vc_buildtools_def.vc_version_numeric: + if vc_buildtools_def.msvc_version_numeric > VS2015.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: uwp/store SDK 8.1 msvc_version constraint: %s > %s VS2015', - repr(vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2015.vc_buildtools_def.vc_version_numeric) + repr(vc_buildtools_def.msvc_version_numeric), + repr(VS2015.vc_buildtools_def.msvc_version_numeric) ) - if toolset and toolset.vs_def != msvc.vs_def: - err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset version {} > {} VS2015".format( + if toolset and toolset.is_user: + err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset {} MSVC_VERSION {} > {} VS2015".format( repr(sdk_version), repr(platform_def.vc_platform), - repr(toolset.version), repr(VS2015.vc_buildtools_def.vc_version) + repr(toolset.version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version) ) else: err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: MSVC_VERSION {} > {} VS2015".format( repr(sdk_version), repr(platform_def.vc_platform), - repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) + repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version) ) return err_msg @@ -310,7 +345,7 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist): if not sdk_version: return None - err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version) + err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version, env) if err_msg: raise MSVCArgumentError(err_msg) @@ -333,7 +368,10 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist): def _msvc_script_default_sdk(env, msvc, platform_def, arglist, force_sdk: bool=False): - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric: + return None + + if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env): return None sdk_list = WinSDK.get_sdk_version_list(msvc.vs_def, platform_def) @@ -415,10 +453,11 @@ def _msvc_sxs_toolset_folder(msvc, sxs_folder): if Util.is_toolset_sxs(sxs_folder): return sxs_folder, sxs_folder - key = (msvc.vs_def.vc_buildtools_def.vc_version, sxs_folder) - if key in _msvc_sxs_bugfix_folder: - sxs_version = _msvc_sxs_bugfix_folder[key] - return sxs_folder, sxs_version + for vc_buildseries_def in msvc.vs_def.vc_buildtools_def.vc_buildseries_list: + key = (vc_buildseries_def.vc_version, sxs_folder) + sxs_version = _msvc_sxs_bugfix_folder.get(key) + if sxs_version: + return sxs_folder, sxs_version debug('sxs folder: ignore version=%s', repr(sxs_folder)) return None, None @@ -574,49 +613,60 @@ def _msvc_version_toolset_vcvars(msvc, vc_dir, toolset_version): def _msvc_script_argument_toolset_constraints(msvc, toolset_version): - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: msvc version constraint: %s < %s VS2017', - repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2017.vc_buildtools_def.vc_version_numeric) + repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric), + repr(VS2017.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format( - repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version) + repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version) ) return err_msg - toolset_verstr = Util.get_msvc_version_prefix(toolset_version) + toolset_series = Util.get_msvc_version_prefix(toolset_version) - if not toolset_verstr: + if not toolset_series: debug('invalid: msvc version: toolset_version=%s', repr(toolset_version)) err_msg = 'MSVC_TOOLSET_VERSION {} format is not supported'.format( repr(toolset_version) ) return err_msg - toolset_vernum = float(toolset_verstr) + toolset_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(toolset_series) + if not toolset_buildseries_def: + debug('invalid: msvc version: toolset_version=%s', repr(toolset_version)) + err_msg = 'MSVC_TOOLSET_VERSION {} build series {} is not supported'.format( + repr(toolset_version), repr(toolset_series) + ) + return err_msg + + toolset_buildtools_def = Config.VC_BUILDTOOLS_MAP[toolset_buildseries_def.vc_buildseries] - if toolset_vernum < VS2015.vc_buildtools_def.vc_version_numeric: + toolset_verstr = toolset_buildtools_def.msvc_version + toolset_vernum = toolset_buildtools_def.msvc_version_numeric + + if toolset_vernum < VS2015.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: toolset version constraint: %s < %s VS2015', - repr(toolset_vernum), repr(VS2015.vc_buildtools_def.vc_version_numeric) + repr(toolset_vernum), repr(VS2015.vc_buildtools_def.msvc_version_numeric) ) - err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} < {} VS2015".format( - repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.vc_version) + err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} < {} VS2015".format( + repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.msvc_version) ) return err_msg - if toolset_vernum > msvc.vs_def.vc_buildtools_def.vc_version_numeric: + if toolset_vernum > msvc.vs_def.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: toolset version constraint: toolset %s > %s msvc', - repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric) + repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric) ) - err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} > {} MSVC_VERSION".format( + err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} > {} MSVC_VERSION".format( repr(toolset_version), repr(toolset_verstr), repr(msvc.version) ) return err_msg - if toolset_vernum == VS2015.vc_buildtools_def.vc_version_numeric: + if toolset_vernum == VS2015.vc_buildtools_def.msvc_version_numeric: # tooset = 14.0 if Util.is_toolset_full(toolset_version): if not Util.is_toolset_140(toolset_version): @@ -624,7 +674,7 @@ def _msvc_script_argument_toolset_constraints(msvc, toolset_version): 'invalid: toolset version 14.0 constraint: %s != 14.0', repr(toolset_version) ) - err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} != '14.0'".format( + err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} != '14.0'".format( repr(toolset_version), repr(toolset_version) ) return err_msg @@ -688,7 +738,7 @@ def _msvc_script_argument_toolset(env, msvc, vc_dir, arglist): def _msvc_script_default_toolset(env, msvc, vc_dir, arglist, force_toolset: bool=False): - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric: return None toolset_default = _msvc_default_toolset(msvc, vc_dir) @@ -729,26 +779,26 @@ def _user_script_argument_toolset(env, toolset_version, user_argstr): def _msvc_script_argument_spectre_constraints(msvc, toolset, spectre_libs, platform_def): - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: msvc version constraint: %s < %s VS2017', - repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2017.vc_buildtools_def.vc_version_numeric) + repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric), + repr(VS2017.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format( - repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version) + repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version) ) return err_msg if toolset: - if toolset.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: + if toolset.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: toolset version constraint: %s < %s VS2017', - repr(toolset.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2017.vc_buildtools_def.vc_version_numeric) + repr(toolset.vc_buildtools_def.msvc_version_numeric), + repr(VS2017.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: toolset version {} < {} VS2017".format( - repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.vc_version) + repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.msvc_version) ) return err_msg @@ -836,14 +886,14 @@ def _msvc_script_argument_user(env, msvc, arglist): if not script_args: return None - if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: + if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric: debug( 'invalid: msvc version constraint: %s < %s VS2015', - repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), - repr(VS2015.vc_buildtools_def.vc_version_numeric) + repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric), + repr(VS2015.vc_buildtools_def.msvc_version_numeric) ) err_msg = "MSVC_SCRIPT_ARGS ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( - repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) + repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version) ) raise MSVCArgumentError(err_msg) @@ -873,7 +923,18 @@ def _msvc_process_construction_variables(env) -> bool: return False -def msvc_script_arguments(env, version, vc_dir, arg): +def msvc_script_arguments_has_uwp(env): + + if not _msvc_process_construction_variables(env): + return False + + uwp_app = env.get('MSVC_UWP_APP') + is_uwp = bool(uwp_app and uwp_app in _ARGUMENT_BOOLEAN_TRUE_LEGACY) + + debug('is_uwp=%s', is_uwp) + return is_uwp + +def msvc_script_arguments(env, version, vc_dir, arg=None): arguments = [arg] if arg else [] @@ -889,10 +950,12 @@ def msvc_script_arguments(env, version, vc_dir, arg): if _msvc_process_construction_variables(env): + target_arch = env.get('TARGET_ARCH') + # MSVC_UWP_APP if 'MSVC_UWP_APP' in env: - uwp = _msvc_script_argument_uwp(env, msvc, arglist) + uwp = _msvc_script_argument_uwp(env, msvc, arglist, target_arch) else: uwp = None @@ -926,7 +989,7 @@ def msvc_script_arguments(env, version, vc_dir, arg): if user_toolset: toolset = None elif toolset_version: - toolset = _toolset_version(toolset_version) + toolset = _toolset_version(toolset_version, is_user=True) elif default_toolset: toolset = _toolset_version(default_toolset) else: @@ -958,7 +1021,7 @@ def msvc_script_arguments(env, version, vc_dir, arg): if user_argstr: _user_script_argument_spectre(env, spectre, user_argstr) - if msvc.vs_def.vc_buildtools_def.vc_version == '14.0': + if msvc.vs_def.vc_buildtools_def.msvc_version == '14.0': if user_uwp and sdk_version and len(arglist) == 2: # VS2015 toolset argument order issue: SDK store => store SDK arglist_reverse = True @@ -1003,6 +1066,14 @@ def _msvc_toolset_versions_internal(msvc_version, vc_dir, full: bool=True, sxs: return toolset_versions +def _msvc_version_toolsets_internal(msvc_version, vc_dir): + + msvc = _msvc_version(msvc_version) + + toolsets_sxs, toolsets_full = _msvc_version_toolsets(msvc, vc_dir) + + return toolsets_sxs, toolsets_full + def _msvc_toolset_versions_spectre_internal(msvc_version, vc_dir): msvc = _msvc_version(msvc_version) diff --git a/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py b/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py index 670576b046..308e436976 100644 --- a/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py +++ b/SCons/Tool/MSCommon/MSVC/ScriptArgumentsTests.py @@ -36,6 +36,10 @@ from SCons.Tool.MSCommon.MSVC import Util from SCons.Tool.MSCommon.MSVC import WinSDK from SCons.Tool.MSCommon.MSVC import ScriptArguments +from SCons.Tool.MSCommon.MSVC.Kind import ( + msvc_version_is_express, + msvc_version_is_btdispatch, +) from SCons.Tool.MSCommon.MSVC.Exceptions import ( MSVCInternalError, @@ -127,7 +131,7 @@ class Data: for vcver in Config.MSVC_VERSION_SUFFIX.keys(): version_def = Util.msvc_version_components(vcver) - vc_dir = vc.find_vc_pdir(None, vcver) + vc_dir = vc.find_vc_pdir(vcver) t = (version_def, vc_dir) ALL_VERSIONS_PAIRS.append(t) if vc_dir: @@ -229,7 +233,13 @@ def test_msvc_script_arguments_defaults(self) -> None: for version_def, vc_dir in Data.INSTALLED_VERSIONS_PAIRS: for arg in ('', 'arch'): scriptargs = func(env, version_def.msvc_version, vc_dir, arg) - if version_def.msvc_vernum >= 14.0: + sdk_supported = True + if version_def.msvc_verstr == '14.0': + if msvc_version_is_express(version_def.msvc_version): + sdk_supported = False + elif msvc_version_is_btdispatch(version_def.msvc_version): + sdk_supported = False + if version_def.msvc_vernum >= 14.0 and sdk_supported: if arg and scriptargs.startswith(arg): testargs = scriptargs[len(arg):].lstrip() else: @@ -289,7 +299,7 @@ def run_msvc_script_args_none(self) -> None: {'MSVC_SCRIPT_ARGS': None, 'MSVC_SPECTRE_LIBS': None}, ]: env = Environment(**kwargs) - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) def run_msvc_script_args(self) -> None: func = ScriptArguments.msvc_script_arguments @@ -307,12 +317,17 @@ def run_msvc_script_args(self) -> None: toolset_def = toolset_versions[0] if toolset_versions else Util.msvc_extended_version_components(version_def.msvc_verstr) - earlier_toolset_versions = [toolset_def for toolset_def in toolset_versions if toolset_def.msvc_vernum != version_def.msvc_vernum] + earlier_toolset_versions = [earlier_toolset_def for earlier_toolset_def in toolset_versions if earlier_toolset_def.msvc_vernum != version_def.msvc_vernum] earlier_toolset_def = earlier_toolset_versions[0] if earlier_toolset_versions else None + vc_buildtools_def = Config.MSVC_BUILDTOOLS_EXTERNAL[toolset_def.msvc_buildtools] + vc_buildseries_def = vc_buildtools_def.vc_buildseries_list[0] + + latest_buildseries_major, latest_buildseries_minor = [int(comp) for comp in vc_buildseries_def.vc_version.split('.')] + # should not raise exception (argument not validated) env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol') - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) for kwargs in [ {'MSVC_UWP_APP': False, 'MSVC_SCRIPT_ARGS': None}, @@ -323,7 +338,7 @@ def run_msvc_script_args(self) -> None: {'MSVC_SPECTRE_LIBS': 'True', 'MSVC_SCRIPT_ARGS': '-vcvars_spectre_libs=spectre'}, # not boolean ignored ]: env = Environment(**kwargs) - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) for msvc_uwp_app in (True, False): @@ -362,15 +377,15 @@ def run_msvc_script_args(self) -> None: (expect, { 'MSVC_SDK_VERSION': sdk_def.sdk_version, 'MSVC_UWP_APP': msvc_uwp_app, - 'MSVC_TOOLSET_VERSION': version_def.msvc_verstr + 'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version }), ] + more_tests: env = Environment(**kwargs) if exc: with self.assertRaises(MSVCArgumentError): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) else: - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) else: @@ -379,14 +394,14 @@ def run_msvc_script_args(self) -> None: {'MSVC_SDK_VERSION': sdk_def.sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, ]: env = Environment(**kwargs) - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) for kwargs in [ - {'MSVC_SCRIPT_ARGS': '-vcvars_ver={}'.format(version_def.msvc_verstr)}, - {'MSVC_TOOLSET_VERSION': version_def.msvc_verstr}, + {'MSVC_SCRIPT_ARGS': '-vcvars_ver={}'.format(toolset_def.msvc_toolset_version)}, + {'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version}, ]: env = Environment(**kwargs) - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) msvc_toolset_notfound_version = Data.msvc_toolset_notfound_version(version_def.msvc_version) @@ -398,7 +413,7 @@ def run_msvc_script_args(self) -> None: ]: env = Environment(**kwargs) with self.assertRaises(MSVCToolsetVersionNotFound): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) msvc_sdk_notfound_version = Data.msvc_sdk_notfound_version(version_def.msvc_version) @@ -407,15 +422,15 @@ def run_msvc_script_args(self) -> None: ]: env = Environment(**kwargs) with self.assertRaises(MSVCSDKVersionNotFound): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) have_spectre = toolset_def.msvc_toolset_version in Data.SPECTRE_TOOLSET_VERSIONS.get(version_def.msvc_version,[]) env = Environment(MSVC_SPECTRE_LIBS=True, MSVC_TOOLSET_VERSION=toolset_def.msvc_toolset_version) if not have_spectre: with self.assertRaises(MSVCSpectreLibsNotFound): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) else: - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) msvc_sdk_version = Data.msvc_sdk_version(version_def.msvc_version) @@ -443,14 +458,14 @@ def run_msvc_script_args(self) -> None: }, (MSVCArgumentError, ), ), # multiple definitions - ({'MSVC_TOOLSET_VERSION': version_def.msvc_verstr, - 'MSVC_SCRIPT_ARGS': "-vcvars_ver={}".format(version_def.msvc_verstr) + ({'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version, + 'MSVC_SCRIPT_ARGS': "-vcvars_ver={}".format(toolset_def.msvc_toolset_version) }, (MSVCArgumentError, ), ), # multiple definitions (args) - ({'MSVC_TOOLSET_VERSION': version_def.msvc_verstr, - 'MSVC_SCRIPT_ARGS': "-vcvars_ver={0} undefined -vcvars_ver={0}".format(version_def.msvc_verstr) + ({'MSVC_TOOLSET_VERSION': toolset_def.msvc_toolset_version, + 'MSVC_SCRIPT_ARGS': "-vcvars_ver={0} undefined -vcvars_ver={0}".format(toolset_def.msvc_toolset_version) }, (MSVCArgumentError, ), ), @@ -484,7 +499,7 @@ def run_msvc_script_args(self) -> None: (MSVCArgumentError, ), ), # toolset > msvc_version - ({'MSVC_TOOLSET_VERSION': '{}.{}'.format(version_def.msvc_major, version_def.msvc_minor+1), + ({'MSVC_TOOLSET_VERSION': '{}.{}'.format(latest_buildseries_major, latest_buildseries_minor+1), }, (MSVCArgumentError, ), ), @@ -519,25 +534,90 @@ def run_msvc_script_args(self) -> None: ] + more_tests: env = Environment(**kwargs) with self.assertRaises(exc_t): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) elif version_def.msvc_verstr == '14.0': - # VS2015: MSVC_SDK_VERSION and MSVC_UWP_APP + + if msvc_version_is_express(version_def.msvc_version): + sdk_supported = False + uwp_supported = True # based on target arch + elif msvc_version_is_btdispatch(version_def.msvc_version): + sdk_supported = False + uwp_supported = False + else: + sdk_supported = True + uwp_supported = True env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol') - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) - for msvc_uwp_app in (True, False): + if sdk_supported: + # VS2015: MSVC_SDK_VERSION + + if uwp_supported: + # VS2015: MSVC_UWP_APP + + for msvc_uwp_app in (True, False): + + sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app) + for sdk_version in sdk_list: + + for kwargs in [ + {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, + {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, + ]: + env = Environment(**kwargs) + _ = func(env, version_def.msvc_version, vc_dir) + + else: + # VS2015: MSVC_UWP_APP error + + for msvc_uwp_app in (True,): + + sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app) + for sdk_version in sdk_list: + + for kwargs in [ + {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, + {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, + ]: + env = Environment(**kwargs) + with self.assertRaises(MSVCArgumentError): + _ = func(env, version_def.msvc_version, vc_dir) + + else: + # VS2015: MSVC_SDK_VERSION error sdk_list = WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app=msvc_uwp_app) for sdk_version in sdk_list: - for kwargs in [ - {'MSVC_SCRIPT_ARGS': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, - {'MSVC_SDK_VERSION': sdk_version, 'MSVC_UWP_APP': msvc_uwp_app}, - ]: - env = Environment(**kwargs) - _ = func(env, version_def.msvc_version, vc_dir, '') + env = Environment(MSVC_SDK_VERSION=sdk_version) + with self.assertRaises(MSVCArgumentError): + _ = func(env, version_def.msvc_version, vc_dir) + + # MSVC_SCRIPT_ARGS sdk_version not validated + env = Environment(MSVC_SCRIPT_ARGS=sdk_version) + _ = func(env, version_def.msvc_version, vc_dir) + + if uwp_supported: + # VS2015: MSVC_UWP_APP + + for msvc_uwp_app in (True, False): + env = Environment(MSVC_UWP_APP=msvc_uwp_app) + _ = func(env, version_def.msvc_version, vc_dir) + + else: + # VS2015: MSVC_UWP_APP error + + for msvc_uwp_app in (True,): + + env = Environment(MSVC_UWP_APP=msvc_uwp_app) + with self.assertRaises(MSVCArgumentError): + _ = func(env, version_def.msvc_version, vc_dir) + + # MSVC_SCRIPT_ARGS store not validated + env = Environment(MSVC_SCRIPT_ARGS='store') + _ = func(env, version_def.msvc_version, vc_dir) for kwargs in [ {'MSVC_SPECTRE_LIBS': True, 'MSVC_SCRIPT_ARGS': None}, @@ -545,14 +625,14 @@ def run_msvc_script_args(self) -> None: ]: env = Environment(**kwargs) with self.assertRaises(MSVCArgumentError): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) else: # VS2013 and earlier: no arguments env = Environment(MSVC_SCRIPT_ARGS='undefinedsymbol') with self.assertRaises(MSVCArgumentError): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) for kwargs in [ {'MSVC_UWP_APP': True, 'MSVC_SCRIPT_ARGS': None}, @@ -563,7 +643,7 @@ def run_msvc_script_args(self) -> None: ]: env = Environment(**kwargs) with self.assertRaises(MSVCArgumentError): - _ = func(env, version_def.msvc_version, vc_dir, '') + _ = func(env, version_def.msvc_version, vc_dir) def test_msvc_script_args_none(self) -> None: force = ScriptArguments.msvc_force_default_arguments(force=False) diff --git a/SCons/Tool/MSCommon/MSVC/Util.py b/SCons/Tool/MSCommon/MSVC/Util.py index 6fd188bc35..0da58e1e4d 100644 --- a/SCons/Tool/MSCommon/MSVC/Util.py +++ b/SCons/Tool/MSCommon/MSVC/Util.py @@ -37,6 +37,15 @@ from . import Config + +# call _initialize method upon class definition completion + +class AutoInitialize: + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if hasattr(cls, '_initialize') and callable(getattr(cls, '_initialize', None)): + cls._initialize() + # path utilities # windows drive specification (e.g., 'C:') @@ -234,6 +243,26 @@ def get_msvc_version_prefix(version): rval = m.group('version') return rval +def get_msvc_version_prefix_suffix(version): + """ + Get the msvc version number prefix and suffix from a string. + + Args: + version: str + version specification + + Returns: + (str, str): the msvc version prefix and suffix + + """ + prefix = suffix = '' + if version: + m = re_msvc_version.match(version) + if m: + prefix = m.group('msvc_version') + suffix = m.group('suffix') if m.group('suffix') else '' + return prefix, suffix + # toolset version query utilities def is_toolset_full(toolset_version) -> bool: @@ -322,15 +351,21 @@ def msvc_version_components(vcver): return msvc_version_components_def _MSVC_EXTENDED_VERSION_COMPONENTS_DEFINITION = namedtuple('MSVCExtendedVersionComponentsDefinition', [ - 'msvc_version', # msvc version (e.g., '14.1Exp') - 'msvc_verstr', # msvc version numeric string (e.g., '14.1') - 'msvc_suffix', # msvc version component type (e.g., 'Exp') - 'msvc_vernum', # msvc version floating point number (e.g, 14.1) - 'msvc_major', # msvc major version integer number (e.g., 14) - 'msvc_minor', # msvc minor version integer number (e.g., 1) - 'msvc_comps', # msvc version components tuple (e.g., ('14', '1')) + 'msvc_version', # msvc version (e.g., '14.1Exp') + 'msvc_verstr', # msvc version numeric string (e.g., '14.1') + 'msvc_suffix', # msvc version component type (e.g., 'Exp') + 'msvc_suffix_rank', # msvc version component rank (0, 1) + 'msvc_vernum', # msvc version floating point number (e.g, 14.1) + 'msvc_major', # msvc major version integer number (e.g., 14) + 'msvc_minor', # msvc minor version integer number (e.g., 1) + 'msvc_comps', # msvc version components tuple (e.g., ('14', '1')) + 'msvc_buildtools', # msvc build tools + 'msvc_buildtools_num', # msvc build tools integer number + 'msvc_buildseries', # msvc build series + 'msvc_buildseries_num', # msvc build series floating point number 'msvc_toolset_version', # msvc toolset version 'msvc_toolset_comps', # msvc toolset version components + 'msvc_toolset_is_sxs', # msvc toolset version is sxs 'version', # msvc version or msvc toolset version ]) @@ -355,11 +390,19 @@ def msvc_extended_version_components(version): msvc_toolset_version = m.group('version') msvc_toolset_comps = tuple(msvc_toolset_version.split('.')) + msvc_toolset_is_sxs = is_toolset_sxs(msvc_toolset_version) - msvc_verstr = get_msvc_version_prefix(msvc_toolset_version) - if not msvc_verstr: + vc_verstr = get_msvc_version_prefix(msvc_toolset_version) + if not vc_verstr: return None + vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(vc_verstr) + if not vc_buildseries_def: + return None + + vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries] + + msvc_verstr = vc_buildtools_def.msvc_version msvc_suffix = m.group('suffix') if m.group('suffix') else '' msvc_version = msvc_verstr + msvc_suffix @@ -376,12 +419,18 @@ def msvc_extended_version_components(version): msvc_version = msvc_version, msvc_verstr = msvc_verstr, msvc_suffix = msvc_suffix, + msvc_suffix_rank = 0 if not msvc_suffix else 1, msvc_vernum = msvc_vernum, msvc_major = msvc_major, msvc_minor = msvc_minor, msvc_comps = msvc_comps, + msvc_buildtools = vc_buildtools_def.msvc_version, + msvc_buildtools_num = vc_buildtools_def.msvc_version_numeric, + msvc_buildseries = vc_buildseries_def.vc_version, + msvc_buildseries_num = vc_buildseries_def.vc_version_numeric, msvc_toolset_version = msvc_toolset_version, msvc_toolset_comps = msvc_toolset_comps, + msvc_toolset_is_sxs = msvc_toolset_is_sxs, version = version, ) diff --git a/SCons/Tool/MSCommon/MSVC/__init__.py b/SCons/Tool/MSCommon/MSVC/__init__.py index 766894d9bb..f87b0f1d8c 100644 --- a/SCons/Tool/MSCommon/MSVC/__init__.py +++ b/SCons/Tool/MSCommon/MSVC/__init__.py @@ -40,6 +40,7 @@ from . import Config # noqa: F401 from . import Util # noqa: F401 from . import Registry # noqa: F401 +from . import Kind # noqa: F401 from . import SetupEnvDefault # noqa: F401 from . import Policy # noqa: F401 from . import WinSDK # noqa: F401 diff --git a/SCons/Tool/MSCommon/README.rst b/SCons/Tool/MSCommon/README.rst index 5ab07ad631..b37f409d3d 100644 --- a/SCons/Tool/MSCommon/README.rst +++ b/SCons/Tool/MSCommon/README.rst @@ -27,16 +27,176 @@ Design Notes ``MSCommon/vc.py`` and are available via the ``SCons.Tool.MSCommon`` namespace. +MSVC Detection Priority +======================= + +For msvc version specifications without an 'Exp' suffix, an express +installation is used only when no other installation is detected. + ++---------+---------+----------------------------------------------------------+ +| Product | VCVer | Priority | ++=========+=========+==========================================================+ +| VS2022 | 14.3 | Enterprise, Professional, Community, BuildTools | ++---------+---------+----------------------------------------------------------+ +| VS2019 | 14.2 | Enterprise, Professional, Community, BuildTools | ++---------+---------+----------------------------------------------------------+ +| VS2017 | 14.1 | Enterprise, Professional, Community, BuildTools, Express | ++---------+---------+----------------------------------------------------------+ +| VS2017 | 14.1Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2015 | 14.0 | [Develop, BuildTools, CmdLine], Express | ++---------+---------+----------------------------------------------------------+ +| VS2015 | 14.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2013 | 12.0 | Develop, Express | ++---------+---------+----------------------------------------------------------+ +| VS2013 | 12.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2012 | 11.0 | Develop, Express | ++---------+---------+----------------------------------------------------------+ +| VS2012 | 11.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2010 | 10.0 | Develop, Express | ++---------+---------+----------------------------------------------------------+ +| VS2010 | 10.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2008 | 9.0 | Develop, VCForPython, Express | ++---------+---------+----------------------------------------------------------+ +| VS2008 | 9.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2005 | 8.0 | Develop, Express | ++---------+---------+----------------------------------------------------------+ +| VS2005 | 8.0Exp | Express | ++---------+---------+----------------------------------------------------------+ +| VS2003 | 7.1 | Develop | ++---------+---------+----------------------------------------------------------+ +| VS2002 | 7.0 | Develop | ++---------+---------+----------------------------------------------------------+ +| VS6.0 | 6.0 | Develop | ++---------+---------+----------------------------------------------------------+ + +Legend: + + Develop + devenv.com or msdev.com is detected. + + Express + WDExpress.exe or VCExpress.exe is detected. + + BuildTools [VS2015] + The vcvarsall batch file dispatches to the buildtools batch file. + + CmdLine [VS2015] + Neither Develop, Express, or BuildTools. + +VS2015 Edition Limitations +========================== + +VS2015 BuildTools +----------------- + +The VS2015 BuildTools stand-alone batch file does not support the ``sdk version`` argument. + +The VS2015 BuildTools stand-alone batch file does not support the ``store`` argument. + +These arguments appear to be silently ignored and likely would result in compiler +and/or linker build failures. + +The VS2015 BuildTools ``vcvarsall.bat`` batch file dispatches to the stand-alone buildtools +batch file under certain circumstances. A fragment from the vcvarsall batch file is: + +:: + + if exist "%~dp0..\common7\IDE\devenv.exe" goto setup_VS + if exist "%~dp0..\common7\IDE\wdexpress.exe" goto setup_VS + if exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto setup_buildsku + + :setup_VS + + ... + + :setup_buildsku + if not exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto usage + set CurrentDir=%CD% + call "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" %1 %2 + cd /d %CurrentDir% + goto :eof + +VS2015 Express +-------------- + +The VS2015 Express batch file does not support the ``sdk version`` argument. + +The VS2015 Express batch file does not support the ``store`` argument for the ``amd64`` and +``arm`` target architectures + +amd64 Target Architecture +^^^^^^^^^^^^^^^^^^^^^^^^^ + +As installed, VS2015 Express does not support the ``store`` argument for the ``amd64`` target +architecture. The generated ``store`` library paths include directories that do not exist. + +The store library paths appear in two places in the ``vcvarsx86_amd64`` batch file: + +:: + + :setstorelib + @if exist "%VCINSTALLDIR%LIB\amd64\store" set LIB=%VCINSTALLDIR%LIB\amd64\store;%LIB% + ... + :setstorelibpath + @if exist "%VCINSTALLDIR%LIB\amd64\store" set LIBPATH=%VCINSTALLDIR%LIB\amd64\store;%LIBPATH% + +The correct store library paths would be: + +:: + + :setstorelib + @if exist "%VCINSTALLDIR%LIB\store\amd64" set LIB=%VCINSTALLDIR%LIB\store\amd64;%LIB% + ... + :setstorelibpath + @if exist "%VCINSTALLDIR%LIB\store\amd64" set LIBPATH=%VCINSTALLDIR%LIB\store\amd64;%LIBPATH% + +arm Target Architecture +^^^^^^^^^^^^^^^^^^^^^^^ + +As installed, VS2015 Express does not support the ``store`` argument for the ``arm`` target +architecture. The generated ``store`` library paths include directories that do not exist. + +The store library paths appear in two places in the ``vcvarsx86_arm`` batch file: + +:: + + :setstorelib + @if exist "%VCINSTALLDIR%LIB\ARM\store" set LIB=%VCINSTALLDIR%LIB\ARM\store;%LIB% + ... + :setstorelibpath + @if exist "%VCINSTALLDIR%LIB\ARM\store" set LIBPATH=%VCINSTALLDIR%LIB\ARM\store;%LIBPATH% + +The correct store library paths would be file: + +:: + + :setstorelib + @if exist "%VCINSTALLDIR%LIB\store\ARM" set LIB=%VCINSTALLDIR%LIB\store\ARM;%LIB% + ... + :setstorelibpath + @if exist "%VCINSTALLDIR%LIB\store\ARM" set LIBPATH=%VCINSTALLDIR%LIB\store\ARM;%LIBPATH% + + Known Issues ============ The following issues are known to exist: * Using ``MSVC_USE_SCRIPT`` and ``MSVC_USE_SCRIPT_ARGS`` to call older Microsoft SDK - ``SetEnv.cmd`` batch files may result in build failures. Some of these batch files - require delayed expansion to be enabled which is not usually the Windows default. - One solution would be to launch the MSVC batch file command in a new command interpreter - instance with delayed expansion enabled via command-line options. + ``SetEnv.cmd`` batch files may result in build failures. + + Typically, the reasons for build failures with SDK batch files are one, or both, of: + + * The batch files require delayed expansion to be enabled which is not usually the Windows default. + + * The batch files inspect environment variables that are not defined in the minimal subprocess + environment in which the batch files are invoked. * The code to suppress the "No versions of the MSVC compiler were found" warning for the default environment was moved from ``MSCommon/vc.py`` to ``MSCommon/MSVC/SetupEnvDefault.py``. @@ -47,20 +207,24 @@ The following issues are known to exist: Experimental Features ===================== -msvc_query_version_toolset(version=None, prefer_newest=True) ------------------------------------------------------------- +msvc_query_version_toolset(version=None, prefer_newest=True, vswhere_exe=None) +------------------------------------------------------------------------------ The experimental function ``msvc_query_version_toolset`` was added to ``MSCommon/vc.py`` -and is available via the ``SCons.Tool.MSCommon`` namespace. This function takes a version -specification or a toolset version specification and a product preference as arguments and -returns the msvc version and the msvc toolset version for the corresponding version specification. +and is available via the ``SCons.Tool.MSCommon`` namespace. + +This function takes a version specification or a toolset version specification, an optional product +preference, and an optional vswhere executable location as arguments and returns the msvc version and +the msvc toolset version for the corresponding version specification. This is a proxy for using the toolset version for selection until that functionality can be added. Example usage: + :: for version in [ + '14.4', '14.3', '14.2', '14.1', @@ -90,6 +254,7 @@ Example usage: print('{}Query: {} version={}, prefer_newest={}'.format(newline, msg, version, prefer_newest)) Example output fragment + :: Build: _build003 {'MSVC_VERSION': '14.3', 'MSVC_TOOLSET_VERSION': '14.29.30133'} @@ -117,6 +282,7 @@ added to the batch file argument list. This is intended to make the cache more updates that may change the default toolset version and/or the default SDK version. Example usage: + :: @echo Enabling scons cache ... @@ -139,6 +305,7 @@ Enabling warnings to be produced for detected msvc batch file errors may provide for build failures. Refer to the documentation for details. Change the default policy: + :: from SCons.Tool.MSCommon import msvc_set_scripterror_policy @@ -146,6 +313,7 @@ Change the default policy: msvc_set_scripterror_policy('Warning') Specify the policy per-environment: + :: env = Environment(MSVC_VERSION='14.3', MSVC_SPECTRE_LIBS=True, MSVC_SCRIPTERROR_POLICY='Warning') @@ -171,6 +339,7 @@ On occasion, the raw vswhere output may prove useful especially if there are sus detection of installed msvc instances. Windows command-line sample invocations: + :: @rem 64-Bit Windows @@ -188,31 +357,37 @@ Batch File Arguments Supported MSVC batch file arguments by product: -======= === === ======= ======= -Product UWP SDK Toolset Spectre -======= === === ======= ======= -VS2022 X X X X -------- --- --- ------- ------- -VS2019 X X X X -------- --- --- ------- ------- -VS2017 X X X X -------- --- --- ------- ------- -VS2015 X X -======= === === ======= ======= ++---------+---------+--------+---------+---------+ +| Product | UWP | SDK | Toolset | Spectre | ++=========+=========+========+=========+=========+ +| VS2022 | X | X | X | X | ++---------+---------+--------+---------+---------+ +| VS2019 | X | X | X | X | ++---------+---------+--------+---------+---------+ +| VS2017 | X | X | X | X | ++---------+---------+--------+---------+---------+ +| VS2015 | X [1] | X [2] | | | ++---------+---------+--------+---------+---------+ + +Notes: + +1) The BuildTools edition does not support the ``store`` argument. The Express edition + supports the ``store`` argument for the ``x86`` target only. +2) The ``sdk version`` argument is not supported in the BuildTools and Express editions. Supported MSVC batch file arguments in SCons: -======== ====================================== =================================================== -Argument Construction Variable Script Argument Equivalent -======== ====================================== =================================================== -UWP ``MSVC_UWP_APP=True`` ``MSVC_SCRIPT_ARGS='store'`` --------- -------------------------------------- --------------------------------------------------- -SDK ``MSVC_SDK_VERSION='10.0.20348.0'`` ``MSVC_SCRIPT_ARGS='10.0.20348.0'`` --------- -------------------------------------- --------------------------------------------------- -Toolset ``MSVC_TOOLSET_VERSION='14.31.31103'`` ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'`` --------- -------------------------------------- --------------------------------------------------- -Spectre ``MSVC_SPECTRE_LIBS=True`` ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'`` -======== ====================================== =================================================== ++----------+----------------------------------------+-----------------------------------------------------+ +| Argument | Construction Variable | Script Argument Equivalent | ++==========+========================================+=====================================================+ +| UWP | ``MSVC_UWP_APP=True`` | ``MSVC_SCRIPT_ARGS='store'`` | ++----------+----------------------------------------+-----------------------------------------------------+ +| SDK | ``MSVC_SDK_VERSION='10.0.20348.0'`` | ``MSVC_SCRIPT_ARGS='10.0.20348.0'`` | ++----------+----------------------------------------+-----------------------------------------------------+ +| Toolset | ``MSVC_TOOLSET_VERSION='14.31.31103'`` | ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'`` | ++----------+----------------------------------------+-----------------------------------------------------+ +| Spectre | ``MSVC_SPECTRE_LIBS=True`` | ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'`` | ++----------+----------------------------------------+-----------------------------------------------------+ **MSVC_SCRIPT_ARGS contents are not validated. Utilizing script arguments that have construction variable equivalents is discouraged and may lead to difficult to diagnose build errors.** @@ -244,6 +419,7 @@ that the msvc batch files would return. When using ``MSVC_SCRIPT_ARGS``, the toolset specification should be omitted entirely. Local installation and summary test results: + :: VS2022\VC\Auxiliary\Build\Microsoft.VCToolsVersion.v143.default.txt @@ -253,6 +429,7 @@ Local installation and summary test results: 14.32.31326 Toolset version summary: + :: 14.31.31103 Environment() @@ -268,6 +445,7 @@ Toolset version summary: 14.32.31326 Environment(MSVC_SCRIPT_ARGS=['-vcvars_ver=14.32']) VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment: + :: @echo -vcvars_ver=version : Version of VC++ Toolset to select @@ -289,6 +467,7 @@ VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment: @echo SxS toolset to [VSInstallDir]\VC\MSVC\Tools\ directory. VS2022 batch file fragment to determine the default toolset version: + :: @REM Add MSVC @@ -315,75 +494,114 @@ Visual Studio Version Notes SDK Versions ------------ -==== ============ -SDK Format -==== ============ -10.0 10.0.XXXXX.Y ----- ------------ -8.1 8.1 -==== ============ ++------+-------------------+ +| SDK | Format | ++======+===================+ +| 10.0 | 10.0.XXXXX.Y [1] | ++------+-------------------+ +| 8.1 | 8.1 | ++------+-------------------+ + +Notes: + +1) The Windows 10 SDK version number is 10.0.20348.0 and earlier. + + The Windows 11 SDK version number is 10.0.22000.194 and later. + +BuildSeries Versions +-------------------- + ++-------------+-------+-------+ +| BuildSeries | VCVER | CLVER | ++=============+=======+=======+ +| 14.4 | 14.4X | 19.4 | ++-------------+-------+-------+ +| 14.3 | 14.3X | 19.3 | ++-------------+-------+-------+ +| 14.2 | 14.2X | 19.2 | ++-------------+-------+-------+ +| 14.1 | 14.1X | 19.1 | ++-------------+-------+-------+ +| 14.0 | 14.0 | 19.0 | ++-------------+-------+-------+ +| 12.0 | 12.0 | 18.0 | ++-------------+-------+-------+ +| 11.0 | 11.0 | 17.0 | ++-------------+-------+-------+ +| 10.0 | 10.0 | 16.0 | ++-------------+-------+-------+ +| 9.0 | 9.0 | 15.0 | ++-------------+-------+-------+ +| 8.0 | 8.0 | 14.0 | ++-------------+-------+-------+ +| 7.1 | 7.1 | 13.1 | ++-------------+-------+-------+ +| 7.0 | 7.0 | 13.0 | ++-------------+-------+-------+ +| 6.0 | 6.0 | 12.0 | ++-------------+-------+-------+ BuildTools Versions ------------------- -========== ===== ===== ======== -BuildTools VCVER CLVER MSVCRT -========== ===== ===== ======== -v143 14.3 19.3 140/ucrt ----------- ----- ----- -------- -v142 14.2 19.2 140/ucrt ----------- ----- ----- -------- -v141 14.1 19.1 140/ucrt ----------- ----- ----- -------- -v140 14.0 19.0 140/ucrt ----------- ----- ----- -------- -v120 12.0 18.0 120 ----------- ----- ----- -------- -v110 11.0 17.0 110 ----------- ----- ----- -------- -v100 10.0 16.0 100 ----------- ----- ----- -------- -v90 9.0 15.0 90 ----------- ----- ----- -------- -v80 8.0 14.0 80 ----------- ----- ----- -------- -v71 7.1 13.1 71 ----------- ----- ----- -------- -v70 7.0 13.0 70 ----------- ----- ----- -------- -v60 6.0 12.0 60 -========== ===== ===== ======== ++------------+-------------+----------+ +| BuildTools | BuildSeries | MSVCRT | ++============+=============+==========+ +| v143 | 14.4, 14.3 | 140/ucrt | ++------------+-------------+----------+ +| v142 | 14.2 | 140/ucrt | ++------------+-------------+----------+ +| v141 | 14.1 | 140/ucrt | ++------------+-------------+----------+ +| v140 | 14.0 | 140/ucrt | ++------------+-------------+----------+ +| v120 | 12.0 | 120 | ++------------+-------------+----------+ +| v110 | 11.0 | 110 | ++------------+-------------+----------+ +| v100 | 10.0 | 100 | ++------------+-------------+----------+ +| v90 | 9.0 | 90 | ++------------+-------------+----------+ +| v80 | 8.0 | 80 | ++------------+-------------+----------+ +| v71 | 7.1 | 71 | ++------------+-------------+----------+ +| v70 | 7.0 | 70 | ++------------+-------------+----------+ +| v60 | 6.0 | 60 | ++------------+-------------+----------+ Product Versions ---------------- -======== ===== ========= ====================== -Product VSVER SDK BuildTools -======== ===== ========= ====================== -2022 17.0 10.0, 8.1 v143, v142, v141, v140 --------- ----- --------- ---------------------- -2019 16.0 10.0, 8.1 v142, v141, v140 --------- ----- --------- ---------------------- -2017 15.0 10.0, 8.1 v141, v140 --------- ----- --------- ---------------------- -2015 14.0 10.0, 8.1 v140 --------- ----- --------- ---------------------- -2013 12.0 v120 --------- ----- --------- ---------------------- -2012 11.0 v110 --------- ----- --------- ---------------------- -2010 10.0 v100 --------- ----- --------- ---------------------- -2008 9.0 v90 --------- ----- --------- ---------------------- -2005 8.0 v80 --------- ----- --------- ---------------------- -2003.NET 7.1 v71 --------- ----- --------- ---------------------- -2002.NET 7.0 v70 --------- ----- --------- ---------------------- -6.0 6.0 v60 -======== ===== ========= ====================== ++----------+-------+-------+-----------+------------------------+ +| Product | VSVER | SCons | SDK | BuildTools | ++==========+=======+=======+===========+========================+ +| 2022 | 17.0 | 14.3 | 10.0, 8.1 | v143, v142, v141, v140 | ++----------+-------+-------+-----------+------------------------+ +| 2019 | 16.0 | 14.2 | 10.0, 8.1 | v142, v141, v140 | ++----------+-------+-------+-----------+------------------------+ +| 2017 | 15.0 | 14.1 | 10.0, 8.1 | v141, v140 | ++----------+-------+-------+-----------+------------------------+ +| 2015 | 14.0 | 14.0 | 10.0, 8.1 | v140 | ++----------+-------+-------+-----------+------------------------+ +| 2013 | 12.0 | 12.0 | | v120 | ++----------+-------+-------+-----------+------------------------+ +| 2012 | 11.0 | 11.0 | | v110 | ++----------+-------+-------+-----------+------------------------+ +| 2010 | 10.0 | 10.0 | | v100 | ++----------+-------+-------+-----------+------------------------+ +| 2008 | 9.0 | 9.0 | | v90 | ++----------+-------+-------+-----------+------------------------+ +| 2005 | 8.0 | 8.0 | | v80 | ++----------+-------+-------+-----------+------------------------+ +| 2003.NET | 7.1 | 7.1 | | v71 | ++----------+-------+-------+-----------+------------------------+ +| 2002.NET | 7.0 | 7.0 | | v70 | ++----------+-------+-------+-----------+------------------------+ +| 6.0 | 6.0 | 6.0 | | v60 | ++----------+-------+-------+-----------+------------------------+ SCons Implementation Notes diff --git a/SCons/Tool/MSCommon/__init__.py b/SCons/Tool/MSCommon/__init__.py index c3078ac630..63f9ae4de6 100644 --- a/SCons/Tool/MSCommon/__init__.py +++ b/SCons/Tool/MSCommon/__init__.py @@ -45,6 +45,9 @@ msvc_toolset_versions, msvc_toolset_versions_spectre, msvc_query_version_toolset, + vswhere_register_executable, + vswhere_get_executable, + vswhere_freeze_executable, ) from SCons.Tool.MSCommon.vs import ( # noqa: F401 @@ -60,6 +63,8 @@ msvc_get_notfound_policy, msvc_set_scripterror_policy, msvc_get_scripterror_policy, + msvc_notfound_policy_contextmanager, + msvc_scripterror_policy_contextmanager, ) from .MSVC.Exceptions import ( # noqa: F401 @@ -78,7 +83,9 @@ MSVCUnsupportedHostArch, MSVCUnsupportedTargetArch, MSVCScriptNotFound, + MSVCUseScriptError, MSVCUseSettingsError, + VSWhereUserError, ) from .MSVC.Util import ( # noqa: F401 diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py index af3afb5150..27a8460393 100644 --- a/SCons/Tool/MSCommon/common.py +++ b/SCons/Tool/MSCommon/common.py @@ -30,19 +30,67 @@ import os import re import sys +import time from contextlib import suppress from subprocess import DEVNULL, PIPE from pathlib import Path +import SCons.Errors import SCons.Util import SCons.Warnings + +# TODO: Hard-coded list of the variables that (may) need to be +# imported from os.environ[] for the chain of development batch +# files to execute correctly. One call to vcvars*.bat may +# end up running a dozen or more scripts, changes not only with +# each release but with what is installed at the time. We think +# in modern installations most are set along the way and don't +# need to be picked from the env, but include these for safety's sake. +# Any VSCMD variables definitely are picked from the env and +# control execution in interesting ways. +# Note these really should be unified - either controlled by vs.py, +# or synced with the the common_tools_var # settings in vs.py. +VS_VC_VARS = [ + 'COMSPEC', # path to "shell" + 'OS', # name of OS family: Windows_NT or undefined (95/98/ME) + 'VS170COMNTOOLS', # path to common tools for given version + 'VS160COMNTOOLS', + 'VS150COMNTOOLS', + 'VS140COMNTOOLS', + 'VS120COMNTOOLS', + 'VS110COMNTOOLS', + 'VS100COMNTOOLS', + 'VS90COMNTOOLS', + 'VS80COMNTOOLS', + 'VS71COMNTOOLS', + 'VSCOMNTOOLS', + 'MSDevDir', + 'VSCMD_DEBUG', # enable logging and other debug aids + 'VSCMD_SKIP_SENDTELEMETRY', + 'windir', # windows directory (SystemRoot not available in 95/98/ME) + 'VCPKG_DISABLE_METRICS', + 'VCPKG_ROOT', + 'POWERSHELL_TELEMETRY_OPTOUT', + 'PSDisableModuleAnalysisCacheCleanup', + 'PSModuleAnalysisCachePath', +] + class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault): pass +def _check_logfile(logfile): + if logfile and '"' in logfile: + err_msg = ( + "SCONS_MSCOMMON_DEBUG value contains double quote character(s)\n" + f" SCONS_MSCOMMON_DEBUG={logfile}" + ) + raise SCons.Errors.UserError(err_msg) + return logfile + # SCONS_MSCOMMON_DEBUG is internal-use so undocumented: # set to '-' to print to console, else set to filename to log to -LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG') +LOGFILE = _check_logfile(os.environ.get('SCONS_MSCOMMON_DEBUG')) if LOGFILE: import logging @@ -129,7 +177,15 @@ def format(self, record): log_handler = logging.StreamHandler(sys.stdout) else: log_prefix = '' - log_handler = logging.FileHandler(filename=LOGFILE) + try: + log_handler = logging.FileHandler(filename=LOGFILE) + except (OSError, FileNotFoundError) as e: + err_msg = ( + "Could not create logfile, check SCONS_MSCOMMON_DEBUG\n" + f" SCONS_MSCOMMON_DEBUG={LOGFILE}\n" + f" {e.__class__.__name__}: {str(e)}" + ) + raise SCons.Errors.UserError(err_msg) log_formatter = _CustomFormatter(log_prefix) log_handler.setFormatter(log_formatter) logger = logging.getLogger(name=__name__) @@ -287,6 +343,112 @@ def _force_vscmd_skip_sendtelemetry(env): return True +class _PathManager: + + _PSEXECUTABLES = ( + "pwsh.exe", + "powershell.exe", + ) + + _PSMODULEPATH_MAP = {os.path.normcase(os.path.abspath(p)): p for p in [ + # os.path.expandvars(r"%USERPROFILE%\Documents\PowerShell\Modules"), # current user + os.path.expandvars(r"%ProgramFiles%\PowerShell\Modules"), # all users + os.path.expandvars(r"%ProgramFiles%\PowerShell\7\Modules"), # installation location + # os.path.expandvars(r"%USERPROFILE%\Documents\WindowsPowerShell\Modules"), # current user + os.path.expandvars(r"%ProgramFiles%\WindowsPowerShell\Modules"), # all users + os.path.expandvars(r"%windir%\System32\WindowsPowerShell\v1.0\Modules"), # installation location + ]} + + _cache_norm_path = {} + + @classmethod + def _get_norm_path(cls, p): + norm_path = cls._cache_norm_path.get(p) + if norm_path is None: + norm_path = os.path.normcase(os.path.abspath(p)) + cls._cache_norm_path[p] = norm_path + cls._cache_norm_path[norm_path] = norm_path + return norm_path + + _cache_is_psmodulepath = {} + + @classmethod + def _is_psmodulepath(cls, p): + is_psmodulepath = cls._cache_is_psmodulepath.get(p) + if is_psmodulepath is None: + norm_path = cls._get_norm_path(p) + is_psmodulepath = bool(norm_path in cls._PSMODULEPATH_MAP) + cls._cache_is_psmodulepath[p] = is_psmodulepath + cls._cache_is_psmodulepath[norm_path] = is_psmodulepath + return is_psmodulepath + + _cache_psmodulepath_paths = {} + + @classmethod + def get_psmodulepath_paths(cls, pathspec): + psmodulepath_paths = cls._cache_psmodulepath_paths.get(pathspec) + if psmodulepath_paths is None: + psmodulepath_paths = [] + for p in pathspec.split(os.pathsep): + p = p.strip() + if not p: + continue + if not cls._is_psmodulepath(p): + continue + psmodulepath_paths.append(p) + psmodulepath_paths = tuple(psmodulepath_paths) + cls._cache_psmodulepath_paths[pathspec] = psmodulepath_paths + return psmodulepath_paths + + _cache_psexe_paths = {} + + @classmethod + def get_psexe_paths(cls, pathspec): + psexe_paths = cls._cache_psexe_paths.get(pathspec) + if psexe_paths is None: + psexe_set = set(cls._PSEXECUTABLES) + psexe_paths = [] + for p in pathspec.split(os.pathsep): + p = p.strip() + if not p: + continue + for psexe in psexe_set: + psexe_path = os.path.join(p, psexe) + if not os.path.exists(psexe_path): + continue + psexe_paths.append(p) + psexe_set.remove(psexe) + break + if psexe_set: + continue + break + psexe_paths = tuple(psexe_paths) + cls._cache_psexe_paths[pathspec] = psexe_paths + return psexe_paths + + _cache_minimal_pathspec = {} + + @classmethod + def get_minimal_pathspec(cls, pathlist): + pathlist_t = tuple(pathlist) + minimal_pathspec = cls._cache_minimal_pathspec.get(pathlist_t) + if minimal_pathspec is None: + minimal_paths = [] + seen = set() + for p in pathlist: + p = p.strip() + if not p: + continue + norm_path = cls._get_norm_path(p) + if norm_path in seen: + continue + seen.add(norm_path) + minimal_paths.append(p) + minimal_pathspec = os.pathsep.join(minimal_paths) + cls._cache_minimal_pathspec[pathlist_t] = minimal_pathspec + return minimal_pathspec + + def normalize_env(env, keys, force: bool=False): """Given a dictionary representing a shell environment, add the variables from os.environ needed for the processing of .bat files; the keys are @@ -307,6 +469,11 @@ def normalize_env(env, keys, force: bool=False): for k in keys: if k in os.environ and (force or k not in normenv): normenv[k] = os.environ[k] + debug("keys: normenv[%s]=%s", k, normenv[k]) + else: + debug("keys: skipped[%s]", k) + + syspath_pathlist = normenv.get("PATH", "").split(os.pathsep) # add some things to PATH to prevent problems: # Shouldn't be necessary to add system32, since the default environment @@ -314,24 +481,37 @@ def normalize_env(env, keys, force: bool=False): sys32_dir = os.path.join( os.environ.get("SystemRoot", os.environ.get("windir", r"C:\Windows")), "System32" ) - if sys32_dir not in normenv["PATH"]: - normenv["PATH"] = normenv["PATH"] + os.pathsep + sys32_dir + syspath_pathlist.append(sys32_dir) # Without Wbem in PATH, vcvarsall.bat has a "'wmic' is not recognized" # error starting with Visual Studio 2017, although the script still # seems to work anyway. sys32_wbem_dir = os.path.join(sys32_dir, 'Wbem') - if sys32_wbem_dir not in normenv['PATH']: - normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_wbem_dir + syspath_pathlist.append(sys32_wbem_dir) # Without Powershell in PATH, an internal call to a telemetry # function (starting with a VS2019 update) can fail # Note can also set VSCMD_SKIP_SENDTELEMETRY to avoid this. - sys32_ps_dir = os.path.join(sys32_dir, r'WindowsPowerShell\v1.0') - if sys32_ps_dir not in normenv['PATH']: - normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_ps_dir + # Find the powershell executable paths. Add the known powershell.exe + # path to the end of the shell system path (just in case). + # The VS vcpkg component prefers pwsh.exe if it's on the path. + sys32_ps_dir = os.path.join(sys32_dir, 'WindowsPowerShell', 'v1.0') + psexe_searchlist = os.pathsep.join([os.environ.get("PATH", ""), sys32_ps_dir]) + psexe_pathlist = _PathManager.get_psexe_paths(psexe_searchlist) + + # Add powershell executable paths in the order discovered. + syspath_pathlist.extend(psexe_pathlist) + + normenv['PATH'] = _PathManager.get_minimal_pathspec(syspath_pathlist) debug("PATH: %s", normenv['PATH']) + + # Add psmodulepath paths in the order discovered. + psmodulepath_pathlist = _PathManager.get_psmodulepath_paths(os.environ.get("PSModulePath", "")) + if psmodulepath_pathlist: + normenv["PSModulePath"] = _PathManager.get_minimal_pathspec(psmodulepath_pathlist) + + debug("PSModulePath: %s", normenv.get('PSModulePath','')) return normenv @@ -342,41 +522,13 @@ def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False): # Create a blank environment, for use in launching the tools env = SCons.Environment.Environment(tools=[]) - # TODO: Hard-coded list of the variables that (may) need to be - # imported from os.environ[] for the chain of development batch - # files to execute correctly. One call to vcvars*.bat may - # end up running a dozen or more scripts, changes not only with - # each release but with what is installed at the time. We think - # in modern installations most are set along the way and don't - # need to be picked from the env, but include these for safety's sake. - # Any VSCMD variables definitely are picked from the env and - # control execution in interesting ways. - # Note these really should be unified - either controlled by vs.py, - # or synced with the the common_tools_var # settings in vs.py. - vs_vc_vars = [ - 'COMSPEC', # path to "shell" - 'OS', # name of OS family: Windows_NT or undefined (95/98/ME) - 'VS170COMNTOOLS', # path to common tools for given version - 'VS160COMNTOOLS', - 'VS150COMNTOOLS', - 'VS140COMNTOOLS', - 'VS120COMNTOOLS', - 'VS110COMNTOOLS', - 'VS100COMNTOOLS', - 'VS90COMNTOOLS', - 'VS80COMNTOOLS', - 'VS71COMNTOOLS', - 'VSCOMNTOOLS', - 'MSDevDir', - 'VSCMD_DEBUG', # enable logging and other debug aids - 'VSCMD_SKIP_SENDTELEMETRY', - 'windir', # windows directory (SystemRoot not available in 95/98/ME) - ] - env['ENV'] = normalize_env(env['ENV'], vs_vc_vars, force=False) + env['ENV'] = normalize_env(env['ENV'], VS_VC_VARS, force=False) if skip_sendtelemetry: _force_vscmd_skip_sendtelemetry(env) + # debug("ENV=%r", env['ENV']) + if args: debug("Calling '%s %s'", vcbat, args) cmd_str = '"%s" %s & set' % (vcbat, args) @@ -384,10 +536,15 @@ def get_output(vcbat, args=None, env=None, skip_sendtelemetry=False): debug("Calling '%s'", vcbat) cmd_str = '"%s" & set' % vcbat + beg_time = time.time() + cp = SCons.Action.scons_subproc_run( env, cmd_str, stdin=DEVNULL, stdout=PIPE, stderr=PIPE, ) + end_time = time.time() + debug("Elapsed %.2fs", end_time - beg_time) + # Extra debug logic, uncomment if necessary # debug('stdout:%s', cp.stdout) # debug('stderr:%s', cp.stderr) @@ -442,6 +599,7 @@ def add_env(rmatch, key, dkeep=dkeep) -> None: # it. path = path.strip('"') dkeep[key].append(str(path)) + debug("dkeep[%s].append(%r)", key, path) for line in output.splitlines(): for k, value in rdk.items(): diff --git a/SCons/Tool/MSCommon/sdk.py b/SCons/Tool/MSCommon/sdk.py index c6600f3a5b..a06dfacfc6 100644 --- a/SCons/Tool/MSCommon/sdk.py +++ b/SCons/Tool/MSCommon/sdk.py @@ -372,7 +372,7 @@ def mssdk_setup_env(env): return msvs_version = env.subst(msvs_version) from . import vs - msvs = vs.get_vs_by_version(msvs_version) + msvs = vs.get_vs_by_version(msvs_version, env) debug('mssdk_setup_env:msvs is :%s', msvs) if not msvs: debug('mssdk_setup_env: no VS version detected, bailingout:%s', msvs) diff --git a/SCons/Tool/MSCommon/vc.py b/SCons/Tool/MSCommon/vc.py index e43c618cbd..032dbf202e 100644 --- a/SCons/Tool/MSCommon/vc.py +++ b/SCons/Tool/MSCommon/vc.py @@ -25,9 +25,6 @@ MS Compilers: Visual C/C++ detection and configuration. # TODO: -# * gather all the information from a single vswhere call instead -# of calling repeatedly (use json format?) -# * support passing/setting location for vswhere in env. # * supported arch for versions: for old versions of batch file without # argument, giving bogus argument cannot be detected, so we have to hardcode # this here @@ -52,19 +49,26 @@ namedtuple, OrderedDict, ) +import json +from functools import cmp_to_key +import enum import SCons.Util import SCons.Warnings from SCons.Tool import find_program_path from . import common -from .common import CONFIG_CACHE, debug +from .common import ( + CONFIG_CACHE, + debug, +) from .sdk import get_installed_sdks from . import MSVC from .MSVC.Exceptions import ( VisualCException, + MSVCInternalError, MSVCUserError, MSVCArgumentError, MSVCToolsetVersionNotFound, @@ -81,9 +85,15 @@ class MSVCUnsupportedTargetArch(VisualCException): class MSVCScriptNotFound(MSVCUserError): pass +class MSVCUseScriptError(MSVCUserError): + pass + class MSVCUseSettingsError(MSVCUserError): pass +class VSWhereUserError(MSVCUserError): + pass + # internal exceptions class UnsupportedVersion(VisualCException): @@ -102,8 +112,8 @@ class BatchFileExecutionError(VisualCException): # MSVC 9.0 preferred query order: # True: VCForPython, VisualStudio -# FAlse: VisualStudio, VCForPython -_VC90_Prefer_VCForPython = True +# False: VisualStudio, VCForPython +_VC90_Prefer_VCForPython = False # Dict to 'canonalize' the arch _ARCH_TO_CANONICAL = { @@ -393,7 +403,7 @@ def _make_target_host_map(all_hosts, host_all_targets_map): # debug("_LE2019_HOST_TARGET_CFG: %s", _LE2019_HOST_TARGET_CFG) -# 14.0 (VS2015) to 8.0 (VS2005) +# 14.0 (VS2015) to 10.0 (VS2010) # Given a (host, target) tuple, return a tuple containing the argument for # the batch file and a tuple of the path components to find cl.exe. @@ -402,23 +412,23 @@ def _make_target_host_map(all_hosts, host_all_targets_map): # bin directory (i.e., /VC/bin). Any other tools are in subdirectory # named for the the host/target pair or a single name if the host==target. -_LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS = { +_LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS = { - ('amd64', 'amd64') : ('amd64', ('bin', 'amd64')), - ('amd64', 'x86') : ('amd64_x86', ('bin', 'amd64_x86')), - ('amd64', 'arm') : ('amd64_arm', ('bin', 'amd64_arm')), + ('amd64', 'amd64') : ('amd64', 'vcvars64.bat', ('bin', 'amd64')), + ('amd64', 'x86') : ('amd64_x86', 'vcvarsamd64_x86.bat', ('bin', 'amd64_x86')), + ('amd64', 'arm') : ('amd64_arm', 'vcvarsamd64_arm.bat', ('bin', 'amd64_arm')), - ('x86', 'amd64') : ('x86_amd64', ('bin', 'x86_amd64')), - ('x86', 'x86') : ('x86', ('bin', )), - ('x86', 'arm') : ('x86_arm', ('bin', 'x86_arm')), - ('x86', 'ia64') : ('x86_ia64', ('bin', 'x86_ia64')), + ('x86', 'amd64') : ('x86_amd64', 'vcvarsx86_amd64.bat', ('bin', 'x86_amd64')), + ('x86', 'x86') : ('x86', 'vcvars32.bat', ('bin', )), + ('x86', 'arm') : ('x86_arm', 'vcvarsx86_arm.bat', ('bin', 'x86_arm')), + ('x86', 'ia64') : ('x86_ia64', 'vcvarsx86_ia64.bat', ('bin', 'x86_ia64')), - ('arm64', 'amd64') : ('amd64', ('bin', 'amd64')), - ('arm64', 'x86') : ('amd64_x86', ('bin', 'amd64_x86')), - ('arm64', 'arm') : ('amd64_arm', ('bin', 'amd64_arm')), + ('arm64', 'amd64') : ('amd64', 'vcvars64.bat', ('bin', 'amd64')), + ('arm64', 'x86') : ('amd64_x86', 'vcvarsamd64_x86.bat', ('bin', 'amd64_x86')), + ('arm64', 'arm') : ('amd64_arm', 'vcvarsamd64_arm.bat', ('bin', 'amd64_arm')), - ('arm', 'arm') : ('arm', ('bin', 'arm')), - ('ia64', 'ia64') : ('ia64', ('bin', 'ia64')), + ('arm', 'arm') : ('arm', 'vcvarsarm.bat', ('bin', 'arm')), + ('ia64', 'ia64') : ('ia64', 'vcvars64.bat', ('bin', 'ia64')), } @@ -454,6 +464,53 @@ def _make_target_host_map(all_hosts, host_all_targets_map): # debug("_LE2015_HOST_TARGET_CFG: %s", _LE2015_HOST_TARGET_CFG) +# 9.0 (VS2008) to 8.0 (VS2005) + +_LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS = { + + ('amd64', 'amd64') : ('amd64', 'vcvarsamd64.bat', ('bin', 'amd64')), + ('amd64', 'x86') : ('x86', 'vcvars32.bat', ('bin', )), + + ('x86', 'amd64') : ('x86_amd64', 'vcvarsx86_amd64.bat', ('bin', 'x86_amd64')), + ('x86', 'x86') : ('x86', 'vcvars32.bat', ('bin', )), + ('x86', 'ia64') : ('x86_ia64', 'vcvarsx86_ia64.bat', ('bin', 'x86_ia64')), + + ('arm64', 'amd64') : ('amd64', 'vcvarsamd64.bat', ('bin', 'amd64')), + ('arm64', 'x86') : ('x86', 'vcvars32.bat', ('bin', )), + + ('ia64', 'ia64') : ('ia64', 'vcvarsia64.bat', ('bin', 'ia64')), + +} + +_LE2008_HOST_TARGET_CFG = _host_target_config_factory( + + label = 'LE2008', + + host_all_hosts = OrderedDict([ + ('amd64', ['amd64', 'x86']), + ('x86', ['x86']), + ('arm64', ['amd64', 'x86']), + ('ia64', ['ia64']), + ]), + + host_all_targets = { + 'amd64': ['amd64', 'x86'], + 'x86': ['x86', 'amd64', 'ia64'], + 'arm64': ['amd64', 'x86'], + 'ia64': ['ia64'], + }, + + host_def_targets = { + 'amd64': ['amd64', 'x86'], + 'x86': ['x86'], + 'arm64': ['amd64', 'x86'], + 'ia64': ['ia64'], + }, + +) + +# debug("_LE2008_HOST_TARGET_CFG: %s", _LE2008_HOST_TARGET_CFG) + # 7.1 (VS2003) and earlier # For 7.1 (VS2003) and earlier, there are only x86 targets and the batch files @@ -487,6 +544,8 @@ def _make_target_host_map(all_hosts, host_all_targets_map): _CL_EXE_NAME = 'cl.exe' +_VSWHERE_EXE = 'vswhere.exe' + def get_msvc_version_numeric(msvc_version): """Get the raw version numbers from a MSVC_VERSION string, so it could be cast to float or other numeric values. For example, '14.0Exp' @@ -566,9 +625,12 @@ def get_host_target(env, msvc_version, all_host_targets: bool=False): elif 143 > vernum_int >= 141: # 14.2 (VS2019) to 14.1 (VS2017) host_target_cfg = _LE2019_HOST_TARGET_CFG - elif 141 > vernum_int >= 80: - # 14.0 (VS2015) to 8.0 (VS2005) + elif 141 > vernum_int >= 100: + # 14.0 (VS2015) to 10.0 (VS2010) host_target_cfg = _LE2015_HOST_TARGET_CFG + elif 100 > vernum_int >= 80: + # 9.0 (VS2008) to 8.0 (VS2005) + host_target_cfg = _LE2008_HOST_TARGET_CFG else: # 80 > vernum_int # 7.1 (VS2003) and earlier host_target_cfg = _LE2003_HOST_TARGET_CFG @@ -687,49 +749,72 @@ def _skip_sendtelemetry(env): "7.0", "6.0"] -# if using vswhere, configure command line arguments to probe for installed VC editions -_VCVER_TO_VSWHERE_VER = { - '14.3': [ - ["-version", "[17.0, 18.0)"], # default: Enterprise, Professional, Community (order unpredictable?) - ["-version", "[17.0, 18.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"], # BuildTools - ], - '14.2': [ - ["-version", "[16.0, 17.0)"], # default: Enterprise, Professional, Community (order unpredictable?) - ["-version", "[16.0, 17.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"], # BuildTools - ], - '14.1': [ - ["-version", "[15.0, 16.0)"], # default: Enterprise, Professional, Community (order unpredictable?) - ["-version", "[15.0, 16.0)", "-products", "Microsoft.VisualStudio.Product.BuildTools"], # BuildTools - ], - '14.1Exp': [ - ["-version", "[15.0, 16.0)", "-products", "Microsoft.VisualStudio.Product.WDExpress"], # Express - ], -} - +# VS2017 and later: use a single vswhere json query to find all installations + +# vswhere query: +# map vs major version to vc version (no suffix) +# build set of supported vc versions (including suffix) + +_VSWHERE_VSMAJOR_TO_VCVERSION = {} +_VSWHERE_SUPPORTED_VCVER = set() + +for vs_major, vc_version, vc_ver_list in ( + ('17', '14.3', None), + ('16', '14.2', None), + ('15', '14.1', ['14.1Exp']), +): + _VSWHERE_VSMAJOR_TO_VCVERSION[vs_major] = vc_version + _VSWHERE_SUPPORTED_VCVER.add(vc_version) + if vc_ver_list: + for vc_ver in vc_ver_list: + _VSWHERE_SUPPORTED_VCVER.add(vc_ver) + +# vwhere query: +# build of set of candidate component ids +# preferred ranking: Enterprise, Professional, Community, BuildTools, Express +# Ent, Pro, Com, BT, Exp are in the same list +# Exp also has it's own list +# currently, only the express (Exp) suffix is expected + +_VSWHERE_COMPONENTID_CANDIDATES = set() +_VSWHERE_COMPONENTID_RANKING = {} +_VSWHERE_COMPONENTID_SUFFIX = {} +_VSWHERE_COMPONENTID_SCONS_SUFFIX = {} + +for component_id, component_rank, component_suffix, scons_suffix in ( + ('Enterprise', 140, 'Ent', ''), + ('Professional', 130, 'Pro', ''), + ('Community', 120, 'Com', ''), + ('BuildTools', 110, 'BT', ''), + ('WDExpress', 100, 'Exp', 'Exp'), +): + _VSWHERE_COMPONENTID_CANDIDATES.add(component_id) + _VSWHERE_COMPONENTID_RANKING[component_id] = component_rank + _VSWHERE_COMPONENTID_SUFFIX[component_id] = component_suffix + _VSWHERE_COMPONENTID_SCONS_SUFFIX[component_id] = scons_suffix + +# VS2015 and earlier: configure registry queries to probe for installed VC editions _VCVER_TO_PRODUCT_DIR = { - '14.3': [ - (SCons.Util.HKEY_LOCAL_MACHINE, r'')], # not set by this version - '14.2': [ - (SCons.Util.HKEY_LOCAL_MACHINE, r'')], # not set by this version - '14.1': [ - (SCons.Util.HKEY_LOCAL_MACHINE, r'')], # not set by this version - '14.1Exp': [ - (SCons.Util.HKEY_LOCAL_MACHINE, r'')], # not set by this version '14.0': [ - (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir')], + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir') + ], '14.0Exp': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\14.0\Setup\VS\ProductDir'), - (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\14.0\Setup\VC\ProductDir')], + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\14.0\Setup\VC\ProductDir'), + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\14.0\Setup\VC\ProductDir') + ], '12.0': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\12.0\Setup\VC\ProductDir'), ], '12.0Exp': [ + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\12.0\Setup\VS\ProductDir'), (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\12.0\Setup\VC\ProductDir'), ], '11.0': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\11.0\Setup\VC\ProductDir'), ], '11.0Exp': [ + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\WDExpress\11.0\Setup\VS\ProductDir'), (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\11.0\Setup\VC\ProductDir'), ], '10.0': [ @@ -739,17 +824,20 @@ def _skip_sendtelemetry(env): (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\10.0\Setup\VC\ProductDir'), ], '9.0': [ - (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), + (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), # vs root + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), # vs root (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\9.0\Setup\VC\ProductDir',), ] if _VC90_Prefer_VCForPython else [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\9.0\Setup\VC\ProductDir',), - (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), + (SCons.Util.HKEY_CURRENT_USER, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), # vs root + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\DevDiv\VCForPython\9.0\installdir',), # vs root ], '9.0Exp': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\9.0\Setup\VC\ProductDir'), ], '8.0': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VisualStudio\8.0\Setup\VC\ProductDir'), + (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\8.0\Setup\VC\ProductDir'), ], '8.0Exp': [ (SCons.Util.HKEY_LOCAL_MACHINE, r'Microsoft\VCExpress\8.0\Setup\VC\ProductDir'), @@ -765,6 +853,62 @@ def _skip_sendtelemetry(env): ] } +# detect ide binaries + +VS2022_VS2002_DEV = ( + MSVC.Kind.IDE_PROGRAM_DEVENV_COM, # devenv.com +) + +VS1998_DEV = ( + MSVC.Kind.IDE_PROGRAM_MSDEV_COM, # MSDEV.COM +) + +VS2017_EXP = ( + MSVC.Kind.IDE_PROGRAM_WDEXPRESS_EXE, # WDExpress.exe +) + +VS2015_VS2012_EXP = ( + MSVC.Kind.IDE_PROGRAM_WDEXPRESS_EXE, # WDExpress.exe [Desktop] + MSVC.Kind.IDE_PROGRAM_VSWINEXPRESS_EXE, # VSWinExpress.exe [Windows] + MSVC.Kind.IDE_PROGRAM_VWDEXPRESS_EXE, # VWDExpress.exe [Web] +) + +VS2010_VS2005_EXP = ( + MSVC.Kind.IDE_PROGRAM_VCEXPRESS_EXE, +) + +# detect productdir kind + +_DETECT = MSVC.Kind.VCVER_KIND_DETECT + +_VCVER_KIND_DETECT = { + + # 'VCVer': (relpath from pdir to vsroot, path from vsroot to ide binaries, ide binaries) + + '14.3': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV), # 2022 + '14.2': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV), # 2019 + '14.1': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2017_EXP), # 2017 + '14.1Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2017_EXP), # 2017 + + '14.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2015 + '14.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2015 + '12.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2013 + '12.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2013 + '11.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2012 + '11.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2015_VS2012_EXP), # 2012 + + '10.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2010 + '10.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2010 + '9.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2008 + '9.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2008 + '8.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2005 + '8.0Exp': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV + VS2010_VS2005_EXP), # 2005 + + '7.1': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV), # 2003 + '7.0': _DETECT(root='..', path=r'Common7\IDE', programs=VS2022_VS2002_DEV), # 2001 + + '6.0': _DETECT(root='..', path=r'Common\MSDev98\Bin', programs=VS1998_DEV), # 1998 +} def msvc_version_to_maj_min(msvc_version): msvc_version_numeric = get_msvc_version_numeric(msvc_version) @@ -780,33 +924,581 @@ def msvc_version_to_maj_min(msvc_version): raise ValueError("Unrecognized version %s (%s)" % (msvc_version,msvc_version_numeric)) from None -VSWHERE_PATHS = [os.path.join(p,'vswhere.exe') for p in [ +_VSWHERE_EXEGROUP_MSVS = [os.path.join(p, _VSWHERE_EXE) for p in [ + # For bug 3333: support default location of vswhere for both + # 64 and 32 bit windows installs. + # For bug 3542: also accommodate not being on C: drive. os.path.expandvars(r"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer"), os.path.expandvars(r"%ProgramFiles%\Microsoft Visual Studio\Installer"), +]] + +_VSWHERE_EXEGROUP_PKGMGR = [os.path.join(p, _VSWHERE_EXE) for p in [ os.path.expandvars(r"%ChocolateyInstall%\bin"), + os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WinGet\Links"), + os.path.expanduser(r"~\scoop\shims"), + os.path.expandvars(r"%SCOOP%\shims"), ]] +_VSWhereBinary = namedtuple('_VSWhereBinary', [ + 'vswhere_exe', + 'vswhere_norm', +]) + +class VSWhereBinary(_VSWhereBinary, MSVC.Util.AutoInitialize): + + _UNDEFINED_VSWHERE_BINARY = None + + _cache_vswhere_paths = {} + + @classmethod + def _initialize(cls): + vswhere_binary = cls( + vswhere_exe=None, + vswhere_norm=None, + ) + cls._UNDEFINED_VSWHERE_BINARY = vswhere_binary + for symbol in (None, ''): + cls._cache_vswhere_paths[symbol] = vswhere_binary + + @classmethod + def factory(cls, vswhere_exe): + + if not vswhere_exe: + return cls._UNDEFINED_VSWHERE_BINARY + + vswhere_binary = cls._cache_vswhere_paths.get(vswhere_exe) + if vswhere_binary: + return vswhere_binary + + vswhere_norm = MSVC.Util.normalize_path(vswhere_exe) + + vswhere_binary = cls._cache_vswhere_paths.get(vswhere_norm) + if vswhere_binary: + return vswhere_binary + + vswhere_binary = cls( + vswhere_exe=vswhere_exe, + vswhere_norm=vswhere_norm, + ) + + cls._cache_vswhere_paths[vswhere_exe] = vswhere_binary + cls._cache_vswhere_paths[vswhere_norm] = vswhere_binary + + return vswhere_binary + +class _VSWhereExecutable(MSVC.Util.AutoInitialize): + + debug_extra = None + + class _VSWhereUserPriority(enum.IntEnum): + HIGH = 0 # before msvs locations + DEFAULT = enum.auto() # after msvs locations and before pkgmgr locations + LOW = enum.auto() # after pkgmgr locations + + priority_default = _VSWhereUserPriority.DEFAULT + priority_map = {} + priority_symbols = [] + priority_indmap = {} + + for e in _VSWhereUserPriority: + for symbol in (e.name.lower(), e.name.lower().capitalize(), e.name): + priority_map[symbol] = e.value + priority_symbols.append(symbol) + priority_indmap[e.value] = e.name + + UNDEFINED_VSWHERE_BINARY = VSWhereBinary.factory(None) + + @classmethod + def reset(cls): + + cls._vswhere_exegroups = None + cls._vswhere_exegroups_user = None + cls._vswhere_exegroup_msvs = None + cls._vswhere_exegroup_pkgmgr = None + + cls.vswhere_frozen_flag = False + cls.vswhere_frozen_binary = None + + @classmethod + def _initialize(cls): + cls.debug_extra = common.debug_extra(cls) + cls.reset() + + @classmethod + def _vswhere_exegroup_binaries(cls, vswhere_exe_list, label): + vswhere_binaries = [] + for vswhere_exe in vswhere_exe_list: + if not os.path.exists(vswhere_exe): + continue + vswhere_binary = VSWhereBinary.factory(vswhere_exe) + vswhere_binaries.append(vswhere_binary) + debug( + "insert exegroup=%s, vswhere_binary=%s", + label, vswhere_binary, extra=cls.debug_extra + ) + return vswhere_binaries + + @classmethod + def _get_vswhere_exegroups(cls): + + if cls._vswhere_exegroups is None: + + cls._vswhere_exegroup_msvs = cls._vswhere_exegroup_binaries(_VSWHERE_EXEGROUP_MSVS, 'MSVS') + cls._vswhere_exegroup_pkgmgr = cls._vswhere_exegroup_binaries(_VSWHERE_EXEGROUP_PKGMGR, 'PKGMGR') + + cls._vswhere_exegroups_user = [ + [] for e in cls._VSWhereUserPriority + ] + + vswhere_exegroups = [ + cls._vswhere_exegroups_user[cls._VSWhereUserPriority.HIGH], + cls._vswhere_exegroup_msvs, + cls._vswhere_exegroups_user[cls._VSWhereUserPriority.DEFAULT], + cls._vswhere_exegroup_pkgmgr, + cls._vswhere_exegroups_user[cls._VSWhereUserPriority.LOW], + ] + + cls._vswhere_exegroups = vswhere_exegroups + + return cls._vswhere_exegroups + + @classmethod + def _get_vswhere_exegroups_user(cls): + + if cls._vswhere_exegroups_user is None: + cls._get_vswhere_exegroups() + + return cls._vswhere_exegroups_user + + @classmethod + def _vswhere_exegroup_binary(cls, exegroup): + # first vswhere binary in group or UNDEFINED_VSWHERE_BINARY + vswhere_binary = exegroup[0] if exegroup else cls.UNDEFINED_VSWHERE_BINARY + return vswhere_binary + + @classmethod + def _vswhere_current_binary(cls): + # first vswhere binary in priority order or UNDEFINED_VSWHERE_BINARY + vswhere_binary = cls.UNDEFINED_VSWHERE_BINARY + vswhere_exegroups = cls._get_vswhere_exegroups() + for exegroup in vswhere_exegroups: + binary = cls._vswhere_exegroup_binary(exegroup) + if binary == cls.UNDEFINED_VSWHERE_BINARY: + continue + vswhere_binary = binary + break + return vswhere_binary + + @classmethod + def _vswhere_all_executables(cls): + # unique vswhere executables in priority order + vswhere_exe_list = [] + vswhere_norm_set = set() + vswhere_exegroups = cls._get_vswhere_exegroups() + for exegroup in vswhere_exegroups: + for vswhere_binary in exegroup: + if vswhere_binary.vswhere_norm in vswhere_norm_set: + continue + vswhere_norm_set.add(vswhere_binary.vswhere_norm) + vswhere_exe_list.append(vswhere_binary.vswhere_exe) + return vswhere_exe_list + + @classmethod + def is_frozen(cls) -> bool: + rval = bool(cls.vswhere_frozen_flag) + return rval + + @classmethod + def freeze_vswhere_binary(cls): + if not cls.vswhere_frozen_flag: + cls.vswhere_frozen_flag = True + cls.vswhere_frozen_binary = cls._vswhere_current_binary() + debug("freeze=%s", cls.vswhere_frozen_binary, extra=cls.debug_extra) + return cls.vswhere_frozen_binary + + @classmethod + def freeze_vswhere_executable(cls): + vswhere_binary = cls.freeze_vswhere_binary() + vswhere_exe = vswhere_binary.vswhere_exe + return vswhere_exe + + @classmethod + def get_vswhere_executable(cls): + if cls.vswhere_frozen_flag: + vswhere_binary = cls.vswhere_frozen_binary + else: + vswhere_binary = cls._vswhere_current_binary() + vswhere_exe = vswhere_binary.vswhere_exe + debug("vswhere_exe=%s", repr(vswhere_exe), extra=cls.debug_extra) + return vswhere_exe + + @classmethod + def _vswhere_priority_group(cls, priority): + if not priority: + group = cls.priority_default + else: + group = cls.priority_map.get(priority) + if group is None: + msg = f'Value specified for vswhere executable priority is not supported: {priority!r}:\n' \ + f' Valid values are: {cls.priority_symbols}' + debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra) + raise VSWhereUserError(msg) + return group + + @classmethod + def register_vswhere_executable(cls, vswhere_exe, priority=None): + + vswhere_binary = cls.UNDEFINED_VSWHERE_BINARY + + if not vswhere_exe: + # ignore: None or empty + return vswhere_binary + + if not os.path.exists(vswhere_exe): + msg = f'Specified vswhere executable not found: {vswhere_exe!r}.' + debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra) + raise VSWhereUserError(msg) + + group = cls._vswhere_priority_group(priority) + + vswhere_binary = VSWhereBinary.factory(vswhere_exe) + + if cls.vswhere_frozen_flag: + + if vswhere_binary.vswhere_norm == cls.vswhere_frozen_binary.vswhere_norm: + # ignore: user executable == frozen executable + return vswhere_binary + + msg = 'A different vswhere execuable cannot be requested after initial detetection:\n' \ + f' initial vswhere executable: {cls.vswhere_frozen_binary.vswhere_exe!r}\n' \ + f' request vswhere executable: {vswhere_binary.vswhere_exe!r}' + + debug(f'VSWhereUserError: {msg}', extra=cls.debug_extra) + raise VSWhereUserError(msg) + + vswhere_exegroups_user = cls._get_vswhere_exegroups_user() + + exegroup = vswhere_exegroups_user[group] + group_binary = cls._vswhere_exegroup_binary(exegroup) + + if vswhere_binary.vswhere_norm == group_binary.vswhere_norm: + # ignore: user executable == exegroup[0] executable + return vswhere_binary + + exegroup.insert(0, vswhere_binary) + debug( + "insert exegroup=user[%s], vswhere_binary=%s", + cls.priority_indmap[group], vswhere_binary, extra=cls.debug_extra + ) + + return vswhere_binary + + @classmethod + def vswhere_freeze_executable(cls, vswhere_exe): + vswhere_binary = cls.register_vswhere_executable(vswhere_exe, priority='high') + frozen_binary = cls.freeze_vswhere_binary() + return frozen_binary, vswhere_binary + + @classmethod + def vswhere_freeze_env(cls, env): + + if env is None: + # no environment, VSWHERE undefined + vswhere_exe = None + write_vswhere = False + elif not env.get('VSWHERE'): + # environment, VSWHERE undefined/none/empty + vswhere_exe = None + write_vswhere = True + else: + # environment, VSWHERE defined + vswhere_exe = env.subst('$VSWHERE') + write_vswhere = False + + frozen_binary, vswhere_binary = cls.vswhere_freeze_executable(vswhere_exe) + + if write_vswhere and frozen_binary.vswhere_norm != vswhere_binary.vswhere_norm: + env['VSWHERE'] = frozen_binary.vswhere_exe + debug( + "env['VSWHERE']=%s", + repr(frozen_binary.vswhere_exe), extra=cls.debug_extra + ) + + return frozen_binary, vswhere_binary + +# external use + +def vswhere_register_executable(vswhere_exe, priority=None, freeze=False): + debug( + 'register vswhere_exe=%s, priority=%s, freeze=%s', + repr(vswhere_exe), repr(priority), repr(freeze) + ) + _VSWhereExecutable.register_vswhere_executable(vswhere_exe, priority=priority) + if freeze: + _VSWhereExecutable.freeze_vswhere_executable() + rval = _VSWhereExecutable.get_vswhere_executable() + debug('current vswhere_exe=%s, is_frozen=%s', repr(rval), _VSWhereExecutable.is_frozen()) + return vswhere_exe + +def vswhere_get_executable(): + debug('') + vswhere_exe = _VSWhereExecutable.get_vswhere_executable() + return vswhere_exe + +def vswhere_freeze_executable(): + debug('') + vswhere_exe = _VSWhereExecutable.freeze_vswhere_executable() + return vswhere_exe + +# internal use only +vswhere_freeze_env = _VSWhereExecutable.vswhere_freeze_env + def msvc_find_vswhere(): """ Find the location of vswhere """ - # For bug 3333: support default location of vswhere for both - # 64 and 32 bit windows installs. - # For bug 3542: also accommodate not being on C: drive. # NB: this gets called from testsuite on non-Windows platforms. # Whether that makes sense or not, don't break it for those. - vswhere_path = None - for pf in VSWHERE_PATHS: - if os.path.exists(pf): - vswhere_path = pf + vswhere_path = _VSWhereExecutable.get_vswhere_executable() + return vswhere_path + +_MSVCInstance = namedtuple('_MSVCInstance', [ + 'vc_path', + 'vc_version', + 'vc_version_numeric', + 'vc_version_scons', + 'vc_release', + 'vc_component_id', + 'vc_component_rank', + 'vc_component_suffix', +]) + +class MSVCInstance(_MSVCInstance): + + @staticmethod + def msvc_instances_default_order(a, b): + # vc version numeric: descending order + if a.vc_version_numeric != b.vc_version_numeric: + return 1 if a.vc_version_numeric < b.vc_version_numeric else -1 + # vc release: descending order (release, preview) + if a.vc_release != b.vc_release: + return 1 if a.vc_release < b.vc_release else -1 + # component rank: descending order + if a.vc_component_rank != b.vc_component_rank: + return 1 if a.vc_component_rank < b.vc_component_rank else -1 + return 0 + +class _VSWhere(MSVC.Util.AutoInitialize): + + debug_extra = None + + @classmethod + def reset(cls): + + cls.seen_vswhere = set() + cls.seen_root = set() + + cls.msvc_instances = [] + cls.msvc_map = {} + + @classmethod + def _initialize(cls): + cls.debug_extra = common.debug_extra(cls) + cls.reset() + + @classmethod + def _filter_vswhere_binary(cls, vswhere_binary): + + vswhere_norm = None + + if vswhere_binary.vswhere_norm not in cls.seen_vswhere: + cls.seen_vswhere.add(vswhere_binary.vswhere_norm) + vswhere_norm = vswhere_binary.vswhere_norm + + return vswhere_norm + + @classmethod + def _new_roots_discovered(cls): + if len(cls.seen_vswhere) > 1: + raise MSVCInternalError(f'vswhere discovered new msvc installations after initial detection') + + @classmethod + def _vswhere_query_json_output(cls, vswhere_exe, vswhere_args): + + vswhere_json = None + + once = True + while once: + once = False + # using break for single exit (unless exception) + + vswhere_cmd = [vswhere_exe] + vswhere_args + ['-format', 'json', '-utf8'] + debug("running: %s", vswhere_cmd, extra=cls.debug_extra) + + try: + cp = subprocess.run(vswhere_cmd, stdout=PIPE, stderr=PIPE, check=True) + except OSError as e: + errmsg = str(e) + debug("%s: %s", type(e).__name__, errmsg, extra=cls.debug_extra) + break + except Exception as e: + errmsg = str(e) + debug("%s: %s", type(e).__name__, errmsg, extra=cls.debug_extra) + raise + + if not cp.stdout: + debug("no vswhere information returned", extra=cls.debug_extra) + break + + vswhere_output = cp.stdout.decode('utf8', errors='replace') + if not vswhere_output: + debug("no vswhere information output", extra=cls.debug_extra) + break + + try: + vswhere_output_json = json.loads(vswhere_output) + except json.decoder.JSONDecodeError: + debug("json decode exception loading vswhere output", extra=cls.debug_extra) + break + + vswhere_json = vswhere_output_json break - return vswhere_path + debug( + 'vswhere_json=%s, vswhere_exe=%s', + bool(vswhere_json), repr(vswhere_exe), extra=cls.debug_extra + ) + + return vswhere_json + + @classmethod + def vswhere_update_msvc_map(cls, vswhere_exe): + + frozen_binary, vswhere_binary = _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe) + + vswhere_norm = cls._filter_vswhere_binary(frozen_binary) + if not vswhere_norm: + return cls.msvc_map + + debug('vswhere_norm=%s', repr(vswhere_norm), extra=cls.debug_extra) + + vswhere_json = cls._vswhere_query_json_output( + vswhere_norm, + ['-all', '-products', '*'] + ) + + if not vswhere_json: + return cls.msvc_map + + n_instances = len(cls.msvc_instances) + + for instance in vswhere_json: + + #print(json.dumps(instance, indent=4, sort_keys=True)) + + installation_path = instance.get('installationPath') + if not installation_path or not os.path.exists(installation_path): + continue + + vc_path = os.path.join(installation_path, 'VC') + if not os.path.exists(vc_path): + continue + + vc_root = MSVC.Util.normalize_path(vc_path) + if vc_root in cls.seen_root: + continue + cls.seen_root.add(vc_root) + + installation_version = instance.get('installationVersion') + if not installation_version: + continue + + vs_major = installation_version.split('.')[0] + if not vs_major in _VSWHERE_VSMAJOR_TO_VCVERSION: + debug('ignore vs_major: %s', vs_major, extra=cls.debug_extra) + continue + + vc_version = _VSWHERE_VSMAJOR_TO_VCVERSION[vs_major] + + product_id = instance.get('productId') + if not product_id: + continue + + component_id = product_id.split('.')[-1] + if component_id not in _VSWHERE_COMPONENTID_CANDIDATES: + debug('ignore component_id: %s', component_id, extra=cls.debug_extra) + continue + + component_rank = _VSWHERE_COMPONENTID_RANKING.get(component_id,0) + if component_rank == 0: + raise MSVCInternalError(f'unknown component_rank for component_id: {component_id!r}') + + scons_suffix = _VSWHERE_COMPONENTID_SCONS_SUFFIX[component_id] + + if scons_suffix: + vc_version_scons = vc_version + scons_suffix + else: + vc_version_scons = vc_version + + is_prerelease = True if instance.get('isPrerelease', False) else False + is_release = False if is_prerelease else True + + msvc_instance = MSVCInstance( + vc_path = vc_path, + vc_version = vc_version, + vc_version_numeric = float(vc_version), + vc_version_scons = vc_version_scons, + vc_release = is_release, + vc_component_id = component_id, + vc_component_rank = component_rank, + vc_component_suffix = component_suffix, + ) + + cls.msvc_instances.append(msvc_instance) + + new_roots = bool(len(cls.msvc_instances) > n_instances) + if new_roots: + + cls.msvc_instances = sorted( + cls.msvc_instances, + key=cmp_to_key(MSVCInstance.msvc_instances_default_order) + ) + + cls.msvc_map = {} + + for msvc_instance in cls.msvc_instances: + + debug( + 'msvc instance: msvc_version=%s, is_release=%s, component_id=%s, vc_path=%s', + repr(msvc_instance.vc_version_scons), msvc_instance.vc_release, + repr(msvc_instance.vc_component_id), repr(msvc_instance.vc_path), + extra=cls.debug_extra + ) -def find_vc_pdir_vswhere(msvc_version, env=None): + key = (msvc_instance.vc_version_scons, msvc_instance.vc_release) + cls.msvc_map.setdefault(key,[]).append(msvc_instance) + + if msvc_instance.vc_version_scons == msvc_instance.vc_version: + continue + + key = (msvc_instance.vc_version, msvc_instance.vc_release) + cls.msvc_map.setdefault(key,[]).append(msvc_instance) + + cls._new_roots_discovered() + + debug( + 'new_roots=%s, msvc_instances=%s', + new_roots, len(cls.msvc_instances), extra=cls.debug_extra + ) + + return cls.msvc_map + +_cache_pdir_vswhere_queries = {} + +def _find_vc_pdir_vswhere(msvc_version, vswhere_exe): """ Find the MSVC product directory using the vswhere program. Args: msvc_version: MSVC version to search for - env: optional to look up VSWHERE variable + vswhere_exe: vswhere executable or None Returns: MSVC install dir or None @@ -815,57 +1507,178 @@ def find_vc_pdir_vswhere(msvc_version, env=None): UnsupportedVersion: if the version is not known by this file """ + global _cache_pdir_vswhere_queries + + msvc_map = _VSWhere.vswhere_update_msvc_map(vswhere_exe) + if not msvc_map: + return None + + rval = _cache_pdir_vswhere_queries.get(msvc_version, UNDEFINED) + if rval != UNDEFINED: + debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(rval)) + return rval + + if msvc_version not in _VSWHERE_SUPPORTED_VCVER: + debug("Unknown version of MSVC: %s", msvc_version) + raise UnsupportedVersion("Unknown version %s" % msvc_version) + + is_release = True + key = (msvc_version, is_release) + + msvc_instances = msvc_map.get(key, UNDEFINED) + if msvc_instances == UNDEFINED: + debug( + 'msvc instances lookup failed: msvc_version=%s, is_release=%s', + repr(msvc_version), repr(is_release) + ) + msvc_instances = [] + + save_pdir_kind = [] + + pdir = None + kind_t = None + + for msvc_instance in msvc_instances: + + vcdir = msvc_instance.vc_path + + vckind_t = MSVC.Kind.msvc_version_pdir_vswhere_kind(msvc_version, vcdir, _VCVER_KIND_DETECT[msvc_version]) + if vckind_t.skip: + if vckind_t.save: + debug('save kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir)) + save_pdir_kind.append((vcdir, vckind_t)) + else: + debug('skip kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir)) + continue + + pdir = vcdir + kind_t = vckind_t + break + + if not pdir and not kind_t: + if save_pdir_kind: + pdir, kind_t = save_pdir_kind[0] + + MSVC.Kind.msvc_version_register_kind(msvc_version, kind_t) + + debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(pdir)) + _cache_pdir_vswhere_queries[msvc_version] = pdir + + return pdir + +_cache_pdir_registry_queries = {} + +def _find_vc_pdir_registry(msvc_version): + """ Find the MSVC product directory using the registry. + + Args: + msvc_version: MSVC version to search for + + Returns: + MSVC install dir or None + + Raises: + UnsupportedVersion: if the version is not known by this file + + """ + global _cache_pdir_registry_queries + + rval = _cache_pdir_registry_queries.get(msvc_version, UNDEFINED) + if rval != UNDEFINED: + debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(rval)) + return rval + try: - vswhere_version = _VCVER_TO_VSWHERE_VER[msvc_version] + regkeys = _VCVER_TO_PRODUCT_DIR[msvc_version] except KeyError: debug("Unknown version of MSVC: %s", msvc_version) raise UnsupportedVersion("Unknown version %s" % msvc_version) from None - if env is None or not env.get('VSWHERE'): - vswhere_path = msvc_find_vswhere() - else: - vswhere_path = env.subst('$VSWHERE') - - if vswhere_path is None: - return None + save_pdir_kind = [] - debug('VSWHERE: %s', vswhere_path) - for vswhere_version_args in vswhere_version: + is_win64 = common.is_win64() - vswhere_cmd = [vswhere_path] + vswhere_version_args + ["-property", "installationPath"] + pdir = None + kind_t = None - debug("running: %s", vswhere_cmd) + root = 'Software\\' + for hkroot, key in regkeys: - # TODO: Python 3.7 - # cp = subprocess.run(vswhere_cmd, capture_output=True, check=True) # 3.7+ only - cp = subprocess.run(vswhere_cmd, stdout=PIPE, stderr=PIPE, check=True) + if not hkroot or not key: + continue - if cp.stdout: - # vswhere could return multiple lines, e.g. if Build Tools - # and {Community,Professional,Enterprise} are both installed. - # We could define a way to pick the one we prefer, but since - # this data is currently only used to make a check for existence, - # returning the first hit should be good enough. - lines = cp.stdout.decode("mbcs").splitlines() - return os.path.join(lines[0], 'VC') + if is_win64: + msregkeys = [root + 'Wow6432Node\\' + key, root + key] else: - # We found vswhere, but no install info available for this version - pass + msregkeys = [root + key] - return None + vcdir = None + for msregkey in msregkeys: + debug('trying VC registry key %s', repr(msregkey)) + try: + vcdir = common.read_reg(msregkey, hkroot) + except OSError: + continue + if vcdir: + break + if not vcdir: + debug('no VC registry key %s', repr(key)) + continue -def find_vc_pdir(env, msvc_version): - """Find the MSVC product directory for the given version. + is_vcforpython = False + + is_vsroot = False + if msvc_version == '9.0' and key.lower().endswith('\\vcforpython\\9.0\\installdir'): + # Visual C++ for Python registry key is VS installdir (root) not VC productdir + is_vsroot = True + is_vcforpython = True + elif msvc_version in ('14.0Exp', '12.0Exp', '11.0Exp') and key.lower().endswith('\\setup\\vs\\productdir'): + # Visual Studio 2015/2013/2012 Express is VS productdir (root) not VC productdir + is_vsroot = True + + if is_vsroot: + vcdir = os.path.join(vcdir, 'VC') + debug('convert vs root to vc dir: %s', repr(vcdir)) + + if not os.path.exists(vcdir): + debug('reg says dir is %s, but it does not exist. (ignoring)', repr(vcdir)) + continue + + vckind_t = MSVC.Kind.msvc_version_pdir_registry_kind(msvc_version, vcdir, _VCVER_KIND_DETECT[msvc_version], is_vcforpython) + if vckind_t.skip: + if vckind_t.save: + debug('save kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir)) + save_pdir_kind.append((vcdir, vckind_t)) + else: + debug('skip kind: msvc_version=%s, pdir=%s', repr(msvc_version), repr(vcdir)) + continue + + pdir = vcdir + kind_t = vckind_t + break - Tries to look up the path using a registry key from the table - _VCVER_TO_PRODUCT_DIR; if there is no key, calls find_vc_pdir_wshere - for help instead. + if not pdir and not kind_t: + if save_pdir_kind: + pdir, kind_t = save_pdir_kind[0] + + MSVC.Kind.msvc_version_register_kind(msvc_version, kind_t) + + debug('msvc_version=%s, pdir=%s', repr(msvc_version), repr(pdir)) + _cache_pdir_registry_queries[msvc_version] = pdir + + return pdir + +def _find_vc_pdir(msvc_version, vswhere_exe): + """Find the MSVC product directory for the given version. Args: msvc_version: str msvc version (major.minor, e.g. 10.0) + vswhere_exe: str + vswhere executable or None + Returns: str: Path found in registry, or None @@ -875,52 +1688,66 @@ def find_vc_pdir(env, msvc_version): UnsupportedVersion inherits from VisualCException. """ - root = 'Software\\' - try: - hkeys = _VCVER_TO_PRODUCT_DIR[msvc_version] - except KeyError: - debug("Unknown version of MSVC: %s", msvc_version) - raise UnsupportedVersion("Unknown version %s" % msvc_version) from None - for hkroot, key in hkeys: - try: - comps = None - if not key: - comps = find_vc_pdir_vswhere(msvc_version, env) - if not comps: - debug('no VC found for version %s', repr(msvc_version)) - raise OSError - debug('VC found: %s', repr(msvc_version)) - return comps - else: - if common.is_win64(): - try: - # ordinarily at win64, try Wow6432Node first. - comps = common.read_reg(root + 'Wow6432Node\\' + key, hkroot) - except OSError: - # at Microsoft Visual Studio for Python 2.7, value is not in Wow6432Node - pass - if not comps: - # not Win64, or Microsoft Visual Studio for Python 2.7 - comps = common.read_reg(root + key, hkroot) - except OSError: - debug('no VC registry key %s', repr(key)) - else: - if msvc_version == '9.0': - if key.lower().endswith('\\vcforpython\\9.0\\installdir'): - # Visual C++ for Python registry key is installdir (root) not productdir (vc) - comps = os.path.join(comps, 'VC') - elif msvc_version == '14.0Exp': - if key.lower().endswith('\\setup\\vs\\productdir'): - # Visual Studio 14.0 Express registry key is installdir (root) not productdir (vc) - comps = os.path.join(comps, 'VC') - debug('found VC in registry: %s', comps) - if os.path.exists(comps): - return comps - else: - debug('reg says dir is %s, but it does not exist. (ignoring)', comps) + if msvc_version in _VSWHERE_SUPPORTED_VCVER: + + pdir = _find_vc_pdir_vswhere(msvc_version, vswhere_exe) + if pdir: + debug('VC found: %s, dir=%s', repr(msvc_version), repr(pdir)) + return pdir + + elif msvc_version in _VCVER_TO_PRODUCT_DIR: + + pdir = _find_vc_pdir_registry(msvc_version) + if pdir: + debug('VC found: %s, dir=%s', repr(msvc_version), repr(pdir)) + return pdir + + else: + + debug("Unknown version of MSVC: %s", repr(msvc_version)) + raise UnsupportedVersion("Unknown version %s" % repr(msvc_version)) from None + + debug('no VC found for version %s', repr(msvc_version)) return None +def find_vc_pdir(msvc_version, env=None): + """Find the MSVC product directory for the given version. + + Args: + msvc_version: str + msvc version (major.minor, e.g. 10.0) + env: + optional to look up VSWHERE variable + + Returns: + str: Path found in registry, or None + + Raises: + UnsupportedVersion: if the version is not known by this file. + + UnsupportedVersion inherits from VisualCException. + + """ + + frozen_binary, _ = _VSWhereExecutable.vswhere_freeze_env(env) + + pdir = _find_vc_pdir(msvc_version, frozen_binary.vswhere_exe) + debug('pdir=%s', repr(pdir)) + + return pdir + +# register find_vc_pdir function with Kind routines +# in case of unknown msvc_version (just in case) +MSVC.Kind.register_msvc_version_pdir_func(find_vc_pdir) + +def _reset_vc_pdir(): + debug('reset pdir caches') + global _cache_pdir_vswhere_queries + global _cache_pdir_registry_queries + _cache_pdir_vswhere_queries = {} + _cache_pdir_registry_queries = {} + def find_batch_file(msvc_version, host_arch, target_arch, pdir): """ Find the location of the batch script which should set up the compiler @@ -940,6 +1767,7 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir): arg = '' vcdir = None clexe = None + depbat = None if vernum_int >= 143: # 14.3 (VS2022) and later @@ -953,15 +1781,26 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir): batfile, _ = _LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)] batfilename = os.path.join(batfiledir, batfile) vcdir = pdir - elif 141 > vernum_int >= 80: - # 14.0 (VS2015) to 8.0 (VS2005) - arg, cl_path_comps = _LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host_arch, target_arch)] + elif 141 > vernum_int >= 100: + # 14.0 (VS2015) to 10.0 (VS2010) + arg, batfile, cl_path_comps = _LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)] batfilename = os.path.join(pdir, "vcvarsall.bat") - if msvc_version == '9.0' and not os.path.exists(batfilename): - # Visual C++ for Python batch file is in installdir (root) not productdir (vc) - batfilename = os.path.normpath(os.path.join(pdir, os.pardir, "vcvarsall.bat")) - # Visual C++ for Python sdk batch files do not point to the VCForPython installation + depbat = os.path.join(pdir, *cl_path_comps, batfile) + clexe = os.path.join(pdir, *cl_path_comps, _CL_EXE_NAME) + elif 100 > vernum_int >= 80: + # 9.0 (VS2008) to 8.0 (VS2005) + arg, batfile, cl_path_comps = _LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host_arch, target_arch)] + if vernum_int == 90 and MSVC.Kind.msvc_version_is_vcforpython(msvc_version): + # 9.0 (VS2008) Visual C++ for Python: + # sdk batch files do not point to the VCForPython installation + # vcvarsall batch file is in installdir not productdir (e.g., vc\..\vcvarsall.bat) + # dependency batch files are not called from vcvarsall.bat sdk_pdir = None + batfilename = os.path.join(pdir, os.pardir, "vcvarsall.bat") + depbat = None + else: + batfilename = os.path.join(pdir, "vcvarsall.bat") + depbat = os.path.join(pdir, *cl_path_comps, batfile) clexe = os.path.join(pdir, *cl_path_comps, _CL_EXE_NAME) else: # 80 > vernum_int # 7.1 (VS2003) and earlier @@ -974,7 +1813,11 @@ def find_batch_file(msvc_version, host_arch, target_arch, pdir): batfilename = None if clexe and not os.path.exists(clexe): - debug("cl.exe not found: %s", clexe) + debug("%s not found: %s", _CL_EXE_NAME, clexe) + batfilename = None + + if depbat and not os.path.exists(depbat): + debug("dependency batch file not found: %s", depbat) batfilename = None return batfilename, arg, vcdir, sdk_pdir @@ -999,16 +1842,23 @@ def find_batch_file_sdk(host_arch, target_arch, sdk_pdir): return None __INSTALLED_VCS_RUN = None + +def _reset_installed_vcs(): + global __INSTALLED_VCS_RUN + debug('reset __INSTALLED_VCS_RUN') + __INSTALLED_VCS_RUN = None + _VC_TOOLS_VERSION_FILE_PATH = ['Auxiliary', 'Build', 'Microsoft.VCToolsVersion.default.txt'] _VC_TOOLS_VERSION_FILE = os.sep.join(_VC_TOOLS_VERSION_FILE_PATH) -def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool: - """Return status of finding a cl.exe to use. +def _check_files_exist_in_vc_dir(env, vc_dir, msvc_version) -> bool: + """Return status of finding batch file and cl.exe to use. - Locates cl in the vc_dir depending on TARGET_ARCH, HOST_ARCH and the - msvc version. TARGET_ARCH and HOST_ARCH can be extracted from the - passed env, unless the env is None, in which case the native platform is - assumed for the host and all associated targets. + Locates required vcvars batch files and cl in the vc_dir depending on + TARGET_ARCH, HOST_ARCH and the msvc version. TARGET_ARCH and HOST_ARCH + can be extracted from the passed env, unless the env is None, in which + case the native platform is assumed for the host and all associated + targets. Args: env: Environment @@ -1067,33 +1917,72 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool: debug('unsupported host/target platform combo: (%s,%s)', host_platform, target_platform) continue - _, cl_path_comps = batchfile_clpathcomps + batfile, cl_path_comps = batchfile_clpathcomps + + batfile_path = os.path.join(vc_dir, "Auxiliary", "Build", batfile) + if not os.path.exists(batfile_path): + debug("batch file not found: %s", batfile_path) + continue + cl_path = os.path.join(vc_dir, 'Tools', 'MSVC', vc_specific_version, *cl_path_comps, _CL_EXE_NAME) - debug('checking for %s at %s', _CL_EXE_NAME, cl_path) + if not os.path.exists(cl_path): + debug("%s not found: %s", _CL_EXE_NAME, cl_path) + continue - if os.path.exists(cl_path): - debug('found %s!', _CL_EXE_NAME) - return True + debug('%s found: %s', _CL_EXE_NAME, cl_path) + return True elif 141 > vernum_int >= 80: # 14.0 (VS2015) to 8.0 (VS2005) + if vernum_int >= 100: + # 14.0 (VS2015) to 10.0 (VS2010) + host_target_batcharg_batchfile_clpathcomps = _LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS + else: + # 9.0 (VS2008) to 8.0 (VS2005) + host_target_batcharg_batchfile_clpathcomps = _LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS + + if vernum_int == 90 and MSVC.Kind.msvc_version_is_vcforpython(msvc_version): + # 9.0 (VS2008) Visual C++ for Python: + # vcvarsall batch file is in installdir not productdir (e.g., vc\..\vcvarsall.bat) + # dependency batch files are not called from vcvarsall.bat + batfile_path = os.path.join(vc_dir, os.pardir, "vcvarsall.bat") + check_depbat = False + else: + batfile_path = os.path.join(vc_dir, "vcvarsall.bat") + check_depbat = True + + if not os.path.exists(batfile_path): + debug("batch file not found: %s", batfile_path) + return False + for host_platform, target_platform in host_target_list: debug('host platform %s, target platform %s for version %s', host_platform, target_platform, msvc_version) - batcharg_clpathcomps = _LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS.get((host_platform, target_platform), None) - if batcharg_clpathcomps is None: + batcharg_batchfile_clpathcomps = host_target_batcharg_batchfile_clpathcomps.get( + (host_platform, target_platform), None + ) + + if batcharg_batchfile_clpathcomps is None: debug('unsupported host/target platform combo: (%s,%s)', host_platform, target_platform) continue - _, cl_path_comps = batcharg_clpathcomps + _, batfile, cl_path_comps = batcharg_batchfile_clpathcomps + + if check_depbat: + batfile_path = os.path.join(vc_dir, *cl_path_comps, batfile) + if not os.path.exists(batfile_path): + debug("batch file not found: %s", batfile_path) + continue + cl_path = os.path.join(vc_dir, *cl_path_comps, _CL_EXE_NAME) - debug('checking for %s at %s', _CL_EXE_NAME, cl_path) + if not os.path.exists(cl_path): + debug("%s not found: %s", _CL_EXE_NAME, cl_path) + continue - if os.path.exists(cl_path): - debug('found %s', _CL_EXE_NAME) - return True + debug('%s found: %s', _CL_EXE_NAME, cl_path) + return True elif 80 > vernum_int >= 60: # 7.1 (VS2003) to 6.0 (VS6) @@ -1110,7 +1999,7 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool: for cl_dir in cl_dirs: cl_path = os.path.join(cl_root, cl_dir, _CL_EXE_NAME) if os.path.exists(cl_path): - debug('%s found %s', _CL_EXE_NAME, cl_path) + debug('%s found: %s', _CL_EXE_NAME, cl_path) return True return False @@ -1123,6 +2012,8 @@ def _check_cl_exists_in_vc_dir(env, vc_dir, msvc_version) -> bool: def get_installed_vcs(env=None): global __INSTALLED_VCS_RUN + _VSWhereExecutable.vswhere_freeze_env(env) + if __INSTALLED_VCS_RUN is not None: return __INSTALLED_VCS_RUN @@ -1138,17 +2029,17 @@ def get_installed_vcs(env=None): for ver in _VCVER: debug('trying to find VC %s', ver) try: - VC_DIR = find_vc_pdir(env, ver) + VC_DIR = find_vc_pdir(ver, env) if VC_DIR: debug('found VC %s', ver) - if _check_cl_exists_in_vc_dir(env, VC_DIR, ver): + if _check_files_exist_in_vc_dir(env, VC_DIR, ver): installed_versions.append(ver) else: debug('no compiler found %s', ver) else: debug('return None for ver %s', ver) - except (MSVCUnsupportedTargetArch, MSVCUnsupportedHostArch): - # Allow this exception to propagate further as it should cause + except (MSVCUnsupportedTargetArch, MSVCUnsupportedHostArch, VSWhereUserError): + # Allow these exceptions to propagate further as it should cause # SCons to exit with an error code raise except VisualCException as e: @@ -1164,8 +2055,10 @@ def get_installed_vcs(env=None): def reset_installed_vcs() -> None: """Make it try again to find VC. This is just for the tests.""" - global __INSTALLED_VCS_RUN - __INSTALLED_VCS_RUN = None + _reset_installed_vcs() + _reset_vc_pdir() + _VSWhereExecutable.reset() + _VSWhere.reset() MSVC._reset() def msvc_default_version(env=None): @@ -1341,7 +2234,7 @@ def msvc_find_valid_batch_script(env, version): # Find the product directory pdir = None try: - pdir = find_vc_pdir(env, version) + pdir = find_vc_pdir(version, env) except UnsupportedVersion: # Unsupported msvc version (raise MSVCArgumentError?) pass @@ -1386,6 +2279,13 @@ def msvc_find_valid_batch_script(env, version): if not vc_script: continue + if not target_platform and MSVC.ScriptArguments.msvc_script_arguments_has_uwp(env): + # no target arch specified and is a store/uwp build + if MSVC.Kind.msvc_version_skip_uwp_target(env, version): + # store/uwp may not be supported for all express targets (prevent constraint error) + debug('skip uwp target arch: version=%s, target=%s', repr(version), repr(target_arch)) + continue + # Try to use the located batch file for this host/target platform combo arg = MSVC.ScriptArguments.msvc_script_arguments(env, version, vc_dir, arg) debug('trying vc_script:%s, vc_script_args:%s', repr(vc_script), arg) @@ -1483,6 +2383,9 @@ def get_use_script_use_settings(env): def msvc_setup_env(env): debug('called') + + _VSWhereExecutable.vswhere_freeze_env(env) + version = get_default_version(env) if version is None: if not msvc_setup_env_user(env): @@ -1616,13 +2519,17 @@ def msvc_sdk_versions(version=None, msvc_uwp_app: bool=False): msg = f'Unsupported version {version!r}' raise MSVCArgumentError(msg) - rval = MSVC.WinSDK.get_msvc_sdk_version_list(version, msvc_uwp_app) + rval = MSVC.WinSDK.get_msvc_sdk_version_list(version_def.msvc_version, msvc_uwp_app) return rval -def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False): - debug('msvc_version=%s, full=%s, sxs=%s', repr(msvc_version), repr(full), repr(sxs)) +def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False, vswhere_exe=None): + debug( + 'msvc_version=%s, full=%s, sxs=%s, vswhere_exe=%s', + repr(msvc_version), repr(full), repr(sxs), repr(vswhere_exe) + ) + + _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe) - env = None rval = [] if not msvc_version: @@ -1636,7 +2543,7 @@ def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False): msg = f'Unsupported msvc version {msvc_version!r}' raise MSVCArgumentError(msg) - vc_dir = find_vc_pdir(env, msvc_version) + vc_dir = _find_vc_pdir(msvc_version, vswhere_exe) if not vc_dir: debug('VC folder not found for version %s', repr(msvc_version)) return rval @@ -1644,10 +2551,11 @@ def msvc_toolset_versions(msvc_version=None, full: bool=True, sxs: bool=False): rval = MSVC.ScriptArguments._msvc_toolset_versions_internal(msvc_version, vc_dir, full=full, sxs=sxs) return rval -def msvc_toolset_versions_spectre(msvc_version=None): - debug('msvc_version=%s', repr(msvc_version)) +def msvc_toolset_versions_spectre(msvc_version=None, vswhere_exe=None): + debug('msvc_version=%s, vswhere_exe=%s', repr(msvc_version), repr(vswhere_exe)) + + _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe) - env = None rval = [] if not msvc_version: @@ -1661,7 +2569,7 @@ def msvc_toolset_versions_spectre(msvc_version=None): msg = f'Unsupported msvc version {msvc_version!r}' raise MSVCArgumentError(msg) - vc_dir = find_vc_pdir(env, msvc_version) + vc_dir = _find_vc_pdir(msvc_version, vswhere_exe) if not vc_dir: debug('VC folder not found for version %s', repr(msvc_version)) return rval @@ -1669,30 +2577,91 @@ def msvc_toolset_versions_spectre(msvc_version=None): rval = MSVC.ScriptArguments._msvc_toolset_versions_spectre_internal(msvc_version, vc_dir) return rval -def msvc_query_version_toolset(version=None, prefer_newest: bool=True): +_InstalledVersionToolset = namedtuple('_InstalledVersionToolset', [ + 'msvc_version_def', + 'toolset_version_def', +]) + +_InstalledVCSToolsetsComponents = namedtuple('_InstalledVCSToolsetComponents', [ + 'sxs_map', + 'toolset_vcs', + 'msvc_toolset_component_defs', +]) + +def get_installed_vcs_toolsets_components(vswhere_exe=None): + debug('vswhere_exe=%s', repr(vswhere_exe)) + + _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe) + + sxs_map = {} + toolset_vcs = set() + msvc_toolset_component_defs = [] + + vcs = get_installed_vcs() + for msvc_version in vcs: + + vc_dir = _find_vc_pdir(msvc_version, vswhere_exe) + if not vc_dir: + continue + + msvc_version_def = MSVC.Util.msvc_version_components(msvc_version) + toolset_vcs.add(msvc_version_def.msvc_version) + + if msvc_version_def.msvc_vernum > 14.0: + + toolsets_sxs, toolsets_full = MSVC.ScriptArguments._msvc_version_toolsets_internal( + msvc_version, vc_dir + ) + + debug('msvc_version=%s, toolset_sxs=%s', repr(msvc_version), repr(toolsets_sxs)) + sxs_map.update(toolsets_sxs) + + else: + + toolsets_full = [msvc_version_def.msvc_verstr] + + for toolset_version in toolsets_full: + debug('msvc_version=%s, toolset_version=%s', repr(msvc_version), repr(toolset_version)) + + toolset_version_def = MSVC.Util.msvc_extended_version_components(toolset_version) + if not toolset_version_def: + continue + toolset_vcs.add(toolset_version_def.msvc_version) + + rval = _InstalledVersionToolset( + msvc_version_def=msvc_version_def, + toolset_version_def=toolset_version_def, + ) + msvc_toolset_component_defs.append(rval) + + installed_vcs_toolsets_components = _InstalledVCSToolsetsComponents( + sxs_map=sxs_map, + toolset_vcs=toolset_vcs, + msvc_toolset_component_defs=msvc_toolset_component_defs, + ) + + return installed_vcs_toolsets_components + +def msvc_query_version_toolset(version=None, prefer_newest: bool=True, vswhere_exe=None): """ - Returns an msvc version and a toolset version given a version + Return an msvc version and a toolset version given a version specification. This is an EXPERIMENTAL proxy for using a toolset version to perform msvc instance selection. This function will be removed when toolset version is taken into account during msvc instance selection. - Search for an installed Visual Studio instance that supports the - specified version. + This function searches for an installed Visual Studio instance that + contains the requested version. A component suffix (e.g., Exp) is not + supported for toolset version specifications (e.g., 14.39). - When the specified version contains a component suffix (e.g., Exp), - the msvc version is returned and the toolset version is None. No - search if performed. + An MSVCArgumentError is raised when argument validation fails. An + MSVCToolsetVersionNotFound exception is raised when the requested + version is not found. - When the specified version does not contain a component suffix, the - version is treated as a toolset version specification. A search is - performed for the first msvc instance that contains the toolset - version. - - Only Visual Studio 2017 and later support toolset arguments. For - Visual Studio 2015 and earlier, the msvc version is returned and - the toolset version is None. + For Visual Studio 2015 and earlier, the msvc version is returned and + the toolset version is None. For Visual Studio 2017 and later, the + selected toolset version is returned. Args: @@ -1706,6 +2675,9 @@ def msvc_query_version_toolset(version=None, prefer_newest: bool=True): the native Visual Studio instance is not detected, prefer newer Visual Studio instances. + vswhere_exe: str + A vswhere executable location or None. + Returns: tuple: A tuple containing the msvc version and the msvc toolset version. The msvc toolset version may be None. @@ -1714,112 +2686,140 @@ def msvc_query_version_toolset(version=None, prefer_newest: bool=True): MSVCToolsetVersionNotFound: when the specified version is not found. MSVCArgumentError: when argument validation fails. """ - debug('version=%s, prefer_newest=%s', repr(version), repr(prefer_newest)) + debug( + 'version=%s, prefer_newest=%s, vswhere_exe=%s', + repr(version), repr(prefer_newest), repr(vswhere_exe) + ) + + _VSWhereExecutable.vswhere_freeze_executable(vswhere_exe) - env = None msvc_version = None msvc_toolset_version = None - if not version: - version = msvc_default_version() + with MSVC.Policy.msvc_notfound_policy_contextmanager('suppress'): - if not version: - debug('no msvc versions detected') - return msvc_version, msvc_toolset_version + vcs = get_installed_vcs() - version_def = MSVC.Util.msvc_extended_version_components(version) + if not version: + version = msvc_default_version() - if not version_def: - msg = f'Unsupported msvc version {version!r}' - raise MSVCArgumentError(msg) + if not version: + msg = f'No versions of the MSVC compiler were found' + debug(f'MSVCToolsetVersionNotFound: {msg}') + raise MSVCToolsetVersionNotFound(msg) - if version_def.msvc_suffix: - if version_def.msvc_verstr != version_def.msvc_toolset_version: - # toolset version with component suffix - msg = f'Unsupported toolset version {version!r}' - raise MSVCArgumentError(msg) + version_def = MSVC.Util.msvc_extended_version_components(version) - if version_def.msvc_vernum > 14.0: - # VS2017 and later - force_toolset_msvc_version = False - else: - # VS2015 and earlier - force_toolset_msvc_version = True - extended_version = version_def.msvc_verstr + '0.00000' - if not extended_version.startswith(version_def.msvc_toolset_version): - # toolset not equivalent to msvc version - msg = 'Unsupported toolset version {} (expected {})'.format( - repr(version), repr(extended_version) - ) + if not version_def: + msg = f'Unsupported MSVC version {version!r}' + debug(f'MSVCArgumentError: {msg}') raise MSVCArgumentError(msg) - msvc_version = version_def.msvc_version + msvc_version = version_def.msvc_version - if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP: - # VS2013 and earlier - debug( - 'ignore: msvc_version=%s, msvc_toolset_version=%s', - repr(msvc_version), repr(msvc_toolset_version) - ) - return msvc_version, msvc_toolset_version + if version_def.msvc_suffix: - if force_toolset_msvc_version: - query_msvc_toolset_version = version_def.msvc_verstr - else: - query_msvc_toolset_version = version_def.msvc_toolset_version + if version_def.msvc_verstr != version_def.msvc_toolset_version: + # toolset version with component suffix + msg = f'Unsupported MSVC toolset version {version!r}' + debug(f'MSVCArgumentError: {msg}') + raise MSVCArgumentError(msg) - if prefer_newest: - query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version] - else: - query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_DEFAULTS_MAP[msvc_version] + \ - MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version] + if msvc_version not in vcs: + msg = f'MSVC version {msvc_version!r} not found' + debug(f'MSVCToolsetVersionNotFound: {msg}') + raise MSVCToolsetVersionNotFound(msg) + + if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP: + debug( + 'suffix: msvc_version=%s, msvc_toolset_version=%s', + repr(msvc_version), repr(msvc_toolset_version) + ) + return msvc_version, msvc_toolset_version - seen_msvc_version = set() - for query_msvc_version in query_version_list: + if version_def.msvc_vernum > 14.0: + # VS2017 and later + force_toolset_msvc_version = False + else: + # VS2015 and earlier + force_toolset_msvc_version = True + extended_version = version_def.msvc_verstr + '0.00000' + if not extended_version.startswith(version_def.msvc_toolset_version): + # toolset not equivalent to msvc version + msg = 'Unsupported MSVC toolset version {} (expected {})'.format( + repr(version), repr(extended_version) + ) + debug(f'MSVCArgumentError: {msg}') + raise MSVCArgumentError(msg) - if query_msvc_version in seen_msvc_version: - continue - seen_msvc_version.add(query_msvc_version) + if msvc_version not in MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP: - vc_dir = find_vc_pdir(env, query_msvc_version) - if not vc_dir: - continue + # VS2013 and earlier + + if msvc_version not in vcs: + msg = f'MSVC version {version!r} not found' + debug(f'MSVCToolsetVersionNotFound: {msg}') + raise MSVCToolsetVersionNotFound(msg) - if query_msvc_version.startswith('14.0'): - # VS2015 does not support toolset version argument - msvc_toolset_version = None debug( - 'found: msvc_version=%s, msvc_toolset_version=%s', - repr(query_msvc_version), repr(msvc_toolset_version) + 'earlier: msvc_version=%s, msvc_toolset_version=%s', + repr(msvc_version), repr(msvc_toolset_version) ) - return query_msvc_version, msvc_toolset_version + return msvc_version, msvc_toolset_version - try: - toolset_vcvars = MSVC.ScriptArguments._msvc_toolset_internal(query_msvc_version, query_msvc_toolset_version, vc_dir) - if toolset_vcvars: - msvc_toolset_version = toolset_vcvars + if force_toolset_msvc_version: + query_msvc_toolset_version = version_def.msvc_verstr + else: + query_msvc_toolset_version = version_def.msvc_toolset_version + + if prefer_newest: + query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version] + else: + query_version_list = MSVC.Config.MSVC_VERSION_TOOLSET_DEFAULTS_MAP[msvc_version] + \ + MSVC.Config.MSVC_VERSION_TOOLSET_SEARCH_MAP[msvc_version] + + seen_msvc_version = set() + for query_msvc_version in query_version_list: + + if query_msvc_version in seen_msvc_version: + continue + seen_msvc_version.add(query_msvc_version) + + vc_dir = _find_vc_pdir(query_msvc_version, vswhere_exe) + if not vc_dir: + continue + + if query_msvc_version.startswith('14.0'): + # VS2015 does not support toolset version argument + msvc_toolset_version = None debug( 'found: msvc_version=%s, msvc_toolset_version=%s', repr(query_msvc_version), repr(msvc_toolset_version) ) return query_msvc_version, msvc_toolset_version - except MSVCToolsetVersionNotFound: - pass + try: + toolset_vcvars = MSVC.ScriptArguments._msvc_toolset_internal(query_msvc_version, query_msvc_toolset_version, vc_dir) + if toolset_vcvars: + msvc_toolset_version = toolset_vcvars + debug( + 'found: msvc_version=%s, msvc_toolset_version=%s', + repr(query_msvc_version), repr(msvc_toolset_version) + ) + return query_msvc_version, msvc_toolset_version + + except MSVCToolsetVersionNotFound: + pass - msvc_toolset_version = query_msvc_toolset_version + msvc_toolset_version = query_msvc_toolset_version debug( 'not found: msvc_version=%s, msvc_toolset_version=%s', repr(msvc_version), repr(msvc_toolset_version) ) - if version_def.msvc_verstr == msvc_toolset_version: - msg = f'MSVC version {version!r} was not found' - MSVC.Policy.msvc_notfound_handler(None, msg) - return msvc_version, msvc_toolset_version - msg = f'MSVC toolset version {version!r} not found' + debug(f'MSVCToolsetVersionNotFound: {msg}') raise MSVCToolsetVersionNotFound(msg) diff --git a/SCons/Tool/MSCommon/vcTests.py b/SCons/Tool/MSCommon/vcTests.py index 9c2bd0ac8c..e1146e89be 100644 --- a/SCons/Tool/MSCommon/vcTests.py +++ b/SCons/Tool/MSCommon/vcTests.py @@ -57,12 +57,22 @@ def testDefaults(self) -> None: Verify that msvc_find_vswhere() find's files in the specified paths """ # import pdb; pdb.set_trace() - vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc.VSWHERE_PATHS] + test_all_dirs = [] + base_dir = test.workpath('fake_vswhere') + + vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_MSVS] + test_vswhere_dirs = [os.path.join(base_dir,d[1:]) for d in vswhere_dirs] + SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_MSVS = test_vswhere_dirs + test_all_dirs += test_vswhere_dirs + + vswhere_dirs = [os.path.splitdrive(p)[1] for p in SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_PKGMGR] test_vswhere_dirs = [os.path.join(base_dir,d[1:]) for d in vswhere_dirs] + SCons.Tool.MSCommon.vc._VSWHERE_EXEGROUP_PKGMGR = test_vswhere_dirs + test_all_dirs += test_vswhere_dirs - SCons.Tool.MSCommon.vc.VSWHERE_PATHS = test_vswhere_dirs - for vsw in test_vswhere_dirs: + for vsw in test_all_dirs: + SCons.Tool.MSCommon.vc._VSWhereExecutable.reset() VswhereTestCase._createVSWhere(vsw) find_path = SCons.Tool.MSCommon.vc.msvc_find_vswhere() self.assertTrue(vsw == find_path, "Didn't find vswhere in %s found in %s" % (vsw, find_path)) @@ -75,7 +85,7 @@ def testDefaults(self) -> None: class MSVcTestCase(unittest.TestCase): @staticmethod - def _createDummyCl(path, add_bin: bool=True) -> None: + def _createDummyFile(path, filename, add_bin: bool=True) -> None: """ Creates a dummy cl.exe in the correct directory. It will create all missing parent directories as well @@ -94,7 +104,7 @@ def _createDummyCl(path, add_bin: bool=True) -> None: if create_path and not os.path.isdir(create_path): os.makedirs(create_path) - create_this = os.path.join(create_path,'cl.exe') + create_this = os.path.join(create_path, filename) # print("Creating: %s"%create_this) with open(create_this,'w') as ct: @@ -105,18 +115,10 @@ def runTest(self) -> None: """ Check that all proper HOST_PLATFORM and TARGET_PLATFORM are handled. Verify that improper HOST_PLATFORM and/or TARGET_PLATFORM are properly handled. - by SCons.Tool.MSCommon.vc._check_cl_exists_in_vc_dir() + by SCons.Tool.MSCommon.vc._check_files_exist_in_vc_dir() """ - check = SCons.Tool.MSCommon.vc._check_cl_exists_in_vc_dir - - env={'TARGET_ARCH':'x86'} - _, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[('x86','x86')] - path = os.path.join('.', *clpathcomps) - MSVcTestCase._createDummyCl(path, add_bin=False) - - # print("retval:%s"%check(env, '.', '8.0')) - + check = SCons.Tool.MSCommon.vc._check_files_exist_in_vc_dir # Setup for 14.1 (VS2017) and later tests @@ -131,122 +133,70 @@ def runTest(self) -> None: except IOError as e: print("Failed trying to write :%s :%s" % (tools_version_file, e)) - # Test 14.3 (VS2022) and later vc_ge2022_list = SCons.Tool.MSCommon.vc._GE2022_HOST_TARGET_CFG.all_pairs - for host, target in vc_ge2022_list: batfile, clpathcomps = SCons.Tool.MSCommon.vc._GE2022_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host,target)] # print("GE 14.3 Got: (%s, %s) -> (%s, %s)"%(host,target,batfile,clpathcomps)) env={'TARGET_ARCH':target, 'HOST_ARCH':host} + path = os.path.join('.', "Auxiliary", "Build", batfile) + MSVcTestCase._createDummyFile(path, batfile, add_bin=False) path = os.path.join('.', 'Tools', 'MSVC', MS_TOOLS_VERSION, *clpathcomps) - MSVcTestCase._createDummyCl(path, add_bin=False) + MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False) result=check(env, '.', '14.3') # print("for:(%s, %s) got :%s"%(host, target, result)) self.assertTrue(result, "Checking host: %s target: %s" % (host, target)) - # Now test bogus value for HOST_ARCH - env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'} - try: - result=check(env, '.', '14.3') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedHostArch: - pass - else: - self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH']) - - # Now test bogus value for TARGET_ARCH - env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'} - try: - result=check(env, '.', '14.3') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedTargetArch: - pass - else: - self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH']) - # Test 14.2 (VS2019) to 14.1 (VS2017) versions vc_le2019_list = SCons.Tool.MSCommon.vc._LE2019_HOST_TARGET_CFG.all_pairs - for host, target in vc_le2019_list: batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2019_HOST_TARGET_BATCHFILE_CLPATHCOMPS[(host,target)] # print("LE 14.2 Got: (%s, %s) -> (%s, %s)"%(host,target,batfile,clpathcomps)) env={'TARGET_ARCH':target, 'HOST_ARCH':host} path = os.path.join('.', 'Tools', 'MSVC', MS_TOOLS_VERSION, *clpathcomps) - MSVcTestCase._createDummyCl(path, add_bin=False) + MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False) result=check(env, '.', '14.1') # print("for:(%s, %s) got :%s"%(host, target, result)) self.assertTrue(result, "Checking host: %s target: %s" % (host, target)) - # Now test bogus value for HOST_ARCH - env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'} - try: - result=check(env, '.', '14.1') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedHostArch: - pass - else: - self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH']) - - # Now test bogus value for TARGET_ARCH - env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'} - try: - result=check(env, '.', '14.1') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedTargetArch: - pass - else: - self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH']) - - # Test 14.0 (VS2015) to 8.0 (VS2005) versions + # Test 14.0 (VS2015) to 10.0 (VS2010) versions vc_le2015_list = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_CFG.all_pairs - for host, target in vc_le2015_list: - batarg, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_CLPATHCOMPS[(host, target)] - # print("LE 14.0 Got: (%s, %s) -> (%s, %s)"%(host,target,batarg,clpathcomps)) + batarg, batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2015_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host, target)] + # print("LE 14.0 Got: (%s, %s) -> (%s, %s, %s)"%(host,target,batarg,batfile,clpathcomps)) env={'TARGET_ARCH':target, 'HOST_ARCH':host} + MSVcTestCase._createDummyFile('.', 'vcvarsall.bat', add_bin=False) path = os.path.join('.', *clpathcomps) - MSVcTestCase._createDummyCl(path, add_bin=False) - result=check(env, '.', '9.0') + MSVcTestCase._createDummyFile(path, batfile, add_bin=False) + MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False) + result=check(env, '.', '10.0') # print("for:(%s, %s) got :%s"%(host, target, result)) self.assertTrue(result, "Checking host: %s target: %s" % (host, target)) - # Now test bogus value for HOST_ARCH - env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'} - try: - result=check(env, '.', '9.0') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedHostArch: - pass - else: - self.fail('Did not fail when HOST_ARCH specified as: %s' % env['HOST_ARCH']) - - # Now test bogus value for TARGET_ARCH - env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'} - try: - result=check(env, '.', '9.0') - # print("for:%s got :%s"%(env, result)) - self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) - except MSVCUnsupportedTargetArch: - pass - else: - self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH']) + # Test 9.0 (VC2008) to 8.0 (VS2005) + vc_le2008_list = SCons.Tool.MSCommon.vc._LE2008_HOST_TARGET_CFG.all_pairs + for host, target in vc_le2008_list: + batarg, batfile, clpathcomps = SCons.Tool.MSCommon.vc._LE2008_HOST_TARGET_BATCHARG_BATCHFILE_CLPATHCOMPS[(host, target)] + # print("LE 9.0 Got: (%s, %s) -> (%s, %s, %s)"%(host,target,batarg,batfile,clpathcomps)) + env={'TARGET_ARCH':target, 'HOST_ARCH':host} + MSVcTestCase._createDummyFile('.', 'vcvarsall.bat', add_bin=False) + path = os.path.join('.', *clpathcomps) + MSVcTestCase._createDummyFile(path, batfile, add_bin=False) + MSVcTestCase._createDummyFile(path, 'cl.exe', add_bin=False) + # check will fail if '9.0' and VCForPython (layout different) + result=check(env, '.', '8.0') + # print("for:(%s, %s) got :%s"%(host, target, result)) + self.assertTrue(result, "Checking host: %s target: %s" % (host, target)) # Test 7.1 (VS2003) and earlier vc_le2003_list = SCons.Tool.MSCommon.vc._LE2003_HOST_TARGET_CFG.all_pairs - for host, target in vc_le2003_list: # print("LE 7.1 Got: (%s, %s)"%(host,target)) env={'TARGET_ARCH':target, 'HOST_ARCH':host} path = os.path.join('.') - MSVcTestCase._createDummyCl(path) + MSVcTestCase._createDummyFile(path, 'cl.exe') result=check(env, '.', '6.0') # print("for:(%s, %s) got :%s"%(host, target, result)) self.assertTrue(result, "Checking host: %s target: %s" % (host, target)) @@ -254,7 +204,7 @@ def runTest(self) -> None: # Now test bogus value for HOST_ARCH env={'TARGET_ARCH':'x86', 'HOST_ARCH':'GARBAGE'} try: - result=check(env, '.', '6.0') + result=check(env, '.', '14.3') # print("for:%s got :%s"%(env, result)) self.assertFalse(result, "Did not fail with bogus HOST_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) except MSVCUnsupportedHostArch: @@ -265,7 +215,7 @@ def runTest(self) -> None: # Now test bogus value for TARGET_ARCH env={'TARGET_ARCH':'GARBAGE', 'HOST_ARCH':'x86'} try: - result=check(env, '.', '6.0') + result=check(env, '.', '14.3') # print("for:%s got :%s"%(env, result)) self.assertFalse(result, "Did not fail with bogus TARGET_ARCH host: %s target: %s" % (env['HOST_ARCH'], env['TARGET_ARCH'])) except MSVCUnsupportedTargetArch: @@ -273,12 +223,29 @@ def runTest(self) -> None: else: self.fail('Did not fail when TARGET_ARCH specified as: %s' % env['TARGET_ARCH']) - class Data: - HAVE_MSVC = True if MSCommon.vc.msvc_default_version() else False + DEFAULT_VERSION = MSCommon.vc.msvc_default_version() + + if DEFAULT_VERSION: + HAVE_MSVC = True + DEFAULT_VERSION_DEF = MSCommon.msvc_version_components(DEFAULT_VERSION) + else: + HAVE_MSVC = False + DEFAULT_VERSION_DEF = None + INSTALLED_VCS = MSCommon.vc.get_installed_vcs() INSTALLED_VCS_COMPONENTS = MSCommon.vc.get_installed_vcs_components() + INSTALLED_VCS_TOOLSETS_COMPONENTS = MSCommon.vc.get_installed_vcs_toolsets_components() + + @classmethod + def query_version_list(cls, vcver): + # VS 2022 (14.3) can have either/both toolset versions 14.3X and 14.4X + if vcver == '14.3' or (vcver is None and cls.DEFAULT_VERSION == '14.3'): + vcver_list = ['14.4', '14.3'] + else: + vcver_list = [vcver] + return vcver_list @classmethod def _msvc_toolset_notfound_list(cls, toolset_seen, toolset_list): @@ -291,11 +258,11 @@ def _msvc_toolset_notfound_list(cls, toolset_seen, toolset_list): if len(comps) != 3: continue # full versions only + ival = int(comps[-1]) nloop = 0 while nloop < 10: - ival = int(comps[-1]) if ival == 0: - ival = 1000000 + ival = 100000 ival -= 1 version = '{}.{}.{:05d}'.format(comps[0], comps[1], ival) if version not in toolset_seen: @@ -371,10 +338,18 @@ def test_valid_vcver(self) -> None: version_def = MSCommon.msvc_version_components(symbol) for msvc_uwp_app in (True, False): sdk_list = MSCommon.vc.msvc_sdk_versions(version=symbol, msvc_uwp_app=msvc_uwp_app) - if Data.HAVE_MSVC and version_def.msvc_vernum >= 14.0: + if version_def.msvc_vernum < 14.0: + # version < VS2015 + # does not accept sdk version argument + self.assertFalse(sdk_list, "SDK list is not empty for msvc version {}".format(repr(symbol))) + elif Data.HAVE_MSVC and Data.DEFAULT_VERSION_DEF.msvc_vernum >= 14.0: + # version >= VS2015 and default_version >= VS2015 + # no guarantee test is valid: compatible sdks may not be installed as version gap grows self.assertTrue(sdk_list, "SDK list is empty for msvc version {}".format(repr(symbol))) else: - self.assertFalse(sdk_list, "SDK list is not empty for msvc version {}".format(repr(symbol))) + # version >= VS2015 and default_version < VS2015 + # skip test: version is not installed; compatible windows sdks may not be installed + pass def test_valid_vcver_toolsets(self) -> None: for symbol in MSCommon.vc._VCVER: @@ -490,15 +465,26 @@ class MsvcQueryVersionToolsetTests(unittest.TestCase): def run_valid_default_msvc(self, have_msvc) -> None: for prefer_newest in (True, False): - msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( - version=None, prefer_newest=prefer_newest - ) - expect = (have_msvc and msvc_version) or (not have_msvc and not msvc_version) - self.assertTrue(expect, "unexpected msvc_version {} for for msvc version {}".format( + if not have_msvc: + with self.assertRaises(MSCommon.vc.MSVCToolsetVersionNotFound): + msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( + version=None, prefer_newest=prefer_newest + ) + continue + msvc_version = msvc_toolset_version = None + for vcver in Data.query_version_list(None): + try: + msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( + version=vcver, prefer_newest=prefer_newest + ) + break + except MSCommon.vc.MSVCToolsetVersionNotFound: + pass + self.assertTrue(msvc_version, "unexpected msvc_version {} for for msvc version {}".format( repr(msvc_version), repr(None) )) version_def = MSCommon.msvc_version_components(msvc_version) - if have_msvc and version_def.msvc_vernum > 14.0: + if version_def.msvc_vernum > 14.0: # VS2017 and later for toolset version self.assertTrue(msvc_toolset_version, "msvc_toolset_version is undefined for msvc version {}".format( repr(None) @@ -513,11 +499,24 @@ def test_valid_default_msvc(self) -> None: def test_valid_vcver(self) -> None: for symbol in MSCommon.vc._VCVER: + have_msvc = bool(symbol in Data.INSTALLED_VCS_TOOLSETS_COMPONENTS.toolset_vcs) version_def = MSCommon.msvc_version_components(symbol) for prefer_newest in (True, False): - msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( - version=symbol, prefer_newest=prefer_newest - ) + if not have_msvc: + with self.assertRaises(MSCommon.vc.MSVCToolsetVersionNotFound): + msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( + version=symbol, prefer_newest=prefer_newest + ) + continue + msvc_version = msvc_toolset_version = None + for vcver in Data.query_version_list(symbol): + try: + msvc_version, msvc_toolset_version = MSCommon.vc.msvc_query_version_toolset( + version=vcver, prefer_newest=prefer_newest + ) + break + except: + pass self.assertTrue(msvc_version, "msvc_version is undefined for msvc version {}".format(repr(symbol))) if version_def.msvc_vernum > 14.0: # VS2017 and later for toolset version diff --git a/SCons/Tool/MSCommon/vs.py b/SCons/Tool/MSCommon/vs.py index af0fd26e5a..34fb670b5f 100644 --- a/SCons/Tool/MSCommon/vs.py +++ b/SCons/Tool/MSCommon/vs.py @@ -28,7 +28,6 @@ import os import SCons.Errors -import SCons.Tool.MSCommon.vc import SCons.Util from .common import ( @@ -40,6 +39,19 @@ read_reg, ) +from .vc import ( + find_vc_pdir, + get_msvc_version_numeric, + reset_installed_vcs, + vswhere_freeze_env, +) + + +# Visual Studio express version policy when unqualified version is not installed: +# True: use express version for unqualified version (e.g., use 12.0Exp for 12.0) +# False: do not use express version for unqualified version +_VSEXPRESS_USE_VERSTR = True + class VisualStudio: """ @@ -48,6 +60,9 @@ class VisualStudio: """ def __init__(self, version, **kw) -> None: self.version = version + self.verstr = get_msvc_version_numeric(version) + self.vernum = float(self.verstr) + self.is_express = True if self.verstr != self.version else False kw['vc_version'] = kw.get('vc_version', version) kw['sdk_version'] = kw.get('sdk_version', version) self.__dict__.update(kw) @@ -66,7 +81,7 @@ def find_batch_file(self): return batch_file def find_vs_dir_by_vc(self, env): - dir = SCons.Tool.MSCommon.vc.find_vc_pdir(env, self.vc_version) + dir = find_vc_pdir(self.vc_version, env) if not dir: debug('no installed VC %s', self.vc_version) return None @@ -428,6 +443,7 @@ def reset(self) -> None: def get_installed_visual_studios(env=None): global InstalledVSList global InstalledVSMap + vswhere_freeze_env(env) if InstalledVSList is None: InstalledVSList = [] InstalledVSMap = {} @@ -436,12 +452,21 @@ def get_installed_visual_studios(env=None): if vs.get_executable(env): debug('found VS %s', vs.version) InstalledVSList.append(vs) + if vs.is_express and vs.verstr not in InstalledVSMap: + if _VSEXPRESS_USE_VERSTR: + InstalledVSMap[vs.verstr] = vs InstalledVSMap[vs.version] = vs return InstalledVSList +def _get_installed_vss(env=None): + get_installed_visual_studios(env) + versions = list(InstalledVSMap.keys()) + return versions + def reset_installed_visual_studios() -> None: global InstalledVSList global InstalledVSMap + debug('') InstalledVSList = None InstalledVSMap = None for vs in SupportedVSList: @@ -449,7 +474,7 @@ def reset_installed_visual_studios() -> None: # Need to clear installed VC's as well as they are used in finding # installed VS's - SCons.Tool.MSCommon.vc.reset_installed_vcs() + reset_installed_vcs() # We may be asked to update multiple construction environments with @@ -487,7 +512,7 @@ def reset_installed_visual_studios() -> None: def msvs_exists(env=None) -> bool: return len(get_installed_visual_studios(env)) > 0 -def get_vs_by_version(msvs): +def get_vs_by_version(msvs, env=None): global InstalledVSMap global SupportedVSMap @@ -495,7 +520,7 @@ def get_vs_by_version(msvs): if msvs not in SupportedVSMap: msg = "Visual Studio version %s is not supported" % repr(msvs) raise SCons.Errors.UserError(msg) - get_installed_visual_studios() + get_installed_visual_studios(env) vs = InstalledVSMap.get(msvs) debug('InstalledVSMap:%s', InstalledVSMap) debug('found vs:%s', vs) @@ -524,7 +549,7 @@ def get_default_version(env): """ if 'MSVS' not in env or not SCons.Util.is_Dict(env['MSVS']): # get all versions, and remember them for speed later - versions = [vs.version for vs in get_installed_visual_studios()] + versions = _get_installed_vss(env) env['MSVS'] = {'VERSIONS' : versions} else: versions = env['MSVS'].get('VERSIONS', []) @@ -570,7 +595,7 @@ def merge_default_version(env) -> None: # TODO: refers to versions and arch which aren't defined; called nowhere. Drop? def msvs_setup_env(env) -> None: - msvs = get_vs_by_version(version) + msvs = get_vs_by_version(version, env) if msvs is None: return batfilename = msvs.get_batch_file() @@ -582,7 +607,7 @@ def msvs_setup_env(env) -> None: vars = ('LIB', 'LIBPATH', 'PATH', 'INCLUDE') - msvs_list = get_installed_visual_studios() + msvs_list = get_installed_visual_studios(env) vscommonvarnames = [vs.common_tools_var for vs in msvs_list] save_ENV = env['ENV'] nenv = normalize_env(env['ENV'], @@ -597,11 +622,10 @@ def msvs_setup_env(env) -> None: for k, v in vars.items(): env.PrependENVPath(k, v, delete_existing=1) -def query_versions(): +def query_versions(env=None): """Query the system to get available versions of VS. A version is considered when a batfile is found.""" - msvs_list = get_installed_visual_studios() - versions = [msvs.version for msvs in msvs_list] + versions = _get_installed_vss(env) return versions # Local Variables: diff --git a/SCons/Tool/Tool.xml b/SCons/Tool/Tool.xml index fd8deb87af..fcd33fe93d 100644 --- a/SCons/Tool/Tool.xml +++ b/SCons/Tool/Tool.xml @@ -1,9 +1,10 @@ .mm -suffixes as Objective C++ files. +suffixes as Objective-C++ files. On case-sensitive systems (Linux, UNIX, and other POSIX-alikes), SCons also treats .C @@ -566,7 +567,7 @@ When this &consvar; is defined, a versioned shared library is created by the &b-link-SharedLibrary; builder. This activates the &cv-link-_SHLIBVERSIONFLAGS; and thus modifies the &cv-link-SHLINKCOM; as required, adds the version number to the library name, and creates the symlinks -that are needed. &cv-link-SHLIBVERSION; versions should exist as alpha-numeric, +that are needed. &cv-link-SHLIBVERSION; versions should exist as alphanumeric, decimal-delimited values as defined by the regular expression "\w+[\.\w+]*". Example &cv-link-SHLIBVERSION; values include '1', '1.2.3', and '1.2.gitaa412c8b'. diff --git a/SCons/Tool/__init__.py b/SCons/Tool/__init__.py index 474414e3dc..23b7eeba2c 100644 --- a/SCons/Tool/__init__.py +++ b/SCons/Tool/__init__.py @@ -33,10 +33,11 @@ tool specifications. """ +from __future__ import annotations + import sys import os import importlib.util -from typing import Optional import SCons.Builder import SCons.Errors @@ -101,6 +102,7 @@ 'gettext': 'gettext_tool', 'clang++': 'clangxx', 'as': 'asm', + 'ninja' : 'ninja_tool' } @@ -691,8 +693,8 @@ def tool_list(platform, env): if str(platform) == 'win32': "prefer Microsoft tools on Windows" linkers = ['mslink', 'gnulink', 'ilink', 'linkloc', 'ilink32'] - c_compilers = ['msvc', 'mingw', 'gcc', 'intelc', 'icl', 'icc', 'cc', 'bcc32'] - cxx_compilers = ['msvc', 'intelc', 'icc', 'g++', 'cxx', 'bcc32'] + c_compilers = ['msvc', 'mingw', 'gcc', 'clang', 'intelc', 'icl', 'icc', 'cc', 'bcc32'] + cxx_compilers = ['msvc', 'intelc', 'icc', 'g++', 'clang++', 'cxx', 'bcc32'] assemblers = ['masm', 'nasm', 'gas', '386asm'] fortran_compilers = ['gfortran', 'g77', 'ifl', 'cvf', 'f95', 'f90', 'fortran'] ars = ['mslib', 'ar', 'tlib'] @@ -757,8 +759,8 @@ def tool_list(platform, env): else: "prefer GNU tools on all other platforms" linkers = ['gnulink', 'ilink'] - c_compilers = ['gcc', 'intelc', 'icc', 'cc'] - cxx_compilers = ['g++', 'intelc', 'icc', 'cxx'] + c_compilers = ['gcc', 'clang', 'intelc', 'icc', 'cc'] + cxx_compilers = ['g++', 'clang++', 'intelc', 'icc', 'cxx'] assemblers = ['gas', 'nasm', 'masm'] fortran_compilers = ['gfortran', 'g77', 'ifort', 'ifl', 'f95', 'f90', 'f77'] ars = ['ar', ] @@ -824,7 +826,7 @@ def tool_list(platform, env): return [x for x in tools if x] -def find_program_path(env, key_program, default_paths=None, add_path: bool=False) -> Optional[str]: +def find_program_path(env, key_program, default_paths=None, add_path: bool=False) -> str | None: """ Find the location of a tool using various means. diff --git a/SCons/Tool/aixc++.xml b/SCons/Tool/aixc++.xml index 91849eda03..4e78fb35a3 100644 --- a/SCons/Tool/aixc++.xml +++ b/SCons/Tool/aixc++.xml @@ -1,9 +1,10 @@ The C++ compiler. -See also &cv-link-SHCXX; for compiling to shared objects.. +See also &cv-link-SHCXX; for compiling to shared objects. @@ -67,7 +68,7 @@ The command line used to compile a C++ source file to an object file. Any options specified in the &cv-link-CXXFLAGS; and &cv-link-CPPFLAGS; construction variables are included on this command line. -See also &cv-link-SHCXXCOM; for compiling to shared objects.. +See also &cv-link-SHCXXCOM; for compiling to shared objects. @@ -78,7 +79,7 @@ See also &cv-link-SHCXXCOM; for compiling to shared objects.. If set, the string displayed when a C++ source file is compiled to a (static) object file. If not set, then &cv-link-CXXCOM; (the command line) is displayed. -See also &cv-link-SHCXXCOMSTR; for compiling to shared objects.. +See also &cv-link-SHCXXCOMSTR; for compiling to shared objects. @@ -95,7 +96,7 @@ By default, this includes the value of &cv-link-CCFLAGS;, so that setting &cv-CCFLAGS; affects both C and C++ compilation. If you want to add C++-specific flags, you must set or override the value of &cv-link-CXXFLAGS;. -See also &cv-link-SHCXXFLAGS; for compiling to shared objects.. +See also &cv-link-SHCXXFLAGS; for compiling to shared objects. diff --git a/SCons/Tool/cc.xml b/SCons/Tool/cc.xml index 7c8f944e70..065f608c1e 100644 --- a/SCons/Tool/cc.xml +++ b/SCons/Tool/cc.xml @@ -1,9 +1,10 @@ None: source_file = entry['file'] output_file = entry['output'] + if not source_file.is_derived(): + source_file = source_file.srcnode() + if use_abspath: - source_file = source_file.srcnode().abspath + source_file = source_file.abspath output_file = output_file.abspath else: - source_file = source_file.srcnode().path + source_file = source_file.path output_file = output_file.path if use_path_filter and not fnmatch.fnmatch(output_file, use_path_filter): @@ -189,9 +194,8 @@ def compilation_db_emitter(target, source, env): if not target and len(source) == 1: target = source - # Default target name is compilation_db.json if not target: - target = ['compile_commands.json', ] + target = [DEFAULT_DB_NAME] # No source should have been passed. Drop it. if source: @@ -224,13 +228,17 @@ def generate(env, **kwargs) -> None: ), itertools.product( ASSuffixes, - [(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM")], - [(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM")], + [ + (static_obj, SCons.Defaults.StaticObjectEmitter, "$ASCOM"), + (shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASCOM") + ], ), itertools.product( ASPPSuffixes, - [(static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM")], - [(shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM")], + [ + (static_obj, SCons.Defaults.StaticObjectEmitter, "$ASPPCOM"), + (shared_obj, SCons.Defaults.SharedObjectEmitter, "$ASPPCOM") + ], ), ) diff --git a/SCons/Tool/compilation_db.xml b/SCons/Tool/compilation_db.xml index bad808f684..dfc89a56b4 100644 --- a/SCons/Tool/compilation_db.xml +++ b/SCons/Tool/compilation_db.xml @@ -1,9 +1,11 @@ None: # TODO: Set minimum version of saxon-xslt to be 8.x (lower than this only supports xslt 1.0. # see: https://saxon.sourceforge.net/saxon6.5.5/ # see: https://saxon.sourceforge.net/ -xsltproc_com = {'xsltproc' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE', - 'saxon' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS', - # Note if saxon-xslt is version 5.5 the proper arguments are: (swap order of docbook_xsl and source) - # 'saxon-xslt' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $SOURCE $DOCBOOK_XSL $DOCBOOK_XSLTPROCPARAMS', - 'saxon-xslt' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS', - 'xalan' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -q -out $TARGET -xsl $DOCBOOK_XSL -in $SOURCE'} -xmllint_com = {'xmllint' : '$DOCBOOK_XMLLINT $DOCBOOK_XMLLINTFLAGS --xinclude $SOURCE > $TARGET'} -fop_com = {'fop' : '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -fo $SOURCE -pdf $TARGET', - 'xep' : '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -valid -fo $SOURCE -pdf $TARGET', - 'jw' : '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -f docbook -b pdf $SOURCE -o $TARGET'} +xsltproc_com = { + 'xsltproc': '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE', + 'saxon': '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS', + # Note if saxon-xslt is version 5.5 the proper arguments are: (swap order of docbook_xsl and source) + # 'saxon-xslt' : '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $SOURCE $DOCBOOK_XSL $DOCBOOK_XSLTPROCPARAMS', + 'saxon-xslt': '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -o $TARGET $DOCBOOK_XSL $SOURCE $DOCBOOK_XSLTPROCPARAMS', + 'xalan': '$DOCBOOK_XSLTPROC $DOCBOOK_XSLTPROCFLAGS -q -out $TARGET -xsl $DOCBOOK_XSL -in $SOURCE', +} +xmllint_com = { + 'xmllint': '$DOCBOOK_XMLLINT $DOCBOOK_XMLLINTFLAGS --xinclude $SOURCE > $TARGET' +} +fop_com = { + 'fop': '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -fo $SOURCE -pdf $TARGET', + 'xep': '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -valid -fo $SOURCE -pdf $TARGET', + 'jw': '$DOCBOOK_FOP $DOCBOOK_FOPFLAGS -f docbook -b pdf $SOURCE -o $TARGET', +} def __detect_cl_tool(env, chainkey, cdict, cpriority=None) -> None: """ @@ -180,11 +186,11 @@ def __detect_cl_tool(env, chainkey, cdict, cpriority=None) -> None: cpriority = cdict.keys() for cltool in cpriority: if __debug_tool_location: - print("DocBook: Looking for %s"%cltool) + print(f"DocBook: Looking for {cltool}") clpath = env.WhereIs(cltool) if clpath: if __debug_tool_location: - print("DocBook: Found:%s"%cltool) + print(f"DocBook: Found:{cltool}") env[chainkey] = clpath if not env[chainkey + 'COM']: env[chainkey + 'COM'] = cdict[cltool] diff --git a/SCons/Tool/docbook/docbook.xml b/SCons/Tool/docbook/docbook.xml index c367520aed..464c1b2556 100644 --- a/SCons/Tool/docbook/docbook.xml +++ b/SCons/Tool/docbook/docbook.xml @@ -1,11 +1,10 @@ -This tool tries to make working with Docbook in SCons a little easier. +This tool tries to make working with Docbook in &SCons; a little easier. It provides several toolchains for creating different output formats, like HTML or PDF. Contained in the package is a distribution of the Docbook XSL stylesheets as of version 1.76.1. @@ -287,7 +286,7 @@ if one of them is installed (fop gets checked first). -Additonal command-line flags for the external executable +Additional command-line flags for the external executable xsltproc (or saxon, xalan). @@ -297,7 +296,7 @@ Additonal command-line flags for the external executable -Additonal command-line flags for the external executable +Additional command-line flags for the external executable xmllint. @@ -306,7 +305,7 @@ Additonal command-line flags for the external executable -Additonal command-line flags for the +Additional command-line flags for the PDF renderer fop or xep. @@ -315,7 +314,7 @@ PDF renderer fop or xep. -Additonal parameters that are not intended for the XSLT processor executable, but +Additional parameters that are not intended for the XSLT processor executable, but the XSL processing itself. By default, they get appended at the end of the command line for saxon and saxon-xslt, respectively. diff --git a/SCons/Tool/dvi.xml b/SCons/Tool/dvi.xml index ee67e1474e..a631c2abad 100644 --- a/SCons/Tool/dvi.xml +++ b/SCons/Tool/dvi.xml @@ -1,9 +1,10 @@ .ps by default) is added automatically to the target -if it is not already present. Example: +if it is not already present. +&b-PostScript; is a single-source builder. +Example: - + # builds from aaa.tex env.PostScript(target = 'aaa.ps', source = 'aaa.tex') # builds bbb.ps from bbb.dvi env.PostScript(target = 'bbb', source = 'bbb.dvi') - + diff --git a/SCons/Tool/f03.xml b/SCons/Tool/f03.xml index c9893855a9..3a0a8a2182 100644 --- a/SCons/Tool/f03.xml +++ b/SCons/Tool/f03.xml @@ -1,9 +1,10 @@ -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/f08.xml b/SCons/Tool/f08.xml index 8f69b932dd..8fd017679e 100644 --- a/SCons/Tool/f08.xml +++ b/SCons/Tool/f08.xml @@ -1,9 +1,10 @@ -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/f77.xml b/SCons/Tool/f77.xml index f60d617875..e6992986f5 100644 --- a/SCons/Tool/f77.xml +++ b/SCons/Tool/f77.xml @@ -1,9 +1,10 @@ -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/f90.xml b/SCons/Tool/f90.xml index ab76deff44..89c5ddced9 100644 --- a/SCons/Tool/f90.xml +++ b/SCons/Tool/f90.xml @@ -1,9 +1,10 @@ -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/f95.xml b/SCons/Tool/f95.xml index 59cfd78b89..5dd55d943f 100644 --- a/SCons/Tool/f95.xml +++ b/SCons/Tool/f95.xml @@ -1,9 +1,10 @@ -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/fortran.xml b/SCons/Tool/fortran.xml index 4a092eceae..2b62f05005 100644 --- a/SCons/Tool/fortran.xml +++ b/SCons/Tool/fortran.xml @@ -1,9 +1,10 @@ diff --git a/SCons/Tool/g++.xml b/SCons/Tool/g++.xml index 3bc3ec25ef..c2625da8cd 100644 --- a/SCons/Tool/g++.xml +++ b/SCons/Tool/g++.xml @@ -1,9 +1,10 @@ -This is actually a toolset, which supports internationalization and -localization of software being constructed with SCons. The toolset loads -following tools: +A toolset supporting internationalization and +localization of software being constructed with &SCons;. +The toolset loads the following tools: - &t-link-xgettext; - to extract internationalized messages from source code to - POT file(s), + &t-link-xgettext; - extract internationalized messages from source code to + POT file(s). - &t-link-msginit; - may be optionally used to initialize PO - files, + &t-link-msginit; - initialize PO + files during initial translation of a project. - &t-link-msgmerge; - to update PO files, that already contain + &t-link-msgmerge; - update PO files that already contain translated messages, - &t-link-msgfmt; - to compile textual PO file to binary - installable MO file. + &t-link-msgfmt; - compile textual PO files to binary + installable MO files. -When you enable &t-gettext;, it internally loads all abovementioned tools, +When you enable &t-gettext;, it internally loads all the above-mentioned tools, so you're encouraged to see their individual documentation. Each of the above tools provides its own builder(s) which may be used to perform particular activities related to software internationalization. You -may be however interested in top-level +may be however interested in top-level &b-link-Translate; builder. -To use &t-gettext; tools add 'gettext' tool to your -environment: +To use the &t-gettext; tools, add the 'gettext' tool to your +&consenv;: - - env = Environment( tools = ['default', 'gettext'] ) - + +env = Environment(tools=['default', 'gettext']) + @@ -81,54 +82,56 @@ environment: -This pseudo-builder belongs to &t-link-gettext; toolset. The builder extracts -internationalized messages from source files, updates POT -template (if necessary) and then updates PO translations (if -necessary). If &cv-link-POAUTOINIT; is set, missing PO files +This pseudo-Builder is part of the &t-link-gettext; toolset. +The builder extracts internationalized messages from source files, +updates the POT template (if necessary) +and then updates PO translations (if necessary). +If &cv-link-POAUTOINIT; is set, missing PO files will be automatically created (i.e. without translator person intervention). The variables &cv-link-LINGUAS_FILE; and &cv-link-POTDOMAIN; are taken into -acount too. All other construction variables used by &b-link-POTUpdate;, and +account too. All other construction variables used by &b-link-POTUpdate;, and &b-link-POUpdate; work here too. Example 1. The simplest way is to specify input files and output languages inline in -a SCons script when invoking &b-Translate; +a SCons script when invoking &b-Translate;: - + # SConscript in 'po/' directory -env = Environment( tools = ["default", "gettext"] ) -env['POAUTOINIT'] = 1 -env.Translate(['en','pl'], ['../a.cpp','../b.cpp']) - +env = Environment(tools=["default", "gettext"]) +env['POAUTOINIT'] = True +env.Translate(['en', 'pl'], ['../a.cpp', '../b.cpp']) + Example 2. -If you wish, you may also stick to conventional style known from +If you wish, you may also stick to the conventional style known from autotools, i.e. using POTFILES.in and LINGUAS files +to specify the targets and sources: - + # LINGUAS -en pl -#end - +en pl +# end + - + # POTFILES.in a.cpp b.cpp # end - + - + # SConscript -env = Environment( tools = ["default", "gettext"] ) -env['POAUTOINIT'] = 1 +env = Environment(tools=["default", "gettext"]) +env['POAUTOINIT'] = True env['XGETTEXTPATH'] = ['../'] -env.Translate(LINGUAS_FILE = 1, XGETTEXTFROM = 'POTFILES.in') - +env.Translate(LINGUAS_FILE=True, XGETTEXTFROM='POTFILES.in') + The last approach is perhaps the recommended one. It allows easily split @@ -141,7 +144,7 @@ factor" synchronizing these two scripts is then the content of LINGUAS file. Note, that the updated POT and PO files are usually going to be committed back to the repository, so they must be updated within the source -directory (and not in variant directories). Additionaly, the file listing of +directory (and not in variant directories). Additionally, the file listing of po/ directory contains LINGUAS file, so the source tree looks familiar to translators, and they may work with the project in their usual way. @@ -151,62 +154,65 @@ project in their usual way. Example 3. Let's prepare a development tree as below - + project/ + SConstruct - + build/ + + build/ + src/ + po/ + SConscript + SConscript.i18n + POTFILES.in + LINGUAS - + -with build being variant directory. Write the top-level +with build being the variant directory. +Write the top-level SConstruct script as follows - - # SConstruct - env = Environment( tools = ["default", "gettext"] ) - VariantDir('build', 'src', duplicate = 0) - env['POAUTOINIT'] = 1 - SConscript('src/po/SConscript.i18n', exports = 'env') - SConscript('build/po/SConscript', exports = 'env') - + +# SConstruct +env = Environment(tools=["default", "gettext"]) +VariantDir('build', 'src', duplicate=False) +env['POAUTOINIT'] = True +SConscript('src/po/SConscript.i18n', exports='env') +SConscript('build/po/SConscript', exports='env') + the src/po/SConscript.i18n as - - # src/po/SConscript.i18n - Import('env') - env.Translate(LINGUAS_FILE=1, XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../']) - + +# src/po/SConscript.i18n +Import('env') +env.Translate(LINGUAS_FILE=True, XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../']) + and the src/po/SConscript - - # src/po/SConscript - Import('env') - env.MOFiles(LINGUAS_FILE = 1) - + +# src/po/SConscript +Import('env') +env.MOFiles(LINGUAS_FILE=True) + -Such setup produces POT and PO files -under source tree in src/po/ and binary -MO files under variant tree in +Such a setup produces POT and PO files +under the source tree in src/po/ and binary +MO files under the variant tree in build/po/. This way the POT and PO files are separated from other output files, which must not be committed back to source repositories (e.g. MO files). - -In above example, the PO files are not updated, -nor created automatically when you issue scons '.' command. -The files must be updated (created) by hand via scons -po-update and then MO files can be compiled by -running scons '.'. - +In the above example, +the PO files are not updated, +nor created automatically when you issue the command +scons .. +The files must be updated (created) by hand via +scons po-update +and then MO files can be compiled by +running scons .. + @@ -244,10 +250,10 @@ them). The &cv-LINGUAS_FILE; defines file(s) containing list of additional linguas to be processed by &b-link-POInit;, &b-link-POUpdate; or &b-link-MOFiles; builders. It also affects &b-link-Translate; builder. If the variable contains -a string, it defines name of the list file. The &cv-LINGUAS_FILE; may be a +a string, it defines the name of the list file. The &cv-LINGUAS_FILE; may be a list of file names as well. If &cv-LINGUAS_FILE; is set to -True (or non-zero numeric value), the list will be read from -default file named +a non-string truthy value, the list will be read from +the file named LINGUAS. diff --git a/SCons/Tool/gfortran.xml b/SCons/Tool/gfortran.xml index 95458fc46a..fc18327820 100644 --- a/SCons/Tool/gfortran.xml +++ b/SCons/Tool/gfortran.xml @@ -1,9 +1,10 @@ -This Tool sets the required construction variables for working with +This &f-Tool; sets the required construction variables for working with the Ghostscript software. It also registers an appropriate Action with the &b-link-PDF; Builder, such that the conversion from PS/EPS to PDF happens automatically for the TeX/LaTeX toolchain. diff --git a/SCons/Tool/hpc++.xml b/SCons/Tool/hpc++.xml index 76c09795da..96d0d15a1b 100644 --- a/SCons/Tool/hpc++.xml +++ b/SCons/Tool/hpc++.xml @@ -1,9 +1,10 @@ int: else: copy2(source, dest) st = os.stat(source) - os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) + os.chmod(dest, stat.S_IMODE(st.st_mode) | stat.S_IWRITE) return 0 @@ -204,7 +204,7 @@ def copyFuncVersionedLib(dest, source, env) -> int: pass copy2(source, dest) st = os.stat(source) - os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) + os.chmod(dest, stat.S_IMODE(st.st_mode) | stat.S_IWRITE) installShlibLinks(dest, source, env) return 0 diff --git a/SCons/Tool/install.xml b/SCons/Tool/install.xml index f4295a03f4..3cbb0f4f6a 100644 --- a/SCons/Tool/install.xml +++ b/SCons/Tool/install.xml @@ -1,9 +1,10 @@ --install-sandbox command line option is given, the target directory will be prefixed by the directory path specified. -This is useful to test installs without installing to +This is useful to test installation behavior without installing to a "live" location in the system. -See also &FindInstalledFiles;. +See also &f-link-FindInstalledFiles;. For more thoughts on installation, see the User Guide (particularly the section on Command-Line Targets and the chapters on Installing Files and on Alias Targets). diff --git a/SCons/Tool/intelc.xml b/SCons/Tool/intelc.xml index 87958a5101..ea9e1c0ed0 100644 --- a/SCons/Tool/intelc.xml +++ b/SCons/Tool/intelc.xml @@ -1,9 +1,10 @@ List[str]: +def jarSources(target, source, env, for_signature) -> list[str]: """Only include sources that are not a manifest file.""" try: env['JARCHDIR'] diff --git a/SCons/Tool/jar.xml b/SCons/Tool/jar.xml index 403aacb154..ba8b2dff22 100644 --- a/SCons/Tool/jar.xml +++ b/SCons/Tool/jar.xml @@ -1,11 +1,10 @@ General options passed to the Java archive tool. -By default this is set to +By default, this is set to to create the necessary jar diff --git a/SCons/Tool/javac.xml b/SCons/Tool/javac.xml index 6f48356e40..c43470cf11 100644 --- a/SCons/Tool/javac.xml +++ b/SCons/Tool/javac.xml @@ -1,11 +1,10 @@ LANG environment variable to tell the compiler what encoding is used. - For portibility, it's best if the encoding is hard-coded - so that the compile will work if it is done on a system + For portability, it's best if the encoding is hard-coded, + so that the compilation works when run on a system with a different encoding. @@ -267,7 +266,7 @@ env = Environment(JAVACCOMSTR="Compiling class files $TARGETS from $SOURCES") If &cv-JAVACLASSPATH; is a single string containing search path separator characters (: for POSIX systems or - ; for Windows), + ; for Windows), it will be split on the separator into a list of individual paths for dependency scanning purposes. It will not be modified for JDK command-line usage, @@ -278,7 +277,7 @@ env = Environment(JAVACCOMSTR="Compiling class files $TARGETS from $SOURCES") &SCons; always - supplies a + supplies a when invoking the Java compiler &javac;, regardless of the setting of &cv-link-JAVASOURCEPATH;, as it passes the path(s) to the source(s) supplied @@ -365,7 +364,7 @@ env = Environment(JAVACCOMSTR="Compiling class files $TARGETS from $SOURCES") that &SCons; expects will be generated by the &javac; compiler. Setting &cv-JAVAVERSION; to a version greater than 1.4 makes &SCons; realize that a build - with such a compiler is actually up to date. + with such a compiler is actually up-to-date. The default is 1.4. diff --git a/SCons/Tool/javah.xml b/SCons/Tool/javah.xml index 16e969a5f6..7021753c71 100644 --- a/SCons/Tool/javah.xml +++ b/SCons/Tool/javah.xml @@ -1,11 +1,10 @@ tuple: return target, source -def get_lex_path(env, append_paths: bool=False) -> Optional[str]: +def get_lex_path(env, append_paths: bool=False) -> str | None: """ Returns the path to the lex tool, searching several possible names. @@ -162,7 +163,7 @@ def generate(env) -> None: env['_LEX_TABLES'] = '${LEX_TABLES_FILE and "--tables-file=" + str(LEX_TABLES_FILE)}' -def exists(env) -> Optional[str]: +def exists(env) -> str | None: if sys.platform == 'win32': return get_lex_path(env) else: diff --git a/SCons/Tool/lex.xml b/SCons/Tool/lex.xml index 391e429895..028072b6e5 100644 --- a/SCons/Tool/lex.xml +++ b/SCons/Tool/lex.xml @@ -1,31 +1,10 @@ -Sets construction variables for the &lex; lexical analyser. +Sets construction variables for the &lex; lexical analyzer. @@ -144,7 +123,7 @@ command-line option. Use this in preference to including -Used only on windows environments to set a lex flag to prevent 'unistd.h' from being included. The default value is '--nounistd'. +Used only in Windows environments to set a lex flag to prevent 'unistd.h' from being included. The default value is '--nounistd'. diff --git a/SCons/Tool/link.xml b/SCons/Tool/link.xml index 210d946de5..63d499c011 100644 --- a/SCons/Tool/link.xml +++ b/SCons/Tool/link.xml @@ -1,9 +1,10 @@ This construction variable automatically introduces &cv-link-_LDMODULEVERSIONFLAGS; -if &cv-link-LDMODULEVERSION; is set. Othervise it evaluates to an empty string. +if &cv-link-LDMODULEVERSION; is set. Otherwise, it evaluates to an empty string. @@ -74,7 +75,7 @@ if &cv-link-LDMODULEVERSION; is set. Othervise it evaluates to an empty string. This construction variable automatically introduces &cv-link-_SHLIBVERSIONFLAGS; -if &cv-link-SHLIBVERSION; is set. Othervise it evaluates to an empty string. +if &cv-link-SHLIBVERSION; is set. Otherwise, it evaluates to an empty string. @@ -152,7 +153,7 @@ to '.dll'. -Used to override &cv-link-SHLIBNOVERSIONSYMLINKS;/&cv-link-LDMODULENOVERSIONSYMLINKS; when +Used to override &cv-link-SHLIBNOVERSIONSYMLINKS;/&cv-link-LDMODULENOVERSIONSYMLINKS; when creating versioned import library for a shared library/loadable module. If not defined, then &cv-link-SHLIBNOVERSIONSYMLINKS;/&cv-link-LDMODULENOVERSIONSYMLINKS; is used to determine whether to disable symlink generation or not. @@ -410,7 +411,7 @@ The variable is used, for example, by &t-link-gnulink; linker tool. -This will construct the SONAME using on the base library name +This will construct the SONAME using on the base library name (test in the example below) and use specified SOVERSION to create SONAME. @@ -419,7 +420,7 @@ env.SharedLibrary('test', 'test.c', SHLIBVERSION='0.1.2', SOVERSION='2') The variable is used, for example, by &t-link-gnulink; linker tool. -In the example above SONAME would be libtest.so.2 +In the example above SONAME would be libtest.so.2 which would be a symlink and point to libtest.so.0.1.2 diff --git a/SCons/Tool/linkCommon/SharedLibrary.py b/SCons/Tool/linkCommon/SharedLibrary.py index 30170f8bb8..04de7fac02 100644 --- a/SCons/Tool/linkCommon/SharedLibrary.py +++ b/SCons/Tool/linkCommon/SharedLibrary.py @@ -202,7 +202,7 @@ def setup_shared_lib_logic(env) -> None: # Note this is gnu style env["SHLIBSONAMEFLAGS"] = "-Wl,-soname=$_SHLIBSONAME" - env["_SHLIBVERSION"] = "${SHLIBVERSION and '.'+SHLIBVERSION or ''}" + env["_SHLIBVERSION"] = "${'.' + SHLIBVERSION if SHLIBVERSION else ''}" env["_SHLIBVERSIONFLAGS"] = "$SHLIBVERSIONFLAGS -Wl,-soname=$_SHLIBSONAME" env["SHLIBEMITTER"] = [lib_emitter, shlib_symlink_emitter] diff --git a/SCons/Tool/linkloc.xml b/SCons/Tool/linkloc.xml index 2b5ba6a5d1..86d80d73cd 100644 --- a/SCons/Tool/linkloc.xml +++ b/SCons/Tool/linkloc.xml @@ -1,9 +1,10 @@ -This scons tool is a part of scons &t-link-gettext; toolset. It provides scons -interface to msgfmt(1) command, which generates binary -message catalog (MO) from a textual translation description -(PO). +This tool is a part of the &t-link-gettext; toolset. +It provides &SCons; +an interface to the msgfmt(1) command +by setting up the &b-link-MOFiles; builder, +which generates binary message catalog (MO) files +from a textual translation description +(PO files). @@ -48,8 +52,12 @@ message catalog (MO) from a textual translation description -This builder belongs to &t-link-msgfmt; tool. The builder compiles +This builder is set up by the &t-link-msgfmt; tool. +The builder compiles PO files to MO files. +&b-MOFiles; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. @@ -57,19 +65,17 @@ This builder belongs to &t-link-msgfmt; tool. The builder compiles Create pl.mo and en.mo by compiling pl.po and en.po: - - # ... - env.MOFiles(['pl', 'en']) - + +env.MOFiles(['pl', 'en']) + Example 2. Compile files for languages defined in LINGUAS file: - - # ... - env.MOFiles(LINGUAS_FILE = 1) - + +env.MOFiles(LINGUAS_FILE=True) + Example 3. @@ -77,24 +83,22 @@ Create pl.mo and en.mo by compiling pl.po and en.po plus files for languages defined in LINGUAS file: - - # ... - env.MOFiles(['pl', 'en'], LINGUAS_FILE = 1) - + +env.MOFiles(['pl', 'en'], LINGUAS_FILE=True) + Example 4. -Compile files for languages defined in LINGUAS file +Compile files for languages defined in LINGUAS file (another version): - - # ... - env['LINGUAS_FILE'] = 1 - env.MOFiles() - + +env['LINGUAS_FILE'] = True +env.MOFiles() + - + @@ -103,7 +107,7 @@ See &t-link-msgfmt; tool and &b-link-MOFiles; builder. - + @@ -113,7 +117,7 @@ See &t-link-msgfmt; tool and &b-link-MOFiles; builder. - + @@ -122,17 +126,17 @@ See &t-link-msgfmt; tool and &b-link-MOFiles; builder. - + -String to display when msgfmt(1) is invoked +String to display when msgfmt(1) is invoked (default: '', which means ``print &cv-link-MSGFMTCOM;''). See &t-link-msgfmt; tool and &b-link-MOFiles; builder. - + diff --git a/SCons/Tool/msginit.xml b/SCons/Tool/msginit.xml index 70178518b6..225074d308 100644 --- a/SCons/Tool/msginit.xml +++ b/SCons/Tool/msginit.xml @@ -1,9 +1,10 @@ -This scons tool is a part of scons &t-link-gettext; toolset. It provides -scons interface to msginit(1) program, which creates new +This tool is a part of scons &t-link-gettext; toolset. It provides +&SCons; an interface to the msginit(1) program, +by setting up the &b-link-POInit; builder, +which creates a new PO file, initializing the meta information with values from -user's environment (or options). +the &consenv; (or options). @@ -53,26 +56,31 @@ user's environment (or options). -This builder belongs to &t-link-msginit; tool. The builder initializes missing -PO file(s) if &cv-link-POAUTOINIT; is set. If -&cv-link-POAUTOINIT; is not set (default), &b-POInit; prints instruction for -user (that is supposed to be a translator), telling how the -PO file should be initialized. In normal projects +This builder is set up by the &t-link-msginit; tool. +The builder initializes missing +PO file(s) if &cv-link-POAUTOINIT; is set. +If &cv-link-POAUTOINIT; is not set (the default), +&b-POInit; prints instruction for the user (such as a translator), +telling how the PO file should be initialized. +In normal projects you should not use &b-POInit; and use &b-link-POUpdate; instead. &b-link-POUpdate; chooses intelligently between msgmerge(1) and msginit(1). &b-POInit; always uses msginit(1) and should be regarded as builder for special purposes or for temporary use (e.g. for quick, one time initialization of a bunch of PO files) or for tests. +&b-POInit; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. Target nodes defined through &b-POInit; are not built by default (they're Ignored from '.' node) but are added to -special Alias ('po-create' by default). +special &f-link-Alias; ('po-create' by default). The alias name may be changed through the &cv-link-POCREATE_ALIAS; -construction variable. All PO files defined through -&b-POInit; may be easily initialized by scons po-create. +&consvar;. All PO files defined through +&b-POInit; may be easily initialized by scons po-create. @@ -80,31 +88,27 @@ construction variable. All PO files defined through Initialize en.po and pl.po from messages.pot: - - # ... - env.POInit(['en', 'pl']) # messages.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl']) # messages.pot --> [en.po, pl.po] + Example 2. Initialize en.po and pl.po from foo.pot: - - # ... - env.POInit(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.po] + Example 3. Initialize en.po and pl.po from -foo.pot but using &cv-link-POTDOMAIN; construction -variable: +foo.pot but using the &cv-link-POTDOMAIN; &consvar;: - - # ... - env.POInit(['en', 'pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] + Example 4. @@ -112,10 +116,9 @@ Initialize PO files for languages defined in LINGUAS file. The files will be initialized from template messages.pot: - - # ... - env.POInit(LINGUAS_FILE = 1) # needs 'LINGUAS' file - + +env.POInit(LINGUAS_FILE=True) # needs 'LINGUAS' file + Example 5. @@ -124,34 +127,30 @@ Initialize en.po and pl.pl LINGUAS file. The files will be initialized from template messages.pot: - - # ... - env.POInit(['en', 'pl'], LINGUAS_FILE = 1) - + +env.POInit(['en', 'pl'], LINGUAS_FILE=True) + Example 6. You may preconfigure your environment first, and then initialize PO files: - - # ... - env['POAUTOINIT'] = 1 - env['LINGUAS_FILE'] = 1 - env['POTDOMAIN'] = 'foo' - env.POInit() - + +env['POAUTOINIT'] = True +env['LINGUAS_FILE'] = True +env['POTDOMAIN'] = 'foo' +env.POInit() + which has same efect as: - - # ... - env.POInit(POAUTOINIT = 1, LINGUAS_FILE = 1, POTDOMAIN = 'foo') - + +env.POInit(POAUTOINIT=True, LINGUAS_FILE=True, POTDOMAIN='foo') + - @@ -161,7 +160,7 @@ See &t-link-msginit; tool and &b-link-POInit; builder. - + @@ -170,17 +169,17 @@ See &t-link-msginit; tool and &b-link-POInit; builder. - + Path to msginit(1) program (found via -Detect()). +&f-link-Detect;). See &t-link-msginit; tool and &b-link-POInit; builder. - + @@ -189,17 +188,18 @@ See &t-link-msginit; tool and &b-link-POInit; builder. - + -String to display when msginit(1) is invoked -(default: '', which means ``print &cv-link-MSGINITCOM;''). +String to display when msginit(1) is invoked. +The default is an empty string, +which will print the command line (&cv-link-MSGINITCOM;). See &t-link-msginit; tool and &b-link-POInit; builder. - + @@ -209,7 +209,7 @@ See &t-link-msginit; tool and &b-link-POInit; builder. - + diff --git a/SCons/Tool/msgmerge.xml b/SCons/Tool/msgmerge.xml index 266ccc7049..f318d47050 100644 --- a/SCons/Tool/msgmerge.xml +++ b/SCons/Tool/msgmerge.xml @@ -1,9 +1,10 @@ -This scons tool is a part of scons &t-link-gettext; toolset. It provides -scons interface to msgmerge(1) command, which merges two -Uniform style .po files together. +This tool is a part of scons &t-link-gettext; toolset. It provides +&SCons; an interface to the msgmerge(1) command, +by setting up the &b-link-POUpdate; builder, +which merges two +Uniform style .po files together. @@ -50,23 +53,29 @@ Uniform style .po files together. -The builder belongs to &t-link-msgmerge; tool. The builder updates +The builder is set up by the &t-link-msgmerge; tool. +part of the &t-link-gettext; toolset. +The builder updates PO files with msgmerge(1), or initializes -missing PO files as described in documentation of -&t-link-msginit; tool and &b-link-POInit; builder (see also -&cv-link-POAUTOINIT;). Note, that &b-POUpdate; does not add its -targets to po-create alias as &b-link-POInit; -does. +missing PO files as described in the documentation of the +&t-link-msginit; tool and the &b-link-POInit; builder (see also +&cv-link-POAUTOINIT;). +&b-POUpdate; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. -Target nodes defined through &b-POUpdate; are not built by default -(they're Ignored from '.' node). Instead, -they are added automatically to special Alias +The target nodes are not +selected for building by default (e.g. scons .). +Instead, they are added automatically to special &f-link-Alias; ('po-update' by default). The alias name may be changed -through the &cv-link-POUPDATE_ALIAS; construction variable. You can easily -update PO files in your project by scons -po-update. +through the &cv-link-POUPDATE_ALIAS; &consvar;. You can easily +update PO files in your project by +scons po-update. +Note that &b-POUpdate; does not add its +targets to the po-create alias as &b-link-POInit; +does. @@ -76,49 +85,44 @@ Update en.po and pl.po from assuming that the later one exists or there is rule to build it (see &b-link-POTUpdate;): - - # ... - env.POUpdate(['en','pl']) # messages.pot --> [en.po, pl.po] - + +env.POUpdate(['en','pl']) # messages.pot --> [en.po, pl.po] + Example 2. Update en.po and pl.po from foo.pot template: - - # ... - env.POUpdate(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.pl] - + +env.POUpdate(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.pl] + Example 3. Update en.po and pl.po from foo.pot (another version): - - # ... - env.POUpdate(['en', 'pl'], POTDOMAIN='foo') # foo.pot -- > [en.po, pl.pl] - + +env.POUpdate(['en', 'pl'], POTDOMAIN='foo') # foo.pot -- > [en.po, pl.pl] + Example 4. Update files for languages defined in LINGUAS file. The files are updated from messages.pot template: - - # ... - env.POUpdate(LINGUAS_FILE = 1) # needs 'LINGUAS' file - + +env.POUpdate(LINGUAS_FILE=True) # needs 'LINGUAS' file + Example 5. Same as above, but update from foo.pot template: - - # ... - env.POUpdate(LINGUAS_FILE = 1, source = ['foo']) - + +env.POUpdate(LINGUAS_FILE=True, source=['foo']) + Example 6. @@ -126,20 +130,19 @@ Update en.po and pl.po plus files for languages defined in LINGUAS file. The files are updated from messages.pot template: - - # produce 'en.po', 'pl.po' + files defined in 'LINGUAS': - env.POUpdate(['en', 'pl' ], LINGUAS_FILE = 1) - + +# produce 'en.po', 'pl.po' + files defined in 'LINGUAS': +env.POUpdate(['en', 'pl' ], LINGUAS_FILE=True) + Example 7. Use &cv-link-POAUTOINIT; to automatically initialize PO file if it doesn't exist: - - # ... - env.POUpdate(LINGUAS_FILE = 1, POAUTOINIT = 1) - + +env.POUpdate(LINGUAS_FILE=True, POAUTOINIT=True) + Example 8. @@ -148,18 +151,16 @@ Update PO files for languages defined in foo.pot template. All necessary settings are pre-configured via environment. - - # ... - env['POAUTOINIT'] = 1 - env['LINGUAS_FILE'] = 1 - env['POTDOMAIN'] = 'foo' - env.POUpdate() - + +env['POAUTOINIT'] = True +env['LINGUAS_FILE'] = True +env['POTDOMAIN'] = 'foo' +env.POUpdate() + - @@ -169,7 +170,7 @@ See &t-link-msgmerge; tool and &b-link-POUpdate; builder. - + @@ -179,7 +180,7 @@ See &t-link-msgmerge; tool and &b-link-POUpdate; builder. - + @@ -188,17 +189,18 @@ See &t-link-msgmerge; tool and &b-link-POUpdate; builder. - + -String to be displayed when msgmerge(1) is invoked -(default: '', which means ``print &cv-link-MSGMERGECOM;''). +String to be displayed when msgmerge(1) is invoked. +The default is an empty string, +which will print the command line (&cv-link-MSGMERGECOM;). See &t-link-msgmerge; tool and &b-link-POUpdate; builder. - + diff --git a/SCons/Tool/mslib.xml b/SCons/Tool/mslib.xml index a79ec4869e..fdd9ea77c9 100644 --- a/SCons/Tool/mslib.xml +++ b/SCons/Tool/mslib.xml @@ -1,9 +1,10 @@ 6.0A, 6.0, 2003R2 -and +and 2003R1. diff --git a/SCons/Tool/msvc.py b/SCons/Tool/msvc.py index 6afa171c97..e73869bc4e 100644 --- a/SCons/Tool/msvc.py +++ b/SCons/Tool/msvc.py @@ -72,12 +72,12 @@ def msvc_set_PCHPDBFLAGS(env) -> None: if env.get('MSVC_VERSION',False): maj, min = msvc_version_to_maj_min(env['MSVC_VERSION']) if maj < 8: - env['PCHPDBFLAGS'] = SCons.Util.CLVar(['${(PDB and "/Yd") or ""}']) + env['PCHPDBFLAGS'] = SCons.Util.CLVar(['${"/Yd" if PDB else ""}']) else: env['PCHPDBFLAGS'] = '' else: # Default if we can't determine which version of MSVC we're using - env['PCHPDBFLAGS'] = SCons.Util.CLVar(['${(PDB and "/Yd") or ""}']) + env['PCHPDBFLAGS'] = SCons.Util.CLVar(['${"/Yd" if PDB else ""}']) def pch_emitter(target, source, env): @@ -143,7 +143,7 @@ def gen_ccpchflags(env, target, source, for_signature): pch_node = get_pch_node(env, target, source) if not pch_node: return '' - + return SCons.Util.CLVar(["/Yu$PCHSTOP", "/Fp%s" % pch_node]) pch_action = SCons.Action.Action('$PCHCOM', '$PCHCOMSTR') @@ -256,7 +256,7 @@ def generate(env) -> None: static_obj.add_emitter(suffix, static_object_emitter) shared_obj.add_emitter(suffix, shared_object_emitter) - env['CCPDBFLAGS'] = SCons.Util.CLVar(['${(PDB and "/Z7") or ""}']) + env['CCPDBFLAGS'] = SCons.Util.CLVar(['${"/Z7" if PDB else ""}']) env['CCPCHFLAGS'] = gen_ccpchflags env['_MSVC_OUTPUT_FLAG'] = msvc_output_flag env['_CCCOMCOM'] = '$CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $CCPCHFLAGS $CCPDBFLAGS' @@ -296,9 +296,6 @@ def generate(env) -> None: # without it for lex generation env["LEXUNISTD"] = SCons.Util.CLVar("--nounistd") - # Get user specified vswhere location or locate. - env['VSWHERE'] = env.get('VSWHERE', msvc_find_vswhere()) - # Set-up ms tools paths msvc_setup_env_once(env, tool=tool_name) diff --git a/SCons/Tool/msvc.xml b/SCons/Tool/msvc.xml index a0f657368c..c6fa977723 100644 --- a/SCons/Tool/msvc.xml +++ b/SCons/Tool/msvc.xml @@ -1,7 +1,7 @@ num_jobs. @@ -438,7 +438,7 @@ V10DebugSettings = { } # -# 3. Select the dictionary you want depending on the version of visual Studio +# 3. Select the dictionary you want depending on the version of Visual Studio # Files you want to generate. # if not env.GetOption('userfile'): @@ -540,6 +540,74 @@ env.MSVSProject( + + In addition to the mandatory arguments above, the following optional + values may be specified as keyword arguments: + + + + auto_filter_projects + + + Under certain circumstances, solution file names or + solution file nodes may be present in the + projects argument list. + When solution file names or nodes are present in the + projects argument list, the generated + solution file may contain erroneous Project records resulting + in VS IDE error messages when opening the generated solution + file. + By default, an exception is raised when a solution file + name or solution file node is detected in the + projects argument list. + + + The accepted values for auto_filter_projects + are: + + + + None + + + An exception is raised when a solution file name or solution + file node is detected in the projects + argument list. + + + None is the default value. + + + + + True or evaluates True + + + Automatically remove solution file names and solution file + nodes from the projects argument list. + + + + + False or evaluates False + + + Leave the solution file names and solution file nodes + in the projects argument list. + An exception is not raised. + + + When opening the generated solution file with the VS IDE, + the VS IDE will likely report that there are erroneous + Project records that are not supported or that need to be + modified. + + + + + + + Example Usage: env.MSVSSolution( @@ -679,7 +747,7 @@ env.MSVSSolution( SccProjectFilePathRelativizedFromConnection[i] (where [i] ranges from 0 to the number of projects in the solution) attributes of the GlobalSection(SourceCodeControl) - section of the Microsoft Visual Studio solution file. Similarly + section of the Microsoft Visual Studio solution file. Similarly, the relative solution file path is placed as the values of the SccLocalPath[i] (where [i] ranges from 0 to the number of projects in the solution) attributes of the @@ -735,7 +803,7 @@ env.MSVSSolution( and &cv-MSVC_VERSION; is not, &cv-MSVC_VERSION; will be initialized to the value of &cv-MSVS_VERSION;. - An error is raised if If both are set and have different values, + An error is raised if both are set and have different values. diff --git a/SCons/Tool/msvsTests.py b/SCons/Tool/msvsTests.py index dd708d0a7e..dd423d4bd9 100644 --- a/SCons/Tool/msvsTests.py +++ b/SCons/Tool/msvsTests.py @@ -420,6 +420,14 @@ def get(self, name, value=None): def Dir(self, name): return self.fs.Dir(name) + def File(self, name): + return self.fs.File(name) + + def subst(self, key): + if key[0] == '$': + key = key[1:] + return self[key] + class RegKey: """key class for storing an 'open' registry key""" @@ -579,7 +587,7 @@ def DummyQueryValue(key, value): def DummyExists(path) -> bool: return True -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -635,9 +643,9 @@ def _TODO_test_merge_default_version(self) -> None: """Test the merge_default_version() function""" pass - def test_query_versions(self) -> None: + def test_query_versions(self, env=None) -> None: """Test retrieval of the list of visual studio versions""" - v1 = query_versions() + v1 = query_versions(env) assert not v1 or str(v1[0]) == self.highest_version, \ (v1, self.highest_version) assert len(v1) == self.number_of_versions, v1 @@ -880,9 +888,9 @@ class msvs71TestCase(msvsTestCase): class msvs8ExpTestCase(msvsTestCase): # XXX: only one still not working """Test MSVS 8 Express Registry""" registry = DummyRegistry(regdata_8exp + regdata_cv) - default_version = '8.0Exp' - highest_version = '8.0Exp' - number_of_versions = 1 + default_version = '8.0' + highest_version = '8.0' + number_of_versions = 2 install_locs = { '6.0' : {}, '7.0' : {}, @@ -947,7 +955,7 @@ class msvsEmptyTestCase(msvsTestCase): SCons.Util.RegEnumKey = DummyEnumKey SCons.Util.RegEnumValue = DummyEnumValue SCons.Util.RegQueryValueEx = DummyQueryValue - SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere + SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere os.path.exists = DummyExists # make sure all files exist :-) os.path.isfile = DummyExists # make sure all files are files :-) diff --git a/SCons/Tool/mwcc.xml b/SCons/Tool/mwcc.xml index a0c6edae2c..c2a6db4d62 100644 --- a/SCons/Tool/mwcc.xml +++ b/SCons/Tool/mwcc.xml @@ -1,9 +1,10 @@ None: @@ -42,7 +46,7 @@ def register_custom_handler(env, name, handler) -> None: def register_custom_rule_mapping(env, pre_subst_string, rule) -> None: """Register a function to call for a given rule.""" - SCons.Tool.ninja.Globals.__NINJA_RULE_MAPPING[pre_subst_string] = rule + __NINJA_RULE_MAPPING[pre_subst_string] = rule def register_custom_rule(env, rule, command, description: str="", deps=None, pool=None, use_depfile: bool=False, use_response_file: bool=False, response_file_content: str="$rspc") -> None: @@ -78,7 +82,7 @@ def set_build_node_callback(env, node, callback) -> None: node.attributes.ninja_build_callback = callback -def get_generic_shell_command(env, node, action, targets, sources, executor: Optional[ExecutorType] = None): +def get_generic_shell_command(env, node, action, targets, sources, executor: Executor | None = None): return ( "GENERATED_CMD", { @@ -231,7 +235,7 @@ def gen_get_response_file_command(env, rule, tool, tool_is_dynamic: bool=False, if "$" in tool: tool_is_dynamic = True - def get_response_file_command(env, node, action, targets, sources, executor: Optional[ExecutorType] = None): + def get_response_file_command(env, node, action, targets, sources, executor: Executor | None = None): if hasattr(action, "process"): cmd_list, _, _ = action.process(targets, sources, env, executor=executor) cmd_list = [str(c).replace("$", "$$") for c in cmd_list[0]] diff --git a/SCons/Tool/ninja/NinjaState.py b/SCons/Tool/ninja_tool/NinjaState.py similarity index 97% rename from SCons/Tool/ninja/NinjaState.py rename to SCons/Tool/ninja_tool/NinjaState.py index 549af7855e..4b102d1437 100644 --- a/SCons/Tool/ninja/NinjaState.py +++ b/SCons/Tool/ninja_tool/NinjaState.py @@ -60,12 +60,11 @@ def __init__(self, env, ninja_file, ninja_syntax) -> None: if not self.ninja_bin_path: # default to using ninja installed with python module ninja_bin = 'ninja.exe' if env["PLATFORM"] == "win32" else 'ninja' + self.ninja_bin_path = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - ninja_bin)) + ninja.BIN_DIR, ninja_bin + )) + if not os.path.exists(self.ninja_bin_path): # couldn't find it, just give the bin name and hope # its in the path later @@ -232,7 +231,7 @@ def __init__(self, env, ninja_file, ninja_syntax) -> None: "TEMPLATE": { "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH $out", "description": "Defer to SCons to build $out", - "pool": "local_pool", + "pool": "install_pool", "restat": 1 }, "EXIT_SCONS_DAEMON": { @@ -398,7 +397,7 @@ def generate(self): self.rules.update({key: non_rsp_rule}) else: self.rules.update({key: rule}) - + self.pools.update(self.env.get(NINJA_POOLS, {})) content = io.StringIO() @@ -435,7 +434,7 @@ def generate(self): generated_source_files = sorted( [] if not generated_sources_build else generated_sources_build['implicit'] ) - + def check_generated_source_deps(build): return ( build != generated_sources_build @@ -464,7 +463,7 @@ def check_generated_source_deps(build): rule="phony", implicit=generated_source_files ) - + def check_generated_source_deps(build): return ( not build["rule"] == "INSTALL" @@ -661,7 +660,7 @@ def check_generated_source_deps(build): all_targets = [str(node) for node in NINJA_DEFAULT_TARGETS] else: all_targets = list(all_targets) - + if len(all_targets) == 0: all_targets = ["phony_default"] ninja_sorted_build( @@ -669,7 +668,7 @@ def check_generated_source_deps(build): outputs=all_targets, rule="phony", ) - + ninja.default([self.ninja_syntax.escape_path(path) for path in sorted(all_targets)]) with NamedTemporaryFile(delete=False, mode='w') as temp_ninja_file: @@ -754,7 +753,7 @@ def action_to_ninja_build(self, node, action=None): # Ninja builders out of being sources of ninja builders but I # can't fix every DAG problem so we just skip ninja_builders # if we find one - if SCons.Tool.ninja.NINJA_STATE.ninja_file == str(node): + if SCons.Tool.ninja_tool.NINJA_STATE.ninja_file == str(node): build = None elif isinstance(action, SCons.Action.FunctionAction): build = self.handle_func_action(node, action) @@ -825,15 +824,18 @@ def handle_func_action(self, node, action): # pylint: disable=too-many-branches def handle_list_action(self, node, action): - """TODO write this comment""" - results = [ - self.action_to_ninja_build(node, action=act) - for act in action.list - if act is not None - ] - results = [ - result for result in results if result is not None and result["outputs"] - ] + """Handle a ListAction and return a ninja build object.""" + results = [] + for act in action.list: + if act is None: + continue + if isinstance(act, SCons.Action.FunctionAction): + result = self.handle_func_action(node, act) + else: + result = self.action_to_ninja_build(node, action=act) + if result: + results.append(result) + results = [result for result in results if "outputs" in result] if not results: return None @@ -844,17 +846,16 @@ def handle_list_action(self, node, action): all_outputs = list({output for build in results for output in build["outputs"]}) dependencies = list({dep for build in results for dep in build.get("implicit", [])}) - if results[0]["rule"] == "CMD" or results[0]["rule"] == "GENERATED_CMD": + if results[0]["rule"] in ('CMD', 'GENERATED_CMD'): cmdline = "" for cmd in results: - # Occasionally a command line will expand to a # whitespace only string (i.e. ' '). Which is not a # valid command but does not trigger the empty command # condition if not cmdstr. So here we strip preceding # and proceeding whitespace to make strings like the # above become empty strings and so will be skipped. - if not cmd.get("variables") or not cmd["variables"].get("cmd"): + if "variables" not in cmd or not cmd["variables"].get("cmd"): continue cmdstr = cmd["variables"]["cmd"].strip() @@ -886,7 +887,8 @@ def handle_list_action(self, node, action): if cmdline: ninja_build = { "outputs": all_outputs, - "rule": get_rule(node, "GENERATED_CMD"), + "rule": get_rule(node, results[0]["rule"]), + "inputs": get_inputs(node), "variables": { "cmd": cmdline, "env": get_command_env(env, targets, node.sources), diff --git a/SCons/Tool/ninja/Overrides.py b/SCons/Tool/ninja_tool/Overrides.py similarity index 100% rename from SCons/Tool/ninja/Overrides.py rename to SCons/Tool/ninja_tool/Overrides.py diff --git a/SCons/Tool/ninja/Rules.py b/SCons/Tool/ninja_tool/Rules.py similarity index 100% rename from SCons/Tool/ninja/Rules.py rename to SCons/Tool/ninja_tool/Rules.py diff --git a/SCons/Tool/ninja/Utils.py b/SCons/Tool/ninja_tool/Utils.py similarity index 95% rename from SCons/Tool/ninja/Utils.py rename to SCons/Tool/ninja_tool/Utils.py index 126d5de0a9..3ffa465a58 100644 --- a/SCons/Tool/ninja/Utils.py +++ b/SCons/Tool/ninja_tool/Utils.py @@ -20,17 +20,22 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import annotations + import os import shutil from os.path import join as joinpath from collections import OrderedDict -from typing import Optional +from typing import TYPE_CHECKING import SCons from SCons.Action import get_default_ENV, _string_from_cmd_list from SCons.Script import AddOption from SCons.Util import is_List, flatten_sequence -from SCons.Util.sctyping import ExecutorType + +if TYPE_CHECKING: + from SCons.Executor import Executor class NinjaExperimentalWarning(SCons.Warnings.WarningOnByDefault): pass @@ -45,6 +50,7 @@ def ninja_add_command_line_options() -> None: metavar='BOOL', action="store_true", default=False, + settable=True, help='Disable automatically running ninja after scons') AddOption('--disable-ninja', @@ -52,6 +58,7 @@ def ninja_add_command_line_options() -> None: metavar='BOOL', action="store_true", default=False, + settable=True, help='Disable ninja generation and build with scons even if tool is loaded. '+ 'Also used by ninja to build targets which only scons can build.') @@ -60,6 +67,7 @@ def ninja_add_command_line_options() -> None: metavar='BOOL', action="store_true", default=False, + settable=True, help='Allow scons to skip regeneration of the ninja file and restarting of the daemon. ' + 'Care should be taken in cases where Glob is in use or SCons generated files are used in ' + 'command lines.') @@ -346,7 +354,7 @@ def get_comstr(env, action, targets, sources): return action.genstring(targets, sources, env) -def generate_command(env, node, action, targets, sources, executor: Optional[ExecutorType] = None): +def generate_command(env, node, action, targets, sources, executor: Executor | None = None): # Actions like CommandAction have a method called process that is # used by SCons to generate the cmd_line they need to run. So # check if it's a thing like CommandAction and call it if we can. @@ -355,14 +363,13 @@ def generate_command(env, node, action, targets, sources, executor: Optional[Exe cmd = _string_from_cmd_list(cmd_list[0]) else: # Anything else works with genstring, this is most commonly hit by - # ListActions which essentially call process on all of their - # commands and concatenate it for us. + # ListActions which essentially call process() on all of their + # commands and concatenate the result for us. genstring = action.genstring(targets, sources, env) if executor is not None: cmd = env.subst(genstring, executor=executor) else: - cmd = env.subst(genstring, targets, sources) - + cmd = env.subst(genstring, target=targets, source=sources) cmd = cmd.replace("\n", " && ").strip() if cmd.endswith("&&"): cmd = cmd[0:-2].strip() @@ -405,14 +412,14 @@ def ninja_stat(_self, path): """ try: - return SCons.Tool.ninja.Globals.NINJA_STAT_MEMO[path] + return SCons.Tool.ninja_tool.Globals.NINJA_STAT_MEMO[path] except KeyError: try: result = os.stat(path) except os.error: result = None - SCons.Tool.ninja.Globals.NINJA_STAT_MEMO[path] = result + SCons.Tool.ninja_tool.Globals.NINJA_STAT_MEMO[path] = result return result @@ -422,7 +429,7 @@ def ninja_whereis(thing, *_args, **_kwargs): # Optimize for success, this gets called significantly more often # when the value is already memoized than when it's not. try: - return SCons.Tool.ninja.Globals.NINJA_WHEREIS_MEMO[thing] + return SCons.Tool.ninja_tool.Globals.NINJA_WHEREIS_MEMO[thing] except KeyError: # TODO: Fix this to respect env['ENV']['PATH']... WPD # We do not honor any env['ENV'] or env[*] variables in the @@ -435,7 +442,7 @@ def ninja_whereis(thing, *_args, **_kwargs): # with shell quoting is nigh impossible. So I've decided to # cross that bridge when it's absolutely required. path = shutil.which(thing) - SCons.Tool.ninja.Globals.NINJA_WHEREIS_MEMO[thing] = path + SCons.Tool.ninja_tool.Globals.NINJA_WHEREIS_MEMO[thing] = path return path diff --git a/SCons/Tool/ninja_tool/UtilsTests.py b/SCons/Tool/ninja_tool/UtilsTests.py new file mode 100644 index 0000000000..fe4f57e3ef --- /dev/null +++ b/SCons/Tool/ninja_tool/UtilsTests.py @@ -0,0 +1,126 @@ +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Tests for the Ninja tool's Utility functions + +from __future__ import annotations + +import unittest + +import TestCmd + +from SCons.Action import CommandAction, ListAction +from SCons.Action import CommandGeneratorAction, FunctionAction, LazyAction +from SCons.Environment import Base as BaseEnvironment +from SCons.Tool.ninja_tool.Utils import generate_command + + +class TestGenerateCommand(unittest.TestCase): + """Test that generate_command returns reasonable results for Actions.""" + + def setUp(self): + self.env = BaseEnvironment() + self.target = ["target.o"] + self.source = ["source.c"] + + def test_command_action(self): + """Test basic CommandAction handling.""" + action = CommandAction("gcc -c -o $TARGET $SOURCE") + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, f"gcc -c -o {self.target[0]} {self.source[0]}") + + def test_list_action(self): + """Test ListAction concatenation.""" + actions = [ + CommandAction("mkdir -p build"), + CommandAction("gcc -c -o $TARGET $SOURCE"), + ] + action = ListAction(actions) + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, f"mkdir -p build && gcc -c -o {self.target[0]} {self.source[0]}") + + def test_dollar_escaping(self): + """Test dollar signs get properly escaped.""" + action = CommandAction("echo $DOLLAR_VAR") + self.env['DOLLAR_VAR'] = "$5" + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, "echo $$5") + + def test_command_generator_action(self): + """Test CommandGeneratorAction handling.""" + + def generator(target, source, env, for_signature): + return f"gcc -c -o {target[0]} {source[0]}" + + action = CommandGeneratorAction(generator, kw={}) + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, f"gcc -c -o {self.target[0]} {self.source[0]}") + + def test_function_action_simple(self): + """Test FunctionAction with a simple function.""" + + def build_func(target, source, env): + return 0 + + action = FunctionAction(build_func, kw={}) + cmd = generate_command(self.env, None, action, self.target, self.source) + # we could be less precise here, really just care it returns *something* + self.assertEqual(cmd, "build_func(target, source, env)") + + def test_function_action_with_args(self): + """Test FunctionAction with function taking arguments.""" + + def build_func(target, source, env, arg1="default"): + return 0 + + action = FunctionAction(build_func, kw={'vars': ['arg1']}) + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertIsNotNone(cmd) + + def test_lazy_action_str(self): + """Test LazyAction functionality with a string value.""" + self.env['CCCOM'] = 'gcc -c -o $TARGET $SOURCE' + action = LazyAction("CCCOM", kw={}) + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, f"gcc -c -o {self.target[0]} {self.source[0]}") + + def test_lazy_action_function(self): + """Test LazyAction functionality with a function value.""" + + def generator(target, source, env, for_signature): + return f"gcc -c -o {target[0]} {source[0]}" + + self.env['CCCOM'] = generator + action = LazyAction("CCCOM", kw={}) + cmd = generate_command(self.env, None, action, self.target, self.source) + self.assertEqual(cmd, f"gcc -c -o {self.target[0]} {self.source[0]}") + + +if __name__ == "__main__": + unittest.main() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/SCons/Tool/ninja/__init__.py b/SCons/Tool/ninja_tool/__init__.py similarity index 99% rename from SCons/Tool/ninja/__init__.py rename to SCons/Tool/ninja_tool/__init__.py index 7320d03852..d86c2c9d4d 100644 --- a/SCons/Tool/ninja/__init__.py +++ b/SCons/Tool/ninja_tool/__init__.py @@ -32,7 +32,7 @@ import SCons import SCons.Script -import SCons.Tool.ninja.Globals +from SCons.Tool.ninja_tool.Globals import ninja_builder_initialized from SCons.Script import GetOption from SCons.Util import sanitize_shell_env @@ -187,13 +187,13 @@ def ninja_emitter(target, source, env): def generate(env): """Generate the NINJA builders.""" - global NINJA_STATE, NINJA_CMDLINE_TARGETS + global NINJA_STATE, NINJA_CMDLINE_TARGETS, ninja_builder_initialized if 'ninja' not in GetOption('experimental'): return - if not SCons.Tool.ninja.Globals.ninja_builder_initialized: - SCons.Tool.ninja.Globals.ninja_builder_initialized = True + if not ninja_builder_initialized: + ninja_builder_initialized = True ninja_add_command_line_options() @@ -255,7 +255,7 @@ def ninja_generate_deps(env): pass else: env.Append(CCFLAGS='$CCDEPFLAGS') - + env.AddMethod(CheckNinjaCompdbExpand, "CheckNinjaCompdbExpand") # Provide a way for custom rule authors to easily access command diff --git a/SCons/Tool/ninja/ninja.xml b/SCons/Tool/ninja_tool/ninja.xml similarity index 90% rename from SCons/Tool/ninja/ninja.xml rename to SCons/Tool/ninja_tool/ninja.xml index 664d521780..f89687ef9c 100644 --- a/SCons/Tool/ninja/ninja.xml +++ b/SCons/Tool/ninja_tool/ninja.xml @@ -1,31 +1,10 @@ Determines the type of format ninja should expect when parsing header include depfiles. Can be , , or . - The option corresponds to format, and + The option corresponds to format, and or correspond to . @@ -360,7 +339,7 @@ python -m pip install ninja - If true, causes the build nodes to callback to scons instead of using + If true, causes the build nodes to call back to scons instead of using &ninja; to build them. This is intended to be passed to the environment on the builder invocation. It is useful if you have a build node which does something which is not easily translated into &ninja;. @@ -371,7 +350,7 @@ python -m pip install ninja Internal value used to specify the function to call with argument env to generate the list of files - which if changed would require the &ninja; build file to be regenerated. + which, if changed, would require the &ninja; build file to be regenerated. @@ -379,7 +358,7 @@ python -m pip install ninja - The number of seconds for the SCons deamon launched by ninja to stay alive. + The number of seconds for the SCons daemon launched by ninja to stay alive. (Default: 180000) diff --git a/SCons/Tool/ninja/ninja_daemon_build.py b/SCons/Tool/ninja_tool/ninja_daemon_build.py similarity index 97% rename from SCons/Tool/ninja/ninja_daemon_build.py rename to SCons/Tool/ninja_tool/ninja_daemon_build.py index f198d773c6..7ef52b6cfa 100644 --- a/SCons/Tool/ninja/ninja_daemon_build.py +++ b/SCons/Tool/ninja_tool/ninja_daemon_build.py @@ -40,6 +40,7 @@ import hashlib import traceback import socket +from urllib.parse import quote ninja_builddir = pathlib.Path(sys.argv[2]) daemon_dir = pathlib.Path(tempfile.gettempdir()) / ( @@ -77,7 +78,7 @@ def log_error(msg) -> None: if sys.argv[3] == '--exit': conn.request("GET", "/?exit=1") else: - conn.request("GET", "/?build=" + sys.argv[3]) + conn.request("GET", "/?build=" + quote(sys.argv[3])) response = None while not response: @@ -94,7 +95,7 @@ def log_error(msg) -> None: if status != 200: log_error(msg.decode("utf-8")) exit(1) - + logging.debug(f"Request Done: {sys.argv[3]}") exit(0) diff --git a/SCons/Tool/ninja/ninja_run_daemon.py b/SCons/Tool/ninja_tool/ninja_run_daemon.py similarity index 80% rename from SCons/Tool/ninja/ninja_run_daemon.py rename to SCons/Tool/ninja_tool/ninja_run_daemon.py index 666be0f716..a3a3e7667d 100644 --- a/SCons/Tool/ninja/ninja_run_daemon.py +++ b/SCons/Tool/ninja_tool/ninja_run_daemon.py @@ -72,25 +72,10 @@ def log_error(msg) -> None: ] + sys.argv[1:] logging.debug(f"Starting daemon with {' '.join(cmd)}") - - # TODO: Remove the following when Python3.6 support is dropped. - if sys.platform == 'win32' and sys.version_info[0] == 3 and sys.version_info[1] == 6: - # on Windows with Python version 3.6, popen does not do a good job disconnecting - # the std handles and this make ninja hang because they stay open to the original - # process ninja launched. Here we can force the handles to be separated. - # See: https://docs.python.org/3.6/library/subprocess.html#subprocess.STARTUPINFO - # See Also: https://docs.python.org/3.6/library/subprocess.html#subprocess.Popen - # Note when you don't specify stdin, stdout, and/or stderr they default to None - # which indicates no output redirection will occur. - si = subprocess.STARTUPINFO() - si.dwFlags = subprocess.STARTF_USESTDHANDLES - p = subprocess.Popen( - cmd, close_fds=True, shell=False, startupinfo=si - ) - else: - p = subprocess.Popen( - cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=False, - ) + + p = subprocess.Popen( + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=False, + ) with open(daemon_dir / "pidfile", "w") as f: f.write(str(p.pid)) with open(ninja_builddir / "scons_daemon_dirty", "w") as f: diff --git a/SCons/Tool/ninja/ninja_scons_daemon.py b/SCons/Tool/ninja_tool/ninja_scons_daemon.py similarity index 95% rename from SCons/Tool/ninja/ninja_scons_daemon.py rename to SCons/Tool/ninja_tool/ninja_scons_daemon.py index 8e297d3a92..625cf0cb9e 100644 --- a/SCons/Tool/ninja/ninja_scons_daemon.py +++ b/SCons/Tool/ninja_tool/ninja_scons_daemon.py @@ -34,37 +34,29 @@ the keep alive time will be reset. """ +import hashlib import http.server +import logging +import os +import pathlib +import queue +import signal import socketserver -from urllib.parse import urlparse, parse_qs -import time -from threading import Condition -from subprocess import PIPE, Popen import sys -import os +import tempfile import threading -import queue -import pathlib -import logging -from timeit import default_timer as timer +import time import traceback -import tempfile -import hashlib -import signal +from subprocess import PIPE, Popen +from threading import Condition +from timeit import default_timer as timer +from urllib.parse import urlparse, parse_qs port = int(sys.argv[1]) ninja_builddir = pathlib.Path(sys.argv[2]) daemon_keep_alive = int(sys.argv[3]) args = sys.argv[4:] -# TODO: Remove the following when Python3.6 support is dropped. -# Windows and Python36 passed nothing for the std handles because of issues with popen -# and its handles so we have to make some fake ones to prevent exceptions. -if sys.platform == 'win32' and sys.version_info[0] == 3 and sys.version_info[1] == 6: - from io import StringIO - sys.stderr = StringIO() - sys.stdout = StringIO() - daemon_dir = pathlib.Path(tempfile.gettempdir()) / ( "scons_daemon_" + str(hashlib.md5(str(ninja_builddir).encode()).hexdigest()) ) @@ -168,7 +160,7 @@ def daemon_thread_func(): te.start() daemon_ready = False - + building_node = None startup_complete = False @@ -229,7 +221,18 @@ def daemon_thread_func(): break else: - input_command = "build " + building_node + "\n" + def quote_spaces(arg): + """This is the same as SCons.Subst.quote_spaces + + Copied here because the daemon doesn't have + the right context when running. + """ + if ' ' in arg or '\t' in arg: + return '"%s"' % arg + else: + return str(arg) + + input_command = f'build {quote_spaces(building_node)}\n' daemon_log("input: " + input_command.strip()) p.stdin.write(input_command.encode("utf-8")) @@ -272,8 +275,8 @@ def do_GET(self): global keep_alive_timer try: gets = parse_qs(urlparse(self.path).query) - - # process a request from ninja for a node for scons to build. + + # process a request from ninja for a node for scons to build. # Currently this is a serial process because scons interactive is serial # is it was originally meant for a real human user to be providing input # parallel input was never implemented. @@ -341,8 +344,8 @@ def log_message(self, format, *args) -> None: server_thread.daemon = True server_thread.start() -while (timer() - keep_alive_timer < daemon_keep_alive - and not shared_state.thread_error +while (timer() - keep_alive_timer < daemon_keep_alive + and not shared_state.thread_error and not shared_state.daemon_needs_to_shutdown): time.sleep(1) diff --git a/SCons/Tool/packaging/__init__.py b/SCons/Tool/packaging/__init__.py index ea2f429a10..0bf08fa6ad 100644 --- a/SCons/Tool/packaging/__init__.py +++ b/SCons/Tool/packaging/__init__.py @@ -86,8 +86,7 @@ def Tag(env, target, source, *more_tags, **kw_tags): t.Tag(k, v) def Package(env, target=None, source=None, **kw): - """ Entry point for the package tool. - """ + """Entry point for the package tool.""" # check if we need to find the source files ourselves if not source: source = env.FindInstalledFiles() @@ -96,17 +95,11 @@ def Package(env, target=None, source=None, **kw): raise UserError("No source for Package() given") # decide which types of packages shall be built. Can be defined through - # four mechanisms: command line argument, keyword argument, - # environment argument and default selection (zip or tar.gz) in that - # order. - try: - kw['PACKAGETYPE'] = env['PACKAGETYPE'] - except KeyError: - pass - - if not kw.get('PACKAGETYPE'): + # four mechanisms: command line argument, keyword argument, environment + # argument and default selection (zip or tar.gz) in that order. + kw.setdefault('PACKAGETYPE', env.get('PACKAGETYPE')) + if kw['PACKAGETYPE'] is None: kw['PACKAGETYPE'] = GetOption('package_type') - if kw['PACKAGETYPE'] is None: if 'Tar' in env['BUILDERS']: kw['PACKAGETYPE'] = 'targz' diff --git a/SCons/Tool/packaging/packaging.xml b/SCons/Tool/packaging/packaging.xml index b248880e33..6f1a8d8b5c 100644 --- a/SCons/Tool/packaging/packaging.xml +++ b/SCons/Tool/packaging/packaging.xml @@ -1,9 +1,10 @@ &SCons; can build packages in a number of well known packaging formats. The target package type may be selected with the -the &cv-link-PACKAGETYPE; construction variable +&cv-link-PACKAGETYPE; construction variable or the command line option. The package type may be a list, in which case &SCons; will attempt to build packages for each type in the list. Example: @@ -72,7 +73,7 @@ The currently supported packagers are: msiMicrosoft Installer package -rpmRPM Package Manger package +rpmRPM Package Manager package ipkgItsy Package Management package tarbz2bzip2-compressed tar file targzgzip-compressed tar file @@ -261,7 +262,7 @@ placed if applicable. The default value is &cv-NAME;-&cv-VERSION; Selects the package type to build when using the &b-link-Package; -builder. May be a string or list of strings. See the docuentation +builder. It may be a string or list of strings. See the documentation for the builder for the currently supported types. @@ -540,7 +541,7 @@ field in the RPM -A list used to supply extra defintions or flags +A list used to supply extra definitions or flags to be added to the RPM .spec file. Each item is added as-is with a carriage return appended. This is useful if some specific RPM feature not otherwise diff --git a/SCons/Tool/packaging/rpm.py b/SCons/Tool/packaging/rpm.py index 03633db269..a51e8679e6 100644 --- a/SCons/Tool/packaging/rpm.py +++ b/SCons/Tool/packaging/rpm.py @@ -25,7 +25,6 @@ import SCons.Builder import SCons.Tool.rpmutils -from SCons.Environment import OverrideEnvironment from SCons.Tool.packaging import stripinstallbuilder, src_targz from SCons.Errors import UserError @@ -67,7 +66,7 @@ def package(env, target, source, PACKAGEROOT, NAME, VERSION, kw['SOURCE_URL']=(str(target[0])+".tar.gz").replace('.rpm', '') # mangle the source and target list for the rpmbuild - env = OverrideEnvironment(env, kw) + env = env.Override(kw) target, source = stripinstallbuilder(target, source, env) target, source = addspecfile(target, source, env) target, source = collectintargz(target, source, env) diff --git a/SCons/Tool/pdf.xml b/SCons/Tool/pdf.xml index 8d73cbb800..71dbdda2d7 100644 --- a/SCons/Tool/pdf.xml +++ b/SCons/Tool/pdf.xml @@ -1,9 +1,10 @@ .pdf by default) is added automatically to the target -if it is not already present. Example: +if it is not already present. +&b-PDF; is a single-source builder. +Example: - + # builds from aaa.tex env.PDF(target = 'aaa.pdf', source = 'aaa.tex') # builds bbb.pdf from bbb.dvi env.PDF(target = 'bbb', source = 'bbb.dvi') - + diff --git a/SCons/Tool/pdflatex.xml b/SCons/Tool/pdflatex.xml index 883ddf4156..4f5173ac24 100644 --- a/SCons/Tool/pdflatex.xml +++ b/SCons/Tool/pdflatex.xml @@ -1,9 +1,10 @@ Automatic moc file generation from header files. You do not have to specify moc files explicitly, the tool does it for you. However, there are a few preconditions to do so: Your header file must have -the same filebase as your implementation file and must stay in the same +the same basename as your implementation file and must stay in the same directory. It must have one of the suffixes .h, .hpp, diff --git a/SCons/Tool/rmic.xml b/SCons/Tool/rmic.xml index 28be1f6527..792c5f5301 100644 --- a/SCons/Tool/rmic.xml +++ b/SCons/Tool/rmic.xml @@ -1,11 +1,10 @@ #): @@ -216,7 +217,7 @@ env = Environment(SWIGPATH='#/include') -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: diff --git a/SCons/Tool/tar.xml b/SCons/Tool/tar.xml index 7f1011f800..98bfbd3cc2 100644 --- a/SCons/Tool/tar.xml +++ b/SCons/Tool/tar.xml @@ -1,9 +1,10 @@ -By default the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; +By default, the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; Examples: @@ -81,7 +82,7 @@ env.Textfile(target='bar', source=['lalala', 'tanteratei'], LINESEPARATOR='|*') # nested lists are flattened automatically env.Textfile(target='blob', source=['lalala', ['Goethe', 42, 'Schiller'], 'tanteratei']) -# files may be used as input by wraping them in File() +# files may be used as input by wrapping them in File() env.Textfile( target='concat', # concatenate files with a marker between source=[File('concat1'), File('concat2')], @@ -130,7 +131,7 @@ are flattened. See also &b-link-Textfile;. -By default the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; +By default, the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; Examples: @@ -153,7 +154,7 @@ it may be either a Python dictionary or a sequence of If it is a dictionary it is converted into a list of tuples with unspecified order, so if one key is a prefix of another key -or if one substitution could be further expanded by another subsitition, +or if one substitution could be further expanded by another substitution, it is unpredictable whether the expansion will occur. @@ -184,7 +185,7 @@ env.Substfile('foo.in', SUBST_DICT=bad_foo) good_foo = [('$foobar', '$foobar'), ('$foo', '$foo')] env.Substfile('foo.in', SUBST_DICT=good_foo) -# UNPREDICTABLE - one substitution could be futher expanded +# UNPREDICTABLE - one substitution could be further expanded bad_bar = {'@bar@': '@soap@', '@soap@': 'lye'} env.Substfile('bar.in', SUBST_DICT=bad_bar) diff --git a/SCons/Tool/tlib.xml b/SCons/Tool/tlib.xml index 8b1ea89df8..1fb34ef38c 100644 --- a/SCons/Tool/tlib.xml +++ b/SCons/Tool/tlib.xml @@ -1,9 +1,10 @@ -Sets construction variables for the Borlan -tib library archiver. +Sets construction variables for the Borland +tlib library archiver. diff --git a/SCons/Tool/xgettext.xml b/SCons/Tool/xgettext.xml index 78afade846..940e24a45f 100644 --- a/SCons/Tool/xgettext.xml +++ b/SCons/Tool/xgettext.xml @@ -1,9 +1,10 @@ -This scons tool is a part of scons &t-link-gettext; toolset. It provides -scons interface to xgettext(1) -program, which extracts internationalized messages from source code. The tool -provides &b-POTUpdate; builder to make PO -Template files. +This tool is a part of the &t-link-gettext; toolset. It provides +&SCons; an interface to the xgettext(1) +program, which extracts internationalized messages from source code. +The tool sets up the &b-POTUpdate; builder to make PO +Template files. @@ -57,17 +58,20 @@ provides &b-POTUpdate; builder to make PO -The builder belongs to &t-link-xgettext; tool. The builder updates target -POT file if exists or creates one if it doesn't. The node is -not built by default (i.e. it is Ignored from -'.'), but only on demand (i.e. when given -POT file is required or when special alias is invoked). This -builder adds its targe node (messages.pot, say) to a +The builder is set up by the &t-link-xgettext; tool, +part of the &t-link-gettext; toolset. +The builder updates the target +POT file if exists or creates it if it doesn't. +The target node is not +selected for building by default (e.g. scons .), +but only on demand (i.e. when the given +POT file is required or when special alias is invoked). +This builder adds its target node (messages.pot, say) to a special alias (pot-update by default, see &cv-link-POTUPDATE_ALIAS;) so you can update/create them easily with -scons pot-update. The file is not written until there is no +scons pot-update. The file is not written until there is no real change in internationalized messages (or in comments that enter -POT file). +POT file). @@ -85,86 +89,87 @@ not. Let's create po/ directory and place following SConstruct script there: - - # SConstruct in 'po/' subdir - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(['foo'], ['../a.cpp', '../b.cpp']) - env.POTUpdate(['bar'], ['../c.cpp', '../d.cpp']) - + +# SConstruct in 'po/' subdir +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(['foo'], ['../a.cpp', '../b.cpp']) +env.POTUpdate(['bar'], ['../c.cpp', '../d.cpp']) + Then invoke scons few times: - - user@host:$ scons # Does not create foo.pot nor bar.pot - user@host:$ scons foo.pot # Updates or creates foo.pot - user@host:$ scons pot-update # Updates or creates foo.pot and bar.pot - user@host:$ scons -c # Does not clean foo.pot nor bar.pot. - + +$ scons # Does not create foo.pot nor bar.pot +$ scons foo.pot # Updates or creates foo.pot +$ scons pot-update # Updates or creates foo.pot and bar.pot +$ scons -c # Does not clean foo.pot nor bar.pot. + the results shall be as the comments above say. Example 2. -The &b-POTUpdate; builder may be used with no target specified, in which -case default target messages.pot will be used. The -default target may also be overridden by setting &cv-link-POTDOMAIN; construction -variable or providing it as an override to &b-POTUpdate; builder: - - - # SConstruct script - env = Environment( tools = ['default', 'xgettext'] ) - env['POTDOMAIN'] = "foo" - env.POTUpdate(source = ["a.cpp", "b.cpp"]) # Creates foo.pot ... - env.POTUpdate(POTDOMAIN = "bar", source = ["c.cpp", "d.cpp"]) # and bar.pot - +The target argument can be omitted, in which +case the default target name messages.pot is used. +The target may also be overridden by setting the &cv-link-POTDOMAIN; +&consvar; or providing it as an override to the &b-POTUpdate; builder: + + +# SConstruct script +env = Environment(tools=['default', 'xgettext']) +env['POTDOMAIN'] = "foo" +env.POTUpdate(source=["a.cpp", "b.cpp"]) # Creates foo.pot ... +env.POTUpdate(POTDOMAIN="bar", source=["c.cpp", "d.cpp"]) # and bar.pot + Example 3. -The sources may be specified within separate file, for example +The source parameter may also be omitted, +if it is specified in a separate file, for example POTFILES.in: - - # POTFILES.in in 'po/' subdirectory - ../a.cpp - ../b.cpp - # end of file - + +# POTFILES.in in 'po/' subdirectory +../a.cpp +../b.cpp +# end of file + The name of the file (POTFILES.in) containing the list of sources is provided via &cv-link-XGETTEXTFROM;: - - # SConstruct file in 'po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in') - + +# SConstruct file in 'po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in') + Example 4. -You may use &cv-link-XGETTEXTPATH; to define source search path. Assume, for -example, that you have files a.cpp, +You can use &cv-link-XGETTEXTPATH; to define the source search path. +Assume, for example, that you have files a.cpp, b.cpp, po/SConstruct, -po/POTFILES.in. Then your POT-related -files could look as below: - - - # POTFILES.in in 'po/' subdirectory - a.cpp - b.cpp - # end of file - +po/POTFILES.in. +Then your POT-related files could look like this: + + +# POTFILES.in in 'po/' subdirectory +a.cpp +b.cpp +# end of file + - - # SConstruct file in 'po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH='../') - + +# SConstruct file in 'po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH='../') + Example 5. -Multiple search directories may be defined within a list, i.e. -XGETTEXTPATH = ['dir1', 'dir2', ...]. The order in the list +Multiple search directories may be defined as a list, i.e. +XGETTEXTPATH=['dir1', 'dir2', ...]. The order in the list determines the search order of source files. The path to the first file found is used. @@ -172,54 +177,53 @@ is used. Let's create 0/1/po/SConstruct script: - - # SConstruct file in '0/1/po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../', '../../']) - + +# SConstruct file in '0/1/po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../', '../../']) + and 0/1/po/POTFILES.in: - - # POTFILES.in in '0/1/po/' subdirectory - a.cpp - # end of file - + +# POTFILES.in in '0/1/po/' subdirectory +a.cpp +# end of file + Write two *.cpp files, the first one is 0/a.cpp: - - /* 0/a.cpp */ - gettext("Hello from ../../a.cpp") - + +/* 0/a.cpp */ +gettext("Hello from ../../a.cpp") + and the second is 0/1/a.cpp: - - /* 0/1/a.cpp */ - gettext("Hello from ../a.cpp") - + +/* 0/1/a.cpp */ +gettext("Hello from ../a.cpp") + then run scons. You'll obtain 0/1/po/messages.pot with the message "Hello from ../a.cpp". When you reverse order in $XGETTEXTFOM, i.e. when you write SConscript as - - # SConstruct file in '0/1/po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../../', '../']) - + +# SConstruct file in '0/1/po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../../', '../']) + then the messages.pot will contain -msgid "Hello from ../../a.cpp" line and not +msgid "Hello from ../../a.cpp" line and not msgid "Hello from ../a.cpp". - @@ -228,7 +232,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -238,7 +242,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -248,7 +252,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -257,7 +261,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -267,7 +271,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -276,7 +280,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -289,7 +293,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + @@ -302,17 +306,17 @@ See also &t-link-xgettext; tool and &b-link-POTUpdate; builder. - + This flag is used to add single search path to -xgettext(1)'s commandline (default: +xgettext(1)'s command line (default: '-D'). - + @@ -320,17 +324,17 @@ This flag is used to add single search path to - + This flag is used to add single &cv-link-XGETTEXTFROM; file to -xgettext(1)'s commandline (default: +xgettext(1)'s command line (default: '-f'). - + @@ -338,7 +342,7 @@ This flag is used to add single &cv-link-XGETTEXTFROM; file to - + @@ -347,16 +351,16 @@ form source and target (default: '${TARGET.filebase}'). - + -Internal "macro". Genrates list of -D<dir> flags +Internal "macro". Generates list of -D<dir> flags from the &cv-link-XGETTEXTPATH; list. - + @@ -366,8 +370,4 @@ from &cv-link-XGETTEXTFROM;. - - diff --git a/SCons/Tool/yacc.py b/SCons/Tool/yacc.py index 7a4ddfc54e..bfd82f6053 100644 --- a/SCons/Tool/yacc.py +++ b/SCons/Tool/yacc.py @@ -34,9 +34,10 @@ selection method. """ +from __future__ import annotations + import os.path import sys -from typing import Optional import SCons.Defaults import SCons.Tool @@ -68,7 +69,7 @@ def _yaccEmitter(target, source, env, ysuf, hsuf) -> tuple: # If -d is specified on the command line, yacc will emit a .h # or .hpp file with the same base name as the .c or .cpp output file. - # if '-d' in flags: + # if '-d' in flags: # or bison options -H, --header, --defines (obsolete) if "-d" in flags or "-H" in flags or "--header" in flags or "--defines" in flags: target.append(targetBase + env.subst(hsuf, target=target, source=source)) @@ -76,7 +77,7 @@ def _yaccEmitter(target, source, env, ysuf, hsuf) -> tuple: # If -g is specified on the command line, yacc will emit a graph # file with the same base name as the .c or .cpp output file. # TODO: should this be handled like -v? i.e. a side effect, not target - # if "-g" in flags: + # if "-g" in flags: # or bison option --graph if "-g" in flags or "--graph" in flags: target.append(targetBase + env.subst("$YACC_GRAPH_FILE_SUFFIX")) @@ -134,7 +135,7 @@ def yyEmitter(target, source, env) -> tuple: return _yaccEmitter(target, source, env, ['.yy'], '$YACCHXXFILESUFFIX') -def get_yacc_path(env, append_paths: bool=False) -> Optional[str]: +def get_yacc_path(env, append_paths: bool=False) -> str | None: """ Returns the path to the yacc tool, searching several possible names. @@ -200,7 +201,7 @@ def generate(env) -> None: env['_YACC_GRAPH'] = '${YACC_GRAPH_FILE and "--graph=" + str(YACC_GRAPH_FILE)}' -def exists(env) -> Optional[str]: +def exists(env) -> str | None: if 'YACC' in env: return env.Detect(env['YACC']) diff --git a/SCons/Tool/yacc.xml b/SCons/Tool/yacc.xml index 82725dbade..89f45ba4e4 100644 --- a/SCons/Tool/yacc.xml +++ b/SCons/Tool/yacc.xml @@ -1,31 +1,10 @@ .gv. -Changed in version 4.X.Y: deprecated. The default value +Changed in version 4.6.0: deprecated. The default value changed from .vcg (&bison; stopped generating .vcg output with version 2.4, in 2006). @@ -261,7 +240,7 @@ Various yacc tools have emitted various formats at different times. Set this to match what your parser generator produces. -New in version 4.X.Y. +New in version 4.6.0. diff --git a/SCons/Tool/zip.xml b/SCons/Tool/zip.xml index f9f01d9907..9458a12e70 100644 --- a/SCons/Tool/zip.xml +++ b/SCons/Tool/zip.xml @@ -1,9 +1,10 @@ An optional zip root directory (default empty). The filenames stored in the zip file will be relative to this directory, if given. -Otherwise the filenames are relative to the current directory of the +Otherwise, the filenames are relative to the current directory of the command. For instance: diff --git a/SCons/Util/UtilTests.py b/SCons/Util/UtilTests.py index ff32bab8d2..88ff923a6a 100644 --- a/SCons/Util/UtilTests.py +++ b/SCons/Util/UtilTests.py @@ -21,16 +21,19 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from __future__ import annotations + import functools import hashlib import io import os +import subprocess import sys import unittest import unittest.mock import warnings from collections import UserDict, UserList, UserString, namedtuple -from typing import Union +from typing import Callable import TestCmd @@ -72,7 +75,10 @@ to_String, to_bytes, to_str, + wait_for_process_to_die, + _wait_for_process_to_die_non_psutil, ) +from SCons.Util.envs import is_valid_construction_var from SCons.Util.hashes import ( _attempt_init_of_python_3_9_hash_object, _attempt_get_hash_function, @@ -80,6 +86,13 @@ _set_allowed_viable_default_hashes, ) +try: + import psutil + has_psutil = True +except ImportError: + has_psutil = False + + # These Util classes have no unit tests. Some don't make sense to test? # DisplayEngine, Delegate, MethodWrapper, UniqueList, Unbuffered, Null, NullSeq @@ -532,8 +545,8 @@ def test_get_native_path(self) -> None: def test_PrependPath(self) -> None: """Test prepending to a path""" - p1: Union[list, str] = r'C:\dir\num\one;C:\dir\num\two' - p2: Union[list, str] = r'C:\mydir\num\one;C:\mydir\num\two' + p1: list | str = r'C:\dir\num\one;C:\dir\num\two' + p2: list | str = r'C:\mydir\num\one;C:\mydir\num\two' # have to include the pathsep here so that the test will work on UNIX too. p1 = PrependPath(p1, r'C:\dir\num\two', sep=';') p1 = PrependPath(p1, r'C:\dir\num\three', sep=';') @@ -544,14 +557,14 @@ def test_PrependPath(self) -> None: assert p2 == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two', p2 # check (only) first one is kept if there are dupes in new - p3: Union[list, str] = r'C:\dir\num\one' + p3: list | str = r'C:\dir\num\one' p3 = PrependPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';') assert p3 == r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\one', p3 def test_AppendPath(self) -> None: """Test appending to a path.""" - p1: Union[list, str] = r'C:\dir\num\one;C:\dir\num\two' - p2: Union[list, str] = r'C:\mydir\num\one;C:\mydir\num\two' + p1: list | str = r'C:\dir\num\one;C:\dir\num\two' + p2: list | str = r'C:\mydir\num\one;C:\mydir\num\two' # have to include the pathsep here so that the test will work on UNIX too. p1 = AppendPath(p1, r'C:\dir\num\two', sep=';') p1 = AppendPath(p1, r'C:\dir\num\three', sep=';') @@ -562,13 +575,13 @@ def test_AppendPath(self) -> None: assert p2 == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one', p2 # check (only) last one is kept if there are dupes in new - p3: Union[list, str] = r'C:\dir\num\one' + p3: list | str = r'C:\dir\num\one' p3 = AppendPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';') assert p3 == r'C:\dir\num\one;C:\dir\num\three;C:\dir\num\two', p3 def test_PrependPathPreserveOld(self) -> None: """Test prepending to a path while preserving old paths""" - p1: Union[list, str] = r'C:\dir\num\one;C:\dir\num\two' + p1: list | str = r'C:\dir\num\one;C:\dir\num\two' # have to include the pathsep here so that the test will work on UNIX too. p1 = PrependPath(p1, r'C:\dir\num\two', sep=';', delete_existing=False) p1 = PrependPath(p1, r'C:\dir\num\three', sep=';') @@ -576,7 +589,7 @@ def test_PrependPathPreserveOld(self) -> None: def test_AppendPathPreserveOld(self) -> None: """Test appending to a path while preserving old paths""" - p1: Union[list, str] = r'C:\dir\num\one;C:\dir\num\two' + p1: list | str = r'C:\dir\num\one;C:\dir\num\two' # have to include the pathsep here so that the test will work on UNIX too. p1 = AppendPath(p1, r'C:\dir\num\one', sep=';', delete_existing=False) p1 = AppendPath(p1, r'C:\dir\num\three', sep=';') @@ -845,6 +858,21 @@ def test_intern(self) -> None: s4 = silent_intern("spam") assert id(s1) == id(s4) + @unittest.skipUnless(has_psutil, "requires psutil") + def test_wait_for_process_to_die_success_psutil(self) -> None: + self._test_wait_for_process(wait_for_process_to_die) + + def test_wait_for_process_to_die_success_non_psutil(self) -> None: + self._test_wait_for_process(_wait_for_process_to_die_non_psutil) + + def _test_wait_for_process( + self, wait_fn: Callable[[int], None] + ) -> None: + cmd = [sys.executable, "-c", ""] + p = subprocess.Popen(cmd) + p.wait() + wait_fn(p.pid) + class HashTestCase(unittest.TestCase): @@ -1206,6 +1234,41 @@ def test_default(self) -> None: assert var is False, 'var should be False, not %s' % repr(var) +class EnvironmentVariableTestCase(unittest.TestCase): + + def test_is_valid_construction_var(self) -> None: + """Testing is_valid_construction_var()""" + r = is_valid_construction_var("_a") + assert r, r + r = is_valid_construction_var("z_") + assert r, r + r = is_valid_construction_var("X_") + assert r, r + r = is_valid_construction_var("2a") + assert not r, r + r = is_valid_construction_var("a2_") + assert r, r + r = is_valid_construction_var("/") + assert not r, r + r = is_valid_construction_var("_/") + assert not r, r + r = is_valid_construction_var("a/") + assert not r, r + r = is_valid_construction_var(".b") + assert not r, r + r = is_valid_construction_var("_.b") + assert not r, r + r = is_valid_construction_var("b1._") + assert not r, r + r = is_valid_construction_var("-b") + assert not r, r + r = is_valid_construction_var("_-b") + assert not r, r + r = is_valid_construction_var("b1-_") + assert not r, r + + + if __name__ == "__main__": unittest.main() diff --git a/SCons/Util/__init__.py b/SCons/Util/__init__.py index 95c1b9978d..ff693c8d7c 100644 --- a/SCons/Util/__init__.py +++ b/SCons/Util/__init__.py @@ -49,6 +49,8 @@ # ) # (issue filed on this upstream, for now just be aware) +from __future__ import annotations + import copy import hashlib import logging @@ -59,7 +61,7 @@ from collections import UserDict, UserList, deque from contextlib import suppress from types import MethodType, FunctionType -from typing import Optional, Union, Any, List +from typing import Any from logging import Formatter # Util split into a package. Make sure things that used to work @@ -107,6 +109,7 @@ AppendPath, AddPathIfNotExists, AddMethod, + is_valid_construction_var, ) from .filelock import FileLock, SConsLockFailure @@ -202,11 +205,11 @@ def __str__(self) -> str: def __iter__(self): return iter(self.data) - def __call__(self, *args, **kwargs) -> 'NodeList': + def __call__(self, *args, **kwargs) -> NodeList: result = [x(*args, **kwargs) for x in self.data] return self.__class__(result) - def __getattr__(self, name) -> 'NodeList': + def __getattr__(self, name) -> NodeList: """Returns a NodeList of `name` from each member.""" result = [getattr(x, name) for x in self.data] return self.__class__(result) @@ -253,8 +256,8 @@ def render_tree( root, child_func, prune: bool = False, - margin: List[bool] = [False], - visited: Optional[dict] = None, + margin: list[bool] = [False], + visited: dict | None = None, ) -> str: """Render a tree of nodes into an ASCII tree view. @@ -322,8 +325,8 @@ def print_tree( child_func, prune: bool = False, showtags: int = 0, - margin: List[bool] = [False], - visited: Optional[dict] = None, + margin: list[bool] = [False], + visited: dict | None = None, lastChild: bool = False, singleLineDraw: bool = False, ) -> None: @@ -693,7 +696,7 @@ def RegOpenKeyEx(root, key): if sys.platform == 'win32': - def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]: + def WhereIs(file, path=None, pathext=None, reject=None) -> str | None: if path is None: try: path = os.environ['PATH'] @@ -730,7 +733,7 @@ def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]: elif os.name == 'os2': - def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]: + def WhereIs(file, path=None, pathext=None, reject=None) -> str | None: if path is None: try: path = os.environ['PATH'] @@ -762,7 +765,7 @@ def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]: else: - def WhereIs(file, path=None, pathext=None, reject=None) -> Optional[str]: + def WhereIs(file, path=None, pathext=None, reject=None) -> str | None: import stat # pylint: disable=import-outside-toplevel if path is None: @@ -1294,36 +1297,70 @@ def print_time(): return print_time -def wait_for_process_to_die(pid) -> None: +def wait_for_process_to_die(pid: int) -> None: """ - Wait for specified process to die, or alternatively kill it - NOTE: This function operates best with psutil pypi package + Wait for the specified process to die. + TODO: Add timeout which raises exception """ # wait for the process to fully killed try: import psutil # pylint: disable=import-outside-toplevel while True: + # TODO: this should use psutil.process_exists() or psutil.Process.wait() + # The psutil docs explicitly recommend against using process_iter()/pids() + # for checking the existence of a process. if pid not in [proc.pid for proc in psutil.process_iter()]: break time.sleep(0.1) except ImportError: # if psutil is not installed we can do this the hard way - while True: - if sys.platform == 'win32': - import ctypes # pylint: disable=import-outside-toplevel - PROCESS_QUERY_INFORMATION = 0x1000 - processHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid) - if processHandle == 0: - break - ctypes.windll.kernel32.CloseHandle(processHandle) - time.sleep(0.1) - else: - try: - os.kill(pid, 0) - except OSError: - break - time.sleep(0.1) + _wait_for_process_to_die_non_psutil(pid, timeout=-1.0) + + +def _wait_for_process_to_die_non_psutil(pid: int, timeout: float = 60.0) -> None: + start_time = time.time() + while True: + if not _is_process_alive(pid): + break + if timeout >= 0.0 and time.time() - start_time > timeout: + raise TimeoutError(f"timed out waiting for process {pid}") + time.sleep(0.1) + + +if sys.platform == 'win32': + def _is_process_alive(pid: int) -> bool: + import ctypes # pylint: disable=import-outside-toplevel + PROCESS_QUERY_INFORMATION = 0x1000 + STILL_ACTIVE = 259 + + processHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid) + if processHandle == 0: + return False + + # OpenProcess() may successfully return a handle even for terminated + # processes when something else in the system is still holding a + # reference to their handle. Call GetExitCodeProcess() to check if the + # process has already exited. + try: + exit_code = ctypes.c_ulong() + success = ctypes.windll.kernel32.GetExitCodeProcess( + processHandle, ctypes.byref(exit_code)) + if success: + return exit_code.value == STILL_ACTIVE + finally: + ctypes.windll.kernel32.CloseHandle(processHandle) + + return True + +else: + def _is_process_alive(pid: int) -> bool: + try: + os.kill(pid, 0) + return True + except OSError: + return False + # From: https://stackoverflow.com/questions/1741972/how-to-use-different-formatters-with-the-same-logging-handler-in-python class DispatchingFormatter(Formatter): diff --git a/SCons/Util/envs.py b/SCons/Util/envs.py index db4d65ab94..9c97da6e9e 100644 --- a/SCons/Util/envs.py +++ b/SCons/Util/envs.py @@ -9,9 +9,12 @@ that don't need the specifics of the Environment class. """ +from __future__ import annotations + +import re import os from types import MethodType, FunctionType -from typing import Union, Callable, Optional, Any +from typing import Callable, Any from .sctypes import is_List, is_Tuple, is_String @@ -21,8 +24,8 @@ def PrependPath( newpath, sep=os.pathsep, delete_existing: bool = True, - canonicalize: Optional[Callable] = None, -) -> Union[list, str]: + canonicalize: Callable | None = None, +) -> list | str: """Prepend *newpath* path elements to *oldpath*. Will only add any particular path once (leaving the first one it @@ -111,8 +114,8 @@ def AppendPath( newpath, sep=os.pathsep, delete_existing: bool = True, - canonicalize: Optional[Callable] = None, -) -> Union[list, str]: + canonicalize: Callable | None = None, +) -> list | str: """Append *newpath* path elements to *oldpath*. Will only add any particular path once (leaving the last one it @@ -238,7 +241,7 @@ class MethodWrapper: a new underlying object being copied (without which we wouldn't need to save that info). """ - def __init__(self, obj: Any, method: Callable, name: Optional[str] = None) -> None: + def __init__(self, obj: Any, method: Callable, name: str | None = None) -> None: if name is None: name = method.__name__ self.object = obj @@ -274,7 +277,7 @@ def clone(self, new_object): # is not needed, the remaining bit is now used inline in AddMethod. -def AddMethod(obj, function: Callable, name: Optional[str] = None) -> None: +def AddMethod(obj, function: Callable, name: str | None = None) -> None: """Add a method to an object. Adds *function* to *obj* if *obj* is a class object. @@ -313,7 +316,7 @@ def AddMethod(obj, function: Callable, name: Optional[str] = None) -> None: function.__code__, function.__globals__, name, function.__defaults__ ) - method: Union[MethodType, MethodWrapper, Callable] + method: MethodType | MethodWrapper | Callable if hasattr(obj, '__class__') and obj.__class__ is not type: # obj is an instance, so it gets a bound method. @@ -329,6 +332,15 @@ def AddMethod(obj, function: Callable, name: Optional[str] = None) -> None: setattr(obj, name, method) +# This routine is used to validate that a construction var name can be used +# as a Python identifier, which we require. However, Python 3 introduced an +# isidentifier() string method so there's really not any need for it now. +_is_valid_var_re = re.compile(r'[_a-zA-Z]\w*$') + +def is_valid_construction_var(varstr: str) -> bool: + """Return True if *varstr* is a legitimate name of a construction variable.""" + return bool(_is_valid_var_re.match(varstr)) + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/SCons/Util/filelock.py b/SCons/Util/filelock.py index 8ebf3889fe..730f48607b 100644 --- a/SCons/Util/filelock.py +++ b/SCons/Util/filelock.py @@ -30,9 +30,10 @@ # The lock attributes could probably be made opaque. Showed one visible # in the example above, but not sure the benefit of that. +from __future__ import annotations + import os import time -from typing import Optional class SConsLockFailure(Exception): @@ -75,8 +76,8 @@ class FileLock: def __init__( self, file: str, - timeout: Optional[int] = None, - delay: Optional[float] = 0.05, + timeout: int | None = None, + delay: float | None = 0.05, writer: bool = False, ) -> None: if timeout is not None and delay is None: @@ -90,7 +91,7 @@ def __init__( # Our simple first guess is just put it where the file is. self.file = file self.lockfile = f"{file}.lock" - self.lock: Optional[int] = None + self.lock: int | None = None self.timeout = 999999 if timeout == 0 else timeout self.delay = 0.0 if delay is None else delay self.writer = writer @@ -128,7 +129,7 @@ def release_lock(self) -> None: os.unlink(self.lockfile) self.lock = None - def __enter__(self) -> "FileLock": + def __enter__(self) -> FileLock: """Context manager entry: acquire lock if not holding.""" if not self.lock: self.acquire_lock() diff --git a/SCons/Util/hashes.py b/SCons/Util/hashes.py index 566897abbe..016354c6e5 100644 --- a/SCons/Util/hashes.py +++ b/SCons/Util/hashes.py @@ -8,10 +8,11 @@ Routines for working with content and signature hashes. """ +from __future__ import annotations + import functools import hashlib import sys -from typing import Optional, Union from .sctypes import to_bytes diff --git a/SCons/Util/sctypes.py b/SCons/Util/sctypes.py index bcbefb6c80..b95e395c80 100644 --- a/SCons/Util/sctypes.py +++ b/SCons/Util/sctypes.py @@ -6,12 +6,13 @@ Routines which check types and do type conversions. """ +from __future__ import annotations import codecs import os import pprint import re -from typing import Optional +import sys from collections import UserDict, UserList, UserString, deque from collections.abc import MappingView, Iterable @@ -50,38 +51,67 @@ # Empirically, it is faster to check explicitly for str than for basestring. BaseStringTypes = str +# Later Python versions allow us to explicitly apply type hints based off the +# return value similar to isinstance(), albeit not as precise. +if sys.version_info >= (3, 13): + from typing import TypeAlias, TypeIs + + DictTypeRet: TypeAlias = TypeIs[dict | UserDict] + ListTypeRet: TypeAlias = TypeIs[list | UserList | deque] + SequenceTypeRet: TypeAlias = TypeIs[list | tuple | deque | UserList | MappingView] + TupleTypeRet: TypeAlias = TypeIs[tuple] + StringTypeRet: TypeAlias = TypeIs[str | UserString] +elif sys.version_info >= (3, 10): + from typing import TypeAlias, TypeGuard + + DictTypeRet: TypeAlias = TypeGuard[dict | UserDict] + ListTypeRet: TypeAlias = TypeGuard[list | UserList | deque] + SequenceTypeRet: TypeAlias = TypeGuard[list | tuple | deque | UserList | MappingView] + TupleTypeRet: TypeAlias = TypeGuard[tuple] + StringTypeRet: TypeAlias = TypeGuard[str | UserString] +else: + # Because we have neither `TypeAlias` class nor `type` keyword pre-3.10, + # the boolean fallback type has to be wrapped in the legacy `Union` class. + from typing import Union + + DictTypeRet = Union[bool, bool] + ListTypeRet = Union[bool, bool] + SequenceTypeRet = Union[bool, bool] + TupleTypeRet = Union[bool, bool] + StringTypeRet = Union[bool, bool] + def is_Dict( # pylint: disable=redefined-outer-name,redefined-builtin obj, isinstance=isinstance, DictTypes=DictTypes -) -> bool: +) -> DictTypeRet: """Check if object is a dict.""" return isinstance(obj, DictTypes) def is_List( # pylint: disable=redefined-outer-name,redefined-builtin obj, isinstance=isinstance, ListTypes=ListTypes -) -> bool: +) -> ListTypeRet: """Check if object is a list.""" return isinstance(obj, ListTypes) def is_Sequence( # pylint: disable=redefined-outer-name,redefined-builtin obj, isinstance=isinstance, SequenceTypes=SequenceTypes -) -> bool: +) -> SequenceTypeRet: """Check if object is a sequence.""" return isinstance(obj, SequenceTypes) def is_Tuple( # pylint: disable=redefined-builtin obj, isinstance=isinstance, tuple=tuple -) -> bool: +) -> TupleTypeRet: """Check if object is a tuple.""" return isinstance(obj, tuple) def is_String( # pylint: disable=redefined-outer-name,redefined-builtin obj, isinstance=isinstance, StringTypes=StringTypes -) -> bool: +) -> StringTypeRet: """Check if object is a string.""" return isinstance(obj, StringTypes) @@ -328,7 +358,7 @@ def get_os_env_bool(name: str, default: bool=False) -> bool: _get_env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$') -def get_environment_var(varstr) -> Optional[str]: +def get_environment_var(varstr) -> str | None: """Return undecorated construction variable string. Determine if *varstr* looks like a reference diff --git a/SCons/Util/sctyping.py b/SCons/Util/sctyping.py deleted file mode 100644 index 5da5eb9fe7..0000000000 --- a/SCons/Util/sctyping.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: MIT -# -# Copyright The SCons Foundation - -"""Various SCons type aliases. - -For representing complex types across the entire repo without risking -circular dependencies, we take advantage of TYPE_CHECKING to import -modules in an tool-only environment. This allows us to introduce -hinting that resolves as expected in IDEs without clashing at runtime. - -For consistency, it's recommended to ALWAYS use these aliases in a -type-hinting context, even if the type is actually expected to be -resolved in a given file. -""" - -from typing import Union, TYPE_CHECKING - -if TYPE_CHECKING: - import SCons.Executor - - -# Because we don't have access to TypeAlias until 3.10, we have to utilize -# 'Union' for all aliases. As it expects at least two entries, anything that -# is only represented with a single type needs to list itself twice. -ExecutorType = Union["SCons.Executor.Executor", "SCons.Executor.Executor"] - - -# Local Variables: -# tab-width:4 -# indent-tabs-mode:nil -# End: -# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/SCons/Variables/BoolVariable.py b/SCons/Variables/BoolVariable.py index 4d998e4d9e..573a0244e7 100644 --- a/SCons/Variables/BoolVariable.py +++ b/SCons/Variables/BoolVariable.py @@ -32,17 +32,19 @@ ... """ -from typing import Tuple, Callable +from __future__ import annotations + +from typing import Callable import SCons.Errors __all__ = ['BoolVariable',] -TRUE_STRINGS = ('y', 'yes', 'true', 't', '1', 'on' , 'all') +TRUE_STRINGS = ('y', 'yes', 'true', 't', '1', 'on', 'all') FALSE_STRINGS = ('n', 'no', 'false', 'f', '0', 'off', 'none') -def _text2bool(val: str) -> bool: +def _text2bool(val: str | bool) -> bool: """Convert boolean-like string to boolean. If *val* looks like it expresses a bool-like value, based on @@ -54,6 +56,9 @@ def _text2bool(val: str) -> bool: Raises: ValueError: if *val* cannot be converted to boolean. """ + if isinstance(val, bool): + # mainly for the subst=False case: default might be a bool + return val lval = val.lower() if lval in TRUE_STRINGS: return True @@ -63,10 +68,10 @@ def _text2bool(val: str) -> bool: raise ValueError(f"Invalid value for boolean variable: {val!r}") -def _validator(key, val, env) -> None: +def _validator(key: str, val, env) -> None: """Validate that the value of *key* in *env* is a boolean. - Parmaeter *val* is not used in the check. + Parameter *val* is not used in the check. Usable as a validator function for SCons Variables. @@ -80,7 +85,7 @@ def _validator(key, val, env) -> None: raise SCons.Errors.UserError(msg) from None # lint: W0622: Redefining built-in 'help' (redefined-builtin) -def BoolVariable(key, help: str, default) -> Tuple[str, str, str, Callable, Callable]: +def BoolVariable(key, help: str, default) -> tuple[str, str, str, Callable, Callable]: """Return a tuple describing a boolean SCons Variable. The input parameters describe a boolean variable, using a string diff --git a/SCons/Variables/BoolVariableTests.py b/SCons/Variables/BoolVariableTests.py index 4c9b23e805..6d950fe3a8 100644 --- a/SCons/Variables/BoolVariableTests.py +++ b/SCons/Variables/BoolVariableTests.py @@ -43,7 +43,6 @@ def test_converter(self) -> None: """Test the BoolVariable converter""" opts = SCons.Variables.Variables() opts.Add(SCons.Variables.BoolVariable('test', 'test option help', False)) - o = opts.options[0] true_values = [ @@ -76,6 +75,17 @@ def test_converter(self) -> None: with self.assertRaises(ValueError): o.converter('x') + # Synthesize the case where the variable is created with subst=False: + # Variables code won't subst before calling the converter, + # and we might have pulled a bool from the option default. + with self.subTest(): + x = o.converter(True) + assert x, f"converter returned False for {t!r}" + with self.subTest(): + x = o.converter(False) + assert not x, f"converter returned False for {t!r}" + + def test_validator(self) -> None: """Test the BoolVariable validator""" opts = SCons.Variables.Variables() diff --git a/SCons/Variables/EnumVariable.py b/SCons/Variables/EnumVariable.py index d13e9a9fdb..a576af513e 100644 --- a/SCons/Variables/EnumVariable.py +++ b/SCons/Variables/EnumVariable.py @@ -43,7 +43,9 @@ ... """ -from typing import Callable, List, Optional, Tuple +from __future__ import annotations + +from typing import Callable import SCons.Errors @@ -69,33 +71,29 @@ def EnumVariable( key, help: str, default: str, - allowed_values: List[str], - map: Optional[dict] = None, + allowed_values: list[str], + map: dict | None = None, ignorecase: int = 0, -) -> Tuple[str, str, str, Callable, Callable]: +) -> tuple[str, str, str, Callable, Callable]: """Return a tuple describing an enumaration SCons Variable. - The input parameters describe a variable with only predefined values - allowed. The value of *ignorecase* defines the behavior of the + An Enum Variable is an abstraction that allows choosing one + value from a provided list of possibilities (*allowed_values*). + The value of *ignorecase* defines the behavior of the validator and converter: if ``0``, the validator/converter are case-sensitive; if ``1``, the validator/converter are case-insensitive; if ``2``, the validator/converter are case-insensitive and the converted value will always be lower-case. Arguments: - key: variable name, passed directly through to the return tuple. - default: default values, passed directly through to the return tuple. + key: the name of the variable. + default: default value, passed directly through to the return tuple. help: descriptive part of the help text, will have the allowed values automatically appended. - allowed_values: list of the allowed values for this variable. + allowed_values: the values for the choice. map: optional dictionary which may be used for converting the input value into canonical values (e.g. for aliases). ignorecase: defines the behavior of the validator and converter. - validator: callback function to test whether the value is in the - list of allowed values. - converter: callback function to convert input values according to - the given *map*-dictionary. Unmapped input values are returned - unchanged. Returns: A tuple including an appropriate converter and validator. diff --git a/SCons/Variables/EnumVariableTests.py b/SCons/Variables/EnumVariableTests.py index c4f32782d6..41cb396768 100644 --- a/SCons/Variables/EnumVariableTests.py +++ b/SCons/Variables/EnumVariableTests.py @@ -97,9 +97,9 @@ def test_converter(self) -> None: 'c' : 'three'}, ignorecase=2)) - o0 = opts.options[0] - o1 = opts.options[1] - o2 = opts.options[2] + opt0 = opts.options[0] + opt1 = opts.options[1] + opt2 = opts.options[2] table = { 'one' : ['one', 'one', 'one'], @@ -119,13 +119,13 @@ def test_converter(self) -> None: 'C' : ['C', 'three', 'three'], } - for k, l in table.items(): - x = o0.converter(k) - assert x == l[0], f"o0 got {x}, expected {l[0]}" - x = o1.converter(k) - assert x == l[1], f"o1 got {x}, expected {l[1]}" - x = o2.converter(k) - assert x == l[2], f"o2 got {x}, expected {l[2]}" + for k, expected in table.items(): + x = opt0.converter(k) + assert x == expected[0], f"opt0 got {x}, expected {expected[0]}" + x = opt1.converter(k) + assert x == expected[1], f"opt1 got {x}, expected {expected[1]}" + x = opt2.converter(k) + assert x == expected[2], f"opt2 got {x}, expected {expected[2]}" def test_validator(self) -> None: """Test the EnumVariable validator""" @@ -149,9 +149,9 @@ def test_validator(self) -> None: 'c' : 'three'}, ignorecase=2)) - o0 = opts.options[0] - o1 = opts.options[1] - o2 = opts.options[2] + opt0 = opts.options[0] + opt1 = opts.options[1] + opt2 = opts.options[2] def valid(o, v) -> None: o.validator('X', v, {}) @@ -159,7 +159,7 @@ def valid(o, v) -> None: def invalid(o, v) -> None: with self.assertRaises( SCons.Errors.UserError, - msg=f"did not catch expected UserError for o = {o.key}, v = {v}", + msg=f"did not catch expected UserError for o = {o.key!r}, v = {v!r}", ): o.validator('X', v, {}) table = { @@ -181,10 +181,26 @@ def invalid(o, v) -> None: 'no_v' : [invalid, invalid, invalid], } - for v, l in table.items(): - l[0](o0, v) - l[1](o1, v) - l[2](o2, v) + for v, expected in table.items(): + expected[0](opt0, v) + expected[1](opt1, v) + expected[2](opt2, v) + + # make sure there are no problems with space-containing entries + opts = SCons.Variables.Variables() + opts.Add( + SCons.Variables.EnumVariable( + 'test0', + help='test option help', + default='nospace', + allowed_values=['nospace', 'with space'], + map={}, + ignorecase=0, + ) + ) + opt = opts.options[0] + valid(opt, 'nospace') + valid(opt, 'with space') if __name__ == "__main__": diff --git a/SCons/Variables/ListVariable.py b/SCons/Variables/ListVariable.py index 0c9fa13289..90f563b56c 100644 --- a/SCons/Variables/ListVariable.py +++ b/SCons/Variables/ListVariable.py @@ -23,10 +23,11 @@ """Variable type for List Variables. -A list variable may given as 'all', 'none' or a list of names -separated by comma. After the variable has been processed, the variable -value holds either the named list elements, all list elements or no -list elements at all. +A list variable allows selecting one or more from a supplied set of +allowable values, as well as from an optional mapping of alternate names +(such as aliases and abbreviations) and the special names "all" and +"none". Specified values are converted during processing into values +only from the allowable values set. Usage example:: @@ -52,8 +53,11 @@ # Known Bug: This should behave like a Set-Type, but does not really, # since elements can occur twice. +from __future__ import annotations + import collections -from typing import Callable, List, Optional, Tuple, Union +import functools +from typing import Callable import SCons.Util @@ -63,17 +67,24 @@ class _ListVariable(collections.UserList): """Internal class holding the data for a List Variable. - The initializer accepts two arguments, the list of actual values - given, and the list of allowable values. Not normally instantiated - by hand, but rather by the ListVariable converter function. + This is normally not directly instantiated, rather the ListVariable + converter callback "converts" string input (or the default value + if none) into an instance and stores it. + + Args: + initlist: the list of actual values given. + allowedElems: the list of allowable values. """ - def __init__(self, initlist=None, allowedElems=None) -> None: + def __init__( + self, initlist: list | None = None, allowedElems: list | None = None + ) -> None: if initlist is None: initlist = [] if allowedElems is None: allowedElems = [] super().__init__([_f for _f in initlist if _f]) + # TODO: why sorted? don't we want to display in the order user gave? self.allowedElems = sorted(allowedElems) def __cmp__(self, other): @@ -106,7 +117,15 @@ def prepare_to_store(self): return str(self) def _converter(val, allowedElems, mapdict) -> _ListVariable: - """Convert list variables.""" + """Callback to convert list variables into a suitable form. + + The arguments *allowedElems* and *mapdict* are non-standard + for a :class:`Variables` converter: the lambda in the + :func:`ListVariable` function arranges for us to be called correctly. + + Incoming values "all" and "none" are recognized and converted + into their expanded form. + """ if val == 'none': val = [] elif val == 'all': @@ -114,18 +133,47 @@ def _converter(val, allowedElems, mapdict) -> _ListVariable: else: val = [_f for _f in val.split(',') if _f] val = [mapdict.get(v, v) for v in val] - notAllowed = [v for v in val if v not in allowedElems] - if notAllowed: - raise ValueError( - f"Invalid value(s) for option: {','.join(notAllowed)}" - ) return _ListVariable(val, allowedElems) -# def _validator(key, val, env) -> None: -# """ """ -# # TODO: write validator for list variable -# pass +def _validator(key, val, env) -> None: + """Callback to validate supplied value(s) for a ListVariable. + + Validation means "is *val* in the allowed list"? *val* has + been subject to substitution before the validator is called. The + converter created a :class:`_ListVariable` container which is stored + in *env* after it runs; this includes the allowable elements list. + Substitution makes a string made out of the values (only), + so we need to fish the allowed elements list out of the environment + to complete the validation. + + Note that since 18b45e456, whether ``subst`` has been + called is conditional on the value of the *subst* argument to + :meth:`~SCons.Variables.Variables.Add`, so we have to account for + possible different types of *val*. + + Raises: + UserError: if validation failed. + + .. versionadded:: 4.8.0 + ``_validator`` split off from :func:`_converter` with an additional + check for whether *val* has been substituted before the call. + """ + allowedElems = env[key].allowedElems + if isinstance(val, _ListVariable): # not substituted, use .data + notAllowed = [v for v in val.data if v not in allowedElems] + else: # presumably a string + notAllowed = [v for v in val.split() if v not in allowedElems] + if notAllowed: + # Converter only synthesized 'all' and 'none', they are never + # in the allowed list, so we need to add those to the error message + # (as is done for the help msg). + valid = ','.join(allowedElems + ['all', 'none']) + msg = ( + f"Invalid value(s) for variable {key!r}: {','.join(notAllowed)!r}. " + f"Valid values are: {valid}" + ) + raise SCons.Errors.UserError(msg) from None # lint: W0622: Redefining built-in 'help' (redefined-builtin) @@ -133,41 +181,54 @@ def _converter(val, allowedElems, mapdict) -> _ListVariable: def ListVariable( key, help: str, - default: Union[str, List[str]], - names: List[str], - map: Optional[dict] = None, -) -> Tuple[str, str, str, None, Callable]: + default: str | list[str], + names: list[str], + map: dict | None = None, + validator: Callable | None = None, +) -> tuple[str, str, str, Callable, Callable]: """Return a tuple describing a list variable. - The input parameters describe a list variable, where the values - can be one or more from *names* plus the special values ``all`` - and ``none``. + A List Variable is an abstraction that allows choosing one or more + values from a provided list of possibilities (*names*). The special terms + "all" and "none" are also provided to help make the selection. Arguments: key: the name of the list variable. help: the basic help message. Will have text appended indicating - the allowable values (not including any extra names from *map*). - default: the default value(s) for the list variable. Can be - given as string (possibly comma-separated), or as a list of strings. - ``all`` or ``none`` are allowed as *default*. - names: the allowable values. Must be a list of strings. + the allowed values (not including any extra names from *map*). + default: the default value(s) for the list variable. Can be given + as string (use commas to -separated multiple values), or as a list + of strings. "all" or "none" are allowed as *default*. + A must-specify list variable can be simulated by giving a value + that is not part of *names*, which will cause validation to fail + if the variable is not supplied in the input sources. + names: the values to choose from. Must be a list of strings. map: optional dictionary to map alternative names to the ones in - *names*, providing a form of alias. The converter will make - the replacement, names from *map* are not stored and will + *names*, providing a form of alias. The converter will perform + the replacement. Names from *map* are not stored, and will not appear in the help message. + validator: optional callback to validate supplied values. + The default validator is used if not specified. Returns: A tuple including the correct converter and validator. The result is usable as input to :meth:`~SCons.Variables.Variables.Add`. + + .. versionchanged:: 4.8.0 + The validation step was split from the converter to allow for + custom validators. The *validator* keyword argument was added. """ if map is None: map = {} + if validator is None: + validator = _validator names_str = f"allowed names: {' '.join(names)}" if SCons.Util.is_List(default): default = ','.join(default) help = '\n '.join( (help, '(all|none|comma-separated list of names)', names_str)) - return key, help, default, None, lambda val: _converter(val, names, map) + converter = functools.partial(_converter, allowedElems=names, mapdict=map) + return key, help, default, validator, converter # Local Variables: # tab-width:4 diff --git a/SCons/Variables/ListVariableTests.py b/SCons/Variables/ListVariableTests.py index 54aad051d1..ca5a7935b0 100644 --- a/SCons/Variables/ListVariableTests.py +++ b/SCons/Variables/ListVariableTests.py @@ -21,6 +21,8 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Test List Variables elements.""" + import copy import unittest @@ -38,7 +40,7 @@ def test_ListVariable(self) -> None: assert o.key == 'test', o.key assert o.help == 'test option help\n (all|none|comma-separated list of names)\n allowed names: one two three', repr(o.help) assert o.default == 'all', o.default - assert o.validator is None, o.validator + assert o.validator is not None, o.validator assert o.converter is not None, o.converter opts = SCons.Variables.Variables() @@ -50,12 +52,22 @@ def test_ListVariable(self) -> None: assert o.default == 'one,three' def test_converter(self) -> None: - """Test the ListVariable converter""" - opts = SCons.Variables.Variables() - opts.Add(SCons.Variables.ListVariable('test', 'test option help', 'all', - ['one', 'two', 'three'], - {'ONE':'one', 'TWO':'two'})) + """Test the ListVariable converter. + There is now a separate validator (for a long time validation was + in the converter), but it depends on the _ListVariable instance the + converter creates, so it's easier to test them in the same function. + """ + opts = SCons.Variables.Variables() + opts.Add( + SCons.Variables.ListVariable( + 'test', + 'test option help', + default='all', + names=['one', 'two', 'three'], + map={'ONE': 'one', 'TWO': 'two'}, + ) + ) o = opts.options[0] x = o.converter('all') @@ -101,8 +113,50 @@ def test_converter(self) -> None: x = o.converter('three,ONE,TWO') assert str(x) == 'all', x - with self.assertRaises(ValueError): - x = o.converter('no_match') + # invalid value should convert (no change) without error + x = o.converter('no_match') + assert str(x) == 'no_match', x + + # validator checks + + # first, the one we just set up + with self.assertRaises(SCons.Errors.UserError): + o.validator('test', 'no_match', {"test": x}) + + # now a new option, this time with a name w/ space in it (issue #4585) + opts.Add( + SCons.Variables.ListVariable( + 'test2', + help='test2 option help', + default='two', + names=['one', 'two', 'three', 'four space'], + ) + ) + o = opts.options[1] + + def test_valid(opt, seq): + """Validate a ListVariable value. + + Call the converter manually, since we need the _ListVariable + object to pass to the validator - this would normally be done + by the Variables.Update method. + """ + x = opt.converter(seq) + self.assertIsNone(opt.validator(opt.key, x, {opt.key: x})) + + with self.subTest(): + test_valid(o, 'one') + with self.subTest(): + test_valid(o, 'one,two,three') + with self.subTest(): + test_valid(o, 'all') + with self.subTest(): + test_valid(o, 'none') + with self.subTest(): + test_valid(o, 'four space') + with self.subTest(): + test_valid(o, 'one,four space') + def test_copy(self) -> None: """Test copying a ListVariable like an Environment would""" @@ -111,9 +165,8 @@ def test_copy(self) -> None: ['one', 'two', 'three'])) o = opts.options[0] - - l = o.converter('all') - n = l.__class__(copy.copy(l)) + res = o.converter('all') + _ = res.__class__(copy.copy(res)) if __name__ == "__main__": unittest.main() diff --git a/SCons/Variables/PackageVariable.py b/SCons/Variables/PackageVariable.py index fc281250c7..df20bd26dc 100644 --- a/SCons/Variables/PackageVariable.py +++ b/SCons/Variables/PackageVariable.py @@ -50,8 +50,11 @@ ... # build with x11 ... """ +from __future__ import annotations + import os -from typing import Callable, Optional, Tuple +import functools +from typing import Callable import SCons.Errors @@ -60,28 +63,43 @@ ENABLE_STRINGS = ('1', 'yes', 'true', 'on', 'enable', 'search') DISABLE_STRINGS = ('0', 'no', 'false', 'off', 'disable') -def _converter(val): - """Convert package variables. +def _converter(val: str | bool, default: str) -> str | bool: + """Convert a package variable. + + Returns *val* if it looks like a path string, and ``False`` if it + is a disabling string. If *val* is an enabling string, returns + *default* unless *default* is an enabling or disabling string, + in which case ignore *default* and return ``True``. - Returns True or False if one of the recognized truthy or falsy - values is seen, else return the value unchanged (expected to - be a path string). + .. versionchanged: 4.9.0 + Now returns the default in case of a truthy value, matching what the + public documentation always claimed, except if the default looks + like one of the true/false strings. """ - lval = val.lower() - if lval in ENABLE_STRINGS: - return True + if isinstance(val, bool): + # check for non-subst case, so we don't lower() a bool. + lval = str(val).lower() + else: + lval = val.lower() if lval in DISABLE_STRINGS: return False + if lval in ENABLE_STRINGS: + # Can't return the default if it is one of the enable/disable strings; + # test code expects a bool. + if default in ENABLE_STRINGS + DISABLE_STRINGS: + return True + else: + return default return val -def _validator(key, val, env, searchfunc) -> None: +def _validator(key: str, val, env, searchfunc) -> None: """Validate package variable for valid path. Checks that if a path is given as the value, that pathname actually exists. """ - # NOTE: searchfunc is currently undocumented and unsupported if env[key] is True: + # NOTE: searchfunc is not in the documentation. if searchfunc: env[key] = searchfunc(key, val) # TODO: need to check path, or be sure searchfunc raises. @@ -92,29 +110,24 @@ def _validator(key, val, env, searchfunc) -> None: # lint: W0622: Redefining built-in 'help' (redefined-builtin) def PackageVariable( - key: str, help: str, default, searchfunc: Optional[Callable] = None -) -> Tuple[str, str, str, Callable, Callable]: + key: str, help: str, default, searchfunc: Callable | None = None +) -> tuple[str, str, str, Callable, Callable]: """Return a tuple describing a package list SCons Variable. The input parameters describe a 'package list' variable. Returns a tuple with the correct converter and validator appended. The result is usable as input to :meth:`~SCons.Variables.Variables.Add`. - A 'package list' variable may either be a truthy string from + A 'package list' variable may be specified as a truthy string from :const:`ENABLE_STRINGS`, a falsy string from - :const:`DISABLE_STRINGS`, or a pathname string. + :const:`DISABLE_STRINGS`, or as a pathname string. This information is appended to *help* using only one string each for truthy/falsy. """ - # NB: searchfunc is currently undocumented and unsupported help = '\n '.join((help, f'( yes | no | /path/to/{key} )')) - return ( - key, - help, - default, - lambda k, v, e: _validator(k, v, e, searchfunc), - _converter, - ) + validator = functools.partial(_validator, searchfunc=searchfunc) + converter = functools.partial(_converter, default=default) + return key, help, default, validator, converter # Local Variables: # tab-width:4 diff --git a/SCons/Variables/PackageVariableTests.py b/SCons/Variables/PackageVariableTests.py index ed8ec30db4..8ef52433b6 100644 --- a/SCons/Variables/PackageVariableTests.py +++ b/SCons/Variables/PackageVariableTests.py @@ -82,6 +82,44 @@ def test_converter(self) -> None: x = o.converter(str(False)) assert not x, "converter returned a string when given str(False)" + # Synthesize the case where the variable is created with subst=False: + # Variables code won't subst before calling the converter, + # and we might have pulled a bool from the option default. + with self.subTest(): + x = o.converter(True) + assert x, f"converter returned False for {t!r}" + with self.subTest(): + x = o.converter(False) + assert not x, f"converter returned False for {t!r}" + + # When the variable is created with boolean string make sure the converter + # returns the correct result i.e. a bool or a passed path + opts = SCons.Variables.Variables() + opts.Add(SCons.Variables.PackageVariable('test', 'test build variable help', 'yes')) + o = opts.options[0] + + x = o.converter(str(True)) + assert not isinstance(x, str), "converter with default str(yes) returned a string when given str(True)" + assert x, "converter with default str(yes) returned False for str(True)" + x = o.converter(str(False)) + assert not isinstance(x, str), "converter with default str(yes) returned a string when given str(False)" + assert not x, "converter with default str(yes) returned True for str(False)" + x = o.converter('/explicit/path') + assert x == '/explicit/path', "converter with default str(yes) did not return path" + + opts = SCons.Variables.Variables() + opts.Add(SCons.Variables.PackageVariable('test', 'test build variable help', 'no')) + o = opts.options[0] + + x = o.converter(str(True)) + assert not isinstance(x, str), "converter with default str(no) returned a string when given str(True)" + assert x, "converter with default str(no) returned False for str(True)" + x = o.converter(str(False)) + assert not isinstance(x, str), "converter with default str(no) returned a string when given str(False)" + assert not x, "converter with default str(no) returned True for str(False)" + x = o.converter('/explicit/path') + assert x == '/explicit/path', "converter with default str(no) did not return path" + def test_validator(self) -> None: """Test the PackageVariable validator""" opts = SCons.Variables.Variables() @@ -101,7 +139,7 @@ def test_validator(self) -> None: o.validator('T', '/path', env) o.validator('X', exists, env) - with self.assertRaises(SCons.Errors.UserError) as cm: + with self.assertRaises(SCons.Errors.UserError): o.validator('X', does_not_exist, env) diff --git a/SCons/Variables/PathVariable.py b/SCons/Variables/PathVariable.py index 6ea4e6bc93..f840a95d26 100644 --- a/SCons/Variables/PathVariable.py +++ b/SCons/Variables/PathVariable.py @@ -71,10 +71,11 @@ ) """ +from __future__ import annotations import os import os.path -from typing import Callable, Optional, Tuple +from typing import Callable import SCons.Errors import SCons.Util @@ -93,12 +94,12 @@ class _PathVariableClass: """ @staticmethod - def PathAccept(key, val, env) -> None: + def PathAccept(key: str, val, env) -> None: """Validate path with no checking.""" return @staticmethod - def PathIsDir(key, val, env) -> None: + def PathIsDir(key: str, val, env) -> None: """Validate path is a directory.""" if os.path.isdir(val): return @@ -109,7 +110,7 @@ def PathIsDir(key, val, env) -> None: raise SCons.Errors.UserError(msg) @staticmethod - def PathIsDirCreate(key, val, env) -> None: + def PathIsDirCreate(key: str, val, env) -> None: """Validate path is a directory, creating if needed.""" if os.path.isdir(val): return @@ -123,7 +124,7 @@ def PathIsDirCreate(key, val, env) -> None: raise SCons.Errors.UserError(msg) from exc @staticmethod - def PathIsFile(key, val, env) -> None: + def PathIsFile(key: str, val, env) -> None: """Validate path is a file.""" if not os.path.isfile(val): if os.path.isdir(val): @@ -133,7 +134,7 @@ def PathIsFile(key, val, env) -> None: raise SCons.Errors.UserError(msg) @staticmethod - def PathExists(key, val, env) -> None: + def PathExists(key: str, val, env) -> None: """Validate path exists.""" if not os.path.exists(val): msg = f'Path for variable {key!r} does not exist: {val}' @@ -141,8 +142,8 @@ def PathExists(key, val, env) -> None: # lint: W0622: Redefining built-in 'help' (redefined-builtin) def __call__( - self, key, help: str, default, validator: Optional[Callable] = None - ) -> Tuple[str, str, str, Callable, None]: + self, key: str, help: str, default, validator: Callable | None = None + ) -> tuple[str, str, str, Callable, None]: """Return a tuple describing a path list SCons Variable. The input parameters describe a 'path list' variable. Returns @@ -161,7 +162,7 @@ def __call__( helpmsg = f'{help} ( /path/to/{key[0]} )' else: helpmsg = f'{help} ( /path/to/{key} )' - return (key, helpmsg, default, validator, None) + return key, helpmsg, default, validator, None PathVariable = _PathVariableClass() diff --git a/SCons/Variables/PathVariableTests.py b/SCons/Variables/PathVariableTests.py index efc75f1878..b093053d74 100644 --- a/SCons/Variables/PathVariableTests.py +++ b/SCons/Variables/PathVariableTests.py @@ -28,7 +28,7 @@ import SCons.Variables import TestCmd -from TestCmd import IS_WINDOWS +from TestCmd import IS_WINDOWS, IS_ROOT class PathVariableTestCase(unittest.TestCase): def test_PathVariable(self) -> None: @@ -93,7 +93,7 @@ def test_PathIsDir(self): self.assertEqual(str(e), f"Directory path for variable 'X' does not exist: {dne}") def test_PathIsDirCreate(self): - """Test the PathIsDirCreate validator""" + """Test the PathIsDirCreate validator.""" opts = SCons.Variables.Variables() opts.Add(SCons.Variables.PathVariable('test', 'test build variable help', @@ -115,6 +115,27 @@ def test_PathIsDirCreate(self): e = cm.exception self.assertEqual(str(e), f"Path for variable 'X' is a file, not a directory: {f}") + + @unittest.skipIf(IS_ROOT, "Skip creating bad directory if running as root.") + def test_PathIsDirCreate_bad_dir(self): + """Test the PathIsDirCreate validator with an uncreatable dir. + + Split from :meth:`test_PathIsDirCreate` to be able to skip on root. + We want to be able to skip only this one testcase and run the rest. + """ + opts = SCons.Variables.Variables() + opts.Add( + SCons.Variables.PathVariable( + 'test', + 'test build variable help', + '/default/path', + SCons.Variables.PathVariable.PathIsDirCreate, + ) + ) + + test = TestCmd.TestCmd(workdir='') + o = opts.options[0] + # pick a directory path that can't be mkdir'd if IS_WINDOWS: f = r'\\noserver\noshare\yyy\zzz' @@ -125,8 +146,9 @@ def test_PathIsDirCreate(self): e = cm.exception self.assertEqual(str(e), f"Path for variable 'X' could not be created: {f}") + def test_PathIsFile(self): - """Test the PathIsFile validator""" + """Test the PathIsFile validator.""" opts = SCons.Variables.Variables() opts.Add(SCons.Variables.PathVariable('test', 'test build variable help', @@ -196,7 +218,7 @@ class ValidatorError(Exception): pass def my_validator(key, val, env): - raise ValidatorError(f"my_validator() got called for {key!r}, {val}!") + raise ValidatorError(f"my_validator() got called for {key!r}, {val!r}!") opts = SCons.Variables.Variables() opts.Add(SCons.Variables.PathVariable('test2', @@ -207,7 +229,7 @@ def my_validator(key, val, env): with self.assertRaises(ValidatorError) as cm: o.validator('Y', 'value', {}) e = cm.exception - self.assertEqual(str(e), f"my_validator() got called for 'Y', value!") + self.assertEqual(str(e), "my_validator() got called for 'Y', 'value'!") if __name__ == "__main__": diff --git a/SCons/Variables/VariablesTests.py b/SCons/Variables/VariablesTests.py index 145bee31f3..c409db4022 100644 --- a/SCons/Variables/VariablesTests.py +++ b/SCons/Variables/VariablesTests.py @@ -29,6 +29,7 @@ import SCons.Subst import SCons.Warnings from SCons.Util import cmp +from SCons.Variables import * class Environment: @@ -150,6 +151,48 @@ def test_Update(self) -> None: opts.Update(env, {}) assert env['ANSWER'] == 54 + # Test that the value is not substituted if 'subst' is False + # and that it is if 'subst' is True. + def check_no_subst(key, value, env) -> None: + """Check that variable was not substituted before we get called.""" + assert value == "$ORIGIN", \ + f"Validator: '$ORIGIN' was substituted to {value!r}" + + def conv_no_subst(value) -> None: + """Check that variable was not substituted before we get called.""" + assert value == "$ORIGIN", \ + f"Converter: '$ORIGIN' was substituted to {value!r}" + return value + + def check_subst(key, value, env) -> None: + """Check that variable was substituted before we get called.""" + assert value == "Value", \ + f"Validator: '$SUB' was not substituted {value!r} instead of 'Value'" + + def conv_subst(value) -> None: + """Check that variable was not substituted before we get called.""" + assert value == "Value", \ + f"Converter: '$SUB' was substituted to {value!r} instead of 'Value'" + return value + + opts.Add('NOSUB', + help='Variable whose value will not be substituted', + default='$ORIGIN', + validator=check_no_subst, + converter=conv_no_subst, + subst=False) + opts.Add('SUB', + help='Variable whose value will be substituted', + default='$VAR', + validator=check_subst, + converter=conv_subst, + subst=True) + env = Environment() + env['VAR'] = "Value" + opts.Update(env) + assert env['NOSUB'] == "$ORIGIN", env['NOSUB'] + assert env['SUB'] == env['VAR'], env['SUB'] + # Test that a bad value from the file is used and # validation fails correctly. test = TestSCons.TestSCons() @@ -164,7 +207,6 @@ def test_Update(self) -> None: lambda x: int(x) + 12) env = Environment() - exc_caught = None with self.assertRaises(AssertionError): opts.Update(env) @@ -333,8 +375,10 @@ def test_Save(self) -> None: opts = SCons.Variables.Variables() def bool_converter(val): - if val in [1, 'y']: val = 1 - if val in [0, 'n']: val = 0 + if val in [1, 'y']: + val = 1 + if val in [0, 'n']: + val = 0 return val # test saving out empty file @@ -506,7 +550,7 @@ def my_format(env, opt, help, default, actual, aliases) -> str: check, lambda x: int(x) + 12) - opts.Add('B', + opts.Add(['B', 'BOPTION'], 'b - alpha test', "42", check, @@ -522,9 +566,9 @@ def my_format(env, opt, help, default, actual, aliases) -> str: opts.Update(env, {}) expect = """\ -ANSWER 42 54 THE answer to THE question ['ANSWER'] -B 42 54 b - alpha test ['B'] -A 42 54 a - alpha test ['A'] +ANSWER 42 54 THE answer to THE question [] +B 42 54 b - alpha test ['BOPTION'] +A 42 54 a - alpha test [] """ text = opts.GenerateHelpText(env) @@ -532,9 +576,9 @@ def my_format(env, opt, help, default, actual, aliases) -> str: self.assertEqual(expect, text) expectAlpha = """\ -A 42 54 a - alpha test ['A'] -ANSWER 42 54 THE answer to THE question ['ANSWER'] -B 42 54 b - alpha test ['B'] +A 42 54 a - alpha test [] +ANSWER 42 54 THE answer to THE question [] +B 42 54 b - alpha test ['BOPTION'] """ text = opts.GenerateHelpText(env, sort=cmp) with self.subTest(): @@ -610,41 +654,50 @@ def test_unknown(self) -> None: self.assertEqual('answer', env['ANSWER']) def test_AddOptionUpdatesUnknown(self) -> None: - """Test updating of the 'unknown' dict""" - opts = SCons.Variables.Variables() + """Test updating of the 'unknown' dict. - opts.Add('A', - 'A test variable', - "1") + Get one unknown from args and one from a variables file. + Add these later, making sure they no longer appear in unknowns + after the subsequent Update(). + While we're here, test the *defaulted* attribute. + """ + test = TestSCons.TestSCons() + var_file = test.workpath('vars.py') + test.write('vars.py', 'FROMFILE="added"') + opts = SCons.Variables.Variables(files=var_file) + opts.Add('A', 'A test variable', default="1") + opts.Add('B', 'Test variable B', default="1") args = { 'A' : 'a', 'ADDEDLATER' : 'notaddedyet', } - env = Environment() - opts.Update(env,args) + opts.Update(env, args) r = opts.UnknownVariables() with self.subTest(): - self.assertEqual({'ADDEDLATER': 'notaddedyet'}, r) + self.assertEqual('notaddedyet', r['ADDEDLATER']) + self.assertEqual('added', r['FROMFILE']) self.assertEqual('a', env['A']) + self.assertEqual(['B'], opts.defaulted) - opts.Add('ADDEDLATER', - 'An option not present initially', - "1") - + opts.Add('ADDEDLATER', 'An option not present initially', "1") + opts.Add('FROMFILE', 'An option from a file also absent', "1") args = { 'A' : 'a', 'ADDEDLATER' : 'added', } - opts.Update(env, args) r = opts.UnknownVariables() with self.subTest(): self.assertEqual(0, len(r)) + self.assertNotIn('ADDEDLATER', r) self.assertEqual('added', env['ADDEDLATER']) + self.assertNotIn('FROMFILE', r) + self.assertEqual('added', env['FROMFILE']) + self.assertEqual(['B'], opts.defaulted) def test_AddOptionWithAliasUpdatesUnknown(self) -> None: """Test updating of the 'unknown' dict (with aliases)""" @@ -680,6 +733,27 @@ def test_AddOptionWithAliasUpdatesUnknown(self) -> None: assert len(r) == 0, r assert env['ADDEDLATER'] == 'added', env['ADDEDLATER'] + def test_VariableTypesImportVisibility(self) -> None: + """Test that 'from SCons.Variables import *' will import all types of SCons defined Variables + """ + + try: + x = BoolVariable('test', 'test option help', False) + y = EnumVariable('test', 'test option help', 0, + ['one', 'two', 'three'], + {}) + z = ListVariable('test', 'test option help', 'all', + ['one', 'two', 'three']) + o = PackageVariable('test', 'test build variable help', '/default/path') + p = PathVariable('test', + 'test build variable help', + '/default/path') + except Exception as e: + self.fail(f"Could not import all known Variable types: {e}") + + + + if __name__ == "__main__": unittest.main() diff --git a/SCons/Variables/__init__.py b/SCons/Variables/__init__.py index 867493d7c8..ca18432153 100644 --- a/SCons/Variables/__init__.py +++ b/SCons/Variables/__init__.py @@ -23,120 +23,139 @@ """Adds user-friendly customizable variables to an SCons build.""" +from __future__ import annotations + import os.path import sys +from contextlib import suppress +from dataclasses import dataclass from functools import cmp_to_key -from typing import Callable, Dict, List, Optional, Sequence, Union +from typing import Any, Callable, Sequence -import SCons.Environment import SCons.Errors import SCons.Util import SCons.Warnings # Note: imports are for the benefit of SCons.Main (and tests); since they # are not used here, the "as Foo" form is for checkers. -from .BoolVariable import BoolVariable as BoolVariable -from .EnumVariable import EnumVariable as EnumVariable -from .ListVariable import ListVariable as ListVariable -from .PackageVariable import PackageVariable as PackageVariable -from .PathVariable import PathVariable as PathVariable +from .BoolVariable import BoolVariable +from .EnumVariable import EnumVariable +from .ListVariable import ListVariable +from .PackageVariable import PackageVariable +from .PathVariable import PathVariable __all__ = [ "Variable", "Variables", + "BoolVariable", + "EnumVariable", + "ListVariable", + "PackageVariable", + "PathVariable", ] + +@dataclass(order=True) class Variable: """A Build Variable.""" - - __slots__ = ('key', 'aliases', 'help', 'default', 'validator', 'converter') - - def __lt__(self, other): - """Comparison fuction so Variable instances sort.""" - return self.key < other.key - - def __str__(self) -> str: - """Provide a way to "print" a Variable object.""" - return f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, {self.validator}, {self.converter})" + __slots__ = ('key', 'aliases', 'help', 'default', 'validator', 'converter', 'do_subst') + key: str + aliases: list[str] + help: str + default: Any + validator: Callable | None + converter: Callable | None + do_subst: bool class Variables: - """A container for multiple Build Variables. + """A container for Build Variables. + + Includes a method to populate the variables with values into a + construction envirionment, and methods to render the help text. - Includes methods to updates the environment with the variables, - and to render the help text. + Note that the pubic API for creating a ``Variables`` object is + :func:`SCons.Script.Variables`, a kind of factory function, which + defaults to supplying the contents of :attr:`~SCons.Script.ARGUMENTS` + as the *args* parameter if it was not otherwise given. That is the + behavior documented in the manpage for ``Variables`` - and different + from the default if you instantiate this directly. Arguments: files: string or list of strings naming variable config scripts (default ``None``) args: dictionary to override values set from *files*. (default ``None``) - is_global: if true, return a global singleton Variables object instead - of a fresh instance. Currently inoperable (default ``False``) + is_global: if true, return a global singleton :class:`Variables` object + instead of a fresh instance. Currently inoperable (default ``False``) .. versionchanged:: 4.8.0 - The default for *is_global* changed to ``False`` (previously - ``True`` but it had no effect due to an implementation error). - *is_global* is deprecated. + The default for *is_global* changed to ``False`` (the previous + default ``True`` had no effect due to an implementation error). + + .. deprecated:: 4.8.0 + *is_global* is deprecated. + + .. versionadded:: 4.9.0 + The :attr:`defaulted` attribute now lists those variables which + were filled in from default values. """ def __init__( self, - files: Optional[Union[str, Sequence[str]]] = None, - args: Optional[dict] = None, + files: str | Sequence[str | None] = None, + args: dict | None = None, is_global: bool = False, ) -> None: - self.options: List[Variable] = [] + self.options: list[Variable] = [] self.args = args if args is not None else {} - if not SCons.Util.is_List(files): + if not SCons.Util.is_Sequence(files): files = [files] if files else [] - self.files = files - self.unknown: Dict[str, str] = {} + self.files: Sequence[str] = files + self.unknown: dict[str, str] = {} + self.defaulted: list[str] = [] def __str__(self) -> str: - """Provide a way to "print" a Variables object.""" - s = "Variables(\n options=[\n" - for option in self.options: - s += f" {str(option)},\n" - s += " ],\n" - s += f" args={self.args},\n files={self.files},\n unknown={self.unknown},\n)" - return s + """Provide a way to "print" a :class:`Variables` object.""" + opts = ',\n'.join((f" {option!s}" for option in self.options)) + return ( + f"Variables(\n options=[\n{opts}\n ],\n" + f" args={self.args},\n" + f" files={self.files},\n" + f" unknown={self.unknown},\n" + f" defaulted={self.defaulted},\n)" + ) # lint: W0622: Redefining built-in 'help' def _do_add( self, - key: Union[str, List[str]], + key: str | Sequence[str], help: str = "", default=None, - validator: Optional[Callable] = None, - converter: Optional[Callable] = None, + validator: Callable | None = None, + converter: Callable | None = None, **kwargs, ) -> None: - """Create a Variable and add it to the list. + """Create a :class:`Variable` and add it to the list. - Internal routine, not public API. - """ - option = Variable() + This is the internal implementation for :meth:`Add` and + :meth:`AddVariables`. Not part of the public API. - # If we get a list or a tuple, we take the first element as the - # option key and store the remaining in aliases. + .. versionadded:: 4.8.0 + *subst* keyword argument is now recognized. + """ + # aliases needs to be a list for later concatenation operations if SCons.Util.is_Sequence(key): - option.key = key[0] - option.aliases = list(key[1:]) + name, aliases = key[0], list(key[1:]) else: - option.key = key - # TODO: normalize to not include key in aliases. Currently breaks tests. - option.aliases = [key,] - if not SCons.Environment.is_valid_construction_var(option.key): - raise SCons.Errors.UserError(f"Illegal Variables key {option.key!r}") - option.help = help - option.default = default - option.validator = validator - option.converter = converter - + name, aliases = key, [] + if not name.isidentifier(): + raise SCons.Errors.UserError(f"Illegal Variables key {name!r}") + do_subst = kwargs.pop("subst", True) + option = Variable(name, aliases, help, default, validator, converter, do_subst) self.options.append(option) - # options might be added after the 'unknown' dict has been set up, - # so we remove the key and all its aliases from that dict + # options might be added after the 'unknown' dict has been set up: + # look for and remove the key and all its aliases from that dict for alias in option.aliases + [option.key,]: if alias in self.unknown: del self.unknown[alias] @@ -147,39 +166,48 @@ def keys(self) -> list: yield option.key def Add( - self, key: Union[str, Sequence], *args, **kwargs, + self, key: str | Sequence, *args, **kwargs, ) -> None: """Add a Build Variable. Arguments: - key: the name of the variable, or a 5-tuple (or list). - If *key* is a tuple, and there are no additional positional - arguments, it is unpacked into the variable name plus the four - listed keyword arguments from below. - If *key* is a tuple and there are additional positional arguments, - the first word of the tuple is taken as the variable name, - and the remainder as aliases. - args: optional positional arguments, corresponding to the four - listed keyword arguments. + key: the name of the variable, or a 5-tuple (or other sequence). + If *key* is a tuple, and there are no additional arguments + except the *help*, *default*, *validator* and *converter* + keyword arguments, *key* is unpacked into the variable name + plus the *help*, *default*, *validator* and *converter* + arguments; if there are additional arguments, the first + elements of *key* is taken as the variable name, and the + remainder as aliases. + args: optional positional arguments, corresponding to the + *help*, *default*, *validator* and *converter* keyword args. kwargs: arbitrary keyword arguments used by the variable itself. Keyword Args: - help: help text for the variable (default: ``""``) + help: help text for the variable (default: empty string) default: default value for variable (default: ``None``) validator: function called to validate the value (default: ``None``) converter: function to be called to convert the variable's value before putting it in the environment. (default: ``None``) + subst: perform substitution on the value before the converter + and validator functions (if any) are called (default: ``True``) + + .. versionadded:: 4.8.0 + The *subst* keyword argument is now specially recognized. """ if SCons.Util.is_Sequence(key): - if not (len(args) or len(kwargs)): - return self._do_add(*key) + # If no other positional args (and no fundamental kwargs), + # unpack key, and pass the kwargs on: + known_kw = {'help', 'default', 'validator', 'converter'} + if not args and not known_kw.intersection(kwargs.keys()): + return self._do_add(*key, **kwargs) return self._do_add(key, *args, **kwargs) def AddVariables(self, *optlist) -> None: - """Add a list of Build Variables. + """Add Build Variables. - Each list element is a tuple/list of arguments to be passed on + Each *optlist* element is a sequence of arguments to be passed on to the underlying method for adding variables. Example:: @@ -194,34 +222,60 @@ def AddVariables(self, *optlist) -> None: for opt in optlist: self._do_add(*opt) - def Update(self, env, args: Optional[dict] = None) -> None: + def Update(self, env, args: dict | None = None) -> None: """Update an environment with the Build Variables. + This is where the work of adding variables to the environment + happens, The input sources saved at init time are scanned for + variables to add, though if *args* is passed, then it is used + instead of the saved one. If any variable description set up + a callback for a validator and/or converter, those are called. + Variables from the input sources which do not match a variable + description in this object are ignored for purposes of adding + to *env*, but are saved in the :attr:`unknown` dict attribute. + Variables which are set in *env* from the default in a variable + description and not from the input sources are saved in the + :attr:`defaulted` list attribute. + Args: env: the environment to update. args: a dictionary of keys and values to update in *env*. If omitted, uses the saved :attr:`args` """ - # first pull in the defaults + # first pull in the defaults, except any which are None. values = {opt.key: opt.default for opt in self.options if opt.default is not None} + self.defaulted = list(values) - # next set the values specified in any options script(s) + # next set the values specified in any saved-variables script(s) for filename in self.files: # TODO: issue #816 use Node to access saved-variables file? if os.path.exists(filename): - # lint: W0622: Redefining built-in 'dir' - dir = os.path.split(os.path.abspath(filename))[0] - if dir: - sys.path.insert(0, dir) + # issue #4645: don't exec directly into values, + # so we can iterate through for unknown variables. + temp_values = {} + dirname = os.path.split(os.path.abspath(filename))[0] + if dirname: + sys.path.insert(0, dirname) try: - values['__name__'] = filename + temp_values['__name__'] = filename with open(filename) as f: contents = f.read() - exec(contents, {}, values) + exec(contents, {}, temp_values) finally: - if dir: + if dirname: del sys.path[0] - del values['__name__'] + del temp_values['__name__'] + + for arg, value in temp_values.items(): + added = False + for option in self.options: + if arg in option.aliases + [option.key,]: + values[option.key] = value + with suppress(ValueError): + self.defaulted.remove(option.key) + added = True + if not added: + self.unknown[arg] = value # set the values specified on the command line if args is None: @@ -232,12 +286,22 @@ def Update(self, env, args: Optional[dict] = None) -> None: for option in self.options: if arg in option.aliases + [option.key,]: values[option.key] = value + with suppress(ValueError): + self.defaulted.remove(option.key) added = True if not added: self.unknown[arg] = value - # put the variables in the environment: + # put the variables in the environment # (don't copy over variables that are not declared as options) + # + # Nitpicking: in OO terms, this method increases coupling as its + # main work is to update a different object (env), rather than + # the object it's bound to (although it does update self, too). + # It's tricky to decouple because the algorithm counts on directly + # setting a var in *env* first so it can call env.subst() on it + # to transform it. + for option in self.options: try: env[option.key] = values[option.key] @@ -247,7 +311,10 @@ def Update(self, env, args: Optional[dict] = None) -> None: # apply converters for option in self.options: if option.converter and option.key in values: - value = env.subst(f'${option.key}') + if option.do_subst: + value = env.subst('${%s}' % option.key) + else: + value = env[option.key] try: try: env[option.key] = option.converter(value) @@ -262,7 +329,17 @@ def Update(self, env, args: Optional[dict] = None) -> None: # apply validators for option in self.options: if option.validator and option.key in values: - option.validator(option.key, env.subst(f'${option.key}'), env) + if option.do_subst: + val = env[option.key] + if not SCons.Util.is_String(val): + # issue #4585: a _ListVariable should not be further + # substituted, breaks on values with spaces. + value = val + else: + value = env.subst('${%s}' % option.key) + else: + value = env[option.key] + option.validator(option.key, value, env) def UnknownVariables(self) -> dict: """Return dict of unknown variables. @@ -325,7 +402,7 @@ def Save(self, filename, env) -> None: msg = f'Error writing options to file: {filename}\n{exc}' raise SCons.Errors.UserError(msg) from exc - def GenerateHelpText(self, env, sort: Union[bool, Callable] = False) -> str: + def GenerateHelpText(self, env, sort: bool | Callable = False) -> str: """Generate the help text for the Variables object. Args: @@ -335,12 +412,12 @@ def GenerateHelpText(self, env, sort: Union[bool, Callable] = False) -> str: (must take two arguments and return ``-1``, ``0`` or ``1``) or a boolean to indicate if it should be sorted. """ - # TODO the 'sort' argument matched the old way Python's sorted() - # worked, taking a comparison function argument. That has been - # removed so now we have to convert to a key. + # TODO this interface was designed when Python's sorted() took an + # optional comparison function (pre-3.0). Since it no longer does, + # we use functools.cmp_to_key() since can't really change the + # documented meaning of the "sort" argument. Maybe someday? if callable(sort): options = sorted(self.options, key=cmp_to_key(lambda x, y: sort(x.key, y.key))) - elif sort is True: options = sorted(self.options) else: @@ -367,7 +444,7 @@ def FormatVariableHelpText( help: str, default, actual, - aliases: Optional[List[str]] = None, + aliases: list[str | None] = None, ) -> str: """Format the help text for a single variable. diff --git a/SCons/Warnings.py b/SCons/Warnings.py index d604659c40..b9f9cc16f4 100644 --- a/SCons/Warnings.py +++ b/SCons/Warnings.py @@ -61,8 +61,10 @@ framework and it will behave like an ordinary exception. """ +from __future__ import annotations + import sys -from typing import Callable, Sequence, Optional +from typing import Callable, Sequence import SCons.Errors @@ -75,7 +77,7 @@ # Function to emit the warning. Initialized by SCons/Main.py for regular use; # the unit test will set to a capturing version for testing. -_warningOut: Optional[Callable] = None +_warningOut: Callable | None = None class SConsWarning(SCons.Errors.UserError): diff --git a/SCons/__init__.py b/SCons/__init__.py index e89057fe29..b831ae56c5 100644 --- a/SCons/__init__.py +++ b/SCons/__init__.py @@ -1,9 +1,9 @@ -__version__="4.7.0" -__copyright__="Copyright (c) 2001 - 2024 The SCons Foundation" +__version__="4.9.1" +__copyright__="Copyright (c) 2001 - 2025 The SCons Foundation" __developer__="bdbaddog" -__date__="Sun, 17 Mar 2024 17:33:54 -0700" +__date__="Thu, 27 Mar 2025 11:44:24 -0700" __buildsys__="M1Dog2021" -__revision__="265be6883fadbb5a545612265acc919595158366" -__build__="265be6883fadbb5a545612265acc919595158366" +__revision__="39a12f34d532ab2493e78a7b73aeab2250852790" +__build__="39a12f34d532ab2493e78a7b73aeab2250852790" # make sure compatibility is always in place import SCons.compat # noqa \ No newline at end of file diff --git a/SCons/cpp.py b/SCons/cpp.py index 1093ae2ac2..63e49ae88c 100644 --- a/SCons/cpp.py +++ b/SCons/cpp.py @@ -146,13 +146,27 @@ def Cleanup_CPP_Expressions(ts): # ...and compile the expression. CPP_to_Python_Ops_Expression = re.compile(expr) +# integer specifications +int_suffix_opt = r'(?:[uU](?:l{1,2}|L{1,2}|[zZ]|wb|WB)?|(?:l{1,2}|L{1,2}|[zZ]|wb|WB)[uU]?)?' + +hex_integer = fr'(0[xX][0-9A-Fa-f]+){int_suffix_opt}' +bin_integer = fr'(0[bB][01]+){int_suffix_opt}' +oct_integer = fr'(0[0-7]*){int_suffix_opt}' +dec_integer = fr'([1-9][0-9]*){int_suffix_opt}' + +int_boundary = r'[a-zA-Z0-9_]' +int_neg_lookbehind = fr'(? None: def scons_current_file(self, t) -> None: self.current_file = t[1] + def eval_constant_expression(self, s): + """ + Evaluates a C preprocessor expression. + + This is done by converting it to a Python equivalent and + eval()ing it in the C preprocessor namespace we use to + track #define values. + + Returns None if the eval() result is not an integer. + """ + s = CPP_to_Python(s) + try: + rval = eval(s, self.constant_expression_namespace) + except (NameError, TypeError, SyntaxError) as e: + rval = None + if not isinstance(rval, int): + rval = None + return rval + def eval_expression(self, t): """ Evaluates a C preprocessor expression. @@ -507,9 +543,11 @@ def do_define(self, t) -> None: Default handling of a #define line. """ _, name, args, expansion = t - try: - expansion = int(expansion) - except (TypeError, ValueError): + + rval = self.eval_constant_expression(expansion) + if rval is not None: + expansion = rval + else: # handle "defined" chain "! (defined (A) || defined (B)" ... if "defined " in expansion: self.cpp_namespace[name] = self.eval_expression(t[2:]) diff --git a/SCons/cppTests.py b/SCons/cppTests.py index 85f01b7813..8b079ec742 100644 --- a/SCons/cppTests.py +++ b/SCons/cppTests.py @@ -151,7 +151,7 @@ #include #endif -#if ! (defined (DEFINED_A) || defined (DEFINED_B) +#if ! (defined (DEFINED_A) || defined (DEFINED_B)) #include #else #include @@ -236,11 +236,189 @@ #include #endif -#if 123456789UL || 0x13L -#include +#if (123456789UL || 0x13L) +#include #else -#include +#include #endif + +#if (123456789UL && 0x0LU) +#include +#else +#include +#endif + +#if 123456789UL +#include +#else +#include +#endif + +#if 1234U +#include +#else +#include +#endif + +#if 1234L +#include +#else +#include +#endif + +#if 1234ULL +#include +#else +#include +#endif + +#define X1234UL 1 +#if X1234UL +#include +#else +#include +#endif + +#define X1234U 1 +#if X1234U +#include +#else +#include +#endif + +#define X1234L 1 +#if X1234L +#include +#else +#include +#endif + +#define X1234ULL 1 +#if X1234ULL +#include +#else +#include +#endif + +#define DEC0 0 +#define HEX0 0x0 +#define HEXF 0xF + +#if DEC0 +#include +#else +#include +#endif + +#if ! DEC0 +#include +#else +#include +#endif + +#if (DEC0) +#include +#else +#include +#endif + +#if !(DEC0) +#include +#else +#include +#endif + +#if HEX0 +#include +#else +#include +#endif + +#if ! HEX0 +#include +#else +#include +#endif + +#if (HEX0) +#include +#else +#include +#endif + +#if !(HEX0) +#include +#else +#include +#endif + +#if HEXF +#include +#else +#include +#endif + +#if ! HEXF +#include +#else +#include +#endif + +#if (HEXF) +#include +#else +#include +#endif + +#if !(HEXF) +#include +#else +#include +#endif + +#if defined(DEC0) && (DEC0 & 0x1) +#include +#else +#include +#endif + +#if !(defined(HEXF) && (HEXF & 0x1)) +#include +#else +#include +#endif + +#define X2345ULL 1 +#if !(X2345ULL > 4567ull) +#include +#else +#include +#endif + +#if !0ull +#include +#else +#include +#endif + +#define X0U 0U +#if X0U +#include +#else +#include +#endif + +#define XF1 (0x0U & 0x1U) +#if XF1 +#include +#else +#include +#endif + +#define ABC00 0U +#define ABC01 1U +#define ABC_(a, b) ABC##a##b +#define ABC ABC_(ZERO, ZERO) """ @@ -303,8 +481,8 @@ #define FUNC39a(x0, y0) FILE39 #define FUNC40a(x0, y0) FILE40 -#define FUNC39b(x1, y2) FUNC39a(x1, y1) -#define FUNC40b(x1, y2) FUNC40a(x1, y1) +#define FUNC39b(x1, y1) FUNC39a(x1, y1) +#define FUNC40b(x1, y1) FUNC40a(x1, y1) #define FUNC39c(x2, y2) FUNC39b(x2, y2) #define FUNC40c(x2, y2) FUNC40b(x2, y2) @@ -480,7 +658,12 @@ def test_expression(self) -> None: """Test #if with arithmetic expressions""" expect = self.expression_expect result = self.cpp.process_contents(expression_input) - assert expect == result, (expect, result) + if expect != result: + for e,r in zip(expect, result): + if e != r: + print("ERROR->",end="") + print(f"{e}: {r}") + assert expect == result, f"\nexpect:{expect}\nresult:{result}" def test_undef(self) -> None: """Test #undef handling""" @@ -588,7 +771,41 @@ class PreProcessorTestCase(cppAllTestCase): ('include', '<', 'file28-yes'), ('include', '"', 'file29-yes'), ('include', '<', 'file30-yes'), - ('include', '<', 'file301-yes'), + + ('include', '<', 'file301or-yes'), + ('include', '<', 'file301and-no'), + ('include', '<', 'file301ul-yes'), + ('include', '<', 'file301u-yes'), + ('include', '<', 'file301l-yes'), + ('include', '<', 'file301ull-yes'), + + ('include', '<', 'file302-yes'), + ('include', '<', 'file303-yes'), + ('include', '<', 'file304-yes'), + ('include', '<', 'file305-yes'), + + ('include', '<', 'file401-no'), + ('include', '<', 'file402-yes'), + ('include', '<', 'file403-no'), + ('include', '<', 'file404-yes'), + + ('include', '<', 'file411-no'), + ('include', '<', 'file412-yes'), + ('include', '<', 'file413-no'), + ('include', '<', 'file414-yes'), + + ('include', '<', 'file421-yes'), + ('include', '<', 'file422-no'), + ('include', '<', 'file423-yes'), + ('include', '<', 'file424-no'), + + ('include', '<', 'file431-no'), + ('include', '<', 'file432-no'), + + ('include', '<', 'file501-yes'), + ('include', '<', 'file502-yes'), + ('include', '<', 'file503-no'), + ('include', '<', 'file504-no'), ] undef_expect = [ @@ -717,8 +934,68 @@ class DumbPreProcessorTestCase(cppAllTestCase): ('include', '"', 'file29-yes'), ('include', '<', 'file30-yes'), ('include', '<', 'file30-no'), - ('include', '<', 'file301-yes'), - ('include', '<', 'file301-no'), + + ('include', '<', 'file301or-yes'), + ('include', '<', 'file301or-no'), + ('include', '<', 'file301and-yes'), + ('include', '<', 'file301and-no'), + ('include', '<', 'file301ul-yes'), + ('include', '<', 'file301ul-no'), + ('include', '<', 'file301u-yes'), + ('include', '<', 'file301u-no'), + ('include', '<', 'file301l-yes'), + ('include', '<', 'file301l-no'), + ('include', '<', 'file301ull-yes'), + ('include', '<', 'file301ull-no'), + + ('include', '<', 'file302-yes'), + ('include', '<', 'file302-no'), + ('include', '<', 'file303-yes'), + ('include', '<', 'file303-no'), + ('include', '<', 'file304-yes'), + ('include', '<', 'file304-no'), + ('include', '<', 'file305-yes'), + ('include', '<', 'file305-no'), + + ('include', '<', 'file401-yes'), + ('include', '<', 'file401-no'), + ('include', '<', 'file402-yes'), + ('include', '<', 'file402-no'), + ('include', '<', 'file403-yes'), + ('include', '<', 'file403-no'), + ('include', '<', 'file404-yes'), + ('include', '<', 'file404-no'), + + ('include', '<', 'file411-yes'), + ('include', '<', 'file411-no'), + ('include', '<', 'file412-yes'), + ('include', '<', 'file412-no'), + ('include', '<', 'file413-yes'), + ('include', '<', 'file413-no'), + ('include', '<', 'file414-yes'), + ('include', '<', 'file414-no'), + + ('include', '<', 'file421-yes'), + ('include', '<', 'file421-no'), + ('include', '<', 'file422-yes'), + ('include', '<', 'file422-no'), + ('include', '<', 'file423-yes'), + ('include', '<', 'file423-no'), + ('include', '<', 'file424-yes'), + ('include', '<', 'file424-no'), + ('include', '<', 'file431-yes'), + ('include', '<', 'file431-no'), + ('include', '<', 'file432-yes'), + ('include', '<', 'file432-no'), + + ('include', '<', 'file501-yes'), + ('include', '<', 'file501-no'), + ('include', '<', 'file502-yes'), + ('include', '<', 'file502-no'), + ('include', '<', 'file503-yes'), + ('include', '<', 'file503-no'), + ('include', '<', 'file504-yes'), + ('include', '<', 'file504-no'), ] undef_expect = [ diff --git a/SConstruct b/SConstruct index 41b1d355be..39c398badd 100644 --- a/SConstruct +++ b/SConstruct @@ -36,7 +36,7 @@ copyright_years = strftime('2001 - %Y') # This gets inserted into the man pages to reflect the month of release. month_year = strftime('%B %Y') project = 'scons' -default_version = '4.7.1' +default_version = '4.9.1' copyright = f"Copyright (c) {copyright_years} The SCons Foundation" # We let the presence or absence of various utilities determine whether @@ -205,7 +205,7 @@ wheel = env.Command( '$DISTDIR/SCons-${VERSION}-py3-none-any.whl', '$DISTDIR/SCons-${VERSION}.tar.gz', ], - source=['setup.cfg', 'setup.py', 'SCons/__init__.py'] + man_pages, + source=['pyproject.toml', 'setup.py', 'SCons/__init__.py'] + man_pages, action='$PYTHON -m build --outdir $DISTDIR', ) env.Alias("wheel", wheel[0]) @@ -215,7 +215,7 @@ env.Alias("tar-gz", wheel[1]) # and it deletes its isolated env so we can't just zip that one up. zip_file = env.Command( target='$DISTDIR/SCons-${VERSION}.zip', - source=['setup.cfg', 'setup.py', 'SCons/__init__.py'] + man_pages, + source=['pyproject.toml', 'setup.py', 'SCons/__init__.py'] + man_pages, action='$PYTHON setup.py sdist --format=zip', ) env.Alias("zip", zip_file) diff --git a/bench/bench.py b/bench/bench.py index 5adac230cd..bafe5a29d4 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -62,10 +62,10 @@ -h, --help Display this help and exit -i ITER, --iterations ITER Run each code snippet ITER times --time Use the time.time function - -r RUNS, --runs RUNS Average times for RUNS invocations of + -r RUNS, --runs RUNS Average times for RUNS invocations of """ -# How many times each snippet of code will be (or should be) run by the +# How many times each snippet of code will be (or should be) run by the # functions under test to gather the time (the "inner loop"). Iterations = 1000 @@ -191,19 +191,17 @@ def display(func, result_label, results): # pprint(results_dict) -# breakpoint() + tests = [label for label, args, kw in Data] -columns = ['Python Version', 'Implementation', 'Test'] + tests +columns = ['Python Version', 'Implementation'] + tests with open(results_filename, 'a') as r: - print("Python Version,%s" % ".".join(columns), file=r) - # print("Python Version,%s" % ".".join(columns)) + print(",".join(columns), file=r) - for implementation in results_dict.keys(): + for implementation in results_dict: + print(f'{py_ver_string},"{implementation}"', file=r, end='') for test in tests: - print(f'{py_ver_string},"{implementation}","{test}",%8.3f' % results_dict[implementation][test], file=r) - # print(f'{py_ver_string},"{implementation}","{test}",%8.3f' % results_dict[implementation][test]) - - + print(',%8.3f' % results_dict[implementation][test], file=r, end='') + print(file=r) # Local Variables: # tab-width:4 diff --git a/bench/env.__setitem__.py b/bench/env.__setitem__.py index f9fe0c0cd1..e915e46338 100644 --- a/bench/env.__setitem__.py +++ b/bench/env.__setitem__.py @@ -1,8 +1,7 @@ # __COPYRIGHT__ # # Benchmarks for testing various possible implementations of the -# env.__setitem__() method(s) in the src/engine/SCons/Environment.py -# module. +# env.__setitem__() method(s) in the SCons/Environment.py module. from __future__ import print_function @@ -25,10 +24,10 @@ def __init__(self, name, num, init, statement): self.name = name self.statement = statement self.__result = None - + def timeit(self): self.__result = self.__timer.timeit(self.__num) - + def getResult(self): return self.__result @@ -58,13 +57,14 @@ def times(num=1000000, init='', title='Results:', **statements): script_dir = os.path.split(filename)[0] if script_dir: script_dir = script_dir + '/' -sys.path = [os.path.abspath(script_dir + '../src/engine')] + sys.path +sys.path = [os.path.abspath(script_dir + '..')] + sys.path import SCons.Errors import SCons.Environment +import SCons.Util -is_valid_construction_var = SCons.Environment.is_valid_construction_var -global_valid_var = re.compile(r'[_a-zA-Z]\w*$') +is_valid_construction_var = SCons.Util.is_valid_construction_var +global_valid_var = SCons.Util.envs._is_valid_var_re # The classes with different __setitem__() implementations that we're # going to horse-race. @@ -80,7 +80,7 @@ def times(num=1000000, init='', title='Results:', **statements): # # The env_Original subclass contains the original implementation (which # actually had the is_valid_construction_var() function in SCons.Util -# originally). +# originally). Update: it has moved back, to avoid import loop with Variables. # # The other subclasses (except for env_Best) each contain *one* # significant change from the env_Original implementation. The doc string @@ -105,7 +105,7 @@ class Environment: 'SOURCES' : None, } _special_set_keys = list(_special_set.keys()) - _valid_var = re.compile(r'[_a-zA-Z]\w*$') + _valid_var = global_valid_var def __init__(self, **kw): self._dict = kw @@ -116,7 +116,7 @@ def __setitem__(self, key, value): if special: special(self, key, value) else: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -176,7 +176,7 @@ def __setitem__(self, key, value): if key in self._special_set: self._special_set[key](self, key, value) else: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -186,7 +186,7 @@ def __setitem__(self, key, value): if key in ('BUILDERS', 'SCANNERS', 'TARGET', 'TARGETS', 'SOURCE', 'SOURCES'): self._special_set[key](self, key, value) else: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -196,7 +196,7 @@ def __setitem__(self, key, value): if key in ['BUILDERS', 'SCANNERS', 'TARGET', 'TARGETS', 'SOURCE', 'SOURCES']: self._special_set[key](self, key, value) else: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -206,7 +206,7 @@ def __setitem__(self, key, value): if key in self._special_set_keys: self._special_set[key](self, key, value) else: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -220,7 +220,7 @@ def __setitem__(self, key, value): try: self._dict[key] except KeyError: - if not SCons.Environment.is_valid_construction_var(key): + if not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -232,7 +232,7 @@ def __setitem__(self, key, value): special(self, key, value) else: if key not in self._dict \ - and not SCons.Environment.is_valid_construction_var(key): + and not SCons.Util.is_valid_construction_var(key): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value @@ -247,8 +247,8 @@ def __setitem__(self, key, value): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value -class env_Best_has_key(Environment): - """Best __setitem__(), with has_key""" +class env_Best_has_key_global_regex(Environment): + """Best __setitem__(), with membership test and global regex""" def __setitem__(self, key, value): if key in self._special_set: self._special_set[key](self, key, value) @@ -258,6 +258,18 @@ def __setitem__(self, key, value): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value +class env_Best_has_key_function(Environment): + """Best __setitem__(), with membership_test and validator function""" + def __setitem__(self, key, value): + if key in self._special_set: + self._special_set[key](self, key, value) + else: + if key not in self._dict \ + and not is_valid_construction_var(key): + raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) + self._dict[key] = value + + class env_Best_list(Environment): """Best __setitem__(), with a list""" def __setitem__(self, key, value): @@ -284,6 +296,16 @@ def __setitem__(self, key, value): raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) self._dict[key] = value +class env_Best_isidentifier(Environment): + """Best __setitem__(), with membership test and isidentifier""" + def __setitem__(self, key, value): + if key in self._special_set: + self._special_set[key](self, key, value) + else: + if key not in self._dict and not key.isidentifier(): + raise SCons.Errors.UserError("Illegal construction variable `%s'" % key) + self._dict[key] = value + # We'll use the names of all the env_* classes we find later to build # the dictionary of statements to be timed, and the import statement # that the timer will use to get at these classes. diff --git a/bench/is_types.py b/bench/is_types.py index 01db768ca9..ecc0edbdb0 100644 --- a/bench/is_types.py +++ b/bench/is_types.py @@ -2,10 +2,11 @@ # # Benchmarks for testing various possible implementations # of the is_Dict(), is_List() and is_String() functions in -# src/engine/SCons/Util.py. +# SCons/Util.py. import types -from collections import UserDict, UserList, UserString +from collections import UserDict, UserList, UserString, deque +from collections.abc import Iterable, MappingView, MutableMapping, MutableSequence DictType = dict ListType = list @@ -28,19 +29,20 @@ def original_is_String(e): # New candidates that explicitly check for whether the object is an # InstanceType before calling isinstance() on the corresponding User* -# type. -# InstanceType was only for old-style classes, so absent in Python 3 -# this this is no different than the previous +# type. Update: meaningless in Python 3, InstanceType was only for +# old-style classes, so these are just removed. +# XXX -def checkInstanceType_is_Dict(e): - return isinstance(e, (dict, UserDict)) +# New candidates that try more generic names from collections: -def checkInstanceType_is_List(e): - return isinstance(e, (list, UserList)) +def new_is_Dict(e): + return isinstance(e, MutableMapping) -def checkInstanceType_is_String(e): - return isinstance(e, (str, UserString)) +def new_is_List(e): + return isinstance(e, MutableSequence) +def new_is_String(e): + return isinstance(e, (str, UserString)) # Improved candidates that cache the type(e) result in a variable # before doing any checks. @@ -51,7 +53,7 @@ def cache_type_e_is_Dict(e): def cache_type_e_is_List(e): t = type(e) - return t is list or isinstance(e, UserList) + return t is list or isinstance(e, (UserList, deque)) def cache_type_e_is_String(e): t = type(e) @@ -68,7 +70,7 @@ def global_cache_type_e_is_Dict(e): def global_cache_type_e_is_List(e): t = type(e) - return t is ListType or isinstance(e, UserList) + return t is ListType or isinstance(e, (UserList, deque)) def global_cache_type_e_is_String(e): t = type(e) @@ -77,30 +79,10 @@ def global_cache_type_e_is_String(e): # Alternative that uses a myType() function to map the User* objects # to their corresponding underlying types. - -instanceTypeMap = { - UserDict : dict, - UserList : list, - UserString : str, -} - -def myType(obj): - t = type(obj) - if t is types.InstanceType: - t = instanceTypeMap.get(obj.__class__, t) - return t - -def myType_is_Dict(e): - return myType(e) is dict - -def myType_is_List(e): - return myType(e) is list - -def myType_is_String(e): - return myType(e) is str - +# Again, since this used InstanceType, it's not useful for Python 3. +# These are the actual test entry points def Func01(obj): """original_is_String""" @@ -118,19 +100,19 @@ def Func03(obj): original_is_Dict(obj) def Func04(obj): - """checkInstanceType_is_String""" + """new_is_String""" for i in IterationList: - checkInstanceType_is_String(obj) + new_is_String(obj) def Func05(obj): - """checkInstanceType_is_List""" + """new_is_List""" for i in IterationList: - checkInstanceType_is_List(obj) + new_is_List(obj) def Func06(obj): - """checkInstanceType_is_Dict""" + """new_is_Dict""" for i in IterationList: - checkInstanceType_is_Dict(obj) + new_is_Dict(obj) def Func07(obj): """cache_type_e_is_String""" @@ -162,22 +144,6 @@ def Func12(obj): for i in IterationList: global_cache_type_e_is_Dict(obj) -#def Func13(obj): -# """myType_is_String""" -# for i in IterationList: -# myType_is_String(obj) -# -#def Func14(obj): -# """myType_is_List""" -# for i in IterationList: -# myType_is_List(obj) -# -#def Func15(obj): -# """myType_is_Dict""" -# for i in IterationList: -# myType_is_Dict(obj) - - # Data to pass to the functions on each run. Each entry is a # three-element tuple: @@ -217,6 +183,11 @@ class A: (UserList([]),), {}, ), + ( + "deque", + (deque([]),), + {}, + ), ( "UserDict", (UserDict({}),), diff --git a/bench/lvars-gvars.py b/bench/lvars-gvars.py index 0a81337581..91d76a0c43 100644 --- a/bench/lvars-gvars.py +++ b/bench/lvars-gvars.py @@ -23,9 +23,9 @@ """ Functions and data for timing different idioms for fetching a keyword -value from a pair of dictionaries for localand global values. This was +value from a pair of dictionaries for local and global values. This was used to select how to most efficiently expand single $KEYWORD strings -in src/engine/SCons/Subst.py. +in SCons/Subst.py (StringSubber and ListSubber). """ def Func1(var, gvars, lvars): @@ -40,7 +40,7 @@ def Func1(var, gvars, lvars): x = '' def Func2(var, gvars, lvars): - """lvars has_key(), gvars try:-except:""" + """lvars membership test, gvars try:-except:""" for i in IterationList: if var in lvars: x = lvars[var] @@ -51,7 +51,7 @@ def Func2(var, gvars, lvars): x = '' def Func3(var, gvars, lvars): - """lvars has_key(), gvars has_key()""" + """lvars membership test, gvars membership test)""" for i in IterationList: if var in lvars: x = lvars[var] @@ -71,7 +71,7 @@ def Func4(var, gvars, lvars): def Func5(var, gvars, lvars): """Chained get with default values""" for i in IterationList: - x = lvars.get(var,gvars.get(var,'')) + x = lvars.get(var, gvars.get(var, '')) # Data to pass to the functions on each run. Each entry is a diff --git a/bin/SConsDoc.py b/bin/SConsDoc.py index 82e2c720e7..9c81b187b6 100644 --- a/bin/SConsDoc.py +++ b/bin/SConsDoc.py @@ -1,34 +1,18 @@ #!/usr/bin/env python # -# Copyright (c) 2010 The SCons Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -# -# Module for handling SCons documentation processing. -# +# SPDX-FileCopyrightText: Copyright The SCons Foundation (https://scons.org) +# SPDX-License-Identifier: MIT + +""" +SCons Documentation Processing module +===================================== -__doc__ = r""" -This module parses home-brew XML files that document various things -in SCons. Right now, it handles Builders, functions, construction -variables, and Tools, but we expect it to get extended in the future. +This module parses home-brew XML files that document important SCons +components. Currently it handles Builders, Environment functions/methods, +Construction Variables, and Tools (further expansion is possible). These +documentation snippets are turned into files with content and reference +tags that can be included into the manpage and/or user guide, which +prevents a lot of duplication. In general, you can use any DocBook tag in the input, and this module just adds processing various home-brew tags to try to make life a @@ -36,6 +20,8 @@ Builder example: +.. code-block:: xml + This is the summary description of an SCons Builder. @@ -47,7 +33,7 @@ interpolated by specifying the &b-link-BUILDER; element. - Unlike normal XML, blank lines are significant in these + Unlike vanilla DocBook, blank lines are significant in these descriptions and serve to separate paragraphs. They'll get replaced in DocBook output with appropriate tags to indicate a new paragraph. @@ -60,6 +46,8 @@ Function example: +.. code-block:: xml + (arg1, arg2, key=value) @@ -81,7 +69,7 @@ anywhere in the document by specifying the &f-FUNCTION; element or the &f-env-FUNCTION; element. Links to this definition may be interpolated by specifying - the &f-link-FUNCTION: or &f-link-env-FUNCTION; element. + the &f-link-FUNCTION; or &f-link-env-FUNCTION; element. @@ -92,6 +80,8 @@ Construction variable example: +.. code-block:: xml + This is the summary description of a construction variable. @@ -111,6 +101,8 @@ Tool example: +.. code-block:: xml + This is the summary description of an SCons Tool. @@ -156,12 +148,13 @@ # Namespace for schema instances xsi = "http://www.w3.org/2001/XMLSchema-instance" -# Header comment with copyright +# Header comment with copyright (unused at present) copyright_comment = """ -__COPYRIGHT__ +SPDX-FileCopyrightText: Copyright The SCons Foundation (https://scons.org) +SPDX-License-Identifier: MIT +SPDX-FileType: DOCUMENTATION This file is processed by the bin/SConsDoc.py module. -See its docstring for a discussion of the format. """ def isSConsXml(fpath): @@ -216,12 +209,11 @@ def __init__(self, name_, uri_): self.name = name_ self.uri = uri_ - def getEntityString(self): - txt = """ - %(perc)s%(name)s; -""" % {'perc': perc, 'name': self.name, 'uri': self.uri} - - return txt + def getEntityString(self) -> str: + return f"""\ + + %{self.name}; +""" class DoctypeDeclaration: @@ -241,7 +233,7 @@ def addEntity(self, name, uri): self.entries.append(DoctypeEntity(name, uri)) def createDoctype(self): - content = '\n' @@ -292,7 +284,7 @@ def setAttribute(node, att, value): @staticmethod def getText(root): return root.text - + @staticmethod def appendCvLink(root, key, lntail): linknode = etree.Entity('cv-link-' + key) @@ -328,15 +320,15 @@ def writeTree(root, fpath): def prettyPrintFile(fpath): with open(fpath,'rb') as fin: tree = etree.parse(fin) - pretty_content = etree.tostring(tree, encoding="utf-8", + pretty_content = etree.tostring(tree, encoding="utf-8", pretty_print=True) - + with open(fpath,'wb') as fout: fout.write(pretty_content) @staticmethod def decorateWithHeader(root): - root.attrib["{"+xsi+"}schemaLocation"] = "%s %s/scons.xsd" % (dbxsd, dbxsd) + root.attrib["{"+xsi+"}schemaLocation"] = f"{dbxsd} {dbxsd}/scons.xsd" return root def newXmlTree(self, root): @@ -358,22 +350,22 @@ def validateXml(fpath, xmlschema_context): try: doc = etree.parse(fpath) except Exception as e: - print("ERROR: %s fails to parse:"%fpath) + print(f"ERROR: {fpath} fails to parse:") print(e) return False doc.xinclude() try: TreeFactory.xmlschema.assertValid(doc) except etree.XMLSchemaValidateError as e: - print("ERROR: %s fails to validate:" % fpath) + print(f"ERROR: {fpath} fails to validate:") print(e) print(e.error_log.last_error.message) - print("In file: [%s]" % e.error_log.last_error.filename) + print(f"In file: [{e.error_log.last_error.filename}]") print("Line : %d" % e.error_log.last_error.line) return False except Exception as e: - print("ERROR: %s fails to validate:" % fpath) + print(f"ERROR: {fpath} fails to validate:") print(e) return False @@ -383,14 +375,14 @@ def validateXml(fpath, xmlschema_context): def findAll(root, tag, ns=None, xp_ctxt=None, nsmap=None): expression = ".//{%s}%s" % (nsmap[ns], tag) if not ns or not nsmap: - expression = ".//%s" % tag + expression = f".//{tag}" return root.findall(expression) @staticmethod def findAllChildrenOf(root, tag, ns=None, xp_ctxt=None, nsmap=None): expression = "./{%s}%s/*" % (nsmap[ns], tag) if not ns or not nsmap: - expression = "./%s/*" % tag + expression = f"./{tag}/*" return root.findall(expression) @staticmethod @@ -434,8 +426,6 @@ def __del__(self): if self.xpath_context is not None: self.xpath_context.xpathFreeContext() -perc = "%" - def validate_all_xml(dpaths, xsdfile=default_xsd): xmlschema_context = etree.parse(xsdfile) @@ -455,10 +445,7 @@ def validate_all_xml(dpaths, xsdfile=default_xsd): fails = [] fpaths = sorted(fpaths) for idx, fp in enumerate(fpaths): - fpath = os.path.join(path, fp) - print("%.2f%s (%d/%d) %s" % (float(idx + 1) * 100.0 /float(len(fpaths)), - perc, idx + 1, len(fpaths), fp)) - + print(f"{(idx + 1) / len(fpaths):7.2%} ({idx + 1}/{len(fpaths)}) {fp}") if not tf.validateXml(fp, xmlschema_context): fails.append(fp) continue @@ -518,7 +505,7 @@ def __str__(self): result = [] for m in re.findall(r'([a-zA-Z/_]+|[^a-zA-Z/_]+)', s): if ' ' in m: - m = '"%s"' % m + m = f'"{m}"' result.append(m) return ' '.join(result) def append(self, data): diff --git a/bin/SConsExamples.py b/bin/SConsExamples.py index e7a20ce2ea..90205fef27 100644 --- a/bin/SConsExamples.py +++ b/bin/SConsExamples.py @@ -1,90 +1,136 @@ -# !/usr/bin/env python +#!/usr/bin/env python # -# Copyright (c) 2010 The SCons Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# SPDX-FileCopyrightText: Copyright The SCons Foundation (https://scons.org) +# SPDX-License-Identifier: MIT -# -# -# This script looks for some XML tags that describe SCons example -# configurations and commands to execute in those configurations, and -# uses TestCmd.py to execute the commands and insert the output from -# those commands into the XML that we output. This way, we can run a -# script and update all of our example documentation output without -# a lot of laborious by-hand checking. -# -# An "SCons example" looks like this, and essentially describes a set of -# input files (program source files as well as SConscript files): -# -# -# -# env = Environment() -# env.Program('foo') -# -# -# int main(void) { printf("foo.c\n"); } -# -# -# -# The contents within the tag will get written -# into a temporary directory whenever example output needs to be -# generated. By default, the contents are not inserted into text -# directly, unless you set the "printme" attribute on one or more files, -# in which case they will get inserted within a tag. -# This makes it easy to define the example at the appropriate -# point in the text where you intend to show the SConstruct file. -# -# Note that you should usually give the a "name" -# attribute so that you can refer to the example configuration later to -# run SCons and generate output. -# -# If you just want to show a file's contents without worry about running -# SCons, there's a shorter tag: -# -# -# env = Environment() -# env.Program('foo') -# -# -# This is essentially equivalent to , -# but it's more straightforward. -# -# SCons output is generated from the following sort of tag: -# -# -# scons -Q foo -# scons -Q foo -# -# -# You tell it which example to use with the "example" attribute, and then -# give it a list of tags to execute. You can also -# supply an "os" tag, which specifies the type of operating system this -# example is intended to show; if you omit this, default value is "posix". -# -# The generated XML will show the command line (with the appropriate -# command-line prompt for the operating system), execute the command in -# a temporary directory with the example files, capture the standard -# output from SCons, and insert it into the text as appropriate. -# Error output gets passed through to your error output so you -# can see if there are any problems executing the command. -# +""" +SCons Example Generator +======================= + +Generate example outputs for the SCons documentation (primarily +the User Guide) by processing custom XML tags that describe example SCons +projects. The generator automates tasks that would otherwise require +considerable manual effort: verifying that example projects run correctly, +and capturing their output for documentation. Conceptually this is a +bit like Python ``doctest``, but file-based rather than snippet-based. + +An example consists of three parts: + +#. Project files (``scons_example`` tag) +#. Build commands (``scons_output_command`` tag) +#. Generated output (``scons_output`` tag) + +Here's a minimal example project: + +.. code-block:: xml + + + + env = Environment() + env.Program('foo') + + + int main(void) { printf("foo.c\\n"); } + + + +The example project's ``name`` attribute provides a handle for later +associating the output with this project. + +Each ``file`` tag describes the contents of a file to be +created when setting up the project, and each ``directory`` tag +describes a directory to be created. +Both take a ``name`` attribute. +The optional ``chmod`` attribute can be used if the created file +or directory needs something other than default permissions, +usually to make a file executable. +The ``printme`` attribute indicates whether a file's contents +should be shown in the documentation. +Any file with a ``printme`` value of ``1`` will have contents +generated for inclusion; ``SConstruct`` and other SConscript +files will normally set this. +The default is to not show, since often the contents of the +source files is needed to make the build work, +but is not often not that interesting when illustrating an SCons concept. + +To show just an SConstruct file, use the shorthand ``sconstruct`` tag: + +.. code-block:: xml + + + env = Environment() + env.Program('foo') + + +This is equivalent to: + +.. code-block:: xml + + + + ...contents... + + + +The ``scons_example_file`` tag allows you to display the contents +of a file outside the context of its definition in ``scons_example``. +This looks like: + +.. code-block:: xml + + + +Link an example project to its output using the ``scons_output`` tag: + +.. code-block:: xml + + + scons -Q foo + + +The ``example`` attribute associates the project of that name with the output. +The ``os`` attribute can be used to indicate a specific platform +(for example, to display a suitable shell prompt). The default is ``posix``. +The ``suffix`` attribute allows tracking outputs from multiple +ways of running a project. The optional ``tools`` attribute gives a +non-default tool list for this run. + +An ``scons_output_command`` tag inside an ``scons_output`` contains +the instructions to build the project. There can be several build +commands defined for a given example project: + +.. code-block:: xml + + + scons -Q foo + + + scons -Q --option foo + + +The command's ``environment`` attribute can be used to set environment +variables before the command is run. The ``output`` attribute can be +used to emit commentary in the output display that is not produced +by the command itself, for example: + +.. code-block:: xml + + edit hello.h + +The actual command text has some special recognized values: + +* ``scons`` - to run scons +* ``touch`` - to update the file change time +* ``edit`` - to change a file's contents without changing its behavior (works for C / C++) +* ``ls`` - generate a directory listing +* ``sleep`` - delay for a while + +The generator will: +1. Show the OS-appropriate command prompt +2. Execute the command in a temporary directory +3. Capture SCons standard output for the documentation +4. Pass through error output for troubleshooting +""" import os import re @@ -286,7 +332,7 @@ def ensureExampleOutputsExist(dpath): key + '_' + r.name.replace("/", "_")) # Write file with open(fpath, 'w') as f: - f.write("%s\n" % content) + f.write(f"{content}\n") perc = "%" @@ -309,9 +355,7 @@ def createAllExampleOutputs(dpath): for key, value in examples.items(): # Process all scons_output tags - print("%.2f%s (%d/%d) %s" % (float(idx + 1) * 100.0 / float(total), - perc, idx + 1, total, key)) - + print(f"{(idx + 1) / total:7.2%} ({idx + 1}/{total}) {key}") create_scons_output(value) # Process all scons_example_file tags for r in value.files: @@ -322,7 +366,7 @@ def createAllExampleOutputs(dpath): key + '_' + r.name.replace("/", "_")) # Write file with open(fpath, 'w') as f: - f.write("%s\n" % content) + f.write(f"{content}\n") idx += 1 def collectSConsExampleNames(fpath): @@ -347,7 +391,7 @@ def collectSConsExampleNames(fpath): if n not in suffixes: suffixes[n] = [] else: - print("Error: Example in file '%s' is missing a name!" % fpath) + print(f"Error: Example in file '{fpath}' is missing a name.") failed_suffixes = True for o in stf.findAll(t.root, "scons_output", SConsDoc.dbxid, @@ -356,11 +400,11 @@ def collectSConsExampleNames(fpath): if stf.hasAttribute(o, 'example'): n = stf.getAttribute(o, 'example') else: - print("Error: scons_output in file '%s' is missing an example name!" % fpath) + print(f"Error: scons_output in file '{fpath}' is missing an example name.") failed_suffixes = True if n not in suffixes: - print("Error: scons_output in file '%s' is referencing non-existent example '%s'!" % (fpath, n)) + print(f"Error: scons_output in file '{fpath}' is referencing non-existent example '{n}'.") failed_suffixes = True continue @@ -368,13 +412,13 @@ def collectSConsExampleNames(fpath): if stf.hasAttribute(o, 'suffix'): s = stf.getAttribute(o, 'suffix') else: - print("Error: scons_output in file '%s' (example '%s') is missing a suffix!" % (fpath, n)) + print(f"Error: scons_output in file '{fpath}' (example '{n}') is missing a suffix.") failed_suffixes = True if s not in suffixes[n]: suffixes[n].append(s) else: - print("Error: scons_output in file '%s' (example '%s') is using a duplicate suffix '%s'!" % (fpath, n, s)) + print(f"Error: scons_output in file '{fpath}' (example '{n}') is using a duplicate suffix '{s}'.") failed_suffixes = True return names, failed_suffixes @@ -395,7 +439,7 @@ def exampleNamesAreUnique(dpath): unique = False i = allnames.intersection(names) if i: - print("Not unique in %s are: %s" % (fpath, ', '.join(i))) + print(f"Not unique in {fpath} are: {', '.join(i)}") unique = False allnames |= names @@ -549,7 +593,7 @@ def process(source_file, ofp): elif line[:11] != "STRIP CCCOM": ofp.write(line) - with open(str(target[0]), "w") as fp: + with open(target[0], "w") as fp: for src in map(str, source): process(src, fp) fp.write('debug = ' + ARGUMENTS.get('debug', '0') + '\\n') diff --git a/bin/scons-diff.py b/bin/scons-diff.py index b1b2eec4ed..a60337afeb 100644 --- a/bin/scons-diff.py +++ b/bin/scons-diff.py @@ -90,8 +90,10 @@ def simple_diff(a, b, fromfile='', tofile='', output like the simple, unadorned 'diff" command. """ sm = difflib.SequenceMatcher(None, a, b) + def comma(x1, x2): - return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2) + return x1 + 1 == str(x2) if x2 else '%s,%s' % (x1 + 1, x2) + result = [] for op, a1, a2, b1, b2 in sm.get_opcodes(): if op == 'delete': diff --git a/bin/scons-time.py b/bin/scons-time.py index c8121fb6f2..d81d4c4e00 100644 --- a/bin/scons-time.py +++ b/bin/scons-time.py @@ -260,6 +260,8 @@ def redirect_to_file(command, log): def tee_to_file(command, log): return '%s 2>&1 | tee %s' % (command, log) +def makedict(**kw): + return kw class SConsTimer: """ @@ -278,9 +280,6 @@ class SConsTimer: name = 'scons-time' name_spaces = ' ' * len(name) - def makedict(**kw): - return kw - default_settings = makedict( chdir=None, config_file=None, diff --git a/bin/update-release-info.py b/bin/update-release-info.py index c911fca38f..4f7cf40035 100644 --- a/bin/update-release-info.py +++ b/bin/update-release-info.py @@ -173,7 +173,7 @@ def set_new_date(self): Mon, 05 Jun 2010 21:17:15 -0700 NEW DATE WILL BE INSERTED HERE """ - min = (time.daylight and time.altzone or time.timezone) // 60 + min = (time.altzone if time.daylight else time.timezone) // 60 hr = min // 60 min = -(min % 60 + hr * 100) self.new_date = (time.strftime('%a, %d %b %Y %X', self.release_date + (0, 0, 0)) diff --git a/doc/SConscript b/doc/SConscript index a15c71e87f..57ce85c0a9 100644 --- a/doc/SConscript +++ b/doc/SConscript @@ -582,6 +582,9 @@ else: if 'man' in targets: for m in man_page_list: + manpage = os.path.join(build, 'man', m) + tar_deps.append(manpage) + tar_list.append(manpage) man, _1 = os.path.splitext(m) pdf = os.path.join(build, 'PDF', f'{man}-man.pdf') diff --git a/doc/generated/builders.gen b/doc/generated/builders.gen index 4febbcfe77..cb04e7ccdb 100644 --- a/doc/generated/builders.gen +++ b/doc/generated/builders.gen @@ -47,7 +47,7 @@ This is useful for "one-off" builds where a full Builder is not needed. Since the anonymous Builder is never hooked into the standard Builder framework, -an Action must always be specfied. +an Action must always be specified. See the &f-link-Command; function description for the calling syntax and details. @@ -474,12 +474,12 @@ in this case. If the command line option is given, the target directory will be prefixed by the directory path specified. -This is useful to test installs without installing to +This is useful to test installation behavior without installing to a "live" location in the system. -See also &FindInstalledFiles;. +See also &f-link-FindInstalledFiles;. For more thoughts on installation, see the User Guide (particularly the section on Command-Line Targets and the chapters on Installing Files and on Alias Targets). @@ -642,8 +642,8 @@ env.Java(target='classes', source=['File1.java', 'File2.java']) In this case, the user must specify the LANG environment variable to tell the compiler what encoding is used. - For portibility, it's best if the encoding is hard-coded - so that the compile will work if it is done on a system + For portability, it's best if the encoding is hard-coded, + so that the compilation works when run on a system with a different encoding. @@ -787,8 +787,12 @@ env.Moc('foo.cpp') # generates foo.moc MOFiles() env.MOFiles() -This builder belongs to &t-link-msgfmt; tool. The builder compiles +This builder is set up by the &t-link-msgfmt; tool. +The builder compiles PO files to MO files. +&b-MOFiles; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. @@ -796,19 +800,17 @@ This builder belongs to &t-link-msgfmt; tool. The builder compiles Create pl.mo and en.mo by compiling pl.po and en.po: - - # ... - env.MOFiles(['pl', 'en']) - + +env.MOFiles(['pl', 'en']) + Example 2. Compile files for languages defined in LINGUAS file: - - # ... - env.MOFiles(LINGUAS_FILE = 1) - + +env.MOFiles(LINGUAS_FILE=True) + Example 3. @@ -816,21 +818,19 @@ Create pl.mo and en.mo by compiling pl.po and en.po plus files for languages defined in LINGUAS file: - - # ... - env.MOFiles(['pl', 'en'], LINGUAS_FILE = 1) - + +env.MOFiles(['pl', 'en'], LINGUAS_FILE=True) + Example 4. -Compile files for languages defined in LINGUAS file +Compile files for languages defined in LINGUAS file (another version): - - # ... - env['LINGUAS_FILE'] = 1 - env.MOFiles() - + +env['LINGUAS_FILE'] = True +env.MOFiles() + @@ -1075,7 +1075,7 @@ Compile files for languages defined in LINGUAS file "build" from the Visual Studio interface) you lose the direct control of target selection and command-line options you would have if launching the build directly from &SCons;, - because these will be hardcoded in the project file to the + because these will be hard-coded in the project file to the values specified in the &b-MSVSProject; call. You can regain some of this control by defining multiple variants, using multiple &b-MSVSProject; calls to arrange different build @@ -1088,7 +1088,7 @@ Compile files for languages defined in LINGUAS file it is important not to set Visual Studio to do parallel builds, as it will then launch the separate project builds in parallel, and &SCons; does not work well if called that way. - Instead you can set up the &SCons; build for parallel building - + Instead, you can set up the &SCons; build for parallel building - see the &f-link-SetOption; function for how to do this with num_jobs. @@ -1234,7 +1234,7 @@ V10DebugSettings = { } # -# 3. Select the dictionary you want depending on the version of visual Studio +# 3. Select the dictionary you want depending on the version of Visual Studio # Files you want to generate. # if not env.GetOption('userfile'): @@ -1337,6 +1337,74 @@ env.MSVSProject( + + In addition to the mandatory arguments above, the following optional + values may be specified as keyword arguments: + + + + auto_filter_projects + + + Under certain circumstances, solution file names or + solution file nodes may be present in the + projects argument list. + When solution file names or nodes are present in the + projects argument list, the generated + solution file may contain erroneous Project records resulting + in VS IDE error messages when opening the generated solution + file. + By default, an exception is raised when a solution file + name or solution file node is detected in the + projects argument list. + + + The accepted values for auto_filter_projects + are: + + + + None + + + An exception is raised when a solution file name or solution + file node is detected in the projects + argument list. + + + None is the default value. + + + + + True or evaluates True + + + Automatically remove solution file names and solution file + nodes from the projects argument list. + + + + + False or evaluates False + + + Leave the solution file names and solution file nodes + in the projects argument list. + An exception is not raised. + + + When opening the generated solution file with the VS IDE, + the VS IDE will likely report that there are erroneous + Project records that are not supported or that need to be + modified. + + + + + + + Example Usage: env.MSVSSolution( @@ -1442,7 +1510,7 @@ env = Environment(tools=['default', 'packaging']) &SCons; can build packages in a number of well known packaging formats. The target package type may be selected with the -the &cv-link-PACKAGETYPE; construction variable +&cv-link-PACKAGETYPE; construction variable or the command line option. The package type may be a list, in which case &SCons; will attempt to build packages for each type in the list. Example: @@ -1460,7 +1528,7 @@ The currently supported packagers are: msiMicrosoft Installer package -rpmRPM Package Manger package +rpmRPM Package Manager package ipkgItsy Package Management package tarbz2bzip2-compressed tar file targzgzip-compressed tar file @@ -1606,41 +1674,48 @@ or The suffix specified by the &cv-link-PDFSUFFIX; construction variable (.pdf by default) is added automatically to the target -if it is not already present. Example: +if it is not already present. +&b-PDF; is a single-source builder. +Example: - + # builds from aaa.tex env.PDF(target = 'aaa.pdf', source = 'aaa.tex') # builds bbb.pdf from bbb.dvi env.PDF(target = 'bbb', source = 'bbb.dvi') - + POInit() env.POInit() -This builder belongs to &t-link-msginit; tool. The builder initializes missing -PO file(s) if &cv-link-POAUTOINIT; is set. If -&cv-link-POAUTOINIT; is not set (default), &b-POInit; prints instruction for -user (that is supposed to be a translator), telling how the -PO file should be initialized. In normal projects +This builder is set up by the &t-link-msginit; tool. +The builder initializes missing +PO file(s) if &cv-link-POAUTOINIT; is set. +If &cv-link-POAUTOINIT; is not set (the default), +&b-POInit; prints instruction for the user (such as a translator), +telling how the PO file should be initialized. +In normal projects you should not use &b-POInit; and use &b-link-POUpdate; instead. &b-link-POUpdate; chooses intelligently between msgmerge(1) and msginit(1). &b-POInit; always uses msginit(1) and should be regarded as builder for special purposes or for temporary use (e.g. for quick, one time initialization of a bunch of PO files) or for tests. +&b-POInit; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. Target nodes defined through &b-POInit; are not built by default (they're Ignored from '.' node) but are added to -special Alias ('po-create' by default). +special &f-link-Alias; ('po-create' by default). The alias name may be changed through the &cv-link-POCREATE_ALIAS; -construction variable. All PO files defined through -&b-POInit; may be easily initialized by scons po-create. +&consvar;. All PO files defined through +&b-POInit; may be easily initialized by scons po-create. @@ -1648,31 +1723,27 @@ construction variable. All PO files defined through Initialize en.po and pl.po from messages.pot: - - # ... - env.POInit(['en', 'pl']) # messages.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl']) # messages.pot --> [en.po, pl.po] + Example 2. Initialize en.po and pl.po from foo.pot: - - # ... - env.POInit(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.po] + Example 3. Initialize en.po and pl.po from -foo.pot but using &cv-link-POTDOMAIN; construction -variable: +foo.pot but using the &cv-link-POTDOMAIN; &consvar;: - - # ... - env.POInit(['en', 'pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] - + +env.POInit(['en', 'pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] + Example 4. @@ -1680,10 +1751,9 @@ Initialize PO files for languages defined in LINGUAS file. The files will be initialized from template messages.pot: - - # ... - env.POInit(LINGUAS_FILE = 1) # needs 'LINGUAS' file - + +env.POInit(LINGUAS_FILE=True) # needs 'LINGUAS' file + Example 5. @@ -1692,30 +1762,27 @@ Initialize en.po and pl.pl LINGUAS file. The files will be initialized from template messages.pot: - - # ... - env.POInit(['en', 'pl'], LINGUAS_FILE = 1) - + +env.POInit(['en', 'pl'], LINGUAS_FILE=True) + Example 6. You may preconfigure your environment first, and then initialize PO files: - - # ... - env['POAUTOINIT'] = 1 - env['LINGUAS_FILE'] = 1 - env['POTDOMAIN'] = 'foo' - env.POInit() - + +env['POAUTOINIT'] = True +env['LINGUAS_FILE'] = True +env['POTDOMAIN'] = 'foo' +env.POInit() + which has same efect as: - - # ... - env.POInit(POAUTOINIT = 1, LINGUAS_FILE = 1, POTDOMAIN = 'foo') - + +env.POInit(POAUTOINIT=True, LINGUAS_FILE=True, POTDOMAIN='foo') + @@ -1731,32 +1798,37 @@ or The suffix specified by the &cv-link-PSSUFFIX; construction variable (.ps by default) is added automatically to the target -if it is not already present. Example: +if it is not already present. +&b-PostScript; is a single-source builder. +Example: - + # builds from aaa.tex env.PostScript(target = 'aaa.ps', source = 'aaa.tex') # builds bbb.ps from bbb.dvi env.PostScript(target = 'bbb', source = 'bbb.dvi') - + POTUpdate() env.POTUpdate() -The builder belongs to &t-link-xgettext; tool. The builder updates target -POT file if exists or creates one if it doesn't. The node is -not built by default (i.e. it is Ignored from -'.'), but only on demand (i.e. when given -POT file is required or when special alias is invoked). This -builder adds its targe node (messages.pot, say) to a +The builder is set up by the &t-link-xgettext; tool, +part of the &t-link-gettext; toolset. +The builder updates the target +POT file if exists or creates it if it doesn't. +The target node is not +selected for building by default (e.g. scons .), +but only on demand (i.e. when the given +POT file is required or when special alias is invoked). +This builder adds its target node (messages.pot, say) to a special alias (pot-update by default, see &cv-link-POTUPDATE_ALIAS;) so you can update/create them easily with -scons pot-update. The file is not written until there is no +scons pot-update. The file is not written until there is no real change in internationalized messages (or in comments that enter -POT file). +POT file). @@ -1774,86 +1846,87 @@ not. Let's create po/ directory and place following SConstruct script there: - - # SConstruct in 'po/' subdir - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(['foo'], ['../a.cpp', '../b.cpp']) - env.POTUpdate(['bar'], ['../c.cpp', '../d.cpp']) - + +# SConstruct in 'po/' subdir +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(['foo'], ['../a.cpp', '../b.cpp']) +env.POTUpdate(['bar'], ['../c.cpp', '../d.cpp']) + Then invoke scons few times: - - user@host:$ scons # Does not create foo.pot nor bar.pot - user@host:$ scons foo.pot # Updates or creates foo.pot - user@host:$ scons pot-update # Updates or creates foo.pot and bar.pot - user@host:$ scons -c # Does not clean foo.pot nor bar.pot. - + +$ scons # Does not create foo.pot nor bar.pot +$ scons foo.pot # Updates or creates foo.pot +$ scons pot-update # Updates or creates foo.pot and bar.pot +$ scons -c # Does not clean foo.pot nor bar.pot. + the results shall be as the comments above say. Example 2. -The &b-POTUpdate; builder may be used with no target specified, in which -case default target messages.pot will be used. The -default target may also be overridden by setting &cv-link-POTDOMAIN; construction -variable or providing it as an override to &b-POTUpdate; builder: - - - # SConstruct script - env = Environment( tools = ['default', 'xgettext'] ) - env['POTDOMAIN'] = "foo" - env.POTUpdate(source = ["a.cpp", "b.cpp"]) # Creates foo.pot ... - env.POTUpdate(POTDOMAIN = "bar", source = ["c.cpp", "d.cpp"]) # and bar.pot - +The target argument can be omitted, in which +case the default target name messages.pot is used. +The target may also be overridden by setting the &cv-link-POTDOMAIN; +&consvar; or providing it as an override to the &b-POTUpdate; builder: + + +# SConstruct script +env = Environment(tools=['default', 'xgettext']) +env['POTDOMAIN'] = "foo" +env.POTUpdate(source=["a.cpp", "b.cpp"]) # Creates foo.pot ... +env.POTUpdate(POTDOMAIN="bar", source=["c.cpp", "d.cpp"]) # and bar.pot + Example 3. -The sources may be specified within separate file, for example +The source parameter may also be omitted, +if it is specified in a separate file, for example POTFILES.in: - - # POTFILES.in in 'po/' subdirectory - ../a.cpp - ../b.cpp - # end of file - + +# POTFILES.in in 'po/' subdirectory +../a.cpp +../b.cpp +# end of file + The name of the file (POTFILES.in) containing the list of sources is provided via &cv-link-XGETTEXTFROM;: - - # SConstruct file in 'po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in') - + +# SConstruct file in 'po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in') + Example 4. -You may use &cv-link-XGETTEXTPATH; to define source search path. Assume, for -example, that you have files a.cpp, +You can use &cv-link-XGETTEXTPATH; to define the source search path. +Assume, for example, that you have files a.cpp, b.cpp, po/SConstruct, -po/POTFILES.in. Then your POT-related -files could look as below: +po/POTFILES.in. +Then your POT-related files could look like this: - - # POTFILES.in in 'po/' subdirectory - a.cpp - b.cpp - # end of file - + +# POTFILES.in in 'po/' subdirectory +a.cpp +b.cpp +# end of file + - - # SConstruct file in 'po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH='../') - + +# SConstruct file in 'po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH='../') + Example 5. -Multiple search directories may be defined within a list, i.e. -XGETTEXTPATH = ['dir1', 'dir2', ...]. The order in the list +Multiple search directories may be defined as a list, i.e. +XGETTEXTPATH=['dir1', 'dir2', ...]. The order in the list determines the search order of source files. The path to the first file found is used. @@ -1861,47 +1934,47 @@ is used. Let's create 0/1/po/SConstruct script: - - # SConstruct file in '0/1/po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../', '../../']) - + +# SConstruct file in '0/1/po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../', '../../']) + and 0/1/po/POTFILES.in: - - # POTFILES.in in '0/1/po/' subdirectory - a.cpp - # end of file - + +# POTFILES.in in '0/1/po/' subdirectory +a.cpp +# end of file + Write two *.cpp files, the first one is 0/a.cpp: - - /* 0/a.cpp */ - gettext("Hello from ../../a.cpp") - + +/* 0/a.cpp */ +gettext("Hello from ../../a.cpp") + and the second is 0/1/a.cpp: - - /* 0/1/a.cpp */ - gettext("Hello from ../a.cpp") - + +/* 0/1/a.cpp */ +gettext("Hello from ../a.cpp") + then run scons. You'll obtain 0/1/po/messages.pot with the message "Hello from ../a.cpp". When you reverse order in $XGETTEXTFOM, i.e. when you write SConscript as - - # SConstruct file in '0/1/po/' subdirectory - env = Environment( tools = ['default', 'xgettext'] ) - env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../../', '../']) - + +# SConstruct file in '0/1/po/' subdirectory +env = Environment(tools=['default', 'xgettext']) +env.POTUpdate(XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../../', '../']) + then the messages.pot will contain -msgid "Hello from ../../a.cpp" line and not +msgid "Hello from ../../a.cpp" line and not msgid "Hello from ../a.cpp". @@ -1911,23 +1984,29 @@ then the messages.pot will contain POUpdate() env.POUpdate() -The builder belongs to &t-link-msgmerge; tool. The builder updates +The builder is set up by the &t-link-msgmerge; tool. +part of the &t-link-gettext; toolset. +The builder updates PO files with msgmerge(1), or initializes -missing PO files as described in documentation of -&t-link-msginit; tool and &b-link-POInit; builder (see also -&cv-link-POAUTOINIT;). Note, that &b-POUpdate; does not add its -targets to po-create alias as &b-link-POInit; -does. +missing PO files as described in the documentation of the +&t-link-msginit; tool and the &b-link-POInit; builder (see also +&cv-link-POAUTOINIT;). +&b-POUpdate; is a single-source builder. +The source parameter +can also be omitted if &cv-link-LINGUAS_FILE; is set. -Target nodes defined through &b-POUpdate; are not built by default -(they're Ignored from '.' node). Instead, -they are added automatically to special Alias +The target nodes are not +selected for building by default (e.g. scons .). +Instead, they are added automatically to special &f-link-Alias; ('po-update' by default). The alias name may be changed -through the &cv-link-POUPDATE_ALIAS; construction variable. You can easily -update PO files in your project by scons -po-update. +through the &cv-link-POUPDATE_ALIAS; &consvar;. You can easily +update PO files in your project by +scons po-update. +Note that &b-POUpdate; does not add its +targets to the po-create alias as &b-link-POInit; +does. @@ -1937,49 +2016,44 @@ Update en.po and pl.po from assuming that the later one exists or there is rule to build it (see &b-link-POTUpdate;): - - # ... - env.POUpdate(['en','pl']) # messages.pot --> [en.po, pl.po] - + +env.POUpdate(['en','pl']) # messages.pot --> [en.po, pl.po] + Example 2. Update en.po and pl.po from foo.pot template: - - # ... - env.POUpdate(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.pl] - + +env.POUpdate(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.pl] + Example 3. Update en.po and pl.po from foo.pot (another version): - - # ... - env.POUpdate(['en', 'pl'], POTDOMAIN='foo') # foo.pot -- > [en.po, pl.pl] - + +env.POUpdate(['en', 'pl'], POTDOMAIN='foo') # foo.pot -- > [en.po, pl.pl] + Example 4. Update files for languages defined in LINGUAS file. The files are updated from messages.pot template: - - # ... - env.POUpdate(LINGUAS_FILE = 1) # needs 'LINGUAS' file - + +env.POUpdate(LINGUAS_FILE=True) # needs 'LINGUAS' file + Example 5. Same as above, but update from foo.pot template: - - # ... - env.POUpdate(LINGUAS_FILE = 1, source = ['foo']) - + +env.POUpdate(LINGUAS_FILE=True, source=['foo']) + Example 6. @@ -1987,20 +2061,19 @@ Update en.po and pl.po plus files for languages defined in LINGUAS file. The files are updated from messages.pot template: - - # produce 'en.po', 'pl.po' + files defined in 'LINGUAS': - env.POUpdate(['en', 'pl' ], LINGUAS_FILE = 1) - + +# produce 'en.po', 'pl.po' + files defined in 'LINGUAS': +env.POUpdate(['en', 'pl' ], LINGUAS_FILE=True) + Example 7. Use &cv-link-POAUTOINIT; to automatically initialize PO file if it doesn't exist: - - # ... - env.POUpdate(LINGUAS_FILE = 1, POAUTOINIT = 1) - + +env.POUpdate(LINGUAS_FILE=True, POAUTOINIT=True) + Example 8. @@ -2009,13 +2082,12 @@ Update PO files for languages defined in foo.pot template. All necessary settings are pre-configured via environment. - - # ... - env['POAUTOINIT'] = 1 - env['LINGUAS_FILE'] = 1 - env['POTDOMAIN'] = 'foo' - env.POUpdate() - + +env['POAUTOINIT'] = True +env['LINGUAS_FILE'] = True +env['POTDOMAIN'] = 'foo' +env.POUpdate() + @@ -2508,7 +2580,7 @@ are flattened. See also &b-link-Textfile;. -By default the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; +By default, the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; Examples: @@ -2531,7 +2603,7 @@ it may be either a Python dictionary or a sequence of If it is a dictionary it is converted into a list of tuples with unspecified order, so if one key is a prefix of another key -or if one substitution could be further expanded by another subsitition, +or if one substitution could be further expanded by another substitution, it is unpredictable whether the expansion will occur. @@ -2562,7 +2634,7 @@ env.Substfile('foo.in', SUBST_DICT=bad_foo) good_foo = [('$foobar', '$foobar'), ('$foo', '$foo')] env.Substfile('foo.in', SUBST_DICT=good_foo) -# UNPREDICTABLE - one substitution could be futher expanded +# UNPREDICTABLE - one substitution could be further expanded bad_bar = {'@bar@': '@soap@', '@soap@': 'lye'} env.Substfile('bar.in', SUBST_DICT=bad_bar) @@ -2654,7 +2726,7 @@ and &cv-link-TEXTFILESUFFIX; &consvars; are automatically added to the target if they are not already present. -By default the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; +By default, the target file encoding is "utf-8" and can be changed by &cv-link-FILE_ENCODING; Examples: @@ -2668,7 +2740,7 @@ env.Textfile(target='bar', source=['lalala', 'tanteratei'], LINESEPARATOR='|*') # nested lists are flattened automatically env.Textfile(target='blob', source=['lalala', ['Goethe', 42, 'Schiller'], 'tanteratei']) -# files may be used as input by wraping them in File() +# files may be used as input by wrapping them in File() env.Textfile( target='concat', # concatenate files with a marker between source=[File('concat1'), File('concat2')], @@ -2705,54 +2777,56 @@ env.Textfile( Translate() env.Translate() -This pseudo-builder belongs to &t-link-gettext; toolset. The builder extracts -internationalized messages from source files, updates POT -template (if necessary) and then updates PO translations (if -necessary). If &cv-link-POAUTOINIT; is set, missing PO files +This pseudo-Builder is part of the &t-link-gettext; toolset. +The builder extracts internationalized messages from source files, +updates the POT template (if necessary) +and then updates PO translations (if necessary). +If &cv-link-POAUTOINIT; is set, missing PO files will be automatically created (i.e. without translator person intervention). The variables &cv-link-LINGUAS_FILE; and &cv-link-POTDOMAIN; are taken into -acount too. All other construction variables used by &b-link-POTUpdate;, and +account too. All other construction variables used by &b-link-POTUpdate;, and &b-link-POUpdate; work here too. Example 1. The simplest way is to specify input files and output languages inline in -a SCons script when invoking &b-Translate; +a SCons script when invoking &b-Translate;: - + # SConscript in 'po/' directory -env = Environment( tools = ["default", "gettext"] ) -env['POAUTOINIT'] = 1 -env.Translate(['en','pl'], ['../a.cpp','../b.cpp']) - +env = Environment(tools=["default", "gettext"]) +env['POAUTOINIT'] = True +env.Translate(['en', 'pl'], ['../a.cpp', '../b.cpp']) + Example 2. -If you wish, you may also stick to conventional style known from +If you wish, you may also stick to the conventional style known from autotools, i.e. using POTFILES.in and LINGUAS files +to specify the targets and sources: - + # LINGUAS -en pl -#end - +en pl +# end + - + # POTFILES.in a.cpp b.cpp # end - + - + # SConscript -env = Environment( tools = ["default", "gettext"] ) -env['POAUTOINIT'] = 1 +env = Environment(tools=["default", "gettext"]) +env['POAUTOINIT'] = True env['XGETTEXTPATH'] = ['../'] -env.Translate(LINGUAS_FILE = 1, XGETTEXTFROM = 'POTFILES.in') - +env.Translate(LINGUAS_FILE=True, XGETTEXTFROM='POTFILES.in') + The last approach is perhaps the recommended one. It allows easily split @@ -2765,7 +2839,7 @@ factor" synchronizing these two scripts is then the content of LINGUAS file. Note, that the updated POT and PO files are usually going to be committed back to the repository, so they must be updated within the source -directory (and not in variant directories). Additionaly, the file listing of +directory (and not in variant directories). Additionally, the file listing of po/ directory contains LINGUAS file, so the source tree looks familiar to translators, and they may work with the project in their usual way. @@ -2775,62 +2849,65 @@ project in their usual way. Example 3. Let's prepare a development tree as below - + project/ + SConstruct - + build/ + + build/ + src/ + po/ + SConscript + SConscript.i18n + POTFILES.in + LINGUAS - + -with build being variant directory. Write the top-level +with build being the variant directory. +Write the top-level SConstruct script as follows - - # SConstruct - env = Environment( tools = ["default", "gettext"] ) - VariantDir('build', 'src', duplicate = 0) - env['POAUTOINIT'] = 1 - SConscript('src/po/SConscript.i18n', exports = 'env') - SConscript('build/po/SConscript', exports = 'env') - + +# SConstruct +env = Environment(tools=["default", "gettext"]) +VariantDir('build', 'src', duplicate=False) +env['POAUTOINIT'] = True +SConscript('src/po/SConscript.i18n', exports='env') +SConscript('build/po/SConscript', exports='env') + the src/po/SConscript.i18n as - - # src/po/SConscript.i18n - Import('env') - env.Translate(LINGUAS_FILE=1, XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../']) - + +# src/po/SConscript.i18n +Import('env') +env.Translate(LINGUAS_FILE=True, XGETTEXTFROM='POTFILES.in', XGETTEXTPATH=['../']) + and the src/po/SConscript - - # src/po/SConscript - Import('env') - env.MOFiles(LINGUAS_FILE = 1) - + +# src/po/SConscript +Import('env') +env.MOFiles(LINGUAS_FILE=True) + -Such setup produces POT and PO files -under source tree in src/po/ and binary -MO files under variant tree in +Such a setup produces POT and PO files +under the source tree in src/po/ and binary +MO files under the variant tree in build/po/. This way the POT and PO files are separated from other output files, which must not be committed back to source repositories (e.g. MO files). - -In above example, the PO files are not updated, -nor created automatically when you issue scons '.' command. -The files must be updated (created) by hand via scons -po-update and then MO files can be compiled by -running scons '.'. - +In the above example, +the PO files are not updated, +nor created automatically when you issue the command +scons .. +The files must be updated (created) by hand via +scons po-update +and then MO files can be compiled by +running scons .. + diff --git a/doc/generated/examples/builderscommands_ex5_1.xml b/doc/generated/examples/builderscommands_ex3_1.xml similarity index 100% rename from doc/generated/examples/builderscommands_ex5_1.xml rename to doc/generated/examples/builderscommands_ex3_1.xml diff --git a/doc/generated/examples/caching_ex-random_1.xml b/doc/generated/examples/caching_ex-random_1.xml index 00041ee128..0e4663cf92 100644 --- a/doc/generated/examples/caching_ex-random_1.xml +++ b/doc/generated/examples/caching_ex-random_1.xml @@ -1,8 +1,8 @@ % scons -Q -cc -o f3.o -c f3.c -cc -o f5.o -c f5.c cc -o f4.o -c f4.c cc -o f1.o -c f1.c +cc -o f5.o -c f5.c +cc -o f3.o -c f3.c cc -o f2.o -c f2.c cc -o prog f1.o f2.o f3.o f4.o f5.o diff --git a/doc/generated/examples/commandline_BoolVariable_5.xml b/doc/generated/examples/commandline_BoolVariable_5.xml index 3cd75e9aca..f5bb77834c 100644 --- a/doc/generated/examples/commandline_BoolVariable_5.xml +++ b/doc/generated/examples/commandline_BoolVariable_5.xml @@ -1,6 +1,6 @@ % scons -Q RELEASE=bad_value foo.o -scons: *** Error converting option: RELEASE -Invalid value for boolean option: bad_value +scons: *** Error converting option: 'RELEASE' +Invalid value for boolean variable: 'bad_value' File "/home/my/project/SConstruct", line 3, in <module> diff --git a/doc/generated/examples/commandline_EnumVariable_2.xml b/doc/generated/examples/commandline_EnumVariable_2.xml index 8856d29fe2..b5b99caae5 100644 --- a/doc/generated/examples/commandline_EnumVariable_2.xml +++ b/doc/generated/examples/commandline_EnumVariable_2.xml @@ -1,5 +1,5 @@ % scons -Q COLOR=magenta foo.o -scons: *** Invalid value for option COLOR: magenta. Valid values are: ('red', 'green', 'blue') +scons: *** Invalid value for enum variable 'COLOR': 'magenta'. Valid values are: ('red', 'green', 'blue') File "/home/my/project/SConstruct", line 10, in <module> diff --git a/doc/generated/examples/commandline_EnumVariable_4.xml b/doc/generated/examples/commandline_EnumVariable_4.xml index 3e73c5b23a..0714d4bc7e 100644 --- a/doc/generated/examples/commandline_EnumVariable_4.xml +++ b/doc/generated/examples/commandline_EnumVariable_4.xml @@ -1,13 +1,13 @@ % scons -Q COLOR=Red foo.o -scons: *** Invalid value for option COLOR: Red. Valid values are: ('red', 'green', 'blue') +scons: *** Invalid value for enum variable 'COLOR': 'Red'. Valid values are: ('red', 'green', 'blue') File "/home/my/project/SConstruct", line 10, in <module> % scons -Q COLOR=BLUE foo.o -scons: *** Invalid value for option COLOR: BLUE. Valid values are: ('red', 'green', 'blue') +scons: *** Invalid value for enum variable 'COLOR': 'BLUE'. Valid values are: ('red', 'green', 'blue') File "/home/my/project/SConstruct", line 10, in <module> % scons -Q COLOR=nAvY foo.o -scons: *** Invalid value for option COLOR: nAvY. Valid values are: ('red', 'green', 'blue') +scons: *** Invalid value for enum variable 'COLOR': 'nAvY'. Valid values are: ('red', 'green', 'blue') File "/home/my/project/SConstruct", line 10, in <module> diff --git a/doc/generated/examples/commandline_ListVariable_3.xml b/doc/generated/examples/commandline_ListVariable_3.xml index 11936ab0fb..70047f21c3 100644 --- a/doc/generated/examples/commandline_ListVariable_3.xml +++ b/doc/generated/examples/commandline_ListVariable_3.xml @@ -1,6 +1,5 @@ % scons -Q COLORS=magenta foo.o -scons: *** Error converting option: COLORS -Invalid value(s) for option: magenta +scons: *** Invalid value(s) for variable 'COLORS': 'magenta'. Valid values are: blue,green,red,all,none File "/home/my/project/SConstruct", line 7, in <module> diff --git a/doc/generated/examples/commandline_ListVariable_4.xml b/doc/generated/examples/commandline_ListVariable_4.xml index d464c48396..b87dd79571 100644 --- a/doc/generated/examples/commandline_ListVariable_4.xml +++ b/doc/generated/examples/commandline_ListVariable_4.xml @@ -1,6 +1,5 @@ % scons -Q foo.o -scons: *** Error converting option: COLORS -Invalid value(s) for option: 0 +scons: *** Invalid value(s) for variable 'COLORS': '0'. Valid values are: blue,green,red,all,none File "/home/my/project/SConstruct", line 7, in <module> diff --git a/doc/generated/examples/commandline_PackageVariable_1.xml b/doc/generated/examples/commandline_PackageVariable_1.xml index 367638f371..49c6c85c7e 100644 --- a/doc/generated/examples/commandline_PackageVariable_1.xml +++ b/doc/generated/examples/commandline_PackageVariable_1.xml @@ -3,7 +3,7 @@ cc -o foo.o -c -DPACKAGE="/opt/location" foo.c % scons -Q PACKAGE=/usr/local/location foo.o cc -o foo.o -c -DPACKAGE="/usr/local/location" foo.c % scons -Q PACKAGE=yes foo.o -cc -o foo.o -c -DPACKAGE="True" foo.c +cc -o foo.o -c -DPACKAGE="/opt/location" foo.c % scons -Q PACKAGE=no foo.o cc -o foo.o -c -DPACKAGE="False" foo.c diff --git a/doc/generated/examples/commandline_PathVariable_2.xml b/doc/generated/examples/commandline_PathVariable_2.xml index 156702af16..bd85d4cfb7 100644 --- a/doc/generated/examples/commandline_PathVariable_2.xml +++ b/doc/generated/examples/commandline_PathVariable_2.xml @@ -1,5 +1,5 @@ % scons -Q CONFIG=/does/not/exist foo.o -scons: *** Path for option CONFIG does not exist: /does/not/exist +scons: *** Path for variable 'CONFIG' does not exist: /does/not/exist File "/home/my/project/SConstruct", line 7, in <module> diff --git a/doc/generated/examples/troubleshoot_Dump_1.xml b/doc/generated/examples/troubleshoot_Dump_1.xml index 54d1288d2d..b03d9327c5 100644 --- a/doc/generated/examples/troubleshoot_Dump_1.xml +++ b/doc/generated/examples/troubleshoot_Dump_1.xml @@ -62,7 +62,7 @@ scons: Reading SConscript files ... 'TEMPFILEARGESCFUNC': <function quote_spaces at 0x700000>, 'TEMPFILEARGJOIN': ' ', 'TEMPFILEPREFIX': '@', - 'TOOLS': ['install', 'install'], + 'TOOLS': ['install'], '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, ' 'TARGET, SOURCE)}', '_CPPINCFLAGS': '${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, ' diff --git a/doc/generated/examples/troubleshoot_Dump_2.xml b/doc/generated/examples/troubleshoot_Dump_2.xml index a29772a2b8..30c24d833f 100644 --- a/doc/generated/examples/troubleshoot_Dump_2.xml +++ b/doc/generated/examples/troubleshoot_Dump_2.xml @@ -106,8 +106,7 @@ scons: Reading SConscript files ... 'TEMPFILEARGESCFUNC': <function quote_spaces at 0x700000>, 'TEMPFILEARGJOIN': '\n', 'TEMPFILEPREFIX': '@', - 'TOOLS': ['msvc', 'install', 'install'], - 'VSWHERE': None, + 'TOOLS': ['msvc', 'install'], '_CCCOMCOM': '$CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $CCPCHFLAGS $CCPDBFLAGS', '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, ' 'TARGET, SOURCE)}', diff --git a/doc/generated/examples/troubleshoot_Dump_ENV_1.xml b/doc/generated/examples/troubleshoot_Dump_ENV_1.xml index 1d009115a5..92334e7ddd 100644 --- a/doc/generated/examples/troubleshoot_Dump_ENV_1.xml +++ b/doc/generated/examples/troubleshoot_Dump_ENV_1.xml @@ -1,6 +1,6 @@ % scons scons: Reading SConscript files ... -{'PATH': '/usr/local/bin:/opt/bin:/bin:/usr/bin:/snap/bin'} +{'ENV': {'PATH': '/usr/local/bin:/opt/bin:/bin:/usr/bin:/snap/bin'}} scons: done reading SConscript files. scons: Building targets ... scons: `.' is up to date. diff --git a/doc/generated/examples/troubleshoot_Dump_ENV_2.xml b/doc/generated/examples/troubleshoot_Dump_ENV_2.xml index 0879e27e88..f602dedec0 100644 --- a/doc/generated/examples/troubleshoot_Dump_ENV_2.xml +++ b/doc/generated/examples/troubleshoot_Dump_ENV_2.xml @@ -1,8 +1,8 @@ C:\>scons scons: Reading SConscript files ... -{ 'PATH': 'C:\\WINDOWS\\System32:/usr/bin', - 'PATHEXT': '.COM;.EXE;.BAT;.CMD', - 'SystemRoot': 'C:\\WINDOWS'} +{ 'ENV': { 'PATH': 'C:\\WINDOWS\\System32:/usr/bin', + 'PATHEXT': '.COM;.EXE;.BAT;.CMD', + 'SystemRoot': 'C:\\WINDOWS'}} scons: done reading SConscript files. scons: Building targets ... scons: `.' is up to date. diff --git a/doc/generated/examples/troubleshoot_stacktrace_2.xml b/doc/generated/examples/troubleshoot_stacktrace_2.xml index 0250025757..068a1e148d 100644 --- a/doc/generated/examples/troubleshoot_stacktrace_2.xml +++ b/doc/generated/examples/troubleshoot_stacktrace_2.xml @@ -1,9 +1,9 @@ % scons -Q --debug=stacktrace scons: *** [prog.o] Source `prog.c' not found, needed by target `prog.o'. scons: internal stack trace: - File "SCons/Taskmaster/Job.py", line 671, in _work + File "SCons/Taskmaster/Job.py", line 670, in _work task.prepare() - File "SCons/Script/Main.py", line 201, in prepare + File "SCons/Script/Main.py", line 209, in prepare return SCons.Taskmaster.OutOfDateTask.prepare(self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "SCons/Taskmaster/__init__.py", line 195, in prepare diff --git a/doc/generated/examples/troubleshoot_taskmastertrace_1.xml b/doc/generated/examples/troubleshoot_taskmastertrace_1.xml index 70f3fbb9f1..8fa6b5e159 100644 --- a/doc/generated/examples/troubleshoot_taskmastertrace_1.xml +++ b/doc/generated/examples/troubleshoot_taskmastertrace_1.xml @@ -1,8 +1,8 @@ % scons -Q --taskmastertrace=- prog -Job.NewParallel._work(): [Thread:8149967104] Gained exclusive access -Job.NewParallel._work(): [Thread:8149967104] Starting search -Job.NewParallel._work(): [Thread:8149967104] Found 0 completed tasks to process -Job.NewParallel._work(): [Thread:8149967104] Searching for new tasks +Job.NewParallel._work(): [Thread:8286867328] Gained exclusive access +Job.NewParallel._work(): [Thread:8286867328] Starting search +Job.NewParallel._work(): [Thread:8286867328] Found 0 completed tasks to process +Job.NewParallel._work(): [Thread:8286867328] Searching for new tasks Taskmaster: Looking for a node to evaluate Taskmaster: Considering node <no_state 0 'prog'> and its children: @@ -18,12 +18,12 @@ Taskmaster: Evaluating <pending 0 'prog.c'> Task.make_ready_current(): node <pending 0 'prog.c'> Task.prepare(): node <up_to_date 0 'prog.c'> -Job.NewParallel._work(): [Thread:8149967104] Found internal task +Job.NewParallel._work(): [Thread:8286867328] Found internal task Task.executed_with_callbacks(): node <up_to_date 0 'prog.c'> Task.postprocess(): node <up_to_date 0 'prog.c'> Task.postprocess(): removing <up_to_date 0 'prog.c'> Task.postprocess(): adjusted parent ref count <pending 1 'prog.o'> -Job.NewParallel._work(): [Thread:8149967104] Searching for new tasks +Job.NewParallel._work(): [Thread:8286867328] Searching for new tasks Taskmaster: Looking for a node to evaluate Taskmaster: Considering node <no_state 0 'inc.h'> and its children: @@ -31,12 +31,12 @@ Taskmaster: Evaluating <pending 0 'inc.h'> Task.make_ready_current(): node <pending 0 'inc.h'> Task.prepare(): node <up_to_date 0 'inc.h'> -Job.NewParallel._work(): [Thread:8149967104] Found internal task +Job.NewParallel._work(): [Thread:8286867328] Found internal task Task.executed_with_callbacks(): node <up_to_date 0 'inc.h'> Task.postprocess(): node <up_to_date 0 'inc.h'> Task.postprocess(): removing <up_to_date 0 'inc.h'> Task.postprocess(): adjusted parent ref count <pending 0 'prog.o'> -Job.NewParallel._work(): [Thread:8149967104] Searching for new tasks +Job.NewParallel._work(): [Thread:8286867328] Searching for new tasks Taskmaster: Looking for a node to evaluate Taskmaster: Considering node <pending 0 'prog.o'> and its children: @@ -46,19 +46,19 @@ Taskmaster: Evaluating <pending 0 'prog.o'> Task.make_ready_current(): node <pending 0 'prog.o'> Task.prepare(): node <executing 0 'prog.o'> -Job.NewParallel._work(): [Thread:8149967104] Found task requiring execution -Job.NewParallel._work(): [Thread:8149967104] Executing task +Job.NewParallel._work(): [Thread:8286867328] Found task requiring execution +Job.NewParallel._work(): [Thread:8286867328] Executing task Task.execute(): node <executing 0 'prog.o'> cc -o prog.o -c -I. prog.c -Job.NewParallel._work(): [Thread:8149967104] Enqueueing executed task results -Job.NewParallel._work(): [Thread:8149967104] Gained exclusive access -Job.NewParallel._work(): [Thread:8149967104] Starting search -Job.NewParallel._work(): [Thread:8149967104] Found 1 completed tasks to process +Job.NewParallel._work(): [Thread:8286867328] Enqueueing executed task results +Job.NewParallel._work(): [Thread:8286867328] Gained exclusive access +Job.NewParallel._work(): [Thread:8286867328] Starting search +Job.NewParallel._work(): [Thread:8286867328] Found 1 completed tasks to process Task.executed_with_callbacks(): node <executing 0 'prog.o'> Task.postprocess(): node <executed 0 'prog.o'> Task.postprocess(): removing <executed 0 'prog.o'> Task.postprocess(): adjusted parent ref count <pending 0 'prog'> -Job.NewParallel._work(): [Thread:8149967104] Searching for new tasks +Job.NewParallel._work(): [Thread:8286867328] Searching for new tasks Taskmaster: Looking for a node to evaluate Taskmaster: Considering node <pending 0 'prog'> and its children: @@ -67,21 +67,21 @@ Taskmaster: Evaluating <pending 0 'prog'> Task.make_ready_current(): node <pending 0 'prog'> Task.prepare(): node <executing 0 'prog'> -Job.NewParallel._work(): [Thread:8149967104] Found task requiring execution -Job.NewParallel._work(): [Thread:8149967104] Executing task +Job.NewParallel._work(): [Thread:8286867328] Found task requiring execution +Job.NewParallel._work(): [Thread:8286867328] Executing task Task.execute(): node <executing 0 'prog'> cc -o prog prog.o -Job.NewParallel._work(): [Thread:8149967104] Enqueueing executed task results -Job.NewParallel._work(): [Thread:8149967104] Gained exclusive access -Job.NewParallel._work(): [Thread:8149967104] Starting search -Job.NewParallel._work(): [Thread:8149967104] Found 1 completed tasks to process +Job.NewParallel._work(): [Thread:8286867328] Enqueueing executed task results +Job.NewParallel._work(): [Thread:8286867328] Gained exclusive access +Job.NewParallel._work(): [Thread:8286867328] Starting search +Job.NewParallel._work(): [Thread:8286867328] Found 1 completed tasks to process Task.executed_with_callbacks(): node <executing 0 'prog'> Task.postprocess(): node <executed 0 'prog'> -Job.NewParallel._work(): [Thread:8149967104] Searching for new tasks +Job.NewParallel._work(): [Thread:8286867328] Searching for new tasks Taskmaster: Looking for a node to evaluate Taskmaster: No candidate anymore. -Job.NewParallel._work(): [Thread:8149967104] Found no task requiring execution, and have no jobs: marking complete -Job.NewParallel._work(): [Thread:8149967104] Gained exclusive access -Job.NewParallel._work(): [Thread:8149967104] Completion detected, breaking from main loop +Job.NewParallel._work(): [Thread:8286867328] Found no task requiring execution, and have no jobs: marking complete +Job.NewParallel._work(): [Thread:8286867328] Gained exclusive access +Job.NewParallel._work(): [Thread:8286867328] Completion detected, breaking from main loop diff --git a/doc/generated/functions.gen b/doc/generated/functions.gen index 4a5a39119f..72d3aa4c30 100644 --- a/doc/generated/functions.gen +++ b/doc/generated/functions.gen @@ -26,13 +26,12 @@ for a complete explanation of the arguments and behavior. Note that the &f-env-Action; form of the invocation will expand -construction variables in any argument strings, +&consvars; in any argument strings, including the action argument, at the time it is called -using the construction variables in the -env -construction environment through which +using the &consvars; in the +&consenv; through which &f-env-Action; was called. The &f-Action; global function form delays all variable expansion @@ -84,16 +83,23 @@ env.other_method_name('another arg') - AddOption(arguments) + AddOption(opt_str, ..., attr=value, ...) Adds a local (project-specific) command-line option. -arguments -are the same as those supported by the add_option -method in the standard Python library module optparse, -with a few additional capabilities noted below. -See the documentation for -optparse -for a thorough discussion of its option-processing capabities. +One or more opt_str values are +the strings representing how the option can be called, +while the keyword arguments define attributes of the option. +For the most part these are the same as for the +OptionParser.add_option +method in the standard Python library module +optparse, +but with a few additional capabilities noted below. +See the + +optparse documentation +for a thorough discussion of its option-processing capabilities. +All options added through &f-AddOption; are placed +in a special "Local Options" option group. @@ -104,10 +110,9 @@ method, &f-AddOption; allows setting the nargs keyword value to -a string consisting of a question mark -('?') +a string '?' (question mark) to indicate that the option argument for -that option string is optional. +that option string may be omitted. If the option string is present on the command line but has no matching option argument, the value of the @@ -149,21 +154,25 @@ the option value may be accessed using &f-link-GetOption; or &f-link-env-GetOption;. -&f-link-SetOption; is not currently supported for -options added with &f-AddOption;. - +override a value set in an SConscript file. + + + +Changed in 4.8.0: added the +settable keyword argument +to enable an added option to be settable via &SetOption;. @@ -242,7 +251,7 @@ Future versions of &SCons; will likely forbid such usage. AddPostAction(target, action) env.AddPostAction(target, action) -Arranges for the specified +Arrange for the specified action to be performed after the specified @@ -268,13 +277,19 @@ foo = Program('foo.c') AddPostAction(foo, Chmod('$TARGET', "a-x")) + +If a target is an &f-Alias;, +action is associated with the +action of the alias, if specified. + + AddPreAction(target, action) env.AddPreAction(target, action) -Arranges for the specified +Arrange for the specified action to be performed before the specified @@ -297,38 +312,46 @@ one or more targets in the list. Note that if any of the targets are built in multiple steps, the action will be invoked just -before the "final" action that specifically +before the action step that specifically generates the specified target(s). -For example, when building an executable program -from a specified source -.c -file via an intermediate object file: +It may not always be obvious +if the process is multi-step - for example, +if you use the &Program; builder to +construct an executable program from a +.c source file, +&scons; builds an intermediate object file first; +the pre-action is invoked after this step +and just before the link command to +generate the executable program binary. +Example: foo = Program('foo.c') -AddPreAction(foo, 'pre_action') +AddPreAction(foo, 'echo "Running pre-action"') + +$ scons -Q +gcc -o foo.o -c foo.c +echo "Running pre-action" +Running pre-action +gcc -o foo foo.o + + -The specified -pre_action -would be executed before -&scons; -calls the link command that actually -generates the executable program binary -foo, -not before compiling the -foo.c -file into an object file. +If a target is an &f-Alias;, +action is associated with the +action of the alias, if specified. + Alias(alias, [source, [action]]) env.Alias(alias, [source, [action]]) -Creates an alias target that +Create an Alias node that can be used as a reference to zero or more other targets, specified by the optional source parameter. Aliases provide a way to give a shorter or more descriptive @@ -395,16 +418,16 @@ env.Alias('update', ['file1', 'file2'], "update_database $SOURCES") AllowSubstExceptions([exception, ...]) -Specifies the exceptions that will be allowed -when expanding construction variables. +Specifies the exceptions that will be ignored +when expanding &consvars;. By default, -any construction variable expansions that generate a -NameError +any &consvar; expansions that generate a +&NameError; or -IndexError +&IndexError; exception will expand to a '' -(an empty string) and not cause scons to fail. +(an empty string) and not cause &scons; to fail. All exceptions not in the specified list will generate an error message and terminate processing. @@ -415,7 +438,8 @@ If &f-AllowSubstExceptions; is called multiple times, each call completely overwrites the previous list -of allowed exceptions. +of ignored exceptions. +Calling it with no arguments means no exceptions will be ignored. @@ -429,7 +453,7 @@ Example: AllowSubstExceptions() # Also allow a string containing a zero-division expansion -# like '${1 / 0}' to evalute to ''. +# like '${1 / 0}' to evaluate to ''. AllowSubstExceptions(IndexError, NameError, ZeroDivisionError) @@ -440,7 +464,7 @@ AllowSubstExceptions(IndexError, NameError, ZeroDivisionError) Marks each given target -so that it is always assumed to be out of date, +so that it is always assumed to be out-of-date, and will always be rebuilt if needed. Note, however, that &f-AlwaysBuild; @@ -495,15 +519,15 @@ named key, then key is simply stored with a value of val. Otherwise, val is -combinined with the existing value, +combined with the existing value, possibly converting into an appropriate type which can hold the expanded contents. There are a few special cases to be aware of. Normally, when two strings are combined, the result is a new string containing their concatenation (and you are responsible for supplying any needed separation); -however, the contents of &cv-link-CPPDEFINES; will -will be postprocessed by adding a prefix and/or suffix +however, the contents of &cv-link-CPPDEFINES; +will be post-processed by adding a prefix and/or suffix to each entry when the command line is produced, so &SCons; keeps them separate - appending a string will result in a separate string entry, @@ -619,7 +643,7 @@ scons: `.' is up to date. Changed in version 4.5: -clarifined the use of tuples vs. other types, +clarified the use of tuples vs. other types, handling is now consistent across the four functions. @@ -646,7 +670,7 @@ See &cv-link-CPPDEFINES; for more details. Appending a string val -to a dictonary-typed &consvar; enters +to a dictionary-typed &consvar; enters val as the key in the dictionary, and None as its value. Using a tuple type to supply a key-value pair @@ -773,14 +797,14 @@ for a complete explanation of the arguments and behavior. Note that the env.Builder() form of the invocation will expand -construction variables in any arguments strings, +&consvars; in any arguments strings, including the action argument, at the time it is called -using the construction variables in the +using the &consvars; in the env -construction environment through which +&consenv; through which &f-env-Builder; was called. The &f-Builder; @@ -811,12 +835,12 @@ disables derived file caching. Calling the environment method &f-link-env-CacheDir; limits the effect to targets built -through the specified construction environment. +through the specified &consenv;. Calling the global function &f-link-CacheDir; sets a global default that will be used by all targets built -through construction environments +through &consenvs; that do not set up environment-specific caching by calling &f-env-CacheDir;. @@ -917,44 +941,33 @@ to arrange for cache pruning, expiry, access control, etc. if needed. - Clean(targets, files_or_dirs) - env.Clean(targets, files_or_dirs) + Clean(targets, files) + env.Clean(targets, files) -This specifies a list of files or directories which should be removed -whenever the targets are specified with the - -command line option. -The specified targets may be a list -or an individual target. -Multiple calls to -&f-Clean; -are legal, -and create new targets or add files and directories to the -clean list for the specified targets. - - - -Multiple files or directories should be specified -either as separate arguments to the -&f-Clean; -method, or as a list. -&f-Clean; -will also accept the return value of any of the construction environment -Builder methods. -Examples: +Set additional files +for removal when any of +targets are selected +for cleaning +( +command line option). +targets and +files +can each be a single filename or node, +or a list of filenames or nodes. +These can refer to files or directories. +Calling this method repeatedly +has an additive effect. The related &f-link-NoClean; -function overrides calling -&f-Clean; -for the same target, -and any targets passed to both functions will -not -be removed by the - -option. +method has higher priority: +any target specified to +&f-NoClean; +will not be cleaned even if also given as +a files +parameter to &f-Clean;. @@ -968,10 +981,13 @@ Clean(['foo', 'bar'], 'something_else_to_clean') +&SCons; does not directly track directories as targets - +they are created if needed and not normally removed +in clean mode. In this example, installing the project creates a subdirectory for the documentation. -This statement causes the subdirectory to be removed -if the project is deinstalled. +The &f-Clean; call ensures that the subdirectory is removed +if the project is uninstalled. Clean(docdir, os.path.join(docdir, projectname)) @@ -981,11 +997,12 @@ Clean(docdir, os.path.join(docdir, projectname)) env.Clone([key=val, ...]) -Returns a separate copy of a construction environment. -If there are any keyword arguments specified, -they are added to the returned copy, +Returns an independent copy of a &consenv;. +If there are any unrecognized keyword arguments specified, +they are added as &consvars; in the copy, overwriting any existing values -for the keywords. +for those keywords. +See the manpage section "Construction Environments" for more details. @@ -998,8 +1015,9 @@ env3 = env.Clone(CCFLAGS='-g') -Additionally, a list of tools and a toolpath may be specified, as in -the &f-link-Environment; constructor: +A list of tools +and a toolpath may be specified, +as in the &f-link-Environment; constructor: @@ -1012,7 +1030,7 @@ env4 = env.Clone(tools=['msvc', MyTool]) The parse_flags -keyword argument is also recognized to allow merging command-line +keyword argument is also recognized, to allow merging command-line style arguments into the appropriate construction variables (see &f-link-env-MergeFlags;). @@ -1021,6 +1039,17 @@ variables (see &f-link-env-MergeFlags;). # create an environment for compiling programs that use wxWidgets wx_env = env.Clone(parse_flags='!wx-config --cflags --cxxflags') + + +The variables +keyword argument is also recognized, to allow (re)initializing +&consvars; from a Variables object. + + + +Changed in version 4.8.0: +the variables parameter was added. + @@ -1124,7 +1153,7 @@ env.Command( import os def rename(env, target, source): - os.rename('.tmp', str(target[0])) + os.rename('.tmp', target[0]) env.Command( @@ -1183,13 +1212,14 @@ for a complete explanation of the arguments and behavior. DebugOptions([json]) -Allows setting options for SCons debug options. Currently the only supported value is - json which sets the path to the json file created when +Allows setting options for SCons debug options. Currently, the only supported value is + json which sets the path to the JSON file created when --debug=json is set. DebugOptions(json='#/build/output/scons_stats.json') +New in version 4.6.0. @@ -1197,13 +1227,11 @@ DebugOptions(json='#/build/output/scons_stats.json') env.Decider(function) Specifies that all up-to-date decisions for -targets built through this construction environment -will be handled by the specified -function. +targets built through this &consenv; +will be handled by function. function can be the name of a function or one of the following strings -that specify the predefined decision function -that will be applied: +that specify a predefined decider function: @@ -1212,7 +1240,7 @@ that will be applied: "content" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's content has changed since the last time the target was built, as determined by performing a checksum @@ -1234,7 +1262,7 @@ can still be used as a synonym, but is deprecated. "content-timestamp" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's content has changed since the last time the target was built, except that dependencies with a timestamp that matches @@ -1271,7 +1299,7 @@ can still be used as a synonym, but is deprecated. "timestamp-newer" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's timestamp is newer than the target file's timestamp. This is the behavior of the classic Make utility, and @@ -1285,7 +1313,7 @@ can be used a synonym for "timestamp-match" -Specifies that a target shall be considered out of date and rebuilt +Specifies that a target shall be considered out-of-date and rebuilt if the dependency's timestamp is different than the timestamp recorded the last time the target was built. This provides behavior very similar to the classic Make utility @@ -1310,7 +1338,7 @@ Examples: Decider('timestamp-match') # Use hash content signatures for any targets built -# with the attached construction environment. +# with the attached &consenv;. env.Decider('content') @@ -1331,7 +1359,7 @@ The Node (file) which should cause the target to be rebuilt -if it has "changed" since the last tme +if it has "changed" since the last time target was built. @@ -1406,7 +1434,7 @@ otherwise not be rebuilt). Note that the decision can be made -using whatever criteria are appopriate. +using whatever criteria are appropriate. Ignoring some or all of the function arguments is perfectly normal. @@ -1480,41 +1508,51 @@ env.Default(hello) - DefaultEnvironment([**kwargs]) + DefaultEnvironment([key=value, ...]) Instantiates and returns the global &consenv; object. -This environment is used internally by SCons -when it executes many of the global functions listed in this section -(that is, those not called as methods of a specific &consenv;). -The &defenv; is a singleton: -the keyword arguments are used only on the first call; -on subsequent calls the already-constructed object is returned +The &DefEnv; is used internally by &SCons; +when executing a global function +or the global form of a Builder method +that requires access to a &consenv;. + + + +On the first call, +arguments are interpreted as for the &f-link-Environment; function. +The &DefEnv; is a singleton; +subsequent calls to &f-DefaultEnvironment; return +the already-constructed object, and any keyword arguments are silently ignored. -The &defenv; can still be modified after instantiation -in the same way as any other &consenv;. -The &defenv; is independent: -modifying it has no effect on any other &consenv; -constructed by an &f-link-Environment; or &f-link-Clone; call. - - - -It is not mandatory to call &f-DefaultEnvironment;: -the &defenv; is instantiated automatically when the -build phase begins if this function has not been called; -however calling it explicitly gives the opportunity to -affect and examine the contents of the &defenv;. -Instantiation happens even if no build instructions -appar to use it, as there are internal uses. -If there are no uses in the project &SConscript; files, -a small performance gain may be seen by calling -&f-DefaultEnvironment; with an empty tools list, -thus avoiding that part of the initialization cost. -This is mainly of interest in testing when &scons; is -launched repeatedly in a short time period: - -DefaultEnvironment(tools=[]) - + + +The &DefEnv; can be modified after instantiation, +similar to other &consenvs;, +although some &consenv; methods may be unavailable. +Modifying the &DefEnv; has no effect on any other &consenv;, +either existing or newly constructed. + + + +It is not necessary to explicitly call &f-DefaultEnvironment;. +&SCons; instantiates the &defenv; automatically when the +build phase begins, if has not already been done. +However, calling it explicitly provides the opportunity to +affect and examine its contents. +Instantiation occurs even if nothing in the build system +appears to use it, due to internal uses. + + + +If the project &SConscript; files do not use global functions or Builders, +a small performance gain may be achieved by calling +&f-DefaultEnvironment; with an empty tools list +(DefaultEnvironment(tools=[])). +This avoids the tool initialization cost for the &DefEnv;, +which is mainly of interest in the test suite +where &scons; is launched repeatedly in a short time period. + @@ -1582,14 +1620,20 @@ but will not include any such extension in the return value. - env.Dictionary([vars]) + env.Dictionary([var, ...], [as_dict=]) -Returns a dictionary object -containing the &consvars; in the &consenv;. -If there are any arguments specified, -the values of the specified &consvars; -are returned as a string (if one -argument) or as a list of strings. +Return an object containing &consvars; from +env. +If var is omitted, +all the &consvars; with their values +are returned in a dict. +If var is specified, +and as_dict is true, +the specified &consvars; are returned in a dict; +otherwise (the default, for backwards compatibility), +values only are returned, +as a scalar if one var is given, +or as a list if multiples. @@ -1600,6 +1644,24 @@ Example: cvars = env.Dictionary() cc_values = env.Dictionary('CC', 'CCFLAGS', 'CCCOM') + + +The object returned by &f-link-env-Dictionary; should be treated +as a read-only view into the &consvars;. +Some &consvars; require special internal handling, +and modifying them through the &f-env-Dictionary; object can bypass +that handling and cause data inconsistencies. +The primary use of &f-env-Dictionary; is for diagnostic purposes - +it is used widely by test cases specifically because +it bypasses the special handling so that behavior +can be verified. + + + +Changed in 4.9.0: +as_dict added. + + @@ -1640,19 +1702,28 @@ for more information. - env.Dump([key], [format]) + env.Dump([var, ...], [format=TYPE]) -Serializes &consvars; to a string. -The method supports the following formats specified by -format: +Serialize &consvars; from env to a string. +If var is omitted, +all the &consvars; are serialized. +If one or more var values are supplied, +only those variables and their values are serialized. + + + +The optional format string +selects the serialization format: + pretty -Returns a pretty printed representation of the environment (if -format -is not specified, this is the default). +Returns a pretty-printed representation +of the &consvars; - the result will look like a +&Python; dict +(this is the default). @@ -1660,38 +1731,47 @@ is not specified, this is the default). json -Returns a JSON-formatted string representation of the environment. +Returns a JSON-formatted representation of the variables. +The variables will be presented as a JSON object literal, +the JSON equivalent of a &Python; dict.. -If key is -None (the default) the entire -dictionary of &consvars; is serialized. -If supplied, it is taken as the name of a &consvar; -whose value is serialized. + +Changed in 4.9.0: +More than one key can be specified. +The returned string always looks like a dict +(or equivalent in other formats); +previously a single key serialized only the value, +not the key with the value. -This SConstruct: +Examples: this &SConstruct; -env=Environment() +env = Environment() print(env.Dump('CCCOM')) +print(env.Dump('CC', 'CCFLAGS', format='json')) -will print: +will print something like: -'$CC -c -o $TARGET $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $SOURCES' +{'CCCOM': '$CC -o $TARGET -c $CFLAGS $CCFLAGS $_CCCOMCOM $SOURCES'} +{ + "CC": "gcc", + "CCFLAGS": [] +} -While this SConstruct: +While this &SConstruct;: @@ -1700,7 +1780,7 @@ print(env.Dump()) -will print: +will print something like: { 'AR': 'ar', @@ -1711,6 +1791,7 @@ will print: 'ASFLAGS': [], ... + @@ -1762,7 +1843,7 @@ EnsureSConsVersion(0,96,90) Environment([key=value, ...]) env.Environment([key=value, ...]) -Return a new construction environment +Return a new &consenv; initialized with the specified key=value pairs. @@ -1772,7 +1853,8 @@ The keyword arguments toolpath, tools and variables -are also specially recognized. +are specially recognized and do not lead to +&consvar; creation. See the manpage section "Construction Environments" for more details. @@ -2116,7 +2198,7 @@ FindSourceFiles('src') -As you can see build support files (SConstruct in the above example) +As you can see, build support files (&SConstruct; in the above example) will also be returned by this function. @@ -2324,9 +2406,10 @@ file is found. env.GetOption(name) Query the value of settable options which may have been set -on the command line, or by using the &f-link-SetOption; function. +on the command line, via option defaults, +or by using the &f-link-SetOption; function. The value of the option is returned in a type matching how the -option was declared - see the documentation for the +option was declared - see the documentation of the corresponding command line option for information about each specific option. @@ -2570,7 +2653,7 @@ processed prior to the &f-GetOption; call in the &SConscript; files. Returns the current SCons version in the form of a Tuple[int, int, int], representing the major, minor, and revision values respectively. -Added in 4.7.1. +Added in 4.8.0. @@ -2587,8 +2670,8 @@ or (most commonly) relative to the directory of the current &f-Glob; matches both files stored on disk and Nodes which &SCons; already knows about, even if any corresponding file is not currently stored on disk. -The evironment method form (&f-env-Glob;) -performs string substition on +The environment method form (&f-env-Glob;) +performs string substitution on pattern and returns whatever matches the resulting expanded pattern. The results are sorted, unlike for the similar &Python; @@ -2679,7 +2762,7 @@ If the optional source argument evaluates true, and the local directory is a variant directory, -then &f-Glob; returnes Nodes from +then &f-Glob; returns Nodes from the corresponding source directory, rather than the local directory. @@ -2713,7 +2796,7 @@ directory.) The optional exclude argument may be set to a pattern or a list of patterns -descibing files or directories +describing files or directories to filter out of the match list. Elements matching a least one specified pattern will be excluded. These patterns use the same syntax as for @@ -2858,7 +2941,7 @@ Import("*") The specified string will be preserved as-is -and not have construction variables expanded. +and not have &consvars; expanded. @@ -2907,7 +2990,7 @@ In case of duplication, any &consvar; names that end in PATH keep the left-most value so the -path searcb order is not altered. +path search order is not altered. All other &consvars; keep the right-most value. If unique is false, @@ -2957,7 +3040,7 @@ either as separate arguments to the &f-NoCache; method, or as a list. &f-NoCache; -will also accept the return value of any of the construction environment +will also accept the return value of any of the &consenv; Builder methods. @@ -2979,34 +3062,20 @@ NoCache(env.Program('hello', 'hello.c')) - NoClean(target, ...) - env.NoClean(target, ...) + NoClean(targets, ...) + env.NoClean(targets, ...) -Specifies a list of files or directories which should -not -be removed whenever the targets (or their dependencies) -are specified with the - -command line option. -The specified targets may be a list -or an individual target. -Multiple calls to -&f-NoClean; -are legal, -and prevent each specified target -from being removed by calls to the - -option. - - - -Multiple files or directories should be specified -either as separate arguments to the -&f-NoClean; -method, or as a list. -&f-NoClean; -will also accept the return value of any of the construction environment -Builder methods. +Specifies files or directories which should not +be removed whenever a specified +target (or its dependencies) +is selected and clean mode is active +( +command line option). +targets +may be one or more file or directory names +or nodes, +and/or lists of names or nodes. +&f-NoClean; can be called multiple times. @@ -3015,11 +3084,9 @@ Calling for a target overrides calling &f-link-Clean; for the same target, -and any targets passed to both functions will +so any targets passed to both functions will not -be removed by the - -option. +be removed in clean mode. @@ -3046,7 +3113,7 @@ is omitted or None, &f-link-env-MergeFlags; is used. By default, duplicate values are not -added to any construction variables; +added to any &consvars;; you can specify unique=False to allow duplicate values to be added. @@ -3069,11 +3136,11 @@ the output produced by command in order to distribute it to appropriate &consvars;. &f-env-MergeFlags; uses a separate function to do that processing - -see &f-link-env-ParseFlags; for the details, including a -a table of options and corresponding construction variables. +see &f-link-env-ParseFlags; for the details, including +a table of options and corresponding &consvars;. To provide alternative processing of the output of command, -you can suppply a custom +you can supply a custom function, which must accept three arguments: the &consenv; to modify, @@ -3146,12 +3213,12 @@ function. Parses one or more strings containing typical command-line flags for GCC-style tool chains and returns a dictionary with the flag values -separated into the appropriate SCons construction variables. +separated into the appropriate SCons &consvars;. Intended as a companion to the &f-link-env-MergeFlags; method, but allows for the values in the returned dictionary to be modified, if necessary, -before merging them into the construction environment. +before merging them into the &consenv;. (Note that &f-env-MergeFlags; will call this method if its argument is not a dictionary, @@ -3180,7 +3247,7 @@ See &f-link-ParseConfig; for more details. Flag values are translated according to the prefix found, -and added to the following construction variables: +and added to the following &consvars;: @@ -3220,8 +3287,7 @@ and added to the following construction variables: Any other strings not associated with options are assumed to be the names of libraries and added to the -&cv-LIBS; -construction variable. +&cv-LIBS; &consvar;. @@ -3246,7 +3312,7 @@ selected by plat (defaults to the detected platform for the current system) that can be used to initialize -a construction environment by passing it as the +a &consenv; by passing it as the platform keyword argument to the &f-link-Environment; function. @@ -3292,7 +3358,7 @@ Returns a list of the affected target nodes. env.Prepend(key=val, [...]) Prepend values to &consvars; in the current &consenv;, -Works like &f-link-env-Append; (see for details), +works like &f-link-env-Append; (see for details), except that values are added to the front, rather than the end, of any existing value of the &consvar; @@ -3497,7 +3563,7 @@ If the string contains the verbatim substring it will be replaced with the Node. Note that, for performance reasons, this is not -a regular SCons variable substition, +a regular SCons variable substitution, so you can not use other variables or use curly braces. The following example will print the name of @@ -3597,7 +3663,7 @@ env = Environment( env.Replace(key=val, [...]) -Replaces construction variables in the Environment +Replaces &consvars; in the Environment with the specified keyword arguments. @@ -3614,50 +3680,43 @@ env.Replace(CCFLAGS='-g', FOO='foo.xxx') Repository(directory) env.Repository(directory) -Specifies that +Sets directory -is a repository to be searched for files. +as a repository to be searched for files contributing to the build. Multiple calls to &f-Repository; -are legal, -and each one adds to the list of -repositories that will be searched. +are allowed, +with repositories searched in the given order. +Repositories specified via command-line option +have higher priority. -To +In &scons;, -a repository is a copy of the source tree, -from the top-level directory on down, -which may contain -both source files and derived files +a repository is partial or complete copy of the source tree, +from the top-level directory down, +containing source files that can be used to build targets in -the local source tree. -The canonical example would be an -official source tree maintained by an integrator. -If the repository contains derived files, -then the derived files should have been built using -&scons;, -so that the repository contains the necessary -signature information to allow -&scons; -to figure out when it is appropriate to -use the repository copy of a derived file, -instead of building one locally. +the current worktree. +Repositories can also contain derived files. +An example might be an official source tree maintained by an integrator. +If a repository contains derived files, +they should be the result of building with &SCons;, +so a signature database (sconsign) is present +in the repository, +allowing better decisions on whether they are +up-to-date or not. Note that if an up-to-date derived file already exists in a repository, -&scons; -will +&scons; will not make a copy in the local directory tree. -In order to guarantee that a local copy -will be made, -use the -&f-link-Local; -method. +If you need a local copy to be made, +use the &f-link-Local; method. @@ -3700,7 +3759,7 @@ env.Requires('foo', 'file-that-must-be-built-before-foo') Return to the calling SConscript, optionally returning the values of variables named in vars. -Multiple strings contaning variable names may be passed to +Multiple strings containing variable names may be passed to &f-Return;. A string containing white space is split into individual variable names. Returns the value if one variable is specified, @@ -4105,7 +4164,7 @@ in a separate .sconsign file in each directory, not in a single combined database file. -This is a backwards-compatibility meaure to support +This is a backwards-compatibility measure to support what was the default behavior prior to &SCons; 0.97 (i.e. before 2008). Use of this mode is discouraged and may be @@ -4143,7 +4202,7 @@ SConsignFile(dbm_module=dbm.gnu) env.SetDefault(key=val, [...]) -Sets construction variables to default values specified with the keyword +Sets &consvars; to default values specified with the keyword arguments if (and only if) the variables are not already set. The following statements are equivalent: @@ -4191,6 +4250,16 @@ are not settable using &f-SetOption; since those files must be read in order to find the &f-SetOption; call in the first place. + +For project-specific options (sometimes called +local options) +added via an &f-link-AddOption; call, +&f-SetOption; is available only after the +&f-AddOption; call has completed successfully, +and only if that call included the +settable=True argument. + + The settable variables with their associated command-line options are: @@ -4590,8 +4659,8 @@ Tag('file2.txt', DOC) - Tool(name, [toolpath, **kwargs]) - env.Tool(name, [toolpath, **kwargs]) + Tool(name, [toolpath, key=value, ...]) + env.Tool(name, [toolpath, key=value, ...]) Locates the tool specification module name and returns a callable tool object for that tool. @@ -4757,7 +4826,7 @@ Returns a Node object representing the specified &Python; Value Nodes can be used as dependencies of targets. If the string representation of the Value Node changes between &SCons; runs, it is considered -out of date and any targets depending it will be rebuilt. +out-of-date and any targets depending on it will be rebuilt. Since Value Nodes have no filesystem representation, timestamps are not used; the timestamp deciders perform the same content-based up to date check. @@ -4804,7 +4873,7 @@ def create(target, source, env): Writes 'prefix=$SOURCE' into the file name given as $TARGET. """ - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(b'prefix=' + source[0].get_contents() + b'\n') # Fetch the prefix= argument, if any, from the command line. @@ -4945,6 +5014,17 @@ SConscript(dirs=['build/src','build/doc']) SConscript(dirs='src', variant_dir='build/src', duplicate=0) SConscript(dirs='doc', variant_dir='build/doc', duplicate=0) + + + + Virtualenv() + +If the &SCons; process is running inside a &Python; +virtual environment, +return the path to the directory where that environment is stored, +else an empty string. +The result can be treated as a boolean value if the path is unneeded. + diff --git a/doc/generated/functions.mod b/doc/generated/functions.mod index 0fb4a354ee..f75ba297dc 100644 --- a/doc/generated/functions.mod +++ b/doc/generated/functions.mod @@ -89,6 +89,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. ValidateOptions"> Value"> VariantDir"> +Virtualenv"> WhereIs"> env.Action"> @@ -172,6 +173,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. env.ValidateOptions"> env.Value"> env.VariantDir"> +env.Virtualenv"> env.WhereIs"> For each &Builder; call that causes linking with libraries, &SCons; will add the libraries in the setting of &cv-LIBS; -in effect at that moment to the dependecy graph +in effect at that moment to the dependency graph as dependencies of the target being generated. -The library list will transformed to command line +The library list will be transformed to command-line arguments through the automatically-generated &cv-link-_LIBFLAGS; &consvar; which is constructed by @@ -4581,10 +4581,10 @@ It defaults to the current system line separator. The &cv-LINGUAS_FILE; defines file(s) containing list of additional linguas to be processed by &b-link-POInit;, &b-link-POUpdate; or &b-link-MOFiles; builders. It also affects &b-link-Translate; builder. If the variable contains -a string, it defines name of the list file. The &cv-LINGUAS_FILE; may be a +a string, it defines the name of the list file. The &cv-LINGUAS_FILE; may be a list of file names as well. If &cv-LINGUAS_FILE; is set to -True (or non-zero numeric value), the list will be read from -default file named +a non-string truthy value, the list will be read from +the file named LINGUAS. @@ -4830,7 +4830,7 @@ See &t-link-msgfmt; tool and &b-link-MOFiles; builder. MSGFMTCOMSTR -String to display when msgfmt(1) is invoked +String to display when msgfmt(1) is invoked (default: '', which means ``print &cv-link-MSGFMTCOM;''). See &t-link-msgfmt; tool and &b-link-MOFiles; builder. @@ -4852,7 +4852,7 @@ See &t-link-msgfmt; tool and &b-link-MOFiles; builder. Path to msginit(1) program (found via -Detect()). +&f-link-Detect;). See &t-link-msginit; tool and &b-link-POInit; builder. @@ -4872,8 +4872,9 @@ See &t-link-msginit; tool and &b-link-POInit; builder. MSGINITCOMSTR -String to display when msginit(1) is invoked -(default: '', which means ``print &cv-link-MSGINITCOM;''). +String to display when msginit(1) is invoked. +The default is an empty string, +which will print the command line (&cv-link-MSGINITCOM;). See &t-link-msginit; tool and &b-link-POInit; builder. @@ -4928,8 +4929,9 @@ See &t-link-msgmerge; tool and &b-link-POUpdate; builder. MSGMERGECOMSTR -String to be displayed when msgmerge(1) is invoked -(default: '', which means ``print &cv-link-MSGMERGECOM;''). +String to be displayed when msgmerge(1) is invoked. +The default is an empty string, +which will print the command line (&cv-link-MSGMERGECOM;). See &t-link-msgmerge; tool and &b-link-POUpdate; builder. @@ -4968,7 +4970,7 @@ Supported versions include 6.0A, 6.0, 2003R2 -and +and 2003R1. @@ -5979,41 +5981,51 @@ If the specified version is unavailable and/or unknown to &SCons;, a warning is issued showing the versions actually discovered, and the build will eventually fail indicating a missing compiler binary. If &cv-MSVC_VERSION; is not set, &SCons; will (by default) select the -latest version of &MSVC; installed on your system. +latest version of &MSVC; installed on your system +(excluding any preview versions). + + + + + In order to take effect, &cv-MSVC_VERSION; must be set before + the initial &MSVC; compiler discovery takes place. + Discovery happens, at the latest, during the first call to the + &f-link-Environment; function, unless a tools + list is specified which excludes the entire &MSVC; toolchain - + that is, omits "defaults" + and any specific tool module that refers to parts of the toolchain + (&t-link-msvc;, &t-link-mslink;, &t-link-masm;, &t-link-midl; + and &t-link-msvs;). In this case, detection is deferred until + any one of those tool modules is invoked manually. + The following two examples illustrate this: + + + +# MSVC_VERSION set as Environment is created +env = Environment(MSVC_VERSION='14.2') + +# Initialization deferred with empty tools, triggered manually +env = Environment(tools=[]) +env['MSVC_VERSION'] = '14.2 +env.Tool('msvc') +env.Tool('mslink') +env.Tool('msvs') + + + + The valid values for &cv-MSVC_VERSION; represent major versions of the compiler, except that versions ending in Exp -refer to "Express" or "Express for Desktop" Visual Studio editions, -which require distinct entries because they use a different -filesystem layout and have feature limitations compared to -the full version. +refer to "Express" or "Express for Desktop" Visual Studio editions. Values that do not look like a valid compiler version string are not supported. - -To have the desired effect, &cv-MSVC_VERSION; must be set by the -time compiler discovery takes place. -If the default tools list -or an explicit tools list including &t-link-msvc; is used, -discovery takes place as the &consenv; is created, -so passing it as an argument in the the &f-link-Environment; call -is the effective solution. -Otherwise, &cv-MSVC_VERSION; must be set before the first msvc tool is -loaded into the environment. -See the manpage section "Construction Environments" for an example. - - The following table shows the correspondence of &cv-MSVC_VERSION; values to various version indicators ('x' is used as a placeholder for a single digit that can vary). -Note that it is not necessary to install Visual Studio -to build with &SCons; (for example, you can install only -Build Tools), but if Visual Studio is installed, -additional builders such as &b-link-MSVSSolution; and -&b-link-MSVSProject; become available and will -correspond to the indicated versions. @@ -6063,10 +6075,10 @@ Visual Studio "14.1Exp" - 14.1 - 1910 + 14.1 or 14.1x + 191x Visual Studio 2017 Express - 15.0 + 15.x "14.0" @@ -6177,6 +6189,31 @@ Visual Studio + + + + + It is not necessary to install a Visual Studio IDE + to build with &SCons; (for example, you can install only + Build Tools), but when a Visual Studio IDE is installed, + additional builders such as &b-link-MSVSSolution; and + &b-link-MSVSProject; become available and correspond to + the specified versions. + + + + Versions ending in Exp refer to historical + "Express" or "Express for Desktop" Visual Studio editions, + which had feature limitations compared to the full editions. + It is only necessary to specify the Exp + suffix to select the express edition when both express and + non-express editions of the same product are installed + simultaneously. The Exp suffix is unnecessary, + but accepted, when only the express edition is installed. + + + + The compilation environment can be further or more precisely specified through the use of several other &consvars;: see the descriptions of @@ -6333,7 +6370,7 @@ and &cv-link-MSVC_USE_SETTINGS;. SccProjectFilePathRelativizedFromConnection[i] (where [i] ranges from 0 to the number of projects in the solution) attributes of the GlobalSection(SourceCodeControl) - section of the Microsoft Visual Studio solution file. Similarly + section of the Microsoft Visual Studio solution file. Similarly, the relative solution file path is placed as the values of the SccLocalPath[i] (where [i] ranges from 0 to the number of projects in the solution) attributes of the @@ -6398,7 +6435,7 @@ and &cv-link-MSVC_USE_SETTINGS;. and &cv-MSVC_VERSION; is not, &cv-MSVC_VERSION; will be initialized to the value of &cv-MSVS_VERSION;. - An error is raised if If both are set and have different values, + An error is raised if both are set and have different values. @@ -6651,7 +6688,7 @@ scons NINJA_CMD_ARGS="-v -j 3" Determines the type of format ninja should expect when parsing header include depfiles. Can be , , or . - The option corresponds to format, and + The option corresponds to format, and or correspond to . @@ -6724,7 +6761,7 @@ scons NINJA_CMD_ARGS="-v -j 3" NINJA_FORCE_SCONS_BUILD - If true, causes the build nodes to callback to scons instead of using + If true, causes the build nodes to call back to scons instead of using &ninja; to build them. This is intended to be passed to the environment on the builder invocation. It is useful if you have a build node which does something which is not easily translated into &ninja;. @@ -6794,7 +6831,7 @@ scons NINJA_CMD_ARGS="-v -j 3" Internal value used to specify the function to call with argument env to generate the list of files - which if changed would require the &ninja; build file to be regenerated. + which, if changed, would require the &ninja; build file to be regenerated. @@ -6803,7 +6840,7 @@ scons NINJA_CMD_ARGS="-v -j 3" NINJA_SCONS_DAEMON_KEEP_ALIVE - The number of seconds for the SCons deamon launched by ninja to stay alive. + The number of seconds for the SCons daemon launched by ninja to stay alive. (Default: 180000) @@ -6881,7 +6918,7 @@ placed if applicable. The default value is &cv-NAME;-&cv-VERSION; Selects the package type to build when using the &b-link-Package; -builder. May be a string or list of strings. See the docuentation +builder. It may be a string or list of strings. See the documentation for the builder for the currently supported types. @@ -6965,7 +7002,7 @@ only if the &cv-link-PDB; &consvar; is set. This variable specifies how much of a source file is precompiled. This variable is ignored by tools other than &MSVC;, or when -the PCH variable is not being used. When this variable is define it +the PCH variable is not being used. When this variable is defined, it must be a string that is the name of the header that is included at the end of the precompiled portion of the source files, or the empty string if the "#pragma hrdstop" construct is being used: @@ -8259,7 +8296,7 @@ See also &cv-link-CXXFLAGS; for compiling to static objects. The name of the compiler to use when compiling D source -destined to be in a shared objects. +destined to be in a shared object. See also &cv-link-DC; for compiling to static objects. @@ -8971,7 +9008,7 @@ When this &consvar; is defined, a versioned shared library is created by the &b-link-SharedLibrary; builder. This activates the &cv-link-_SHLIBVERSIONFLAGS; and thus modifies the &cv-link-SHLINKCOM; as required, adds the version number to the library name, and creates the symlinks -that are needed. &cv-link-SHLIBVERSION; versions should exist as alpha-numeric, +that are needed. &cv-link-SHLIBVERSION; versions should exist as alphanumeric, decimal-delimited values as defined by the regular expression "\w+[\.\w+]*". Example &cv-link-SHLIBVERSION; values include '1', '1.2.3', and '1.2.gitaa412c8b'. @@ -9111,7 +9148,7 @@ The variable is used, for example, by &t-link-gnulink; linker tool. A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9138,7 +9175,7 @@ field in the controlling information for Ipkg and RPM packages. A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9149,7 +9186,7 @@ for more information). SOVERSION -This will construct the SONAME using on the base library name +This will construct the SONAME using on the base library name (test in the example below) and use specified SOVERSION to create SONAME. @@ -9158,7 +9195,7 @@ env.SharedLibrary('test', 'test.c', SHLIBVERSION='0.1.2', SOVERSION='2') The variable is used, for example, by &t-link-gnulink; linker tool. -In the example above SONAME would be libtest.so.2 +In the example above SONAME would be libtest.so.2 which would be a symlink and point to libtest.so.0.1.2 @@ -9423,7 +9460,7 @@ will be looked-up relative to the SConscript directory when they are used in a command. To force &scons; -to look-up a directory relative to the root of the source tree use +to lookup a directory relative to the root of the source tree, use a top-relative path (#): @@ -9432,7 +9469,7 @@ env = Environment(SWIGPATH='#/include') -The directory look-up can also be forced using the +The directory lookup can also be forced using the &Dir;() function: @@ -9520,7 +9557,7 @@ General options passed to the tar archiver. A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9596,7 +9633,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9616,16 +9653,41 @@ The suffix used for tar file names. TEMPFILE -A callable object used to handle overly long command line strings, -since operations which call out to a shell will fail -if the line is longer than the shell can accept. -This tends to particularly impact linking. -The tempfile object stores the command line in a temporary -file in the appropriate format, and returns -an alternate command line so the invoked tool will make -use of the contents of the temporary file. -If you need to replace the default tempfile object, -the callable should take into account the settings of +Holds a callable object which will be invoked to transform long +command lines (string or list) into an alternate form. +Length limits on various operating systems +may cause long command lines to fail when calling out to +a shell to run the command. +Most often affects linking, when there are many object files and/or +libraries to be linked, but may also affect other +compilation steps which have many arguments. +&cv-TEMPFILE; is not called directly, +but rather is typically embedded in another +&consvar;, to be expanded when used. Example: + + + +env["TEMPFILE"] = TempFileMunge +env["LINKCOM"] = "${TEMPFILE('$LINK $TARGET $SOURCES', '$LINKCOMSTR')}" + + + +The SCons default value for &cv-TEMPFILE;, +TempFileMunge, +performs command substitution on the passed command line, +calculates whether modification is needed, +then puts all but the first word (assumed to be the command name) +of the resulting list into a temporary file +(sometimes called a response file or command file), +and returns a new command line consisting of the +the command name and an appropriately formatted reference +to the temporary file. + + + +A replacement for the default tempfile object would need +to do fundamentally the same thing, including taking into account +the values of &cv-link-MAXLINELENGTH;, &cv-link-TEMPFILEPREFIX;, &cv-link-TEMPFILESUFFIX;, @@ -9633,6 +9695,11 @@ the callable should take into account the settings of &cv-link-TEMPFILEDIR; and &cv-link-TEMPFILEARGESCFUNC;. +If a particular use case requires a different transformation +than the default, it is recommended to copy the mechanism and +define a new &consvar; and rewrite the relevant *COM +variable(s) to use it, to avoid possibly disrupting existing uses +of &cv-TEMPFILE;. @@ -9647,8 +9714,8 @@ If you need to apply extra operations on a command argument (to fix Windows slashes, normalize paths, etc.) before writing to the temporary file, you can set the &cv-TEMPFILEARGESCFUNC; variable to a custom function. -Such a function takes a single string argument and returns -a new string with any modifications applied. +The function must accept a single string argument and +and return a new string with any modifications applied. Example: @@ -9691,7 +9758,39 @@ Note this value is used literally and not expanded by the subst logic. The directory to create the long-lines temporary file in. +If unset, the &Python; +tempfile module chooses the directory +based on the TMPDIR, +TEMP +or TMP environment variables. +If the default path causes processing errors, +set &cv-TEMPFILEDIR; to a safer alternative. +For example, on Windows, +the default temporary file path contains the username. +If the username contains non-7-bit-ASCII characters, +there may decoding errors opening the path to the temporary file. +See also &cv-link-TEMPFILEENCODING;. + + + + + TEMPFILEENCODING + + +By default, the long-lines temporary file (aka "response file") created by the +&cv-link-TEMPFILE; function will be encoded in the &Python; +default encoding, UTF-8. +If the external command which reads the response file +encounters decoding errors +(usually, if that command depends on Windows legacy code pages, +and a pathname in the response file +or the response file path itself cannot +be represented in the 7-bit ASCII characer set), +set this variable to the appropriate codec. +See also &cv-link-TEMPFILEDIR;. + +New in version NEXT_RELEASE @@ -9701,6 +9800,8 @@ The directory to create the long-lines temporary file in. The prefix for the name of the temporary file used to store command lines exceeding &cv-link-MAXLINELENGTH;. +The prefix must include the compiler syntax to actually +include and process the file. The default prefix is '@', which works for the &MSVC; and GNU toolchains on Windows. Set this appropriately for other toolchains, @@ -9716,7 +9817,7 @@ or '-via' for ARM toolchain. The suffix for the name of the temporary file used to store command lines exceeding &cv-link-MAXLINELENGTH;. -The suffix should include the dot ('.') if one is wanted as +The suffix should include the dot ('.') if one is needed as it will not be added automatically. The default is .lnk. @@ -9815,7 +9916,7 @@ The value is informative and is not guaranteed to be complete. A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9827,7 +9928,7 @@ for more information). A reserved variable name -that may not be set or used in a construction environment. +that may not be set or used in a &consenv;. (See the manpage section "Variable Substitution" for more information). @@ -9864,48 +9965,65 @@ The version of the project, specified as a string. VSWHERE -Specify the location of vswhere.exe. +Specify the location of vswhere.exe. - The vswhere.exe executable is distributed with Microsoft Visual Studio and Build - Tools since the 2017 edition, but is also available standalone. - It provides full information about installations of 2017 and later editions. - With the argument, vswhere.exe can detect installations of the 2010 through 2015 - editions with limited data returned. -If VSWHERE is set, &SCons; will use that location. + The vswhere.exe executable is distributed with + Microsoft Visual Studio and Build Tools since the 2017 edition, + but is also available as a standalone installation. + It allows queries to obtain detailed information about + installations of 2017 and later editions. + &SCons; makes use of this information to determine + the state of compiler support for those editions. - Otherwise &SCons; will look in the following locations and set VSWHERE to the path of the first vswhere.exe -located. + Setting the &cv-VSWHERE; variable to the path to a specific + vswhere.exe binary + causes &SCons; to use that binary. + If not set, &SCons; will search for one, + looking in the following locations in order, + using the first found + (&cv-VSWHERE; is updated with the location): - -%ProgramFiles(x86)%\Microsoft Visual Studio\Installer -%ProgramFiles%\Microsoft Visual Studio\Installer -%ChocolateyInstall%\bin - + +%ProgramFiles(x86)%\Microsoft Visual Studio\Installer +%ProgramFiles%\Microsoft Visual Studio\Installer +%ChocolateyInstall%\bin +%LOCALAPPDATA%\Microsoft\WinGet\Links +%USERPROFILE%\scoop\shims +%SCOOP%\shims + - - Note that VSWHERE must be set at the same time or prior to any of &t-link-msvc;, &t-link-msvs; , and/or &t-link-mslink; &f-link-Tool; being initialized. - Either set it as follows - -env = Environment(VSWHERE='c:/my/path/to/vswhere') - + + + In order to take effect, &cv-VSWHERE; must be set before + the initial &MSVC; compiler discovery takes place. + Discovery happens, at the latest, during the first call to the + &f-link-Environment; function, unless a tools + list is specified which excludes the entire &MSVC; toolchain - + that is, omits "defaults" + and any specific tool module that refers to parts of the toolchain + (&t-link-msvc;, &t-link-mslink;, &t-link-masm;, &t-link-midl; + and &t-link-msvs;). In this case, detection is deferred until + any one of those tool modules is invoked manually. + The following two examples illustrate this: + -or if your &consenv; is created specifying an empty tools list -(or a list of tools which omits all of default, msvs, msvc, and mslink), -and also before &f-link-env-Tool; is called to ininitialize any of those tools: + +# VSWHERE set as Environment is created +env = Environment(VSWHERE='c:/my/path/to/vswhere') - - env = Environment(tools=[]) - env['VSWHERE'] = r'c:/my/vswhere/install/location/vswhere.exe' - env.Tool('msvc') - env.Tool('mslink') - env.Tool('msvs') - - +# Initialization deferred with empty tools, triggered manually +env = Environment(tools=[]) +env['VSWHERE'] = r'c:/my/vswhere/install/location/vswhere.exe' +env.Tool('msvc') +env.Tool('mslink') +env.Tool('msvs') + + @@ -10275,7 +10393,7 @@ field in the RPM X_RPM_EXTRADEFS -A list used to supply extra defintions or flags +A list used to supply extra definitions or flags to be added to the RPM .spec file. Each item is added as-is with a carriage return appended. This is useful if some specific RPM feature not otherwise @@ -10556,7 +10674,7 @@ See &t-link-xgettext; tool and &b-link-POTUpdate; builder. _XGETTEXTFROMFLAGS -Internal "macro". Genrates list of -D<dir> flags +Internal "macro". Generates list of -D<dir> flags from the &cv-link-XGETTEXTPATH; list. @@ -10567,7 +10685,7 @@ from the &cv-link-XGETTEXTPATH; list. This flag is used to add single &cv-link-XGETTEXTFROM; file to -xgettext(1)'s commandline (default: +xgettext(1)'s command line (default: '-f'). @@ -10611,7 +10729,7 @@ from &cv-link-XGETTEXTFROM;. This flag is used to add single search path to -xgettext(1)'s commandline (default: +xgettext(1)'s command line (default: '-D'). @@ -10668,7 +10786,7 @@ Various yacc tools have emitted various formats at different times. Set this to match what your parser generator produces. -New in version 4.X.Y. +New in version 4.6.0. @@ -10826,7 +10944,7 @@ The value is used only if &cv-YACC_GRAPH_FILE_SUFFIX; is not set. The default value is .gv. -Changed in version 4.X.Y: deprecated. The default value +Changed in version 4.6.0: deprecated. The default value changed from .vcg (&bison; stopped generating .vcg output with version 2.4, in 2006). @@ -10923,7 +11041,7 @@ General options passed to the zip utility. An optional zip root directory (default empty). The filenames stored in the zip file will be relative to this directory, if given. -Otherwise the filenames are relative to the current directory of the +Otherwise, the filenames are relative to the current directory of the command. For instance: diff --git a/doc/generated/variables.mod b/doc/generated/variables.mod index 3b3e81eb5a..b0b322e325 100644 --- a/doc/generated/variables.mod +++ b/doc/generated/variables.mod @@ -591,6 +591,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. $TEMPFILEARGESCFUNC"> $TEMPFILEARGJOIN"> $TEMPFILEDIR"> +$TEMPFILEENCODING"> $TEMPFILEPREFIX"> $TEMPFILESUFFIX"> $TEX"> @@ -1273,6 +1274,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. $TEMPFILEARGESCFUNC"> $TEMPFILEARGJOIN"> $TEMPFILEDIR"> +$TEMPFILEENCODING"> $TEMPFILEPREFIX"> $TEMPFILESUFFIX"> $TEX"> diff --git a/doc/images/overview.graphml b/doc/images/overview.graphml index 74f80d7c6b..63ecd36bc2 100644 --- a/doc/images/overview.graphml +++ b/doc/images/overview.graphml @@ -1,39 +1,39 @@ - - - + + + - + - + - + - User's view + User's view - + - Folder 1 + Folder 1 @@ -46,10 +46,11 @@ - + - XML files (src/user/man/...) + XML files (user guide, +manpage, source tree) @@ -63,10 +64,10 @@ - + - XML validation + XML validation @@ -83,7 +84,8 @@ - Writer + Writer + @@ -102,11 +104,10 @@ - + - Creating entity lists - + Creating entity lists @@ -120,11 +121,11 @@ - + - Check that example -names are unique + Check that example + names are unique @@ -141,7 +142,7 @@ names are unique - Create example outputs + Create example outputs @@ -158,8 +159,8 @@ names are unique - Resolve XIncludes for text -and examples + Resolve XIncludes for + text and examples @@ -173,10 +174,10 @@ and examples - + - Create HTML, PDF, Man + Create HTML, PDF, Man @@ -190,10 +191,10 @@ and examples - + - Install in proper place + Install in proper place for packaging @@ -208,11 +209,11 @@ for packaging - + - Create API doc -(Epydoc) + Create API doc +(Sphinx) @@ -229,7 +230,8 @@ for packaging - get validated + get validated + @@ -267,7 +269,9 @@ for packaging - + + + @@ -300,7 +304,8 @@ for packaging - switching to Docbook + switching to Docbook + @@ -333,13 +338,14 @@ for packaging - edits/creates + edits/creates + - + <?xml version="1.0" encoding="utf-8"?> <!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> diff --git a/doc/images/overview.png b/doc/images/overview.png index f1189c9208..b83e9556aa 100644 Binary files a/doc/images/overview.png and b/doc/images/overview.png differ diff --git a/doc/man/scons-time.xml b/doc/man/scons-time.xml index 794e593632..2ce8c3bbab 100644 --- a/doc/man/scons-time.xml +++ b/doc/man/scons-time.xml @@ -41,7 +41,7 @@ - scons-time + scons-time subcommand options arguments @@ -129,7 +129,7 @@ DESCRIPTION -The +The scons-time command runs an SCons configuration through a standard set of profiled timings @@ -703,7 +703,7 @@ and processing the SConscript files. Full build SCons is run to build everything specified in the configuration. -Specific targets to be passed in on the command l ine +Specific targets to be passed in on the command line may be specified by the targets keyword in a configuration file; see below for details. @@ -800,7 +800,7 @@ if the first argument is a directory, the default prefix is the name of the directory; if the first argument is an archive (tar or zip file), -the default prefix is the the base name of the archive, +the default prefix is the base name of the archive, that is, what remains after stripping the archive suffix (.tgz, .tar.gz or .zip). @@ -984,7 +984,7 @@ value: SConscripts (total execution time for the SConscript files themselves), SCons -(exectuion time in SCons code itself) +(execution time in SCons code itself) or commands (execution time of the commands and other actions @@ -1169,7 +1169,7 @@ arguments = ['project-1.2.tgz', 'project-SConscripts.tar'] # so tell scons-time to chdir there before building. subdir = 'project-1.2' -# Set the prefix so output log files and profiles are named: +# Set the prefix, so output log files and profiles are named: # project-000-[012].{log,prof} # project-001-[012].{log,prof} # etc. diff --git a/doc/man/scons.xml b/doc/man/scons.xml index cdaaa44ac9..c446b649c2 100644 --- a/doc/man/scons.xml +++ b/doc/man/scons.xml @@ -1,29 +1,10 @@ DESCRIPTION -&scons; +&SCons; is an extensible open source build system that orchestrates the construction of software (and other tangible products such as documentation files) by determining which @@ -106,7 +87,7 @@ and a database of information about previous builds so details do not have to be recalculated each run. -&scons; requires &Python; 3.6 or later to run; +&scons; requires &Python; 3.7 or later to run; there should be no other dependencies or requirements, unless the experimental Ninja tool is used (requires the ninja package). @@ -117,30 +98,32 @@ The CPython project retired 3.5 in Sept 2020: . -Changed in version 4.8.0: -support for &Python; 3.6 is deprecated and will be removed -in a future &SCons; release. +Changed in version 4.9.0: +support for &Python; 3.6 is removed. The CPython project retired 3.6 in Sept 2021: . -You set up an &SCons; -build system by writing a script + +You set up an &SCons; build by writing a script that describes things to build (targets), and, if necessary, the rules to build those files (actions). &SCons; comes with a collection of Builder methods -which apply premade actions for building many common software components +which supply premade Actions for building many common software components such as executable programs, object files and libraries, so that for many software projects, only the targets and input files (sources) need be specified in a call to a builder. -&SCons; thus can operate at a level of abstraction above that of pure filenames. -For example if you specify a library target named "foo", + + + +&SCons; operates at a level of abstraction above that of pure filenames. +For example if you specify a shared library target named "foo", &SCons; keeps track of the actual operating system dependent filename -(such as libfoo.so on a GNU/Linux system), -and how to refer to that library in later construction steps -that want to use it, so you don't have to specify that precise -information yourself. +(such as libfoo.so on a GNU/Linux system +and foo.dll on Windows), +and gives you a handle to refer to that target in other steps, +so you don't have to use system-specific strings yourself. &SCons; can also scan automatically for dependency information, such as header files included by source code files (for example, #include @@ -154,51 +137,59 @@ to support additional input file types. Information about files involved in the build, including a cryptographic hash of the contents of source files, is cached for later reuse. -By default this hash (the &contentsig;) -is used to determine if a file has changed since the last build, -although this can be controlled by selecting an appropriate +By default, this hash (the &contentsig;) +is used to decide if a file has changed since the last build, +although other algorithms can be used by selecting an appropriate &f-link-Decider; function. Implicit dependency files are also part of out-of-date computation. The scanned implicit dependency information can optionally be cached and used to speed up future builds. A hash of each executed build action (the &buildsig;) -is cached, so that changes to build instructions (changing flags, etc.) -or to the build tools themselves (new version) +is also cached, so that changes to build instructions (changing flags, etc.) +or to the build tools themselves (e.g. a compiler upgrade) can also trigger a rebuild. -&SCons; supports the concept of separated source and build -directories through the definition of +&SCons; supports separated source and build +directories (also called "out-of-tree builds") +through the definition of variant directories -(see the &f-link-VariantDir; function). +Using a separated build directory helps keep +the source directory clean of artifacts when doing searches, +allows setting up differing builds ("variants") without conflicts, +and allows resetting the build by just removing the build directory +(note that SCons does have a "clean" mode as well). +See the &f-link-VariantDir; description for more details. When invoked, &scons; -looks for a file named -&SConstruct; -(other names are also accepted, -see ) -in the current directory and reads the -build configuration from that file; -that directory is considered the project top directory. +looks for a file describing the build configuration +in the current directory and reads that in. +The file is by default named &SConstruct;, +although some variants of that, +or a developer-chosen name, are also accepted +(see ). +If found, the current directory +is set as the project top directory. Certain command-line options specify alternate -places to look for the &SConstruct; +places to look for &SConstruct; (see , , and ), -which will set the project top directory to the place found. -A path to the main build configuration file can also be +which will set the project top directory to the path found. +A path to the build configuration can also be specified with the option, which leaves the current directory as the project top directory. + The build configuration may be split into multiple files: -the &SConstruct; file may specify additional +the &SConstruct; file can specify additional configuration files by calling the &f-link-SConscript; function, and any file thus invoked may @@ -215,23 +206,24 @@ configuration files for a project (including the &SConstruct; file), regardless of the actual file names or number of such files. A hierarchical build is not recursive - all of -the SConscript files are processed in a single pass, -although each is processed in a separate context so -as not to interfere with one another. &SCons; provides -mechanisms for information to be shared between -SConscript files when needed. +the SConscript files are processed in a single pass +so that &scons; has a picture of the complete +dependency tree when it begins considering what needs building. +Each SConscript file is processed in a separate context +so settings made in one script do not leak into another; +information can however be shared explicitly between scripts. -Before reading the &SConscript; files, +Before reading the SConscript files, &scons; -looks for a directory named -site_scons -in various system directories and in the -project top directory, or, if specified, -the directory from the +looks for a site directory - +a directory named site_scons +is searched for in various system directories and in the +project top directory, or if the -option instead, and prepends the ones it -finds to the &Python; module search path (sys.path), +option is given, checks only for that directory. +Found site directories are prepended +to the &Python; module search path (sys.path), thus allowing modules in such directories to be imported in the normal &Python; way in &SConscript; files. For each found site directory, @@ -250,28 +242,31 @@ controlling the site directories. &SConscript; files are written in the -&Python; programming language, -although it is normally not necessary to be a &Python; -programmer to use &scons; effectively. -&SConscript; files are invoked in a context that makes -the facilities described in this manual page available -in their local namespace without any special steps. +&Python; programming language. +For many tasks, the simple syntax can be understood from examples, +so it is normally not necessary to be a &Python; +programmer to use &SCons; effectively. +&SConscript; files are executed in a context that makes +the facilities described in this manual page directly +available (that is, no need to import). Standard &Python; scripting capabilities -such as flow control, data manipulation, and imported &Python; libraries -are available to use to handle complicated build situations. +such as flow control, data manipulation, and imported &Python; modules +are available to use in more complicated build configurations. Other &Python; files can be made a part of the build system, but they do not automatically have the &SCons; context and need to import it if they need access (described later). -&scons; -reads and executes all of the included &SConscript; files +&SCons; reads and executes all of the included &SConscript; files before it begins building any targets. -To make this clear, -&scons; -prints the following messages about what it is doing: +Progress messages show this behavior +(the state change lines - those +beginning with the scons: tag - +may be suppressed using the + option): + $ scons foo.out @@ -283,43 +278,48 @@ scons: done building targets. $ -The status messages -(lines beginning with the scons: tag) -may be suppressed using the - -option. - To assure reproducible builds, &SCons; uses a restricted execution environment for running external commands used to build targets, -rather then propagating the full environment +rather than propagating the full environment in effect at the time &scons; was called. -This helps avoid problems like picking up accidental settings, +This helps avoid problems like picking up accidental +or malicious settings, temporary debug values that are no longer needed, -or one developer having different settings than another -(or than the CI/CD pipeline). -Environment variables that are needed for proper -operation of such commands need to be set explicitly, -which can be done either by assigning the desired values, -or by picking values individually out of environment variables -using the &Python; os.environ dictionary. -The execution environment for a given &consenv; is -contained in its &cv-link-ENV; &consvar;. -A few environment variables are picked up automatically - -see ). - - - -In particular, if the compiler or other commands -that you want to use to build your target files -are not in standard system locations, -&scons; -will not find them unless -you explicitly include the locations into the -PATH element of the -execution environment. +or a developer having different settings than another +(or than the CI pipeline). +Environment variables needed for the proper +operation of such commands must be set in the +execution environment explicitly, +either by assigning the desired values, +or by picking those values individually or collectively +out of environment variables exposed by the &Python; +os.environ dictionary +(as external program inputs they should be validated +before use). +The execution environment for a given &consenv; +is its &cv-link-ENV; value. +A small number of environment variables are picked up automatically +by &scons; itself (see ). + + + +In particular, if a compiler or other external command +needed to build a target file +is not in &scons;' idea of a standard system location, +it will not be found at runtime unless +you explicitly add the location into the +execution environment's PATH element. +This is a particular consideration on Windows platforms, +where it is common for a command to install into an app-specific +location and depend on setting +PATH in order for them to be found, +which does not automatically work for &SCons;. + + + One example approach is to extract the entire PATH environment variable and set that into the @@ -342,13 +342,13 @@ import os env = Environment( ENV={ 'PATH': os.environ['PATH'], - 'ANDROID_HOME': os.environ['ANDROID_HOME'], - 'ANDROID_NDK_HOME': os.environ['ANDROID_NDK_HOME'], + 'MODULEPATH': os.environ['MODULEPATH'], + 'PKG_CONFIG_PATH': os.environ['PKG_CONFIG_PATH'], } ) -Or you may explicitly propagate the invoking user's +Or you can explicitly propagate the invoking user's complete external environment: @@ -450,10 +450,13 @@ searches in order for the &MSVC; tools, the MinGW tool chain, the Intel compiler tools, +the GCC tools, +the LLVM/clang tools, and the PharLap ETS compiler. On Windows system which identify as cygwin (that is, if &scons; is invoked from a cygwin shell), the order changes to prefer the GCC toolchain over the MSVC tools. + On OS/2 systems, &scons; searches in order for the @@ -466,13 +469,18 @@ searches for the native compiler tools (MIPSpro, Visual Age, aCC, and Forte tools respectively) and the GCC tool chain. On all other platforms, -including POSIX (Linux and UNIX) platforms, +including POSIX (Linux and UNIX) and macOS platforms, &scons; searches in order for the GCC tool chain, +the LLVM/clang tools, and the Intel compiler tools. -These default values may be overridden -by appropriate setting of &consvars;. +The default tool selection can be pre-empted +through the use of the tools +argument to &consenv; creation methods, +explicitly calling the &f-link-Tool; loader, +the through the setting of various setting of &consvars;. + Target Selection @@ -748,7 +756,7 @@ built during this invocation. -When using a derived-file cache show the command +When using a derived-file cache, show the command that would have been executed to build the file (or the corresponding *COMSTR contents if set) @@ -780,7 +788,7 @@ This saves time by not running the same configuration tests every time you invoke scons, but will overlook changes in system header files or external commands (such as compilers) -if you don't specify those dependecies explicitly. +if you don't specify those dependencies explicitly. This is the default behavior. @@ -791,7 +799,7 @@ This is the default behavior. If this mode is specified, all configuration tests will be re-run regardless of whether the -cached results are out of date. +cached results are out-of-date. This can be used to explicitly force the configuration tests to be updated in response to an otherwise unconfigured change @@ -1042,6 +1050,11 @@ The names &SConstruct; and &SConscript; are now recognized without requiring .py suffix. + +Changed in version 4.8.0: +The name SCsub is now recognized +without requiring .py suffix. + @@ -1053,7 +1066,7 @@ recognized without requiring is prepared for building. &scons; prints this for each target it considers, even if that -target is up to date (see also ). +target is up-to-date (see also ). This can help debug problems with targets that aren't being built; it shows whether &scons; @@ -1327,7 +1340,7 @@ to use the specified algorithm. If this option is omitted, the first supported hash format found is selected. -Typically this is MD5, however, on a FIPS-compliant system +Typically, this is MD5, however, on a FIPS-compliant system using a version of &Python; older than 3.9, SHA1 or SHA256 is chosen as the default. &Python; 3.9 and onwards clients always default to MD5, even in FIPS mode. @@ -1615,7 +1628,7 @@ scons>>> exit -Specifies the maximum number of comcurrent jobs (commands) to run. +Specifies the maximum number of concurrent jobs (commands) to run. If there is more than one option, the last one is effective. @@ -1738,7 +1751,7 @@ if not GetOption("no_exec"): determine what would be built. For example, if a file generated by a builder action is also used as a source in the build, that file is not available to scan for dependencies at all -in an unbuilt tree, and may contain out of date information in a +in an unbuilt tree, and may contain out-of-date information in a previously built tree. @@ -1746,7 +1759,7 @@ previously built tree. as they would make changes to the filesystem (see &cv-link-CONFIGUREDIR; and &cv-link-CONFIGURELOG;). It can use stored information from a previous build, -if it is not out of date, +if it is not out-of-date, so a "priming" build may make subsequent no-exec runs more useful. @@ -1765,7 +1778,7 @@ added to the module search path sys.path, searched for a site_init.py file, or have their site_tools directory included in the tool search path. -Can be overridden by a subequent +Can be overridden by a subsequent option. @@ -1831,8 +1844,8 @@ The results may be analyzed using the &Python; Do not run any commands, or print anything. Just return an exit -status that is zero if the specified targets are already up to -date, non-zero otherwise. +status that is zero if the specified targets are already up-to-date, +non-zero otherwise. @@ -2354,7 +2367,7 @@ is run with the &Python; option or from optimized &Python; (.pyo) modules. Note the "no-" prefix is part of the name of this warning. -Add an additional "-no" to disable. +Add another "-no" to disable. @@ -2369,7 +2382,7 @@ option is used. These warnings are enabled by default. Note the "no-" prefix is part of the name of this warning. -Add an additional "-no" to disable. +Add another "-no" to disable. @@ -2450,12 +2463,15 @@ These warnings are enabled by default. -Search the specified repository + +Search repository for any input and target -files not found in the local directory hierarchy. Multiple - -options may be specified, in which case the -repositories are searched in the order specified. +files not found in the local directory hierarchy. +Multiple +options may be specified, +with repositories searched in the given order. +See &f-link-Repository; for more information. + @@ -2495,7 +2511,7 @@ the build don't accidentally step on each other. You have to be explicit about sharing information, by using the &f-link-Export; function or the &exports; argument to the &f-link-SConscript; function, as well as the &f-link-Return; function -in a called &SConscript; file, and comsume shared information by using the +in a called &SConscript; file, and consume shared information by using the &f-link-Import; function. @@ -2508,7 +2524,7 @@ that can be used in &SConscript; files. Quick links: Construction Environments Tools Builder Methods - Methods and Functions to do Things + Functions and Environment Methods SConscript Variables Construction Variables Configure Contexts @@ -2545,8 +2561,8 @@ env['FOO'] = 'foo' Note that certain settings which affect tool detection are -referenced only when the tools are initializided, -so you either need either to supply them as part of the call to +referenced only when the tools are initialized, +so you need either to supply them as part of the call to &f-link-Environment;, or defer tool initialization. For example, initializing the &MSVC; version you wish to use: @@ -2650,7 +2666,7 @@ one of the pre-defined platforms posix, sunos or win32), -or it may be be a callable platform object +or a callable platform object returned by a call to &f-link-Platform; selecting a pre-defined platform, or it may be a user-supplied callable, @@ -2667,7 +2683,7 @@ env = Environment(platform=my_platform) Note that supplying a non-default platform or custom -fuction for initialization +function for initialization may bypass settings that should happen for the host system and should be used with care. It is most useful in the case where the platform is an alternative for @@ -2701,7 +2717,7 @@ See for details. which are used to help initialize the &consenv; prior to building, and more can be written to suit a particular purpose, or added from external sources (a repository of -constributed tools is available). +contributed tools is available). More information on writing custom tools can be found in the Extending SCons section and specifically Tool Modules. @@ -2810,7 +2826,7 @@ specified relationship into its internal dependency node graph, and only later makes the decision on whether anything is actually built, since this depends on command-line options, target selection rules, and whether the target(s) are -out of date with respect to the sources. +out-of-date with respect to the sources. @@ -2903,7 +2919,7 @@ of the &SConscript; file currently being processed. &SCons; also recognizes a third way to specify path strings: if the string begins with the # character it is -top-relative - it works like a relative path but the +top-relative - it works like a relative path, but the search follows down from the project top directory rather than from the current directory. The # can optionally be followed by a pathname separator, @@ -3256,7 +3272,7 @@ object_files.extend(Object('bar.c')) The path name for a Node's file may be used -by passing the Node to &Python;'s builtin +by passing the Node to &Python;'s built-in str function: @@ -3373,89 +3389,98 @@ object. - -Methods and Functions To Do Things + +&SCons; Functions and Environment Methods -In addition to Builder methods, -&scons; -provides a number of other &consenv; methods -and global functions to -manipulate the build configuration. -Usually, a &consenv; method -and global function with the same name both exist -for convenience. -In the following list, the global function -is documented in this style: + +&SCons; provides a variety of &consenv; methods +and global functions to manipulate the build configuration. +Often, a &consenv; method and a global function with +the same name exist for convenience. +In this section, both forms are shown if the function can be called +in either way. +The documentation style for these is as follows: + -Function(arguments, [optional arguments]) +Function(arguments, [optional arguments, ...]) # Global function +env.Function(arguments, [optional arguments, ...]) # Environment method -and the &consenv; method looks like: - - -env.Function(arguments, [optional arguments]) - + +In these function signatures, +arguments in brackets ([]) are optional, +and ellipses (...) indicate possible repetition. +Positional vs. keyword arguments are usually detailed +in the following text, not in the signature itself. +The &Python; positional-only (/) +and keyword-only (*) markers are not used. + -If the function can be called both ways, -then both forms are listed. + +When the &Python; keyword=value style is shown, +it can have two meanings. +If the keyword argument is known to the function, +the value is the default for that argument if it is omitted. +If the keyword is unknown to the function, +some methods treat it as a &consvar; assignment; +otherwise an exception is raised for an unknown argument. + -The global function and same-named -&consenv; method -provide almost identical functionality, with a couple of exceptions. -First, many of the &consenv; methods affect only that -&consenv;, while the global function has a -global effect (or, alternatively, takes an additional -positional argument to specify the affected &consenv;). -Second, where appropriate, -calling the functionality through a &consenv; will -substitute &consvars; into -any supplied string arguments, while the global function, -unless it takes a &consenv; parameter, -does not have the context of a &consenv; to pick variables from, -and thus cannot perform substitutions. -For example: + +A global function and a same-named &consenv; method +have the same base functionality, +with two key differences: + - -Default('$FOO') + + + +&Consenv; methods that change the environment +act on the environment instance from which they are called, +while the corresponding global function acts on +a special “hidden” &consenv; called the Default Environment. +In some cases, the global function may take +an initial argument giving the object to operate on. + + + + +String-valued arguments +(including strings in list-valued arguments) +are subject to construction variable expansion +by the environment method form; +variable expansion is not immediately performed in the global function. +For example, Default('$MYTARGET') +adds '$MYTARGET' to the +list of default targets, +while if the value in env of +MYTARGET is 'mine', +env.Default('$MYTARGET' adds +'mine' +to the default targets. +For more details on &consvar; expansion, see the +&Consvars; section. + + + -env = Environment(FOO='foo') -env.Default('$FOO') - - -In the above example, -the call to the global &f-Default; -function will add a target named -$FOO -to the list of default targets, -while the call to the -&f-env-Default; &consenv; method -will expand the value -and add a target named -foo -to the list of default targets. -For more on &consvar; expansion, -see the -&Consvars; -section below. - - - -Global functions are automatically in scope inside -&SConscript; files. -If you have custom &Python; code that you import into an &SConscript; file, -such code will need to bring them into their own scope. -You can do that by adding the following import -to the &Python; module: + +Global functions are automatically in scope inside &SConscript; files. +If your project adds &Python; modules that you include +via the &Python; import statement +from an &SConscript; file, +such code will need to add the functions +to that module’s global scope explicitly. +You can do that by adding the following import to the &Python; module: +from SCons.Script import *. + - -from SCons.Script import * - + +&SCons; provides the following &consenv; methods and global functions. +The list can be augmented on a project basis using &f-link-AddMethod; + -&Consenv; methods -and global functions provided by -&scons; -include: @@ -3575,7 +3600,7 @@ selects for building as a result of making the sure the specified targets are up to date, if those targets did not appear on the command line. The list is empty if neither -command line targets or &Default; calls are present. +command line targets nor &Default; calls are present. The elements of this list may be strings @@ -3612,10 +3637,10 @@ the command line. If there are command line targets, this list has the same contents as &BUILD_TARGETS;. If there are no targets specified on the command line, -the list is empty. The elements of this list are strings. +this list is empty. The elements of this list are strings. This can be used, for example, to take specific actions only -when a certain targets is explicitly requested for building. +when a certain target(s) are explicitly requested for building. Example: @@ -3653,7 +3678,7 @@ if 'foo' in [str(t) for t in DEFAULT_TARGETS]: The contents of the &DEFAULT_TARGETS; -list change on on each successive call to the +list changes on each successive call to the &Default; function: @@ -3847,23 +3872,34 @@ The &f-link-env-Dump; method can be called to examine the Configure Contexts &SCons; -supports a -&configure_context;, -an integrated mechanism similar to the -various AC_CHECK macros in GNU &Autoconf; -for testing the existence of external items needed +provides an integrated autoconfiguration mechanism +(inspired by GNU &Autoconf; but intrinsic to &SCons;), +for checking the existence of external items needed for the build, such as C header files, libraries, etc. +This can be used to build optional features only +if support for them is available, +abort the build quickly if required elements are missing, +or just tune the build to the specific build platform. The mechanism is portable across platforms. -&scons; +You activate the configuration sysem by creating a +&configure_context;, +which holds accumulated information while the +checks are being performed, +request the desired checks, +and then transfer the information to the regular build environment. +Optionally, a configure header that C or C++ +code can include can also be generated. +&SCons; does not maintain an explicit cache of the tested values -(this is different than &Autoconf;), +(unlike &Autoconf;), but uses its normal dependency tracking to keep the checked values -up to date. However, users may override this behaviour with the +up to date. You may override this behavior with the -command line option. +command line option. + @@ -3871,9 +3907,9 @@ command line option. env.Configure([custom_tests, conf_dir, log_file, config_h, clean, help]) Create a &configure_context;, which tracks information -discovered while running tests. The context includes a local &consenv; +discovered while running checks. The context includes a local &consenv; (available as context.env) -which is used when running the tests and +which is used when running the checks and which can be updated with the check results. Only one context may be active at a time, but a new context can be created @@ -3882,10 +3918,6 @@ For the global function form, the required env describes the initial values for the context's local &consenv;; for the &consenv; method form the instance provides the values. - -Changed in version 4.0: raises an exception -on an attempt to create a new context when there is an active context. - custom_tests specifies a dictionary containing custom checks (see details below). @@ -3908,30 +3940,30 @@ under that build's variant directory. config_h specifies a C header file where the results of tests -will be written. The results will consist of lines like -#define HAVE_STDIO_H, -#define HAVE_LIBM, etc. -Customarily, the name chosen is config.h. -The default is to not write a -config_h -file. +will be written, +so the build can have access to this information by including it. +The results will consist of lines like +#define HAVE_GETADDRINFO 1, +#define HAVE_INTTYPES_H 1, etc. +The default is None, +which creates no configure header. +The convention has been to call +the configure header config.h. You can specify the same config_h file in multiple calls to &Configure;, in which case &SCons; will concatenate all results in the specified file. Note that &SCons; -uses its normal dependency checking -to decide if it's necessary to rebuild -the specified -config_h -file. -This means that the file is not necessarily re-built each +uses its normal dependency tracking +to decide if it's necessary to rebuild the +configure hearer. +This means that the file is not necessarily rebuilt each time scons is run, but is only rebuilt if its contents will have changed -and some target that depends on the -config_h -file is being built. +and some target that depends on the configure header is being built. + + The clean and help @@ -3954,18 +3986,26 @@ or arguments (or both) to avoid unnecessary test execution. + + +Changed in version 4.0: raises an exception +on an attempt to create a new context when there is an active context. + context.Finish() -This method must be called after configuration is done. +Must be called after configuration is complete. Though required, this is not enforced except if &Configure; is called again while there is still an active context, in which case an exception is raised. -&Finish; returns the environment as modified -during the course of running the configuration checks. +Returns the context's &consenv; as modified +during the course of running the configuration checks - +the original environment is unchanged; +typically the returned environment is used to +replace the original. After this method is called, no further checks can be performed with this configuration context. However, you can create a new @@ -3983,7 +4023,7 @@ conf = Configure(env) if not conf.CheckCHeader("math.h"): print("We really need math.h!") Exit(1) -if conf.CheckLibWithHeader("qt", "qapp.h", "c++", "QApplication qapp(0,0);"): +if conf.CheckLibWithHeader("qt", "qapp.h", "c++", call="QApplication qapp(0,0);"): # do stuff for qt - usage, e.g. conf.env.Append(CPPDEFINES="WITH_QT") env = conf.Finish() @@ -3993,7 +4033,7 @@ env = conf.Finish() has the following predefined methods which can be used to perform checks. Where language is an optional parameter, -it specifies the compiler to use for the check, +it specifies the programming language of the check, currently a choice of C or C++. The spellings accepted for C are C or c; @@ -4005,95 +4045,76 @@ If language is omitted, - + context.CheckHeader(header, [include_quotes, language]) -Checks if -header -is usable in the specified language. +Check if header -may be a list, +can be used when building this project. +A generated stub program in the specified +language is built to check. +header may also be a list, in which case the last item in the list is the header file to be checked, and the previous list items are header files whose #include -lines should precede the +directives should precede the header line being checked for. The optional argument include_quotes -must be -a two character string, where the first character denotes the opening -quote and the second character denotes the closing quote. -By default, both characters are " (double quote). +specifies the characters wrapping the header name - +only the first two are considered. +Essentially, this allows you to swap the default +double-quotes ("") +for angle brackets (<>). -Returns a boolean indicating success or failure. + +Returns a boolean indicating success or failure. +If a configure header was requested, +the result is recorded in it in the form of a +preprocessor macro in the case of success, +or an informative comment in the case of failure. + + - + context.CheckCHeader(header, [include_quotes]) -Checks if +Check if header is usable when compiling a C language program. -header -may be a list, -in which case the last item in the list -is the header file to be checked, -and the previous list items are -header files whose -#include -lines should precede the -header line being checked for. -The optional argument -include_quotes -must be -a two character string, where the first character denotes the opening -quote and the second character denotes the closing quote. -By default, both characters are " (double quote). -Note this is a wrapper around -CheckHeader. -Returns a boolean indicating success or failure. +This is a wrapper around +&CheckHeader; - +see its entry for details. + - + context.CheckCXXHeader(header, [include_quotes]) -Checks if +Check if header is usable when compiling a C++ language program. -header -may be a list, -in which case the last item in the list -is the header file to be checked, -and the previous list items are -header files whose -#include -lines should precede the -header line being checked for. -The optional argument -include_quotes -must be -a two character string, where the first character denotes the opening -quote and the second character denotes the closing quote. -By default, both characters are " (double quote). -Note this is a wrapper around -CheckHeader. -Returns a boolean indicating success or failure. +This is a wrapper around +&CheckHeader; - +see its entry for details. + - + context.CheckFunc(function_name, [header, language, funcargs]) -Checks if function_name is usable +Check if function_name is usable in the context's local environment, using the compiler specified by language - that is, can a check referencing it be compiled using the current values of &cv-link-CFLAGS;, &cv-link-CPPFLAGS;, -&cv-link-LIBS; or other relevant &consvars;. + &cv-link-LIBS; or other relevant &consvars;. @@ -4124,32 +4145,55 @@ type. Modern C/C++ compilers reject implicit function declarations and may also function calls whose arguments are not type compatible with the prototype. +Returns a boolean indicating success or failure. +If a configure header was requested, +the result is recorded in it in the form of a +preprocessor macro in the case of success, +or an informative comment in the case of failure. + + Changed in version 4.7.0: added the funcargs. - -Returns a boolean indicating success or failure. - - context.CheckLib([library, symbol, header, language, autoadd=True, append=True, unique=False]) + + context.CheckLib([library, symbol, header, language, extra_libs=None, autoadd=True, append=True, unique=False]) -Checks if +Check if library -provides -symbol by compiling a simple stub program -with the compiler selected by language, -and optionally adds that library to the context. -If supplied, the text of header is included at the -top of the stub. +can be used to build this project +(see also &CheckLibWithHeader;). +A small stub program is generated and linked against +library by the +compiler selected by language. +If symbol is specified, +the stub will contain a reference to that symbol, +to check if it is actually provided by the library. +If supplied, the text of header +is included at the top of the stub; it must be syntactically +correct in language. + + +Note that if symbol is given, +the stub will be generated with an old-style prototype, +as it has no knowledge of the actual prototype +(e.g. char sin(); instead of +double sin(double x);). +Such usage is no longer legal under C23 and later. + + +The remaining arguments should be specified in keyword style. +If extra_libs is specified, +it is a list off additional libraries to include when +linking the stub program (usually, dependencies of +the library being checked). If autoadd is true (the default), -and the library provides the specified -symbol (as defined by successfully -linking the stub program), -it is added to the &cv-link-LIBS; &consvar; in the context. -if append is true (the default), -the library is appended, otherwise it is prepended. +and the link succeeds, +the library is added to the &cv-link-LIBS; &consvar; in the context. +If append is true (the default), +an added library is appended, otherwise it is prepended. If unique is true, and the library would otherwise be added but is already present in &cv-link-LIBS; in the configure context, @@ -4169,7 +4213,7 @@ is omitted or None, then CheckLib just checks if you can link against the specified -library, +library. Note though it is legal syntax, it would not be very useful to call this method with library @@ -4182,107 +4226,128 @@ at least one should be supplied. Changed in version 4.5.0: added the append and unique parameters. + + +Changed in version 4.9.0: added the +extra_libs parameter. - - context.CheckLibWithHeader(library, header, [language, call, autoadd=True, append=True, unique=False]) + + context.CheckLibWithHeader([library, header, language, extra_libs=None, call=None, autoadd=True, append=True, unique=False]) -Provides an alternative to the -CheckLib method -for checking for libraries usable in a build. +Check if library -specifies a library or list of libraries to check. -header -specifies a header to include in the test program, -and language indicates the compiler to use. +can be used to build this project, +when a header file must be included to use library +(see also &CheckLib;). +The first three arguments can be given as +either positional or keyword arguments. +library +specifies a library or list of libraries to check +(the default is None), header -may be a list, -in which case the last item in the list -is the header file to be checked, +specifies a header file or list of header files to include in the test program. +If header is a list, +the last item in the list is the header file to be checked, and the previous list items are header files whose #include lines should precede the header line being checked for. -A code fragment -(must be a a valid expression, including a trailing semicolon) -to serve as the test can be supplied in -call; -if not supplied, +The default is to include no header text. + + + +The remaining parameters should be specified in keyword style. +If provided, call +is a code fragment to compile as the stub test, +replacing the auto-generated stub. +The fragment must be a valid expression in language. +If not supplied, the default checks the ability to link against the specified library. +extra_libs can be used to add additional libraries +to link against (usually, dependencies of the library under test). If autoadd is true (the default), the first library that passes the check -is added to the &cv-link-LIBS; &consvar; in the context +is added to the &cv-link-LIBS; &consvar; in the configure context and the method returns. If append is true (the default), -the library is appended, otherwise prepended. +an added library is appended, otherwise it is prepended. If unique is true, and the library would otherwise be added but is already present in &cv-link-LIBS; in the configure context, it will not be added again. The default is False. + Returns a boolean indicating success or failure. + Changed in version 4.5.0: added the append and unique parameters. + + +Changed in version 4.9.0: added the +extra_libs parameter. - + context.CheckType(type_name, [includes, language]) -Checks for the existence of a type defined by -typedef. -type_name -specifies the typedef name to check for. +Check whether type_name +is defined via a typedef. includes is a string containing one or more #include -lines that will be inserted into the program -that will be run to test for the existence of the type. -Example: +lines that will be placed at the top +of the stub program +that will be compiled to perform the check. +Returns a boolean indicating success or failure. +If a configure header was requested, +the result is recorded in it in the form of a +preprocessor macro in the case of success, +or an informative comment in the case of failure. + + +Example: sconf.CheckType('foo_type', '#include "my_types.h"', 'C++') -Returns a boolean indicating success or failure. - + context.CheckTypeSize(type_name, [header, language, expect]) -Checks for the size of a type defined by -typedef. +Check for the size of a type type_name -specifies the typedef name to check for. +defined via a typedef (or built in). The optional header argument is a string that will be placed at the top -of the test file -that will be compiled -to check if the type exists; +of the stub program +that will be compiled to perform the check - the default is empty. +The check succeeds and returns the size of the type +if it is found, else zero. If the optional -expect, -is supplied, it should be an integer size; -&CheckTypeSize; will fail unless -type_name is actually -that size. -Returns the size in bytes, or zero if the type was not found -(or if the size did not match optional expect). +expect +(integer) parameter is given, +the check succeeds only if the +detected size matches it. + - -For example, +Example: CheckTypeSize('short', expect=2) @@ -4293,10 +4358,10 @@ actually two bytes. - + context.CheckCC() -Checks whether the C compiler +Check whether the C compiler (as defined by the &cv-link-CC; &consvar;) works, by trying to compile a small source file. This provides a more rigorous check: @@ -4310,14 +4375,13 @@ for C source files, so by setting relevant &consvars; it can be used to detect if particular compiler flags will be accepted or rejected by the compiler. -Returns a boolean indicating success or failure. - + context.CheckCXX() -Checks whether the C++ compiler +Check whether the C++ compiler (as defined by the &cv-link-CXX; &consvar;) works, by trying to compile a small source file. This provides a more rigorous check: @@ -4331,14 +4395,13 @@ for C++ source files, so by setting relevant &consvars; it can be used to detect if particular compiler flags will be accepted or rejected by the compiler. -Returns a boolean indicating success or failure. - + context.CheckSHCC() -Checks whether the shared-object C compiler (as defined by the +Check whether the shared-object C compiler (as defined by the &cv-link-SHCC; &consvar;) works by trying to compile a small source file. This provides a more rigorous check: @@ -4354,14 +4417,13 @@ be accepted or rejected by the compiler. Note this does not check whether a shared library/dll can be created. -Returns a boolean indicating success or failure. - + context.CheckSHCXX() -Checks whether the shared-object C++ compiler (as defined by the +Check whether the shared-object C++ compiler (as defined by the &cv-link-SHCXX; &consvar;) works by trying to compile a small source file. This provides a more rigorous check: @@ -4377,14 +4439,13 @@ be accepted or rejected by the compiler. Note this does not check whether a shared library/dll can be created. -Returns a boolean indicating success or failure. - + context.CheckProg(prog_name) -Checks if +Check if prog_name exists in the path &SCons; will use at build time. (context.env['ENV']['PATH']). @@ -4393,10 +4454,10 @@ or None on failure. - + context.CheckDeclaration(symbol, [includes, language]) -Checks if the specified +Check if the specified symbol is declared. includes @@ -4409,12 +4470,12 @@ that will be run to test for the existence of the symbol. - + context.CheckMember(aggregate_member, [header, language]) - Checks for the existence of a member of the C/C++ struct or class. + Check for the existence of a member of the C/C++ struct or class. aggregate_member specifies the struct/class and member to check for. header @@ -4433,16 +4494,17 @@ sconf.CheckMember('struct tm.tm_sec', '#include <time.h>') Returns a boolean indicating success or failure. + Added in 4.4.0. - + context.Define(symbol, [value, comment]) This method does not check for anything, but rather forces the definition of a preprocessor macro that will be added -to the configuration header file. +to the configure header. name is the macro's identifier. If value is given, it will be be used as the macro replacement value. @@ -4532,7 +4594,7 @@ can make use of:: Displays text -as an indicator of progess. +as an indicator of progress. For example: Checking for library X.... Usually called before the check is started. @@ -4557,7 +4619,7 @@ Usually called after the check has completed. chk_ctx.TryCompile(text, extension='') -Checks if a file containing text +Check if a file containing text and given the specified extension (e.g. '.c') can be compiled to an object file @@ -4569,7 +4631,7 @@ Returns a boolean indicating success or failure. chk_ctx.TryLink(text, extension='') -Checks if a file containing text +Check if a file containing text and given the specified extension (e.g. '.c') can be compiled to an executable program @@ -4581,7 +4643,7 @@ Returns a boolean indicating success or failure. chk_ctx.TryRun(text, extension='') -Checks if a file containing text +Check if a file containing text and given the specified extension (e.g. '.c') can be compiled to an excutable program @@ -4601,7 +4663,7 @@ then (False, '') is returned. chk_ctx.TryAction(action, [text, extension='']) -Checks if the specified +Check if the specified action with an optional source file (contents text, @@ -4674,47 +4736,52 @@ env = conf.Finish() Command-Line Construction Variables -Often when building software, -specialized information needs to be conveyed at build time -to override the defaults in the build scripts. -Command-line arguments (like --implcit-cache) -and giving names of build targets are two ways to do that. -Another is to provide variable-assignment arguments -on the command line. -For the particular case where you want to specify new + +&SCons; depends on information stored in &consvars; to +control how targets are built. +It is often necessary to pass +specialized information at build time +to override the variables in the build scripts. +This can be done through variable-assignment arguments +on the command line and/or in stored variable files. + + + +For the case where you want to specify new values for &consvars;, &SCons; provides a &Variables; object to simplify collecting those and updating a &consenv; with the values. -The typical calling style looks like: +This helps processing commands lines like this: -scons VARIABLE=foo +scons VARIABLE=foo OTHERVAR=bar -Variables specified in the above way -can be manually processed by accessing the +Variables supplied on the command line +can always be manually processed by iterating the &ARGUMENTS; dictionary -(or &ARGLIST; list), -but using a &Variables; object allows you to describe +or the &ARGLIST; list, +However, using a &Variables; object allows you to describe anticipated variables, -convert them to a suitable type if necessary, -validate the values are within defined constraints, -and define defaults, help messages and aliases. -This is conceptually similar to the structure of options +perform necessary type conversion, +validate that values meet defined constraints, +and specify default values, help messages and aliases. +This provides a somewhat similar interface to option handling (see &f-link-AddOption;). -It also allows obtaining values from a saved variables file, +A &Variables; object also allows +obtaining values from a saved variables file, or from a custom dictionary in an &SConscript; file. The processed variables can then be applied to the desired &consenv;. -Roughly speaking, arguments are used to convey information to the -&SCons; program about how it should behave; -variables are used to convey information to the build -(although &SCons; does not enforce any such constraint). +Conceptually, command-line targets control what to build, +command-line variables (and variable files) control how to build, +and command-line options control how &SCons; operates +(although &SCons; does not enforce that separation). To obtain an object for manipulating variables, @@ -4725,23 +4792,23 @@ call the &Variables; factory function: Variables([files, [args]]) If files is a filename or list of filenames, -they are considered to be &Python; scripts which will -be executed to set variables when the +they are executed as &Python; scripts +to set saved variables when the Update -method is called - -this allows the use of &Python; syntax in the assignments. -A file can be the result of an earlier call to the +method is called. +This allows the use of &Python; syntax in the assignments. +A variables file can be the result of an previous call to the &Save; method. If files is not specified, or the files argument is None, -then no files will be read. +then no files will be processed. Supplying None is required if there are no files but you want to specify args as a positional argument; -this can be omitted if using the keyword argument style. +or you can use keyword arguments to avoid that. If any of files is missing, it is silently skipped. @@ -4762,8 +4829,8 @@ The key-value pairs from args will be added to those obtained from files, if any. Keys from args -take precendence over same-named keys from files. -If omittted, the default is the +take precedence over same-named keys from files. +If omitted, the default is the &ARGUMENTS; dictionary that holds build variables specified on the command line. @@ -4792,50 +4859,66 @@ vars = Variables(files=None, args=ARGUMENTS) -A &Variables; object serves as a container for -descriptions of variables, -which are added by calling methods of the object. -Each variable consists of a name (which will -become a &consvar;), aliases for the name, +A &Variables; object is a container for variable descriptions, +added by calling the +Add or +AddVariables +methods. +A variable description consists of a name, +a list of aliases for the name, a help message, a default value, and functions to validate and convert values. -Once the object is asked to process variables, -it matches up data from the input -sources it was given with the definitions, -and generates key-value pairs which are added -to the specified &consenv;, -except that if any variable was described -to have a default of None, -it is not added to -the construction environment unless it -appears in the input sources. -Otherwise, a variable not in the -input sources is added using its default value. - - - -Note that since the variables are eventually added as &consvars;, +Processing of input sources is deferred until the +Update +method is called, +at which time the variables are added to the +specified &consenv;, +using the name as the &consvar; name; +any aliases are not added. +Variables from the input sources which do not match any +names or aliases from the variable descriptions in this object are skipped, +except that a dictionary of their names and values are made available in the +unknown +attribute of the &Variables; object. +This list can also be obtained via the +UnknownVariables +method. +If a variable description has a default value +other than None and does not +appear in the input sources, +it is added to the &consenv; with its default value. +A list of variables set from their defaults and +not from the input sources is available as the +defaulted +attribute of the &Variables; object. +The unknown variables and defaulted information is +not available until the &Update; method has run. + + + +Since the variables are eventually added as &consvars;, you should choose variable names which do not unintentionally change pre-defined &consvars; that your project will make use of (see for a reference), -since variables obtained have values overridden, not merged. +since the specified values are assigned, not merged, +to the respective &consvars;. -Also note there is currently no way to use the &Variables; -mechanism to define a variable which the user is -required to supply; -if necessary this can be implemented by accessing -&ARGUMENTS; directly, -although that only applies to the command line, -not to any stored-values files. +The &Variables; subsystem does not directly support a way +to define a variable the user must supply, +but this can be simulated by using a validator function, +and specifying a default value which the validator will reject, +resulting in an invalid value error message +(the convenience methods &EnumVariable; and +&ListVariable; make this relatively straightforward). -A Variables object has the following methods: +A &Variables; object has the following methods: - vars.Add(key, [help, default, validator, converter]) + vars.Add(key, [help, default, validator, converter, subst]) Add a customizable &consvar; to the &Variables; object. key @@ -4844,17 +4927,24 @@ or a sequence of strings, in which case the first item in the sequence is taken as the variable name, and any remaining values are considered aliases for the variable. key is mandatory, -there is no default. +the other fields are optional. help is the help text for the variable (defaults to an empty string). default is the default value of the variable (defaults to None). +The variable will be set to the value of +default if it does +not appear in the input sources, +except if default +is None, +in which case it is not added to the &consenv; +unless it has been set in the input sources. -If the optional validator argument is supplied, +If the validator argument is supplied, it is a callback function to validate the value of the variable when the variables are processed (that is, when the &Update; @@ -4869,7 +4959,7 @@ No return value is expected from the validator. -If the optional converter argument is supplied, +If the converter argument is supplied, it is a callback function to convert the value into one suitable for adding to the &consenv;. A converter function must accept the @@ -4887,6 +4977,16 @@ or there is no separate validator it can raise a ValueError. + +Substitution will be performed on the variable value +before the converter and validator are called, +unless the optional subst parameter +is false (the default is True). +Suppressing substitution may be useful if the variable value +looks like a &consvar; reference (e.g. $VAR) +and the validator and/or converter should see it unexpanded. + + As a special case, if key is a sequence and is the only @@ -4919,6 +5019,11 @@ def valid_color(key, val, env): vars.Add('COLOR', validator=valid_color) + + +Changed in version 4.8.0: +added the subst parameter. + @@ -4950,58 +5055,97 @@ opt.AddVariables( - - vars.Update(env, [args]) + + vars.FormatVariableHelpText(env, opt, help, default, actual, aliases) -Process the input sources recorded -when the &Variables; object was initialized -and update +Returns a formatted string +containing the printable help text +for the single variable opt. +All of the arguments must be supplied +except aliases, which is optional. env -with the customized &consvars;. -The names of any variables in the input sources that are not -configured in the &Variables; object -are recorded and may be retrieved using the -&UnknownVariables; -method. +is the &consenv; containing the variable values, +(env is not used by the standard +implementation of FormatVariableHelpText); +var +is the name of the variable; +help +is the text of the initial help message when the variable was +added to the &Variables; object; +default +is the default value assigned when the variable was added +to the &Variables; object; +actual +is the value as assigned in env +(which may be the same as default, +if none of the input sources assign to the variable); +and aliases +are any alias names for the variable, +if omitted defaults to an empty list. + -If the optional -args -argument is provided, it is a dictionary of variables -to use in place of the one saved when -&Variables; -was called. +FormatVariableHelpText +is normally not called directly, but by +&GenerateHelpText;, which does the work of +obtaining the necessary values. +You can patch in your own +function that takes the same function signature +in order to customize the appearance of variable help messages. +Example: -Normally, &Update; is not called directly, -but rather invoked indirectly by passing the &Variables; object to -the &f-link-Environment; function: - -env = Environment(..., variables=vars) +def my_format(env, var, help, default, actual): + fmt = "\n%s: default=%s actual=%s (%s)\n" + return fmt % (var, default, actual, help) + +vars.FormatVariableHelpText = my_format + +Note that &GenerateHelpText; +will not put any blank lines or extra +characters between the entries, +so you must add those characters to the returned +string if you want the entries separated. + - - vars.UnknownVariables() + + vars.GenerateHelpText(env, [sort]) -Returns a dictionary containing any -variables that were specified in the -files and/or -args parameters -when &Variables; -was called, but the object was not actually configured for. -This information is not available until the -Update -method has run. + +Return a formatted string with the help text collected +from all the variables configured in this &Variables; object. +This string is suitable for passing in to the &f-link-Help; function. +The generated string include an indication of the +actual value in the environment given by env. + + + +If the optional +sort parameter is set to +a callable value, it is used as a comparison function to +determine how to sort the added variables. +This function must accept two arguments, compare them, +and return a negative integer if the first is +less-than the second, zero if equal, or a positive integer +if greater-than. +If sort is not callable, +but evaluates true, +an alphabetical sort is performed. +The default is False (unsorted). -env = Environment(variables=vars) -for key, value in vars.UnknownVariables(): - print("unknown variable: %s=%s" % (key, value)) +Help(vars.GenerateHelpText(env)) + +def cmp(a, b): + return (a > b) - (a < b) + +Help(vars.GenerateHelpText(env, sort=cmp)) @@ -5031,91 +5175,116 @@ vars.Save('variables.cache', env) - - vars.GenerateHelpText(env, [sort]) + + vars.UnknownVariables() - -Return a formatted string with the help text collected -from all the variables configured in this &Variables; object. -This string is suitable for passing in to the &f-link-Help; function. -The generated string include an indication of the -actual value in the environment given by env. +Returns a dictionary containing any +variables that were specified in the +files and/or +args parameters +when &Variables; +was called, but the object was not actually configured for. +This information is not available until the +Update +method has run. + +env = Environment(variables=vars) +for key, value in vars.UnknownVariables(): + print("unknown variable: %s=%s" % (key, value)) + + + + + + + vars.Update(env, [args]) + +Process the input sources recorded +when the &Variables; object was initialized +and update +env +with the customized &consvars;. +The names of any variables in the input sources that are not +configured in the &Variables; object +are recorded and may be retrieved using the +&UnknownVariables; +method. + If the optional -sort parameter is set to -a callable value, it is used as a comparison function to -determine how to sort the added variables. -This function must accept two arguments, compare them, -and return a negative integer if the first is -less-than the second, zero if equal, or a positive integer -if greater-than. -If sort is not callable, -but is set to True, -an alphabetical sort is performed. -The default is False (unsorted). +args +argument is provided, it must be a dictionary of variables, +which will be used in place of the one saved when the +&Variables; object +was created. - -Help(vars.GenerateHelpText(env)) - -def cmp(a, b): - return (a > b) - (a < b) +Normally, &Update; is not called directly, +but rather invoked indirectly by passing the &Variables; object to +the &f-link-Environment; function: -Help(vars.GenerateHelpText(env, sort=cmp)) + +env = Environment(..., variables=vars) + - - vars.FormatVariableHelpText(env, opt, help, default, actual) - -Returns a formatted string -containing the printable help text -for the single option opt. -It is normally not called directly, -but is called by the &GenerateHelpText; -method to create the returned help text. -It may be overridden with your own -function that takes the arguments specified above -and returns a string of help text formatted to your liking. -Note that &GenerateHelpText; -will not put any blank lines or extra -characters in between the entries, -so you must add those characters to the returned -string if you want the entries separated. + +A &Variables; object also makes available two data attributes +that can be read for further information. These only have +values if Update +has previously run. + - -def my_format(env, opt, help, default, actual): - fmt = "\n%s: default=%s actual=%s (%s)\n" - return fmt % (opt, default, actual, help) + + + vars.defaulted + + +A list of variable names that were set in the &consenv; +from the default values in the variable descriptions - +that is, variables that have a default value and were +not defined in the input sources. + + + -vars.FormatVariableHelpText = my_format - + + vars.unknown + + +A dictionary of variables that were specified in the input sources, +but do not have matching variable definitions. +This is the same information that is returned by the +&UnknownVariables; method. + - +Added in 4.9.0: +the defaulted attribute. + -To make it more convenient to describe custom variables, -&SCons; provides some pre-defined variable types, -acessible through factory functions that generate -a tuple appropriate for directly passing to -the &Add; or &AddVariables; method: +&SCons; provides five pre-defined variable types, +accessible through factory functions that generate +a tuple appropriate for directly passing to the +Add or +AddVariables +methods. + BoolVariable(key, help, default) -Set up a Boolean variable. -The variable will use -the specified name -key, -have a default value of +Set up a Boolean variable named key. +The variable will have a default value of default, and help will form the descriptive part of the help text. @@ -5146,12 +5315,10 @@ as false. EnumVariable(key, help, default, allowed_values, [map, ignorecase]) -Set up a variable -whose value may only be from +Set up a variable named key +whose value may only be chosen from a specified list ("enumeration") of values. -The variable will have the name -key, -have a default value of +The variable will have a default value of default and help will form the descriptive part of the help text. @@ -5164,23 +5331,14 @@ argument is a dictionary that can be used to map additional names into a particular name in the allowed_values list. -If the value of optional -ignore_case -is -0 -(the default), -then the values are case-sensitive. -If the value of -ignore_case -is -1, -then values will be matched +If the optional +ignorecase is 0 (the default), +the values are considered case-sensitive. +If ignorecase is 1, +values will be matched case-insensitively. -If the value of -ignore_case -is -2, -then values will be matched +If ignorecase is 2, +values will be matched case-insensitively, and all input values will be converted to lower case. @@ -5188,15 +5346,13 @@ converted to lower case. - ListVariable(key, help, default, names, [map]) + ListVariable(key, help, default, names, [map, validator]) -Set up a variable -whose value may be one or more +Set up a variable named key +whose value may be one or more choices from a specified list of values. -The variable will have the name -key, -have a default value of +The variable will have a default value of default, and help will form the descriptive part of the help text. @@ -5205,11 +5361,12 @@ Any value that is not in all or none will raise an error. -More than one value may be specified, -separated by commas. +Use a comma separator to specify multiple values. default may be specified -either as a string of comma-separated value, -or as a list of values. +either as a string of comma-separated values, +or as a &Python; list of values. + + The optional map argument is a dictionary @@ -5221,6 +5378,15 @@ list. (Note that the additional values accepted through the use of a map are not reflected in the generated help message). + +The optional validator argument +can be used to specify a custom validator callback function, +as described for Add. +The default is to use an internal validator routine. + +Added in 4.8.0: +the validator parameter. + @@ -5228,49 +5394,64 @@ reflected in the generated help message). PackageVariable(key, help, default) -Set up a variable for a package, -where if the variable is specified, -the &consvar; named by key -will end with a value of True, -False, or a user-specified value. -For example, -a package could be a third-party software component, -the build could use the information to -exclude the package, include the package in the standard way, -or include the package using a specified -directory path to find the package files. +Set up a variable named key +to help control a build component, +such as a software package. +Similar to a +BoolVariable, +but can also specify a path string +to provide additional information to the build, +for example the path to a configuration file describing the package, +or a directory containing the package headers and libraries. +The variable will have a default value of +default, +and the help parameter +will be used as the descriptive part of the help text. + -The variable will have a default value -default, -and help -will form the descriptive part of the help text. -The variable supports (case-insensitive) truthy values +Any of the (case-insensitive) strings 1, yes, true, on, enable -and +or search -to indicate the package is "enabled", -and the (case-insensitive) falsy values +can be used to indicate the package is "enabled", +and can also be used as the value of default. +A value of boolean True is +produced except for the special case described +for a path string. + + + +Any of the (case-insensitive) strings 0, no, false, off -and +or disable -to indicate the package is "disabled". +can be used to indicate the package is "disabled", +and can also be used as the value of default. +A value of boolean False is +produced. + -The value -of the variable may also be set to an -arbitrary string, -which is taken to be the path name to the package -that is being enabled. -The validator will raise an exception -if this path does not exist in the filesystem. +A string which is neither an enabling or disabling +string is considered a "path string". +A path string can be given when specifying the variable, +and can also be used as the value of default. +A path string must refer to an existing filesystem path, +but any further meaning is left to the build system to decide. +The path string is produced. +As a special case, +if default is a path string, +and the variable is specified with an enabling string, +the default path string is produced, +rather than True. @@ -5279,22 +5460,18 @@ if this path does not exist in the filesystem. PathVariable(key, help, default, [validator]) -Set up a variable -whose value is expected to be a path name. -The &consvar; named by key -will have have a default value of +Set up a variable named key to hold a path string. +The variable will have a default value of default, -and help -will form the descriptive part of the help text. +and the help parameter +will be used as the descriptive part of the help text. -An optional -validator argument -may be specified. -The validator will be called to -verify that the specified path -is acceptable. +The optional +validator parameter +describes a callback function which will be called to +verify that the specified path is acceptable. SCons supplies the following ready-made validators: @@ -5361,8 +5538,10 @@ if the specified value is not acceptable. These functions make it convenient to create a number of variables with consistent behavior -in a single call to the &AddVariables; -method: +in a single call to the +AddVariables +method: + vars.AddVariables( @@ -5377,7 +5556,7 @@ vars.AddVariables( default="no", allowed_values=("yes", "no", "full"), map={}, - ignorecase=0, # case sensitive + ignorecase=0, # case-sensitive ), ListVariable( "shared", @@ -5393,7 +5572,8 @@ vars.AddVariables( PathVariable( "qtdir", help="where the root of Qt is installed", - default=qtdir), + default=qtdir + ), PathVariable( "foopath", help="where the foo library is installed", @@ -5564,7 +5744,7 @@ in the directory represented by If the original Node is a File Node, -these methods will place the the new Node in the same +these methods will place the new Node in the same directory as the one the original Node represents: @@ -5641,7 +5821,7 @@ Aliases are virtual objects - they will not themselves result in physical objects being constructed, but are entered into the dependency graph related to their sources. An alias is checked for up to date by checking if -its sources are up to date. +its sources are up-to-date. An alias is built by making sure its sources have been built, and if any building took place, applying any Actions that are defined as part of the alias. @@ -5653,7 +5833,7 @@ which is used for disambiguation. If an alias source has a string valued name, it will be resolved to a filesystem entry Node, unless it is found in the alias namespace, -in which case it it resolved to the matching alias Node. +in which case it is resolved to the matching alias Node. As a result, the order of &f-Alias; calls is significant. An alias can refer to another alias, but only if the other alias has previously been created. @@ -5663,7 +5843,7 @@ other alias has previously been created. The &f-link-Value; method returns a Value Node. Value nodes are often used for generated data that will not have any corresponding filesystem entry, -but will be used to determine whether a build target is out of date, +but will be used to determine whether a build target is out-of-date, or to include as part of a build Action. Common examples are timestamp strings, revision control version strings @@ -5820,7 +6000,7 @@ If the suffix is a string, then &scons; prepends a '.' to the suffix if it's not already there. The string returned by the callable object or obtained from the -dictionary is untouched and you need to manually prepend a '.' +dictionary is untouched, and you need to manually prepend a '.' if one is required. @@ -7253,7 +7433,7 @@ changed since the target was last built. These names are reserved and may not be assigned to or used as &consvars;. &SCons; computes them in a context-dependent manner -and they and are not retrieved from a &consenv;. +and they are not retrieved from a &consenv;. For example, the following builder call: @@ -7593,7 +7773,7 @@ expensive for limited benefit - consider for example the C standard header file string.h. The scanner function is not passed any special information -to help make this choice, so the decision making encoded +to help make this choice, so the decision-making encoded in the scanner function must be carefully considered. @@ -7750,7 +7930,7 @@ Nodes for additional scanning. -Once created, a Scanner can added to an environment +Once created, a Scanner can be added to an environment by setting it in the &cv-link-SCANNERS; list, which automatically triggers &SCons; to also add it to the environment as a method. @@ -7924,7 +8104,7 @@ env = Environment(tools=[my_tool]) An element of the tools list may also be a two-element list or tuple of the form (toolname, kw_dict). -SCons searches for the a tool specification module +SCons searches for the tool specification module toolname as described above, and passes kw_dict, @@ -8023,7 +8203,7 @@ dialect setup (expressed as a set of &consvars;) depending on the file suffix. By default, all of these setups start out the same, but individual &consvars; can be modified as needed to tune a given dialect. -Each of these dialacts has a tool specification module +Each of these dialects has a tool specification module whose documentation describes the &consvars; associated with that dialect: .f (as well as .for and .ftn) @@ -8052,7 +8232,7 @@ code that was the only available option in FORTRAN 77 and earlier, and .f90 refers to free-format source code which became available as of the Fortran 90 standard. Some compilers recognize suffixes which correspond to Fortran -specifications later then F90 as equivalent to +specifications later than F90 as equivalent to .f90 for this purpose, while some do not - check the documentation for your compiler. An occasionally suggested policy suggestion is to use only @@ -8102,7 +8282,7 @@ which shall first be run through the standard C preprocessor. The lower-cased versions of these suffixes do not trigger this behavior. -On systems which do not distinguish between uppper +On systems which do not distinguish between upper and lower case in filenames, this behavior is not available, but files suffixed with either @@ -8182,8 +8362,8 @@ in particular the recommendation to use the msys2 version of scons.bat batch file, there are (at least) two ramifications. Note this is no longer the default - &scons; installed -via &Python;''s pip installer -will have an scons.exe which does +via &Python;'s pip installer +will have a scons.exe which does not have these limitations: @@ -8804,7 +8984,7 @@ env.Program('MyApp', ['Foo.cpp', 'Bar.cpp']) In general, &scons; is not controlled by environment variables set in the shell used to invoke it, leaving it up to the &SConscript; file author to import those if desired. -However the following variables are imported by +However, the following variables are imported by &scons; itself if set: diff --git a/doc/overview.rst b/doc/overview.rst index 74aa6886d9..8a1d56a79e 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -6,19 +6,25 @@ SCons Documentation Toolchain Introduction ============ -This text tries to give an overview of the current SCons documentation -toolchain. As a prospective doc editor, you should be able to quickly +This is an overview of the current SCons documentation toolchain. +As a prospective doc editor, you should be able to quickly understand the basic concepts (if not, please let the project know how it falls short). It is also a reference for core developers and the release team. .. image:: images/overview.png -The diagram above roughly shows the steps that we currently need for -creating all the MAN pages, User manuals and reference documents. You -may think: "Geeez, that looks so complicated. Why can't they simply -convert XML files to PDF with Docbook, or use reST?" Please be patient, -and continue reading. Things will get a little clearer soon. +The diagram above roughly shows the steps that we currently use. +Note there are two separate sets of documentation maintained with +the project code itself: the reference manual and user guide, +which have xml source files; and the docstrings in the Python code, +from which the API documentation is generated. Most of the time +when we talk about "SCons Documentation" we mean the former, +as that's what is intended for end-user consumption. + +If you look at the diagram, it looks a bit complicated. after +reading this overview, it will be clear what is actually happening, +and why we need all these steps. Our toolchain doesn't only produce HTML and PDF files that are nice to look at, it also performs a lot of processing under the covers. We @@ -30,31 +36,36 @@ So let's start right at the top... Writer's view ============= -SCons documentation is written in Docbook (the xml variant). -The toolchain is set up so that a writer has a restricted view of the +SCons documentation is written in Docbook XML. +The toolchain is set up so a writer has a restricted view of the whole "document processing thingy". All you should need to be concerned with is to edit existing text or write new sections and paragraphs. Sometimes even a completely new chapter has to be added. The hope is that you can fire up your XML editor of choice and type away. -XML is easy to get wrong, so you need to case about +XML is easy to get wrong, so you need to care about validating the XML files -against our special "SCons Docbook DTD/XSD". You can run the -special script +against our special "SCons Docbook DTD/XSD". +If you're not using an XML editor, validate by :: python bin/docs-validate.py -from the top source folder to validate. If you are able to use -an XML editor, many of the potential problems are avoided - -the most common error is not matching tag opening and closing +from the top source folder. If you are able to use +an XML editor, many of the potential problems are avoided, +as it will complain real-time. +The most common error is not matching tag opening and closing (for example ``foo`` is an easy typing error to make, as is starting a ````, typing text, and not adding the -closing ````). XML editors make it much harder to make -those errors, which minor though they seem, will completely -break the document build. +closing ````, as is mistyping one of the two +tag markers). XML editors make it much harder to make +those errors, which, minor though they seem, will completely +break the document build. Unfortunately, the validation tools +(which rely on the capabilities of the XML parser in use) +are not that hard to fool, so you may get errors which are +not that easy to deciper. There isn't much we can do about that. Everything's looking okay, all validation passed? Good, then simply commits your new work, and create a pull request on Github. That's it! @@ -71,6 +82,8 @@ the actual XML files. You can call from within the directory, and have the MAN pages or HTML created...even PDF, if you have a renderer installed (``fop``, ``xep`` or ``jw``). +At least ``fop`` doesn't like everything the docbook/xml chain +produces and will spew a lot of errors, which we *think* are harmless. Validation ========== @@ -104,10 +117,11 @@ These entities can be used in the MAN page and the User manual. Note that the four type tags themselves (````, ````, ```` and ````) can only be used in documentation sources in the ``SCons`` directory; the build will not scan for these -in the ``doc`` directory. +tags in files in the ``doc`` directory. -When you add an XML file in the ``SCons/Tools`` folder, e.g. for -a tool named ``foobar``, you can use the two entities +When you add tool documentation using the ```` tag, +let's say for a tool named ``foobar``, you can use the two +automatically generated entities *t-foobar* which prints the name of the Tool, and @@ -118,6 +132,10 @@ a tool named ``foobar``, you can use the two entities The link will be to the appropriate Appendix in the User Guide, or to the proper section in the manpage. +The ```` tag similarly generates entities with the *b-* prefix, +the ```` tag generates entities with the *f-* prefix, +and the ```` tag generates entities with the *cv-* prefix. + In the case of Functions, there may be pairs of these, depending on the value of the signature attribute: this attribute tells whether only the global function form, or only the environment @@ -144,7 +162,7 @@ By calling the script you can recreate the lists of entities (``*.mod``) in the ``generated`` folder. At the same time, this will generate the matching ``*.gen`` -files, which list the full description of all the Builders, Tools, +files, which hold the full descriptions of all the Builders, Tools, Functions and CVars for the MAN page and the User Guide's appendix. Thus, you want to regenerate when there's a change to any of those four special elements, or an added or deleted element. @@ -201,6 +219,47 @@ to write it all in text, as in *See the manpage section "Builder Objects"* than to leave a dangling reference in one of the docs. +Context +======= +While it is very convenient to document related +things together in one xml file, and this is encouraged +as it helps writers keep things in sync, +be aware the information recorded inside the four special tags +will not be presented together in the output documents. +For example, when documenting a Tool in +``SCons/Tool/newtool.xml`` using the ```` tag, +and noting that the tool ```` or ```` +certain construction variables, +those construction variables can be documented +right there as well using ```` tags. +When processed with ``SConsDoc`` module, +this will generate xml from the +```` tag into the ``tools.{gen,mod}`` files, +and xml from the ```` tag into +the ``variables.{gen,mod}`` files; +those files are then included each into their own +section, so the entries may end up separated by +hundreds of lines in the final output. +The special entries will also be sorted in their +own sections, which might cause two entries using the +same tag in the same source file to be separated. +All this to say: do not write your doc text +with the idea that the locality you see in the xml source file +will be preserved when consumed in a web browser, +manpage viewer, PDF file, etc. Provide sufficient context +so entries can stand on their own. + +Another quirk is that ``SConsDoc`` +will take all occurrences of a special tag and +combine those contents into a single entry in the generated file. +As such, normally there should be only one definition of +each element project-wide. This particularly comes up in tool definitions, +as several tools may refer to the same construction variable. +It is suggested to pick one file to write the documentation in, +and then in the other tool documents referencing it, +place a comment indicating which file the variable is documented in - +this will keep future editors from having to hunt too far for it. + SCons Examples ============== @@ -224,7 +283,7 @@ Before this script starts to generate any output, it checks whether the names of all defined examples are unique. Another important prerequisite is that for every example all the single ``scons_output`` blocks need to have a ``suffix`` attribute defined. These suffixes also have to be -unique, within each example. +unique, within each example, as this controls the ordering. All example output files (``*.xml``) get written to the folder ``doc/generated/examples``, together with all files defined via the @@ -233,8 +292,14 @@ part so that the version control system can show any unexpected changes in the outputs after editing the docs: :: + git diff doc/generated/examples +Some of the changes in example text are phony: despite best +efforts to eliminate system-specifics, sometimes they leak through. +There is at least one example that gets the pathname to the +build directory of the machine the example is generated on. + Note that these output files are not actually needed for editing the documents. When loading the User manual into an XML editor, you will always see the example's definition. Only when you create some output, diff --git a/doc/python10/summary.xml b/doc/python10/summary.xml index a8f8767e97..0b611984b9 100644 --- a/doc/python10/summary.xml +++ b/doc/python10/summary.xml @@ -40,7 +40,7 @@ This paper has introduced &SCons;, a next-generation build tool with a modular, embeddable architecture and a direct Python interface. &SCons; has a global view of the dependencies in a source - tree, uses MD5 signatures to decide if derived files are out of date, + tree, uses MD5 signatures to decide if derived files are out-of-date, and automatically scans files for dependencies, all of which make &SCons; builds exceptionally reliable. The &SCons; development methodology has been described, notable for its emphasis on automated regression diff --git a/doc/scons.mod b/doc/scons.mod index 938886ba99..ba70c056bf 100644 --- a/doc/scons.mod +++ b/doc/scons.mod @@ -1,39 +1,21 @@ + - + Action"> Builder"> Builders"> @@ -171,9 +154,11 @@ Add"> @@ -323,8 +308,10 @@ -IndexError"> -NameError"> +AttributeError"> +IndexError"> +KeyError"> +NameError"> str"> zipfile"> diff --git a/doc/sphinx/SCons.rst b/doc/sphinx/SCons.rst index 9ab44b020e..951504d403 100644 --- a/doc/sphinx/SCons.rst +++ b/doc/sphinx/SCons.rst @@ -9,21 +9,6 @@ Module contents :undoc-members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - - SCons.Node - SCons.Platform - SCons.Scanner - SCons.Script - SCons.Taskmaster - SCons.Tool - SCons.Util - SCons.Variables - SCons.compat - Submodules ---------- @@ -174,3 +159,27 @@ SCons.exitfuncs module :members: :undoc-members: :show-inheritance: + +SConsDoc documentation module +----------------------------- + +This module is NOT part of the SCons build tool itself. +It is supporting tooling, invoked by tools used to build +documentation components. + +.. automodule:: bin.SConsDoc + :members: + :undoc-members: + :show-inheritance: + +SConsExamples documentation module +---------------------------------- + +This module is NOT part of the SCons build tool itself. +It is supporting tooling, invoked by tools used to build +documentation components. + +.. automodule:: bin.SConsExamples + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index 8bfdc419e5..5b9cbb0d2d 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -18,6 +18,9 @@ # import os import sys + +sys.path.insert(0, os.path.abspath('../../testing/framework')) +sys.path.insert(0, os.path.abspath('../../bin')) sys.path.insert(0, os.path.abspath('../../')) # -- General configuration ------------------------------------------------ @@ -79,8 +82,7 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -# -source_suffix = '.rst' +source_suffix = {'.rst': 'restructuredtext'} # The master toctree document. master_doc = 'index' diff --git a/doc/sphinx/index.rst b/doc/sphinx/index.rst index a21b9db4b7..f79069c448 100644 --- a/doc/sphinx/index.rst +++ b/doc/sphinx/index.rst @@ -1,5 +1,4 @@ -.. SCons documentation master file, originally created by - sphinx-quickstart on Mon Apr 30 09:36:53 2018. +.. SCons documentation master file, originally created by sphinx-quickstart on Mon Apr 30 09:36:53 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. @@ -7,21 +6,44 @@ SCons API Documentation ======================= .. Attention:: + This is the **internal** API Documentation for SCons. - The documentation is automatically generated for each release - from the source code using the - `Sphinx `_ documentation generator. - Missing information is due to shortcomings in the docstrings in the code, - which admittedly could use a lot more work (contributions welcomed!). - - The target audience is both developers working on SCons itself, - and those writing external Tools, Builders, etc. and other - related functionality, who need to reach beyond the Public API. - Note that what is Public API is not clearly deliniated in the API Docs. - The interfaces available for use in SCons configuration scripts - ("SConscript files"), which have a consistency guarantee, - are those documented in the `SCons Reference Manual - `_. + It is generated automatically from code docstrings using + the `Sphinx `_ documentation generator, + and covers much more than the Public API. + If you were looking for the Public API - the interfaces that have + long-term consistency guarantees, which you can reliably use when + writing a build system for a project - see the `SCons Reference Manual + `_. Note that + what is Public API and what is not is often not clearly delineated in + these API Docs. + + The target audience is developers contributing to SCons itself, + and those writing external Tools, Builders, and other related + functionality for their project, who may need to reach beyond the + Public API to accomplish their tasks. Reaching into internals is fine, + but comes with the usual risks of "things here could change, it's up + to you to keep your code working". + + Any missing/incomplete information is due to shortcomings in the + docstrings in the code. Without being flippant, filling + in all the docstrings has not always been a priority across the + two-plus decades SCons has been in existence. Contributions + improving the docstring front are welcome. It is often useful when + making some larger change to fill in docstrings and suitable + type annotations in the code being modified, "leaving the world + a better place", as it were. + + Note that the Sphinx configuration is limited, still a work + in progress. SCons classes which inherit from Python standard + library classes (e.g. ``UserList``, ``UserDict``, ``UserString``), + may be allowed to show inherited mmembers; the Python standard library + occasionally has little to no helpful docstring information. + Inherited interfaces from outside SCons code can be identified by the + lack of a ``[source]`` button to the right of the method signature. + Such classes should be evaluated case-by-case and possibly + switched to not show inherited members, depending on which way + seems to provide the more useful result. .. toctree:: :maxdepth: 2 @@ -38,7 +60,6 @@ SCons API Documentation SCons.Util SCons.Variables - Indices and Tables ================== diff --git a/doc/user/actions.xml b/doc/user/actions.xml index d357eb6190..ed2eeb7df6 100644 --- a/doc/user/actions.xml +++ b/doc/user/actions.xml @@ -3,6 +3,9 @@ def install_in_bin_dirs(env, source): - """Install source in both bin dirs""" + """Install source in both bin directories""" i1 = env.Install("$BIN", source) i2 = env.Install("$LOCALBIN", source) return [i1[0], i2[0]] # Return a list, like a normal builder env = Environment(BIN='__ROOT__/usr/bin', LOCALBIN='#install/bin') env.AddMethod(install_in_bin_dirs, "InstallInBinDirs") -env.InstallInBinDirs(Program('hello.c')) # installs hello in both bin dirs +env.InstallInBinDirs(Program('hello.c')) # installs hello in both bin directories int main() { printf("Hello, world!\n"); } diff --git a/doc/user/alias.xml b/doc/user/alias.xml index 0482b4bbea..20c60a8423 100644 --- a/doc/user/alias.xml +++ b/doc/user/alias.xml @@ -3,6 +3,9 @@ "command not found" (on UNIX or Linux) or "'python' is not recognized as an internal - or external command, operable progam or batch file" + or external command, operable program or batch file" (on Windows cmd). In that case, you need to either install &Python; or fix the search path @@ -152,7 +155,7 @@ Python 3.9.15 Recent versions of the Mac no longer come with &Python; - pre-installed; older versions came with a rather out of date + pre-installed; older versions came with a rather out-of-date version (based on &Python; 2.7) which is insufficient to run current &SCons;. The python.org installer can be used on the Mac, but there are @@ -170,7 +173,7 @@ Python 3.9.15 - &SCons; will work with &Python; 3.6 or later. + &SCons; will work with &Python; 3.7 or later. If you need to install &Python; and have a choice, we recommend using the most recent &Python; version available. Newer &Python; versions have significant improvements @@ -295,7 +298,7 @@ $ python /path/to/unpacked/scripts/scons.p of &Python; software). There is a setup.py file, but it is only tested and used for the automated procedure which prepares an &SCons; bundle for making a release on PyPI, - and even that is not guaranteed to work in future. + and even that is not guaranteed to work in the future. diff --git a/doc/user/builders-built-in.xml b/doc/user/builders-built-in.xml index be3eed72f7..3160223300 100644 --- a/doc/user/builders-built-in.xml +++ b/doc/user/builders-built-in.xml @@ -3,6 +3,9 @@ - Note that &cv-link-SOURCE; and &cv-link-TARGET; are expanded - in the source and target as well, so you can write: + &cv-link-SOURCE; and &cv-link-TARGET; are expanded + in the source and target as well: - - -env.Command('${SOURCE.basename}.out', 'foo.in', build) - - + + +env.Command('${SOURCE.base}.out', File('foo.in'), build) + - which does the same thing as the previous example, but allows you - to avoid repeating yourself. + Which does the same thing as the previous example, but allows you + to write a more generic rule for transforming the source filename + to the target filename, since unlike regular Builders, + &Command; does not have any built-in rules for that. + + Sidebar: Node Special Attributes + - It may be helpful to use the action - keyword to specify the action, is this makes things more clear - to the reader: + The example uses a Node special attribute + (.base, the file without its suffix), + a concept which has not been introduced yet, + but will appear in several subsequent examples + (see details in the Reference Manual section + Substitution: Special Attributes). + Due to the quirks of &SCons;' deferred evaluation scheme, + node special attribues do not currently work + in source and target arguments if the + replacement is a string (like 'foo.in'). + They do work fine in strings describing actions. + You can give &SCons; a little help by + manually converting the filename string to a Node + (see ), + which is the approach used in the example. - - - -env.Command('${SOURCE.basename}.out', 'foo.in', action=build) - - + @@ -184,11 +203,13 @@ env.Command('${SOURCE.basename}.out', 'foo.in', action=build) which include a message based on the type of action. However, you can also construct the &Action; object yourself to pass to &f-Command;, which gives you much more control. + Using the action keyword can also help + make such lines easier to read. Here's an evolution of the example from above showing this approach: - + env = Environment() @@ -197,7 +218,7 @@ def build(target, source, env): return None act = Action(build, cmdstr="Building ${TARGET}") -env.Command('foo.out', 'foo.in', action=act) +env.Command('${SOURCE.base}.out', File('foo.in'), action=act) foo.in @@ -210,7 +231,7 @@ foo.in - + scons -Q diff --git a/doc/user/builders-writing.xml b/doc/user/builders-writing.xml index eb7d974b72..8d2c948bf3 100644 --- a/doc/user/builders-writing.xml +++ b/doc/user/builders-writing.xml @@ -3,6 +3,9 @@ @@ -129,7 +132,7 @@ env.Foo('file.foo', 'file.input') - Then when we run &SCons; it looks like: + Then, when we run &SCons; it looks like: @@ -816,7 +819,7 @@ cat to the file specified by the option's argument. If this option is just supplied to the build, &SCons; will not consider the link map file a tracked target, - which has various undesirable efffects. + which has various undesirable effects. @@ -924,7 +927,7 @@ main() The site_scons directories give you a place to put &Python; modules and packages that you can import into your - &SConscript; files (at the top level) + &SConscript; files (at the top level), add-on tools that can integrate into &SCons; (in a site_tools subdirectory), and a site_scons/site_init.py file that @@ -938,9 +941,9 @@ main() Each system type (Windows, Mac, Linux, etc.) searches a canonical set of directories for site_scons; see the man page for details. - The top-level SConstruct's site_scons dir + The top-level SConstruct's site_scons directory (that is, the one in the project) is always searched last, - and its dir is placed first in the tool path so it overrides all + and its directory is placed first in the tool path so it overrides all others. @@ -949,7 +952,7 @@ main() If you get a tool from somewhere (the &SCons; wiki or a third party, for instance) and you'd like to use it in your project, a - site_scons dir is the simplest place to put it. + site_scons directory is the simplest place to put it. Tools come in two flavors; either a &Python; function that operates on an &Environment; or a &Python; module or package containing two functions, exists() and generate(). @@ -1035,7 +1038,7 @@ env.AddHeader('tgt', 'src') to include in their &SConscript; files: just put them in site_scons/my_utils.py or any valid &Python; module name of your - choice. For instance you can do something like this in + choice. For instance, you can do something like this in site_scons/my_utils.py to add build_id and MakeWorkDir functions: diff --git a/doc/user/builders.xml b/doc/user/builders.xml index 9383424b4a..7ae5fab269 100644 --- a/doc/user/builders.xml +++ b/doc/user/builders.xml @@ -3,6 +3,9 @@ @@ -121,7 +124,7 @@ CacheDir('/usr/local/build_cache') - The use of the &buildsig; provides protection from concflicts: + The use of the &buildsig; provides protection from conflicts: if two developers have different setups, so they would produce built objects that are not identical, then because the difference in tools will show up in the &buildsig;, which is used as the @@ -237,7 +240,7 @@ hello.c - Then when you run &scons; after cleaning + Then, when you run &scons; after cleaning the built targets, it will recompile the object file locally (since it doesn't exist in the derived-file cache directory), @@ -334,7 +337,7 @@ Retrieved `hello' from cache - In this case, you can use the + In this case, you can use the option to tell &SCons; to put all derived files in the cache, even if the files already exist in your local tree diff --git a/doc/user/command-line.xml b/doc/user/command-line.xml index fea1af1336..8e0e0a9de1 100644 --- a/doc/user/command-line.xml +++ b/doc/user/command-line.xml @@ -3,6 +3,9 @@ - &SetOption; is not currently supported for - options added with &AddOption;. + &SetOption; works for options added with &AddOption;, + but only if they were created with + settable=True in the call to &AddOption; + (only available in SCons 4.8.0 and later). @@ -551,6 +556,11 @@ foo.in + + no_progress + + + no_site_dir @@ -660,14 +670,12 @@ foo.in - &SetOption; is not currently supported for - options added with &AddOption;. - + + &f-link-SetOption; works for options added with &AddOption;, + but only if they were created with + settable=True in the call to &AddOption; + (only available in SCons 4.8.0 and later). + @@ -792,7 +800,7 @@ foo.in You may want to control various aspects of your build by allowing variable=value - values to be specified on the command line. + pairs to be specified on the command line. For example, suppose you want to be able to build a debug version of a program by running &SCons; as follows: @@ -871,7 +879,7 @@ prog.c get method, so you can supply a default value if the variable is not given on the command line. Otherwise, the build - will fail with a KeyError + will fail with a &KeyError; if the variable is not set. diff --git a/doc/user/depends.xml b/doc/user/depends.xml index 961edb2c9b..d4083535f6 100644 --- a/doc/user/depends.xml +++ b/doc/user/depends.xml @@ -3,6 +3,9 @@ @@ -552,7 +555,7 @@ int main() { printf("Hello, world!\n"); } The &contentsig;: - a cryptgraphic hash, or checksum, of the file contents + a cryptographic hash, or checksum, of the file contents of the dependency file the last time the ⌖ was built. @@ -963,7 +966,7 @@ SetOption('implicit_cache', 1) When &implicit-cache; is used, &SCons; will ignore any changes that may have been made to search paths - (like &cv-CPPPATH; or &cv-LIBPATH;,). + (like &cv-CPPPATH; or &cv-LIBPATH;). This can lead to &SCons; not rebuilding a file if a change to &cv-CPPPATH; would normally cause a different, same-named file from a different directory to be used. @@ -998,7 +1001,7 @@ SetOption('implicit_cache', 1) external code that you use for compilation, the external header files will have changed and the previously-cached implicit dependencies - will be out of date. + will be out-of-date. You can update them by running &SCons; with the &implicit-deps-changed; option: @@ -1023,7 +1026,7 @@ SetOption('implicit_cache', 1) - By default when caching dependencies, + By default, when caching dependencies, &SCons; notices when a file has been modified and re-scans the file for any updated implicit dependency information. diff --git a/doc/user/environments.xml b/doc/user/environments.xml index b4f86892c7..75d8ddd736 100644 --- a/doc/user/environments.xml +++ b/doc/user/environments.xml @@ -3,6 +3,9 @@ - Unlike &Make;, &SCons; does not automatically + Unlike &Make;, &SCons; does not automatically copy or import values between different environments (with the exception of explicit clones of &consenvs;, which inherit the values from their parent). @@ -455,26 +458,39 @@ environment, of directory names, suffixes, etc. If you're not familiar with the &Python; programming language, - we need to talk a little bit about the &Python; dictionary data type. - A dictionary (also known by terms such as mapping, associative array - and key-value store) associates keys with values, such - that asking the dict about a key gives you back the associated value - and assigning to a key creates the association - either a new - setting if the key was unknown, or replacing the - previous association if the key was already in the dictionary. - Values can be retrieved using item access - (the key name in square brackets ([])), - and dictionaries also provide a - method named get which responds - with a default value, either None or a value - you supply as the second argument, if the key is not in the dictionary, - which avoids failing in that case. The syntax - for initializing a dictionary uses curly braces ({}). - Here are some simple examples (inspired by those in the official - Python tutorial) using syntax that indicates - interacting with the &Python; interpreter - (>>> is the interpreter prompt) - - you can try these out: + here is a short summary of the &Python; dictionary data type, + or "dict". You may also see the terms mapping, associative array + or key-value store used for this type of data structure, + which appears in many programming languages. + + + + + + A dictionary associates keys with values, + so asking the dict about a key gives you back the associated value. + Values can be retrieved using item access: + the key name string in square brackets (mydict["keyname"]). + If the key is not present, you get a &KeyError; exception. + Dicts also provide a get() method + which returns a default value if the key is not present, + so it does not fail in that case. + You can specity the default as a second argument to the + get call, otherwise it defaults to + None. + + + + + + Assigning to a key creates the association - either a new + key/value pair if the key was unknown, or replacing the + previous value if the key was already in the dictionary. + Initializing a dictionary uses curly braces ({}). + Here are some simple examples inspired by those in the official + &Python; tutorial, as you would see them if you typed these to + the interactive &Python; interpreter + (>>> is the interpreter prompt): @@ -500,7 +516,7 @@ None &Consenvs; are written to behave like a &Python; - dictionary, and the &cv-ENV; construction variable in + dictionary, and the &cv-ENV; &consvar; in a &consenv; is a &Python; dictionary. The os.environ value that &Python; uses to make available the external environment is also a @@ -524,7 +540,7 @@ None os.environ dictionary. That syntax means the environ attribute of the os module. - In Python, to access the contents of a module you must first + In &Python;, to access the contents of a module you must first import it - so you would include the import os statement to any &SConscript; file @@ -575,18 +591,18 @@ void main() { } A &consenv; is an object that has a number of associated &consvars;, each with a name and a value, just like a dictionary. - (A construction environment also has an attached + (A &consenv; also has an attached set of &Builder; methods, - about which we'll learn more later.) + which you'll learn more about later.) -
- Creating a &ConsEnv;: the &Environment; Function +
+ Creating a &ConsEnv;: the &Environment; Function - A &consenv; is created by the &Environment; method: + A &consenv; is created by the &f-link-Environment; function: @@ -596,13 +612,12 @@ env = Environment() - By default, &SCons; initializes every - new construction environment + &SCons; initializes every new &consenv; with a set of &consvars; based on the tools that it finds on your system, plus the default set of builder methods necessary for using those tools. - The construction variables + The &consvars; are initialized with values describing the C compiler, the Fortran compiler, @@ -614,7 +629,7 @@ env = Environment() - When you initialize a construction environment + When you initialize a &consenv; you can set the values of the environment's &consvars; to control how a program is built. @@ -634,18 +649,18 @@ int main() { } - The construction environment in this example + The &consenv; in this example is still initialized with the same default - construction variable values, + &consvar; values, except that the user has explicitly specified use of the GNU C compiler &gcc;, and that the (optimization level two) flag should be used when compiling the object file. - In other words, the explicit initializations of + In other words, the explicit initialization of &cv-link-CC; and &cv-link-CCFLAGS; - override the default values in the newly-created - construction environment. + overrides the default values in the newly-created + &consenv;. So a run from this example would look like: @@ -656,13 +671,13 @@ int main() { }
-
+
Fetching Values From a &ConsEnv; You can fetch individual values, known as - Construction Variables, + &ConsVars;, using the same syntax used for accessing individual named items in a &Python; dictionary: @@ -682,8 +697,9 @@ print("LATEX is: %s" % env.get('LATEX', None)) for building any targets, but because it's still a valid &SConstruct; it will be evaluated and the &Python; print calls will output the values - of &cv-link-CC; and &cv-link-LATEX; for us (remember using the - .get() method for fetching means + of &cv-link-CC; and &cv-link-LATEX; for us (remember + from the sidebar that using the + get() method for access means we get a default value back, rather than a failure, if the variable is not set): @@ -761,7 +777,7 @@ print(env.Dump())
-
+
Expanding Values From a &ConsEnv;: the &subst; Method @@ -799,7 +815,7 @@ print("CC is: %s" % env.subst('$CC')) env = Environment(CCFLAGS='-DFOO') -print("CCCOM is: %s" % env['CCCOM']) +print("CCCOM is:", env['CCCOM']) @@ -825,13 +841,13 @@ scons: `.' is up to date. env = Environment(CCFLAGS='-DFOO') -print("CCCOM is: %s" % env.subst('$CCCOM')) +print("CCCOM is:", env.subst('$CCCOM')) Will recursively expand all of - the construction variables prefixed + the &consvars; prefixed with $ (dollar signs), showing us the final output: @@ -854,20 +870,20 @@ scons: `.' is up to date.
-
- Handling Problems With Value Expansion +
+ Handling Problems With Value Expansion (advanced topic) - - If a problem occurs when expanding a construction variable, - by default it is expanded to '' - (an empty string), and will not cause &scons; to fail. + If a problem occurs when expanding a &consvar;, + by default it is expanded to an empty string, + that is, "replaced with nothing" - + &scons; will not fail for unknown variables. env = Environment() -print("value is: %s"%env.subst( '->$MISSING<-' )) +print("value is:", env.subst('->$MISSING<-')) @@ -876,26 +892,33 @@ print("value is: %s"%env.subst( '->$MISSING<-' )) - This default behaviour can be changed using the &AllowSubstExceptions; - function. - When a problem occurs with a variable expansion it generates - an exception, and the &AllowSubstExceptions; function controls - which of these exceptions are actually fatal and which are - allowed to occur safely. By default, &NameError; and &IndexError; - are the two exceptions that are allowed to occur: so instead of - causing &scons; to fail, these are caught, the variable expanded to - '' - and &scons; execution continues. - To require that all construction variable names exist, and that - indexes out of range are not allowed, call &AllowSubstExceptions; - with no extra arguments. + Sometimes this behavior leads to surprises while the build + configuration is being developed, + for example a typo in a variable name isn't reported, + and the variable expression is just dropped (empty string). + &SCons; provides a &f-link-AllowSubstExceptions; function + to allow the behavior to be tuned. + Internally, when a problem occurs with a variable expansion, + it generates an exception, + but before letting that exception kill the build, + &scons; checks a list of exceptions to ignore - by default + &NameError; and &IndexError;. + You can call &AllowSubstExceptions; to set the list of ignored + exceptions to anything you wish, including none at all. + That way, when a variable fails to expand that you thought + should be expanding to something, the build will stop and + you'll get an error message that should help diagnose the problem. + You give &AllowSubstExceptions; as many exception name arguments + as you wish it to ignore, + or call it with no arguments to have all expansion exceptions + propagate and stop &scons;. AllowSubstExceptions() env = Environment() -print("value is: %s"%env.subst( '->$MISSING<-' )) +print("value is:", env.subst('->$MISSING<-')) @@ -905,8 +928,8 @@ print("value is: %s"%env.subst( '->$MISSING<-' )) This can also be used to allow other exceptions that might occur, - most usefully with the ${...} construction - variable syntax. For example, this would allow zero-division to + most usefully with the ${...} &consvar; + syntax. For example, this would allow zero-division to occur in a variable expansion in addition to the default exceptions allowed @@ -915,7 +938,7 @@ print("value is: %s"%env.subst( '->$MISSING<-' )) AllowSubstExceptions(IndexError, NameError, ZeroDivisionError) env = Environment() -print("value is: %s"%env.subst( '->${1 / 0}<-' )) +print("value is:", env.subst('->${1 / 0}<-')) @@ -930,7 +953,7 @@ print("value is: %s"%env.subst( '->${1 / 0}<-' ))
-
+
Controlling the Default &ConsEnv;: the &DefaultEnvironment; Function @@ -986,7 +1009,7 @@ DefaultEnvironment(CC='/usr/local/bin/gcc') previous example, setting the &cv-CC; variable to /usr/local/bin/gcc but as a separate step after - the default construction environment has been initialized: + the default &consenv; has been initialized: @@ -1013,7 +1036,7 @@ def_env['CC'] = '/usr/local/bin/gcc' that &SCons; performs by specifying some specific tool modules with which to - initialize the default construction environment: + initialize the default &consenv;: @@ -1165,29 +1188,29 @@ int main() { }
-
+
Making Copies of &ConsEnvs;: the &Clone; Method - Sometimes you want more than one construction environment + Sometimes you want more than one &consenv; to share the same values for one or more variables. Rather than always having to repeat all of the common - variables when you create each construction environment, + variables when you create each &consenv;, you can use the &f-link-env-Clone; method - to create a copy of a construction environment. + to create a copy of a &consenv;. - Like the &f-link-Environment; call that creates a construction environment, + Like the &f-link-Environment; call that creates a &consenv;, the &Clone; method takes &consvar; assignments, - which will override the values in the copied construction environment. + which will override the values in the copied &consenv;. For example, suppose we want to use &gcc; to create three versions of a program, one optimized, one debug, and one with neither. - We could do this by creating a "base" construction environment + We could do this by creating a "base" &consenv; that sets &cv-link-CC; to &gcc;, and then creating two copies, one which sets &cv-link-CCFLAGS; for optimization @@ -1226,12 +1249,12 @@ int main() { }
-
+
Replacing Values: the &Replace; Method - You can replace existing construction variable values + You can replace existing &consvar; values using the &f-link-env-Replace; method: @@ -1249,10 +1272,10 @@ int main() { } - The replacing value + The new value (-DDEFINE2 in the above example) - completely replaces the value in the - construction environment: + replaces the value in the &consenv; - it's + like a &Python; assignment statement for &consvars;. @@ -1262,9 +1285,8 @@ int main() { } - You can safely call &Replace; - for construction variables that - don't exist in the construction environment: + You can safely call &Replace; for &consvars; that + don't exist in the &consenv; @@ -1278,9 +1300,7 @@ print("NEW_VARIABLE = %s" % env['NEW_VARIABLE']) - In this case, - the construction variable simply - gets added to the construction environment: + In this case, the &consvar; simply gets added to the &consenv;. @@ -1290,8 +1310,26 @@ print("NEW_VARIABLE = %s" % env['NEW_VARIABLE']) + If you have a lot of variables to replace, it may be more + convenient to put them in a dictionary and pass that to the + &Replace; method. That might look like: + + +newvalues = { + "F77PATH": ['foo', '$FOO/bar', blat], + "INCPREFIX": 'foo ', + "INCSUFFIX": 'bar', + "FOO": 'baz', +} +env.Replace(**newvalues) + + + + + + Because the variables - aren't expanded until the construction environment + aren't expanded until the &consenv; is actually used to build the targets, and because &SCons; function and method calls are order-independent, @@ -1353,14 +1391,14 @@ int main() { }
-
+
Setting Values Only If They're Not Already Defined: the &SetDefault; Method Sometimes it's useful to be able to specify - that a construction variable should be - set to a value only if the construction environment + that a &consvar; should be + set to a value only if the &consenv; does not already have that variable defined You can do this with the &f-link-env-SetDefault; method, which behaves similarly to the setdefault @@ -1376,7 +1414,7 @@ env.SetDefault(SPECIAL_FLAG='-extra-option') This is especially useful when writing your own Tool modules - to apply variables to construction environments. + to apply variables to &consenvs;. -
+
Propagating &PATH; From the External Environment @@ -1916,7 +1956,7 @@ env = Environment(ENV=os.environ.copy())
-
+
Adding to <varname>PATH</varname> Values in the Execution Environment @@ -1967,11 +2007,11 @@ env.AppendENVPath('LIB', '/usr/local/lib')
Using the toolpath for external Tools -
+
The default tool search path - Normally when using a tool from the construction environment, + Normally when using a tool from the &consenv;, several different search locations are checked by default. This includes the SCons/Tools/ directory that is part of the &scons; distribution @@ -1980,7 +2020,7 @@ env.AppendENVPath('LIB', '/usr/local/lib') -# Builtin tool or tool located within site_tools +# Built-in tool or tool located within site_tools env = Environment(tools=['SomeTool']) env.SomeTool(targets, sources) @@ -1993,7 +2033,7 @@ SCons/Tool/SomeTool/__init__.py
-
+
Providing an external directory to toolpath @@ -2024,17 +2064,17 @@ SCons/Tool/SomeTool/__init__.py
-
- Nested Tools within a toolpath +
+ Nested Tools within a toolpath (advanced topic) Since &SCons; 3.0, a Builder may be located - within a sub-directory / sub-package of the toolpath. + within a subdirectory / sub-package of the toolpath. This is similar to namespacing within &Python;. With nested or namespaced tools we can use the dot notation - to specify a sub-directory that the tool is located under. + to specify a subdirectory that the tool is located under. @@ -2057,8 +2097,8 @@ SCons/Tool/SubDir1/SubDir2/SomeTool/__init__.py
-
- Using sys.path within the toolpath +
+ Using <literal>sys.path</literal> within the toolpath If we want to access tools external to &scons; which are findable @@ -2095,7 +2135,7 @@ env.SomeTool(targets, sources) -# For Windows based on the python version and install directory, this may be something like +# For Windows based on the Python version and install directory, this may be something like C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool.py C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool\__init__.py @@ -2106,15 +2146,15 @@ C:\Python35\Lib\site-packages\someinstalledpackage\SomeTool\__init__.py
-
+
Using the &PyPackageDir; function to add to the toolpath In some cases you may want to use a tool - located within a installed external pip package. + located within an installed external pip package. This is possible by the use of sys.path with the toolpath. - However in that situation you need to provide a prefix to the toolname + However, in that situation you need to provide a prefix to the toolname to indicate where it is located within sys.path. @@ -2134,10 +2174,10 @@ env.SomeTool(targets, sources) To avoid the use of a prefix within the name of the tool or filtering sys.path for directories, we can use &f-link-PyPackageDir; function to locate the directory of - the python package. + the &Python; package. &f-PyPackageDir; returns a Dir object which represents the path of the directory - for the python package / module specified as a parameter. + for the &Python; package / module specified as a parameter. diff --git a/doc/user/errors.xml b/doc/user/errors.xml index 8b88bb3b2a..7d833e924d 100644 --- a/doc/user/errors.xml +++ b/doc/user/errors.xml @@ -3,6 +3,9 @@ When &ninja; runs the generated &ninja; build file, &ninja; will launch &scons; as a daemon and feed commands to that &scons; process which &ninja; is unable to build directly. This daemon will stay alive until - explicitly killed, or it times out. The timeout is set by &cv-link-NINJA_SCONS_DAEMON_KEEP_ALIVE; . + explicitly killed, or it times out. The timeout is set by &cv-link-NINJA_SCONS_DAEMON_KEEP_ALIVE;. diff --git a/doc/user/factories.xml b/doc/user/factories.xml index 905b959325..529e93fe67 100644 --- a/doc/user/factories.xml +++ b/doc/user/factories.xml @@ -3,6 +3,9 @@ -c option to "clean" a tree of its built targets. - These behaviours can be suppressed with the + These behaviors can be suppressed with the &Precious; and &NoClean; functions, respectively. diff --git a/doc/user/functions.xml b/doc/user/functions.xml index 9185f4adb8..88dddc7c8e 100644 --- a/doc/user/functions.xml +++ b/doc/user/functions.xml @@ -3,6 +3,9 @@ - Now we'll convert the project to a multi-lingual one. If you don't + Now we'll convert the project to a multilingual one. If you don't already have GNU gettext - utilities installed, install them from your preffered + utilities installed, install them from your preferred package repository, or download from http://ftp.gnu.org/gnu/gettext/. For the purpose of this example, you should have following three locales installed on your system: en_US, de_DE and - pl_PL. On debian, for example, you may enable certain + pl_PL. On Debian, for example, you may enable certain locales through dpkg-reconfigure locales. @@ -160,7 +163,7 @@ InstallAs(["locale/de/LC_MESSAGES/hello.mo"], ["de.mo"]) Generate the translation files with scons po-update. - You should see the output from SCons simillar to this: + You should see the output from SCons similar to this: user@host:$ scons po-update scons: Reading SConscript files ... @@ -228,7 +231,7 @@ scons: done building targets. these files under locale folder. - Your program should be now ready. You may try it as follows (linux): + Your program should be now ready. You may try it as follows (Linux): user@host:$ LANG=en_US.UTF-8 ./hello Welcome to beautiful world @@ -347,7 +350,7 @@ gcc -o hello.o -c hello.c gcc -o hello hello.o scons: done building targets. - As you see, the internationalized messages ditn't change, so the + As you see, the internationalized messages didn't change, so the POT and the rest of translation files have not even been touched. diff --git a/doc/user/hierarchy.xml b/doc/user/hierarchy.xml index c501e53cb0..9bce052e18 100644 --- a/doc/user/hierarchy.xml +++ b/doc/user/hierarchy.xml @@ -3,6 +3,9 @@ @@ -890,7 +893,7 @@ void bar(void) { printf("bar/bar.c\n"); } (The corresponding bar/SConscript file should be pretty obvious.) - Then when we run &SCons;, + Then, when we run &SCons;, the object files from the subsidiary subdirectories are all correctly archived in the desired library: diff --git a/doc/user/install.xml b/doc/user/install.xml index b1ae1f6cbd..16e1a72358 100644 --- a/doc/user/install.xml +++ b/doc/user/install.xml @@ -3,6 +3,9 @@ - Note that the the javah command was + Note that the javah command was removed from the JDK as of JDK 10, and the approved method (available since JDK 8) is to use javac to generate native headers at the same time as the Java source - code is compiled.. As such the &b-link-JavaH; builder + code is compiled. As such the &b-link-JavaH; builder is of limited utility in later Java versions. diff --git a/doc/user/less-simple.xml b/doc/user/less-simple.xml index 56d4f49f8a..19dab6f13c 100644 --- a/doc/user/less-simple.xml +++ b/doc/user/less-simple.xml @@ -3,6 +3,9 @@ split() method - of &Python; string objects.. + of &Python; string objects. Unlike the split() method, however, the &Split; function does not require a string as input diff --git a/doc/user/libraries.xml b/doc/user/libraries.xml index 67e8a52a62..bb4b4542d1 100644 --- a/doc/user/libraries.xml +++ b/doc/user/libraries.xml @@ -3,6 +3,9 @@ The SCons Development Team - Released: Mon, 17 Mar 2024 17:52:49 -0700 + Released: Mon, 27 Mar 2025 13:46:28 -0700 - 2004 - 2024 + 2004 - 2025 The SCons Foundation diff --git a/doc/user/make.xml b/doc/user/make.xml index f7ca40e758..b50cbd624e 100644 --- a/doc/user/make.xml +++ b/doc/user/make.xml @@ -3,6 +3,9 @@ -
+
Verifying the Python Version: the &EnsurePythonVersion; Function Although the &SCons; code itself will run - on any 2.x Python version 2.7 or later, - you are perfectly free to make use of - Python syntax and modules from later versions + on any 3.x Python version + (check the release notes for the precise minimum supported release), + you are free to make use of + &Python; syntax and modules from later versions when writing your &SConscript; files or your own local modules. If you do this, it's usually helpful to configure &SCons; to exit gracefully with an error message - if it's being run with a version of Python + if it's being run with a version of &Python; that simply won't work with your code. This is especially true if you're going to use &SCons; to build source code that you plan to distribute publicly, - where you can't be sure of the Python version + where you can't be sure of the &Python; version that an anonymous remote user might use to try to build your software. @@ -59,7 +63,7 @@ SPDX-License-Identifier: MIT &SCons; provides an &EnsurePythonVersion; function for this. You simply pass it the major and minor versions - numbers of the version of Python you require: + numbers of the version of &Python; you require: @@ -71,21 +75,21 @@ SPDX-License-Identifier: MIT -EnsurePythonVersion(2, 5) +EnsurePythonVersion(3, 8) --> -EnsurePythonVersion(2, 5) +EnsurePythonVersion(3, 8) And then &SCons; will exit with the following error message when a user runs it with an unsupported - earlier version of Python: + earlier version of &Python;: @@ -103,12 +107,12 @@ EnsurePythonVersion(2, 5) % scons -Q -Python 2.5 or greater required, but you have Python 2.3.6 +Python 3.7 or greater required, but you have Python 3.6.5
-
+
Verifying the SCons Version: the &EnsureSConsVersion; Function @@ -126,7 +130,7 @@ Python 2.5 or greater required, but you have Python 2.3.6 that verifies the version of &SCons; in the same the &EnsurePythonVersion; function - verifies the version of Python, + verifies the version of &Python;, by passing in the major and minor versions numbers of the version of SCons you require: @@ -177,7 +181,7 @@ SCons 1.0 or greater required, but you have SCons 0.98.5
-
+
Accessing SCons Version: the &GetSConsVersion; Function @@ -203,7 +207,7 @@ else:
-
+
Explicitly Terminating &SCons; While Reading &SConscript; Files: the &Exit; Function @@ -247,18 +251,18 @@ hello.c Note that the &Exit; function - is equivalent to calling the Python + is equivalent to calling the &Python; sys.exit function - (which the it actually calls), + (which it actually calls), but because &Exit; is a &SCons; function, - you don't have to import the Python + you don't have to import the &Python; sys module to use it.
-
+
Searching for Files: the &FindFile; Function @@ -443,13 +447,13 @@ leaf
-
+
Handling Nested Lists: the &Flatten; Function &SCons; supports a &Flatten; function - which takes an input Python sequence + which takes an input &Python; sequence (list or tuple) and returns a flattened list containing just the individual elements of @@ -586,7 +590,7 @@ cc -o prog1 prog1.o prog2.o
-
+
Finding the Invocation Directory: the &GetLaunchDir; Function @@ -610,7 +614,7 @@ env.Command('directory_build_info', Because &SCons; is usually invoked from the top-level directory in which the &SConstruct; file lives, - the Python os.getcwd() + the &Python; os.getcwd() is often equivalent. However, the &SCons; -u, @@ -635,54 +639,90 @@ env.Command('directory_build_info', -
- Virtual environments (virtualenvs) +
+ Using Python Virtual Environments - Virtualenv is a tool to create isolated Python environments. - A python application (such as SCons) may be executed within - an activated virtualenv. The activation of virtualenv modifies - current environment by defining some virtualenv-specific variables - and modifying search PATH, such that executables installed within - virtualenv's home directory are preferred over the ones installed - outside of it. + &Python; supports lightweight "virtual environments" (usually + abbreviated virtualenv) which allow + encapsulation / isolation of package dependencies for a project. + When a &Python; program is executed in the context of a + virtualenv, the paths for package imports are amended so the + modules in the virtualenv are preferred. Depending on how the + virtualenv was configured, system paths may or may not be used + as a fallback (the default is not). + &SCons; itself works as expected when executed within a virtualenv. + However, there may be issues if the project needs + to build using external commands written in &Python; + which are installed in the virtualenv, + or calls the &Python; interpreter to run a script. + &SCons; launches command actions using a special restricted + PATH setting which the new process uses + to find executables. + This path is part of the execution environment + (see ), + and by default, + does not contain any information about the virtualenv. + The result can be commands not found, + or scripts executed with the system default copy of &Python; + rather than the virtualenv one, possibly causing incorrect imports. + If you encounter this problem, &SCons; provides a mechanism + to more fully integrate with a virtualenv. + - Normally, SCons uses hard-coded PATH when searching for external - executables, so it always picks-up executables from these pre-defined - locations. This applies also to python interpreter, which is invoked - by some custom SCons tools or test suites. This means, when running - SCons in a virtualenv, an eventual invocation of python interpreter from - SCons script will most probably jump out of virtualenv and execute - python executable found in hard-coded SCons PATH, not the one which is - executing SCons. Some users may consider this as an inconsistency. + + Use the + to import virtualenv-related environment variables to the + execution environment (&cv-link-ENV;) + and to modify the execution environment's PATH + appropriately to prefer the virtualenv executables and + &Python; interpreter. + + + To make this setting permanent, you can either: + + + Add it to the SCONSFLAGS environment variable , or + + + Set SCONS_ENABLE_VIRTUALENV=1 in your environment. + + + - This issue may be overcome by using the - - option. The option automatically imports virtualenv-related environment - variables to all created construction environment env['ENV'], - and modifies SCons PATH appropriately to prefer virtualenv's executables. - Setting environment variable SCONS_ENABLE_VIRTUALENV=1 - will have same effect. If virtualenv support is enabled system-vide - by the environment variable, it may be suppressed with the - option. + SCONSFLAGS is the preferred approach, + as it's easier to manage a single variable controlling &SCons; + behavior than multiples. + If enabled by environment variable, + the special virtualenv behavior can be disabled for the current + run using the option. - Inside of &SConscript;, a global function Virtualenv is - available. It returns a path to virtualenv's home directory, or - None if &scons; is not running from virtualenv. Note - that this function returns a path even if &scons; is run from an - unactivated virtualenv. + You can query the state at runtime by calling + the &f-link-Virtualenv; global function. + It returns a path to the virtualenv's home directory, + or an empty string if &SCons; is not running in a virtualenv. + + &f-Virtualenv; returns a path even if &SCons; + is run from an unactivated virtualenv. + A virtualenv does not have to be activated to be used, + you only need to use the path to its &Python; interpreter, + but only an activated virtualenv makes available the + suitable PATH elements for &SCons; to + copy in when is used. + +
diff --git a/doc/user/nodes.xml b/doc/user/nodes.xml index 83514ec5f1..eba601465e 100644 --- a/doc/user/nodes.xml +++ b/doc/user/nodes.xml @@ -3,6 +3,9 @@ -
+
Builder Methods Return Lists of Target Nodes @@ -147,7 +150,7 @@ int main() { printf("Goodbye, world!\n"); }
-
+
Explicitly Creating File and Directory Nodes @@ -225,7 +228,7 @@ xyzzy = Entry('xyzzy')
-
+
Printing &Node; File Names @@ -287,7 +290,7 @@ int main() { printf("Hello, world!\n"); }
-
+
Using a &Node;'s File Name as a String @@ -298,7 +301,7 @@ int main() { printf("Hello, world!\n"); } is the name of the file. If you want to do something other than print the name of the file, - you can fetch it by using the builtin Python + you can fetch it by using the built-in Python &str; function. For example, if you want to use the Python os.path.exists @@ -334,7 +337,7 @@ int main() { printf("Hello, world!\n"); }
-
+
&GetBuildPath;: Getting the Path From a &Node; or String @@ -381,7 +384,7 @@ print(env.GetBuildPath([n, "sub/dir/$VAR"])) A key aspect of creating a usable build configuration - is providing useful output from the build + is providing useful output from the build, so its users can readily understand what the build is doing and get information about how to control the build. diff --git a/doc/user/parse_flags_arg.xml b/doc/user/parse_flags_arg.xml index 9e3ad85414..d821f6f34a 100644 --- a/doc/user/parse_flags_arg.xml +++ b/doc/user/parse_flags_arg.xml @@ -3,6 +3,9 @@ config + various utilities with names that end in config return the command-line options for the GNU Compiler Collection (GCC) that are needed to build and link against those libraries; for example, the command-line options @@ -52,7 +55,7 @@ SPDX-License-Identifier: MIT - &SCons; &consvars; have a &f-link-ParseConfig; + &SCons; &consenvs; have a &f-link-ParseConfig; method that asks the host system to execute a command and then configures the appropriate &consvars; based on the output of that command. @@ -99,7 +102,7 @@ scons: `.' is up to date. In the example above, &SCons; has added the include directory to &cv-link-CPPPATH; - (Depending upon what other flags are emitted by the + (depending on what other flags are emitted by the pkg-config command, other variables may have been extended as well.) diff --git a/doc/user/parseflags.xml b/doc/user/parseflags.xml index a007f9b573..eef242ddaf 100644 --- a/doc/user/parseflags.xml +++ b/doc/user/parseflags.xml @@ -3,6 +3,9 @@ - If a string begins with a an exclamation mark (!), + If a string begins with an exclamation mark (!), the string is passed to the shell for execution. The output of the command is then parsed: diff --git a/doc/user/preface.xml b/doc/user/preface.xml index 3a47b63acd..8c0ba874f7 100644 --- a/doc/user/preface.xml +++ b/doc/user/preface.xml @@ -3,6 +3,9 @@ -
+
&SCons; Principles @@ -190,7 +193,7 @@ SPDX-License-Identifier: MIT --> -
+
How to Use this Guide @@ -220,19 +223,23 @@ SPDX-License-Identifier: MIT - It is often useful to keep &SCons; man page open in a separate + If you are viewing an html version of this Guide, there are many + hyperlinks present that you can follow to get more details + if you want them, as the User Guide intentionally does not attempt + to provide every detail, to allow smoother study of the basics. + It may also be useful to keep the &SCons; man page open in a separate browser tab or window to refer to as a complement to this Guide, - as the User Guide does not attempt to provide every detail. - While this Guide's Appendices A-D do duplicate information that appears - in the man page (this is to allow intra-document links to - definitions of &consvars;, builders, tools and environment methods - to work), the rest of the man page is unique content. + which can avoid some jumping back and forth. + The four important manpage sections describiing the + of &consvars;, builders, tools and environment methods + are actually duplicated as appendices in the User Guide, + to avoid inter-document links.
-
+
A Caveat About This Guide's Completeness @@ -269,7 +276,7 @@ SPDX-License-Identifier: MIT
-
+
Acknowledgements @@ -392,7 +399,7 @@ SPDX-License-Identifier: MIT And last, thanks to Guido van Rossum - for his elegant scripting language, + for his elegant scripting language &Python;, which is the basis not only for the &SCons; implementation, but for the interface itself. @@ -400,7 +407,7 @@ SPDX-License-Identifier: MIT
-
+
Contact diff --git a/doc/user/python.xml b/doc/user/python.xml index dc10a7b484..c5d9cc5f5c 100644 --- a/doc/user/python.xml +++ b/doc/user/python.xml @@ -3,6 +3,9 @@ -
+
The &Repository; Method -
+
Limitations on <literal>#include</literal> files in repositories @@ -329,7 +332,7 @@ coming into existence.) - As we've seen, + As you've seen, &SCons; will compile the &hello_c; file from the repository if it doesn't exist in the local directory. @@ -361,7 +364,7 @@ main(int argc, char *argv[]) -env = Environment(CPPPATH = ['.']) +env = Environment(CPPPATH=['.']) env.Program('hello.c') Repository('__ROOT__/usr/repository1') @@ -397,9 +400,9 @@ int main() { printf("Hello, world!\n"); } Some modern versions of C compilers do have an option to disable or control this behavior. If so, add that option to &cv-link-CFLAGS; - (or &cv-link-CXXFLAGS; or both) in your construction environment(s). - Make sure the option is used for all construction - environments that use C preprocessing! + (or &cv-link-CXXFLAGS;, or both) in your &consenvs;. + Make sure the option is used for all &consenv; + that use C preprocessing! @@ -438,7 +441,7 @@ int main() { printf("Hello, world!\n"); }
-
+
Finding the &SConstruct; file in repositories @@ -469,6 +472,18 @@ int main() { printf("Hello, world!\n"); } + + + Note that while other files are searched through the chain of + repositories, &SConstruct; is special - it must be found either in + the current directory or the first directory + specified using the -Y + (or the --repository or + --srcdir synonyms) + command line option, or the build will abort. + + +
@@ -479,9 +494,9 @@ int main() { printf("Hello, world!\n"); } If a repository contains not only source files, but also derived files (such as object files, libraries, or executables), &SCons; will perform - its normal MD5 signature calculation to + its normal signature calculation to decide if a derived file in a repository is up-to-date, - or the derived file must be rebuilt in the local build directory. + or if it needs to be rebuilt in the local build directory. For the &SCons; signature calculation to work correctly, a repository tree must contain the &sconsigndb; files that &SCons; uses to keep track of signature information. @@ -532,8 +547,8 @@ int f2() { printf("file2.c\n"); } Now, with the repository populated, - we only need to create the one local source file - we're interested in working with at the moment, + you only need to create the one local source file + you're interested in working with at the moment, and use the -Y option to tell &SCons; to fetch any other files it needs from the repository: @@ -572,7 +587,7 @@ cc -o hello hello.o /usr/repository1/file1.o /usr/repository1/file2.o If the repository tree contains the complete results of a build, - and we try to build from the repository + and you try to build from the repository without any files in our local tree, something moderately surprising happens: @@ -590,8 +605,7 @@ scons: `hello' is up-to-date. Why does &SCons; say that the &hello; program is up-to-date when there is no &hello; program in the local build directory? - Because the repository (not the local directory) - contains the up-to-date &hello; program, + Because the repository contains the &hello; program, and &SCons; correctly determines that nothing needs to be done to rebuild that up-to-date copy of the file. @@ -600,11 +614,17 @@ scons: `hello' is up-to-date. - There are, however, many times when you want to ensure that a + There are, however, times when you want to ensure that a local copy of a file always exists. - A packaging or testing script, for example, - may assume that certain generated files exist locally. - To tell &SCons; to make a copy of any up-to-date repository + For example, if you are packaging the result of the build, + all the files used in the package need to be present locally, + and the packaging tool is unlikely to know anything about + &SCons; repositories. Similarly, if you build a unit test + program, and then expect to run after the build, + it doesn't help if the test program is somewhere else + and wasn't rebuilt into the local directory. + In these cases, you can tell + &SCons; to make a copy of any up-to-date repository file in the local build directory, use the &Local; function: @@ -623,7 +643,7 @@ int main() { printf("Hello, world!\n"); } - If we then run the same command, + Now, if you run the same command, &SCons; will make a local copy of the program from the repository copy, and tell you that it is doing so: @@ -646,4 +666,36 @@ scons: `hello' is up-to-date.
+
+ Using Repository to separate source and build. + + + + If you want to just do a build where the build artifacts don't + pollute the source directory, the repository mechanism can help with that. + Here's an example: checkout or unpack your project in the + directory src, + and then build it in build: + + + + +$ mkdir build +$ cd build +$ scons -Q -Y ../src +gcc -o foo.o -I. -I/path/to/src -c /path/to/src/foo.c +gcc -o foo foo.o +$ ls +foo foo.o + + + + + It can become awkward to keep having to type + -Y path-to-repo repeatedly. + If so, the option can be placed in &SCONSFLAGS;. + + +
+ diff --git a/doc/user/run.xml b/doc/user/run.xml index acedd4b293..b096331c8e 100644 --- a/doc/user/run.xml +++ b/doc/user/run.xml @@ -3,6 +3,9 @@ One approach for introducing a &Scanner; into the build is in - conjunction with a &Builder;. There are two relvant optional + conjunction with a &Builder;. There are two relevant optional parameters we can use when creating a Builder: source_scanner and target_scanner. @@ -520,7 +523,7 @@ text to include Running this example would only show that the stub build_function is getting called, - so some debug prints were added to the scaner function, + so some debug prints were added to the scanner function, just to show the scanner is being invoked. diff --git a/doc/user/sconf.xml b/doc/user/sconf.xml index 7acacc3725..3eb89c12bd 100644 --- a/doc/user/sconf.xml +++ b/doc/user/sconf.xml @@ -3,6 +3,9 @@ diff --git a/doc/user/separate.xml b/doc/user/separate.xml index 897a126e36..394bf6b4a0 100644 --- a/doc/user/separate.xml +++ b/doc/user/separate.xml @@ -3,6 +3,9 @@ variant directory, a place where you can build a single variant of your software (of course you can define more than one of these if you need to). @@ -205,7 +208,7 @@ int main() { printf("Hello, world!\n"); } except for the extra &variant_dir; argument in the &f-link-SConscript; call. &SCons; handles all the path adjustments for the - out of tree &build; directory while it processes that SConscript file. + out-of-tree &build; directory while it processes that SConscript file. @@ -216,7 +219,7 @@ int main() { printf("Hello, world!\n"); } - When you set up a variant directory &SCons; conceptually behaves as + When you set up a variant directory, &SCons; conceptually behaves as if you requested a build in that directory. As noted in the previous chapter, all builds actually happen from the top level directory, @@ -224,8 +227,8 @@ int main() { printf("Hello, world!\n"); } of it as build in place in the variant directory, not build in source but send build artifacts to the variant directory. - It turns out in place builds are easier to get right than out - of tree builds - so by default &SCons; simulates an in place build + It turns out in place builds are easier to get right than out-of-tree + builds - so by default &SCons; simulates an in place build by making the variant directory look just like the source directory. The most straightforward way to do that is by making copies of the files needed for the build. @@ -495,7 +498,7 @@ int main() { printf("Hello, world!\n"); } to the use of &f-link-SConscript; with the variant_dir argument from earlier in this chapter, - but did require callng the SConscript using the already established + but did require calling the SConscript using the already established variant directory path to trigger that behavior. If you call SConscript('src/SConscript') you would get a normal in-place build in &src;. diff --git a/doc/user/sideeffect.xml b/doc/user/sideeffect.xml index 863bb71c97..c1e2433863 100644 --- a/doc/user/sideeffect.xml +++ b/doc/user/sideeffect.xml @@ -3,6 +3,9 @@ log, which is used as a source for the command to generate file2, but log is unknown to &SCons; on a clean - build: it neither exists, nor is it a target output by any builder. + build: it neither exists nor is it a target output by any builder. The SConscript uses &SideEffect; to inform &SCons; about the additional output file. @@ -111,14 +114,14 @@ f1 = env.Command( In general, &SideEffect; is not intended for the case when a command produces extra target files (that is, files which - will be used as sources to other build steps). For example, the + will be used as sources to other build steps). For example, the &MSVC; compiler is capable of performing incremental linking, for which it uses a status file - such that linking foo.exe also produces a foo.ilk, or uses it if it was already present, if the option was supplied. Specifying foo.ilk as a - side-effect of foo.exe + side effect of foo.exe is not a recommended use of &SideEffect; since foo.ilk is used by the link. &SCons; handles side-effect files @@ -242,7 +245,7 @@ cat - This makes sure the the two + This makes sure the two ./build steps are run sequentially, even with the --jobs=2 in the command line: diff --git a/doc/user/simple.xml b/doc/user/simple.xml index 34a3761ee3..9f57d33311 100644 --- a/doc/user/simple.xml +++ b/doc/user/simple.xml @@ -3,6 +3,9 @@ - Then when you execute &SCons;, + Then, when you execute &SCons;, you will see the output from calling the print function in between the messages about reading the &SConscript; files, diff --git a/doc/user/tasks.xml b/doc/user/tasks.xml index 2c10e0b3c0..f239d0bd6e 100644 --- a/doc/user/tasks.xml +++ b/doc/user/tasks.xml @@ -3,6 +3,9 @@ .h file. + such as an included .h file. If the file1.c and file3.c files in our example @@ -318,7 +321,7 @@ print(env.Dump()) interested in, the &Dump; method allows you to specify a specific &consvar; - that you want to disply. + that you want to display. For example, it's not unusual to want to verify the external environment used to execute build commands, @@ -423,7 +426,7 @@ inc.h - By default &SCons; uses "ASCII art" to draw the tree. It is + By default, &SCons; uses "ASCII art" to draw the tree. It is possible to use line-drawing characters (Unicode calls these Box Drawing) to make a nicer display. To do this, add the qualifier: @@ -655,7 +658,7 @@ cc -o prog prog.o To get some insight into what library names &SCons; is searching for, and in which directories it is searching, - Use the &debug-findlibs; option. + use the &debug-findlibs; option. Given the following input &SConstruct; file: @@ -874,7 +877,7 @@ prog.c - Sometimes SCons doesn't build the target you want + Sometimes SCons doesn't build the target you want, and it's difficult to figure out why. You can use the &debug-prepare; option to see all the targets &SCons; is considering, and whether @@ -890,7 +893,7 @@ prog.c - When using the &Duplicate; option to create variant dirs, + When using the &Duplicate; option to create variant directories, sometimes you may find files not getting linked or copied to where you expect (or not at all), or files mysteriously disappearing. These are usually because of a misconfiguration of some kind in the @@ -898,7 +901,7 @@ prog.c &debug-duplicate; option shows each time a variant file is unlinked and relinked from its source (or copied, depending on settings), and also shows a message for removing "stale" - variant-dir files that no longer have a corresponding source file. + variant-directory files that no longer have a corresponding source file. It also prints a line for each target that's removed just before building, since that can also be mistaken for the same thing. diff --git a/doc/user/variables.xml b/doc/user/variables.xml index 27e3323a55..ac03e9e20b 100644 --- a/doc/user/variables.xml +++ b/doc/user/variables.xml @@ -3,6 +3,9 @@ variant_dir argument to &SConscript;, - you can call the function repeatedely - this example + you can call the function repeatedly - this example does so in a loop. Note that the &f-link-SConscript; trick of passing a list of script files, or a list of source directories, does not work with variant_dir, diff --git a/packaging/etc/README.txt b/packaging/etc/README.txt index ffb9cc10b2..11f1eab309 100644 --- a/packaging/etc/README.txt +++ b/packaging/etc/README.txt @@ -1,10 +1,18 @@ -This directory contains a number of scripts/files useful when building/packageing SCons +This directory contains helpers for doing reproducible builds with SCons. + +To force SCons to propagate SOURCE_DATE_EPOCH from the shell running SCons, +the reproducible_site_init.py file can be installed (as site_init.py) +in any site directory - either in the project itself, or more globally. +See the manpage for default site directories or how to set your own path: +https://scons.org/doc/production/HTML/scons-man.html#opt-site-dir. +This code will make sure SOURCE_DATE_EPOCH is set in the execution +environment, meaning any external commands run by SCons will have it +in their environment. Any logic in your build system itself will still +need to examine this variable. + +The shell script reproducible_install.sh can be used to install the +Python site file in your user site directory ($HOME/.scons/site_scons). +It is careful to not overwrite any existing site_init.py there. This +only works for a POSIX shell. -To force SCons to propagate SOURCE_DATE_EPOCH from the shell running SCons we're providing -a script to create a ~/.scons/site_scons/site_init.py. -Note that reproducible_install.sh will NOT overwite an existing ~/.scons/site_scons/site_init.py This supports https://reproducible-builds.org/specs/source-date-epoch/ -If you wanted to include this in your build tree you would place in site_scons/site_init.py relative -to your SConstruct. -* reproducible_install.sh -* reproducible_site_init.py \ No newline at end of file diff --git a/packaging/etc/reproducible_site_init.py b/packaging/etc/reproducible_site_init.py index 2b6b42a91f..5f7513ba19 100644 --- a/packaging/etc/reproducible_site_init.py +++ b/packaging/etc/reproducible_site_init.py @@ -1,5 +1,6 @@ """ -Use this file as your ~/.site_scons/scons_init.py to enable reprodicble builds as described at +Use this file as your site_init.py in a site directory, +to enable reprodicble builds as described at https://reproducible-builds.org/specs/source-date-epoch/ """ @@ -8,17 +9,22 @@ old_init = SCons.Environment.Base.__init__ -print("Adding logic to propagate SOURCE_DATE_EPOCH from the shell environment when building with SCons") +print( + "Adding logic to propagate SOURCE_DATE_EPOCH from the shell environment when building with SCons" +) def new_init(self, **kw): - """ - This logic will add SOURCE_DATE_EPOCH to the execution environment used to run - all the build commands. + """Replacement Environment initializer. + + When this is monkey-patched into :class:`SCons.Environment.Base` it adds + ``SOURCE_DATE_EPOCH`` to the execution environment used to run + all external build commands; the original iinitializer is called first. """ old_init(self, **kw) - if 'SOURCE_DATE_EPOCH' in os.environ: - self._dict['ENV']['SOURCE_DATE_EPOCH'] = os.environ['SOURCE_DATE_EPOCH'] + epoch = os.environ.get("SOURCE_DATE_EPOCH") + if epoch is not None: + self._dict["ENV"]["SOURCE_DATE_EPOCH"] = epoch SCons.Environment.Base.__init__ = new_init diff --git a/pyproject.toml b/pyproject.toml index 548ae2d65a..241007bdb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,12 +2,139 @@ build-backend = "setuptools.build_meta" requires = ["setuptools"] -# for black and mypy, set the lowest Python version supported -[tool.black] -quiet = true -target-version = ['py36'] -skip-string-normalization = true +[project] +name = "SCons" +description = "Open Source next-generation build tool." +requires-python = ">=3.7" +license = "MIT" # PEP 639 form (new - setuptools >= 77.0) +# Should include docbook license, but this fails: +# license = "MIT AND DocBook-stylesheet" +license-files = [ + "LICENSE", + "SCons/Tool/docbook/docbook-xsl-1.76.1/COPYING", +] +readme = { file = "README-package.rst", content-type = "text/x-rst" } +authors = [{ name = "William Deegan", email = "bill@baddogconsulting.com" }] +dynamic = ["version"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Topic :: Software Development :: Build Tools", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Operating System :: Unix", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", +] -[mypy] -python_version = 3.6 +[project.urls] +Homepage = "https://www.scons.org/" +Documentation = "https://scons.org/documentation.html" +Twitter = "https://twitter.com/SConsProject" +GitHub = "https://github.com/SCons/scons" +Bug-Tracker = "https://github.com/SCons/scons/issues" +Discord = "https://discord.gg/pejaFYrD9n" +"Mailing lists" = "https://scons.org/lists.html" +[project.scripts] +scons = "SCons.Script.Main:main" +sconsign = "SCons.Utilities.sconsign:main" +scons-configure-cache = "SCons.Utilities.ConfigureCache:main" + +[project.optional-dependencies] +dev = [ + "ninja", + "psutil", + "lxml < 5; sys_platform != 'win32' and python_version < '3.13'", +] +pkg = [ + "ninja", + "psutil", + "readme-renderer", + "sphinx", + "sphinx-book-theme", + "rst2pdf", + "build", + "twine", + "packaging", +] + +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.dynamic] +version = {attr = "SCons.__version__"} + +[tool.setuptools.packages.find] +include = ["SCons*"] +namespaces = false + +[tool.setuptools.package-data] +"*" = ["*.txt", "*.rst", "*.1"] +"SCons.Tool.docbook" = ["*.*"] + +[tool.distutils.sdist] +dist-dir = "build/dist" + +[tool.distutils.bdist_wheel] +dist-dir = "build/dist" + +[tool.ruff] +target-version = "py37" # Lowest python version supported +extend-include = ["SConstruct", "SConscript"] +extend-exclude = [ + "bench/", + "bin/", + "doc/", + "src/", + "template/", + "test/", + "testing/", + "timings/", + "SCons/Tool/docbook/docbook-xsl-1.76.1/", +] + +[tool.ruff.lint] +extend-select = [ + "FA", # Future annotations + "UP006", # Use {to} instead of {from} for type annotation + "UP007", # Use `X | Y` for type annotations + "UP037", # Remove quotes from type annotation +] +extend-safe-fixes = ["FA", "UP006", "UP007"] + +[tool.ruff.format] +quote-style = "preserve" # Equivalent to black's "skip-string-normalization" + +[tool.ruff.lint.per-file-ignores] +"SCons/Util/__init__.py" = [ + "F401", # Module imported but unused +] +"SCons/Variables/__init__.py" = [ + "F401", # Symbol imported but unused +] + +[tool.mypy] +python_version = "3.8" +exclude = [ + "^bench/", + "^bin/", + "^doc/", + "^src/", + "^template/", + "^test/", + "^testing/", + "^timings/", + "^SCons/Tool/docbook/docbook-xsl-1.76.1/", +] diff --git a/requirements-pkg.txt b/requirements-pkg.txt index 3b30462a94..aa3f1eef3e 100644 --- a/requirements-pkg.txt +++ b/requirements-pkg.txt @@ -13,3 +13,5 @@ sphinx-book-theme rst2pdf build +twine +packaging diff --git a/runtest.py b/runtest.py index 1922ccff79..7e0a5d16df 100755 --- a/runtest.py +++ b/runtest.py @@ -15,6 +15,8 @@ performs test discovery and processes tests according to options. """ +from __future__ import annotations + import argparse import itertools import os @@ -27,11 +29,10 @@ from io import StringIO from pathlib import Path, PurePath, PureWindowsPath from queue import Queue -from typing import List, TextIO, Optional cwd = os.getcwd() -debug: Optional[str] = None -scons: Optional[str] = None +debug: str | None = None +scons: str | None = None catch_output: bool = False suppress_output: bool = False script = PurePath(sys.argv[0]).name @@ -43,7 +44,7 @@ """ # this is currently expected to be global, maybe refactor later? -unittests: List[str] +unittests: list[str] parser = argparse.ArgumentParser( usage=usagestr, @@ -417,7 +418,7 @@ class SystemExecutor(RuntestBase): def execute(self, env): self.stderr, self.stdout, s = spawn_it(self.command_args, env) self.status = s - if s < 0 or s > 2: + if s < 0 or s > 2 and s != 5: sys.stdout.write("Unexpected exit status %d\n" % s) @@ -573,38 +574,34 @@ def footer(self, f): del os.environ['_JAVA_OPTIONS'] -# ---[ test discovery ]------------------------------------ -# This section figures out which tests to run. +# ---[ Test Discovery ]------------------------------------ +# This section determines which tests to run based on three +# mutually exclusive options: +# 1. Reading test paths from a testlist file (--file or --retry option) +# 2. Using test paths given as command line arguments +# 3. Automatically finding all tests (--all option) # -# The initial testlist is made by reading from the testlistfile, -# if supplied, or by looking at the test arguments, if supplied, -# or by looking for all test files if the "all" argument is supplied. -# One of the three is required. +# Test paths can specify either individual test files, or directories to +# scan for tests. The following test types are recognized: # -# Each test path, whichever of the three sources it comes from, -# specifies either a test file or a directory to search for -# SCons tests. SCons code layout assumes that any file under the 'SCons' -# subdirectory that ends with 'Tests.py' is a unit test, and any Python -# script (*.py) under the 'test' subdirectory is an end-to-end test. -# We need to track these because they are invoked differently. -# find_unit_tests and find_e2e_tests are used for this searching. +# - Unit tests: Files ending in 'Tests.py' under the 'SCons' directory +# - End-to-end tests: Python scripts (*.py) under the 'test' directory +# - External tests: End-to-end tests in paths containing a 'test' +# component (not expected to be local) # -# Note that there are some tests under 'SCons' that *begin* with -# 'test_', but they're packaging and installation tests, not -# functional tests, so we don't execute them by default. (They can -# still be executed by hand, though). +# find_unit_tests() and find_e2e_tests() perform the directory scanning. # -# Test exclusions, if specified, are then applied. - +# After the initial test list is built, any test exclusions specified via +# --exclude-list are applied to produce the final test set. def scanlist(testfile): """ Process a testlist file """ data = StringIO(testfile.read_text()) tests = [t.strip() for t in data.readlines() if not t.startswith('#')] # in order to allow scanned lists to work whether they use forward or - # backward slashes, first create the object as a PureWindowsPath which - # accepts either, then use that to make a Path object to use for - # comparisons like "file in scanned_list". + # backward slashes, on non-Windows first create the object as a + # PureWindowsPath which accepts either, then use that to make a Path + # object for use in comparisons like "if file in scanned_list". if sys.platform == 'win32': return [Path(t) for t in tests if t] else: @@ -633,7 +630,7 @@ def find_e2e_tests(directory): if 'sconstest.skip' in filenames: continue - # Slurp in any tests in exclude lists + # Gather up the data from any exclude lists excludes = [] if ".exclude_tests" in filenames: excludefile = Path(dirpath, ".exclude_tests").resolve() @@ -646,8 +643,7 @@ def find_e2e_tests(directory): return sorted(result) -# initial selection: -# if we have a testlist file read that, else hunt for tests. +# Initial test selection: unittests = [] endtests = [] if args.testlistfile: @@ -666,7 +662,7 @@ def find_e2e_tests(directory): # Clean up path removing leading ./ or .\ name = str(path) if name.startswith('.') and name[1] in (os.sep, os.altsep): - path = path.with_name(tn[2:]) + path = path.with_name(name[2:]) if path.exists(): if path.is_dir(): @@ -674,7 +670,8 @@ def find_e2e_tests(directory): unittests.extend(find_unit_tests(path)) elif path.parts[0] == 'test': endtests.extend(find_e2e_tests(path)) - # else: TODO: what if user pointed to a dir outside scons tree? + elif args.external and 'test' in path.parts: + endtests.extend(find_e2e_tests(path)) else: if path.match("*Tests.py"): unittests.append(path) @@ -701,7 +698,7 @@ def find_e2e_tests(directory): """) sys.exit(1) -# ---[ test processing ]----------------------------------- +# ---[ Test Processing ]----------------------------------- tests = [Test(t) for t in tests] if args.list_only: @@ -784,7 +781,7 @@ def run_test(t, io_lock=None, run_async=True): command_args = [] if debug: command_args.extend(['-m', debug]) - if args.devmode and sys.version_info >= (3, 7, 0): + if args.devmode: command_args.append('-X dev') command_args.append(t.path) if args.runner and t.path in unittests: @@ -794,11 +791,9 @@ def run_test(t, io_lock=None, run_async=True): t.command_str = " ".join(t.command_args) if args.printcommand: if args.print_progress: - t.headline += "%d/%d (%.2f%s) %s\n" % ( - t.testno, total_num_tests, - float(t.testno) * 100.0 / float(total_num_tests), - "%", - t.command_str, + t.headline += ( + f"{t.testno}/{total_num_tests} " + f"({t.testno / total_num_tests:7.2%}) {t.command_str}\n" ) else: t.headline += t.command_str + "\n" @@ -826,10 +821,11 @@ def run_test(t, io_lock=None, run_async=True): class RunTest(threading.Thread): - """ Test Runner class. + """Test Runner thread. - One instance will be created for each job thread in multi-job mode + One will be created for each job in multi-job mode """ + def __init__(self, queue=None, io_lock=None, group=None, target=None, name=None): super().__init__(group=group, target=target, name=name) self.queue = queue diff --git a/scripts/scons-configure-cache.py b/scripts/scons-configure-cache.py index 2ae6b14aa5..c30859db14 100644 --- a/scripts/scons-configure-cache.py +++ b/scripts/scons-configure-cache.py @@ -49,9 +49,9 @@ import sys # python compatibility check -if sys.version_info < (3, 6, 0): +if sys.version_info < (3, 7, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.6.0 is required.\n" +Python >= 3.7.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) diff --git a/scripts/scons.py b/scripts/scons.py index efafdef803..9c5be9d484 100755 --- a/scripts/scons.py +++ b/scripts/scons.py @@ -42,9 +42,9 @@ import sys # Python compatibility check -if sys.version_info < (3, 6, 0): +if sys.version_info < (3, 7, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.6.0 is required.\n" +Python >= 3.7.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) diff --git a/scripts/sconsign.py b/scripts/sconsign.py index 95a9b96e67..a467ebafb5 100644 --- a/scripts/sconsign.py +++ b/scripts/sconsign.py @@ -41,9 +41,9 @@ import sys # python compatibility check -if sys.version_info < (3, 6, 0): +if sys.version_info < (3, 7, 0): msg = "scons: *** SCons version %s does not run under Python version %s.\n\ -Python >= 3.6.0 is required.\n" +Python >= 3.7.0 is required.\n" sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.exit(1) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7c6af765cb..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,81 +0,0 @@ -[metadata] -name = SCons -license = MIT -author = William Deegan -author_email =bill@baddogconsulting.com -long_description = file: README-package.rst -long_description_content_type = text/x-rst -description = Open Source next-generation build tool. -group = Development/Tools -license_file = LICENSE - - -url = https://www.scons.org/ -project_urls = - Documentation = https://scons.org/documentation.html - Twitter = https://twitter.com/SConsProject - GitHub = https://github.com/SCons/scons - Bug-Tracker = https://github.com/SCons/scons/issues - Discord = https://discord.gg/pejaFYrD9n - Mailing lists = https://scons.org/lists.html - - -classifiers = - Development Status :: 5 - Production/Stable - Topic :: Software Development :: Build Tools - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: POSIX :: Linux - Operating System :: Unix - Operating System :: MacOS - Operating System :: Microsoft :: Windows - - -[options] -zip_safe = False -python_requires = >=3.6 -include_package_data = True -packages = find: - - -[options.packages.find] -;include=SCons.* -exclude=template - -[options.entry_points] -console_scripts = - scons = SCons.Script.Main:main - sconsign = SCons.Utilities.sconsign:main - scons-configure-cache = SCons.Utilities.ConfigureCache:main - - -[options.package_data] -* = *.txt, *.rst, *.1 -SCons.Tool.docbook = *.* - - -[options.data_files] -. = scons.1 - scons-time.1 - sconsign.1 - -[sdist] -dist_dir=build/dist - -[bdist_wheel] - ; We're now py3 only -;universal=true -dist_dir=build/dist - diff --git a/setup.py b/setup.py index 6d52278dce..9cb37592a5 100644 --- a/setup.py +++ b/setup.py @@ -14,24 +14,16 @@ def read(rel_path): return fp.read() -def get_version(rel_path): - for line in read(rel_path).splitlines(): - if line.startswith('__version__'): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") - - exclude = ['*Tests'] class build_py(build_py_orig): def find_package_modules(self, package, package_dir): - """ - Custom module to find package modules. - It will strip out any modules which match the glob patters in exclude above + """Custom module to find package modules. + + Will strip out any modules which match the glob patters in + *exclude* above """ modules = super().find_package_modules(package, package_dir) return [(pkg, mod, file, ) for (pkg, mod, file, ) in modules @@ -42,5 +34,4 @@ def find_package_modules(self, package, package_dir): cmdclass={ 'build_py': build_py, }, - version=get_version('SCons/__init__.py'), -) \ No newline at end of file +) diff --git a/shippable.yml b/shippable.yml deleted file mode 100644 index 2590fb1772..0000000000 --- a/shippable.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: python - -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - -script: python runtest.py -a diff --git a/site_scons/BuildCommandLine.py b/site_scons/BuildCommandLine.py index 5d00f6eb8e..41d4cf68c7 100644 --- a/site_scons/BuildCommandLine.py +++ b/site_scons/BuildCommandLine.py @@ -104,7 +104,7 @@ def set_date(self): NEW DATE WILL BE INSERTED HERE """ - min = (time.daylight and time.altzone or time.timezone) // 60 + min = (time.altzone if time.daylight else time.timezone) // 60 hr = min // 60 min = -(min % 60 + hr * 100) # TODO: is it better to take the date of last rev? Externally: diff --git a/site_scons/Utilities.py b/site_scons/Utilities.py index 7ef45a33c6..620595d7ec 100644 --- a/site_scons/Utilities.py +++ b/site_scons/Utilities.py @@ -31,7 +31,7 @@ def whereis(filename): st = os.stat(f_ext) except: continue - if stat.S_IMODE(st[stat.ST_MODE]) & 0o111: + if stat.S_IMODE(st.st_mode) & stat.S_IXUSR: return f_ext return None diff --git a/template/RELEASE.txt b/template/RELEASE.txt index 439c8630d8..9047a7a9a8 100755 --- a/template/RELEASE.txt +++ b/template/RELEASE.txt @@ -6,12 +6,12 @@ Past official release announcements appear at: ================================================================== -A new SCons release, 4.4.1, is now available on the SCons download page: +A new SCons release, NEXT_RELEASE, is now available on the SCons download page: https://scons.org/pages/download.html -Here is a summary of the changes since 4.4.0: +Here is a summary of the changes since PREVIOUS_RELEASE: NEW FUNCTIONALITY ----------------- @@ -62,4 +62,4 @@ Thanks to the following contributors listed below for their contributions to thi ========================================================================================== .. code-block:: text - git shortlog --no-merges -ns 4.0.1..HEAD + git shortlog --no-merges -ns PREVIOUS_RELEASE..HEAD diff --git a/test/AR/AR.py b/test/AR/AR.py index 2b92ff4256..fbf45569d9 100644 --- a/test/AR/AR.py +++ b/test/AR/AR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -35,6 +34,7 @@ test.file_fixture('wrapper.py') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) foo = Environment(LIBS = ['foo'], LIBPATH = ['.']) ar = foo.Dictionary('AR') bar = Environment(LIBS = ['bar'], LIBPATH = ['.'], AR = r'%(_python_)s wrapper.py ' + ar) diff --git a/test/AR/ARCOM.py b/test/AR/ARCOM.py index 9ae5b9fb65..ddb5ba90c4 100644 --- a/test/AR/ARCOM.py +++ b/test/AR/ARCOM.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to configure the $ARCOM construction variable. @@ -38,6 +37,7 @@ test.file_fixture('myrewrite.py') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(tools=['default', 'ar'], ARCOM = r'%(_python_)s mycompile.py ar $TARGET $SOURCES', RANLIB = True, diff --git a/test/AR/ARCOMSTR.py b/test/AR/ARCOMSTR.py index a3a9c8e565..593949ff88 100644 --- a/test/AR/ARCOMSTR.py +++ b/test/AR/ARCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the $ARCOMSTR construction variable allows you to customize @@ -39,6 +38,7 @@ test.file_fixture('myrewrite.py') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(tools=['default', 'ar'], ARCOM = r'%(_python_)s mycompile.py ar $TARGET $SOURCES', ARCOMSTR = 'Archiving $TARGET from $SOURCES', diff --git a/test/AR/ARFLAGS.py b/test/AR/ARFLAGS.py index 21b3793808..f1a0f98cbb 100644 --- a/test/AR/ARFLAGS.py +++ b/test/AR/ARFLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -35,6 +34,7 @@ test.file_fixture('wrapper.py') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) foo = Environment(LIBS = ['foo'], LIBPATH = ['.']) bar = Environment(LIBS = ['bar'], LIBPATH = ['.'], AR = '', ARFLAGS = foo.subst(r'%(_python_)s wrapper.py $AR $ARFLAGS')) diff --git a/test/AS/AS.py b/test/AS/AS.py index 0b6611dd06..cd1c5fe456 100644 --- a/test/AS/AS.py +++ b/test/AS/AS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify the ability to set the $AS construction variable to a different @@ -41,7 +40,9 @@ test.file_fixture(['fixture', 'myas.py']) test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', +DefaultEnvironment(tools=[]) +env = Environment(tools=['link','as','gcc'], + LINK = r'%(_python_)s mylink.py', AS = r'%(_python_)s myas.py', CC = r'%(_python_)s myas.py') env.Program(target = 'test1', source = 'test1.s') diff --git a/test/AS/ASCOM.py b/test/AS/ASCOM.py index ab77586747..01add8cafa 100644 --- a/test/AS/ASCOM.py +++ b/test/AS/ASCOM.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to configure the $ASCOM construction variable. @@ -46,7 +45,9 @@ alt_asm_suffix = '.asm' test.write('SConstruct', """ -env = Environment(ASCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['as'], + ASCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', OBJSUFFIX = '.obj', SHOBJPREFIX = '', SHOBJSUFFIX = '.shobj') diff --git a/test/AS/ASCOMSTR.py b/test/AS/ASCOMSTR.py index 2aab94c5ba..956c97cb6d 100644 --- a/test/AS/ASCOMSTR.py +++ b/test/AS/ASCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the $ASCOMSTR construction variable allows you to configure @@ -47,7 +46,9 @@ alt_asm_suffix = '.asm' test.write('SConstruct', """ -env = Environment(ASCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['as'], + ASCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', ASCOMSTR = 'Assembling $TARGET from $SOURCE', OBJSUFFIX = '.obj') env.Object(target = 'test1', source = 'test1.s') diff --git a/test/AS/ASFLAGS.py b/test/AS/ASFLAGS.py index 4a1c162d56..bbc2188300 100644 --- a/test/AS/ASFLAGS.py +++ b/test/AS/ASFLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import sys @@ -38,14 +37,11 @@ o = ' -x' o_c = ' -x -c' -if sys.platform == 'win32': - import SCons.Tool.MSCommon as msc - - if msc.msvc_exists(): - o_c = ' -x' test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', +DefaultEnvironment(tools=[]) +env = Environment(tools=['link','as'], + LINK = r'%(_python_)s mylink.py', LINKFLAGS = [], AS = r'%(_python_)s myas_args.py', ASFLAGS = '-x', CC = r'%(_python_)s myas_args.py') diff --git a/test/AS/ASPP.py b/test/AS/ASPP.py index 9c8f047b26..b256ace597 100644 --- a/test/AS/ASPP.py +++ b/test/AS/ASPP.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,10 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - import TestSCons @@ -36,7 +34,9 @@ test.file_fixture(['fixture', 'myas.py']) test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', +DefaultEnvironment(tools=[]) +env = Environment(tools=['link','cc','as'], + LINK = r'%(_python_)s mylink.py', LINKFLAGS = [], ASPP = r'%(_python_)s myas.py', CC = r'%(_python_)s myas.py') diff --git a/test/AS/ASPPCOM.py b/test/AS/ASPPCOM.py index ce938bbc1c..5c4c7458c3 100644 --- a/test/AS/ASPPCOM.py +++ b/test/AS/ASPPCOM.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to configure the $ASPPCOM construction variable. @@ -37,7 +36,9 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ -env = Environment(ASPPCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['as'], + ASPPCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', OBJSUFFIX = '.obj', SHOBJPREFIX = '', SHOBJSUFFIX = '.shobj') diff --git a/test/AS/ASPPCOMSTR.py b/test/AS/ASPPCOMSTR.py index 0ee18f5d0d..030d8a598a 100644 --- a/test/AS/ASPPCOMSTR.py +++ b/test/AS/ASPPCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the $ASPPCOMSTR construction variable allows you to customize @@ -38,7 +37,9 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ -env = Environment(ASPPCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['as'], + ASPPCOM = r'%(_python_)s mycompile.py as $TARGET $SOURCE', ASPPCOMSTR = 'Assembling $TARGET from $SOURCE', OBJSUFFIX = '.obj') env.Object(target = 'test1', source = 'test1.spp') diff --git a/test/AS/ASPPFLAGS.py b/test/AS/ASPPFLAGS.py index f7d7c5cf71..90f3f4bb95 100644 --- a/test/AS/ASPPFLAGS.py +++ b/test/AS/ASPPFLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import sys @@ -38,14 +37,11 @@ o = ' -x' o_c = ' -x -c' -if sys.platform == 'win32': - import SCons.Tool.MSCommon as msc - - if msc.msvc_exists(): - o_c = ' -x' test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', +DefaultEnvironment(tools=[]) +env = Environment(tools=['link','as','cc'], + LINK = r'%(_python_)s mylink.py', LINKFLAGS = [], ASPP = r'%(_python_)s myas_args.py', ASPPFLAGS = '-x', CC = r'%(_python_)s myas_args.py') diff --git a/test/AS/ml.py b/test/AS/ml.py index 0506f5f04e..5684fa87d7 100644 --- a/test/AS/ml.py +++ b/test/AS/ml.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify correct use of the live 'ml' assembler. diff --git a/test/AS/nasm.py b/test/AS/nasm.py index 4d93c7f897..09090b6e60 100644 --- a/test/AS/nasm.py +++ b/test/AS/nasm.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify correct use of the live 'nasm' assembler. @@ -39,7 +38,6 @@ test = TestSCons.TestSCons() nasm = test.where_is('nasm') - if not nasm: test.skip_test('nasm not found; skipping test\n') @@ -77,25 +75,26 @@ test.file_fixture('wrapper.py') -test.write('SConstruct', """ -eee = Environment(tools = ['gcc', 'gnulink', 'nasm'], - CFLAGS = ['-m32'], - LINKFLAGS = '-m32', - ASFLAGS = '-f %(nasm_format)s') -fff = eee.Clone(AS = r'%(_python_)s wrapper.py ' + WhereIs('nasm')) -eee.Program(target = 'eee', source = ['eee.asm', 'eee_main.c']) -fff.Program(target = 'fff', source = ['fff.asm', 'fff_main.c']) -""" % locals()) - -test.write('eee.asm', -""" +test.write('SConstruct', f"""\ +_ = DefaultEnvironment(tools=[]) +eee = Environment( + tools=['gcc', 'gnulink', 'nasm'], + CFLAGS=['-m32'], + LINKFLAGS='-m32', + ASFLAGS='-f {nasm_format}', +) +fff = eee.Clone(AS=r'{_python_} wrapper.py ' + WhereIs('nasm')) +eee.Program(target='eee', source=['eee.asm', 'eee_main.c']) +fff.Program(target='fff', source=['fff.asm', 'fff_main.c']) +""") + +test.write('eee.asm', """\ global name name: db 'eee.asm',0 """) -test.write('fff.asm', -""" +test.write('fff.asm', """\ global name name: db 'fff.asm',0 @@ -103,6 +102,8 @@ test.write('eee_main.c', r""" #include +#include + extern char name[]; int @@ -129,20 +130,16 @@ } """) -test.run(arguments = 'eee' + _exe, stderr = None) - -test.run(program = test.workpath('eee'), stdout = "eee_main.c eee.asm\n") +test.run(arguments='eee' + _exe, stderr=None) +test.run(program=test.workpath('eee'), stdout="eee_main.c eee.asm\n") test.must_not_exist('wrapper.out') -test.run(arguments = 'fff' + _exe) - -test.run(program = test.workpath('fff'), stdout = "fff_main.c fff.asm\n") +test.run(arguments='fff' + _exe) +test.run(program=test.workpath('fff'), stdout="fff_main.c fff.asm\n") test.must_match('wrapper.out', "wrapper.py\n") - - test.pass_test() # Local Variables: diff --git a/test/Actions/addpost-link.py b/test/Actions/addpost-link.py index 0a238e11b7..65a59b3897 100644 --- a/test/Actions/addpost-link.py +++ b/test/Actions/addpost-link.py @@ -41,6 +41,7 @@ test.dir_fixture('addpost-link-fixture') test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() mylib = env.StaticLibrary('mytest', 'test_lib.c') diff --git a/test/Actions/append.py b/test/Actions/append.py index 42a414bc62..dcc9898387 100644 --- a/test/Actions/append.py +++ b/test/Actions/append.py @@ -41,17 +41,17 @@ test.dir_fixture('append-fixture') test.write('SConstruct', """ - +DefaultEnvironment(tools=[]) env=Environment() def before(env, target, source): - with open(str(target[0]), "wb") as f: + with open(target[0], "wb") as f: f.write(b"Foo\\n") with open("before.txt", "wb") as f: f.write(b"Bar\\n") def after(env, target, source): - with open(str(target[0]), "rb") as fin, open("after%s", "wb") as fout: + with open(target[0], "rb") as fin, open("after%s", "wb") as fout: fout.write(fin.read()) env.Prepend(LINKCOM=Action(before)) @@ -63,7 +63,7 @@ def after(env, target, source): test.run(arguments='.') test.must_match('before.txt', 'Bar\n') -os.chmod(after_exe, os.stat(after_exe)[stat.ST_MODE] | stat.S_IXUSR) +os.chmod(after_exe, os.stat(after_exe).st_mode | stat.S_IXUSR) test.run(program=after_exe, stdout="Foo\n") test.pass_test() diff --git a/test/Actions/exitstatfunc.py b/test/Actions/exitstatfunc.py index 2e2a540849..53e8477065 100644 --- a/test/Actions/exitstatfunc.py +++ b/test/Actions/exitstatfunc.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that setting exitstatfunc on an Action works as advertised. @@ -33,12 +32,13 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) def always_succeed(s): # Always return 0, which indicates success. return 0 def copy_fail(target, source, env): - with open(str(source[0]), 'rb') as infp, open(str(target[0]), 'wb') as f: + with open(source[0], 'rb') as infp, open(target[0], 'wb') as f: f.write(infp.read()) return 2 diff --git a/test/Actions/function.py b/test/Actions/function.py index 0b191e4044..9b584d807e 100644 --- a/test/Actions/function.py +++ b/test/Actions/function.py @@ -69,7 +69,7 @@ def writeDeps(target, source, env, b=%(b)s, r=r %(extraarg)s , header=header, tr def foo(b=b): return %(nestedfuncexp)s - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(bytearray(header, 'utf-8')) for d in env['ENVDEPS']: f.write(bytearray(d+'%(separator)s', 'utf-8')) @@ -122,7 +122,7 @@ def foo(b=b): def runtest(arguments, expectedOutFile, expectedRebuild=True, stderr=""): test.run( arguments=arguments, - stdout=expectedRebuild and rebuildstr or nobuildstr, + stdout=rebuildstr if expectedRebuild else nobuildstr, stderr="", ) diff --git a/test/Actions/pre-post-fixture/work2/SConstruct b/test/Actions/pre-post-fixture/work2/SConstruct index 347dcbe6f1..b2079f320e 100644 --- a/test/Actions/pre-post-fixture/work2/SConstruct +++ b/test/Actions/pre-post-fixture/work2/SConstruct @@ -3,7 +3,7 @@ # Copyright The SCons Foundation def b(target, source, env): - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write((env['X'] + '\n').encode()) DefaultEnvironment(tools=[]) diff --git a/test/Actions/pre-post-fixture/work3/SConstruct b/test/Actions/pre-post-fixture/work3/SConstruct index 54f537ac8d..db63503d71 100644 --- a/test/Actions/pre-post-fixture/work3/SConstruct +++ b/test/Actions/pre-post-fixture/work3/SConstruct @@ -9,7 +9,7 @@ def post(target, source, env): pass def build(target, source, env): - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(b'build()\n') DefaultEnvironment(tools=[]) diff --git a/test/Actions/pre-post.py b/test/Actions/pre-post.py index ac6a96fd12..14a9b85292 100644 --- a/test/Actions/pre-post.py +++ b/test/Actions/pre-post.py @@ -50,16 +50,16 @@ def before(env, target, source): a=str(target[0]) with open(a, "wb") as f: f.write(b"Foo\\n") - os.chmod(a, os.stat(a)[stat.ST_MODE] | stat.S_IXUSR) + os.chmod(a, os.stat(a).st_mode | stat.S_IXUSR) with open("before.txt", "ab") as f: - f.write((os.path.splitext(str(target[0]))[0] + "\\n").encode()) + f.write((os.path.splitext(target[0])[0] + "\\n").encode()) def after(env, target, source): t = str(target[0]) a = "after_" + t with open(t, "rb") as fin, open(a, "wb") as fout: fout.write(fin.read()) - os.chmod(a, os.stat(a)[stat.ST_MODE] | stat.S_IXUSR) + os.chmod(a, os.stat(a).st_mode | stat.S_IXUSR) foo = env.Program(source='foo.c', target='foo') AddPreAction(foo, before) @@ -104,11 +104,11 @@ def after(env, target, source): DefaultEnvironment(tools=[]) def pre_action(target, source, env): - with open(str(target[0]), 'ab') as f: + with open(target[0], 'ab') as f: f.write(('pre %%s\\n' %% source[0]).encode()) def post_action(target, source, env): - with open(str(target[0]), 'ab') as f: + with open(target[0], 'ab') as f: f.write(('post %%s\\n' %% source[0]).encode()) env = Environment(tools=[]) diff --git a/test/Actions/timestamp.py b/test/Actions/timestamp.py index d94a287ce4..e13ec005a6 100644 --- a/test/Actions/timestamp.py +++ b/test/Actions/timestamp.py @@ -45,7 +45,7 @@ test.write('SConstruct', """\ def my_copy(target, source, env): - with open(str(target[0]), 'w') as f, open(str(source[0]), 'r') as infp: + with open(target[0], 'w') as f, open(source[0], 'r') as infp: f.write(infp.read()) env = Environment() env.Decider('timestamp-match') diff --git a/test/Actions/unicode-signature-fixture/SConstruct b/test/Actions/unicode-signature-fixture/SConstruct index 95c969d29c..3e56b98861 100644 --- a/test/Actions/unicode-signature-fixture/SConstruct +++ b/test/Actions/unicode-signature-fixture/SConstruct @@ -5,7 +5,7 @@ fnode = File(u'foo.txt') def funcact(target, source, env): - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(b"funcact\n") for i in range(300): pass diff --git a/test/AddOption/args-and-targets.py b/test/AddOption/args-and-targets.py index 56bdd8502d..856c868454 100644 --- a/test/AddOption/args-and-targets.py +++ b/test/AddOption/args-and-targets.py @@ -35,7 +35,8 @@ test.write( 'SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) AddOption( '--extra', nargs=1, diff --git a/test/AddOption/basic.py b/test/AddOption/basic.py index b1b8f2e1de..f488c704cb 100644 --- a/test/AddOption/basic.py +++ b/test/AddOption/basic.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,13 +22,10 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ -Verify the help text when the AddOption() function is used (and when -it's not). +Verify added options give the expected default/command line values +when fetched with GetOption. """ import TestSCons @@ -34,36 +33,56 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ -env = Environment() -AddOption('--force', - action="store_true", - help='force installation (overwrite any existing files)') -AddOption('--prefix', - nargs=1, - dest='prefix', - action='store', - type='string', - metavar='DIR', - help='installation prefix') +from SCons.Script.SConsOptions import SConsOption + +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) +AddOption( + '-F', '--force', + action="store_true", + help='force installation (overwrite any existing files)', +) +AddOption( + '--prefix', + nargs=1, + dest='prefix', + action='store', + type='string', + metavar='DIR', + settable=True, + help='installation prefix', +) +AddOption( + '--set', + action="store_true", + help="try SetOption of 'prefix' to '/opt/share'" +) +z_opt = SConsOption("--zcount", type="int", nargs=1, settable=True) +AddOption(z_opt) + f = GetOption('force') if f: f = "True" print(f) print(GetOption('prefix')) +if GetOption('set'): + SetOption('prefix', '/opt/share') + print(GetOption('prefix')) +if GetOption('zcount'): + print(GetOption('zcount')) """) -test.run('-Q -q .', - stdout="None\nNone\n") - -test.run('-Q -q . --force', - stdout="True\nNone\n") - -test.run('-Q -q . --prefix=/home/foo', - stdout="None\n/home/foo\n") - -test.run('-Q -q . -- --prefix=/home/foo --force', - status=1, - stdout="None\nNone\n") +test.run('-Q -q .', stdout="None\nNone\n") +test.run('-Q -q . --force', stdout="True\nNone\n") +test.run('-Q -q . -F', stdout="True\nNone\n") +test.run('-Q -q . --prefix=/home/foo', stdout="None\n/home/foo\n") +test.run('-Q -q . -- --prefix=/home/foo --force', status=1, stdout="None\nNone\n") +# check that SetOption works on prefix... +test.run('-Q -q . --set', stdout="None\nNone\n/opt/share\n") +# but the "command line wins" rule is not violated +test.run('-Q -q . --set --prefix=/home/foo', stdout="None\n/home/foo\n/home/foo\n") +# also try in case we pass a premade option object to AddOption +test.run('-Q -q . --zcount=22', stdout="None\nNone\n22\n") test.pass_test() diff --git a/test/AddOption/help.py b/test/AddOption/help.py index 23e5bd1d27..772a381d63 100644 --- a/test/AddOption/help.py +++ b/test/AddOption/help.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -26,24 +28,27 @@ it's not). """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - import TestSCons test = TestSCons.TestSCons() test.write('SConstruct', """\ -env = Environment() -AddOption('--force', - action="store_true", - help='force installation (overwrite existing files)') -AddOption('--prefix', - nargs=1, - dest='prefix', - action='store', - type='string', - metavar='DIR', - help='installation prefix') +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) +AddOption( + '--force', + action="store_true", + help='force installation (overwrite existing files)', +) +AddOption( + '--prefix', + nargs=1, + dest='prefix', + action='store', + type='string', + metavar='DIR', + help='installation prefix', +) """) expected_lines = [ diff --git a/test/AddOption/longopts.py b/test/AddOption/longopts.py index e0c0df4159..668591b7b7 100644 --- a/test/AddOption/longopts.py +++ b/test/AddOption/longopts.py @@ -35,6 +35,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) AddOption('--myargument', dest='myargument', type='string', default='gully') AddOption('--myarg', dest='myarg', type='string', default='balla') print("myargument: " + str(GetOption('myargument'))) diff --git a/test/AddOption/multi-arg.py b/test/AddOption/multi-arg.py index 1a71cfef06..4361214b8d 100644 --- a/test/AddOption/multi-arg.py +++ b/test/AddOption/multi-arg.py @@ -36,7 +36,7 @@ test.write( 'SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) AddOption('--extras', nargs=2, dest='extras', @@ -71,7 +71,7 @@ test.write( 'SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) AddOption( '--prefix', nargs=1, @@ -101,7 +101,7 @@ # one single-arg option test.run('-Q -q . --prefix=/home/foo', stdout="/home/foo\n()\n") # one two-arg option -test.run('-Q -q . --extras A B', status=1, stdout="None\n('A', 'B')\n") +test.run('-Q -q . --extras A B', status=2, stdout="None\n('A', 'B')\n") # single-arg option followed by two-arg option test.run( '-Q -q . --prefix=/home/foo --extras A B', diff --git a/test/AddOption/optional-arg.py b/test/AddOption/optional-arg.py index b88b796df7..362da00ec4 100644 --- a/test/AddOption/optional-arg.py +++ b/test/AddOption/optional-arg.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify use of the nargs='?' keyword argument to specify a long @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) AddOption('--install', nargs='?', dest='install', @@ -85,6 +85,7 @@ test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) AddOption('-X', nargs='?') """) diff --git a/test/Alias/Alias.py b/test/Alias/Alias.py index 1bb52f6ee9..5915b2df7e 100644 --- a/test/Alias/Alias.py +++ b/test/Alias/Alias.py @@ -44,7 +44,7 @@ test.write('SConstruct', """ B = Builder(action = r'%(_python_)s build.py $TARGET $SOURCES') DefaultEnvironment(tools=[]) # test speedup -env = Environment() +env = Environment(tools=[]) env['BUILDERS']['B'] = B env.B(target = 'f1.out', source = 'f1.in') env.B(target = 'f2.out', source = 'f2.in') @@ -137,7 +137,7 @@ Decider('content') B = Builder(action = r'%(_python_)s build.py $TARGET $SOURCES') DefaultEnvironment(tools=[]) # test speedup -env = Environment() +env = Environment(tools=[]) env['BUILDERS']['B'] = B env.B(target = 'f1.out', source = 'f1.in') env.B(target = 'f2.out', source = 'f2.in') diff --git a/test/Alias/Depends.py b/test/Alias/Depends.py index b1051ad171..8ab2ed6fa6 100644 --- a/test/Alias/Depends.py +++ b/test/Alias/Depends.py @@ -44,7 +44,7 @@ test.write('SConstruct', """ B = Builder(action = r'%(_python_)s build.py $TARGET $SOURCES') DefaultEnvironment(tools=[]) # test speedup -env = Environment() +env = Environment(tools=[]) env['BUILDERS']['B'] = B env.B(target = 'f1.out', source = 'f1.in') env.B(target = 'f2.out', source = 'f2.in') diff --git a/test/Alias/Dir-order.py b/test/Alias/Dir-order.py index f0eaa0ce2d..86d0495c57 100644 --- a/test/Alias/Dir-order.py +++ b/test/Alias/Dir-order.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Validate that calling Dir() for a string after we've used it as an @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) Alias('afoo', 'foo') f = Dir('foo') """) diff --git a/test/Alias/action.py b/test/Alias/action.py index 82124489d5..9a26be1dd4 100644 --- a/test/Alias/action.py +++ b/test/Alias/action.py @@ -33,10 +33,9 @@ test.write('SConstruct', """ def cat(target, source, env): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) def foo(target, source, env): @@ -52,7 +51,7 @@ def bar(target, source, env): f.write(bytearray("bar(%s, %s)\\n" % (target, source),'utf-8')) DefaultEnvironment(tools=[]) # test speedup -env = Environment(BUILDERS = {'Cat':Builder(action=cat)}) +env = Environment(tools=[], BUILDERS = {'Cat':Builder(action=cat)}) env.Alias(target = ['build-f1'], source = 'f1.out', action = foo) f1 = env.Cat('f1.out', 'f1.in') f2 = env.Cat('f2.out', 'f2.in') @@ -69,6 +68,19 @@ def bar(target, source, env): env.Alias('build-add3', f6) env.Alias('build-add3', action=foo) env.Alias('build-add3', action=bar) + + +f7 = env.Cat('f7.out', 'f6.in') +def build_it(target, source, env): + print("build_it: Goodbye") + return 0 + +def string_it(target, source, env): + return("string it: Goodbye") + +s = Action(build_it, string_it) +env.Alias('add_post_action', f7) +env.AddPostAction('add_post_action', s) """) test.write('f1.in', "f1.in 1\n") @@ -133,6 +145,9 @@ def bar(target, source, env): test.must_match('foo', "foo(['build-add3'], ['f6.out'])\n") test.must_match('bar', "bar(['build-add3'], ['f6.out'])\n") +test.run(arguments = 'add_post_action') +test.must_contain_all(test.stdout(), 'string it: Goodbye') + test.pass_test() # Local Variables: diff --git a/test/Alias/errors.py b/test/Alias/errors.py index 1334f4e95b..abac5d569d 100644 --- a/test/Alias/errors.py +++ b/test/Alias/errors.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,16 +22,14 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons test = TestSCons.TestSCons() test.write('SConstruct', """ -env=Environment() +DefaultEnvironment(tools=[]) +env=Environment(tools=[]) Decider('content') env.Alias('C', 'D') env.Alias('B', 'C') diff --git a/test/Alias/scanner.py b/test/Alias/scanner.py index a6ded814d9..2d983c04d6 100644 --- a/test/Alias/scanner.py +++ b/test/Alias/scanner.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,28 +22,26 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# """ Test that an Alias of a node with a Scanner works. """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) XBuilder = Builder(action = cat, src_suffix = '.x', suffix = '.c') -env = Environment() +env = Environment(tools=[]) env.Append(BUILDERS = { 'XBuilder': XBuilder }) f = env.XBuilder(source = ['file.x'], target = ['file.c']) env.Alias(target = ['cfiles'], source = f) diff --git a/test/Alias/srcdir.py b/test/Alias/srcdir.py index 977d114488..5115452bf1 100644 --- a/test/Alias/srcdir.py +++ b/test/Alias/srcdir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that an Alias for a VariantDir()'s source directory works as @@ -62,8 +61,8 @@ test.write('SConstruct', """\ import os.path - -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) def at_copy_ext(target, source, env): n = str(source[0]) diff --git a/test/Batch/Boolean.py b/test/Batch/Boolean.py index c11874502d..f4bc2ce6d1 100644 --- a/test/Batch/Boolean.py +++ b/test/Batch/Boolean.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify basic use of batch_key to write a batch builder that handles @@ -34,11 +33,13 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) + def batch_build(target, source, env): for t, s in zip(target, source): - with open(str(t), 'wb') as f, open(str(s), 'rb') as infp: + with open(t, 'wb') as f, open(s, 'rb') as infp: f.write(infp.read()) -env = Environment() +env = Environment(tools=[]) bb = Action(batch_build, batch_key=True) env['BUILDERS']['Batch'] = Builder(action=bb) env1 = env.Clone() diff --git a/test/Batch/CHANGED_SOURCES.py b/test/Batch/CHANGED_SOURCES.py index 477869ff1f..5899269194 100644 --- a/test/Batch/CHANGED_SOURCES.py +++ b/test/Batch/CHANGED_SOURCES.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify use of $CHANGED_SOURCES with batch builders correctly decides @@ -49,7 +48,8 @@ """) test.write('SConstruct', """ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['BATCH_BUILD'] = 'batch_build.py' env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $CHANGED_SOURCES' bb = Action('$BATCHCOM', batch_key=True, targets='CHANGED_TARGETS') diff --git a/test/Batch/SOURCES.py b/test/Batch/SOURCES.py index 8198f922de..ea17224e4e 100644 --- a/test/Batch/SOURCES.py +++ b/test/Batch/SOURCES.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify use of $SOURCES with batch builders correctly decides to @@ -49,7 +48,8 @@ """) test.write('SConstruct', """ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['BATCH_BUILD'] = 'batch_build.py' env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $SOURCES' bb = Action('$BATCHCOM', batch_key=True) diff --git a/test/Batch/action-changed.py b/test/Batch/action-changed.py index da5115e9ed..771b876250 100644 --- a/test/Batch/action-changed.py +++ b/test/Batch/action-changed.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that targets in a batch builder are rebuilt when the @@ -62,7 +61,9 @@ # Provide IMPLICIT_COMMAND_DEPENDENCIES=2 so we take a dependency on build.py. # Without that, we only scan the first entry in the action string. test.write('SConstruct', """ -env = Environment(IMPLICIT_COMMAND_DEPENDENCIES=2) +DefaultEnvironment(tools=[]) +env = Environment(tools=[], + IMPLICIT_COMMAND_DEPENDENCIES=2) env.PrependENVPath('PATHEXT', '.PY') bb = Action(r'%(_python_)s "%(build_py_workpath)s" $CHANGED_TARGETS -- $CHANGED_SOURCES', batch_key=True, diff --git a/test/Batch/callable.py b/test/Batch/callable.py index 9fe14ee87e..cc7e8e371c 100644 --- a/test/Batch/callable.py +++ b/test/Batch/callable.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify passing in a batch_key callable for more control over how @@ -38,16 +37,17 @@ test.subdir('sub1', 'sub2') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) def batch_build(target, source, env): for t, s in zip(target, source): - with open(str(t), 'wb') as f, open(str(s), 'rb') as infp: + with open(t, 'wb') as f, open(s, 'rb') as infp: f.write(infp.read()) if ARGUMENTS.get('BATCH_CALLABLE'): def batch_key(action, env, target, source): return (id(action), id(env), target[0].dir) else: batch_key=True -env = Environment() +env = Environment(tools=[]) bb = Action(batch_build, batch_key=batch_key) env['BUILDERS']['Batch'] = Builder(action=bb) env1 = env.Clone() diff --git a/test/Batch/changed_sources_alwaysbuild.py b/test/Batch/changed_sources_alwaysbuild.py index ba947d6c35..5e278a5236 100644 --- a/test/Batch/changed_sources_alwaysbuild.py +++ b/test/Batch/changed_sources_alwaysbuild.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that files marked AlwaysBuild also get put into CHANGED_SOURCES. diff --git a/test/Batch/generated.py b/test/Batch/generated.py index 65ce8a855c..59db0f82e6 100644 --- a/test/Batch/generated.py +++ b/test/Batch/generated.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify use of a batch builder when one of the later targets in the @@ -34,15 +33,17 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) + def batch_build(target, source, env): for t, s in zip(target, source): - with open(str(t), 'wb') as fp: + with open(t, 'wb') as fp: if str(t) == 'f3.out': with open('f3.include', 'rb') as f: fp.write(f.read()) - with open(str(s), 'rb') as f: + with open(s, 'rb') as f: fp.write(f.read()) -env = Environment() +env = Environment(tools=[]) bb = Action(batch_build, batch_key=True) env['BUILDERS']['Batch'] = Builder(action=bb) env1 = env.Clone() diff --git a/test/Batch/removed.py b/test/Batch/removed.py index b244cb3b56..60c2fbf852 100644 --- a/test/Batch/removed.py +++ b/test/Batch/removed.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify correct batch builder $CHANGED_SOURCES behavior when some of @@ -48,7 +47,8 @@ """) test.write('SConstruct', """ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['BATCH_BUILD'] = 'batch_build.py' env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $CHANGED_SOURCES' bb = Action('$BATCHCOM', batch_key=True, targets='CHANGED_TARGETS') diff --git a/test/Batch/up_to_date.py b/test/Batch/up_to_date.py index 229c88f4b8..ae73afbced 100644 --- a/test/Batch/up_to_date.py +++ b/test/Batch/up_to_date.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify simple use of $SOURCES with batch builders correctly decide @@ -48,7 +47,8 @@ """) test.write('SConstruct', """ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['BATCH_BUILD'] = 'batch_build.py' env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $SOURCES' bb = Action('$BATCHCOM', batch_key=True) diff --git a/test/Builder-factories.py b/test/Builder-factories.py index e1fb65ce3e..229b4972a0 100644 --- a/test/Builder-factories.py +++ b/test/Builder-factories.py @@ -47,7 +47,7 @@ def mkdir(env, source, target): f.write(b"MakeDirectory\\n") MakeDirectory = Builder(action=mkdir, target_factory=Dir) def collect(env, source, target): - with open(str(target[0]), 'wb') as out: + with open(target[0], 'wb') as out: dir = str(source[0]) for f in sorted(os.listdir(dir)): f = os.path.join(dir, f) diff --git a/test/Builder/TargetSubst.py b/test/Builder/TargetSubst.py index 76ca76c5fd..de4d724f7d 100644 --- a/test/Builder/TargetSubst.py +++ b/test/Builder/TargetSubst.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that the ensure_suffix argument to causes us to add the suffix diff --git a/test/Builder/add_src_builder.py b/test/Builder/add_src_builder.py index e499933d5e..733d1b92a2 100644 --- a/test/Builder/add_src_builder.py +++ b/test/Builder/add_src_builder.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that we can call add_src_builder() to add a builder to diff --git a/test/Builder/different-actions.py b/test/Builder/different-actions.py index f355586665..457639121a 100644 --- a/test/Builder/different-actions.py +++ b/test/Builder/different-actions.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that two builders in two environments with different diff --git a/test/Builder/ensure_suffix.py b/test/Builder/ensure_suffix.py index 52fb1d4217..08cf16e354 100644 --- a/test/Builder/ensure_suffix.py +++ b/test/Builder/ensure_suffix.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that the ensure_suffix argument to causes us to add the suffix diff --git a/test/Builder/errors.py b/test/Builder/errors.py index 375e052a16..1e6a3a0fef 100644 --- a/test/Builder/errors.py +++ b/test/Builder/errors.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to catch Builder creation with poorly specified Actions. @@ -36,8 +35,10 @@ SConstruct_path = test.workpath('SConstruct') sconstruct = """ +DefaultEnvironment(tools=[]) + def buildop(env, source, target): - with open(str(target[0]), 'wb') as outf, open(str(source[0]), 'r') as infp: + with open(target[0], 'wb') as outf, open(source[0], 'r') as infp: for line in inpf.readlines(): if line.find(str(target[0])) == -1: outf.write(line) @@ -54,7 +55,7 @@ def buildop(env, source, target): built """) -python_file_line = test.python_file_line(SConstruct_path, 11) +python_file_line = test.python_file_line(SConstruct_path, 13) ### Gross mistake in Builder spec diff --git a/test/Builder/multi/different-actions.py b/test/Builder/multi/different-actions.py index ec07b624e0..53141e8400 100644 --- a/test/Builder/multi/different-actions.py +++ b/test/Builder/multi/different-actions.py @@ -36,9 +36,9 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=Action(build, varlist=['XXX']), multi=1) diff --git a/test/Builder/multi/different-environments.py b/test/Builder/multi/different-environments.py index 82b10ecb1f..41b59d81cc 100644 --- a/test/Builder/multi/different-environments.py +++ b/test/Builder/multi/different-environments.py @@ -39,10 +39,10 @@ test.write('build.py', r"""\ import sys def build(num, target, source): - with open(str(target), 'wb') as f: + with open(target[0], 'wb') as f: f.write('%s\n' % num) - for s in source: - with open(str(s), 'rb') as infp: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) build(sys.argv[1],sys.argv[2],sys.argv[3:]) """) diff --git a/test/Builder/multi/different-multi.py b/test/Builder/multi/different-multi.py index 3084bf5381..0adabf7d74 100644 --- a/test/Builder/multi/different-multi.py +++ b/test/Builder/multi/different-multi.py @@ -37,9 +37,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) def build2(env, target, source): diff --git a/test/Builder/multi/different-order.py b/test/Builder/multi/different-order.py index 4018159e18..6fc800d5f6 100644 --- a/test/Builder/multi/different-order.py +++ b/test/Builder/multi/different-order.py @@ -39,9 +39,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): for t in target: - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/different-overrides.py b/test/Builder/multi/different-overrides.py index c4267f300c..57418aa2b6 100644 --- a/test/Builder/multi/different-overrides.py +++ b/test/Builder/multi/different-overrides.py @@ -36,9 +36,9 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/different-target-lists.py b/test/Builder/multi/different-target-lists.py index 4b6c49e723..5c3ea76605 100644 --- a/test/Builder/multi/different-target-lists.py +++ b/test/Builder/multi/different-target-lists.py @@ -43,9 +43,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): for t in target: - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/error.py b/test/Builder/multi/error.py index 3b2a8d493c..ae3f48d795 100644 --- a/test/Builder/multi/error.py +++ b/test/Builder/multi/error.py @@ -37,9 +37,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=0) diff --git a/test/Builder/multi/lone-target-list.py b/test/Builder/multi/lone-target-list.py index 885d34a99c..0a1450066b 100644 --- a/test/Builder/multi/lone-target-list.py +++ b/test/Builder/multi/lone-target-list.py @@ -37,9 +37,9 @@ def build(env, target, source): for t in target: - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(t, 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/multi.py b/test/Builder/multi/multi.py index aec09513e5..140b8c4378 100644 --- a/test/Builder/multi/multi.py +++ b/test/Builder/multi/multi.py @@ -37,9 +37,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/same-actions.py b/test/Builder/multi/same-actions.py index f2a8fe3ec2..7b56dd045d 100644 --- a/test/Builder/multi/same-actions.py +++ b/test/Builder/multi/same-actions.py @@ -37,9 +37,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/multi/same-overrides.py b/test/Builder/multi/same-overrides.py index c545329ef6..9b5698bca3 100644 --- a/test/Builder/multi/same-overrides.py +++ b/test/Builder/multi/same-overrides.py @@ -37,10 +37,10 @@ test.write('build.py', r"""\ import sys def build(num, target, source): - with open(str(target), 'wb') as f: + with open(target, 'wb') as f: f.write(bytearray('%s\n'% num,'utf-8')) - for s in source: - with open(str(s), 'rb') as infp: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) build(sys.argv[1], sys.argv[2], sys.argv[3:]) """) diff --git a/test/Builder/multi/same-targets.py b/test/Builder/multi/same-targets.py index 17f6f990dc..f343a82e4e 100644 --- a/test/Builder/multi/same-targets.py +++ b/test/Builder/multi/same-targets.py @@ -38,9 +38,9 @@ def build(env, target, source): for t in target: - with open(str(t), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(t, 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=1) diff --git a/test/Builder/non-multi.py b/test/Builder/non-multi.py index 3c09db168c..83fc5230d3 100644 --- a/test/Builder/non-multi.py +++ b/test/Builder/non-multi.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that a builder without "multi" set can still be called multiple @@ -37,9 +36,9 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) B = Builder(action=build, multi=0) diff --git a/test/Builder/not-a-Builder.py b/test/Builder/not-a-Builder.py index 37ce6056ce..f0a16b3654 100644 --- a/test/Builder/not-a-Builder.py +++ b/test/Builder/not-a-Builder.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the error when trying to configure a Builder with a non-Builder object. diff --git a/test/Builder/same-actions-diff-envs.py b/test/Builder/same-actions-diff-envs.py index b80c9883b0..9d6fcbb049 100644 --- a/test/Builder/same-actions-diff-envs.py +++ b/test/Builder/same-actions-diff-envs.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that two builders in two environments with the same actions generate @@ -37,7 +36,7 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'w') as f: + with open(target[0], 'w') as f: f.write('1') B = Builder(action=build) diff --git a/test/Builder/same-actions-diff-overrides.py b/test/Builder/same-actions-diff-overrides.py index 8f6bdca866..cd7c1527d4 100644 --- a/test/Builder/same-actions-diff-overrides.py +++ b/test/Builder/same-actions-diff-overrides.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that two calls to a builder with different overrides, but the same @@ -37,7 +36,7 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'w') as f: + with open(target[0], 'w') as f: f.write('1') B = Builder(action=build) diff --git a/test/Builder/srcdir.py b/test/Builder/srcdir.py index 6ce27feed8..d084f9380c 100644 --- a/test/Builder/srcdir.py +++ b/test/Builder/srcdir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." """ Verify that specifying a srcdir when calling a Builder correctly diff --git a/test/Builder/wrapper.py b/test/Builder/wrapper.py index acb1d44fd2..a4de067a88 100644 --- a/test/Builder/wrapper.py +++ b/test/Builder/wrapper.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to use a direct Python function to wrap @@ -39,9 +38,9 @@ import os.path import string def cat(target, source, env): - with open(str(target[0]), 'wb') as fp: - for s in map(str, source): - with open(s, 'rb') as infp: + with open(target[0], 'wb') as fp: + for src in source: + with open(src, 'rb') as infp: fp.write(infp.read()) Cat = Builder(action=cat) def Wrapper(env, target, source): diff --git a/test/CC/CC-live.py b/test/CC/CC-live.py new file mode 100644 index 0000000000..4848b0f5f8 --- /dev/null +++ b/test/CC/CC-live.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test the C compiler name variable $CC. +This is a live test, calling the detected C compiler via a wrapper. +""" + +import os +import sys +import TestSCons + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +test = TestSCons.TestSCons() + +test.dir_fixture('CC-fixture') +test.file_fixture('wrapper.py') + +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment() +bar = Environment() + +bar['CC'] = r'{_python_} wrapper.py ' + foo['CC'] +foo.Program(target='foo', source='foo.c') +bar.Program(target='bar', source='bar.c') +""") + +test.run(arguments='foo' + _exe) +test.must_not_exist(test.workpath('wrapper.out')) +test.up_to_date(arguments='foo' + _exe) + +test.run(arguments='bar' + _exe) +test.must_match('wrapper.out', "wrapper.py\n", mode='r') +test.up_to_date(arguments='bar' + _exe) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/CC/CC.py b/test/CC/CC.py index a548421ece..ecf126e416 100644 --- a/test/CC/CC.py +++ b/test/CC/CC.py @@ -23,6 +23,10 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test the C compiler name variable $CC, calling a mocked compiler. +""" + import os import sys import TestSCons @@ -35,59 +39,34 @@ test.dir_fixture('CC-fixture') test.file_fixture('mylink.py') -test.write('SConstruct', """ -cc = Environment().Dictionary('CC') +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) env = Environment( - LINK=r'%(_python_)s mylink.py', + tools=['link', 'cc'], + LINK=r'{_python_} mylink.py', LINKFLAGS=[], - CC=r'%(_python_)s mycc.py', - CXX=cc, - CXXFLAGS=[], + CC=r'{_python_} mycc.py', ) env.Program(target='test1', source='test1.c') -""" % locals()) - -test.run(arguments = '.', stderr = None) +""") +test.run(arguments='.', stderr=None) test.must_match('test1' + _exe, "This is a .c file.\n", mode='r') if os.path.normcase('.c') == os.path.normcase('.C'): - - test.write('SConstruct', """ -cc = Environment().Dictionary('CC') + test.write('SConstruct2', f""" +DefaultEnvironment(tools=[]) env = Environment( - LINK=r'%(_python_)s mylink.py', - CC=r'%(_python_)s mycc.py', - CXX=cc, + tools=['link', 'cc'], + LINK=r'{_python_} mylink.py', + CC=r'{_python_} mycc.py', ) env.Program(target='test2', source='test2.C') -""" % locals()) +""") - test.run(arguments = '.', stderr = None) + test.run(arguments=['-f', 'SConstruct2', '.'], stderr=None) test.must_match('test2' + _exe, "This is a .C file.\n", mode='r') -test.file_fixture('wrapper.py') - -test.write('SConstruct', """ -foo = Environment() -cc = foo.Dictionary('CC') -bar = Environment(CC=r'%(_python_)s wrapper.py ' + cc) -foo.Program(target='foo', source='foo.c') -bar.Program(target='bar', source='bar.c') -""" % locals()) - -test.run(arguments = 'foo' + _exe) - -test.must_not_exist(test.workpath('wrapper.out')) - -test.up_to_date(arguments = 'foo' + _exe) - -test.run(arguments = 'bar' + _exe) - -test.must_match('wrapper.out', "wrapper.py\n", mode='r') - -test.up_to_date(arguments = 'bar' + _exe) - test.pass_test() # Local Variables: diff --git a/test/CC/CCCOM.py b/test/CC/CCCOM.py index 291dad86ae..9ee49b446b 100644 --- a/test/CC/CCCOM.py +++ b/test/CC/CCCOM.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the ability to configure the $CCCOM construction variable. @@ -44,7 +43,9 @@ alt_c_suffix = '.c' test.write('SConstruct', """ -env = Environment(CCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['cc'], + CCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', OBJSUFFIX='.obj') env.Object(target = 'test1', source = 'test1.c') env.Object(target = 'test2', source = 'test2%(alt_c_suffix)s') diff --git a/test/CC/CCCOMSTR.py b/test/CC/CCCOMSTR.py index 9977243867..da07dd3d44 100644 --- a/test/CC/CCCOMSTR.py +++ b/test/CC/CCCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the $CCCOMSTR construction variable allows you to configure @@ -45,7 +44,9 @@ alt_c_suffix = '.c' test.write('SConstruct', """ -env = Environment(CCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', +DefaultEnvironment(tools=[]) +env = Environment(tools=['cc'], + CCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', CCCOMSTR = 'Building $TARGET from $SOURCE', OBJSUFFIX='.obj') env.Object(target = 'test1', source = 'test1.c') diff --git a/test/CC/CCFLAGS.py b/test/CC/CCFLAGS-live.py similarity index 61% rename from test/CC/CCFLAGS.py rename to test/CC/CCFLAGS-live.py index 069b429449..ae280bcc22 100644 --- a/test/CC/CCFLAGS.py +++ b/test/CC/CCFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,18 +22,22 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test behavior of CCFLAGS. + +This is a live test, uses the detected C compiler. +""" import sys + import TestSCons -_obj = TestSCons._obj +test = TestSCons.TestSCons() if sys.platform == 'win32': import SCons.Tool.MSCommon as msc - + if not msc.msvc_exists(): fooflags = '-DFOO' barflags = '-DBAR' @@ -42,18 +48,17 @@ fooflags = '-DFOO' barflags = '-DBAR' -test = TestSCons.TestSCons() +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment(CCFLAGS='{fooflags}') +bar = Environment(CCFLAGS='{barflags}') -test.write('SConstruct', """ -foo = Environment(CCFLAGS = '%s') -bar = Environment(CCFLAGS = '%s') -foo.Object(target = 'foo%s', source = 'prog.c') -bar.Object(target = 'bar%s', source = 'prog.c') -foo.Program(target = 'foo', source = 'foo%s') -bar.Program(target = 'bar', source = 'bar%s') -foo.Program(target = 'prog', source = 'prog.c', - CCFLAGS = '$CCFLAGS -DBAR $BAZ', BAZ = '-DBAZ') -""" % (fooflags, barflags, _obj, _obj, _obj, _obj)) +foo_obj = foo.Object(target='foo', source='prog.c') +bar_obj = bar.Object(target='bar', source='prog.c') +foo.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) +foo.Program(target='prog', source='prog.c', CCFLAGS='$CCFLAGS -DBAR $BAZ', BAZ='-DBAZ') +""") test.write('prog.c', r""" #include @@ -76,29 +81,28 @@ } """) - -test.run(arguments = '.') - -test.run(program = test.workpath('foo'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('prog'), stdout = """\ +test.run(arguments='.') +test.run(program=test.workpath('foo'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('prog'), stdout="""\ prog.c: FOO prog.c: BAR prog.c: BAZ """) -test.write('SConstruct', """ -bar = Environment(CCFLAGS = '%s') -bar.Object(target = 'foo%s', source = 'prog.c') -bar.Object(target = 'bar%s', source = 'prog.c') -bar.Program(target = 'foo', source = 'foo%s') -bar.Program(target = 'bar', source = 'bar%s') -""" % (barflags, _obj, _obj, _obj, _obj)) +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(CCFLAGS='{barflags}') -test.run(arguments = '.') +foo_obj = bar.Object(target='foo', source='prog.c') +bar_obj = bar.Object(target='bar', source='prog.c') +bar.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) +""") -test.run(program = test.workpath('foo'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('foo'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") test.pass_test() diff --git a/test/CC/CCVERSION.py b/test/CC/CCVERSION.py index 3ee601fa1d..b12849af0d 100644 --- a/test/CC/CCVERSION.py +++ b/test/CC/CCVERSION.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import sys import TestSCons diff --git a/test/CC/CFLAGS.py b/test/CC/CFLAGS-live.py similarity index 65% rename from test/CC/CFLAGS.py rename to test/CC/CFLAGS-live.py index 590d6b5439..cae0b22eb2 100644 --- a/test/CC/CFLAGS.py +++ b/test/CC/CFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,17 +22,22 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test behavior of CFLAGS. + +This is a live test, uses the detected C compiler. +""" import sys + import TestSCons test = TestSCons.TestSCons() # Make sure CFLAGS is not passed to CXX by just expanding CXXCOM -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment(CFLAGS='-xyz', CCFLAGS='-abc') print(env.subst('$CXXCOM')) print(env.subst('$CXXCOMSTR')) @@ -41,8 +48,6 @@ test.must_not_contain_any_line(test.stdout(), ["-xyz"]) test.must_contain_all_lines(test.stdout(), ["-abc"]) -_obj = TestSCons._obj - # Test passing CFLAGS to C compiler by actually compiling programs if sys.platform == 'win32': import SCons.Tool.MSCommon as msc @@ -57,17 +62,17 @@ fooflags = '-DFOO' barflags = '-DBAR' +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment(CFLAGS="{fooflags}") +bar = Environment(CFLAGS="{barflags}") -test.write('SConstruct', """ -foo = Environment(CFLAGS = '%s') -bar = Environment(CFLAGS = '%s') -foo.Object(target = 'foo%s', source = 'prog.c') -bar.Object(target = 'bar%s', source = 'prog.c') -foo.Program(target = 'foo', source = 'foo%s') -bar.Program(target = 'bar', source = 'bar%s') -foo.Program(target = 'prog', source = 'prog.c', - CFLAGS = '$CFLAGS -DBAR $BAZ', BAZ = '-DBAZ') -""" % (fooflags, barflags, _obj, _obj, _obj, _obj)) +foo_obj = foo.Object(target='foo', source='prog.c') +bar_obj = bar.Object(target='bar', source='prog.c') +foo.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) +foo.Program(target='prog', source='prog.c', CFLAGS='$CFLAGS -DBAR $BAZ', BAZ='-DBAZ') +""") test.write('prog.c', r""" #include @@ -90,30 +95,28 @@ } """) - - -test.run(arguments = '.') - -test.run(program = test.workpath('foo'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('prog'), stdout = """\ +test.run(arguments='.') +test.run(program=test.workpath('foo'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('prog'), stdout="""\ prog.c: FOO prog.c: BAR prog.c: BAZ """) -test.write('SConstruct', """ -bar = Environment(CFLAGS = '%s') -bar.Object(target = 'foo%s', source = 'prog.c') -bar.Object(target = 'bar%s', source = 'prog.c') -bar.Program(target = 'foo', source = 'foo%s') -bar.Program(target = 'bar', source = 'bar%s') -""" % (barflags, _obj, _obj, _obj, _obj)) +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(CFLAGS='{barflags}') -test.run(arguments = '.') +foo_obj = bar.Object(target='foo', source='prog.c') +bar_obj = bar.Object(target='bar', source='prog.c') +bar.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) +""") -test.run(program = test.workpath('foo'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('foo'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") test.pass_test() diff --git a/test/CC/SHCC-live.py b/test/CC/SHCC-live.py new file mode 100644 index 0000000000..9da11d07b7 --- /dev/null +++ b/test/CC/SHCC-live.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test the C shared-object compiler name variable $SHCC. +This is a live test, calling the detected C compiler via a wrapper. +""" + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() + +test.file_fixture('wrapper.py') +test.file_fixture('CC-fixture/foo.c') +test.file_fixture('CC-fixture/bar.c') + +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment() + +shcc = foo['SHCC'] +bar = Environment(SHCC=r'{_python_} wrapper.py ' + shcc) +foo.SharedObject(target='foo/foo', source='foo.c') +bar.SharedObject(target='bar/bar', source='bar.c') +""") + +test.run(arguments='foo') +test.must_not_exist(test.workpath('wrapper.out')) + +test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n", mode='r') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/CC/SHCCCOM.py b/test/CC/SHCCCOM.py index 5326c01ed9..eb05e9cd02 100644 --- a/test/CC/SHCCCOM.py +++ b/test/CC/SHCCCOM.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import os @@ -44,6 +43,7 @@ alt_c_suffix = '.c' test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(SHCCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', SHOBJPREFIX='', SHOBJSUFFIX='.obj') diff --git a/test/CC/SHCCCOMSTR.py b/test/CC/SHCCCOMSTR.py index 75f3aadcf7..c24f9a93eb 100644 --- a/test/CC/SHCCCOMSTR.py +++ b/test/CC/SHCCCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the $SHCCCOMSTR construction variable allows you to customize @@ -45,6 +44,7 @@ alt_c_suffix = '.c' test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(SHCCCOM = r'%(_python_)s mycompile.py cc $TARGET $SOURCE', SHCCCOMSTR = 'Building $TARGET from $SOURCE', SHOBJPREFIX='', diff --git a/test/CC/SHCCFLAGS.py b/test/CC/SHCCFLAGS-live.py similarity index 68% rename from test/CC/SHCCFLAGS.py rename to test/CC/SHCCFLAGS-live.py index 71ed1c0f98..a511fff0f8 100644 --- a/test/CC/SHCCFLAGS.py +++ b/test/CC/SHCCFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,14 +22,18 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Verify that $SHCCFLAGS settings are used to build shared object files. + +This is a live test, uses the detected C compiler. +""" +import os import sys + import TestSCons -import os - + test = TestSCons.TestSCons() e = test.Environment() @@ -39,16 +45,16 @@ if sys.platform.find('irix') > -1: os.environ['LD_LIBRARYN32_PATH'] = '.' -test.write('SConstruct', """ +test.write('SConstruct', f"""\ DefaultEnvironment(tools=[]) -foo = Environment(SHCCFLAGS = '%s', WINDOWS_INSERT_DEF=1) -bar = Environment(SHCCFLAGS = '%s', WINDOWS_INSERT_DEF=1) +foo = Environment(SHCCFLAGS='{fooflags}', WINDOWS_INSERT_DEF=1) +bar = Environment(SHCCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -foo_obj = foo.SharedObject(target = 'foo', source = 'prog.c') -foo.SharedLibrary(target = 'foo', source = foo_obj) +foo_obj = foo.SharedObject(target='foo', source='prog.c') +foo.SharedLibrary(target='foo', source=foo_obj) -bar_obj = bar.SharedObject(target = 'bar', source = 'prog.c') -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar_obj = bar.SharedObject(target='bar', source='prog.c') +bar.SharedLibrary(target='bar', source=bar_obj) fooMain = foo.Clone(LIBS='foo', LIBPATH='.') foomain_obj = fooMain.Object(target='foomain', source='main.c') @@ -57,7 +63,7 @@ barMain = bar.Clone(LIBS='bar', LIBPATH='.') barmain_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=barmain_obj) -""" % (fooflags, barflags)) +""") test.write('foo.def', r""" LIBRARY "foo" @@ -90,7 +96,7 @@ } """) -test.write('main.c', r""" +test.write('main.c', """\ void doIt(); @@ -102,31 +108,30 @@ } """) -test.run(arguments = '.') - -test.run(program = test.workpath('fooprog'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('barprog'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('barprog'), stdout="prog.c: BAR\n") -test.write('SConstruct', """ -bar = Environment(SHCCFLAGS = '%s', WINDOWS_INSERT_DEF=1) +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(SHCCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -foo_obj = bar.SharedObject(target = 'foo', source = 'prog.c') -bar.SharedLibrary(target = 'foo', source = foo_obj) +foo_obj = bar.SharedObject(target='foo', source='prog.c') +bar.SharedLibrary(target='foo', source=foo_obj) -bar_obj = bar.SharedObject(target = 'bar', source = 'prog.c') -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar_obj = bar.SharedObject(target='bar', source='prog.c') +bar.SharedLibrary(target='bar', source=bar_obj) barMain = bar.Clone(LIBS='bar', LIBPATH='.') foomain_obj = barMain.Object(target='foomain', source='main.c') barmain_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=foomain_obj) barMain.Program(target='fooprog', source=barmain_obj) -""" % barflags) - -test.run(arguments = '.') +""") -test.run(program = test.workpath('fooprog'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('barprog'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('barprog'), stdout="prog.c: BAR\n") test.pass_test() diff --git a/test/CC/SHCFLAGS.py b/test/CC/SHCFLAGS-live.py similarity index 68% rename from test/CC/SHCFLAGS.py rename to test/CC/SHCFLAGS-live.py index a691dbaab9..46ff8952fd 100644 --- a/test/CC/SHCFLAGS.py +++ b/test/CC/SHCFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,14 +22,18 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test behavior of SHCFLAGS. + +This is a live test, uses the detected C compiler. +""" +import os import sys + import TestSCons -import os - + test = TestSCons.TestSCons() e = test.Environment() @@ -39,15 +45,16 @@ if sys.platform.find('irix') > -1: os.environ['LD_LIBRARYN32_PATH'] = '.' -test.write('SConstruct', """ -foo = Environment(SHCFLAGS = '%s', WINDOWS_INSERT_DEF=1) -bar = Environment(SHCFLAGS = '%s', WINDOWS_INSERT_DEF=1) +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment(SHCFLAGS='{fooflags}', WINDOWS_INSERT_DEF=1) +bar = Environment(SHCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -foo_obj = foo.SharedObject(target = 'foo', source = 'prog.c') -foo.SharedLibrary(target = 'foo', source = foo_obj) +foo_obj = foo.SharedObject(target='foo', source='prog.c') +foo.SharedLibrary(target='foo', source=foo_obj) -bar_obj = bar.SharedObject(target = 'bar', source = 'prog.c') -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar_obj = bar.SharedObject(target='bar', source='prog.c') +bar.SharedLibrary(target='bar', source=bar_obj) fooMain = foo.Clone(LIBS='foo', LIBPATH='.') foomain_obj = fooMain.Object(target='foomain', source='main.c') @@ -56,7 +63,7 @@ barMain = bar.Clone(LIBS='bar', LIBPATH='.') barmain_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=barmain_obj) -""" % (fooflags, barflags)) +""") test.write('foo.def', r""" LIBRARY "foo" @@ -101,31 +108,30 @@ } """) -test.run(arguments = '.') - -test.run(program = test.workpath('fooprog'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('barprog'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('barprog'), stdout="prog.c: BAR\n") -test.write('SConstruct', """ -bar = Environment(SHCFLAGS = '%s', WINDOWS_INSERT_DEF=1) +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(SHCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -foo_obj = bar.SharedObject(target = 'foo', source = 'prog.c') -bar.SharedLibrary(target = 'foo', source = foo_obj) +foo_obj = bar.SharedObject(target='foo', source='prog.c') +bar.SharedLibrary(target='foo', source=foo_obj) -bar_obj = bar.SharedObject(target = 'bar', source = 'prog.c') -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar_obj = bar.SharedObject(target='bar', source='prog.c') +bar.SharedLibrary(target='bar', source=bar_obj) barMain = bar.Clone(LIBS='bar', LIBPATH='.') foomain_obj = barMain.Object(target='foomain', source='main.c') barmain_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=foomain_obj) barMain.Program(target='fooprog', source=barmain_obj) -""" % barflags) - -test.run(arguments = '.') +""") -test.run(program = test.workpath('fooprog'), stdout = "prog.c: BAR\n") -test.run(program = test.workpath('barprog'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.c: BAR\n") +test.run(program=test.workpath('barprog'), stdout="prog.c: BAR\n") test.pass_test() diff --git a/test/CPPDEFINES/basic.py b/test/CPPDEFINES/basic.py index b66ea8a876..a85e7eb36b 100755 --- a/test/CPPDEFINES/basic.py +++ b/test/CPPDEFINES/basic.py @@ -32,6 +32,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) test_list = [ 'xyz', ['x', 'y', 'z'], @@ -47,7 +48,7 @@ def generator(target, source, env, for_signature): return 'TARGET_AND_SOURCE_ARE_MISSING' for i in test_list: - env = Environment( + env = Environment(tools=['cc'], CPPDEFPREFIX='-D', CPPDEFSUFFIX='', INTEGER=0, @@ -65,7 +66,7 @@ def generator(target, source, env, for_signature): ) for i in test_list: - env = Environment( + env = Environment(tools=['cc'], CPPDEFPREFIX='|', CPPDEFSUFFIX='|', INTEGER=1, diff --git a/test/CPPDEFINES/fixture/SConstruct-Append b/test/CPPDEFINES/fixture/SConstruct-Append index 8c26270f3d..e991c88087 100644 --- a/test/CPPDEFINES/fixture/SConstruct-Append +++ b/test/CPPDEFINES/fixture/SConstruct-Append @@ -8,38 +8,38 @@ DefaultEnvironment(tools=[]) # Special cases: # https://github.com/SCons/scons/issues/1738 -env_1738_2 = Environment(CPPDEFPREFIX='-D') +env_1738_2 = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_1738_2['CPPDEFINES'] = ['FOO'] env_1738_2.Append(CPPDEFINES={'value': '1'}) print(env_1738_2.subst('$_CPPDEFFLAGS')) # env_1738_2.Object('test_1738_2', 'main.c') # https://github.com/SCons/scons/issues/2300 -env_2300_1 = Environment(CPPDEFINES='foo', CPPDEFPREFIX='-D') +env_2300_1 = Environment(tools=['gcc'],CPPDEFINES='foo', CPPDEFPREFIX='-D') env_2300_1.Append(CPPDEFINES='bar') print(env_2300_1.subst('$_CPPDEFFLAGS')) -env_2300_2 = Environment(CPPDEFINES=['foo'], CPPDEFPREFIX='-D') # note the list +env_2300_2 = Environment(tools=['gcc'],CPPDEFINES=['foo'], CPPDEFPREFIX='-D') # note the list env_2300_2.Append(CPPDEFINES='bar') print(env_2300_2.subst('$_CPPDEFFLAGS')) # An initial space-separated string will be split, but not a string in a list. -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo bar" env_multi.Append(CPPDEFINES="baz") print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = ["foo bar"] env_multi.Append(CPPDEFINES="baz") print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo" env_multi.Append(CPPDEFINES=["bar baz"]) print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo" env_multi.Append(CPPDEFINES="bar baz") print(env_multi.subst('$_CPPDEFFLAGS')) @@ -47,7 +47,7 @@ print(env_multi.subst('$_CPPDEFFLAGS')) # Check that AppendUnique(..., delete_existing=True) works as expected. # Each addition is in different but matching form, and different order # so we expect a reordered list, but with the same macro defines. -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D') env_multi.Append(CPPDEFINES=["Macro1=Value1", ("Macro2", "Value2"), {"Macro3": "Value3"}, "Macro4"]) try: env_multi.AppendUnique(CPPDEFINES="Macro2=Value2", delete_existing=True) @@ -60,9 +60,9 @@ else: print(env_multi.subst('$_CPPDEFFLAGS')) # A lone tuple handled differently than a lone list. -env_multi = Environment(CPPDEFPREFIX='-D', CPPDEFINES=("Macro1", "Value1")) +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D', CPPDEFINES=("Macro1", "Value1")) print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D', CPPDEFINES=["Macro1", "Value1"]) +env_multi = Environment(tools=['gcc'],CPPDEFPREFIX='-D', CPPDEFINES=["Macro1", "Value1"]) print(env_multi.subst('$_CPPDEFFLAGS')) # https://github.com/SCons/scons/issues/1152 @@ -114,7 +114,7 @@ for (t1, c1) in cases: orig = f"{c1!r}" if isinstance(c1, str) else c1 app = f"{c2!r}" if isinstance(c2, str) else c2 print(f" orig = {orig}, append = {app}") - env = Environment(CPPDEFINES=c1, CPPDEFPREFIX='-D') + env = Environment(tools=['gcc'],CPPDEFINES=c1, CPPDEFPREFIX='-D') try: env.Append(CPPDEFINES=c2) final = env.subst('$_CPPDEFFLAGS', source="src", target="tgt") @@ -122,7 +122,7 @@ for (t1, c1) in cases: except Exception as t: print(f"Append:\n FAILED: {t}") - env = Environment(CPPDEFINES=c1, CPPDEFPREFIX='-D') + env = Environment(tools=['gcc'],CPPDEFINES=c1, CPPDEFPREFIX='-D') try: env.AppendUnique(CPPDEFINES=c2) final = env.subst('$_CPPDEFFLAGS', source="src", target="tgt") diff --git a/test/CPPDEFINES/fixture/SConstruct-Prepend b/test/CPPDEFINES/fixture/SConstruct-Prepend index 26546f1135..37126c3729 100644 --- a/test/CPPDEFINES/fixture/SConstruct-Prepend +++ b/test/CPPDEFINES/fixture/SConstruct-Prepend @@ -8,38 +8,38 @@ DefaultEnvironment(tools=[]) # Special cases: # https://github.com/SCons/scons/issues/1738 -env_1738_2 = Environment(CPPDEFPREFIX='-D') +env_1738_2 = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_1738_2['CPPDEFINES'] = ['FOO'] env_1738_2.Prepend(CPPDEFINES={'value': '1'}) print(env_1738_2.subst('$_CPPDEFFLAGS')) # env_1738_2.Object('test_1738_2', 'main.c') # https://github.com/SCons/scons/issues/2300 -env_2300_1 = Environment(CPPDEFINES='foo', CPPDEFPREFIX='-D') +env_2300_1 = Environment(tools=['cc'],CPPDEFINES='foo', CPPDEFPREFIX='-D') env_2300_1.Prepend(CPPDEFINES='bar') print(env_2300_1.subst('$_CPPDEFFLAGS')) -env_2300_2 = Environment(CPPDEFINES=['foo'], CPPDEFPREFIX='-D') # note the list +env_2300_2 = Environment(tools=['cc'],CPPDEFINES=['foo'], CPPDEFPREFIX='-D') # note the list env_2300_2.Prepend(CPPDEFINES='bar') print(env_2300_2.subst('$_CPPDEFFLAGS')) # An initial space-separated string will be split, but not a string in a list. -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo bar" env_multi.Prepend(CPPDEFINES="baz") print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = ["foo bar"] env_multi.Prepend(CPPDEFINES="baz") print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo" env_multi.Prepend(CPPDEFINES=["bar baz"]) print(env_multi.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_multi['CPPDEFINES'] = "foo" env_multi.Prepend(CPPDEFINES="bar baz") print(env_multi.subst('$_CPPDEFFLAGS')) @@ -47,7 +47,7 @@ print(env_multi.subst('$_CPPDEFFLAGS')) # Check that PrependUnique(..., delete_existing=True) works as expected. # Each addition is in different but matching form, and different order # so we expect a reordered list, but with the same macro defines. -env_multi = Environment(CPPDEFPREFIX='-D') +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D') env_multi.Prepend(CPPDEFINES=["Macro1=Value1", ("Macro2", "Value2"), {"Macro3": "Value3"}]) try: env_multi.PrependUnique(CPPDEFINES="Macro2=Value2", delete_existing=True) @@ -60,9 +60,9 @@ else: print(env_multi.subst('$_CPPDEFFLAGS')) # A lone tuple handled differently than a lone list. -env_tuple = Environment(CPPDEFPREFIX='-D', CPPDEFINES=("Macro1", "Value1")) +env_tuple = Environment(tools=['cc'],CPPDEFPREFIX='-D', CPPDEFINES=("Macro1", "Value1")) print(env_tuple.subst('$_CPPDEFFLAGS')) -env_multi = Environment(CPPDEFPREFIX='-D', CPPDEFINES=["Macro1", "Value1"]) +env_multi = Environment(tools=['cc'],CPPDEFPREFIX='-D', CPPDEFINES=["Macro1", "Value1"]) print(env_multi.subst('$_CPPDEFFLAGS')) # https://github.com/SCons/scons/issues/1152 @@ -115,7 +115,7 @@ for (t1, c1) in cases: orig = f"{c1!r}" if isinstance(c1, str) else c1 pre = f"{c2!r}" if isinstance(c2, str) else c2 print(f" orig = {orig}, prepend = {pre}") - env = Environment(CPPDEFINES=c1, CPPDEFPREFIX='-D') + env = Environment(tools=['cc'],CPPDEFINES=c1, CPPDEFPREFIX='-D') try: env.Prepend(CPPDEFINES=c2) final = env.subst('$_CPPDEFFLAGS', source="src", target="tgt") @@ -123,7 +123,7 @@ for (t1, c1) in cases: except Exception as t: print(f"Prepend:\n FAILED: {t}") - env = Environment(CPPDEFINES=c1, CPPDEFPREFIX='-D') + env = Environment(tools=['cc'],CPPDEFINES=c1, CPPDEFPREFIX='-D') try: env.PrependUnique(CPPDEFINES=c2) final = env.subst('$_CPPDEFFLAGS', source="src", target="tgt") diff --git a/test/CPPDEFINES/live.py b/test/CPPDEFINES/live.py index 97e0e13237..6735721ace 100644 --- a/test/CPPDEFINES/live.py +++ b/test/CPPDEFINES/live.py @@ -32,6 +32,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) foo = Environment(CPPDEFINES=['FOO', ('VAL', '$VALUE')], VALUE=7) bar = Environment(CPPDEFINES={'BAR': None, 'VAL': 8}) baz = Environment(CPPDEFINES=['BAZ', ('VAL', 9)]) diff --git a/test/CPPDEFINES/undefined.py b/test/CPPDEFINES/undefined.py index 31568ea4bf..749c63c1d4 100644 --- a/test/CPPDEFINES/undefined.py +++ b/test/CPPDEFINES/undefined.py @@ -32,7 +32,8 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=['cc']) print(env.subst('$_CPPDEFFLAGS')) """) diff --git a/test/CPPPATH/Dir.py b/test/CPPPATH/Dir.py index 8b557dcfab..a0b1b32ac4 100644 --- a/test/CPPPATH/Dir.py +++ b/test/CPPPATH/Dir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that CPPPATH values with Dir nodes work correctly. @@ -37,6 +36,7 @@ test.subdir('inc1', 'inc2', 'inc3', ['inc3', 'subdir']) test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH = [Dir('inc1'), '$INC2', '$INC3/subdir'], INC2 = Dir('inc2'), INC3 = Dir('inc3')) diff --git a/test/CPPPATH/absolute-path.py b/test/CPPPATH/absolute-path.py index f414e09ecb..dca01f0336 100644 --- a/test/CPPPATH/absolute-path.py +++ b/test/CPPPATH/absolute-path.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify the ability to #include a file with an absolute path name. (Which diff --git a/test/CPPPATH/expand-object.py b/test/CPPPATH/expand-object.py index 54e1d397a0..eec4ac815b 100644 --- a/test/CPPPATH/expand-object.py +++ b/test/CPPPATH/expand-object.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Make sure that $CPPPATH expands correctly if one of the subsidiary @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) class XXX: def __init__(self, value): self.value = value diff --git a/test/CPPPATH/function-expansion.py b/test/CPPPATH/function-expansion.py index 8ddd23c142..3fb999fa4c 100644 --- a/test/CPPPATH/function-expansion.py +++ b/test/CPPPATH/function-expansion.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that expansion of construction variables whose values are functions @@ -47,7 +46,7 @@ ['inc3', 'subdir']) test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) def my_cpppaths( target, source, env, for_signature ): return [ Dir('list_inc1'), Dir('list_inc2') ] diff --git a/test/CPPPATH/list-expansion.py b/test/CPPPATH/list-expansion.py index 98817b9a9e..5e88320042 100644 --- a/test/CPPPATH/list-expansion.py +++ b/test/CPPPATH/list-expansion.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that expansion of construction variables whose values are @@ -42,6 +41,7 @@ test.subdir('sub1', 'sub2', 'sub3', 'sub4') test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) class _inc_test: def __init__(self, name): self.name = name diff --git a/test/CPPPATH/match-dir.py b/test/CPPPATH/match-dir.py index 6ec30b4b1f..41f071d7cc 100644 --- a/test/CPPPATH/match-dir.py +++ b/test/CPPPATH/match-dir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that we don't blow up if there's a directory name within @@ -45,6 +44,7 @@ ['src', 'inc', 'inc2']) test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) SConscript('src/SConscript', variant_dir = 'build', duplicate = 0) """) diff --git a/test/CPPPATH/nested-lists.py b/test/CPPPATH/nested-lists.py index aa48b920b9..86fb69b107 100644 --- a/test/CPPPATH/nested-lists.py +++ b/test/CPPPATH/nested-lists.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that CPPPATH values consisting of nested lists work correctly. @@ -37,6 +36,7 @@ test.subdir('inc1', 'inc2', 'inc3') test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH = ['inc1', ['inc2', ['inc3']]]) env.Program('prog.c') """) diff --git a/test/CPPPATH/null.py b/test/CPPPATH/null.py index e23f65582e..b8c6841bbe 100644 --- a/test/CPPPATH/null.py +++ b/test/CPPPATH/null.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR """ Verify that neither a null-string CPPPATH nor a @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH = '') env.Library('one', source = 'empty1.c') env = Environment(CPPPATH = [None]) diff --git a/test/CPPPATH/subdir-as-include.py b/test/CPPPATH/subdir-as-include.py index 06a1a58ca0..25ecd23521 100644 --- a/test/CPPPATH/subdir-as-include.py +++ b/test/CPPPATH/subdir-as-include.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR """ This is an obscure test case. When a file without a suffix is included in @@ -42,6 +41,7 @@ test.subdir('inc1', ['inc1', 'iterator']) test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH = [Dir('inc1')]) env.Program('prog.cpp') diff --git a/test/CXX/CCFLAGS.py b/test/CXX/CCFLAGS-live.py similarity index 74% rename from test/CXX/CCFLAGS.py rename to test/CXX/CCFLAGS-live.py index 061df87a3b..fcce9a32d4 100644 --- a/test/CXX/CCFLAGS.py +++ b/test/CXX/CCFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,28 +22,29 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that we can set both $CCFLAGS and $CXXFLAGS and have them both show up on the compilation lines for C++ source files. + +This is a live test, uses the detected C++ compiler. """ import TestSCons test = TestSCons.TestSCons() -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) foo = Environment() -foo.Append(CCFLAGS = '-DFOO', CXXFLAGS = '-DCXX') +foo.Append(CCFLAGS='-DFOO', CXXFLAGS='-DCXX') bar = Environment() -bar.Append(CCFLAGS = '-DBAR', CXXFLAGS = '-DCXX') -foo_obj = foo.Object(target = 'foo', source = 'prog.cpp') -bar_obj = bar.Object(target = 'bar', source = 'prog.cpp') -foo.Program(target = 'foo', source = foo_obj) -bar.Program(target = 'bar', source = bar_obj) +bar.Append(CCFLAGS='-DBAR', CXXFLAGS='-DCXX') + +foo_obj = foo.Object(target='foo', source='prog.cpp') +bar_obj = bar.Object(target='bar', source='prog.cpp') +foo.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) """) test.write('prog.cpp', r""" @@ -62,10 +65,10 @@ } """) -test.run(arguments = '.') +test.run(arguments='.') -test.run(program = test.workpath('foo'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") +test.run(program=test.workpath('foo'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") test.pass_test() diff --git a/test/CC/SHCC.py b/test/CXX/CXX-live.py similarity index 71% rename from test/CC/SHCC.py rename to test/CXX/CXX-live.py index 1c9f68d489..9f72b78e74 100644 --- a/test/CC/SHCC.py +++ b/test/CXX/CXX-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,60 +22,60 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test the C compiler name variable $CC. +This is a live test, calling the detected C compiler via a wrapper. +""" +import sys import TestSCons _python_ = TestSCons._python_ +_exe = TestSCons._exe test = TestSCons.TestSCons() test.file_fixture('wrapper.py') -test.write('SConstruct', """ +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) foo = Environment() -shcc = foo.Dictionary('SHCC') -bar = Environment(SHCC = r'%(_python_)s wrapper.py ' + shcc) -foo.SharedObject(target = 'foo/foo', source = 'foo.c') -bar.SharedObject(target = 'bar/bar', source = 'bar.c') + +cxx = foo.Dictionary('CXX') +bar = Environment(CXX=r'{_python_} wrapper.py ' + cxx) +foo.Program(target='foo', source='foo.cxx') +bar.Program(target='bar', source='bar.cxx') """ % locals()) -test.write('foo.c', r""" +test.write('foo.cxx', r""" #include #include - int main(int argc, char *argv[]) { - argv[argc++] = "--"; - printf("foo.c\n"); + argv[argc++] = (char *)"--"; + printf("foo.cxx\n"); exit (0); } """) -test.write('bar.c', r""" +test.write('bar.cxx', r""" #include #include - int main(int argc, char *argv[]) { - argv[argc++] = "--"; - printf("foo.c\n"); + argv[argc++] = (char *)"--"; + printf("foo.cxx\n"); exit (0); } """) - -test.run(arguments = 'foo') - +test.run(arguments='foo' + _exe) test.must_not_exist(test.workpath('wrapper.out')) -test.run(arguments = 'bar') - +test.run(arguments='bar' + _exe) test.must_match('wrapper.out', "wrapper.py\n", mode='r') test.pass_test() diff --git a/test/CXX/CXX.py b/test/CXX/CXX.py index ad00b55bf0..baf2c3150a 100644 --- a/test/CXX/CXX.py +++ b/test/CXX/CXX.py @@ -23,6 +23,10 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test the C++ compiler name variable $CXX, calling a mocked compiler. +""" + import sys import TestSCons @@ -34,19 +38,21 @@ test.file_fixture('mylink.py') test.dir_fixture('CXX-fixture') -test.write('SConstruct', """ +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) env = Environment( - LINK=r'%(_python_)s mylink.py', + LINK=r'{_python_} mylink.py', LINKFLAGS=[], - CXX=r'%(_python_)s myc++.py', + CXX=r'{_python_} myc++.py', CXXFLAGS=[], ) + env.Program(target='test1', source='test1.cc') env.Program(target='test2', source='test2.cpp') env.Program(target='test3', source='test3.cxx') env.Program(target='test4', source='test4.c++') env.Program(target='test5', source='test5.C++') -""" % locals()) +""") test.write('test1.cc', r"""This is a .cc file. /*c++*/ @@ -73,80 +79,34 @@ #link """) -test.run(arguments = '.', stderr = None) - +test.run(arguments='.', stderr=None) test.must_match('test1' + _exe, "This is a .cc file.\n", mode='r') - test.must_match('test2' + _exe, "This is a .cpp file.\n", mode='r') - test.must_match('test3' + _exe, "This is a .cxx file.\n", mode='r') - test.must_match('test4' + _exe, "This is a .c++ file.\n", mode='r') - test.must_match('test5' + _exe, "This is a .C++ file.\n", mode='r') if TestSCons.case_sensitive_suffixes('.c', '.C'): - test.write('SConstruct', """ + test.write('SConstruct2', f"""\ +DefaultEnvironment(tools=[]) env = Environment( - LINK=r'%(_python_)s mylink.py', + LINK=r'{_python_} mylink.py', LINKFLAGS=[], - CXX=r'%(_python_)s myc++.py', + CXX=r'{_python_} myc++.py', CXXFLAGS=[], ) + env.Program(target='test6', source='test6.C') -""" % locals()) +""") test.write('test6.C', r"""This is a .C file. /*c++*/ #link """) - test.run(arguments = '.', stderr = None) + test.run(arguments=['-f', 'SConstruct2', '.'], stderr=None) test.must_match('test6' + _exe, "This is a .C file.\n", mode='r') -test.file_fixture('wrapper.py') - -test.write('SConstruct', """ -foo = Environment() -cxx = foo.Dictionary('CXX') -bar = Environment(CXX=r'%(_python_)s wrapper.py ' + cxx) -foo.Program(target='foo', source='foo.cxx') -bar.Program(target='bar', source='bar.cxx') -""" % locals()) - -test.write('foo.cxx', r""" -#include -#include -int -main(int argc, char *argv[]) -{ - argv[argc++] = (char *)"--"; - printf("foo.cxx\n"); - exit (0); -} -""") - -test.write('bar.cxx', r""" -#include -#include -int -main(int argc, char *argv[]) -{ - argv[argc++] = (char *)"--"; - printf("foo.cxx\n"); - exit (0); -} -""") - - -test.run(arguments = 'foo' + _exe) - -test.must_not_exist(test.workpath('wrapper.out')) - -test.run(arguments = 'bar' + _exe) - -test.must_match('wrapper.out', "wrapper.py\n", mode='r') - test.pass_test() # Local Variables: diff --git a/test/CXX/CXXFLAGS.py b/test/CXX/CXXFLAGS-live.py similarity index 66% rename from test/CXX/CXXFLAGS.py rename to test/CXX/CXXFLAGS-live.py index 8d72708ee8..e2bf9501f0 100644 --- a/test/CXX/CXXFLAGS.py +++ b/test/CXX/CXXFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,13 +22,12 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that $CXXFLAGS settings are used to build both static and shared object files. + +This is a live test, uses the detected C++ compiler. """ import os @@ -34,26 +35,26 @@ import TestSCons -_obj = TestSCons._obj +test = TestSCons.TestSCons() if os.name == 'posix': os.environ['LD_LIBRARY_PATH'] = '.' if sys.platform.find('irix') > -1: os.environ['LD_LIBRARYN32_PATH'] = '.' -test = TestSCons.TestSCons() - e = test.Environment() -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) foo = Environment(WINDOWS_INSERT_DEF=1) -foo.Append(CXXFLAGS = '-DFOO') +foo.Append(CXXFLAGS='-DFOO') bar = Environment(WINDOWS_INSERT_DEF=1) -bar.Append(CXXFLAGS = '-DBAR') -foo_obj = foo.SharedObject(target = 'fooshared%(_obj)s', source = 'doIt.cpp') -bar_obj = bar.SharedObject(target = 'barshared%(_obj)s', source = 'doIt.cpp') -foo.SharedLibrary(target = 'foo', source = foo_obj) -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar.Append(CXXFLAGS='-DBAR') + +foo_obj = foo.SharedObject(target='fooshared', source='doIt.cpp') +bar_obj = bar.SharedObject(target='barshared', source='doIt.cpp') +foo.SharedLibrary(target='foo', source=foo_obj) +bar.SharedLibrary(target='bar', source=bar_obj) fooMain = foo.Clone(LIBS='foo', LIBPATH='.') foo_obj = fooMain.Object(target='foomain', source='main.c') @@ -63,11 +64,11 @@ bar_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=bar_obj) -foo_obj = foo.Object(target = 'foostatic', source = 'prog.cpp') -bar_obj = bar.Object(target = 'barstatic', source = 'prog.cpp') -foo.Program(target = 'foo', source = foo_obj) -bar.Program(target = 'bar', source = bar_obj) -""" % locals()) +foo_obj = foo.Object(target='foostatic', source='prog.cpp') +bar_obj = bar.Object(target='barstatic', source='prog.cpp') +foo.Program(target='foo', source=foo_obj) +bar.Program(target='bar', source=bar_obj) +""") test.write('foo.def', r""" LIBRARY "foo" @@ -100,8 +101,7 @@ } """) -test.write('main.c', r""" - +test.write('main.c', """\ void doIt(); int @@ -130,36 +130,33 @@ } """) -test.run(arguments = '.') - -test.run(program = test.workpath('fooprog'), stdout = "doIt.cpp: FOO\n") -test.run(program = test.workpath('barprog'), stdout = "doIt.cpp: BAR\n") -test.run(program = test.workpath('foo'), stdout = "prog.c: FOO\n") -test.run(program = test.workpath('bar'), stdout = "prog.c: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="doIt.cpp: FOO\n") +test.run(program=test.workpath('barprog'), stdout="doIt.cpp: BAR\n") +test.run(program=test.workpath('foo'), stdout="prog.c: FOO\n") +test.run(program=test.workpath('bar'), stdout="prog.c: BAR\n") - -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) bar = Environment(WINDOWS_INSERT_DEF=1) -bar.Append(CXXFLAGS = '-DBAR') -foo_obj = bar.SharedObject(target = 'foo%(_obj)s', source = 'doIt.cpp') -bar_obj = bar.SharedObject(target = 'bar%(_obj)s', source = 'doIt.cpp') -bar.SharedLibrary(target = 'foo', source = foo_obj) -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar.Append(CXXFLAGS='-DBAR') + +foo_obj = bar.SharedObject(target='foo', source='doIt.cpp') +bar_obj = bar.SharedObject(target='bar', source='doIt.cpp') +bar.SharedLibrary(target='foo', source=foo_obj) +bar.SharedLibrary(target='bar', source=bar_obj) barMain = bar.Clone(LIBS='bar', LIBPATH='.') foo_obj = barMain.Object(target='foomain', source='main.c') bar_obj = barMain.Object(target='barmain', source='main.c') barMain.Program(target='barprog', source=foo_obj) barMain.Program(target='fooprog', source=bar_obj) -""" % locals()) - -test.run(arguments = '.') - -test.run(program = test.workpath('fooprog'), stdout = "doIt.cpp: BAR\n") -test.run(program = test.workpath('barprog'), stdout = "doIt.cpp: BAR\n") - +""") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="doIt.cpp: BAR\n") +test.run(program=test.workpath('barprog'), stdout="doIt.cpp: BAR\n") test.pass_test() diff --git a/test/CXX/SHCCFLAGS-live.py b/test/CXX/SHCCFLAGS-live.py new file mode 100644 index 0000000000..10b650523b --- /dev/null +++ b/test/CXX/SHCCFLAGS-live.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Verify that $SHCCFLAGS settings are used to build shared object files. + +This is a live test, uses the detected C compiler. +""" + +import os +import sys + +import TestSCons + +test = TestSCons.TestSCons() + +e = test.Environment() +fooflags = e['SHCCFLAGS'] + ' -DFOO' +barflags = e['SHCCFLAGS'] + ' -DBAR' + +if os.name == 'posix': + os.environ['LD_LIBRARY_PATH'] = '.' +if sys.platform.find('irix') > -1: + os.environ['LD_LIBRARYN32_PATH'] = '.' + +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment(SHCCFLAGS='{fooflags}', WINDOWS_INSERT_DEF=1) +bar = Environment(SHCCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) + +foo_obj = foo.SharedObject(target='foo', source='prog.cpp') +foo.SharedLibrary(target='foo', source=foo_obj) + +bar_obj = bar.SharedObject(target='bar', source='prog.cpp') +bar.SharedLibrary(target='bar', source=bar_obj) + +fooMain = foo.Clone(LIBS='foo', LIBPATH='.') +foo_obj = fooMain.Object(target='foomain', source='main.c') +fooMain.Program(target='fooprog', source=foo_obj) + +barMain = bar.Clone(LIBS='bar', LIBPATH='.') +barmain_obj = barMain.Object(target='barmain', source='main.c') +barMain.Program(target='barprog', source=barmain_obj) +""") + +test.write('foo.def', """\ +LIBRARY "foo" +DESCRIPTION "Foo Shared Library" + +EXPORTS + doIt +""") + +test.write('bar.def', """\ +LIBRARY "bar" +DESCRIPTION "Bar Shared Library" + +EXPORTS + doIt +""") + +test.write('prog.cpp', r""" +#include + +extern "C" void +doIt() +{ +#ifdef FOO + printf("prog.cpp: FOO\n"); +#endif +#ifdef BAR + printf("prog.cpp: BAR\n"); +#endif +} +""") + +test.write('main.c', """\ + +void doIt(); + +int +main(int argc, char* argv[]) +{ + doIt(); + return 0; +} +""") + +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.cpp: FOO\n") +test.run(program=test.workpath('barprog'), stdout="prog.cpp: BAR\n") + +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(SHCCFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) + +foo_obj = bar.SharedObject(target='foo', source='prog.cpp') +bar.SharedLibrary(target='foo', source=foo_obj) + +bar_obj = bar.SharedObject(target='bar', source='prog.cpp') +bar.SharedLibrary(target='bar', source=bar_obj) + +barMain = bar.Clone(LIBS='bar', LIBPATH='.') +foomain_obj = barMain.Object(target='foomain', source='main.c') +barmain_obj = barMain.Object(target='barmain', source='main.c') +barMain.Program(target='barprog', source=foomain_obj) +barMain.Program(target='fooprog', source=barmain_obj) +""") + +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.cpp: BAR\n") +test.run(program=test.workpath('barprog'), stdout="prog.cpp: BAR\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/CXX/SHCXX.py b/test/CXX/SHCXX-live.py similarity index 81% rename from test/CXX/SHCXX.py rename to test/CXX/SHCXX-live.py index 0d1ad3669d..b7318d4b17 100644 --- a/test/CXX/SHCXX.py +++ b/test/CXX/SHCXX-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test the C++ shared-object compiler name variable $SHCXX. +This is a live test, calling the detected C++ compiler via a wrapper. +""" import os @@ -34,12 +38,14 @@ test.file_fixture('wrapper.py') -test.write('SConstruct', """ +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) foo = Environment() + shcxx = foo.Dictionary('SHCXX') -bar = Environment(SHCXX = r'%(_python_)s wrapper.py ' + shcxx) -foo.SharedObject(target = 'foo/foo', source = 'foo.cpp') -bar.SharedObject(target = 'bar/bar', source = 'bar.cpp') +bar = Environment(SHCXX=r'{_python_} wrapper.py ' + shcxx) +foo.SharedObject(target='foo/foo', source='foo.cpp') +bar.SharedObject(target='bar/bar', source='bar.cpp') """ % locals()) test.write('foo.cpp', r""" @@ -66,13 +72,10 @@ } """) - -test.run(arguments = 'foo') - +test.run(arguments='foo') test.fail_test(os.path.exists(test.workpath('wrapper.out'))) -test.run(arguments = 'bar') - +test.run(arguments='bar') test.must_match('wrapper.out', "wrapper.py\n", mode='r') # test.fail_test(test.read('wrapper.out') != "wrapper.py\n") diff --git a/test/CXX/SHCXXFLAGS.py b/test/CXX/SHCXXFLAGS-live.py similarity index 56% rename from test/CXX/SHCXXFLAGS.py rename to test/CXX/SHCXXFLAGS-live.py index 343be30f6e..8ffb97ec81 100644 --- a/test/CXX/SHCXXFLAGS.py +++ b/test/CXX/SHCXXFLAGS-live.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,12 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that $SHCXXFLAGS settings are used to build shared object files. + +This is a live test, uses the detected C and C++ compilers. """ import os @@ -33,37 +34,38 @@ import TestSCons -_obj = TestSCons._obj +test = TestSCons.TestSCons() + +e = test.Environment() +fooflags = e['SHCXXFLAGS'] + ' -DFOO' +barflags = e['SHCXXFLAGS'] + ' -DBAR' if os.name == 'posix': os.environ['LD_LIBRARY_PATH'] = '.' if sys.platform.find('irix') > -1: os.environ['LD_LIBRARYN32_PATH'] = '.' -test = TestSCons.TestSCons() +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +foo = Environment(SHCXXFLAGS='{fooflags}', WINDOWS_INSERT_DEF=1) +bar = Environment(SHCXXFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -e = test.Environment() +foo_obj = foo.SharedObject(target='foo', source='prog.cpp') +foo.SharedLibrary(target='foo', source=foo_obj) -test.write('SConstruct', """ -foo = Environment(WINDOWS_INSERT_DEF=1) -foo.Append(SHCXXFLAGS = '-DFOO') -bar = Environment(WINDOWS_INSERT_DEF=1) -bar.Append(SHCXXFLAGS = '-DBAR') -foo_obj = foo.SharedObject(target = 'foo%(_obj)s', source = 'prog.cpp') -bar_obj = bar.SharedObject(target = 'bar%(_obj)s', source = 'prog.cpp') -foo.SharedLibrary(target = 'foo', source = foo_obj) -bar.SharedLibrary(target = 'bar', source = bar_obj) +bar_obj = bar.SharedObject(target='bar', source='prog.cpp') +bar.SharedLibrary(target='bar', source=bar_obj) fooMain = foo.Clone(LIBS='foo', LIBPATH='.') foo_obj = fooMain.Object(target='foomain', source='main.c') fooMain.Program(target='fooprog', source=foo_obj) barMain = bar.Clone(LIBS='bar', LIBPATH='.') -bar_obj = barMain.Object(target='barmain', source='main.c') -barMain.Program(target='barprog', source=bar_obj) -""" % locals()) +barmain_obj = barMain.Object(target='barmain', source='main.c') +barMain.Program(target='barprog', source=barmain_obj) +""") -test.write('foo.def', r""" +test.write('foo.def', """\ LIBRARY "foo" DESCRIPTION "Foo Shared Library" @@ -71,7 +73,7 @@ doIt """) -test.write('bar.def', r""" +test.write('bar.def', """\ LIBRARY "bar" DESCRIPTION "Bar Shared Library" @@ -94,7 +96,7 @@ } """) -test.write('main.c', r""" +test.write('main.c', """\ void doIt(); @@ -106,30 +108,30 @@ } """) -test.run(arguments = '.') +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.cpp: FOO\n") +test.run(program=test.workpath('barprog'), stdout="prog.cpp: BAR\n") -test.run(program = test.workpath('fooprog'), stdout = "prog.cpp: FOO\n") -test.run(program = test.workpath('barprog'), stdout = "prog.cpp: BAR\n") +test.write('SConstruct', f"""\ +DefaultEnvironment(tools=[]) +bar = Environment(SHCXXFLAGS='{barflags}', WINDOWS_INSERT_DEF=1) -test.write('SConstruct', """ -bar = Environment(WINDOWS_INSERT_DEF=1) -bar.Append(SHCXXFLAGS = '-DBAR') -foo_obj = bar.SharedObject(target = 'foo%(_obj)s', source = 'prog.cpp') -bar_obj = bar.SharedObject(target = 'bar%(_obj)s', source = 'prog.cpp') -bar.SharedLibrary(target = 'foo', source = foo_obj) -bar.SharedLibrary(target = 'bar', source = bar_obj) +foo_obj = bar.SharedObject(target='foo', source='prog.cpp') +bar.SharedLibrary(target='foo', source=foo_obj) -barMain = bar.Clone(LIBS='bar', LIBPATH='.') -foo_obj = barMain.Object(target='foomain', source='main.c') -bar_obj = barMain.Object(target='barmain', source='main.c') -barMain.Program(target='barprog', source=foo_obj) -barMain.Program(target='fooprog', source=bar_obj) -""" % locals()) +bar_obj = bar.SharedObject(target='bar', source='prog.cpp') +bar.SharedLibrary(target='bar', source=bar_obj) -test.run(arguments = '.') +barMain = bar.Clone(LIBS='bar', LIBPATH='.') +foomain_obj = barMain.Object(target='foomain', source='main.c') +barmain_obj = barMain.Object(target='barmain', source='main.c') +barMain.Program(target='barprog', source=foomain_obj) +barMain.Program(target='fooprog', source=barmain_obj) +""") -test.run(program = test.workpath('fooprog'), stdout = "prog.cpp: BAR\n") -test.run(program = test.workpath('barprog'), stdout = "prog.cpp: BAR\n") +test.run(arguments='.') +test.run(program=test.workpath('fooprog'), stdout="prog.cpp: BAR\n") +test.run(program=test.workpath('barprog'), stdout="prog.cpp: BAR\n") test.pass_test() diff --git a/test/CacheDir/CacheDir.py b/test/CacheDir/CacheDir.py index 3b3e72bb18..45533b553c 100644 --- a/test/CacheDir/CacheDir.py +++ b/test/CacheDir/CacheDir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test retrieving derived files from a CacheDir. @@ -42,7 +41,7 @@ src_cat_out = test.workpath('src', 'cat.out') src_all = test.workpath('src', 'all') -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """\ DefaultEnvironment(tools=[]) @@ -57,7 +56,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') @@ -85,8 +84,12 @@ def cat(env, source, target): test.must_not_exist(src_ccc_out) test.must_not_exist(src_all) # Even if you do -n, the cache will be configured. -test.fail_test(os.listdir(cache) != ['config']) - +expect = ['CACHEDIR.TAG', 'config'] +found = sorted(os.listdir(cache)) +test.fail_test( + expect != found, + message=f"expected cachedir contents {expect}, found {found}", +) # Verify that a normal build works correctly, and clean up. # This should populate the cache with our derived files. test.run(chdir = 'src', arguments = '.') diff --git a/test/CacheDir/CacheDir_TryCompile.py b/test/CacheDir/CacheDir_TryCompile.py index dbea9be3e8..c9ab12643f 100644 --- a/test/CacheDir/CacheDir_TryCompile.py +++ b/test/CacheDir/CacheDir_TryCompile.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that CacheDir functions with TryCompile. @@ -39,7 +38,7 @@ cache = test.workpath('cache') -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """\ DefaultEnvironment(tools=[]) diff --git a/test/CacheDir/NoCache.py b/test/CacheDir/NoCache.py index 506a85945b..611b555b5c 100644 --- a/test/CacheDir/NoCache.py +++ b/test/CacheDir/NoCache.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that the NoCache environment method works. @@ -32,7 +31,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'alpha', 'beta') +test.subdir('alpha', 'beta') sconstruct = """ DefaultEnvironment(tools=[]) diff --git a/test/CacheDir/SideEffect.py b/test/CacheDir/SideEffect.py index 3242e780f5..88b3b043c0 100644 --- a/test/CacheDir/SideEffect.py +++ b/test/CacheDir/SideEffect.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._"_" """ Test that use of SideEffect() doesn't interfere with CacheDir. @@ -32,7 +31,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'work') +test.subdir('work') cache = test.workpath('cache') @@ -43,12 +42,10 @@ def copy(source, target): f.write(f2.read()) def build(env, source, target): - s = str(source[0]) - t = str(target[0]) - copy(s, t) + copy(source[0], target[0]) if target[0].side_effects: - with open(str(target[0].side_effects[0]), "a") as side_effect: - side_effect.write(s + ' -> ' + t + '\\n') + with open(target[0].side_effects[0], "a") as side_effect: + side_effect.write(str(source[0]) + ' -> ' + str(target[0]) + '\\n') CacheDir(r'%(cache)s') diff --git a/test/CacheDir/VariantDir.py b/test/CacheDir/VariantDir.py index 2c3d73f613..1b9d2bfa64 100644 --- a/test/CacheDir/VariantDir.py +++ b/test/CacheDir/VariantDir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Test retrieving derived files from a CacheDir when a VariantDir is used. @@ -34,19 +33,21 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'src') +test.subdir('src') cache = test.workpath('cache') cat_out = test.workpath('cat.out') test.write(['src', 'SConscript'], """\ +DefaultEnvironment(tools=[]) + def cat(env, source, target): target = str(target[0]) with open('cat.out', 'a') as f: f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') diff --git a/test/CacheDir/debug.py b/test/CacheDir/debug.py index 9b3b633ee8..cf16f98d10 100644 --- a/test/CacheDir/debug.py +++ b/test/CacheDir/debug.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the --cache-debug option to see if it prints the expected messages. @@ -37,7 +36,7 @@ test = TestSCons.TestSCons(match=TestSCons.match_re) -test.subdir('cache', 'src') +test.subdir('src') cache = test.workpath('cache') debug_out = test.workpath('cache-debug.out') @@ -57,7 +56,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') diff --git a/test/CacheDir/environment.py b/test/CacheDir/environment.py index 1024ce0383..26655ce7fc 100644 --- a/test/CacheDir/environment.py +++ b/test/CacheDir/environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that whether or not a target gets retrieved from a CacheDir @@ -43,7 +42,7 @@ src_cat_out = test.workpath('src', 'cat.out') src_all = test.workpath('src', 'all') -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """\ DefaultEnvironment(tools=[]) @@ -58,7 +57,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env_cache = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env_nocache = env_cache.Clone() @@ -88,7 +87,12 @@ def cat(env, source, target): test.must_not_exist(src_ccc_out) test.must_not_exist(src_all) # Even if you do -n, the cache will be configured. -test.fail_test(os.listdir(cache) != ['config']) +expect = ['CACHEDIR.TAG', 'config'] +found = sorted(os.listdir(cache)) +test.fail_test( + expect != found, + message=f"expected cachedir contents {expect}, found {found}", +) # Verify that a normal build works correctly, and clean up. # This should populate the cache with our derived files. diff --git a/test/CacheDir/multi-targets.py b/test/CacheDir/multi-targets.py index fa6e8d13c0..7c606afdf9 100644 --- a/test/CacheDir/multi-targets.py +++ b/test/CacheDir/multi-targets.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that multiple target files get retrieved from a CacheDir correctly. @@ -32,7 +31,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'multiple') +test.subdir('multiple') cache = test.workpath('cache') diff --git a/test/CacheDir/multiple-targets.py b/test/CacheDir/multiple-targets.py index d985ca07f3..dfde453df4 100644 --- a/test/CacheDir/multiple-targets.py +++ b/test/CacheDir/multiple-targets.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that multiple target files get retrieved from a CacheDir correctly. @@ -33,8 +32,6 @@ test = TestSCons.TestSCons() -test.subdir('cache') - test.write('SConstruct', """\ DefaultEnvironment(tools=[]) def touch(env, source, target): diff --git a/test/CacheDir/option--cd.py b/test/CacheDir/option--cd.py index 3ea739c28b..e3915880b4 100644 --- a/test/CacheDir/option--cd.py +++ b/test/CacheDir/option--cd.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the --cache-disable option when retrieving derived files from a @@ -34,7 +33,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """ DefaultEnvironment(tools=[]) @@ -44,7 +43,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') diff --git a/test/CacheDir/option--cf.py b/test/CacheDir/option--cf.py index 2d51e03fec..52d107b7a0 100644 --- a/test/CacheDir/option--cf.py +++ b/test/CacheDir/option--cf.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." """ Test populating a CacheDir with the --cache-force option. @@ -35,7 +34,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """ DefaultEnvironment(tools=[]) @@ -45,7 +44,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') @@ -89,7 +88,6 @@ def cat(env, source, target): # Blow away and recreate the CacheDir, then verify that --cache-force # repopulates the cache with the local built targets. DO NOT CLEAN UP. shutil.rmtree(test.workpath('cache')) -test.subdir('cache') test.run(chdir = 'src', arguments = '--cache-force .') @@ -107,7 +105,6 @@ def cat(env, source, target): # Blow away and recreate the CacheDir, then verify that --cache-populate # repopulates the cache with the local built targets. DO NOT CLEAN UP. shutil.rmtree(test.workpath('cache')) -test.subdir('cache') test.run(chdir = 'src', arguments = '--cache-populate .') diff --git a/test/CacheDir/option--cr.py b/test/CacheDir/option--cr.py index 0395876b39..a1d32d2277 100644 --- a/test/CacheDir/option--cr.py +++ b/test/CacheDir/option--cr.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test the --cache-readonly option when retrieving derived files from a @@ -34,7 +33,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'src') +test.subdir('src') test.write(['src', 'SConstruct'], """ DefaultEnvironment(tools=[]) @@ -44,7 +43,7 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') diff --git a/test/CacheDir/option--cs.py b/test/CacheDir/option--cs.py index c3e5e5b573..4a0bb33ffa 100644 --- a/test/CacheDir/option--cs.py +++ b/test/CacheDir/option--cs.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Test printing build actions when using the --cache-show option and @@ -39,7 +38,7 @@ test = TestSCons.TestSCons() -test.subdir('cache', 'src1', 'src2') +test.subdir('src1', 'src2') test.write(['src1', 'build.py'], r""" import sys @@ -60,11 +59,11 @@ def cat(env, source, target): f.write(target + "\\n") with open(target, "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) DefaultEnvironment(tools=[]) # test speedup -env = Environment(tools=[], +env = Environment(tools=[], BUILDERS={'Internal':Builder(action=cat), 'External':Builder(action=r'%(_python_)s build.py $TARGET $SOURCES')}) env.External('aaa.out', 'aaa.in') diff --git a/test/CacheDir/readonly-cache.py b/test/CacheDir/readonly-cache.py index 4aad0ee387..579909959d 100755 --- a/test/CacheDir/readonly-cache.py +++ b/test/CacheDir/readonly-cache.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Verify accessing cache works even if it's read-only. diff --git a/test/CacheDir/scanner-target.py b/test/CacheDir/scanner-target.py index 44ba199032..4aa36923fe 100644 --- a/test/CacheDir/scanner-target.py +++ b/test/CacheDir/scanner-target.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Test the case (reported by Jeff Petkau, bug #694744) where a target @@ -36,8 +35,6 @@ test = TestSCons.TestSCons() -test.subdir('cache') - test.write('SConstruct', """\ DefaultEnvironment(tools=[]) import SCons diff --git a/test/CacheDir/source-scanner.py b/test/CacheDir/source-scanner.py index 1f6fef0c15..2ff19ce0ba 100644 --- a/test/CacheDir/source-scanner.py +++ b/test/CacheDir/source-scanner.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._"" """ Test retrieving derived files from a CacheDir. @@ -40,7 +39,7 @@ cache = test.workpath('cache') -test.subdir('cache', 'subdir') +test.subdir('subdir') test.write(['subdir', 'SConstruct'], """\ DefaultEnvironment(tools=[]) diff --git a/test/CacheDir/symlink.py b/test/CacheDir/symlink.py index e23c25eba5..36ca4be34f 100644 --- a/test/CacheDir/symlink.py +++ b/test/CacheDir/symlink.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,15 +21,12 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" import os import sys import TestSCons -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - """ Verify that we push and retrieve a built symlink to/from a CacheDir() as an actualy symlink, not by copying the file contents. diff --git a/test/CacheDir/up-to-date-q.py b/test/CacheDir/up-to-date-q.py index f0e3962eb4..5c71ab97ae 100644 --- a/test/CacheDir/up-to-date-q.py +++ b/test/CacheDir/up-to-date-q.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Verify that targets retrieved from CacheDir() are reported as @@ -40,19 +39,19 @@ # 1. Set up two identical C project directories called 'alpha' and # 'beta', which use the same cache # 2. Invoke scons on 'alpha' -# 3. Invoke scons on 'beta', which successfully draws output +# 3. Invoke scons on 'beta', which successfully draws output # files from the cache # 4. Invoke scons again, asserting (with -q) that 'beta' is up to date # # Step 4 failed in 0.96.93. In practice, this problem would lead to -# lots of unecessary fetches from the cache during incremental +# lots of unecessary fetches from the cache during incremental # builds (because they behaved like non-incremental builds). import TestSCons test = TestSCons.TestSCons() -test.subdir('cache', 'alpha', 'beta') +test.subdir('alpha', 'beta') foo_c = """ int main(void){ return 0; } diff --git a/test/CacheDir/value_dependencies.py b/test/CacheDir/value_dependencies.py index 7992bef79a..37c6153a17 100644 --- a/test/CacheDir/value_dependencies.py +++ b/test/CacheDir/value_dependencies.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._" """ Verify that bwuilds with caching work for an action with a Value as a child @@ -38,7 +37,6 @@ test = TestSCons.TestSCons() test.dir_fixture('value_dependencies') -test.subdir('cache') # First build, populates the cache test.run(arguments='.') diff --git a/test/CacheDir/value_dependencies/SConstruct b/test/CacheDir/value_dependencies/SConstruct index 55c22ffca3..3923228bf1 100644 --- a/test/CacheDir/value_dependencies/SConstruct +++ b/test/CacheDir/value_dependencies/SConstruct @@ -7,8 +7,8 @@ import SCons.Node CacheDir('cache') def b(target, source, env): - with open(target[0].abspath, 'w') as f: - pass + with open(target[0].abspath, 'w') as f: + pass def scan(node, env, path): """Have the node depend on a directory, which depends on a Value node.""" @@ -20,7 +20,7 @@ scanner = Scanner(function=scan, node_class=SCons.Node.Node) builder = Builder(action=b, source_scanner=scanner) DefaultEnvironment(tools=[]) -env = Environment() +env = Environment(tools=[]) env.Append(BUILDERS={'B': builder}) # Create a node and a directory that each depend on an instance of diff --git a/test/Chmod.py b/test/Chmod.py index 7af95b4130..c1a83422b3 100644 --- a/test/Chmod.py +++ b/test/Chmod.py @@ -47,10 +47,11 @@ def cat(env, source, target): target = str(target[0]) with open(target, "wb") as f: for src in source: - with open(str(src), "rb") as infp: + with open(src, "rb") as infp: f.write(infp.read()) Cat = Action(cat) +DefaultEnvironment(tools=[]) # test speedup env = Environment() env.Command( 'bar.out', @@ -154,92 +155,92 @@ def cat(env, source, target): """) test.run(options = '-n', arguments = '.', stdout = expect) -s = stat.S_IMODE(os.stat(test.workpath('f1'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f1')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f1-File'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f1-File')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('d2'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d2')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('d2-Dir'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d2-Dir')).st_mode) test.fail_test(s != 0o555) test.must_not_exist('bar.out') -s = stat.S_IMODE(os.stat(test.workpath('f3'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f3')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('d4'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d4')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('f5'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f5')).st_mode) test.fail_test(s != 0o444) test.must_not_exist('f6.out') test.must_not_exist('f7.out') -s = stat.S_IMODE(os.stat(test.workpath('Chmod-f7.in'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('Chmod-f7.in')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f7.out-Chmod'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f7.out-Chmod')).st_mode) test.fail_test(s != 0o444) test.must_not_exist('f8.out') -s = stat.S_IMODE(os.stat(test.workpath('f9'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f9')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f10'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f10')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('d11'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d11')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('d12'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d12')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('f13'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f13')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f14'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f14')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f15'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f15')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('d16'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d16')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('d17'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d17')).st_mode) test.fail_test(s != 0o555) -s = stat.S_IMODE(os.stat(test.workpath('d18'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d18')).st_mode) test.fail_test(s != 0o555) test.run() -s = stat.S_IMODE(os.stat(test.workpath('f1'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f1')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('f1-File'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f1-File')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('d2'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d2')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('d2-Dir'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d2-Dir')).st_mode) test.fail_test(s != 0o777) test.must_match('bar.out', "bar.in\n") -s = stat.S_IMODE(os.stat(test.workpath('f3'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f3')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('d4'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d4')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('f5'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f5')).st_mode) test.fail_test(s != 0o666) test.must_match('f6.out', "f6.in\n") test.must_match('f7.out', "f7.in\n") -s = stat.S_IMODE(os.stat(test.workpath('Chmod-f7.in'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('Chmod-f7.in')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('f7.out-Chmod'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f7.out-Chmod')).st_mode) test.fail_test(s != 0o666) test.must_match('f8.out', "f8.in\n") -s = stat.S_IMODE(os.stat(test.workpath('f9'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f9')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('f10'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f10')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('d11'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d11')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('d12'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d12')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('f13'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f13')).st_mode) test.fail_test(s != 0o444) -s = stat.S_IMODE(os.stat(test.workpath('f14'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f14')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('f15'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('f15')).st_mode) test.fail_test(s != 0o666) -s = stat.S_IMODE(os.stat(test.workpath('d16'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d16')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('d17'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d17')).st_mode) test.fail_test(s != 0o777) -s = stat.S_IMODE(os.stat(test.workpath('d18'))[stat.ST_MODE]) +s = stat.S_IMODE(os.stat(test.workpath('d18')).st_mode) test.fail_test(s != 0o777) test.pass_test() diff --git a/test/Clang/clang_default_environment.py b/test/Clang/clang_default_environment.py index 5ebd839e46..41bb44e15f 100644 --- a/test/Clang/clang_default_environment.py +++ b/test/Clang/clang_default_environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Clang/clang_specific_environment.py b/test/Clang/clang_specific_environment.py index 81991f0cbf..10637e6635 100644 --- a/test/Clang/clang_specific_environment.py +++ b/test/Clang/clang_specific_environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Clang/clang_static_library.py b/test/Clang/clang_static_library.py index 7b3f5df088..c0d86d19c5 100644 --- a/test/Clang/clang_static_library.py +++ b/test/Clang/clang_static_library.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons from TestCmd import IS_WINDOWS diff --git a/test/Clang/clangxx_default_environment.py b/test/Clang/clangxx_default_environment.py index 5e46404f46..ef5929e7dc 100644 --- a/test/Clang/clangxx_default_environment.py +++ b/test/Clang/clangxx_default_environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Clang/clangxx_shared_library.py b/test/Clang/clangxx_shared_library.py index a16be6beaf..8471aa1680 100644 --- a/test/Clang/clangxx_shared_library.py +++ b/test/Clang/clangxx_shared_library.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Clang/clangxx_specific_environment.py b/test/Clang/clangxx_specific_environment.py index 39eaab135f..f26ebf92c8 100644 --- a/test/Clang/clangxx_specific_environment.py +++ b/test/Clang/clangxx_specific_environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Clang/clangxx_static_library.py b/test/Clang/clangxx_static_library.py index 3ced7fce4c..d697f1a9f5 100644 --- a/test/Clang/clangxx_static_library.py +++ b/test/Clang/clangxx_static_library.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons from TestCmd import IS_WINDOWS diff --git a/test/Clean/Option.py b/test/Clean/Option.py index c63dff733e..8616092ccf 100644 --- a/test/Clean/Option.py +++ b/test/Clean/Option.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that {Set,Get}Option('clean') works correctly to control diff --git a/test/Clean/basic.py b/test/Clean/basic.py index fbff9b1836..ee510cbbde 100644 --- a/test/Clean/basic.py +++ b/test/Clean/basic.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test various basic uses of the -c (clean) option. diff --git a/test/Clean/function.py b/test/Clean/function.py index 45c9753664..2476e61143 100644 --- a/test/Clean/function.py +++ b/test/Clean/function.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify use of the Clean() function. diff --git a/test/Clean/mkfifo.py b/test/Clean/mkfifo.py index 01e4d98958..62d19d0465 100644 --- a/test/Clean/mkfifo.py +++ b/test/Clean/mkfifo.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that SCons reports an error when cleaning up a target directory diff --git a/test/Clean/symlinks.py b/test/Clean/symlinks.py index c782860a76..05fb08be53 100644 --- a/test/Clean/symlinks.py +++ b/test/Clean/symlinks.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify correct deletion of broken symlinks. diff --git a/test/Climb/U-Default-dir.py b/test/Climb/U-Default-dir.py index 323ceb1463..f26fe75fcc 100644 --- a/test/Climb/U-Default-dir.py +++ b/test/Climb/U-Default-dir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Make sure that a Default() directory doesn't cause an exception @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) Default('.') """) diff --git a/test/Climb/U-Default-no-target.py b/test/Climb/U-Default-no-target.py index 62f10675df..331863e210 100644 --- a/test/Climb/U-Default-no-target.py +++ b/test/Climb/U-Default-no-target.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Make sure a Default() target that doesn't exist is handled with @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) Default('not_a_target.in') """) diff --git a/test/Climb/U-no-Default.py b/test/Climb/U-no-Default.py index 24ed4bc8af..940414b934 100644 --- a/test/Climb/U-no-Default.py +++ b/test/Climb/U-no-Default.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Make sure no Default() targets in the SConstruct doesn't cause an diff --git a/test/Climb/explicit-parent--D.py b/test/Climb/explicit-parent--D.py index a1c3aeeeab..1d5c7460bd 100644 --- a/test/Climb/explicit-parent--D.py +++ b/test/Climb/explicit-parent--D.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Make sure explicit targets beginning with ../ get built correctly @@ -41,7 +40,7 @@ def cat(env, source, target): target = str(target[0]) with open(target, 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=cat)}) env.Cat('f1.out', 'f1.in') diff --git a/test/Climb/explicit-parent--U.py b/test/Climb/explicit-parent--U.py index 53086328b9..e129b4aedd 100644 --- a/test/Climb/explicit-parent--U.py +++ b/test/Climb/explicit-parent--U.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Make sure explicit targets beginning with ../ get built correctly. @@ -36,13 +35,15 @@ test.subdir('subdir') test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) def cat(env, source, target): target = str(target[0]) with open(target, 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) -env = Environment(BUILDERS={'Cat':Builder(action=cat)}) +env = Environment(tools=[], + BUILDERS={'Cat':Builder(action=cat)}) env.Cat('foo.out', 'foo.in') SConscript('subdir/SConscript', "env") """) diff --git a/test/Climb/explicit-parent-u.py b/test/Climb/explicit-parent-u.py index 72104441f0..337e0b3827 100644 --- a/test/Climb/explicit-parent-u.py +++ b/test/Climb/explicit-parent-u.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the -u option only builds targets at or below @@ -37,13 +36,15 @@ test.subdir('subdir') test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) def cat(env, source, target): target = str(target[0]) with open(target, 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) -env = Environment(BUILDERS={'Cat':Builder(action=cat)}) +env = Environment(tools=[], + BUILDERS={'Cat':Builder(action=cat)}) env.Cat('f1.out', 'f1.in') env.Cat('f2.out', 'f2.in') SConscript('subdir/SConscript', "env") diff --git a/test/Climb/filename--D.py b/test/Climb/filename--D.py index fee72f6a21..38a337b204 100644 --- a/test/Climb/filename--D.py +++ b/test/Climb/filename--D.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify the ability to use the -D option with the -f option to diff --git a/test/Climb/filename--U.py b/test/Climb/filename--U.py index 91a83f471a..f14593374a 100644 --- a/test/Climb/filename--U.py +++ b/test/Climb/filename--U.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify the ability to use the -U option with the -f option to diff --git a/test/Climb/filename-u.py b/test/Climb/filename-u.py index 006e53e79c..4806c16c63 100644 --- a/test/Climb/filename-u.py +++ b/test/Climb/filename-u.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify the ability to use the -u option with the -f option to diff --git a/test/Climb/option--D.py b/test/Climb/option--D.py index 06c5fa87dd..169678aef5 100644 --- a/test/Climb/option--D.py +++ b/test/Climb/option--D.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons diff --git a/test/Climb/option--U.py b/test/Climb/option--U.py index 137bb268cc..8c34462b45 100644 --- a/test/Climb/option--U.py +++ b/test/Climb/option--U.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import sys diff --git a/test/Climb/option-u.py b/test/Climb/option-u.py index 49874f9211..de98040786 100644 --- a/test/Climb/option-u.py +++ b/test/Climb/option-u.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that the -u option only builds targets at or below @@ -46,7 +45,7 @@ def cat(env, source, target): target = str(target[0]) with open(target, 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env = Environment(tools=[]) env.Append(BUILDERS = {'Cat' : Builder(action=cat)}) diff --git a/test/Clone-Variables.py b/test/Clone-Variables.py new file mode 100644 index 0000000000..383caeafbb --- /dev/null +++ b/test/Clone-Variables.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +""" +Verify that Clone() respects the variables kwarg. + +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.write('SConstruct', """\ +vars = Variables() +vars.Add(BoolVariable('MYTEST', 'help', default=False)) + +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=vars, tools=[]) +print(f"MYTEST={env.Dictionary('MYTEST')}") +env.Replace(MYTEST=True) +print(f"MYTEST={env.Dictionary('MYTEST')}") +env1 = env.Clone(variables=vars) +print(f"MYTEST={env1.Dictionary('MYTEST')}") +""") + +expect = """\ +MYTEST=False +MYTEST=True +MYTEST=False +""" + +test.run(arguments = '-q -Q', stdout=expect) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Command.py b/test/Command.py index 5abdf169a6..687c13fffb 100644 --- a/test/Command.py +++ b/test/Command.py @@ -46,7 +46,7 @@ import sys def buildIt(env, target, source): - with open(str(target[0]), 'w') as f, open(str(source[0]), 'r') as infp: + with open(target[0], 'w') as f, open(source[0], 'r') as infp: xyzzy = env.get('XYZZY', '') if xyzzy: f.write(xyzzy + '\\n') diff --git a/test/CompilationDatabase/fixture/SConstruct_variant b/test/CompilationDatabase/fixture/SConstruct_variant index eaf19e809a..c70c5273be 100644 --- a/test/CompilationDatabase/fixture/SConstruct_variant +++ b/test/CompilationDatabase/fixture/SConstruct_variant @@ -46,3 +46,6 @@ env.Program('build/main', 'build/test_main.c') env.VariantDir('build2','src', duplicate=0) env.Program('build2/main', 'build2/test_main.c') +env.VariantDir('build3','src', duplicate=0) +env.InstallAs('build3/test_main_copy.c', 'src/test_main.c') +env.Program('build3/main', 'build3/test_main_copy.c') diff --git a/test/CompilationDatabase/variant_dir.py b/test/CompilationDatabase/variant_dir.py index 21eb78b922..b0441fa9cb 100644 --- a/test/CompilationDatabase/variant_dir.py +++ b/test/CompilationDatabase/variant_dir.py @@ -78,6 +78,12 @@ "directory": "%(workdir)s", "file": "%(src_file)s", "output": "%(output2_file)s" + }, + { + "command": "%(exe)s mygcc.py cc -o %(output3_file)s -c %(variant3_src_file)s", + "directory": "%(workdir)s", + "file": "%(variant3_src_file)s", + "output": "%(output3_file)s" } ] """ % {'exe': sys.executable, @@ -85,7 +91,9 @@ 'src_file': os.path.join('src', 'test_main.c'), 'output_file': os.path.join('build', 'test_main.o'), 'output2_file': os.path.join('build2', 'test_main.o'), - 'variant_src_file': os.path.join('build', 'test_main.c') + 'output3_file': os.path.join('build3', 'test_main_copy.o'), + 'variant_src_file': os.path.join('build', 'test_main.c'), + 'variant3_src_file': os.path.join('build3', 'test_main_copy.c') } if sys.platform == 'win32': @@ -108,6 +116,12 @@ "directory": "%(workdir)s", "file": "%(abs_src_file)s", "output": "%(abs_output2_file)s" + }, + { + "command": "%(exe)s mygcc.py cc -o %(output3_file)s -c %(variant3_src_file)s", + "directory": "%(workdir)s", + "file": "%(abs_variant3_src_file)s", + "output": "%(abs_output3_file)s" } ] """ % {'exe': sys.executable, @@ -116,9 +130,14 @@ 'abs_src_file': os.path.join(test.workdir, 'src', 'test_main.c'), 'abs_output_file': os.path.join(test.workdir, 'build', 'test_main.o'), 'abs_output2_file': os.path.join(test.workdir, 'build2', 'test_main.o'), + 'abs_output3_file': os.path.join(test.workdir, 'build3', 'test_main_copy.o'), 'output_file': os.path.join('build', 'test_main.o'), 'output2_file': os.path.join('build2', 'test_main.o'), - 'variant_src_file': os.path.join('build', 'test_main.c')} + 'output3_file': os.path.join('build3', 'test_main_copy.o'), + 'abs_variant3_src_file': os.path.join(test.workdir, 'build3', 'test_main_copy.c'), + 'variant_src_file': os.path.join('build', 'test_main.c'), + 'variant3_src_file': os.path.join('build3', 'test_main_copy.c') + } if sys.platform == 'win32': example_abs_file = example_abs_file.replace('\\', '\\\\') diff --git a/test/Configure/Action-error.py b/test/Configure/Action-error.py index 90ba1ee22b..5021e1cc49 100644 --- a/test/Configure/Action-error.py +++ b/test/Configure/Action-error.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that calling Configure from an Action results in a readable error. @@ -33,10 +32,12 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) def ConfigureAction(target, source, env): env.Configure() return 0 -env = Environment(BUILDERS = {'MyAction' : +env = Environment(tools=[], + BUILDERS = {'MyAction' : Builder(action=Action(ConfigureAction))}) env.MyAction('target', []) """) diff --git a/test/Configure/Builder-call.py b/test/Configure/Builder-call.py index b4c9d334d8..bbe3366f06 100644 --- a/test/Configure/Builder-call.py +++ b/test/Configure/Builder-call.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that calling normal Builders from an actual Configure @@ -44,7 +43,8 @@ """) test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) def CustomTest(*args): return 0 conf = env.Configure(custom_tests = {'MyTest' : CustomTest}) diff --git a/test/Configure/CONFIGUREDIR.py b/test/Configure/CONFIGUREDIR.py index 4b5a02e0da..4777b823fc 100644 --- a/test/Configure/CONFIGUREDIR.py +++ b/test/Configure/CONFIGUREDIR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Test that the configure context directory can be specified by @@ -34,11 +33,12 @@ test = TestSCons.TestSCons() test.write("SConstruct", """\ +DefaultEnvironment(tools=[]) def CustomTest(context): context.Message('Executing Custom Test ... ') context.Result(1) -env = Environment(CONFIGUREDIR = 'custom_config_dir') +env = Environment(tools=[], CONFIGUREDIR = 'custom_config_dir') conf = Configure(env, custom_tests = {'CustomTest' : CustomTest}) conf.CustomTest(); env = conf.Finish() diff --git a/test/Configure/CONFIGURELOG.py b/test/Configure/CONFIGURELOG.py index 9b6221beeb..7ced290473 100644 --- a/test/Configure/CONFIGURELOG.py +++ b/test/Configure/CONFIGURELOG.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Test that the configure context log file name can be specified by @@ -36,11 +35,12 @@ SConstruct_path = test.workpath('SConstruct') test.write(SConstruct_path, """\ +DefaultEnvironment(tools=[]) def CustomTest(context): context.Message('Executing Custom Test ...') context.Result(1) -env = Environment(CONFIGURELOG = 'custom.logfile') +env = Environment(tools=[], CONFIGURELOG = 'custom.logfile') conf = Configure(env, custom_tests = {'CustomTest' : CustomTest}) conf.CustomTest(); env = conf.Finish() @@ -49,7 +49,7 @@ def CustomTest(context): test.run() expect = """\ -file %(SConstruct_path)s,line 6: +file %(SConstruct_path)s,line 7: \tConfigure(confdir = .sconf_temp) scons: Configure: Executing Custom Test ... scons: Configure: (cached) yes diff --git a/test/Configure/CheckLibWithHeader_extra_libs.py b/test/Configure/CheckLibWithHeader_extra_libs.py new file mode 100644 index 0000000000..b9c920cf0a --- /dev/null +++ b/test/Configure/CheckLibWithHeader_extra_libs.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE + +""" +Verify that a program which depends on library which in turn depends +on another library can be built correctly using CheckLibWithHeader + +This is a "live" test - requires a configured C compiler/toolchain to run. +""" + +from TestSCons import TestSCons, dll_, _dll + +test = TestSCons(match=TestSCons.match_re_dotall) +test.dir_fixture(['fixture', 'checklib_extra']) + +libA = f"libA/{dll_}A{_dll}" +libB = f"libB/{dll_}B{_dll}" + +test.run(arguments='-C libA') +test.must_exist(libA) +test.run(arguments='-C libB') +test.must_exist(libB) + +test.run() + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Configure/SConscript.py b/test/Configure/SConscript.py index 73afef766f..8378f85560 100644 --- a/test/Configure/SConscript.py +++ b/test/Configure/SConscript.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that Configure contexts from multiple subsidiary SConscript @@ -39,7 +38,8 @@ ['dir2', 'sub1', 'sub2']) test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) SConscript(dirs=['dir1', 'dir2'], exports="env") """) diff --git a/test/Configure/Streamer1.py b/test/Configure/Streamer1.py index 318a9361df..c2312be91b 100644 --- a/test/Configure/Streamer1.py +++ b/test/Configure/Streamer1.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Test for BitBucket PR 126: @@ -37,6 +36,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) # SConstruct # # The CheckHello should return 'yes' if everything works fine. Otherwise it @@ -63,7 +63,7 @@ def CheckHello(context): context.Result('failed') return out -env = Environment() +env = Environment(tools=[]) cfg = Configure(env) cfg.AddTest('CheckHello', CheckHello) diff --git a/test/Configure/VariantDir-SConscript.py b/test/Configure/VariantDir-SConscript.py index 5818fc7793..cb50d3afe2 100644 --- a/test/Configure/VariantDir-SConscript.py +++ b/test/Configure/VariantDir-SConscript.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that Configure calls in SConscript files work when used @@ -45,6 +44,7 @@ CF = test.CF # cached build failure test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) opts = Variables() opts.Add('chdir') env = Environment(options=opts) diff --git a/test/Configure/VariantDir.py b/test/Configure/VariantDir.py index 2c54b84400..a883d48d32 100644 --- a/test/Configure/VariantDir.py +++ b/test/Configure/VariantDir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that Configure contexts work with basic use of VariantDir. @@ -42,6 +41,8 @@ CF = test.CF # cached build failure test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) + env = Environment(LOGFILE='build/config.log') import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/VariantDir2.py b/test/Configure/VariantDir2.py index 98f4ccde8b..e640fac3c7 100644 --- a/test/Configure/VariantDir2.py +++ b/test/Configure/VariantDir2.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that Configure contexts work with SConstruct/SConscript structure @@ -34,6 +33,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) SConscript('SConscript', variant_dir='build', src='.') """) diff --git a/test/Configure/basic.py b/test/Configure/basic.py index 253fa2510a..9d555659f5 100644 --- a/test/Configure/basic.py +++ b/test/Configure/basic.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that basic builds work with Configure contexts. @@ -40,6 +39,7 @@ CF = test.CF # cached build failure test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/build-fail.py b/test/Configure/build-fail.py index 218cf1807a..21d4b5e079 100644 --- a/test/Configure/build-fail.py +++ b/test/Configure/build-fail.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that Configure tests work even after an earlier test fails. @@ -51,6 +50,7 @@ b_boost_hpp = os.path.join('..', 'b', 'boost.hpp') test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) import os def _check(context): for dir in ['a', 'b']: diff --git a/test/Configure/cache-not-ok.py b/test/Configure/cache-not-ok.py index 7502f7a25c..04a348658d 100644 --- a/test/Configure/cache-not-ok.py +++ b/test/Configure/cache-not-ok.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that the cache mechanism works when checks are not ok. @@ -43,6 +42,7 @@ CF = test.CF # cached build failure test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) if not int(ARGUMENTS.get('target_signatures_content', 0)): Decider('timestamp-newer') env = Environment() diff --git a/test/Configure/cache-ok.py b/test/Configure/cache-ok.py index 93d1308fb9..a2ef8fa216 100644 --- a/test/Configure/cache-ok.py +++ b/test/Configure/cache-ok.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that the cache mechanism works when checks are ok. @@ -43,6 +42,7 @@ CF = test.CF # cached build failure test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) if not int(ARGUMENTS.get('target_signatures_content', 0)): Decider('timestamp-newer') env = Environment() diff --git a/test/Configure/clean.py b/test/Configure/clean.py index d7a5dc7b5f..19e4879a2a 100644 --- a/test/Configure/clean.py +++ b/test/Configure/clean.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that we don't perform Configure context actions when the @@ -34,6 +33,7 @@ test = TestSCons.TestSCons(match = TestSCons.match_re_dotall) test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/config-h.py b/test/Configure/config-h.py index aca18e42ac..221e3bd209 100644 --- a/test/Configure/config-h.py +++ b/test/Configure/config-h.py @@ -37,6 +37,7 @@ LIB = "LIB" + lib.upper() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/custom-tests.py b/test/Configure/custom-tests.py index 47b344aff0..9a6d0f61ab 100644 --- a/test/Configure/custom-tests.py +++ b/test/Configure/custom-tests.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,13 +21,12 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify execution of custom test cases. """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -54,6 +55,7 @@ """) test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) def CheckCustom(test): test.Message( 'Executing MyTest ... ' ) retCompileOK = test.TryCompile( '%(compileOK)s', '.c' ) @@ -162,7 +164,7 @@ def CheckEmptyDict(test): conf.CheckDict() conf.CheckEmptyDict() env = conf.Finish() -""" % locals()) +""") test.run() diff --git a/test/Configure/fixture/checklib_extra/SConstruct b/test/Configure/fixture/checklib_extra/SConstruct new file mode 100644 index 0000000000..82bf5aae45 --- /dev/null +++ b/test/Configure/fixture/checklib_extra/SConstruct @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT +# +# Copyright The SCons Foundation +DefaultEnvironment(tools=[]) + +env = Environment( + CPPPATH=['#'], + LIBPATH=['libB', 'libA'], + LIBS=['A', 'B'], + RPATH=['libA', 'libB'], +) + +conf = Configure(env) +if not conf.CheckLibWithHeader( + ['B'], + header="libB/libB.h", + language='C', + extra_libs=['A'], + call='libB();', + autoadd=False, +): + print("Cannot build against 'B' library, exiting.") + Exit(1) +env = conf.Finish() + +# TODO: we should be able to build and run a test program now, +# to make sure Configure() didn't lie to us about usability. +# Disabled for now, because that's trickier in Windows (the rpath +# only works for Linux) +# env.Program(target="testlibs", source="src/test.c") + diff --git a/test/Configure/fixture/checklib_extra/conftest.skip b/test/Configure/fixture/checklib_extra/conftest.skip new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/Configure/fixture/checklib_extra/libA/SConstruct b/test/Configure/fixture/checklib_extra/libA/SConstruct new file mode 100644 index 0000000000..e75e1e1caf --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libA/SConstruct @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MIT +# +# Copyright The SCons Foundation + +SharedLibrary(target='A', source=['libA.c'], CPPDEFINES='BUILDINGSHAREDLIB') + diff --git a/test/Configure/fixture/checklib_extra/libA/libA.c b/test/Configure/fixture/checklib_extra/libA/libA.c new file mode 100644 index 0000000000..01f727c01f --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libA/libA.c @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +// +// Copyright The SCons Foundation + +#include +#include "libA.h" + +LIBA_DECL void libA(void) { + printf("libA\\n"); +} diff --git a/test/Configure/fixture/checklib_extra/libA/libA.h b/test/Configure/fixture/checklib_extra/libA/libA.h new file mode 100644 index 0000000000..9c531a38fe --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libA/libA.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// +// Copyright The SCons Foundation + +#ifndef _LIBA_H +#define _LIBA_H + +// define BUILDINGSHAREDLIB when building libA as shared lib +#ifdef _MSC_VER +# ifdef BUILDINGSHAREDLIB +# define LIBA_DECL __declspec(dllexport) +# else +# define LIBA_DECL __declspec(dllimport) +# endif +#endif // WIN32 + +#ifndef LIBA_DECL +# define LIBA_DECL +#endif + +LIBA_DECL void libA(void); +#endif // _LIBA_H diff --git a/test/Configure/fixture/checklib_extra/libB/SConstruct b/test/Configure/fixture/checklib_extra/libB/SConstruct new file mode 100644 index 0000000000..4e9cfe0448 --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libB/SConstruct @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: MIT +# +# Copyright The SCons Foundation + +SharedLibrary( + target='B', + source=['libB.c'], + LIBS=['A'], + LIBPATH='../libA', + CPPPATH='../libA', + CPPDEFINES='BUILDINGSHAREDLIB', +) diff --git a/test/Configure/fixture/checklib_extra/libB/libB.c b/test/Configure/fixture/checklib_extra/libB/libB.c new file mode 100644 index 0000000000..c861b14cf9 --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libB/libB.c @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// +// Copyright The SCons Foundation + +#include +#include "libA.h" +#include "libB.h" + +LIBB_DECL void libB (void) { + printf("libB\\n"); + libA(); +} diff --git a/test/Configure/fixture/checklib_extra/libB/libB.h b/test/Configure/fixture/checklib_extra/libB/libB.h new file mode 100644 index 0000000000..9872bb31f4 --- /dev/null +++ b/test/Configure/fixture/checklib_extra/libB/libB.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// +// Copyright The SCons Foundation + +#ifndef _LIBB_H +#define _LIBB_H + +// define BUILDINGSHAREDLIB when building libB as shared lib +#ifdef _MSC_VER +# ifdef BUILDINGSHAREDLIB +# define LIBB_DECL __declspec(dllexport) +# else +# define LIBB_DECL __declspec(dllimport) +# endif +#endif // WIN32 + +#ifndef LIBB_DECL +# define LIBB_DECL +#endif + +LIBB_DECL void libB(void); +#endif // _LIBB_H diff --git a/test/Configure/fixture/checklib_extra/src/test.c b/test/Configure/fixture/checklib_extra/src/test.c new file mode 100644 index 0000000000..dedf40ad97 --- /dev/null +++ b/test/Configure/fixture/checklib_extra/src/test.c @@ -0,0 +1,6 @@ +#include "libB/libB.h" + +int main() +{ + libB(); +} diff --git a/test/Configure/from-SConscripts.py b/test/Configure/from-SConscripts.py index 59eba535b1..17d975b0e7 100644 --- a/test/Configure/from-SConscripts.py +++ b/test/Configure/from-SConscripts.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Make sure we can call Configure() from subsidiary SConscript calls. @@ -37,6 +36,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = SConscript('x.scons') """) @@ -48,7 +48,7 @@ """) test.write('y.scons', """\ -env = Environment() +env = Environment(tools=[]) Return('env') """) diff --git a/test/Configure/help.py b/test/Configure/help.py index 32f74dac47..b7184eea93 100644 --- a/test/Configure/help.py +++ b/test/Configure/help.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that we don't perform Configure context actions when the @@ -34,6 +33,7 @@ test = TestSCons.TestSCons(match = TestSCons.match_re_dotall) test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/implicit-cache.py b/test/Configure/implicit-cache.py index 2cf74d17ea..f5fdd4b42b 100644 --- a/test/Configure/implicit-cache.py +++ b/test/Configure/implicit-cache.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,7 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that use of --implicit-cache with the Python Value Nodes @@ -52,7 +54,6 @@ get longer and longer until it blew out the users's memory. """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSConsign from SCons.Util import get_hash_format, get_current_hash_algorithm_used @@ -60,6 +61,7 @@ test = TestSConsign.TestSConsign() test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH=['.']) conf = Configure(env) conf.CheckHeader( 'math.h' ) diff --git a/test/Configure/option--Q.py b/test/Configure/option--Q.py index 198e94fd49..8316ab0268 100644 --- a/test/Configure/option--Q.py +++ b/test/Configure/option--Q.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify that the -Q option suppresses Configure context output. @@ -33,6 +32,7 @@ test = TestSCons.TestSCons() test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment() import os env.AppendENVPath('PATH', os.environ['PATH']) diff --git a/test/Configure/option--config.py b/test/Configure/option--config.py index 3fd0691df7..4c3d7abc47 100644 --- a/test/Configure/option--config.py +++ b/test/Configure/option--config.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -19,10 +21,7 @@ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE """ Verify use of the --config= option. @@ -51,6 +50,7 @@ SConstruct_path = test.workpath('SConstruct') test.write(SConstruct_path, """ +DefaultEnvironment(tools=[]) env = Environment(CPPPATH='#/include') import os env.AppendENVPath('PATH', os.environ['PATH']) @@ -78,7 +78,7 @@ conftest_0_c = conftest_0_base%'.c' conftest_1_base = os.path.join(".sconf_temp", "conftest_%s_0%%s"%conftest_1_c_hash) -SConstruct_file_line = test.python_file_line(SConstruct_path, 6)[:-1] +SConstruct_file_line = test.python_file_line(SConstruct_path, 7)[:-1] expect = """ scons: *** "%(conftest_0_c)s" is not yet built and cache is forced. @@ -191,6 +191,7 @@ # Check the combination of --config=force and Decider('MD5-timestamp') SConstruct_path = test.workpath('SConstruct') test.write(SConstruct_path, """ +DefaultEnvironment(tools=[]) env = Environment() env.Decider('MD5-timestamp') conf = Configure(env) diff --git a/test/Copy-Action.py b/test/Copy-Action.py index 1b1356e30e..c591011ad4 100644 --- a/test/Copy-Action.py +++ b/test/Copy-Action.py @@ -46,10 +46,9 @@ Execute(Copy('d7.out', Glob('f?.in'))) def cat(env, source, target): - target = str(target[0]) - with open(target, "w") as f: + with open(target[0], "w") as f: for src in source: - with open(str(src), "r") as ifp: + with open(src, "r") as ifp: f.write(ifp.read()) Cat = Action(cat) diff --git a/test/Decider/MD5-timestamp-Repository.py b/test/Decider/MD5-timestamp-Repository.py index 201bdfe496..35e24f4979 100644 --- a/test/Decider/MD5-timestamp-Repository.py +++ b/test/Decider/MD5-timestamp-Repository.py @@ -58,7 +58,7 @@ test.sleep() # delay for timestamps test.write(['Repository','content1.in'], "content1.in 2\n") test.touch(['Repository','content2.in']) -time_content = os.stat(os.path.join(repository,'content3.in'))[stat.ST_MTIME] +time_content = os.stat(os.path.join(repository,'content3.in')).st_mtime test.write(['Repository','content3.in'], "content3.in 2\n") test.touch(['Repository','content3.in'], time_content) diff --git a/test/Decider/MD5-timestamp.py b/test/Decider/MD5-timestamp.py index 3815639672..b8988402b0 100644 --- a/test/Decider/MD5-timestamp.py +++ b/test/Decider/MD5-timestamp.py @@ -54,7 +54,7 @@ test.write('content1.in', "content1.in 2\n") test.touch('content2.in') -time_content = os.stat('content3.in')[stat.ST_MTIME] +time_content = os.stat('content3.in').st_mtime test.write('content3.in', "content3.in 2\n") test.touch('content3.in', time_content) diff --git a/test/Decider/switch-rebuild.py b/test/Decider/switch-rebuild.py index 29264553cb..4cd09d6f5e 100644 --- a/test/Decider/switch-rebuild.py +++ b/test/Decider/switch-rebuild.py @@ -38,7 +38,7 @@ Decider('%s') def build(env, target, source): - with open(str(target[0]), 'wt') as f, open(str(source[0]), 'rt') as ifp: + with open(target[0], 'wt') as f, open(source[0], 'rt') as ifp: f.write(ifp.read()) B = Builder(action=build) env = Environment(tools=[], BUILDERS = { 'B' : B }) diff --git a/test/Decider/timestamp.py b/test/Decider/timestamp.py index d713a621dd..1fcb9e70d2 100644 --- a/test/Decider/timestamp.py +++ b/test/Decider/timestamp.py @@ -55,8 +55,8 @@ test.run(arguments = '.') test.up_to_date(arguments = '.') -time_match = os.stat('match2.out')[stat.ST_MTIME] -time_newer = os.stat('newer2.out')[stat.ST_MTIME] +time_match = os.stat('match2.out').st_mtime +time_newer = os.stat('newer2.out').st_mtime # Now make all the source files newer than (different timestamps from) # the last time the targets were built, and touch the target files diff --git a/test/Delete.py b/test/Delete.py index fc4ab4fd3c..fff8946ddd 100644 --- a/test/Delete.py +++ b/test/Delete.py @@ -43,10 +43,9 @@ Execute(Delete('symlinks/dirlink')) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], "wb") as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: ofp.write(ifp.read()) Cat = Action(cat) env = Environment() @@ -193,10 +192,9 @@ def cat(env, source, target): test.write("SConstruct", """\ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ifp: + with open(target[0], "wb") as ifp: for src in source: - with open(str(src), "rb") as ofp: + with open(src, "rb") as ofp: ofp.write(ifp.read()) Cat = Action(cat) env = Environment() diff --git a/test/Dir/source.py b/test/Dir/source.py index a0ba987956..21516bcdbd 100644 --- a/test/Dir/source.py +++ b/test/Dir/source.py @@ -44,7 +44,7 @@ DefaultEnvironment(tools=[]) def writeTarget(target, source, env): - f = open(str(target[0]), 'w') + f = open(target[0], 'w') f.write("stuff\\n") f.close() return 0 diff --git a/test/Docbook/basedir/htmlchunked/htmlchunked_cmd.py b/test/Docbook/basedir/htmlchunked/htmlchunked_cmd.py index e6032327d3..f540ffbb66 100644 --- a/test/Docbook/basedir/htmlchunked/htmlchunked_cmd.py +++ b/test/Docbook/basedir/htmlchunked/htmlchunked_cmd.py @@ -40,11 +40,11 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('output/index.html')) # Cleanup -test.run(arguments=['-f','SConstruct.cmd','-c']) +test.run(arguments=['-f','SConstruct.cmd','-c','DOCBOOK_XSLTPROC=%s'%xsltproc]) test.must_not_exist(test.workpath('output/index.html')) test.pass_test() diff --git a/test/Docbook/basedir/htmlchunked/image/SConstruct.cmd b/test/Docbook/basedir/htmlchunked/image/SConstruct.cmd index 40dc569ce4..d4e0cfccab 100644 --- a/test/Docbook/basedir/htmlchunked/image/SConstruct.cmd +++ b/test/Docbook/basedir/htmlchunked/image/SConstruct.cmd @@ -4,4 +4,8 @@ DefaultEnvironment(tools=[]) env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) +DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") +if DOCBOOK_XSLTPROC: + env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookHtmlChunked('manual', xsl='html.xsl', base_dir='output/') diff --git a/test/Docbook/basedir/htmlhelp/htmlhelp_cmd.py b/test/Docbook/basedir/htmlhelp/htmlhelp_cmd.py index ebefb92335..9d6a4b1614 100644 --- a/test/Docbook/basedir/htmlhelp/htmlhelp_cmd.py +++ b/test/Docbook/basedir/htmlhelp/htmlhelp_cmd.py @@ -40,13 +40,13 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('output/index.html')) test.must_not_be_empty(test.workpath('htmlhelp.hhp')) test.must_not_be_empty(test.workpath('toc.hhc')) # Cleanup -test.run(arguments=['-f','SConstruct.cmd','-c']) +test.run(arguments=['-f','SConstruct.cmd','-c','DOCBOOK_XSLTPROC=%s'%xsltproc]) test.must_not_exist(test.workpath('output/index.html')) test.must_not_exist(test.workpath('htmlhelp.hhp')) test.must_not_exist(test.workpath('toc.hhc')) diff --git a/test/Docbook/basedir/htmlhelp/image/SConstruct.cmd b/test/Docbook/basedir/htmlhelp/image/SConstruct.cmd index f76e99ba82..c41e3c2ec8 100644 --- a/test/Docbook/basedir/htmlhelp/image/SConstruct.cmd +++ b/test/Docbook/basedir/htmlhelp/image/SConstruct.cmd @@ -4,5 +4,9 @@ DefaultEnvironment(tools=[]) env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) +DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") +if DOCBOOK_XSLTPROC: + env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookHtmlhelp('manual', xsl='htmlhelp.xsl', base_dir='output/') diff --git a/test/Docbook/basedir/slideshtml/image/SConstruct.cmd b/test/Docbook/basedir/slideshtml/image/SConstruct.cmd index 151e6039c7..5ed5d40e1d 100644 --- a/test/Docbook/basedir/slideshtml/image/SConstruct.cmd +++ b/test/Docbook/basedir/slideshtml/image/SConstruct.cmd @@ -13,6 +13,9 @@ if v >= (1, 78, 0): DefaultEnvironment(tools=[]) env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) +DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") +if DOCBOOK_XSLTPROC: + env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookSlidesHtml('virt'+ns_ext, xsl='slides.xsl', base_dir='output/') diff --git a/test/Docbook/basedir/slideshtml/slideshtml_cmd.py b/test/Docbook/basedir/slideshtml/slideshtml_cmd.py index b0f1f2512c..b395c05eba 100644 --- a/test/Docbook/basedir/slideshtml/slideshtml_cmd.py +++ b/test/Docbook/basedir/slideshtml/slideshtml_cmd.py @@ -40,12 +40,12 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('output/index.html')) test.must_contain(test.workpath('output/index.html'), 'sfForming') # Cleanup -test.run(arguments=['-f','SConstruct.cmd','-c'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','-c','DOCBOOK_XSLTPROC=%s'%xsltproc]) test.must_not_exist(test.workpath('output/index.html')) test.pass_test() diff --git a/test/Docbook/basic/epub/image/SConstruct.cmd b/test/Docbook/basic/epub/image/SConstruct.cmd index b86c78d224..0e39fea2fc 100644 --- a/test/Docbook/basic/epub/image/SConstruct.cmd +++ b/test/Docbook/basic/epub/image/SConstruct.cmd @@ -7,5 +7,5 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookEpub('manual') diff --git a/test/Docbook/basic/html/html_cmd.py b/test/Docbook/basic/html/html_cmd.py index 122d8bcf79..7e9a0a3ce5 100644 --- a/test/Docbook/basic/html/html_cmd.py +++ b/test/Docbook/basic/html/html_cmd.py @@ -38,7 +38,7 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc]) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('manual.html')) # Cleanup diff --git a/test/Docbook/basic/html/image/SConstruct.cmd b/test/Docbook/basic/html/image/SConstruct.cmd index ef4ecebb90..d4a6939f12 100644 --- a/test/Docbook/basic/html/image/SConstruct.cmd +++ b/test/Docbook/basic/html/image/SConstruct.cmd @@ -7,6 +7,6 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookHtml('manual') diff --git a/test/Docbook/basic/htmlchunked/image/SConstruct.cmd b/test/Docbook/basic/htmlchunked/image/SConstruct.cmd index 765864a006..83d6f3d012 100644 --- a/test/Docbook/basic/htmlchunked/image/SConstruct.cmd +++ b/test/Docbook/basic/htmlchunked/image/SConstruct.cmd @@ -7,6 +7,6 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookHtmlChunked('manual') diff --git a/test/Docbook/basic/htmlhelp/image/SConstruct.cmd b/test/Docbook/basic/htmlhelp/image/SConstruct.cmd index 854a266c3c..cf6a0af005 100644 --- a/test/Docbook/basic/htmlhelp/image/SConstruct.cmd +++ b/test/Docbook/basic/htmlhelp/image/SConstruct.cmd @@ -7,6 +7,6 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookHtmlhelp('manual') diff --git a/test/Docbook/basic/man/image/SConstruct.cmd b/test/Docbook/basic/man/image/SConstruct.cmd index 6474f49faf..a64d27415c 100644 --- a/test/Docbook/basic/man/image/SConstruct.cmd +++ b/test/Docbook/basic/man/image/SConstruct.cmd @@ -7,6 +7,6 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookMan('refdb') diff --git a/test/Docbook/basic/pdf/image/SConstruct.cmd b/test/Docbook/basic/pdf/image/SConstruct.cmd index 5d709051ff..46ca35bd5e 100644 --- a/test/Docbook/basic/pdf/image/SConstruct.cmd +++ b/test/Docbook/basic/pdf/image/SConstruct.cmd @@ -7,6 +7,6 @@ env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") if DOCBOOK_XSLTPROC: env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC - +env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookPdf('manual') diff --git a/test/Docbook/basic/slideshtml/image/SConstruct.cmd b/test/Docbook/basic/slideshtml/image/SConstruct.cmd index 2090bf01f7..7ef7a2b85f 100644 --- a/test/Docbook/basic/slideshtml/image/SConstruct.cmd +++ b/test/Docbook/basic/slideshtml/image/SConstruct.cmd @@ -13,6 +13,9 @@ if v >= (1, 78, 0): DefaultEnvironment(tools=[]) env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) +DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") +if DOCBOOK_XSLTPROC: + env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookSlidesHtml( 'virt' + ns_ext, diff --git a/test/Docbook/basic/slideshtml/slideshtml_cmd.py b/test/Docbook/basic/slideshtml/slideshtml_cmd.py index 9c8097b64a..ff2fde359d 100644 --- a/test/Docbook/basic/slideshtml/slideshtml_cmd.py +++ b/test/Docbook/basic/slideshtml/slideshtml_cmd.py @@ -40,12 +40,12 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('index.html')) test.must_contain(test.workpath('index.html'), 'sfForming') # Cleanup -test.run(arguments=['-f','SConstruct.cmd','-c'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','-c','DOCBOOK_XSLTPROC=%s'%xsltproc]) test.must_not_exist(test.workpath('index.html')) test.pass_test() diff --git a/test/Docbook/basic/slidespdf/image/SConstruct.cmd b/test/Docbook/basic/slidespdf/image/SConstruct.cmd index 18ef25bf4e..eec556d7a9 100644 --- a/test/Docbook/basic/slidespdf/image/SConstruct.cmd +++ b/test/Docbook/basic/slidespdf/image/SConstruct.cmd @@ -4,6 +4,9 @@ DefaultEnvironment(tools=[]) env = Environment(DOCBOOK_PREFER_XSLTPROC=1, tools=['docbook']) +DOCBOOK_XSLTPROC = ARGUMENTS.get('DOCBOOK_XSLTPROC', "") +if DOCBOOK_XSLTPROC: + env['DOCBOOK_XSLTPROC'] = DOCBOOK_XSLTPROC env.Append(DOCBOOK_XSLTPROCFLAGS=['--novalid', '--nonet']) env.DocbookSlidesPdf('virt') diff --git a/test/Docbook/basic/slidespdf/slidespdf_cmd.py b/test/Docbook/basic/slidespdf/slidespdf_cmd.py index 4b7bd2c265..65e2801d4f 100644 --- a/test/Docbook/basic/slidespdf/slidespdf_cmd.py +++ b/test/Docbook/basic/slidespdf/slidespdf_cmd.py @@ -44,12 +44,12 @@ test.dir_fixture('image') # Normal invocation -test.run(arguments=['-f','SConstruct.cmd'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','DOCBOOK_XSLTPROC=%s'%xsltproc], stderr=None) test.must_not_be_empty(test.workpath('virt.fo')) test.must_not_be_empty(test.workpath('virt.pdf')) # Cleanup -test.run(arguments=['-f','SConstruct.cmd','-c'], stderr=None) +test.run(arguments=['-f','SConstruct.cmd','-c','DOCBOOK_XSLTPROC=%s'%xsltproc]) test.must_not_exist(test.workpath('virt.fo')) test.must_not_exist(test.workpath('virt.pdf')) diff --git a/test/EnsurePythonVersion.py b/test/EnsurePythonVersion.py index b40bc711f1..073ed31eab 100644 --- a/test/EnsurePythonVersion.py +++ b/test/EnsurePythonVersion.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -30,21 +29,21 @@ test.write('SConstruct', """\ -EnsurePythonVersion(0,0) +DefaultEnvironment(tools=[]) +EnsurePythonVersion(0, 0) Exit(0) """) test.run() test.write('SConstruct', """\ -EnsurePythonVersion(2000,0) +DefaultEnvironment(tools=[]) +EnsurePythonVersion(2000, 0) Exit(0) """) test.run(status=2) - - test.pass_test() # Local Variables: diff --git a/test/EnsureSConsVersion.py b/test/EnsureSConsVersion.py index efdc17f767..5ac8a285c9 100644 --- a/test/EnsureSConsVersion.py +++ b/test/EnsureSConsVersion.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -31,129 +30,135 @@ import SCons if SCons.__version__ != "__VERSION__": - test.write('SConstruct', """\ -env = Environment() -env.EnsureSConsVersion(0,0) +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) +env.EnsureSConsVersion(0, 0) Exit(0) """) - test.run() test.write('SConstruct', """\ -env = Environment() -env.EnsureSConsVersion(1,0) +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) +env.EnsureSConsVersion(1, 0) Exit(0) """) - test.run() test.write('SConstruct', """\ -env = Environment() -env.EnsureSConsVersion(5,0) +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) +env.EnsureSConsVersion(5, 0) Exit(0) """) - test.run(status=2) test.write('SConstruct', """\ -EnsureSConsVersion(2000,0) +DefaultEnvironment(tools=[]) +EnsureSConsVersion(2000, 0) Exit(0) """) - test.run(status=2) test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env.EnsureSConsVersion(*env.GetSConsVersion()) Exit(0) """) - test.run() test.write('SConstruct', """\ -env = Environment() +DefaultEnvironment(tools=[]) +env = Environment(tools=[]) ver = env.GetSConsVersion() env.EnsureSConsVersion(ver[0], ver[1], ver[2]) Exit(0) """) - test.run() - test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,33) +EnsureSConsVersion(0, 33) """) - test.run() test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,33,0) +EnsureSConsVersion(0, 33, 0) """) - test.run() test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,33,1) +EnsureSConsVersion(0, 33, 1) """) - test.run() test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,33,2) +EnsureSConsVersion(0, 33, 2) """) - test.run() test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,33,3) +EnsureSConsVersion(0, 33, 3) """) - test.run(status=2) test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(0,34) +EnsureSConsVersion(0, 34) """) - test.run(status=2) test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) SCons.__version__ = '0.33.2' -EnsureSConsVersion(1,0) +EnsureSConsVersion(1, 0) """) - test.run(status=2) test.write('SConstruct', """\ import SCons + +DefaultEnvironment(tools=[]) EnsureSConsVersion(*GetSConsVersion()) """) - test.run() test.write('SConstruct', """\ import SCons -ver = GetSConsVersion() -EnsureSConsVersion(ver[0], ver[1], ver[2]) -""") +DefaultEnvironment(tools=[]) +major, minor, patch = GetSConsVersion() +EnsureSConsVersion(major, minor, patch) +""") test.run() - test.pass_test() # Local Variables: diff --git a/test/Errors/Exception.py b/test/Errors/Exception.py index cdd6a3c5ec..9c5f1681a2 100644 --- a/test/Errors/Exception.py +++ b/test/Errors/Exception.py @@ -30,7 +30,7 @@ test.write('SConstruct', """\ def foo(env, target, source): print(str(target[0])) - with open(str(target[0]), 'wt') as f: + with open(target[0], 'wt') as f: f.write('foo') def exit(env, target, source): diff --git a/test/Errors/preparation.py b/test/Errors/preparation.py index 914827a713..51ca2de9b2 100644 --- a/test/Errors/preparation.py +++ b/test/Errors/preparation.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ A currently disabled test that used to verify that we print a useful @@ -49,6 +48,7 @@ test.subdir('install', 'work') test.write(['work', 'SConstruct'], """\ +_ = DefaultEnvironment(tools=[]) file_out = Command('file.out', 'file.in', Copy('$TARGET', '$SOURCE')) Alias("install", file_out) @@ -56,7 +56,7 @@ # IOError or OSError when we try to open it to read its signature. import os os.mkdir('file.in') -""" % locals()) +""") if sys.platform == 'win32': error_message = "Permission denied" diff --git a/test/Exit.py b/test/Exit.py index abaa382c0a..3aa690e44f 100644 --- a/test/Exit.py +++ b/test/Exit.py @@ -103,10 +103,9 @@ test.write(['subdir', 'SConscript'], """\ def exit_builder(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) Exit(27) env = Environment(BUILDERS = {'my_exit' : Builder(action=exit_builder)}) @@ -132,10 +131,9 @@ def exit_scanner(node, env, target): exitscan = Scanner(function = exit_scanner, skeys = ['.k']) def cat(env, source, target): - target = str(target[0]) - with open(target, 'wb') as ofp: + with open(target[0], 'wb') as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: outf.write(ifp.read()) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) diff --git a/test/FindFile.py b/test/FindFile.py index fa195c4d04..53612ae091 100644 --- a/test/FindFile.py +++ b/test/FindFile.py @@ -40,16 +40,16 @@ test.write('SConstruct', """ env = Environment(FILE = 'file', BAR = 'bar') file1 = FindFile('testfile1', [ 'foo', '.', 'bar', 'bar/baz' ]) -with open(str(file1), 'r') as f: +with open(file1, 'r') as f: print(f.read()) file2 = env.FindFile('test${FILE}1', [ 'bar', 'foo', '.', 'bar/baz' ]) -with open(str(file2), 'r') as f: +with open(file2, 'r') as f: print(f.read()) file3 = FindFile('testfile2', [ 'foo', '.', 'bar', 'bar/baz' ]) -with open(str(file3), 'r') as f: +with open(file3, 'r') as f: print(f.read()) file4 = env.FindFile('testfile2', [ '$BAR/baz', 'foo', '.', 'bar' ]) -with open(str(file4), 'r') as f: +with open(file4, 'r') as f: print(f.read()) """) diff --git a/test/Flatten.py b/test/Flatten.py index 40bbb3e08c..7f3acec513 100644 --- a/test/Flatten.py +++ b/test/Flatten.py @@ -36,10 +36,9 @@ test.write(['work', 'SConstruct'], """ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], "wb") as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: ofp.write(ifp.read()) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) f1 = env.Cat('../file1.out', 'file1.in') diff --git a/test/Fortran/F03FLAGS-live.py b/test/Fortran/F03FLAGS-live.py new file mode 100644 index 0000000000..e96e673131 --- /dev/null +++ b/test/Fortran/F03FLAGS-live.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable, using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f03' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f03 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +foo = Environment(F03='%(fc)s') +f03 = foo.Dictionary('F03') +bar = foo.Clone(F03=r'%(_python_)s wrapper.py ' + f03, F03FLAGS='-Ix') +foo.Program(target='foo', source='foo.f03') +bar.Program(target='bar', source='bar.f03') +""" % locals()) + +test.write('foo.f03', r""" + PROGRAM FOO + PRINT *,'foo.f03' + STOP + END +""") + +test.write('bar.f03', r""" + PROGRAM BAR + PRINT *,'bar.f03' + STOP + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f03\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f03\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/F03FLAGS.py b/test/Fortran/F03FLAGS.py index 63d1fcb59c..f527d4efad 100644 --- a/test/Fortran/F03FLAGS.py +++ b/test/Fortran/F03FLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test handling of the dialect-specific FLAGS variable, +using a mocked compiler. +""" import TestSCons @@ -31,40 +35,45 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', - LINKFLAGS = [], - F03 = r'%(_python_)s myfortran_flags.py g03', - F03FLAGS = '-x', - FORTRAN = r'%(_python_)s myfortran_flags.py fortran', - FORTRANFLAGS = '-y') -env.Program(target = 'test01', source = 'test01.f') -env.Program(target = 'test02', source = 'test02.F') -env.Program(target = 'test03', source = 'test03.for') -env.Program(target = 'test04', source = 'test04.FOR') -env.Program(target = 'test05', source = 'test05.ftn') -env.Program(target = 'test06', source = 'test06.FTN') -env.Program(target = 'test07', source = 'test07.fpp') -env.Program(target = 'test08', source = 'test08.FPP') -env.Program(target = 'test13', source = 'test13.f03') -env.Program(target = 'test14', source = 'test14.F03') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + LINK=r"%(_python_)s mylink.py", + LINKFLAGS=[], + F03=r"%(_python_)s myfortran_flags.py g03", + F03FLAGS="-x", + FORTRAN=r"%(_python_)s myfortran_flags.py fortran", + FORTRANFLAGS="-y", +) +env.Program(target="test01", source="test01.f") +env.Program(target="test02", source="test02.F") +env.Program(target="test03", source="test03.for") +env.Program(target="test04", source="test04.FOR") +env.Program(target="test05", source="test05.ftn") +env.Program(target="test06", source="test06.FTN") +env.Program(target="test07", source="test07.fpp") +env.Program(target="test08", source="test08.FPP") +env.Program(target="test09", source="test09.f03") +env.Program(target="test10", source="test10.F03") """ % locals()) -test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") test.write('test03.for', "This is a .for file.\n#link\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#link\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#link\n#fortran\n") test.write('test06.FTN', "This is a .FTN file.\n#link\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#link\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#link\n#fortran\n") -test.write('test13.f03', "This is a .f03 file.\n#link\n#g03\n") -test.write('test14.F03', "This is a .F03 file.\n#link\n#g03\n") +test.write('test09.f03', "This is a .f03 file.\n#link\n#g03\n") +test.write('test10.F03', "This is a .F03 file.\n#link\n#g03\n") -test.run(arguments = '.', stderr = None) +test.run(arguments='.', stderr=None) test.must_match('test01' + _exe, " -c -y\nThis is a .f file.\n") test.must_match('test02' + _exe, " -c -y\nThis is a .F file.\n") @@ -74,56 +83,8 @@ test.must_match('test06' + _exe, " -c -y\nThis is a .FTN file.\n") test.must_match('test07' + _exe, " -c -y\nThis is a .fpp file.\n") test.must_match('test08' + _exe, " -c -y\nThis is a .FPP file.\n") -test.must_match('test13' + _exe, " -c -x\nThis is a .f03 file.\n") -test.must_match('test14' + _exe, " -c -x\nThis is a .F03 file.\n") - - -fc = 'f03' -g03 = test.detect_tool(fc) - - -if g03: - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F03 = '%(fc)s') -f03 = foo.Dictionary('F03') -bar = foo.Clone(F03 = r'%(_python_)s wrapper.py ' + f03, F03FLAGS = '-Ix') -foo.Program(target = 'foo', source = 'foo.f03') -bar.Program(target = 'bar', source = 'bar.f03') -""" % locals()) - - test.write('foo.f03', r""" - PROGRAM FOO - PRINT *,'foo.f03' - STOP - END -""") - - test.write('bar.f03', r""" - PROGRAM BAR - PRINT *,'bar.f03' - STOP - END -""") - - - test.run(arguments = 'foo' + _exe, stderr = None) - - test.run(program = test.workpath('foo'), stdout = " foo.f03\n") - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar' + _exe, stderr = None) - else: - test.run(arguments = 'bar' + _exe) - - test.run(program = test.workpath('bar'), stdout = " bar.f03\n") - - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match('test09' + _exe, " -c -x\nThis is a .f03 file.\n") +test.must_match('test10' + _exe, " -c -x\nThis is a .F03 file.\n") test.pass_test() diff --git a/test/Fortran/F08FLAGS-live.py b/test/Fortran/F08FLAGS-live.py new file mode 100644 index 0000000000..20b9dc4f3a --- /dev/null +++ b/test/Fortran/F08FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable, using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f08' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f08 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +foo = Environment(F08='%(fc)s') +f08 = foo.Dictionary('F08') +bar = foo.Clone(F08=r'%(_python_)s wrapper.py ' + f08, F08FLAGS='-Ix') +foo.Program(target='foo', source='foo.f08') +bar.Program(target='bar', source='bar.f08') +""" % locals()) + +test.write('foo.f08', r""" + PROGRAM FOO + PRINT *,'foo.f08' + END PROGRAM FOO +""") + +test.write('bar.f08', r""" + PROGRAM BAR + PRINT *,'bar.f08' + END PROGRAM BAR +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f08\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f08\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/F08FLAGS.py b/test/Fortran/F08FLAGS.py index 81ae44174c..a42e163fe9 100644 --- a/test/Fortran/F08FLAGS.py +++ b/test/Fortran/F08FLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test handling of the dialect-specific FLAGS variable, +using a mocked compiler. +""" import TestSCons @@ -31,30 +35,35 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', - LINKFLAGS = [], - F08 = r'%(_python_)s myfortran_flags.py g08', - F08FLAGS = '-x', - FORTRAN = r'%(_python_)s myfortran_flags.py fortran', - FORTRANFLAGS = '-y') -env.Program(target = 'test01', source = 'test01.f') -env.Program(target = 'test02', source = 'test02.F') -env.Program(target = 'test03', source = 'test03.for') -env.Program(target = 'test04', source = 'test04.FOR') -env.Program(target = 'test05', source = 'test05.ftn') -env.Program(target = 'test06', source = 'test06.FTN') -env.Program(target = 'test07', source = 'test07.fpp') -env.Program(target = 'test08', source = 'test08.FPP') -env.Program(target = 'test09', source = 'test09.f08') -env.Program(target = 'test10', source = 'test10.F08') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + LINK=r'%(_python_)s mylink.py', + LINKFLAGS=[], + F08=r'%(_python_)s myfortran_flags.py g08', + F08FLAGS='-x', + FORTRAN=r'%(_python_)s myfortran_flags.py fortran', + FORTRANFLAGS='-y', +) +env.Program(target='test01', source='test01.f') +env.Program(target='test02', source='test02.F') +env.Program(target='test03', source='test03.for') +env.Program(target='test04', source='test04.FOR') +env.Program(target='test05', source='test05.ftn') +env.Program(target='test06', source='test06.FTN') +env.Program(target='test07', source='test07.fpp') +env.Program(target='test08', source='test08.FPP') +env.Program(target='test09', source='test09.f08') +env.Program(target='test10', source='test10.F08') """ % locals()) -test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") test.write('test03.for', "This is a .for file.\n#link\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#link\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#link\n#fortran\n") @@ -64,7 +73,7 @@ test.write('test09.f08', "This is a .f08 file.\n#link\n#g08\n") test.write('test10.F08', "This is a .F08 file.\n#link\n#g08\n") -test.run(arguments = '.', stderr = None) +test.run(arguments='.', stderr=None) test.must_match('test01' + _exe, " -c -y\nThis is a .f file.\n") test.must_match('test02' + _exe, " -c -y\nThis is a .F file.\n") @@ -77,52 +86,6 @@ test.must_match('test09' + _exe, " -c -x\nThis is a .f08 file.\n") test.must_match('test10' + _exe, " -c -x\nThis is a .F08 file.\n") - -fc = 'f08' -g08 = test.detect_tool(fc) - - -if g08: - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F08 = '%(fc)s') -f08 = foo.Dictionary('F08') -bar = foo.Clone(F08 = r'%(_python_)s wrapper.py ' + f08, F08FLAGS = '-Ix') -foo.Program(target = 'foo', source = 'foo.f08') -bar.Program(target = 'bar', source = 'bar.f08') -""" % locals()) - - test.write('foo.f08', r""" - PROGRAM FOO - PRINT *,'foo.f08' - ENDPROGRAM FOO -""") - - test.write('bar.f08', r""" - PROGRAM BAR - PRINT *,'bar.f08' - ENDPROGRAM FOO -""") - - - test.run(arguments = 'foo' + _exe, stderr = None) - - test.run(program = test.workpath('foo'), stdout = " foo.f08\n") - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar' + _exe, stderr = None) - else: - test.run(arguments = 'bar' + _exe) - - test.run(program = test.workpath('bar'), stdout = " bar.f08\n") - - test.must_match('wrapper.out', "wrapper.py\n") - test.pass_test() # Local Variables: diff --git a/test/Fortran/F77FLAGS-live.py b/test/Fortran/F77FLAGS-live.py new file mode 100644 index 0000000000..14cd8c6bf9 --- /dev/null +++ b/test/Fortran/F77FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable, using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f77' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f77 tool; skipping test.\n') + +directory = 'x' +test.subdir(directory) +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +foo = Environment(F77='%(fc)s', tools=['default', 'f77'], F77FILESUFFIXES=[".f"]) +f77 = foo.Dictionary('F77') +bar = foo.Clone(F77=r'%(_python_)s wrapper.py ' + f77, F77FLAGS='-I%(directory)s') +foo.Program(target='foo', source='foo.f') +bar.Program(target='bar', source='bar.f') +""" % locals()) + +test.write('foo.f', r""" + PROGRAM FOO + PRINT *,'foo.f' + STOP + END +""") + +test.write('bar.f', r""" + PROGRAM BAR + PRINT *,'bar.f' + STOP + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/F77FLAGS.py b/test/Fortran/F77FLAGS.py index 01bf33b66c..c3c49d9b45 100644 --- a/test/Fortran/F77FLAGS.py +++ b/test/Fortran/F77FLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test handling of the dialect-specific FLAGS variable, +using a mocked compiler. +""" import TestSCons @@ -31,76 +35,29 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', - LINKFLAGS = [], - F77 = r'%(_python_)s myfortran_flags.py g77', - F77FLAGS = '-x') -env.Program(target = 'test09', source = 'test09.f77') -env.Program(target = 'test10', source = 'test10.F77') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + LINK=r'%(_python_)s mylink.py', + LINKFLAGS=[], + F77=r'%(_python_)s myfortran_flags.py g77', + F77FLAGS='-x', +) +env.Program(target='test09', source='test09.f77') +env.Program(target='test10', source='test10.F77') """ % locals()) test.write('test09.f77', "This is a .f77 file.\n#link\n#g77\n") test.write('test10.F77', "This is a .F77 file.\n#link\n#g77\n") - -test.run(arguments = '.', stderr = None) - +test.run(arguments='.', stderr=None) test.must_match('test09' + _exe, " -c -x\nThis is a .f77 file.\n") test.must_match('test10' + _exe, " -c -x\nThis is a .F77 file.\n") - -fc = 'f77' -g77 = test.detect_tool(fc) - -if g77: - - directory = 'x' - test.subdir(directory) - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F77 = '%(fc)s', tools = ['default', 'f77'], F77FILESUFFIXES = [".f"]) -f77 = foo.Dictionary('F77') -bar = foo.Clone(F77 = r'%(_python_)s wrapper.py ' + f77, F77FLAGS = '-I%(directory)s') -foo.Program(target = 'foo', source = 'foo.f') -bar.Program(target = 'bar', source = 'bar.f') -""" % locals()) - - test.write('foo.f', r""" - PROGRAM FOO - PRINT *,'foo.f' - STOP - END -""") - - test.write('bar.f', r""" - PROGRAM BAR - PRINT *,'bar.f' - STOP - END -""") - - - test.run(arguments = 'foo' + _exe, stderr = None) - - test.run(program = test.workpath('foo'), stdout = " foo.f\n") - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar' + _exe, stderr = None) - else: - test.run(arguments = 'bar' + _exe) - - test.run(program = test.workpath('bar'), stdout = " bar.f\n") - - test.must_match('wrapper.out', "wrapper.py\n") - test.pass_test() # Local Variables: diff --git a/test/Fortran/F90FLAGS-live.py b/test/Fortran/F90FLAGS-live.py new file mode 100644 index 0000000000..f5491bca88 --- /dev/null +++ b/test/Fortran/F90FLAGS-live.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable, using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f90' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f90 tool; skipping test.\n') + +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +foo = Environment(F90='%(fc)s') +f90 = foo.Dictionary('F90') +bar = foo.Clone(F90=r'%(_python_)s wrapper.py ' + f90) +foo.Program(target='foo', source='foo.f90') +bar.Program(target='bar', source='bar.f90') +""" % locals()) + +test.write('foo.f90', r""" + PROGRAM FOO + PRINT *,'foo.f90' + END +""") + +test.write('bar.f90', r""" + PROGRAM BAR + PRINT *,'bar.f90' + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f90\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f90\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/F90FLAGS.py b/test/Fortran/F90FLAGS.py index 10a2f35a97..5ae1828d6f 100644 --- a/test/Fortran/F90FLAGS.py +++ b/test/Fortran/F90FLAGS.py @@ -23,6 +23,11 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test handling of the dialect-specific FLAGS variable, +using a mocked compiler. +""" + import TestSCons _python_ = TestSCons._python_ @@ -30,40 +35,45 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', - LINKFLAGS = [], - F90 = r'%(_python_)s myfortran_flags.py g90', - F90FLAGS = '-x', - FORTRAN = r'%(_python_)s myfortran_flags.py fortran', - FORTRANFLAGS = '-y') -env.Program(target = 'test01', source = 'test01.f') -env.Program(target = 'test02', source = 'test02.F') -env.Program(target = 'test03', source = 'test03.for') -env.Program(target = 'test04', source = 'test04.FOR') -env.Program(target = 'test05', source = 'test05.ftn') -env.Program(target = 'test06', source = 'test06.FTN') -env.Program(target = 'test07', source = 'test07.fpp') -env.Program(target = 'test08', source = 'test08.FPP') -env.Program(target = 'test11', source = 'test11.f90') -env.Program(target = 'test12', source = 'test12.F90') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + LINK=r'%(_python_)s mylink.py', + LINKFLAGS=[], + F90=r'%(_python_)s myfortran_flags.py g90', + F90FLAGS='-x', + FORTRAN=r'%(_python_)s myfortran_flags.py fortran', + FORTRANFLAGS='-y', +) +env.Program(target='test01', source='test01.f') +env.Program(target='test02', source='test02.F') +env.Program(target='test03', source='test03.for') +env.Program(target='test04', source='test04.FOR') +env.Program(target='test05', source='test05.ftn') +env.Program(target='test06', source='test06.FTN') +env.Program(target='test07', source='test07.fpp') +env.Program(target='test08', source='test08.FPP') +env.Program(target='test09', source='test09.f90') +env.Program(target='test10', source='test10.F90') """ % locals()) -test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") test.write('test03.for', "This is a .for file.\n#link\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#link\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#link\n#fortran\n") test.write('test06.FTN', "This is a .FTN file.\n#link\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#link\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#link\n#fortran\n") -test.write('test11.f90', "This is a .f90 file.\n#link\n#g90\n") -test.write('test12.F90', "This is a .F90 file.\n#link\n#g90\n") +test.write('test09.f90', "This is a .f90 file.\n#link\n#g90\n") +test.write('test10.F90', "This is a .F90 file.\n#link\n#g90\n") -test.run(arguments = '.', stderr = None) +test.run(arguments='.', stderr=None) test.must_match('test01' + _exe, " -c -y\nThis is a .f file.\n") test.must_match('test02' + _exe, " -c -y\nThis is a .F file.\n") @@ -73,53 +83,8 @@ test.must_match('test06' + _exe, " -c -y\nThis is a .FTN file.\n") test.must_match('test07' + _exe, " -c -y\nThis is a .fpp file.\n") test.must_match('test08' + _exe, " -c -y\nThis is a .FPP file.\n") -test.must_match('test11' + _exe, " -c -x\nThis is a .f90 file.\n") -test.must_match('test12' + _exe, " -c -x\nThis is a .F90 file.\n") - - - -fc = 'f90' -g90 = test.detect_tool(fc) - -if g90: - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F90 = '%(fc)s') -f90 = foo.Dictionary('F90') -bar = foo.Clone(F90 = r'%(_python_)s wrapper.py ' + f90) -foo.Program(target = 'foo', source = 'foo.f90') -bar.Program(target = 'bar', source = 'bar.f90') -""" % locals()) - - test.write('foo.f90', r""" - PROGRAM FOO - PRINT *,'foo.f90' - END -""") - - test.write('bar.f90', r""" - PROGRAM BAR - PRINT *,'bar.f90' - END -""") - - test.run(arguments = 'foo' + _exe, stderr = None) - - test.run(program = test.workpath('foo'), stdout = " foo.f90\n") - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar' + _exe, stderr = None) - else: - test.run(arguments = 'bar' + _exe) - - test.run(program = test.workpath('bar'), stdout = " bar.f90\n") - - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match('test09' + _exe, " -c -x\nThis is a .f90 file.\n") +test.must_match('test10' + _exe, " -c -x\nThis is a .F90 file.\n") test.pass_test() diff --git a/test/Fortran/F95FLAGS-live.py b/test/Fortran/F95FLAGS-live.py new file mode 100644 index 0000000000..5277df93a5 --- /dev/null +++ b/test/Fortran/F95FLAGS-live.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable, using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f95' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f95 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +foo = Environment(F95='%(fc)s') +f95 = foo.Dictionary('F95') +bar = foo.Clone(F95=r'%(_python_)s wrapper.py ' + f95, F95FLAGS='-Ix') +foo.Program(target='foo', source='foo.f95') +bar.Program(target='bar', source='bar.f95') +""" % locals()) + +test.write('foo.f95', r""" + PROGRAM FOO + PRINT *,'foo.f95' + STOP + END +""") + +test.write('bar.f95', r""" + PROGRAM BAR + PRINT *,'bar.f95' + STOP + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f95\n") +test.must_not_exist('wrapper.out') + +if sys.platform.startswith('sunos'): + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f95\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/F95FLAGS.py b/test/Fortran/F95FLAGS.py index e706264197..6e61052d01 100644 --- a/test/Fortran/F95FLAGS.py +++ b/test/Fortran/F95FLAGS.py @@ -23,6 +23,11 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test handling of the dialect-specific FLAGS variable, +using a mocked compiler. +""" + import TestSCons _python_ = TestSCons._python_ @@ -35,7 +40,8 @@ # ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment( LINK=r'%(_python_)s mylink.py', LINKFLAGS=[], @@ -52,8 +58,8 @@ env.Program(target='test06', source='test06.FTN') env.Program(target='test07', source='test07.fpp') env.Program(target='test08', source='test08.FPP') -env.Program(target='test13', source='test13.f95') -env.Program(target='test14', source='test14.F95') +env.Program(target='test09', source='test09.f95') +env.Program(target='test10', source='test10.F95') """ % locals()) test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") @@ -64,8 +70,8 @@ test.write('test06.FTN', "This is a .FTN file.\n#link\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#link\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#link\n#fortran\n") -test.write('test13.f95', "This is a .f95 file.\n#link\n#g95\n") -test.write('test14.F95', "This is a .F95 file.\n#link\n#g95\n") +test.write('test09.f95', "This is a .f95 file.\n#link\n#g95\n") +test.write('test10.F95', "This is a .F95 file.\n#link\n#g95\n") test.run(arguments = '.', stderr = None) @@ -77,57 +83,8 @@ test.must_match('test06' + _exe, " -c -y\nThis is a .FTN file.\n") test.must_match('test07' + _exe, " -c -y\nThis is a .fpp file.\n") test.must_match('test08' + _exe, " -c -y\nThis is a .FPP file.\n") -test.must_match('test13' + _exe, " -c -x\nThis is a .f95 file.\n") -test.must_match('test14' + _exe, " -c -x\nThis is a .F95 file.\n") - - -fc = 'f95' -g95 = test.detect_tool(fc) -if g95: - test.subdir('x') - test.write(['x','dummy.i'], -""" -# Exists only such that -Ix finds the directory... -""") - - # ref: test/fixture/wrapper.py - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F95='%(fc)s') -f95 = foo.Dictionary('F95') -bar = foo.Clone(F95=r'%(_python_)s wrapper.py ' + f95, F95FLAGS='-Ix') -foo.Program(target='foo', source='foo.f95') -bar.Program(target='bar', source='bar.f95') -""" % locals()) - - test.write('foo.f95', r""" - PROGRAM FOO - PRINT *,'foo.f95' - STOP - END -""") - - test.write('bar.f95', r""" - PROGRAM BAR - PRINT *,'bar.f95' - STOP - END -""") - - test.run(arguments='foo' + _exe, stderr=None) - test.run(program=test.workpath('foo'), stdout=" foo.f95\n") - test.must_not_exist('wrapper.out') - - import sys - - if sys.platform.startswith('sunos'): - test.run(arguments='bar' + _exe, stderr=None) - else: - test.run(arguments='bar' + _exe) - - test.run(program=test.workpath('bar'), stdout=" bar.f95\n") - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match('test09' + _exe, " -c -x\nThis is a .f95 file.\n") +test.must_match('test10' + _exe, " -c -x\nThis is a .F95 file.\n") test.pass_test() diff --git a/test/Fortran/FORTRANCOMMONFLAGS-live.py b/test/Fortran/FORTRANCOMMONFLAGS-live.py new file mode 100644 index 0000000000..608c2675c7 --- /dev/null +++ b/test/Fortran/FORTRANCOMMONFLAGS-live.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test that while FORTRANFLAGS is not passed to another dialect, +FORTRANCOMMONFLAGS makes it through. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f90' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find an f90 tool; skipping test.\n') + +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """ +DefaultEnvironment(tools=[]) +foo = Environment(F90='%(fc)s') +f90 = foo.Dictionary('F90') +bar = foo.Clone(F90=r'%(_python_)s wrapper.py ' + f90) +foo.Program(target='foo', source='foo.f90') +bar.Program(target='bar', source='bar.f90') +""" % locals()) + +test.write('foo.f90', r""" + PROGRAM FOO + PRINT *,'foo.f90' + END +""") + +test.write('bar.f90', r""" + PROGRAM BAR + PRINT *,'bar.f90' + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f90\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f90\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/FORTRANCOMMONFLAGS.py b/test/Fortran/FORTRANCOMMONFLAGS.py index 90307094fc..3d13afbd5a 100644 --- a/test/Fortran/FORTRANCOMMONFLAGS.py +++ b/test/Fortran/FORTRANCOMMONFLAGS.py @@ -35,10 +35,13 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) test.write('SConstruct', """ +DefaultEnvironment(tools=[]) env = Environment( LINK=r'%(_python_)s mylink.py', LINKFLAGS=[], @@ -56,8 +59,8 @@ env.Program(target='test06', source='test06.FTN') env.Program(target='test07', source='test07.fpp') env.Program(target='test08', source='test08.FPP') -env.Program(target='test11', source='test11.f90') -env.Program(target='test12', source='test12.F90') +env.Program(target='test09', source='test09.f90') +env.Program(target='test10', source='test10.F90') """ % locals()) test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") @@ -68,8 +71,8 @@ test.write('test06.FTN', "This is a .FTN file.\n#link\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#link\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#link\n#fortran\n") -test.write('test11.f90', "This is a .f90 file.\n#link\n#g90\n") -test.write('test12.F90', "This is a .F90 file.\n#link\n#g90\n") +test.write('test09.f90', "This is a .f90 file.\n#link\n#g90\n") +test.write('test10.F90', "This is a .F90 file.\n#link\n#g90\n") test.run(arguments = '.', stderr = None) @@ -81,47 +84,8 @@ test.must_match('test06' + _exe, " -c -z -y\nThis is a .FTN file.\n") test.must_match('test07' + _exe, " -c -z -y\nThis is a .fpp file.\n") test.must_match('test08' + _exe, " -c -z -y\nThis is a .FPP file.\n") -test.must_match('test11' + _exe, " -c -z -x\nThis is a .f90 file.\n") -test.must_match('test12' + _exe, " -c -z -x\nThis is a .F90 file.\n") - - -fc = 'f90' -g90 = test.detect_tool(fc) -if g90: - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(F90='%(fc)s') -f90 = foo.Dictionary('F90') -bar = foo.Clone(F90=r'%(_python_)s wrapper.py ' + f90) -foo.Program(target='foo', source='foo.f90') -bar.Program(target='bar', source='bar.f90') -""" % locals()) - - test.write('foo.f90', r""" - PROGRAM FOO - PRINT *,'foo.f90' - END -""") - - test.write('bar.f90', r""" - PROGRAM BAR - PRINT *,'bar.f90' - END -""") - - test.run(arguments='foo' + _exe, stderr=None) - test.run(program=test.workpath('foo'), stdout=" foo.f90\n") - test.must_not_exist('wrapper.out') - - import sys - - if sys.platform[:5] == 'sunos': - test.run(arguments='bar' + _exe, stderr=None) - else: - test.run(arguments='bar' + _exe) - test.run(program=test.workpath('bar'), stdout=" bar.f90\n") - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match('test09' + _exe, " -c -z -x\nThis is a .f90 file.\n") +test.must_match('test10' + _exe, " -c -z -x\nThis is a .F90 file.\n") test.pass_test() diff --git a/test/Fortran/FORTRANFLAGS-live.py b/test/Fortran/FORTRANFLAGS-live.py new file mode 100644 index 0000000000..944ec2ed8e --- /dev/null +++ b/test/Fortran/FORTRANFLAGS-live.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the FLAGS variable for the generic dialect, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ + +test = TestSCons.TestSCons() +_exe = TestSCons._exe + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f77' +if not test.detect_tool(fc): + # gfortran names all variants the same, try it too + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a fortran tool; skipping test.\n') + +directory = 'x' +test.subdir(directory) +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """ +DefaultEnvironment(tools=[]) +foo = Environment(FORTRAN='%(fc)s') +f77 = foo.Dictionary('FORTRAN') +bar = foo.Clone( + FORTRAN=r'%(_python_)s wrapper.py ' + f77, FORTRANFLAGS='-I%(directory)s' +) +foo.Program(target='foo', source='foo.f') +bar.Program(target='bar', source='bar.f') +""" % locals()) + +test.write('foo.f', r""" + PROGRAM FOO + PRINT *,'foo.f' + STOP + END +""") + +test.write('bar.f', r""" + PROGRAM BAR + PRINT *,'bar.f' + STOP + END +""") + +test.run(arguments='foo' + _exe, stderr=None) +test.run(program=test.workpath('foo'), stdout=" foo.f\n") +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar' + _exe, stderr=None) +else: + test.run(arguments='bar' + _exe) +test.run(program=test.workpath('bar'), stdout=" bar.f\n") +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/FORTRANFLAGS.py b/test/Fortran/FORTRANFLAGS.py index 218c95f9ef..02d5b99505 100644 --- a/test/Fortran/FORTRANFLAGS.py +++ b/test/Fortran/FORTRANFLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSCons @@ -31,26 +30,31 @@ test = TestSCons.TestSCons() _exe = TestSCons._exe +# ref: test/fixture/mylink.py test.file_fixture('mylink.py') +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(LINK = r'%(_python_)s mylink.py', - LINKFLAGS = [], - FORTRAN = r'%(_python_)s myfortran_flags.py fortran', - FORTRANFLAGS = '-x') -env.Program(target = 'test01', source = 'test01.f') -env.Program(target = 'test02', source = 'test02.F') -env.Program(target = 'test03', source = 'test03.for') -env.Program(target = 'test04', source = 'test04.FOR') -env.Program(target = 'test05', source = 'test05.ftn') -env.Program(target = 'test06', source = 'test06.FTN') -env.Program(target = 'test07', source = 'test07.fpp') -env.Program(target = 'test08', source = 'test08.FPP') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + LINK=r'%(_python_)s mylink.py', + LINKFLAGS=[], + FORTRAN=r'%(_python_)s myfortran_flags.py fortran', + FORTRANFLAGS='-x', +) +env.Program(target='test01', source='test01.f') +env.Program(target='test02', source='test02.F') +env.Program(target='test03', source='test03.for') +env.Program(target='test04', source='test04.FOR') +env.Program(target='test05', source='test05.ftn') +env.Program(target='test06', source='test06.FTN') +env.Program(target='test07', source='test07.fpp') +env.Program(target='test08', source='test08.FPP') """ % locals()) -test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#link\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#link\n#fortran\n") test.write('test03.for', "This is a .for file.\n#link\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#link\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#link\n#fortran\n") @@ -58,7 +62,7 @@ test.write('test07.fpp', "This is a .fpp file.\n#link\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#link\n#fortran\n") -test.run(arguments = '.', stderr = None) +test.run(arguments='.', stderr=None) test.must_match('test01' + _exe, " -c -x\nThis is a .f file.\n") test.must_match('test02' + _exe, " -c -x\nThis is a .F file.\n") @@ -69,56 +73,6 @@ test.must_match('test07' + _exe, " -c -x\nThis is a .fpp file.\n") test.must_match('test08' + _exe, " -c -x\nThis is a .FPP file.\n") - -fc = 'f77' -g77 = test.detect_tool(fc) - -if g77: - - directory = 'x' - test.subdir(directory) - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(FORTRAN = '%(fc)s') -f77 = foo.Dictionary('FORTRAN') -bar = foo.Clone(FORTRAN = r'%(_python_)s wrapper.py ' + f77, FORTRANFLAGS = '-I%(directory)s') -foo.Program(target = 'foo', source = 'foo.f') -bar.Program(target = 'bar', source = 'bar.f') -""" % locals()) - - test.write('foo.f', r""" - PROGRAM FOO - PRINT *,'foo.f' - STOP - END -""") - - test.write('bar.f', r""" - PROGRAM BAR - PRINT *,'bar.f' - STOP - END -""") - - - test.run(arguments = 'foo' + _exe, stderr = None) - - test.run(program = test.workpath('foo'), stdout = " foo.f\n") - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar' + _exe, stderr = None) - else: - test.run(arguments = 'bar' + _exe) - - test.run(program = test.workpath('bar'), stdout = " bar.f\n") - - test.must_match('wrapper.out', "wrapper.py\n") - test.pass_test() # Local Variables: diff --git a/test/Fortran/SHF03FLAGS-live.py b/test/Fortran/SHF03FLAGS-live.py new file mode 100644 index 0000000000..ac17e57eb6 --- /dev/null +++ b/test/Fortran/SHF03FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f03' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a f03 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHF03='%(fc)s') +foo = Environment() +shf03 = foo.Dictionary('SHF03') +bar = foo.Clone(SHF03=r'%(_python_)s wrapper.py ' + shf03) +bar.Append(SHF03FLAGS='-Ix') +foo.SharedLibrary(target='foo/foo', source='foo.f03') +bar.SharedLibrary(target='bar/bar', source='bar.f03') +""" % locals()) + +test.write('foo.f03', r""" + PROGRAM FOO + PRINT *,'foo.f03' + STOP + END +""") + +test.write('bar.f03', r""" + PROGRAM BAR + PRINT *,'bar.f03' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform.startswith('sunos'): + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF03FLAGS.py b/test/Fortran/SHF03FLAGS.py new file mode 100644 index 0000000000..e9bf6632eb --- /dev/null +++ b/test/Fortran/SHF03FLAGS.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a mocked compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +_obj = TestSCons._shobj +obj_ = TestSCons.shobj_ + +test = TestSCons.TestSCons() +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + SHF03=r'%(_python_)s myfortran_flags.py g03', + SHFORTRAN=r'%(_python_)s myfortran_flags.py fortran', +) +env.Append(SHF03FLAGS='-x', SHFORTRANFLAGS='-y') +env.SharedObject(target='test01', source='test01.f') +env.SharedObject(target='test02', source='test02.F') +env.SharedObject(target='test03', source='test03.for') +env.SharedObject(target='test04', source='test04.FOR') +env.SharedObject(target='test05', source='test05.ftn') +env.SharedObject(target='test06', source='test06.FTN') +env.SharedObject(target='test07', source='test07.fpp') +env.SharedObject(target='test08', source='test08.FPP') +env.SharedObject(target='test09', source='test09.f03') +env.SharedObject(target='test10', source='test10.F03') +""" % locals()) + +test.write('test01.f', "This is a .f file.\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#fortran\n") +test.write('test03.for', "This is a .for file.\n#fortran\n") +test.write('test04.FOR', "This is a .FOR file.\n#fortran\n") +test.write('test05.ftn', "This is a .ftn file.\n#fortran\n") +test.write('test06.FTN', "This is a .FTN file.\n#fortran\n") +test.write('test07.fpp', "This is a .fpp file.\n#fortran\n") +test.write('test08.FPP', "This is a .FPP file.\n#fortran\n") +test.write('test09.f03', "This is a .f03 file.\n#g03\n") +test.write('test10.F03', "This is a .F03 file.\n#g03\n") + +test.run(arguments='.', stderr=None) +test.must_match(obj_ + 'test01' + _obj, " -c -y\nThis is a .f file.\n") +test.must_match(obj_ + 'test02' + _obj, " -c -y\nThis is a .F file.\n") +test.must_match(obj_ + 'test03' + _obj, " -c -y\nThis is a .for file.\n") +test.must_match(obj_ + 'test04' + _obj, " -c -y\nThis is a .FOR file.\n") +test.must_match(obj_ + 'test05' + _obj, " -c -y\nThis is a .ftn file.\n") +test.must_match(obj_ + 'test06' + _obj, " -c -y\nThis is a .FTN file.\n") +test.must_match(obj_ + 'test07' + _obj, " -c -y\nThis is a .fpp file.\n") +test.must_match(obj_ + 'test08' + _obj, " -c -y\nThis is a .FPP file.\n") +test.must_match(obj_ + 'test09' + _obj, " -c -x\nThis is a .f03 file.\n") +test.must_match(obj_ + 'test10' + _obj, " -c -x\nThis is a .F03 file.\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF08FLAGS-live.py b/test/Fortran/SHF08FLAGS-live.py new file mode 100644 index 0000000000..162d736ce2 --- /dev/null +++ b/test/Fortran/SHF08FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f08' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a f08 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHF08='%(fc)s') +foo = Environment() +shf08 = foo.Dictionary('SHF08') +bar = foo.Clone(SHF08=r'%(_python_)s wrapper.py ' + shf08) +bar.Append(SHF08FLAGS='-Ix') +foo.SharedLibrary(target='foo/foo', source='foo.f08') +bar.SharedLibrary(target='bar/bar', source='bar.f08') +""" % locals()) + +test.write('foo.f08', r""" + PROGRAM FOO + PRINT *,'foo.f08' + STOP + END +""") + +test.write('bar.f08', r""" + PROGRAM BAR + PRINT *,'bar.f08' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform.startswith('sunos'): + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF08FLAGS.py b/test/Fortran/SHF08FLAGS.py new file mode 100644 index 0000000000..cc9f5370bd --- /dev/null +++ b/test/Fortran/SHF08FLAGS.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a mocked compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +_obj = TestSCons._shobj +obj_ = TestSCons.shobj_ + +test = TestSCons.TestSCons() +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + SHF08=r'%(_python_)s myfortran_flags.py g08', + SHFORTRAN=r'%(_python_)s myfortran_flags.py fortran', +) +env.Append(SHF08FLAGS='-x', SHFORTRANFLAGS='-y') +env.SharedObject(target='test01', source='test01.f') +env.SharedObject(target='test02', source='test02.F') +env.SharedObject(target='test03', source='test03.for') +env.SharedObject(target='test04', source='test04.FOR') +env.SharedObject(target='test05', source='test05.ftn') +env.SharedObject(target='test06', source='test06.FTN') +env.SharedObject(target='test07', source='test07.fpp') +env.SharedObject(target='test08', source='test08.FPP') +env.SharedObject(target='test09', source='test09.f08') +env.SharedObject(target='test10', source='test10.F08') +""" % locals()) + +test.write('test01.f', "This is a .f file.\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#fortran\n") +test.write('test03.for', "This is a .for file.\n#fortran\n") +test.write('test04.FOR', "This is a .FOR file.\n#fortran\n") +test.write('test05.ftn', "This is a .ftn file.\n#fortran\n") +test.write('test06.FTN', "This is a .FTN file.\n#fortran\n") +test.write('test07.fpp', "This is a .fpp file.\n#fortran\n") +test.write('test08.FPP', "This is a .FPP file.\n#fortran\n") +test.write('test09.f08', "This is a .f08 file.\n#g08\n") +test.write('test10.F08', "This is a .F08 file.\n#g08\n") + +test.run(arguments='.', stderr=None) +test.must_match(obj_ + 'test01' + _obj, " -c -y\nThis is a .f file.\n") +test.must_match(obj_ + 'test02' + _obj, " -c -y\nThis is a .F file.\n") +test.must_match(obj_ + 'test03' + _obj, " -c -y\nThis is a .for file.\n") +test.must_match(obj_ + 'test04' + _obj, " -c -y\nThis is a .FOR file.\n") +test.must_match(obj_ + 'test05' + _obj, " -c -y\nThis is a .ftn file.\n") +test.must_match(obj_ + 'test06' + _obj, " -c -y\nThis is a .FTN file.\n") +test.must_match(obj_ + 'test07' + _obj, " -c -y\nThis is a .fpp file.\n") +test.must_match(obj_ + 'test08' + _obj, " -c -y\nThis is a .FPP file.\n") +test.must_match(obj_ + 'test09' + _obj, " -c -x\nThis is a .f08 file.\n") +test.must_match(obj_ + 'test10' + _obj, " -c -x\nThis is a .F08 file.\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF77FLAGS-live.py b/test/Fortran/SHF77FLAGS-live.py new file mode 100644 index 0000000000..b3ec4aa3c7 --- /dev/null +++ b/test/Fortran/SHF77FLAGS-live.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f77' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a f77 tool; skipping test.\n') + +directory = 'x' +test.subdir(directory) +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHF77='%(fc)s') +foo = Environment() +shf77 = foo.Dictionary('SHF77') +bar = foo.Clone( + SHF77=r'%(_python_)s wrapper.py ' + shf77, + tools=["default", 'f77'], + F77FILESUFFIXES=[".f"], +) +bar.Append(SHF77FLAGS='-I%(directory)s') +foo.SharedLibrary(target='foo/foo', source='foo.f') +bar.SharedLibrary(target='bar/bar', source='bar.f') +""" % locals()) + +test.write('foo.f', r""" + PROGRAM FOO + PRINT *,'foo.f' + STOP + END +""") + +test.write('bar.f', r""" + PROGRAM BAR + PRINT *,'bar.f' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF77FLAGS.py b/test/Fortran/SHF77FLAGS.py index 3a7e6b2d9a..5b15616943 100644 --- a/test/Fortran/SHF77FLAGS.py +++ b/test/Fortran/SHF77FLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,83 +22,39 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a mocked compiler. +""" + +import sys import TestSCons _python_ = TestSCons._python_ - _obj = TestSCons._shobj obj_ = TestSCons.shobj_ test = TestSCons.TestSCons() +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(SHF77 = r'%(_python_)s myfortran_flags.py g77') -env.Append(SHF77FLAGS = '-x') -env.SharedObject(target = 'test09', source = 'test09.f77') -env.SharedObject(target = 'test10', source = 'test10.F77') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment(SHF77=r'%(_python_)s myfortran_flags.py g77') +env.Append(SHF77FLAGS='-x') +env.SharedObject(target='test09', source='test09.f77') +env.SharedObject(target='test10', source='test10.F77') """ % locals()) test.write('test09.f77', "This is a .f77 file.\n#g77\n") test.write('test10.F77', "This is a .F77 file.\n#g77\n") -test.run(arguments = '.', stderr = None) - +test.run(arguments='.', stderr=None) test.must_match(obj_ + 'test09' + _obj, " -c -x\nThis is a .f77 file.\n") test.must_match(obj_ + 'test10' + _obj, " -c -x\nThis is a .F77 file.\n") - -fc = 'f77' -g77 = test.detect_tool(fc) - -if g77: - - directory = 'x' - test.subdir(directory) - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(SHF77 = '%(fc)s') -shf77 = foo.Dictionary('SHF77') -bar = foo.Clone(SHF77 = r'%(_python_)s wrapper.py ' + shf77, - tools = ["default", 'f77'], F77FILESUFFIXES = [".f"]) -bar.Append(SHF77FLAGS = '-I%(directory)s') -foo.SharedLibrary(target = 'foo/foo', source = 'foo.f') -bar.SharedLibrary(target = 'bar/bar', source = 'bar.f') -""" % locals()) - - test.write('foo.f', r""" - PROGRAM FOO - PRINT *,'foo.f' - STOP - END -""") - - test.write('bar.f', r""" - PROGRAM BAR - PRINT *,'bar.f' - STOP - END -""") - - - test.run(arguments = 'foo', stderr = None) - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar', stderr = None) - else: - test.run(arguments = 'bar') - - test.must_match('wrapper.out', "wrapper.py\n") - test.pass_test() # Local Variables: diff --git a/test/Fortran/SHF90FLAGS-live.py b/test/Fortran/SHF90FLAGS-live.py new file mode 100644 index 0000000000..08a12a3b68 --- /dev/null +++ b/test/Fortran/SHF90FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f90' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a f90 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHF90='%(fc)s') +foo = Environment() +shf90 = foo.Dictionary('SHF90') +bar = foo.Clone(SHF90=r'%(_python_)s wrapper.py ' + shf90) +bar.Append(SHF90FLAGS='-Ix') +foo.SharedLibrary(target='foo/foo', source='foo.f90') +bar.SharedLibrary(target='bar/bar', source='bar.f90') +""" % locals()) + +test.write('foo.f90', r""" + PROGRAM FOO + PRINT *,'foo.f90' + STOP + END +""") + +test.write('bar.f90', r""" + PROGRAM BAR + PRINT *,'bar.f90' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform.startswith('sunos'): + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF90FLAGS.py b/test/Fortran/SHF90FLAGS.py index 900acbccab..fbc591f956 100644 --- a/test/Fortran/SHF90FLAGS.py +++ b/test/Fortran/SHF90FLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,50 +22,55 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a mocked compiler. +""" + +import sys import TestSCons _python_ = TestSCons._python_ - _obj = TestSCons._shobj obj_ = TestSCons.shobj_ test = TestSCons.TestSCons() +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(SHF90 = r'%(_python_)s myfortran_flags.py g90', - SHFORTRAN = r'%(_python_)s myfortran_flags.py fortran') -env.Append(SHF90FLAGS = '-x', - SHFORTRANFLAGS = '-y') -env.SharedObject(target = 'test01', source = 'test01.f') -env.SharedObject(target = 'test02', source = 'test02.F') -env.SharedObject(target = 'test03', source = 'test03.for') -env.SharedObject(target = 'test04', source = 'test04.FOR') -env.SharedObject(target = 'test05', source = 'test05.ftn') -env.SharedObject(target = 'test06', source = 'test06.FTN') -env.SharedObject(target = 'test07', source = 'test07.fpp') -env.SharedObject(target = 'test08', source = 'test08.FPP') -env.SharedObject(target = 'test11', source = 'test11.f90') -env.SharedObject(target = 'test12', source = 'test12.F90') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment( + SHF90=r'%(_python_)s myfortran_flags.py g90', + SHFORTRAN=r'%(_python_)s myfortran_flags.py fortran', +) +env.Append(SHF90FLAGS='-x', SHFORTRANFLAGS='-y') +env.SharedObject(target='test01', source='test01.f') +env.SharedObject(target='test02', source='test02.F') +env.SharedObject(target='test03', source='test03.for') +env.SharedObject(target='test04', source='test04.FOR') +env.SharedObject(target='test05', source='test05.ftn') +env.SharedObject(target='test06', source='test06.FTN') +env.SharedObject(target='test07', source='test07.fpp') +env.SharedObject(target='test08', source='test08.FPP') +env.SharedObject(target='test09', source='test09.f90') +env.SharedObject(target='test10', source='test10.F90') """ % locals()) -test.write('test01.f', "This is a .f file.\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#fortran\n") test.write('test03.for', "This is a .for file.\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#fortran\n") test.write('test06.FTN', "This is a .FTN file.\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#fortran\n") -test.write('test11.f90', "This is a .f90 file.\n#g90\n") -test.write('test12.F90', "This is a .F90 file.\n#g90\n") - -test.run(arguments = '.', stderr = None) +test.write('test09.f90', "This is a .f90 file.\n#g90\n") +test.write('test10.F90', "This is a .F90 file.\n#g90\n") +test.run(arguments='.', stderr=None) test.must_match(obj_ + 'test01' + _obj, " -c -y\nThis is a .f file.\n") test.must_match(obj_ + 'test02' + _obj, " -c -y\nThis is a .F file.\n") test.must_match(obj_ + 'test03' + _obj, " -c -y\nThis is a .for file.\n") @@ -72,51 +79,8 @@ test.must_match(obj_ + 'test06' + _obj, " -c -y\nThis is a .FTN file.\n") test.must_match(obj_ + 'test07' + _obj, " -c -y\nThis is a .fpp file.\n") test.must_match(obj_ + 'test08' + _obj, " -c -y\nThis is a .FPP file.\n") -test.must_match(obj_ + 'test11' + _obj, " -c -x\nThis is a .f90 file.\n") -test.must_match(obj_ + 'test12' + _obj, " -c -x\nThis is a .F90 file.\n") - -fc = 'f90' -g90 = test.detect_tool(fc) - -if g90: - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(SHF90 = '%(fc)s') -shf90 = foo.Dictionary('SHF90') -bar = foo.Clone(SHF90 = r'%(_python_)s wrapper.py ' + shf90) -bar.Append(SHF90FLAGS = '-Ix') -foo.SharedLibrary(target = 'foo/foo', source = 'foo.f90') -bar.SharedLibrary(target = 'bar/bar', source = 'bar.f90') -""" % locals()) - - test.write('foo.f90', r""" - PROGRAM FOO - PRINT *,'foo.f90' - STOP - END -""") - - test.write('bar.f90', r""" - PROGRAM BAR - PRINT *,'bar.f90' - STOP - END -""") - - - test.run(arguments = 'foo', stderr = None) - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar', stderr = None) - else: - test.run(arguments = 'bar') - - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match(obj_ + 'test09' + _obj, " -c -x\nThis is a .f90 file.\n") +test.must_match(obj_ + 'test10' + _obj, " -c -x\nThis is a .F90 file.\n") test.pass_test() diff --git a/test/Fortran/SHF95FLAGS-live.py b/test/Fortran/SHF95FLAGS-live.py new file mode 100644 index 0000000000..aaaf981cbb --- /dev/null +++ b/test/Fortran/SHF95FLAGS-live.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f95' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a f95 tool; skipping test.\n') + +test.subdir('x') +test.write(['x', 'dummy.i'], """\ +# Exists only such that -Ix finds the directory... +""") +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHF95='%(fc)s') +foo = Environment() +shf95 = foo.Dictionary('SHF95') +bar = foo.Clone(SHF95=r'%(_python_)s wrapper.py ' + shf95) +bar.Append(SHF95FLAGS='-Ix') +foo.SharedLibrary(target='foo/foo', source='foo.f95') +bar.SharedLibrary(target='bar/bar', source='bar.f95') +""" % locals()) + +test.write('foo.f95', r""" + PROGRAM FOO + PRINT *,'foo.f95' + STOP + END +""") + +test.write('bar.f95', r""" + PROGRAM BAR + PRINT *,'bar.f95' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform.startswith('sunos'): + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHF95FLAGS.py b/test/Fortran/SHF95FLAGS.py index dcec49bf7d..f8a80d4b20 100644 --- a/test/Fortran/SHF95FLAGS.py +++ b/test/Fortran/SHF95FLAGS.py @@ -23,10 +23,16 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a mocked compiler. +""" + +import sys + import TestSCons _python_ = TestSCons._python_ - _obj = TestSCons._shobj obj_ = TestSCons.shobj_ @@ -34,7 +40,8 @@ # ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) env = Environment( SHF95=r'%(_python_)s myfortran_flags.py g95', SHFORTRAN=r'%(_python_)s myfortran_flags.py fortran', @@ -48,20 +55,20 @@ env.SharedObject(target='test06', source='test06.FTN') env.SharedObject(target='test07', source='test07.fpp') env.SharedObject(target='test08', source='test08.FPP') -env.SharedObject(target='test13', source='test13.f95') -env.SharedObject(target='test14', source='test14.F95') +env.SharedObject(target='test09', source='test09.f95') +env.SharedObject(target='test10', source='test10.F95') """ % locals()) -test.write('test01.f', "This is a .f file.\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#fortran\n") test.write('test03.for', "This is a .for file.\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#fortran\n") test.write('test06.FTN', "This is a .FTN file.\n#fortran\n") test.write('test07.fpp', "This is a .fpp file.\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#fortran\n") -test.write('test13.f95', "This is a .f95 file.\n#g95\n") -test.write('test14.F95', "This is a .F95 file.\n#g95\n") +test.write('test09.f95', "This is a .f95 file.\n#g95\n") +test.write('test10.F95', "This is a .F95 file.\n#g95\n") test.run(arguments='.', stderr=None) test.must_match(obj_ + 'test01' + _obj, " -c -y\nThis is a .f file.\n") @@ -72,53 +79,8 @@ test.must_match(obj_ + 'test06' + _obj, " -c -y\nThis is a .FTN file.\n") test.must_match(obj_ + 'test07' + _obj, " -c -y\nThis is a .fpp file.\n") test.must_match(obj_ + 'test08' + _obj, " -c -y\nThis is a .FPP file.\n") -test.must_match(obj_ + 'test13' + _obj, " -c -x\nThis is a .f95 file.\n") -test.must_match(obj_ + 'test14' + _obj, " -c -x\nThis is a .F95 file.\n") - -fc = 'f95' -g95 = test.detect_tool(fc) -if g95: - test.subdir('x') - test.write(['x','dummy.i'], -""" -# Exists only such that -Ix finds the directory... -""") - - # ref: test/fixture/wrapper.py - test.file_fixture('wrapper.py') - test.write('SConstruct', """ -foo = Environment(SHF95='%(fc)s') -shf95 = foo.Dictionary('SHF95') -bar = foo.Clone(SHF95=r'%(_python_)s wrapper.py ' + shf95) -bar.Append(SHF95FLAGS='-Ix') -foo.SharedLibrary(target='foo/foo', source='foo.f95') -bar.SharedLibrary(target='bar/bar', source='bar.f95') -""" % locals()) - - test.write('foo.f95', r""" - PROGRAM FOO - PRINT *,'foo.f95' - STOP - END -""") - - test.write('bar.f95', r""" - PROGRAM BAR - PRINT *,'bar.f95' - STOP - END -""") - - test.run(arguments='foo', stderr=None) - test.must_not_exist('wrapper.out') - - import sys - - if sys.platform.startswith('sunos'): - test.run(arguments='bar', stderr=None) - else: - test.run(arguments='bar') - test.must_match('wrapper.out', "wrapper.py\n") +test.must_match(obj_ + 'test09' + _obj, " -c -x\nThis is a .f95 file.\n") +test.must_match(obj_ + 'test10' + _obj, " -c -x\nThis is a .F95 file.\n") test.pass_test() diff --git a/test/Fortran/SHFORTRANFLAGS-live.py b/test/Fortran/SHFORTRANFLAGS-live.py new file mode 100644 index 0000000000..a36cfd1afa --- /dev/null +++ b/test/Fortran/SHFORTRANFLAGS-live.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test handling of the dialect-specific FLAGS variable for shared objects, +using a live compiler. +""" + +import sys + +import TestSCons + +_python_ = TestSCons._python_ +test = TestSCons.TestSCons() + +# ref: test/Fortran/fixture/myfortran_flags.py +test.file_fixture(['fixture', 'myfortran_flags.py']) + +fc = 'f77' +if not test.detect_tool(fc): + fc = 'gfortran' + if not test.detect_tool(fc): + test.skip_test('Could not find a fortran tool; skipping test.\n') + +directory = 'x' +test.subdir(directory) +# ref: test/fixture/wrapper.py +test.file_fixture('wrapper.py') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +# foo = Environment(SHFORTRAN='%(fc)s') +foo = Environment() +shfortran = foo.Dictionary('SHFORTRAN') +bar = foo.Clone(SHFORTRAN=r'%(_python_)s wrapper.py ' + shfortran) +bar.Append(SHFORTRANFLAGS='-I%(directory)s') +foo.SharedLibrary(target='foo/foo', source='foo.f') +bar.SharedLibrary(target='bar/bar', source='bar.f') +""" % locals()) + +test.write('foo.f', r""" + PROGRAM FOO + PRINT *,'foo.f' + STOP + END +""") + +test.write('bar.f', r""" + PROGRAM BAR + PRINT *,'bar.f' + STOP + END +""") + +test.run(arguments='foo', stderr=None) +test.must_not_exist('wrapper.out') + +if sys.platform[:5] == 'sunos': + test.run(arguments='bar', stderr=None) +else: + test.run(arguments='bar') +test.must_match('wrapper.out', "wrapper.py\n") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/Fortran/SHFORTRANFLAGS.py b/test/Fortran/SHFORTRANFLAGS.py index 6a4692bb2c..0954738aa5 100644 --- a/test/Fortran/SHFORTRANFLAGS.py +++ b/test/Fortran/SHFORTRANFLAGS.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,34 +22,35 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +import sys import TestSCons _python_ = TestSCons._python_ -_obj = TestSCons._shobj -obj_ = TestSCons.shobj_ +_obj = TestSCons._shobj +obj_ = TestSCons.shobj_ test = TestSCons.TestSCons() +# ref: test/Fortran/fixture/myfortran_flags.py test.file_fixture(['fixture', 'myfortran_flags.py']) -test.write('SConstruct', """ -env = Environment(SHFORTRAN = r'%(_python_)s myfortran_flags.py fortran') -env.Append(SHFORTRANFLAGS = '-x') -env.SharedObject(target = 'test01', source = 'test01.f') -env.SharedObject(target = 'test02', source = 'test02.F') -env.SharedObject(target = 'test03', source = 'test03.for') -env.SharedObject(target = 'test04', source = 'test04.FOR') -env.SharedObject(target = 'test05', source = 'test05.ftn') -env.SharedObject(target = 'test06', source = 'test06.FTN') -env.SharedObject(target = 'test07', source = 'test07.fpp') -env.SharedObject(target = 'test08', source = 'test08.FPP') +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +env = Environment(SHFORTRAN=r'%(_python_)s myfortran_flags.py fortran') +env.Append(SHFORTRANFLAGS='-x') +env.SharedObject(target='test01', source='test01.f') +env.SharedObject(target='test02', source='test02.F') +env.SharedObject(target='test03', source='test03.for') +env.SharedObject(target='test04', source='test04.FOR') +env.SharedObject(target='test05', source='test05.ftn') +env.SharedObject(target='test06', source='test06.FTN') +env.SharedObject(target='test07', source='test07.fpp') +env.SharedObject(target='test08', source='test08.FPP') """ % locals()) -test.write('test01.f', "This is a .f file.\n#fortran\n") -test.write('test02.F', "This is a .F file.\n#fortran\n") +test.write('test01.f', "This is a .f file.\n#fortran\n") +test.write('test02.F', "This is a .F file.\n#fortran\n") test.write('test03.for', "This is a .for file.\n#fortran\n") test.write('test04.FOR', "This is a .FOR file.\n#fortran\n") test.write('test05.ftn', "This is a .ftn file.\n#fortran\n") @@ -55,8 +58,7 @@ test.write('test07.fpp', "This is a .fpp file.\n#fortran\n") test.write('test08.FPP', "This is a .FPP file.\n#fortran\n") -test.run(arguments = '.', stderr = None) - +test.run(arguments='.', stderr=None) test.must_match(obj_ + 'test01' + _obj, " -c -x\nThis is a .f file.\n") test.must_match(obj_ + 'test02' + _obj, " -c -x\nThis is a .F file.\n") test.must_match(obj_ + 'test03' + _obj, " -c -x\nThis is a .for file.\n") @@ -66,52 +68,6 @@ test.must_match(obj_ + 'test07' + _obj, " -c -x\nThis is a .fpp file.\n") test.must_match(obj_ + 'test08' + _obj, " -c -x\nThis is a .FPP file.\n") -fc = 'f77' -fortran = test.detect_tool(fc) - -if fortran: - - directory = 'x' - test.subdir(directory) - - test.file_fixture('wrapper.py') - - test.write('SConstruct', """ -foo = Environment(SHFORTRAN = '%(fc)s') -shfortran = foo.Dictionary('SHFORTRAN') -bar = foo.Clone(SHFORTRAN = r'%(_python_)s wrapper.py ' + shfortran) -bar.Append(SHFORTRANFLAGS = '-I%(directory)s') -foo.SharedLibrary(target = 'foo/foo', source = 'foo.f') -bar.SharedLibrary(target = 'bar/bar', source = 'bar.f') -""" % locals()) - - test.write('foo.f', r""" - PROGRAM FOO - PRINT *,'foo.f' - STOP - END -""") - - test.write('bar.f', r""" - PROGRAM BAR - PRINT *,'bar.f' - STOP - END -""") - - - test.run(arguments = 'foo', stderr = None) - - test.must_not_exist('wrapper.out') - - import sys - if sys.platform[:5] == 'sunos': - test.run(arguments = 'bar', stderr = None) - else: - test.run(arguments = 'bar') - - test.must_match('wrapper.out', "wrapper.py\n") - test.pass_test() # Local Variables: diff --git a/test/GetBuildFailures/option-k.py b/test/GetBuildFailures/option-k.py index 039ad50a91..50ad302980 100644 --- a/test/GetBuildFailures/option-k.py +++ b/test/GetBuildFailures/option-k.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,14 +22,11 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# """ Verify that a failed build action with -k works as expected. """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - import TestSCons _python_ = TestSCons._python_ @@ -85,7 +84,7 @@ def print_build_failures(): scons: done building targets (errors occurred during build). f4 failed: Error 1 f5 failed: Error 1 -""" % locals() +""" expect_stderr = """\ scons: *** [f4] Error 1 @@ -99,7 +98,7 @@ def print_build_failures(): test.must_match(test.workpath('f3'), 'f3.in\n') test.must_not_exist(test.workpath('f4')) test.must_not_exist(test.workpath('f5')) -test.must_match(test.workpath('f6'), 'f6.in\n') +test.must_match(test.workpath('f6'), 'f6.in\n') test.pass_test() diff --git a/test/GetOption/BadSetOption.py b/test/GetOption/BadSetOption.py index 7b0a33dcaf..f408e39022 100644 --- a/test/GetOption/BadSetOption.py +++ b/test/GetOption/BadSetOption.py @@ -32,7 +32,7 @@ test = TestSCons.TestSCons() badopts = ( - ("no_such_var", True, "This option is not settable from a SConscript file: no_such_var"), + ("no_such_var", True, "This option is not settable from an SConscript file: 'no_such_var'"), ("num_jobs", -22, "A positive integer is required: -22"), ("max_drift", "'Foo'", "An integer is required: 'Foo'"), ("duplicate", "'cookie'", "Not a valid duplication style: cookie"), @@ -45,11 +45,10 @@ SConstruct = "SC-" + opt test.write( SConstruct, - """\ + f"""\ DefaultEnvironment(tools=[]) -SetOption("%(opt)s", %(value)s) -""" - % locals(), +SetOption("{opt}", {value}) +""", ) expect = r"scons: *** %s" % expect test.run(arguments='-Q -f %s .' % SConstruct, stderr=None, status=2) diff --git a/test/Glob/Repository.py b/test/Glob/Repository.py index 714bafa57a..b2b6195760 100644 --- a/test/Glob/Repository.py +++ b/test/Glob/Repository.py @@ -49,10 +49,9 @@ test.write(['repository', 'SConstruct'], """\ DefaultEnvironment(tools=[]) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], 'wb') as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) # Verify that we can glob a repository-only Node that exists diff --git a/test/Glob/VariantDir.py b/test/Glob/VariantDir.py index 6340bb8007..3b64848b04 100644 --- a/test/Glob/VariantDir.py +++ b/test/Glob/VariantDir.py @@ -49,12 +49,12 @@ """) test.write(['src', 'SConscript'], """\ -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) @@ -67,9 +67,9 @@ def concatenate(target, source, env): env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/basic.py b/test/Glob/basic.py index 5e5cdb5ac2..53077ba93c 100644 --- a/test/Glob/basic.py +++ b/test/Glob/basic.py @@ -34,12 +34,12 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/exclude.py b/test/Glob/exclude.py index 658d99acd7..5c909cb186 100644 --- a/test/Glob/exclude.py +++ b/test/Glob/exclude.py @@ -37,12 +37,12 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/source.py b/test/Glob/source.py index 33aff85163..807d2d7de2 100644 --- a/test/Glob/source.py +++ b/test/Glob/source.py @@ -38,12 +38,12 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/strings.py b/test/Glob/strings.py index 2a4a624db6..76824f00f1 100644 --- a/test/Glob/strings.py +++ b/test/Glob/strings.py @@ -46,12 +46,12 @@ """) test.write(['src', 'SConscript'], """\ -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/subdir.py b/test/Glob/subdir.py index 22439f7d07..63135ba08c 100644 --- a/test/Glob/subdir.py +++ b/test/Glob/subdir.py @@ -37,12 +37,12 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) -env = Environment(tools=[]) +env = Environment(tools=[]) def concatenate(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Concatenate'] = Builder(action=concatenate) diff --git a/test/Glob/subst.py b/test/Glob/subst.py index efbc916470..4b38779fc7 100644 --- a/test/Glob/subst.py +++ b/test/Glob/subst.py @@ -38,9 +38,9 @@ env = Environment(tools=[], PATTERN = 'f*.in') def copy(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) env['BUILDERS']['Copy'] = Builder(action=copy) diff --git a/test/HeaderGen.py b/test/HeaderGen.py index f66ef574de..10fa68639f 100644 --- a/test/HeaderGen.py +++ b/test/HeaderGen.py @@ -35,7 +35,7 @@ test.write('SConstruct', """\ def writeFile(target, contents): - with open(str(target[0]), 'w') as f: + with open(target[0], 'w') as f: f.write(contents) return 0 @@ -59,7 +59,7 @@ def writeFile(target, contents): env = Environment() def gen_a_h(target, source, env): - with open(str(target[0]), 'w') as t, open(str(source[0]), 'r') as s: + with open(target[0], 'w') as t, open(source[0], 'r') as s: s.readline() t.write(s.readline()[:-1] + ';\\n') diff --git a/test/Help.py b/test/Help.py index a090d42125..329ac1dd5b 100644 --- a/test/Help.py +++ b/test/Help.py @@ -81,51 +81,19 @@ test.run(arguments = '-h', stdout = expect) -# Bug #2831 - append flag to Help doesn't wipe out addoptions and variables used together -test.write('SConstruct', r""" +# Use fixturized SConstruct_HELP_AddOption.py for the remaining tests +# All of which add help with AddOption() and then call Help() various ways +# to test how the help from the option contributes to the Help()'s output -AddOption( - '--debugging', - dest='debugging', - action='store_true', - default=False, - metavar='BDEBUGGING', - help='Compile with debugging symbols', -) - -vars = Variables() -vars.Add(ListVariable('buildmod', 'List of modules to build', 'none', ['python'])) -DefaultEnvironment(tools=[]) -env = Environment() -Help(vars.GenerateHelpText(env), append=True) -""") +test.file_fixture('SConstruct_HELP_AddOption.py', 'SConstruct') +# Bug #2831 - append flag to Help doesn't wipe out addoptions and variables used together expect = ".*--debugging.*Compile with debugging symbols.*buildmod: List of modules to build.*" test.run(arguments = '-h', stdout = expect, match=TestSCons.match_re_dotall) # Bug 2831 # This test checks to verify that append=False doesn't include anything # but the expected help for the specified Variable() - -test.write('SConstruct', r""" -AddOption( - '--debugging', - dest='debugging', - action='store_true', - default=False, - metavar='BDEBUGGING', - help='Compile with debugging symbols', -) - -vars = Variables() -vars.Add(ListVariable('buildmod', 'List of modules to build', 'none', ['python'])) - -DefaultEnvironment(tools=[]) -env = Environment() - -Help(vars.GenerateHelpText(env), append=False) -""") - expect = """\ scons: Reading SConscript files ... scons: done reading SConscript files. @@ -139,29 +107,10 @@ Use scons -H for help about SCons built-in command-line options. """ -test.run(arguments='-h', stdout=expect) +test.run(arguments='-h NO_APPEND=1', stdout=expect) -# Enhancement: keep_local flag saves the AddOption help, +# Enhancement: local_only flag saves the AddOption help, # but not SCons' own help. -test.write('SConstruct', r""" -AddOption( - '--debugging', - dest='debugging', - action='store_true', - default=False, - metavar='BDEBUGGING', - help='Compile with debugging symbols', -) - -vars = Variables() -vars.Add(ListVariable('buildmod', 'List of modules to build', 'none', ['python'])) - -DefaultEnvironment(tools=[]) -env = Environment() - -Help(vars.GenerateHelpText(env), append=True, keep_local=True) -""") - expect = """\ scons: Reading SConscript files ... scons: done reading SConscript files. @@ -177,7 +126,7 @@ Use scons -H for help about SCons built-in command-line options. """ -test.run(arguments='-h', stdout=expect) +test.run(arguments='-h LOCAL_ONLY=1', stdout=expect) test.pass_test() diff --git a/test/Install/Install.py b/test/Install/Install.py index 2857c72901..8bb9ed719c 100644 --- a/test/Install/Install.py +++ b/test/Install/Install.py @@ -31,6 +31,7 @@ import time import TestSCons +from TestCmd import IS_ROOT test = TestSCons.TestSCons() @@ -49,10 +50,9 @@ test.write(['work', 'SConstruct'], """\ DefaultEnvironment(tools=[]) def cat(env, source, target): - target = str(target[0]) - with open(target, 'wb') as ofp: + with open(target[0], 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) def my_install(dest, source, env): @@ -131,17 +131,17 @@ def my_install(dest, source, env): # if a target can not be unlinked before building it: test.write(['work', 'f1.in'], "f1.in again again\n") -os.chmod(test.workpath('work', 'export'), 0o555) -with open(f1_out, 'rb'): - expect = [ - "Permission denied", - "The process cannot access the file because it is being used by another process", - "Der Prozess kann nicht auf die Datei zugreifen, da sie von einem anderen Prozess verwendet wird", - ] - - test.run(chdir='work', arguments=f1_out, stderr=None, status=2) - - test.must_contain_any_line(test.stderr(), expect) +# This test is not designed to work if running as root +if not IS_ROOT: + os.chmod(test.workpath('work', 'export'), 0o555) + with open(f1_out, 'rb'): + expect = [ + "Permission denied", + "The process cannot access the file because it is being used by another process", + "Der Prozess kann nicht auf die Datei zugreifen, da sie von einem anderen Prozess verwendet wird", + ] + test.run(chdir='work', arguments=f1_out, stderr=None, status=2) + test.must_contain_any_line(test.stderr(), expect) test.pass_test() diff --git a/test/Install/wrap-by-attribute.py b/test/Install/wrap-by-attribute.py index c18ea7757e..7cb645b2da 100644 --- a/test/Install/wrap-by-attribute.py +++ b/test/Install/wrap-by-attribute.py @@ -46,10 +46,9 @@ def cat(env, source, target): - target = str(target[0]) - with open(target, 'wb') as ofp: + with open(target[0], 'wb') as ofp: for src in source: - with open(str(src), 'rb') as ifp: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) diff --git a/test/Interactive/option-j.py b/test/Interactive/option-j.py index 356a06749a..60b8fcc7f4 100644 --- a/test/Interactive/option-j.py +++ b/test/Interactive/option-j.py @@ -40,8 +40,8 @@ def cat(target, source, env): t = str(target[0]) os.mkdir(t + '.started') with open(t, 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) os.mkdir(t + '.finished') @@ -64,8 +64,8 @@ def must_wait_for_f2_b_out(target, source, env): while not os.path.exists(f2_b_started): time.sleep(1) with open(t, 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) os.mkdir(t + '.finished') diff --git a/test/Java/JARCHDIR.py b/test/Java/JARCHDIR.py index c24717231d..c84509bd5d 100644 --- a/test/Java/JARCHDIR.py +++ b/test/Java/JARCHDIR.py @@ -32,7 +32,6 @@ ${TARGET} or ${SOURCE} work. """ - import TestSCons test = TestSCons.TestSCons() @@ -60,7 +59,7 @@ source_env.Jar('out/s.jar', 'in/s.class') Default(bin, jar, inner) -""" % locals()) +""") test.subdir('in') diff --git a/test/Java/JARFLAGS.py b/test/Java/JARFLAGS.py index 36af5929ec..957464d98d 100644 --- a/test/Java/JARFLAGS.py +++ b/test/Java/JARFLAGS.py @@ -39,7 +39,7 @@ env['JARFLAGS'] = 'cvf' class_files = env.Java(target='classes', source='src') env.Jar(target='test.jar', source=class_files) -""" % locals()) +""") test.write(['src', 'Example1.java'], """\ package src; @@ -60,7 +60,7 @@ jar cvf test.jar -C classes src.Example1\\.class .* adding: src.Example1\\.class.* -""" % locals()) +""") test.run(arguments = '.', diff --git a/test/Java/JAVACFLAGS.py b/test/Java/JAVACFLAGS.py index 3a555a3a1a..51bf3f68dd 100644 --- a/test/Java/JAVACFLAGS.py +++ b/test/Java/JAVACFLAGS.py @@ -37,7 +37,7 @@ DefaultEnvironment(tools=[]) env = Environment(tools=['javac'], JAVACFLAGS='-O') env.Java(target='classes', source='src') -""" % locals()) +""") test.write(['src', 'Example1.java'], """\ package src; diff --git a/test/Java/JAVACLASSPATH.py b/test/Java/JAVACLASSPATH.py index 38f01cc03b..48034dd096 100644 --- a/test/Java/JAVACLASSPATH.py +++ b/test/Java/JAVACLASSPATH.py @@ -59,7 +59,7 @@ j1 = env.Java(target='class1', source='com.1/Example1.java') j2 = env.Java(target='class2', source='com.2/Example2.java') env.JavaH(target='outdir', source=[j1, j2], JAVACLASSPATH='class2') -""" % locals()) +""") test.subdir('com.1', 'com.2') diff --git a/test/Java/JAVAH.py b/test/Java/JAVAH.py index 27e98216f2..1ad3933c05 100644 --- a/test/Java/JAVAH.py +++ b/test/Java/JAVAH.py @@ -39,7 +39,7 @@ # Skip this test as SCons doesn't (currently) predict the generated # inner/anonymous class generated .class files generated by gcj -# and so will always fail +# and so will always fail if test.javac_is_gcj: test.skip_test('Test not valid for gcj (gnu java); skipping test(s).\n') @@ -242,8 +242,7 @@ class Private { test.must_match( 'wrapper.out', - "wrapper_with_args.py javah -d outdir2 -classpath class2 com.sub.bar.Example4 com.other.Example5 com.sub.bar.Example6\n" - % locals(), + "wrapper_with_args.py javah -d outdir2 -classpath class2 com.sub.bar.Example4 com.other.Example5 com.sub.bar.Example6\n", mode='r', ) diff --git a/test/Java/multi-step.py b/test/Java/multi-step.py index ab627cb46f..05ef90d40e 100644 --- a/test/Java/multi-step.py +++ b/test/Java/multi-step.py @@ -49,7 +49,7 @@ # Skip this test as SCons doesn't (currently) predict the generated # inner/anonymous class generated .class files generated by gcj -# and so will always fail +# and so will always fail if test.javac_is_gcj: test.skip_test('Test not valid for gcj (gnu java); skipping test(s).\n') @@ -96,6 +96,7 @@ else: tools = ['default', 'javac', 'javah', 'swig'] +_ = DefaultEnvironment(tools=[]) env = Environment(tools=tools, CPPPATH=["$JAVAINCLUDES"]) Export('env') # env.PrependENVPath('PATH',os.environ.get('PATH',[])) @@ -125,7 +126,7 @@ 'buildout/javah/SConscript', ] ) -""" % locals()) +""") test.write(['src', 'HelloApplet', 'Hello.html'], """\ diff --git a/test/Java/no-JARCHDIR.py b/test/Java/no-JARCHDIR.py index d3392518c8..0798b090ad 100644 --- a/test/Java/no-JARCHDIR.py +++ b/test/Java/no-JARCHDIR.py @@ -51,10 +51,11 @@ """) test.write('SConstruct', """\ -env = Environment(tools = ['javac', 'jar']) +_ = DefaultEnvironment(tools=[]) +env = Environment(tools=['javac', 'jar']) jar = env.Jar('x.jar', env.Java(target = 'classes', source = 'src')) -""" % locals()) +""") test.run(arguments = '.') test.run(program = where_jar, arguments = 'tf x.jar') @@ -73,7 +74,7 @@ JARCHDIR = None) jar = env.Jar('x.jar', env.Java(target = 'classes', source = 'src')) -""" % locals()) +""") test.run(arguments = '.') diff --git a/test/Java/swig-dependencies.py b/test/Java/swig-dependencies.py index 48010ad8f0..3fa563dd44 100644 --- a/test/Java/swig-dependencies.py +++ b/test/Java/swig-dependencies.py @@ -47,6 +47,7 @@ test.write(['SConstruct'], """\ import os +_ = DefaultEnvironment(tools=[]) env = Environment(ENV=os.environ) if env['PLATFORM'] != 'win32': env.Append(CPPFLAGS=' -g -Wall') @@ -56,7 +57,7 @@ SConscript('#foo/SConscript') SConscript('#java/SConscript') -""" % locals()) +""") test.write(['foo', 'SConscript'], """\ Import('env') @@ -68,7 +69,7 @@ #include "foo.h" int fooAdd(int a, int b) { - return a + b; + return a + b; } """) @@ -115,11 +116,11 @@ swigflags = '-c++ -java -Wall -Ifoo -DTEST_$PLATFORM' Java_foo_interface = env.SharedLibrary( - 'Java_foo_interface', - 'Java_foo_interface.i', - LIBS = libadd, - LIBPATH = libpath, - SWIGFLAGS = swigflags, + 'Java_foo_interface', + 'Java_foo_interface.i', + LIBS = libadd, + LIBPATH = libpath, + SWIGFLAGS = swigflags, SWIGOUTDIR = Dir('build'), SWIGCXXFILESUFFIX = "_wrap.cpp") diff --git a/test/KeyboardInterrupt.py b/test/KeyboardInterrupt.py index bcb576c893..dbe1d14b75 100644 --- a/test/KeyboardInterrupt.py +++ b/test/KeyboardInterrupt.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that we handle keyboard interrupts (CTRL-C) correctly. @@ -62,7 +61,7 @@ def explode(env, target, source): handler = signal.getsignal(signal.SIGINT) handler(signal.SIGINT, None) - return 0 + return 0 else: # The platform does support process group so we use killpg to send # a SIGINT to everyone. @@ -77,11 +76,11 @@ def explode(env, target, source): all = [] for i in range(40): - all.extend(Object('toto%5d' % i, 'toto.c')) + all.extend(Object(f'toto{i:05}', 'toto.c')) -all.extend(Command( 'broken', 'toto.c', explode)) +all.extend(Command('broken', 'toto.c', explode)) -Default( Alias('all', all)) +Default(Alias('all', all)) """ ) @@ -95,10 +94,14 @@ def explode(env, target, source): def runtest(arguments): test.run(arguments='-c') - test.run(arguments=arguments, status=2, - stdout=interruptedStr, - stderr='.*scons: Build interrupted\\.', - match=TestSCons.match_re_dotall) + test.run( + arguments=arguments, + status=2, + stdout=interruptedStr, + stderr='.*scons: Build interrupted\\.', + match=TestSCons.match_re_dotall, + ) + for i in range(2): runtest('-j1') diff --git a/test/LEX/no_lex.py b/test/LEX/no_lex.py index e53b02e8e9..969dd3840f 100644 --- a/test/LEX/no_lex.py +++ b/test/LEX/no_lex.py @@ -46,7 +46,7 @@ def Detect(self, progs): DefaultEnvironment(tools=[]) foo = TestEnvironment(tools=['default', 'lex']) print(foo.Dictionary('LEX')) -""" % locals()) +""") test.run(arguments='-Q -s', stdout='None\n') diff --git a/test/MSVC/MSVC_SDK_VERSION.py b/test/MSVC/MSVC_SDK_VERSION.py index e5eae6762c..0f0e25a65b 100644 --- a/test/MSVC/MSVC_SDK_VERSION.py +++ b/test/MSVC/MSVC_SDK_VERSION.py @@ -31,6 +31,10 @@ from SCons.Tool.MSCommon.vc import get_installed_vcs_components from SCons.Tool.MSCommon import msvc_sdk_versions from SCons.Tool.MSCommon import msvc_toolset_versions +from SCons.Tool.MSCommon.MSVC.Kind import ( + msvc_version_is_express, + msvc_version_is_btdispatch, +) import TestSCons test = TestSCons.TestSCons() @@ -38,12 +42,28 @@ installed_versions = get_installed_vcs_components() default_version = installed_versions[0] -GE_VS2015_versions = [v for v in installed_versions if v.msvc_vernum >= 14.0] -LT_VS2015_versions = [v for v in installed_versions if v.msvc_vernum < 14.0] + +GE_VS2015_supported_versions = [] +GE_VS2015_unsupported_versions = [] +LT_VS2015_unsupported_versions = [] + +for v in installed_versions: + if v.msvc_vernum > 14.0: + GE_VS2015_supported_versions.append(v) + elif v.msvc_verstr == '14.0': + if msvc_version_is_express(v.msvc_version): + GE_VS2015_unsupported_versions.append((v, 'Express')) + elif msvc_version_is_btdispatch(v.msvc_version): + GE_VS2015_unsupported_versions.append((v, 'BTDispatch')) + else: + GE_VS2015_supported_versions.append(v) + else: + LT_VS2015_unsupported_versions.append(v) + default_sdk_versions_uwp = msvc_sdk_versions(version=None, msvc_uwp_app=True) default_sdk_versions_def = msvc_sdk_versions(version=None, msvc_uwp_app=False) -have_140 = any(v.msvc_verstr == '14.0' for v in GE_VS2015_versions) +have_140 = any(v.msvc_verstr == '14.0' for v in installed_versions) def version_major(version): components = version.split('.') @@ -64,9 +84,10 @@ def version_major_list(version_list): seen_major.add(major) return versions -if GE_VS2015_versions: +if GE_VS2015_supported_versions: - for supported in GE_VS2015_versions: + for supported in GE_VS2015_supported_versions: + # VS2017+ and VS2015 ('14.0') sdk_versions_uwp = msvc_sdk_versions(version=supported.msvc_version, msvc_uwp_app=True) sdk_versions_def = msvc_sdk_versions(version=supported.msvc_version, msvc_uwp_app=False) @@ -176,8 +197,8 @@ def version_major_list(version_list): """.format(repr(supported.msvc_version), repr(toolset_version)) )) test.run(arguments='-Q -s', status=2, stderr=None) - expect = "MSVCArgumentError: MSVC_SDK_VERSION ('8.1') and platform type ('UWP') constraint violation: toolset version {} > '14.0' VS2015:".format( - repr(toolset_version) + expect = "MSVCArgumentError: MSVC_SDK_VERSION ('8.1') and platform type ('UWP') constraint violation: toolset {} MSVC_VERSION {} > '14.0' VS2015:".format( + repr(toolset_version), repr(supported.msvc_version) ) test.must_contain_all(test.stderr(), expect) @@ -203,9 +224,37 @@ def version_major_list(version_list): )) test.run(arguments='-Q -s', stdout='') -if LT_VS2015_versions: +if GE_VS2015_unsupported_versions: + + for unsupported, kind_str in GE_VS2015_unsupported_versions: + # VS2015 Express + + sdk_version = default_sdk_versions_def[0] if default_sdk_versions_def else '8.1' + + test.write('SConstruct', textwrap.dedent( + """ + DefaultEnvironment(tools=[]) + env = Environment(MSVC_VERSION={}, MSVC_SDK_VERSION={}, tools=['msvc']) + """.format(repr(unsupported.msvc_version), repr(sdk_version)) + )) + test.run(arguments='-Q -s', status=2, stderr=None) + expect = "MSVCArgumentError: MSVC_SDK_VERSION ({}) is not supported for MSVC_VERSION {} ({}):".format( + repr(sdk_version), repr(unsupported.msvc_version), repr(kind_str) + ) + test.must_contain_all(test.stderr(), expect) + + # MSVC_SCRIPT_ARGS sdk_version is not validated + test.write('SConstruct', textwrap.dedent( + """ + DefaultEnvironment(tools=[]) + env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS={}, tools=['msvc']) + """.format(repr(unsupported.msvc_version), repr(sdk_version)) + )) + test.run(arguments='-Q -s', stdout='') + +if LT_VS2015_unsupported_versions: - for unsupported in LT_VS2015_versions: + for unsupported in LT_VS2015_unsupported_versions: # must be VS2015 or later sdk_version = default_sdk_versions_def[0] if default_sdk_versions_def else '8.1' diff --git a/test/MSVC/MSVC_TOOLSET_VERSION.py b/test/MSVC/MSVC_TOOLSET_VERSION.py index a20cf8a319..e43f4d7f80 100644 --- a/test/MSVC/MSVC_TOOLSET_VERSION.py +++ b/test/MSVC/MSVC_TOOLSET_VERSION.py @@ -28,6 +28,7 @@ """ import textwrap +import SCons.Tool.MSCommon.MSVC.Config as Config from SCons.Tool.MSCommon.vc import get_installed_vcs_components from SCons.Tool.MSCommon import msvc_toolset_versions @@ -38,10 +39,31 @@ installed_versions = get_installed_vcs_components() default_version = installed_versions[0] + GE_VS2017_versions = [v for v in installed_versions if v.msvc_vernum >= 14.1] LT_VS2017_versions = [v for v in installed_versions if v.msvc_vernum < 14.1] LT_VS2015_versions = [v for v in LT_VS2017_versions if v.msvc_vernum < 14.0] +known_buildseries = set(Config.MSVC_BUILDSERIES_EXTERNAL.keys()) + +def get_toolset_buildseries_version(toolset_version): + comps = toolset_version.split('.') + buildseries_version = comps[0] + '.' + comps[1][0] + vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL[buildseries_version] + vc_version = vc_buildseries_def.vc_version + return vc_version + +def get_latest_buildseries_version(v): + vs_def = Config.MSVC_VERSION_EXTERNAL[v.msvc_verstr] + vc_version = vs_def.vc_buildtools_def.vc_buildseries_list[0].vc_version + return vc_version + +def is_buildseries_known(toolset_version): + comps = toolset_version.split('.') + buildseries_version = comps[0] + '.' + comps[1][0] + rval = bool(buildseries_version in known_buildseries) + return rval + if GE_VS2017_versions: # VS2017 and later for toolset argument @@ -53,6 +75,11 @@ toolset_sxs_versions = msvc_toolset_versions(supported.msvc_version, full=False, sxs=True) toolset_sxs_version = toolset_sxs_versions[0] if toolset_sxs_versions else None + toolset_version = toolset_full_version if toolset_full_version else supported.msvc_version + buildseries_version = get_toolset_buildseries_version(toolset_version) + + latest_buildseries_version = get_latest_buildseries_version(supported) + if toolset_full_version: # toolset version using construction variable @@ -90,21 +117,21 @@ )) test.run(arguments='-Q -s', stdout='') - # msvc_version as toolset version + # build series version as toolset version test.write('SConstruct', textwrap.dedent( """ DefaultEnvironment(tools=[]) env = Environment(MSVC_VERSION={}, MSVC_TOOLSET_VERSION={}, tools=['msvc']) - """.format(repr(supported.msvc_version), repr(supported.msvc_verstr)) + """.format(repr(supported.msvc_version), repr(buildseries_version)) )) test.run(arguments='-Q -s', stdout='') - # msvc_version as toolset version using script argument + # build series version as toolset version using script argument test.write('SConstruct', textwrap.dedent( """ DefaultEnvironment(tools=[]) env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='-vcvars_ver={}', tools=['msvc']) - """.format(repr(supported.msvc_version), supported.msvc_verstr) + """.format(repr(supported.msvc_version), buildseries_version) )) test.run(arguments='-Q -s', stdout='') @@ -113,11 +140,11 @@ """ DefaultEnvironment(tools=[]) env = Environment(MSVC_VERSION={}, MSVC_TOOLSET_VERSION={}, MSVC_SCRIPT_ARGS='-vcvars_ver={}', tools=['msvc']) - """.format(repr(supported.msvc_version), repr(supported.msvc_verstr), supported.msvc_verstr) + """.format(repr(supported.msvc_version), repr(buildseries_version), buildseries_version) )) test.run(arguments='-Q -s', status=2, stderr=None) expect = "MSVCArgumentError: multiple toolset version declarations: MSVC_TOOLSET_VERSION={} and MSVC_SCRIPT_ARGS='-vcvars_ver={}':".format( - repr(supported.msvc_verstr), supported.msvc_verstr + repr(buildseries_version), buildseries_version ) test.must_contain_all(test.stderr(), expect) @@ -149,9 +176,19 @@ ) test.must_contain_all(test.stderr(), expect) - # msvc_toolset_version is invalid (version greater than msvc version) - invalid_toolset_vernum = round(supported.msvc_vernum + 0.1, 1) + # msvc_toolset_version is invalid (version not supported for msvc version) + invalid_toolset_vernum = round(float(latest_buildseries_version) + 0.1, 1) invalid_toolset_version = str(invalid_toolset_vernum) + + if is_buildseries_known(invalid_toolset_version): + expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} > {} MSVC_VERSION:".format( + repr(invalid_toolset_version), repr(invalid_toolset_version), repr(supported.msvc_version) + ) + else: + expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION {} build series {} is not supported:".format( + repr(invalid_toolset_version), repr(invalid_toolset_version) + ) + test.write('SConstruct', textwrap.dedent( """ DefaultEnvironment(tools=[]) @@ -159,9 +196,6 @@ """.format(repr(supported.msvc_version), repr(invalid_toolset_version)) )) test.run(arguments='-Q -s', status=2, stderr=None) - expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} > {} MSVC_VERSION:".format( - repr(invalid_toolset_version), repr(invalid_toolset_version), repr(supported.msvc_version) - ) test.must_contain_all(test.stderr(), expect) # msvc_toolset_version is invalid (version less than 14.0) @@ -173,7 +207,7 @@ """.format(repr(supported.msvc_version), repr(invalid_toolset_version)) )) test.run(arguments='-Q -s', status=2, stderr=None) - expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} < '14.0' VS2015:".format( + expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} < '14.0' VS2015:".format( repr(invalid_toolset_version), repr(invalid_toolset_version) ) test.must_contain_all(test.stderr(), expect) @@ -187,7 +221,7 @@ """.format(repr(supported.msvc_version), repr(invalid_toolset_version)) )) test.run(arguments='-Q -s', status=2, stderr=None) - expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} != '14.0':".format( + expect = "MSVCArgumentError: MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} != '14.0':".format( repr(invalid_toolset_version), repr(invalid_toolset_version) ) test.must_contain_all(test.stderr(), expect) diff --git a/test/MSVC/MSVC_USE_SCRIPT.py b/test/MSVC/MSVC_USE_SCRIPT.py index 67eddfc1e9..d6c683ffe5 100644 --- a/test/MSVC/MSVC_USE_SCRIPT.py +++ b/test/MSVC/MSVC_USE_SCRIPT.py @@ -35,8 +35,9 @@ test.skip_if_not_msvc() test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(tools=['msvc'], MSVC_USE_SCRIPT='nosuchscriptexists') -""" % locals()) +""") test.run(arguments = ".", status=2, stderr=None) test.must_contain_all(test.stderr(), "Script specified by MSVC_USE_SCRIPT not found") diff --git a/test/MSVC/MSVC_USE_SETTINGS.py b/test/MSVC/MSVC_USE_SETTINGS.py index fd6f85ceb5..a473862fa3 100644 --- a/test/MSVC/MSVC_USE_SETTINGS.py +++ b/test/MSVC/MSVC_USE_SETTINGS.py @@ -35,6 +35,7 @@ test.skip_if_not_msvc() test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) e1 = Environment() cl1 = e1.WhereIs('cl.exe') @@ -46,21 +47,21 @@ if cl1 == cl3: print("CL.EXE PATHS MATCH") -""" % locals()) +""") test.run(arguments=".", status=0, stderr=None) test.must_contain_all(test.stdout(), "CL.EXE PATHS MATCH") test.write('SConstruct', """ env = Environment(MSVC_USE_SETTINGS={}) -""" % locals()) +""") test.run(arguments="--warn=visual-c-missing .", status=0, stderr=None) test.must_contain_all(test.stderr(), "Could not find requested MSVC compiler 'cl'") test.write('SConstruct', """ env = Environment(MSVC_USE_SETTINGS='dict or None') -""" % locals()) +""") test.run(arguments=".", status=2, stderr=None) test.must_contain_all(test.stderr(), "MSVCUseSettingsError: MSVC_USE_SETTINGS type error") diff --git a/test/MSVC/MSVC_UWP_APP.py b/test/MSVC/MSVC_UWP_APP.py index 30b07ef771..fd52fd8f5e 100644 --- a/test/MSVC/MSVC_UWP_APP.py +++ b/test/MSVC/MSVC_UWP_APP.py @@ -30,14 +30,33 @@ import re from SCons.Tool.MSCommon.vc import get_installed_vcs_components +from SCons.Tool.MSCommon.vc import get_native_host_platform +from SCons.Tool.MSCommon.vc import _GE2022_HOST_TARGET_CFG +from SCons.Tool.MSCommon.MSVC.Kind import ( + msvc_version_is_express, + msvc_version_is_btdispatch, +) import TestSCons test = TestSCons.TestSCons() test.skip_if_not_msvc() installed_versions = get_installed_vcs_components() -GE_VS2015_versions = [v for v in installed_versions if v.msvc_vernum >= 14.0] -LT_VS2015_versions = [v for v in installed_versions if v.msvc_vernum < 14.0] + +GE_VS2015_supported_versions = [] +GE_VS2015_unsupported_versions = [] +LT_VS2015_unsupported_versions = [] + +for v in installed_versions: + if v.msvc_vernum > 14.0: + GE_VS2015_supported_versions.append(v) + elif v.msvc_verstr == '14.0': + if msvc_version_is_btdispatch(v.msvc_version): + GE_VS2015_unsupported_versions.append((v, 'BTDispatch')) + else: + GE_VS2015_supported_versions.append(v) + else: + LT_VS2015_unsupported_versions.append(v) # Look for the Store VC Lib paths in the LIBPATH: # [VS install path]\VC\LIB\store[\arch] and @@ -46,32 +65,52 @@ # C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\store\amd64 # C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\store\references +# By default, 2015 Express supports the store argument only for x86 targets. +# Using MSVC_SCRIPT_ARGS to set the store argument is not validated and +# will result in the store paths not found on 64-bit hosts when using the +# default target architecture. + +# By default, 2015 BTDispatch silently ignores the store argument. +# Using MSVC_SCRIPT_ARGS to set the store argument is not validated and +# will result in the store paths not found. + +re_lib_eq2015exp_1 = re.compile(r'\\vc\\lib\\store', re.IGNORECASE) + re_lib_eq2015_1 = re.compile(r'\\vc\\lib\\store\\references', re.IGNORECASE) re_lib_eq2015_2 = re.compile(r'\\vc\\lib\\store', re.IGNORECASE) re_lib_ge2017_1 = re.compile(r'\\lib\\x86\\store\\references', re.IGNORECASE) re_lib_ge2017_2 = re.compile(r'\\lib\\x64\\store', re.IGNORECASE) - def check_libpath(msvc, active, output): + def _check_libpath(msvc, output): + is_supported = True outdict = {key.strip(): val.strip() for key, val in [line.split('|') for line in output.splitlines()]} platform = outdict.get('PLATFORM', '') libpath = outdict.get('LIBPATH', '') + uwpsupported = outdict.get('UWPSUPPORTED', '') + if uwpsupported and uwpsupported.split('|')[-1] == '0': + is_supported = False n_matches = 0 if msvc.msvc_verstr == '14.0': + if msvc_version_is_express(msvc.msvc_version): + for regex in (re_lib_eq2015exp_1,): + if regex.search(libpath): + n_matches += 1 + return n_matches > 0, 'store', libpath, is_supported for regex in (re_lib_eq2015_1, re_lib_eq2015_2): if regex.search(libpath): n_matches += 1 - return n_matches >= 2, 'store', libpath + return n_matches >= 2, 'store', libpath, is_supported elif platform == 'UWP': for regex in (re_lib_ge2017_1, re_lib_ge2017_2): if regex.search(libpath): n_matches += 1 - return n_matches > 0, 'uwp', libpath - return False, 'uwp', libpath + return n_matches > 0, 'uwp', libpath, is_supported + return False, 'uwp', libpath, is_supported - found, kind, libpath = _check_libpath(msvc, output) + found, kind, libpath, is_supported = _check_libpath(msvc, output) failmsg = None @@ -84,11 +123,13 @@ def _check_libpath(msvc, output): repr(msvc.msvc_version), repr(kind), repr(libpath) ) - return failmsg + return failmsg, is_supported -if GE_VS2015_versions: +if GE_VS2015_supported_versions: # VS2015 and later for uwp/store argument - for supported in GE_VS2015_versions: + + for supported in GE_VS2015_supported_versions: + for msvc_uwp_app in (True, '1', False, '0', None): active = msvc_uwp_app in (True, '1') @@ -102,7 +143,7 @@ def _check_libpath(msvc, output): """.format(repr(supported.msvc_version), repr(msvc_uwp_app)) )) test.run(arguments='-Q -s', stdout=None) - failmsg = check_libpath(supported, active, test.stdout()) + failmsg, _ = check_libpath(supported, active, test.stdout()) if failmsg: test.fail_test(message=failmsg) @@ -120,23 +161,77 @@ def _check_libpath(msvc, output): if not test.stderr().strip().startswith("MSVCArgumentError: multiple uwp declarations:"): test.fail_test(message='Expected MSVCArgumentError') - # uwp using script argument + if supported.msvc_verstr == '14.0' and msvc_version_is_express(supported.msvc_version): + + # uwp using script argument may not be supported for default target architecture + test.write('SConstruct', textwrap.dedent( + """ + from SCons.Tool.MSCommon.MSVC.Kind import msvc_version_uwp_is_supported + DefaultEnvironment(tools=[]) + env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc']) + is_supported, _ = msvc_version_uwp_is_supported(env['MSVC_VERSION'], env['TARGET_ARCH']) + uwpsupported = '1' if is_supported else '0' + print('LIBPATH|' + env['ENV'].get('LIBPATH', '')) + print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat','')) + print('UWPSUPPORTED|' + uwpsupported) + """.format(repr(supported.msvc_version)) + )) + test.run(arguments='-Q -s', stdout=None) + + else: + + # uwp using script argument + test.write('SConstruct', textwrap.dedent( + """ + DefaultEnvironment(tools=[]) + env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc']) + print('LIBPATH|' + env['ENV'].get('LIBPATH', '')) + print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat','')) + """.format(repr(supported.msvc_version)) + )) + test.run(arguments='-Q -s', stdout=None) + + failmsg, is_supported = check_libpath(supported, True, test.stdout()) + if failmsg and is_supported: + test.fail_test(message=failmsg) + +if GE_VS2015_unsupported_versions: + # VS2015 and later for uwp/store error + + for unsupported, kind_str in GE_VS2015_unsupported_versions: + + for msvc_uwp_app in (True, '1'): + + # uwp using construction variable + test.write('SConstruct', textwrap.dedent( + """ + DefaultEnvironment(tools=[]) + env = Environment(MSVC_VERSION={}, MSVC_UWP_APP={}, tools=['msvc']) + """.format(repr(unsupported.msvc_version), repr(msvc_uwp_app)) + )) + test.run(arguments='-Q -s', status=2, stderr=None) + expect = "MSVCArgumentError: MSVC_UWP_APP ({}) is not supported for MSVC_VERSION {} ({}):".format( + repr(msvc_uwp_app), repr(unsupported.msvc_version), repr(kind_str) + ) + test.must_contain_all(test.stderr(), expect) + + # MSVC_SCRIPT_ARGS store is not validated test.write('SConstruct', textwrap.dedent( """ DefaultEnvironment(tools=[]) env = Environment(MSVC_VERSION={}, MSVC_SCRIPT_ARGS='store', tools=['msvc']) - print('LIBPATH|' + env['ENV'].get('LIBPATH', '')) - print('PLATFORM|' + env['ENV'].get('VSCMD_ARG_app_plat','')) - """.format(repr(supported.msvc_version)) + """.format(repr(unsupported.msvc_version)) )) - test.run(arguments='-Q -s', stdout=None) - failmsg = check_libpath(supported, True, test.stdout()) - if failmsg: - test.fail_test(message=failmsg) + test.run(arguments='-Q -s', stdout='') + failmsg, _ = check_libpath(unsupported, True, test.stdout()) + if not failmsg: + test.fail_test(message='unexpected: store found in libpath') -if LT_VS2015_versions: +if LT_VS2015_unsupported_versions: # VS2013 and earlier for uwp/store error - for unsupported in LT_VS2015_versions: + + for unsupported in LT_VS2015_unsupported_versions: + for msvc_uwp_app in (True, '1', False, '0', None): active = msvc_uwp_app in (True, '1') diff --git a/test/MSVC/PCH-source.py b/test/MSVC/PCH-source.py index 84a39bd78b..99683171b7 100644 --- a/test/MSVC/PCH-source.py +++ b/test/MSVC/PCH-source.py @@ -41,7 +41,7 @@ env['PCH'] = env.PCH('Source1.cpp')[0] env['PCHSTOP'] = 'Header1.hpp' env.Program('foo', ['foo.cpp', 'Source2.cpp', 'Source1.cpp']) -""" % locals()) +""") test.write('Header1.hpp', r""" """) diff --git a/test/MSVC/TARGET_ARCH.py b/test/MSVC/TARGET_ARCH.py index d8a2f933e7..f093a425d8 100644 --- a/test/MSVC/TARGET_ARCH.py +++ b/test/MSVC/TARGET_ARCH.py @@ -38,7 +38,7 @@ DefaultEnvironment(tools=[]) env_64 = Environment(tools=['default', 'msvc'], TARGET_ARCH='amd64') env_32 = Environment(tools=['default', 'msvc'], TARGET_ARCH='x86') -""" % locals()) +""") test.run(arguments=".") @@ -47,7 +47,7 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) env_xx = Environment(tools=['default', 'msvc'], TARGET_ARCH='nosucharch') -""" % locals()) +""") test.run(arguments=".", status=2, stderr=None) test.must_contain_any_line(test.stderr(), "Unrecognized target architecture") @@ -58,7 +58,7 @@ env = Environment(tools=['default', 'msvc'], TARGET_ARCH='arm', MSVC_VERSION='11.0') if env.Detect('cl'): env.Command('checkarm', [], 'cl') -""" % locals()) +""") test.run(arguments=".", stderr=None) if test.stderr().strip() != "" and "ARM" not in test.stderr(): test.fail_test() @@ -68,7 +68,7 @@ env = Environment(tools=['default', 'msvc'], TARGET_ARCH='arm64', MSVC_VERSION='11.0') if env.Detect('cl'): env.Command('checkarm64', [], 'cl') -""" % locals()) +""") test.run(arguments=".", stderr=None) if test.stderr().strip() != "" and "ARM64" not in test.stderr(): test.fail_test() diff --git a/test/MSVC/VSWHERE-fixture/SConstruct b/test/MSVC/VSWHERE-fixture/SConstruct index 74eea18c16..0346f802e0 100644 --- a/test/MSVC/VSWHERE-fixture/SConstruct +++ b/test/MSVC/VSWHERE-fixture/SConstruct @@ -5,12 +5,16 @@ import os import os.path -from SCons.Tool.MSCommon.vc import VSWHERE_PATHS +import SCons.Tool.MSCommon as mscommon +import SCons.Tool.MSCommon.vc as vc # Dump out expected paths -for vw_path in VSWHERE_PATHS: - print("VSWHERE_PATH=%s" % vw_path) - +for exegroup in ( + vc._VSWHERE_EXEGROUP_MSVS, + vc._VSWHERE_EXEGROUP_PKGMGR +): + for vw_path in exegroup: + print("VSWHERE_PATH=%s" % vw_path) # Allow normal detection logic to find vswhere.exe DefaultEnvironment(tools=[]) @@ -21,6 +25,19 @@ print("VSWHERE-detect=%s" % env1['VSWHERE']) v_local = os.path.join(os.getcwd(), 'vswhere.exe') Execute(Copy(os.path.join(os.getcwd(), 'vswhere.exe'), env1['VSWHERE'])) +# Reset vswhere executable manager +# A vswhere.exe not equal to the vswhere.exe for initial detection is an error +vc._VSWhereExecutable.reset() + # With VSWHERE set to copied vswhere.exe (see above), find vswhere.exe env = Environment(VSWHERE=v_local) print("VSWHERE-env=%s" % env['VSWHERE']) + +# Reset vswhere executable manager +# A vswhere.exe not equal to the vswhere.exe for initial detection is an error +vc._VSWhereExecutable.reset() + +# With VSWHERE set to copied vswhere.exe (see above), find vswhere.exe +mscommon.vswhere_register_executable(v_local, priority='high') +env = Environment() +print("VSWHERE-util=%s" % env['VSWHERE']) diff --git a/test/MSVC/VSWHERE.py b/test/MSVC/VSWHERE.py index 8212415f37..b38ae5f2da 100644 --- a/test/MSVC/VSWHERE.py +++ b/test/MSVC/VSWHERE.py @@ -28,6 +28,7 @@ Also test that vswhere.exe is found and sets VSWHERE to the correct values """ import os.path +import SCons.Tool.MSCommon import TestSCons _python_ = TestSCons._python_ @@ -36,6 +37,10 @@ test.skip_if_not_msvc() test.verbose_set(1) +_default_vc = SCons.Tool.MSCommon.vc.get_installed_vcs_components()[0] +if _default_vc.msvc_vernum < 14.1: + test.skip_test("no installed msvc requires vswhere.exe; skipping test\n") + test.dir_fixture('VSWHERE-fixture') test.run(arguments=".") @@ -57,11 +62,14 @@ detected_path = l.strip().split('=')[-1] elif 'VSWHERE-env' in l: env_path = l.strip().split('=')[-1] + elif 'VSWHERE-util' in l: + util_path = l.strip().split('=')[-1] # Debug code # print("VPP:%s" % default_locations) # print("V-D:%s" % detected_path) # print("V-E:%s" % env_path) +# print("V-U:%s" % util_path) test.fail_test( @@ -82,6 +90,12 @@ message='VSWHERE not\n\t%s\n\t but\n\t%s' % (expected_env_path, env_path), ) +expected_util_path = os.path.join(test.workdir, 'vswhere.exe') +test.fail_test( + util_path != expected_env_path, + message='VSWHERE not\n\t%s\n\t but\n\t%s' % (expected_util_path, util_path), +) + test.pass_test() # here for reference, unused @@ -94,6 +108,7 @@ VSWHERE-detect=C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe Copy("C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe", "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe") VSWHERE-env=C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe +VSWHERE-util=C:\Users\Bill\AppData\Local\Temp\testcmd.11256.1ae1_as5\vswhere.exe scons: done reading SConscript files. scons: Building targets ... scons: `.' is up to date. diff --git a/test/MSVC/batch-longlines.py b/test/MSVC/batch-longlines.py index addf001806..0e78fd5ae6 100644 --- a/test/MSVC/batch-longlines.py +++ b/test/MSVC/batch-longlines.py @@ -45,7 +45,7 @@ env = Environment(tools=['msvc', 'mslink'], MSVC_BATCH=ARGUMENTS.get('MSVC_BATCH')) env.SharedLibrary('mylib', Glob('source*.cxx')) -""" % locals()) +""") test.run(arguments='MSVC_BATCH=1 .') test.must_exist('mylib.dll') diff --git a/test/MSVC/msvc.py b/test/MSVC/msvc.py index 6bc36759a2..e81ecea7e8 100644 --- a/test/MSVC/msvc.py +++ b/test/MSVC/msvc.py @@ -116,7 +116,7 @@ # TODO: Reevaluate if having this part of the test makes sense any longer # using precompiled headers should be faster -limit = slow*0.90 +limit = slow*1.00 if fast >= limit: print("Using precompiled headers was not fast enough:") print("slow.obj: %.3fs" % slow) diff --git a/test/MSVC/msvc_cache_force_defaults.py b/test/MSVC/msvc_cache_force_defaults.py index e0ed1c3543..ad67304d56 100644 --- a/test/MSVC/msvc_cache_force_defaults.py +++ b/test/MSVC/msvc_cache_force_defaults.py @@ -30,6 +30,10 @@ import textwrap from SCons.Tool.MSCommon.vc import get_installed_vcs_components +from SCons.Tool.MSCommon.MSVC.Kind import ( + msvc_version_is_express, + msvc_version_is_btdispatch, +) import TestSCons test = TestSCons.TestSCons() @@ -68,8 +72,15 @@ test.run(arguments="-Q -s", status=0, stdout=None) cache_arg = test.stdout().strip() if default_version.msvc_verstr == '14.0': - # VS2015: target_arch msvc_sdk_version - expect = r'^SCRIPT_ARGS: .* [0-9.]+$' + if msvc_version_is_express(default_version.msvc_version): + # VS2015 Express: target_arch + expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+$' + elif msvc_version_is_btdispatch(default_version.msvc_version): + # VS2015 BTDispatch: target_arch + expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+$' + else: + # VS2015: target_arch msvc_sdk_version + expect = r'^SCRIPT_ARGS: [a-zA-Z0-9_]+ [0-9.]+$' else: # VS2017+ msvc_sdk_version msvc_toolset_version expect = r'^SCRIPT_ARGS: [0-9.]+ -vcvars_ver=[0-9.]+$' diff --git a/test/MSVC/no_msvc.py b/test/MSVC/no_msvc.py index 35cce9258d..aa9e2b8e57 100644 --- a/test/MSVC/no_msvc.py +++ b/test/MSVC/no_msvc.py @@ -75,8 +75,7 @@ def exists(env): # test no msvc's and msvc_query_version_toolset() call test.file_fixture('no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py', 'SConstruct') -test.run(arguments='-Q -s') -test.must_contain_all(test.stdout(), 'msvc_version=None, msvc_toolset_version=None') +test.run(arguments='-Q -s', status=2, stderr=r"^.*MSVCToolsetVersionNotFound.+", match=TestSCons.match_re_dotall) test.pass_test() diff --git a/test/MSVC/query_vcbat.py b/test/MSVC/query_vcbat.py index 6e30706b89..28bbd02dc3 100644 --- a/test/MSVC/query_vcbat.py +++ b/test/MSVC/query_vcbat.py @@ -51,7 +51,7 @@ # print k, v #MergeMSVSBatFile(env, 9.0) #print(env['ENV']['PATH']) -print(query_versions()) +print(query_versions(env=None)) """) test.run(stderr=None) diff --git a/test/MSVS/common-prefix.py b/test/MSVS/common-prefix.py index ea95c032a0..070ce0d0e4 100644 --- a/test/MSVS/common-prefix.py +++ b/test/MSVS/common-prefix.py @@ -38,6 +38,8 @@ msg = "Skipping Visual Studio test on non-Windows platform '%s'\n" % sys.platform test.skip_test(msg) +project_guid = TestSConsMSVS.PROJECT_GUID + vcproj_template = """\ """ - - SConscript_contents = """\ env=Environment(tools=['msvs'], MSVS_VERSION = '8.0') testsrc = %(testsrc)s env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID = '%(project_guid)s', slnguid = '{SLNGUID}', srcs = testsrc, buildtarget = 'Test.exe', @@ -98,8 +99,6 @@ auto_build_solution = 0) """ - - test.subdir('work1') testsrc = repr([ @@ -142,8 +141,6 @@ # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) - - test.subdir('work2') testsrc = repr([ @@ -170,8 +167,6 @@ # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) - - test.pass_test() # Local Variables: diff --git a/test/MSVS/runfile.py b/test/MSVS/runfile.py index d2319f330a..08c8f31633 100644 --- a/test/MSVS/runfile.py +++ b/test/MSVS/runfile.py @@ -38,6 +38,8 @@ msg = "Skipping Visual Studio test on non-Windows platform '%s'\n" % sys.platform test.skip_test(msg) +sconscript_dict = {'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} + expected_vcprojfile = """\ """ - - SConscript_contents = """\ env=Environment(tools=['msvs'], MSVS_VERSION = '8.0') env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID = '%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = ['test.cpp'], buildtarget = 'Test.exe', @@ -104,11 +105,9 @@ auto_build_solution = 0) """ - - test.subdir('work1') -test.write(['work1', 'SConstruct'], SConscript_contents) +test.write(['work1', 'SConstruct'], SConscript_contents % sconscript_dict) test.run(chdir='work1', arguments="Test.vcproj") @@ -118,8 +117,6 @@ # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) - - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-6.0-clean.py b/test/MSVS/vs-6.0-clean.py index 0cbadba2f4..3f99d650d3 100644 --- a/test/MSVS/vs-6.0-clean.py +++ b/test/MSVS/vs-6.0-clean.py @@ -34,21 +34,16 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - - # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['6.0'] - - expected_dspfile = TestSConsMSVS.expected_dspfile_6_0 expected_dswfile = TestSConsMSVS.expected_dswfile_6_0 - - test.write('SConstruct', """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='6.0',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='6.0', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test.c'] testincs = ['sdk.h'] @@ -95,16 +90,14 @@ test.must_exist(test.workpath('Test.dsp')) test.must_exist(test.workpath('Test.dsw')) -test.run(arguments='-c Test.dsw') - -test.must_exist(test.workpath('Test.dsp')) -test.must_not_exist(test.workpath('Test.dsw')) - test.run(arguments='-c Test.dsp') test.must_not_exist(test.workpath('Test.dsp')) +test.must_exist(test.workpath('Test.dsw')) +test.run(arguments='-c Test.dsw') +test.must_not_exist(test.workpath('Test.dsw')) test.pass_test() diff --git a/test/MSVS/vs-7.0-clean.py b/test/MSVS/vs-7.0-clean.py index 1194cc1fac..627c5addb9 100644 --- a/test/MSVS/vs-7.0-clean.py +++ b/test/MSVS/vs-7.0-clean.py @@ -34,20 +34,16 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.0'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_0 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_0 - - test.write('SConstruct', """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='7.0',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='7.0', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -56,6 +52,7 @@ testmisc = ['readme.txt'] p = env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', srcs = testsrc, incs = testincs, localincs = testlocalincs, @@ -69,7 +66,7 @@ slnguid = '{SLNGUID}', projects = [p], variant = 'Release') -"""%{'HOST_ARCH':host_arch}) +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID}) test.run(arguments=".") @@ -95,16 +92,13 @@ test.must_exist(test.workpath('Test.vcproj')) test.must_exist(test.workpath('Test.sln')) -test.run(arguments='-c Test.sln') - -test.must_exist(test.workpath('Test.vcproj')) -test.must_not_exist(test.workpath('Test.sln')) - test.run(arguments='-c Test.vcproj') - test.must_not_exist(test.workpath('Test.vcproj')) +test.must_exist(test.workpath('Test.sln')) +test.run(arguments='-c Test.sln') +test.must_not_exist(test.workpath('Test.sln')) test.pass_test() diff --git a/test/MSVS/vs-7.0-files.py b/test/MSVS/vs-7.0-files.py index 9dc33b70d5..202036ce90 100644 --- a/test/MSVS/vs-7.0-files.py +++ b/test/MSVS/vs-7.0-files.py @@ -35,20 +35,16 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - +sconscript_dict = {'HOST_ARCH': host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.0'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_0 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_0 SConscript_contents = TestSConsMSVS.SConscript_contents_7_0 - - -test.write('SConstruct', SConscript_contents%{'HOST_ARCH': host_arch}) +test.write('SConstruct', SConscript_contents % sconscript_dict) test.run(arguments="Test.vcproj") @@ -79,8 +75,6 @@ test.must_not_exist(test.workpath('Test.vcproj')) test.must_not_exist(test.workpath('Test.sln')) - - # Test that running SCons with $PYTHON_ROOT in the environment # changes the .vcproj output as expected. os.environ['PYTHON_ROOT'] = 'xyzzy' @@ -95,8 +89,6 @@ # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) - - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.0-scc-files.py b/test/MSVS/vs-7.0-scc-files.py index f1f77db2e9..2c5c520e60 100644 --- a/test/MSVS/vs-7.0-scc-files.py +++ b/test/MSVS/vs-7.0-scc-files.py @@ -33,22 +33,23 @@ import TestSConsMSVS test = TestSConsMSVS.TestSConsMSVS() +host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.0'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_0 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_0 SConscript_contents = \ -r"""env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='7.0', - CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], - CPPPATH=['inc1', 'inc2'], - MSVS_SCC_CONNECTION_ROOT='.', - MSVS_SCC_PROVIDER='MSSCCI:Perforce SCM', - MSVS_SCC_PROJECT_NAME='Perforce Project', - MSVS_SCC_AUX_PATH='AUX') +r"""env=Environment(tools=['msvs'], + MSVS_VERSION='7.0', + CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], + CPPPATH=['inc1', 'inc2'], + MSVS_SCC_CONNECTION_ROOT='.', + MSVS_SCC_PROVIDER='MSSCCI:Perforce SCM', + MSVS_SCC_PROJECT_NAME='Perforce Project', + MSVS_SCC_AUX_PATH='AUX', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -57,6 +58,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -65,7 +67,7 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""" +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} expected_sln_sccinfo = """\ \tGlobalSection(SourceCodeControl) = preSolution @@ -89,7 +91,6 @@ \tSccProvider="MSSCCI:Perforce SCM" """ - test.write('SConstruct', SConscript_contents) test.run(arguments="Test.vcproj") @@ -108,7 +109,6 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.0-scc-legacy-files.py b/test/MSVS/vs-7.0-scc-legacy-files.py index a180b8ad6a..00ce397d45 100644 --- a/test/MSVS/vs-7.0-scc-legacy-files.py +++ b/test/MSVS/vs-7.0-scc-legacy-files.py @@ -33,20 +33,21 @@ import TestSConsMSVS test = TestSConsMSVS.TestSConsMSVS() +host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.0'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_0 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_0 SConscript_contents = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='7.0', +env=Environment(tools=['msvs'], + MSVS_VERSION='7.0', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], MSVS_SCC_LOCAL_PATH=r'C:\\MyMsVsProjects', - MSVS_SCC_PROJECT_NAME='Perforce Project') + MSVS_SCC_PROJECT_NAME='Perforce Project', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -55,6 +56,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -63,14 +65,13 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""" +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} expected_vcproj_sccinfo = """\ \tSccProjectName="Perforce Project" \tSccLocalPath="C:\\MyMsVsProjects" """ - test.write('SConstruct', SConscript_contents) test.run(arguments="Test.vcproj") @@ -88,7 +89,6 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.0-variant_dir.py b/test/MSVS/vs-7.0-variant_dir.py index 41c3cdcec2..f83d0abe5e 100644 --- a/test/MSVS/vs-7.0-variant_dir.py +++ b/test/MSVS/vs-7.0-variant_dir.py @@ -33,40 +33,33 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - +sconscript_dict = {'HOST_ARCH': host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.0'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_0 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_0 SConscript_contents = TestSConsMSVS.SConscript_contents_7_0 - - test.subdir('src') test.write('SConstruct', """\ SConscript('src/SConscript', variant_dir='build') """) -test.write(['src', 'SConscript'], SConscript_contents%{'HOST_ARCH': host_arch}) +test.write(['src', 'SConscript'], SConscript_contents % sconscript_dict) test.run(arguments=".") -project_guid = "{CB4637F1-2205-50B7-B115-DCFA0DA68FB1}" vcproj = test.read(['src', 'Test.vcproj'], 'r') -expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct', - project_guid=project_guid) +expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct') # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') -expect = test.msvs_substitute(expected_slnfile, '7.0', 'src', - project_guid=project_guid) +expect = test.msvs_substitute(expected_slnfile, '7.0', 'src') # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) @@ -84,8 +77,6 @@ """ % test.workpath('src', 'Test.sln'), mode='r') - - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.1-clean.py b/test/MSVS/vs-7.1-clean.py index 8c9cb87894..403463e0bb 100644 --- a/test/MSVS/vs-7.1-clean.py +++ b/test/MSVS/vs-7.1-clean.py @@ -34,20 +34,16 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.1'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_1 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_1 - - test.write('SConstruct', """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='7.1',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='7.1', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -56,6 +52,7 @@ testmisc = ['readme.txt'] p = env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', srcs = testsrc, incs = testincs, localincs = testlocalincs, @@ -69,7 +66,7 @@ slnguid = '{SLNGUID}', projects = [p], variant = 'Release') -"""%{'HOST_ARCH':host_arch}) +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID}) test.run(arguments=".") @@ -95,16 +92,20 @@ test.must_exist(test.workpath('Test.vcproj')) test.must_exist(test.workpath('Test.sln')) -test.run(arguments='-c Test.sln') - -test.must_exist(test.workpath('Test.vcproj')) -test.must_not_exist(test.workpath('Test.sln')) - test.run(arguments='-c Test.vcproj') test.must_not_exist(test.workpath('Test.vcproj')) +test.must_exist(test.workpath('Test.sln')) + +test.run(arguments='.') +test.must_exist(test.workpath('Test.vcproj')) +test.must_exist(test.workpath('Test.sln')) +test.run(arguments='-c Test.sln') + +test.must_not_exist(test.workpath('Test.vcproj')) +test.must_not_exist(test.workpath('Test.sln')) test.pass_test() diff --git a/test/MSVS/vs-7.1-files.py b/test/MSVS/vs-7.1-files.py index e2a40a813d..9fcea6d5f6 100644 --- a/test/MSVS/vs-7.1-files.py +++ b/test/MSVS/vs-7.1-files.py @@ -35,20 +35,16 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - +sconscript_dict = {'HOST_ARCH': host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.1'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_1 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_1 SConscript_contents = TestSConsMSVS.SConscript_contents_7_1 - - -test.write('SConstruct', SConscript_contents%{'HOST_ARCH': host_arch}) +test.write('SConstruct', SConscript_contents % sconscript_dict) test.run(arguments="Test.vcproj") @@ -79,8 +75,6 @@ test.must_not_exist(test.workpath('Test.vcproj')) test.must_not_exist(test.workpath('Test.sln')) - - # Test that running SCons with $PYTHON_ROOT in the environment # changes the .vcproj output as expected. os.environ['PYTHON_ROOT'] = 'xyzzy' @@ -95,8 +89,6 @@ # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) - - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.1-scc-files.py b/test/MSVS/vs-7.1-scc-files.py index 0b42930b51..4f253e7172 100644 --- a/test/MSVS/vs-7.1-scc-files.py +++ b/test/MSVS/vs-7.1-scc-files.py @@ -33,22 +33,23 @@ import TestSConsMSVS test = TestSConsMSVS.TestSConsMSVS() +host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.1'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_1 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_1 SConscript_contents = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='7.1', +env=Environment(tools=['msvs'], + MSVS_VERSION='7.1', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], MSVS_SCC_CONNECTION_ROOT='.', MSVS_SCC_PROVIDER='MSSCCI:Perforce SCM', MSVS_SCC_PROJECT_NAME='Perforce Project', - MSVS_SCC_AUX_PATH='AUX') + MSVS_SCC_AUX_PATH='AUX', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -57,6 +58,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -65,7 +67,7 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""" +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} expected_sln_sccinfo = """\ \tGlobalSection(SourceCodeControl) = preSolution @@ -89,7 +91,6 @@ \tSccProvider="MSSCCI:Perforce SCM" """ - test.write('SConstruct', SConscript_contents) test.run(arguments="Test.vcproj") @@ -108,7 +109,6 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.1-scc-legacy-files.py b/test/MSVS/vs-7.1-scc-legacy-files.py index bb184d6dec..96ec21d67f 100644 --- a/test/MSVS/vs-7.1-scc-legacy-files.py +++ b/test/MSVS/vs-7.1-scc-legacy-files.py @@ -33,20 +33,21 @@ import TestSConsMSVS test = TestSConsMSVS.TestSConsMSVS() +host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.1'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_1 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_1 SConscript_contents = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='7.1', +env=Environment(tools=['msvs'], + MSVS_VERSION='7.1', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], MSVS_SCC_LOCAL_PATH=r'C:\\MyMsVsProjects', - MSVS_SCC_PROJECT_NAME='Perforce Project') + MSVS_SCC_PROJECT_NAME='Perforce Project', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -55,6 +56,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID='%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -63,14 +65,13 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""" +""" % {'HOST_ARCH':host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} expected_vcproj_sccinfo = """\ \tSccProjectName="Perforce Project" \tSccLocalPath="C:\\MyMsVsProjects" """ - test.write('SConstruct', SConscript_contents) test.run(arguments="Test.vcproj") @@ -88,7 +89,6 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-7.1-variant_dir.py b/test/MSVS/vs-7.1-variant_dir.py index 64467def53..df337ae41e 100644 --- a/test/MSVS/vs-7.1-variant_dir.py +++ b/test/MSVS/vs-7.1-variant_dir.py @@ -33,40 +33,33 @@ test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() - +sconscript_dict = {'HOST_ARCH': host_arch, 'PROJECT_GUID': TestSConsMSVS.PROJECT_GUID} # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = ['7.1'] - - expected_slnfile = TestSConsMSVS.expected_slnfile_7_1 expected_vcprojfile = TestSConsMSVS.expected_vcprojfile_7_1 SConscript_contents = TestSConsMSVS.SConscript_contents_7_1 - - test.subdir('src') test.write('SConstruct', """\ SConscript('src/SConscript', variant_dir='build') """) -test.write(['src', 'SConscript'], SConscript_contents%{'HOST_ARCH': host_arch}) +test.write(['src', 'SConscript'], SConscript_contents % sconscript_dict) test.run(arguments=".") -project_guid = "{CB4637F1-2205-50B7-B115-DCFA0DA68FB1}" vcproj = test.read(['src', 'Test.vcproj'], 'r') -expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct', - project_guid=project_guid) +expect = test.msvs_substitute(expected_vcprojfile, '7.0', None, 'SConstruct') # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') -expect = test.msvs_substitute(expected_slnfile, '7.0', 'src', - project_guid=project_guid) +expect = test.msvs_substitute(expected_slnfile, '7.0', 'src') # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) @@ -84,8 +77,6 @@ """ % test.workpath('src', 'Test.sln'), mode='r') - - test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-files.py b/test/MSVS/vs-files.py index b330ce726f..54933565e4 100644 --- a/test/MSVS/vs-files.py +++ b/test/MSVS/vs-files.py @@ -33,6 +33,8 @@ import TestSConsMSVS +test = None + for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() @@ -103,6 +105,9 @@ # don't compare the pickled data assert vcxproj[:len(expect)] == expect, test.diff_substr(expect, vcxproj) + del os.environ['PYTHON_ROOT'] + +if test: test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-mult-auto-guid.py b/test/MSVS/vs-mult-auto-guid.py new file mode 100644 index 0000000000..412c379ab7 --- /dev/null +++ b/test/MSVS/vs-mult-auto-guid.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and three solution (.sln) file. +""" + +import TestSConsMSVS + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + + for autofilter_projects in (None, False, True): + + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + solution_file_1 = 'Test_1.sln' + solution_file_2 = 'Test_2.sln' + + if major >= 10: + project_guid_1 = "{F7D7CE55-37BF-51DE-8942-9377B2BE8387}" + project_guid_2 = "{8D17BC73-09FD-5B69-BBBF-1E40E0C63456}" + else: + project_guid_1 = "{53EE9FA7-6300-55B8-8A0E-A3DC40983390}" + project_guid_2 = "{57358E9B-126D-53F6-AD5A-559AB4A8EE62}" + + if major >= 10: + solution_guid_1 = "{73ABD7C4-0A64-5192-97C3-252B70C8D1B1}" + solution_guid_2 = "{142F0D4F-9896-5808-AAB4-A80F5661B1FE}" + else: + solution_guid_1 = "{73ABD7C4-0A64-5192-97C3-252B70C8D1B1}" + solution_guid_2 = "{142F0D4F-9896-5808-AAB4-A80F5661B1FE}" + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + have_solution_project_nodes=True, + autofilter_solution_project_nodes=autofilter_projects, + ) + + expected_slnfile_1 = test.get_expected_sln_file_contents( + vc_version, + project_file_1, + ) + + expected_slnfile_2 = test.get_expected_sln_file_contents( + vc_version, + project_file_2, + ) + + SConstruct_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=1, + autofilter_projects=autofilter_projects, + default_guids=True, + ) + + test.write('SConstruct', SConstruct_contents) + + if autofilter_projects is None: + test.run(arguments=".", status=2, stderr=r"^.*scons: [*]{3} An msvs solution file was detected in the MSVSSolution 'projects' argument:.+", match=test.match_re_dotall) + continue + + test.run(arguments=".") + + test.must_exist(test.workpath(project_file_1)) + vcproj = test.read(project_file_1, 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(project_file_2)) + vcproj = test.read(project_file_2, 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(solution_file)) + sln = test.read(solution_file, 'r') + expect = test.msvs_substitute_projects( + expected_slnfile, subdir='src', + project_guid_1=project_guid_1, project_guid_2=project_guid_2, + solution_guid_1=solution_guid_1, solution_guid_2=solution_guid_2, + ) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath(solution_file_1)) + sln = test.read(solution_file_1, 'r') + expect = test.msvs_substitute(expected_slnfile_1, vc_version, subdir='src', project_guid=project_guid_1) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath(solution_file_2)) + sln = test.read(solution_file_2, 'r') + expect = test.msvs_substitute(expected_slnfile_2, vc_version, subdir='src', project_guid=project_guid_2) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath(filters_file_1)) + test.must_exist(test.workpath(filters_file_2)) + else: + test.must_not_exist(test.workpath(filters_file_1)) + test.must_not_exist(test.workpath(filters_file_2)) + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-mult-auto-vardir-guid.py b/test/MSVS/vs-mult-auto-vardir-guid.py new file mode 100644 index 0000000000..87d233220b --- /dev/null +++ b/test/MSVS/vs-mult-auto-vardir-guid.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and three solution (.sln) file. +""" + +import TestSConsMSVS + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + + for autofilter_projects in (None, False, True): + + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + solution_file_1 = 'Test_1.sln' + solution_file_2 = 'Test_2.sln' + + if major >= 10: + project_guid_1 = "{5A243E49-07F0-54C3-B3FD-1DBDF1BA5C9E}" + project_guid_2 = "{E20E17C7-251E-5246-8FD1-5D51978A0A5D}" + else: + project_guid_1 = "{AB46DD68-8CD8-5832-B784-65B216B94739}" + project_guid_2 = "{03EB0BC3-DA68-5825-9EBB-D8713304E739}" + + if major >= 10: + solution_guid_1 = "{5E5E4F5D-3E6A-5958-81C6-D4B8B8C633FA}" + solution_guid_2 = "{ECBCA12A-191D-54EC-BA78-F14249171130}" + else: + solution_guid_1 = "{5E5E4F5D-3E6A-5958-81C6-D4B8B8C633FA}" + solution_guid_2 = "{ECBCA12A-191D-54EC-BA78-F14249171130}" + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + have_solution_project_nodes=True, + autofilter_solution_project_nodes=autofilter_projects, + ) + + expected_slnfile_1 = test.get_expected_sln_file_contents( + vc_version, + project_file_1, + ) + + expected_slnfile_2 = test.get_expected_sln_file_contents( + vc_version, + project_file_2, + ) + + SConscript_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=1, + autofilter_projects=autofilter_projects, + default_guids=True, + ) + + test.subdir('src') + + test.write('SConstruct', """\ +SConscript('src/SConscript', variant_dir='build') +""") + + test.write(['src', 'SConscript'], SConscript_contents) + + if autofilter_projects is None: + test.run(arguments=".", status=2, stderr=r"^.*scons: [*]{3} An msvs solution file was detected in the MSVSSolution 'projects' argument:.+", match=test.match_re_dotall) + continue + + test.run(arguments=".") + + test.must_exist(test.workpath('src', project_file_1)) + vcproj = test.read(['src', project_file_1], 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', project_file_2)) + vcproj = test.read(['src', project_file_2], 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', solution_file)) + sln = test.read(['src', solution_file], 'r') + expect = test.msvs_substitute_projects( + expected_slnfile, subdir='src', + project_guid_1=project_guid_1, project_guid_2=project_guid_2, + solution_guid_1=solution_guid_1, solution_guid_2=solution_guid_2, + ) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath('src', solution_file_1)) + sln = test.read(['src', solution_file_1], 'r') + expect = test.msvs_substitute(expected_slnfile_1, vc_version, subdir='src', project_guid=project_guid_1) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath('src', solution_file_2)) + sln = test.read(['src', solution_file_2], 'r') + expect = test.msvs_substitute(expected_slnfile_2, vc_version, subdir='src', project_guid=project_guid_2) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath('src', filters_file_1)) + test.must_exist(test.workpath('src', filters_file_2)) + else: + test.must_not_exist(test.workpath('src', filters_file_1)) + test.must_not_exist(test.workpath('src', filters_file_2)) + + test.must_match(['build', project_file_1], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_1), mode='r') + + test.must_match(['build', project_file_2], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_2), mode='r') + + test.must_match(['build', solution_file], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file), mode='r') + + test.must_match(['build', solution_file_1], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file_1), mode='r') + + test.must_match(['build', solution_file_2], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file_2), mode='r') + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-mult-auto-vardir.py b/test/MSVS/vs-mult-auto-vardir.py new file mode 100644 index 0000000000..b4d2109312 --- /dev/null +++ b/test/MSVS/vs-mult-auto-vardir.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and three solution (.sln) file. +""" + +import TestSConsMSVS + +project_guid_1 = TestSConsMSVS.PROJECT_GUID_1 +project_guid_2 = TestSConsMSVS.PROJECT_GUID_2 + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + + for autofilter_projects in (None, False, True): + + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + solution_file_1 = 'Test_1.sln' + solution_file_2 = 'Test_2.sln' + + if major >= 10: + solution_guid_1 = "{5E5E4F5D-3E6A-5958-81C6-D4B8B8C633FA}" + solution_guid_2 = "{ECBCA12A-191D-54EC-BA78-F14249171130}" + else: + solution_guid_1 = "{5E5E4F5D-3E6A-5958-81C6-D4B8B8C633FA}" + solution_guid_2 = "{ECBCA12A-191D-54EC-BA78-F14249171130}" + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + have_solution_project_nodes=True, + autofilter_solution_project_nodes=autofilter_projects, + ) + + expected_slnfile_1 = test.get_expected_sln_file_contents( + vc_version, + project_file_1, + ) + + expected_slnfile_2 = test.get_expected_sln_file_contents( + vc_version, + project_file_2, + ) + + SConscript_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=1, + autofilter_projects=autofilter_projects, + ) + + test.subdir('src') + + test.write('SConstruct', """\ +SConscript('src/SConscript', variant_dir='build') +""") + + test.write(['src', 'SConscript'], SConscript_contents) + + if autofilter_projects is None: + test.run(arguments=".", status=2, stderr=r"^.*scons: [*]{3} An msvs solution file was detected in the MSVSSolution 'projects' argument:.+", match=test.match_re_dotall) + continue + + test.run(arguments=".") + + test.must_exist(test.workpath('src', project_file_1)) + vcproj = test.read(['src', project_file_1], 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', project_file_2)) + vcproj = test.read(['src', project_file_2], 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', solution_file)) + sln = test.read(['src', solution_file], 'r') + expect = test.msvs_substitute_projects( + expected_slnfile, subdir='src', + solution_guid_1=solution_guid_1, solution_guid_2=solution_guid_2, + ) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath('src', solution_file_1)) + sln = test.read(['src', solution_file_1], 'r') + expect = test.msvs_substitute(expected_slnfile_1, vc_version, subdir='src', project_guid=project_guid_1) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath('src', solution_file_2)) + sln = test.read(['src', solution_file_2], 'r') + expect = test.msvs_substitute(expected_slnfile_2, vc_version, subdir='src', project_guid=project_guid_2) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath('src', filters_file_1)) + test.must_exist(test.workpath('src', filters_file_2)) + else: + test.must_not_exist(test.workpath('src', filters_file_1)) + test.must_not_exist(test.workpath('src', filters_file_2)) + + test.must_match(['build', project_file_1], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_1), mode='r') + + test.must_match(['build', project_file_2], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_2), mode='r') + + test.must_match(['build', solution_file], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file), mode='r') + + test.must_match(['build', solution_file_1], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file_1), mode='r') + + test.must_match(['build', solution_file_2], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file_2), mode='r') + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-mult-auto.py b/test/MSVS/vs-mult-auto.py new file mode 100644 index 0000000000..57f1145fd1 --- /dev/null +++ b/test/MSVS/vs-mult-auto.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and three solution (.sln) file. +""" + +import TestSConsMSVS + +project_guid_1 = TestSConsMSVS.PROJECT_GUID_1 +project_guid_2 = TestSConsMSVS.PROJECT_GUID_2 + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + + for autofilter_projects in (None, False, True): + + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + solution_file_1 = 'Test_1.sln' + solution_file_2 = 'Test_2.sln' + + if major >= 10: + solution_guid_1 = "{73ABD7C4-0A64-5192-97C3-252B70C8D1B1}" + solution_guid_2 = "{142F0D4F-9896-5808-AAB4-A80F5661B1FE}" + else: + solution_guid_1 = "{73ABD7C4-0A64-5192-97C3-252B70C8D1B1}" + solution_guid_2 = "{142F0D4F-9896-5808-AAB4-A80F5661B1FE}" + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + have_solution_project_nodes=True, + autofilter_solution_project_nodes=autofilter_projects, + ) + + expected_slnfile_1 = test.get_expected_sln_file_contents( + vc_version, + project_file_1, + ) + + expected_slnfile_2 = test.get_expected_sln_file_contents( + vc_version, + project_file_2, + ) + + SConstruct_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=1, + autofilter_projects=autofilter_projects, + ) + + test.write('SConstruct', SConstruct_contents) + + if autofilter_projects is None: + test.run(arguments=".", status=2, stderr=r"^.*scons: [*]{3} An msvs solution file was detected in the MSVSSolution 'projects' argument:.+", match=test.match_re_dotall) + continue + + test.run(arguments=".") + + test.must_exist(test.workpath(project_file_1)) + vcproj = test.read(project_file_1, 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(project_file_2)) + vcproj = test.read(project_file_2, 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(solution_file)) + sln = test.read(solution_file, 'r') + expect = test.msvs_substitute_projects( + expected_slnfile, subdir='src', + solution_guid_1=solution_guid_1, solution_guid_2=solution_guid_2, + ) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath(solution_file_1)) + sln = test.read(solution_file_1, 'r') + expect = test.msvs_substitute(expected_slnfile_1, vc_version, subdir='src', project_guid=project_guid_1) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + test.must_exist(test.workpath(solution_file_2)) + sln = test.read(solution_file_2, 'r') + expect = test.msvs_substitute(expected_slnfile_2, vc_version, subdir='src', project_guid=project_guid_2) + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath(filters_file_1)) + test.must_exist(test.workpath(filters_file_2)) + else: + test.must_not_exist(test.workpath(filters_file_1)) + test.must_not_exist(test.workpath(filters_file_2)) + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-mult-noauto-vardir.py b/test/MSVS/vs-mult-noauto-vardir.py new file mode 100644 index 0000000000..18aacba887 --- /dev/null +++ b/test/MSVS/vs-mult-noauto-vardir.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and a solution (.sln) file. +""" + +import TestSConsMSVS + +project_guid_1 = TestSConsMSVS.PROJECT_GUID_1 +project_guid_2 = TestSConsMSVS.PROJECT_GUID_2 + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + ) + + SConscript_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=0, + ) + + test.subdir('src') + + test.write('SConstruct', """\ +SConscript('src/SConscript', variant_dir='build') +""") + + test.write(['src', 'SConscript'], SConscript_contents) + + test.run(arguments=".") + + test.must_exist(test.workpath('src', project_file_1)) + vcproj = test.read(['src', project_file_1], 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', project_file_2)) + vcproj = test.read(['src', project_file_2], 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath('src', solution_file)) + sln = test.read(['src', solution_file], 'r') + expect = test.msvs_substitute_projects(expected_slnfile, subdir='src') + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath('src', filters_file_1)) + test.must_exist(test.workpath('src', filters_file_2)) + else: + test.must_not_exist(test.workpath('src', filters_file_1)) + test.must_not_exist(test.workpath('src', filters_file_2)) + + test.must_match(['build', project_file_1], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_1), mode='r') + + test.must_match(['build', project_file_2], """\ +This is just a placeholder file. +The real project file is here: +%s +""" % test.workpath('src', project_file_2), mode='r') + + test.must_match(['build', solution_file], """\ +This is just a placeholder file. +The real workspace file is here: +%s +""" % test.workpath('src', solution_file), mode='r') + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-mult-noauto.py b/test/MSVS/vs-mult-noauto.py new file mode 100644 index 0000000000..ab0bb6dc62 --- /dev/null +++ b/test/MSVS/vs-mult-noauto.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test two project files (.vcxproj) and a solution (.sln) file. +""" + +import TestSConsMSVS + +project_guid_1 = TestSConsMSVS.PROJECT_GUID_1 +project_guid_2 = TestSConsMSVS.PROJECT_GUID_2 + +test = None + +for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): + test = TestSConsMSVS.TestSConsMSVS() + + # Make the test infrastructure think we have this version of MSVS installed. + test._msvs_versions = [vc_version] + + dirs = ['inc1', 'inc2'] + major, minor = test.parse_vc_version(vc_version) + project_ext = '.vcproj' if major <= 9 else '.vcxproj' + + project_file_1 = 'Test_1' + project_ext + project_file_2 = 'Test_2' + project_ext + + filters_file_1 = project_file_1 + '.filters' + filters_file_2 = project_file_2 + '.filters' + filters_file_expected = major >= 10 + + solution_file = 'Test.sln' + + expected_vcprojfile_1 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_1, project_guid_1, + ) + + expected_vcprojfile_2 = test.get_expected_projects_proj_file_contents( + vc_version, dirs, project_file_2, project_guid_2, + ) + + expected_slnfile = test.get_expected_projects_sln_file_contents( + vc_version, + project_file_1, + project_file_2, + ) + + SConstruct_contents = test.get_expected_projects_sconscript_file_contents( + vc_version=vc_version, + project_file_1=project_file_1, + project_file_2=project_file_2, + solution_file=solution_file, + autobuild_solution=0, + ) + + test.write('SConstruct', SConstruct_contents) + + test.run(arguments=".") + + test.must_exist(test.workpath(project_file_1)) + vcproj = test.read(project_file_1, 'r') + expect = test.msvs_substitute(expected_vcprojfile_1, vc_version, None, 'SConstruct', project_guid=project_guid_1) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(project_file_2)) + vcproj = test.read(project_file_2, 'r') + expect = test.msvs_substitute(expected_vcprojfile_2, vc_version, None, 'SConstruct', project_guid=project_guid_2) + # don't compare the pickled data + assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) + + test.must_exist(test.workpath(solution_file)) + sln = test.read(solution_file, 'r') + expect = test.msvs_substitute_projects(expected_slnfile, subdir='src') + # don't compare the pickled data + assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) + + if filters_file_expected: + test.must_exist(test.workpath(filters_file_1)) + test.must_exist(test.workpath(filters_file_2)) + else: + test.must_not_exist(test.workpath(filters_file_1)) + test.must_not_exist(test.workpath(filters_file_2)) + + # TODO: clean tests + +if test: + test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/MSVS/vs-scc-files.py b/test/MSVS/vs-scc-files.py index 85fa27c392..4e79e274f5 100644 --- a/test/MSVS/vs-scc-files.py +++ b/test/MSVS/vs-scc-files.py @@ -31,8 +31,11 @@ import TestSConsMSVS +test = None + for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): test = TestSConsMSVS.TestSConsMSVS() + host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = [vc_version] @@ -45,12 +48,14 @@ expected_slnfile = test.get_expected_sln_file_contents(vc_version, project_file) expected_vcprojfile = test.get_expected_proj_file_contents(vc_version, dirs, project_file) SConscript_contents = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='{vc_version}', +env=Environment(tools=['msvs'], + MSVS_VERSION='{vc_version}', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], MSVS_SCC_CONNECTION_ROOT='.', MSVS_SCC_PROVIDER='MSSCCI:Perforce SCM', - MSVS_SCC_PROJECT_NAME='Perforce Project') + MSVS_SCC_PROJECT_NAME='Perforce Project', + HOST_ARCH='{host_arch}') testsrc = ['test1.cpp', 'test2.cpp'] testincs = [r'sdk_dir\\sdk.h'] @@ -59,6 +64,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = '{project_file}', + MSVS_PROJECT_GUID='{project_guid}', srcs = testsrc, incs = testincs, localincs = testlocalincs, @@ -66,7 +72,10 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""".format(vc_version=vc_version, project_file=project_file) +""".format( + vc_version=vc_version, project_file=project_file, + host_arch=host_arch, project_guid=TestSConsMSVS.PROJECT_GUID, +) expected_sln_sccinfo = """\ \tGlobalSection(SourceCodeControl) = preSolution @@ -97,7 +106,6 @@ \t\tMSSCCI:Perforce SCM """ - test.write('SConstruct', SConscript_contents) test.run(arguments=project_file) @@ -116,7 +124,7 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - +if test: test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-scc-legacy-files.py b/test/MSVS/vs-scc-legacy-files.py index 8e2ca15cc7..9943d3d7ce 100644 --- a/test/MSVS/vs-scc-legacy-files.py +++ b/test/MSVS/vs-scc-legacy-files.py @@ -31,8 +31,11 @@ import TestSConsMSVS +test = None + for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): test = TestSConsMSVS.TestSConsMSVS() + host_arch = test.get_vs_host_arch() # Make the test infrastructure think we have this version of MSVS installed. test._msvs_versions = [vc_version] @@ -46,11 +49,13 @@ expected_vcprojfile = test.get_expected_proj_file_contents(vc_version, dirs, project_file) SConscript_contents = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='{vc_version}', +env=Environment(tools=['msvs'], + MSVS_VERSION='{vc_version}', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], MSVS_SCC_LOCAL_PATH=r'C:\\MyMsVsProjects', - MSVS_SCC_PROJECT_NAME='Perforce Project') + MSVS_SCC_PROJECT_NAME='Perforce Project', + HOST_ARCH='{host_arch}') testsrc = ['test1.cpp', 'test2.cpp'] testincs = [r'sdk_dir\\sdk.h'] @@ -59,6 +64,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = '{project_file}', + MSVS_PROJECT_GUID='{project_guid}', srcs = testsrc, incs = testincs, localincs = testlocalincs, @@ -66,7 +72,10 @@ misc = testmisc, buildtarget = 'Test.exe', variant = 'Release') -""".format(vc_version=vc_version, project_file=project_file) +""".format( + vc_version=vc_version, project_file=project_file, + host_arch=host_arch, project_guid=TestSConsMSVS.PROJECT_GUID, +) if major < 10: # VC8 and VC9 used key-value pair format. @@ -98,7 +107,7 @@ # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - +if test: test.pass_test() # Local Variables: diff --git a/test/MSVS/vs-variant_dir.py b/test/MSVS/vs-variant_dir.py index 7d4b9cbf3e..880a36edac 100644 --- a/test/MSVS/vs-variant_dir.py +++ b/test/MSVS/vs-variant_dir.py @@ -31,6 +31,8 @@ import TestSConsMSVS +test = None + for vc_version in TestSConsMSVS.get_tested_proj_file_vc_versions(): test = TestSConsMSVS.TestSConsMSVS() host_arch = test.get_vs_host_arch() @@ -58,21 +60,18 @@ test.run(arguments=".") - project_guid = "{CB4637F1-2205-50B7-B115-DCFA0DA68FB1}" vcproj = test.read(['src', project_file], 'r') - expect = test.msvs_substitute(expected_vcprojfile, vc_version, None, 'SConstruct', - project_guid=project_guid) + expect = test.msvs_substitute(expected_vcprojfile, vc_version, None, 'SConstruct') # don't compare the pickled data assert vcproj[:len(expect)] == expect, test.diff_substr(expect, vcproj) test.must_exist(test.workpath('src', 'Test.sln')) sln = test.read(['src', 'Test.sln'], 'r') - expect = test.msvs_substitute(expected_slnfile, '8.0', 'src', - project_guid=project_guid) + expect = test.msvs_substitute(expected_slnfile, '8.0', 'src') # don't compare the pickled data assert sln[:len(expect)] == expect, test.diff_substr(expect, sln) - test.must_match(['build', 'Test.vcproj'], """\ + test.must_match(['build', project_file], """\ This is just a placeholder file. The real project file is here: %s @@ -84,6 +83,7 @@ %s """ % test.workpath('src', 'Test.sln'), mode='r') +if test: test.pass_test() # Local Variables: diff --git a/test/MinGW/MinGWSharedLibrary.py b/test/MinGW/MinGWSharedLibrary.py index 9fac820d9e..9dbac50576 100644 --- a/test/MinGW/MinGWSharedLibrary.py +++ b/test/MinGW/MinGWSharedLibrary.py @@ -63,7 +63,7 @@ # Now verify versioned shared library doesn't fail env.SharedLibrary('foobar_ver', foobar_obj, SHLIBVERSION='2.4') -""" % locals()) +""") test.run(arguments = ".") diff --git a/test/MinGW/WINDOWS_INSERT_DEF.py b/test/MinGW/WINDOWS_INSERT_DEF.py index 5119c97518..27e075f393 100644 --- a/test/MinGW/WINDOWS_INSERT_DEF.py +++ b/test/MinGW/WINDOWS_INSERT_DEF.py @@ -55,7 +55,7 @@ DefaultEnvironment(tools=[]) env = Environment(TOOLS=['mingw']) hello_dll = env.SharedLibrary(WINDOWS_INSERT_DEF=0, target='hello', source='hello.c') -""" % locals()) +""") test.run(arguments = ".") diff --git a/test/Mkdir.py b/test/Mkdir.py index 367c834952..d177232b66 100644 --- a/test/Mkdir.py +++ b/test/Mkdir.py @@ -40,10 +40,9 @@ Execute(Mkdir('d1')) Execute(Mkdir(Dir('#d1-Dir'))) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) Cat = Action(cat) env = Environment() diff --git a/test/Move.py b/test/Move.py index 1da3aa7b8a..aca8fa424e 100644 --- a/test/Move.py +++ b/test/Move.py @@ -36,10 +36,9 @@ Execute(Move('f1.out', 'f1.in')) Execute(Move('File-f1.out', File('f1.in-File'))) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) Cat = Action(cat) env = Environment() diff --git a/test/NodeOps.py b/test/NodeOps.py index a2db429dbd..5a19c642f8 100644 --- a/test/NodeOps.py +++ b/test/NodeOps.py @@ -64,9 +64,9 @@ if %(_E)s: import os derived = [N.is_derived() for N in Nodes] - real1 = [os.path.exists(str(N)) for N in Nodes] + real1 = [os.path.exists(N) for N in Nodes] exists = [N.exists() for N in Nodes] - real2 = [os.path.exists(str(N)) for N in Nodes] + real2 = [os.path.exists(N) for N in Nodes] for N,D,R,E,F in zip(Nodes, derived, real1, exists, real2): print('%%s: %%s %%s %%s %%s'%%(N,D,R,E,F)) foo.SharedLibrary(target = 'foo', source = 'foo%(_obj)s') @@ -119,13 +119,13 @@ Import('*') def mycopy(env, source, target): - with open(str(target[0]), 'wt') as fo, open(str(source[0]), 'rt') as fi: + with open(target[0], 'wt') as fo, open(source[0], 'rt') as fi: fo.write(fi.read()) def exists_test(node): - before = os.path.exists(str(node)) # doesn't exist yet in VariantDir + before = os.path.exists(node) # doesn't exist yet in VariantDir via_node = node.exists() # side effect causes copy from src - after = os.path.exists(str(node)) + after = os.path.exists(node) node.is_derived() import SCons.Script if GetOption('no_exec'): diff --git a/test/Removed/BuildDir/Old/BuildDir.py b/test/Removed/BuildDir/Old/BuildDir.py index 1a1ba02f6e..7463f21eec 100644 --- a/test/Removed/BuildDir/Old/BuildDir.py +++ b/test/Removed/BuildDir/Old/BuildDir.py @@ -224,8 +224,8 @@ def filter_tempnam(err): def equal_stats(x,y): x = os.stat(x) y = os.stat(y) - return (stat.S_IMODE(x[stat.ST_MODE]) == stat.S_IMODE(y[stat.ST_MODE]) and - x[stat.ST_MTIME] == y[stat.ST_MTIME]) + return (stat.S_IMODE(x.st_mode) == stat.S_IMODE(y.st_mode) and + x.st_mtime == y.st_mtime) # Make sure we did duplicate the source files in build/var2, # and that their stats are the same: @@ -233,7 +233,7 @@ def equal_stats(x,y): test.must_exist(['work1', 'build', 'var2', 'f2.in']) test.fail_test(not equal_stats(test.workpath('work1', 'build', 'var2', 'f1.c'), test.workpath('work1', 'src', 'f1.c'))) test.fail_test(not equal_stats(test.workpath('work1', 'build', 'var2', 'f2.in'), test.workpath('work1', 'src', 'f2.in'))) - + # Make sure we didn't duplicate the source files in build/var3. test.must_not_exist(['work1', 'build', 'var3', 'f1.c']) test.must_not_exist(['work1', 'build', 'var3', 'f2.in']) diff --git a/test/Removed/BuildDir/Old/SConscript-build_dir.py b/test/Removed/BuildDir/Old/SConscript-build_dir.py index 0d1ba6abde..ed520370ee 100644 --- a/test/Removed/BuildDir/Old/SConscript-build_dir.py +++ b/test/Removed/BuildDir/Old/SConscript-build_dir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that specifying a build_dir argument to SConscript still works. @@ -108,7 +107,7 @@ def cat(env, source, target): # VariantDir('build/var9', '.') # SConscript('build/var9/src/SConscript') SConscript('src/SConscript', build_dir='build/var9', src_dir='.') -""") +""") test.subdir(['test', 'src'], ['test', 'alt']) @@ -152,8 +151,8 @@ def cat(env, source, target): def equal_stats(x,y): x = os.stat(x) y = os.stat(y) - return (stat.S_IMODE(x[stat.ST_MODE]) == stat.S_IMODE(y[stat.ST_MODE]) and - x[stat.ST_MTIME] == y[stat.ST_MTIME]) + return (stat.S_IMODE(x.st_mode) == stat.S_IMODE(y.st_mode) and + x.st_mtime == y.st_mtime) # Make sure we did duplicate the source files in build/var1, # and that their stats are the same: @@ -168,12 +167,12 @@ def equal_stats(x,y): test.must_exist(test.workpath('test', 'build', 'var2', file)) test.fail_test(not equal_stats(test.workpath('test', 'build', 'var2', file), test.workpath('test', 'src', file))) - + # Make sure we didn't duplicate the source files in build/var3. test.must_not_exist(test.workpath('test', 'build', 'var3', 'aaa.in')) test.must_not_exist(test.workpath('test', 'build', 'var3', 'bbb.in')) test.must_not_exist(test.workpath('test', 'build', 'var3', 'ccc.in')) - + #XXX We can't support var4 and var5 yet, because our VariantDir linkage #XXX is to an entire source directory. We haven't yet generalized our #XXX infrastructure to be able to take the SConscript file from one source @@ -200,12 +199,12 @@ def equal_stats(x,y): test.must_exist(test.workpath('build', 'var6', file)) test.fail_test(not equal_stats(test.workpath('build', 'var6', file), test.workpath('test', 'src', file))) - + # Make sure we didn't duplicate the source files in build/var7. test.must_not_exist(test.workpath('build', 'var7', 'aaa.in')) test.must_not_exist(test.workpath('build', 'var7', 'bbb.in')) test.must_not_exist(test.workpath('build', 'var7', 'ccc.in')) - + # Make sure we didn't duplicate the source files in build/var8. test.must_not_exist(test.workpath('build', 'var8', 'aaa.in')) test.must_not_exist(test.workpath('build', 'var8', 'bbb.in')) @@ -219,6 +218,7 @@ def equal_stats(x,y): """) test.write(['test2', 'SConscript'], """\ +DefaultEnvironment(tools=[]) # test speedup env = Environment() foo_obj = env.Object('foo.c') env.Program('foo', [foo_obj, 'bar.c']) diff --git a/test/Removed/Old/warn-missing-sconscript.py b/test/Removed/Old/warn-missing-sconscript.py index 7859b6423e..2c733aae7d 100644 --- a/test/Removed/Old/warn-missing-sconscript.py +++ b/test/Removed/Old/warn-missing-sconscript.py @@ -59,7 +59,7 @@ def build(target, source, env): # this is the old message: #expect = r""" #scons: warning: Ignoring missing SConscript 'no_such_file' -"" + TestSCons.file_expr +#"""" + TestSCons.file_expr test.run(arguments='--warn=missing-sconscript .', stderr=expect) test.run(arguments='--warn=no-missing-sconscript .', stderr="") diff --git a/test/Repository/LIBPATH.py b/test/Repository/LIBPATH.py index 646b5d7e4f..9e19b3f64d 100644 --- a/test/Repository/LIBPATH.py +++ b/test/Repository/LIBPATH.py @@ -46,7 +46,7 @@ def write_LIBDIRFLAGS(env, target, source): pre = env.subst('$LIBDIRPREFIX') suf = env.subst('$LIBDIRSUFFIX') - with open(str(target[0]), 'w') as f: + with open(target[0], 'w') as f: for arg in env.subst('$_LIBDIRFLAGS', target=target).split(): if arg[:len(pre)] == pre: arg = arg[len(pre):] diff --git a/test/Repository/SConscript.py b/test/Repository/SConscript.py index 72e2d27bc5..3208304915 100644 --- a/test/Repository/SConscript.py +++ b/test/Repository/SConscript.py @@ -51,10 +51,9 @@ test.write(['rep1', 'src', 'SConscript'], """\ def cat(env, source, target): - target = str(target[0]) - with open(target, "w") as ofp: + with open(target[0], "w") as ofp: for src in source: - with open(str(src), "r") as ifp: + with open(src, "r") as ifp: ofp.write(ifp.read()) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) @@ -88,10 +87,9 @@ def cat(env, source, target): test.write(['rep2', 'src', 'SConscript'], """\ def cat(env, source, target): - target = str(target[0]) - with open(target, "w") as ofp: + with open(target[0], "w") as ofp: for src in source: - with open(str(src), "r") as ifp: + with open(src, "r") as ifp: ofp.write(ifp.read()) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) diff --git a/test/Repository/option-f.py b/test/Repository/option-f.py index f1b2cc6945..0dfea7504c 100644 --- a/test/Repository/option-f.py +++ b/test/Repository/option-f.py @@ -42,10 +42,9 @@ test.write(['repository', 'SConstruct'], """\ Repository(r'%s') def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], "wb") as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: ofp.write(ifp.read()) env = Environment(BUILDERS={'Build':Builder(action=cat)}) diff --git a/test/Requires/basic.py b/test/Requires/basic.py index 4851ac8b5b..2948d9da61 100644 --- a/test/Requires/basic.py +++ b/test/Requires/basic.py @@ -35,9 +35,9 @@ test.write('SConstruct', """ def append_prereq_func(target, source, env): - with open(str(target[0]), 'wb') as ofp: - for s in source: - with open(str(s), 'rb') as ifp: + with open(target[0], 'wb') as ofp: + for src in source: + with open(src, 'rb') as ifp: ofp.write(ifp.read()) with open('prereq.out', 'rb') as ifp: ofp.write(ifp.read()) diff --git a/test/Requires/eval-order.py b/test/Requires/eval-order.py index fddf232284..b076d51132 100644 --- a/test/Requires/eval-order.py +++ b/test/Requires/eval-order.py @@ -34,9 +34,9 @@ test.write('SConstruct', """ def copy_and_create_func(target, source, env): - with open(str(target[0]), 'w') as ofp: - for s in source: - with open(str(s), 'r') as ifp: + with open(target[0], 'w') as ofp: + for src in source: + with open(src, 'r') as ifp: ofp.write(ifp.read()) with open('file.in', 'w') as f: f.write("file.in 1\\n") diff --git a/test/SCONSFLAGS.py b/test/SCONSFLAGS.py index 4a44f30b07..ffccf1f2f8 100644 --- a/test/SCONSFLAGS.py +++ b/test/SCONSFLAGS.py @@ -62,15 +62,15 @@ test.must_not_contain_any_line(test.stdout(), ['Help text.']) test.must_contain_all_lines(test.stdout(), ['-H, --help-options']) -os.environ['SCONSFLAGS'] = '-Z' - expect = r"""usage: scons [OPTIONS] [VARIABLES] [TARGETS] SCons Error: no such option: -Z """ -test.run(arguments = "-H", status = 2, - stderr = expect, match=TestSCons.match_exact) +test.run(arguments="-Z", status=2, stderr=expect, match=TestSCons.match_exact) + +os.environ['SCONSFLAGS'] = '-Z' +test.run(status=2, stderr=expect, match=TestSCons.match_exact) test.pass_test() diff --git a/test/SPAWN.py b/test/SPAWN.py index ca9d3448d0..5939f4fca2 100644 --- a/test/SPAWN.py +++ b/test/SPAWN.py @@ -42,7 +42,7 @@ ofp.write(ifp.read()) """) -test.write('SConstruct', """ +test.write('SConstruct', """\ import subprocess import sys @@ -56,16 +56,19 @@ def my_spawn2(sh, escape, cmd, args, env): cp = subprocess.run(s, shell=True) return cp.returncode -env = Environment(MY_SPAWN1 = my_spawn1, - MY_SPAWN2 = my_spawn2, - COMMAND = r'%(_python_)s cat.py $TARGET $SOURCES') -env1 = env.Clone(SPAWN = my_spawn1) +DefaultEnvironment() +env = Environment( + MY_SPAWN1=my_spawn1, + MY_SPAWN2=my_spawn2, + COMMAND=r'%(_python_)s cat.py $TARGET $SOURCES', +) +env1 = env.Clone(SPAWN=my_spawn1) env1.Command('file1.out', 'file1.in', '$COMMAND') -env2 = env.Clone(SPAWN = '$MY_SPAWN2') +env2 = env.Clone(SPAWN='$MY_SPAWN2') env2.Command('file2.out', 'file2.in', '$COMMAND') -env3 = env.Clone(SPAWN = '${USE_TWO and MY_SPAWN2 or MY_SPAWN1}') +env3 = env.Clone(SPAWN='${MY_SPAWN2 if USE_TWO else MY_SPAWN1}') env3.Command('file3.out', 'file3.in', '$COMMAND', USE_TWO=0) env3.Command('file4.out', 'file4.in', '$COMMAND', USE_TWO=1) """ % locals()) diff --git a/test/Scanner/Scanner.py b/test/Scanner/Scanner.py index 4889d0f280..d64a7ab6cd 100644 --- a/test/Scanner/Scanner.py +++ b/test/Scanner/Scanner.py @@ -117,7 +117,7 @@ def third(env, target, source): contents = source[0].get_contents() # print("TYPE:"+str(type(contents))) contents = contents.replace(b'getfile', b'MISSEDME') - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(contents) kbld = Builder(action=r'%(_python_)s build.py $SOURCES $TARGET', diff --git a/test/Scanner/empty-implicit.py b/test/Scanner/empty-implicit.py index a1e7b032eb..33482d2a7c 100644 --- a/test/Scanner/empty-implicit.py +++ b/test/Scanner/empty-implicit.py @@ -37,11 +37,11 @@ import os.path def scan(node, env, envkey, arg): - print('XScanner: node = '+os.path.split(str(node))[1]) + print('XScanner: node = '+os.path.split(node)[1]) return [] def exists_check(node, env): - return os.path.exists(str(node)) + return os.path.exists(node) XScanner = Scanner(name = 'XScanner', function = scan, @@ -50,8 +50,8 @@ def exists_check(node, env): skeys = ['.x']) def echo(env, target, source): - t = os.path.split(str(target[0]))[1] - s = os.path.split(str(source[0]))[1] + t = os.path.split(target[0])[1] + s = os.path.split(source[0])[1] print('create %s from %s' % (t, s)) with open(t, 'wb') as ofb, open(s, 'rb') as ifb: ofb.write(ifb.read()) diff --git a/test/Scanner/exception.py b/test/Scanner/exception.py index 90791cb06c..c87d93df0b 100644 --- a/test/Scanner/exception.py +++ b/test/Scanner/exception.py @@ -67,10 +67,9 @@ def process(outf, inf): outf.write(line) def cat(env, source, target): - target = str(target[0]) - with open(target, 'wb') as outf: + with open(target[0], 'wb') as outf: for src in source: - with open(str(src), 'rb') as inf: + with open(src, 'rb') as inf: process(outf, inf) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) diff --git a/test/Scanner/no-Dir-node.py b/test/Scanner/no-Dir-node.py index ef90934247..e10dc5d983 100644 --- a/test/Scanner/no-Dir-node.py +++ b/test/Scanner/no-Dir-node.py @@ -81,7 +81,7 @@ def process(infp, outfp): test.write('SConstruct', """\ def foo(target, source, env): - fp = open(str(target[0]), 'w') + fp = open(target[0], 'w') for c in sorted(source[0].children(), key=lambda t: t.name): fp.write('%s\\n' % c) fp.close() diff --git a/test/Scanner/scan-once.py b/test/Scanner/scan-once.py index bbe2594c2d..304b523762 100644 --- a/test/Scanner/scan-once.py +++ b/test/Scanner/scan-once.py @@ -36,11 +36,11 @@ import os.path def scan(node, env, envkey, arg): - print('XScanner: node = '+ os.path.split(str(node))[1]) + print('XScanner: node = '+ os.path.split(node)[1]) return [] def exists_check(node, env): - return os.path.exists(str(node)) + return os.path.exists(node) XScanner = Scanner(name = 'XScanner', function = scan, @@ -49,8 +49,8 @@ def exists_check(node, env): skeys = ['.x']) def echo(env, target, source): - t = os.path.split(str(target[0]))[1] - s = os.path.split(str(source[0]))[1] + t = os.path.split(target[0])[1] + s = os.path.split(source[0])[1] print('create %s from %s' % (t, s)) Echo = Builder(action = Action(echo, None), diff --git a/test/SideEffect/Issues/3013/files/SConstruct b/test/SideEffect/Issues/3013/files/SConstruct index 9294cb0308..9d897c2a4e 100644 --- a/test/SideEffect/Issues/3013/files/SConstruct +++ b/test/SideEffect/Issues/3013/files/SConstruct @@ -6,7 +6,7 @@ DefaultEnvironment(tools=[]) env = Environment() def make_file(target, source, env): - with open(str(target[0]), 'w') as f: + with open(target[0], 'w') as f: f.write('gobldygook') with open(str(target[0]) + '_side_effect', 'w') as side_effect: side_effect.write('anything') @@ -23,4 +23,3 @@ SConscript( exports={'env':env}, duplicate=0 ) - diff --git a/test/SideEffect/basic.py b/test/SideEffect/basic.py index b5b6381e21..aa8099c5d2 100644 --- a/test/SideEffect/basic.py +++ b/test/SideEffect/basic.py @@ -41,9 +41,9 @@ def copy(source, target): f.write(f2.read()) def build(env, source, target): - copy(str(source[0]), str(target[0])) + copy(source[0], target[0]) if target[0].side_effects: - with open(str(target[0].side_effects[0]), "ab") as side_effect: + with open(target[0].side_effects[0], "ab") as side_effect: side_effect.write(('%%s -> %%s\\n'%%(str(source[0]), str(target[0]))).encode()) Build = Builder(action=build) diff --git a/test/SideEffect/variant_dir.py b/test/SideEffect/variant_dir.py index 0e9ae532a5..3f815d313c 100644 --- a/test/SideEffect/variant_dir.py +++ b/test/SideEffect/variant_dir.py @@ -35,16 +35,16 @@ test = TestSCons.TestSCons() -test.write('SConstruct', +test.write('SConstruct', """ def copy(source, target): with open(target, "wb") as f, open(source, "rb") as f2: f.write(f2.read()) def build(env, source, target): - copy(str(source[0]), str(target[0])) + copy(source[0], target[0]) if target[0].side_effects: - with open(str(target[0].side_effects[0]), "ab") as side_effect: + with open(target[0].side_effects[0], "ab") as side_effect: side_effect.write(('%s -> %s\\n'%(str(source[0]), str(target[0]))).encode()) Build = Builder(action=build) diff --git a/test/TARGET-dir.py b/test/TARGET-dir.py index 652cf77516..a3cb8da08f 100644 --- a/test/TARGET-dir.py +++ b/test/TARGET-dir.py @@ -42,10 +42,9 @@ test.write('SConstruct', """ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) f.close() env = Environment(CPPPATH='${TARGET.dir}') diff --git a/test/TEX/LATEX2.py b/test/TEX/LATEX2.py index 6dc5050d93..a7215e906b 100644 --- a/test/TEX/LATEX2.py +++ b/test/TEX/LATEX2.py @@ -47,10 +47,12 @@ test.write('SConstruct', """ import os + +_ = DefaultEnvironment(tools=[]) foo = Environment() foo['TEXINPUTS'] = ['subdir',os.environ.get('TEXINPUTS', '')] foo.PDF(source = ['foo.ltx','bar.latex','makeindex.tex','latexi.tex']) -""" % locals()) +""") latex = r""" \documentclass{letter} diff --git a/test/TEX/LATEXCOMSTR.py b/test/TEX/LATEXCOMSTR.py index 41c5dc72c6..de63ebfa33 100644 --- a/test/TEX/LATEXCOMSTR.py +++ b/test/TEX/LATEXCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -r""" +""" Test that the $LATEXCOMSTR construction variable allows you to configure the C compilation output. """ @@ -38,6 +37,7 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(TOOLS = ['latex'], LATEXCOM = r'%(_python_)s mycompile.py latex $TARGET $SOURCE', LATEXCOMSTR = 'Building $TARGET from $SOURCE') @@ -51,7 +51,7 @@ test.run(stdout = test.wrap_stdout("""\ Building test1.dvi from test1.latex -""" % locals())) +""")) test.must_match('test1.dvi', "test1.latex\n") diff --git a/test/TEX/PDFLATEXCOMSTR.py b/test/TEX/PDFLATEXCOMSTR.py index d695bdec10..f0e9467d5b 100644 --- a/test/TEX/PDFLATEXCOMSTR.py +++ b/test/TEX/PDFLATEXCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -r""" +""" Test that the $PDFLATEXCOMSTR construction variable allows you to configure the C compilation output. """ @@ -38,6 +37,7 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(TOOLS = ['pdflatex'], PDFLATEXCOM = r'%(_python_)s mycompile.py latex $TARGET $SOURCE', PDFLATEXCOMSTR = 'Building $TARGET from $SOURCE') @@ -51,7 +51,7 @@ test.run(stdout = test.wrap_stdout("""\ Building test1.pdf from test1.latex -""" % locals())) +""")) test.must_match('test1.pdf', "test1.latex\n") diff --git a/test/TEX/PDFTEXCOMSTR.py b/test/TEX/PDFTEXCOMSTR.py index 50edd28a07..30f3539264 100644 --- a/test/TEX/PDFTEXCOMSTR.py +++ b/test/TEX/PDFTEXCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -r""" +""" Test that the $PDFTEXCOMSTR construction variable allows you to configure the C compilation output. """ @@ -38,6 +37,7 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(TOOLS = ['pdftex'], PDFTEXCOM = r'%(_python_)s mycompile.py tex $TARGET $SOURCE', PDFTEXCOMSTR = 'Building $TARGET from $SOURCE') @@ -51,7 +51,7 @@ test.run(stdout = test.wrap_stdout("""\ Building test1.pdf from test1.tex -""" % locals())) +""")) test.must_match('test1.pdf', "test1.tex\n") diff --git a/test/TEX/TEXCOMSTR.py b/test/TEX/TEXCOMSTR.py index 9dbba133ab..c41de6d191 100644 --- a/test/TEX/TEXCOMSTR.py +++ b/test/TEX/TEXCOMSTR.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -r""" +""" Test that the $TEXCOMSTR construction variable allows you to configure the C compilation output. """ @@ -38,9 +37,12 @@ test.file_fixture('mycompile.py') test.write('SConstruct', """ -env = Environment(TOOLS = ['tex'], - TEXCOM = r'%(_python_)s mycompile.py tex $TARGET $SOURCE', - TEXCOMSTR = 'Building $TARGET from $SOURCE') +_ = DefaultEnvironment(tools=[]) +env = Environment( + TOOLS=['tex'], + TEXCOM=r'%(_python_)s mycompile.py tex $TARGET $SOURCE', + TEXCOMSTR='Building $TARGET from $SOURCE', +) env.DVI('test1') """ % locals()) @@ -49,9 +51,9 @@ /*tex*/ """) -test.run(stdout = test.wrap_stdout("""\ +test.run(stdout=test.wrap_stdout("""\ Building test1.dvi from test1.tex -""" % locals())) +""")) test.must_match('test1.dvi', "test1.tex\n") diff --git a/test/TEX/configure.py b/test/TEX/configure.py index 9fb4b3e8a5..31bec050a8 100644 --- a/test/TEX/configure.py +++ b/test/TEX/configure.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - -r""" +""" Verify execution of custom test case. The old code base would not be able to fail the test """ @@ -50,6 +49,8 @@ # misspell package name to ensure failure test.write('SConstruct', r""" +import os + lmodern_test_text = r''' \documentclass{article} \usepackage{lmodernD} @@ -68,13 +69,13 @@ def CheckLModern(context): context.Result(is_ok) return is_ok -import os +_ = DefaultEnvironment(tools=[]) env = Environment() env['TEXINPUTS'] = '.' conf = Configure( env, custom_tests={'CheckLModern' : CheckLModern} ) conf.CheckLModern() env = conf.Finish() -""" % locals()) +""") test.run() diff --git a/test/TEX/dryrun.py b/test/TEX/dryrun.py index 90357fcc66..4dc12e4e35 100644 --- a/test/TEX/dryrun.py +++ b/test/TEX/dryrun.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,11 +22,8 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" -r""" +""" Validate that we can set the LATEX string to our own utility, that the produced .dvi, .aux and .log files get removed by the -c option, and that we can use this to wrap calls to the real latex utility. @@ -43,9 +42,11 @@ test.write('SConstruct', """ import os + +_ = DefaultEnvironment(tools=[]) foo = Environment() foo.DVI(target = 'foo.dvi', source = 'foo.ltx') -""" % locals()) +""") test.write('foo.ltx', r""" \documentclass{letter} diff --git a/test/TEX/glossaries.py b/test/TEX/glossaries.py index 069fcc3769..d418ee266e 100644 --- a/test/TEX/glossaries.py +++ b/test/TEX/glossaries.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" +r""" Validate that use of \glossaries in TeX source files causes SCons to be aware of the necessary created glossary files. diff --git a/test/TEX/rename_result.py b/test/TEX/rename_result.py index f67e5692e3..5d9f1b0368 100644 --- a/test/TEX/rename_result.py +++ b/test/TEX/rename_result.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" r""" Validate that we can rename the output from latex to the @@ -42,11 +41,13 @@ test.write('SConstruct', """ import os + +_ = DefaultEnvironment(tools=[]) foo = Environment() foo['TEXINPUTS'] = [ 'subdir', os.environ.get('TEXINPUTS', '') ] foo.DVI(target = 'foobar.dvi', source = 'foo.ltx') foo.PDF(target = 'bar.xyz', source = 'bar.ltx') -""" % locals()) +""") test.write('foo.ltx', r""" \documentclass{letter} diff --git a/test/TEX/subdir_variantdir_include2.py b/test/TEX/subdir_variantdir_include2.py index 953a2293e1..2b615f194d 100644 --- a/test/TEX/subdir_variantdir_include2.py +++ b/test/TEX/subdir_variantdir_include2.py @@ -23,7 +23,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" +r""" Verify that we execute TeX in a subdirectory (if that's where the document resides) by checking that all the auxiliary files get created there and not in the top-level directory. Test this when variantDir is used diff --git a/test/TEX/usepackage.py b/test/TEX/usepackage.py index 66510c2e35..34d22b35f9 100644 --- a/test/TEX/usepackage.py +++ b/test/TEX/usepackage.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" r""" Validate that we can set the LATEX string to our own utility, that @@ -43,10 +42,12 @@ test.write('SConstruct', """ import os + +_ = DefaultEnvironment(tools=[]) foo = Environment() foo['TEXINPUTS'] = [ 'subdir', os.environ.get('TEXINPUTS', '') ] foo.DVI(target = 'foo.dvi', source = 'foo.ltx') -""" % locals()) +""") test.write('foo.ltx', r""" \documentclass{letter} diff --git a/test/Touch.py b/test/Touch.py index 3538c7d338..e3619466aa 100644 --- a/test/Touch.py +++ b/test/Touch.py @@ -38,10 +38,9 @@ Execute(Touch(File('f1-File'))) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) Cat = Action(cat) diff --git a/test/Value/Value.py b/test/Value/Value.py index 7bb48ca9f1..2995364201 100644 --- a/test/Value/Value.py +++ b/test/Value/Value.py @@ -52,7 +52,7 @@ def __str__(self): C = Custom(P) def create(target, source, env): - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(source[0].get_contents()) DefaultEnvironment(tools=[]) # test speedup @@ -68,7 +68,7 @@ def create_value(target, source, env): target[0].write(source[0].get_contents()) def create_value_file(target, source, env): - with open(str(target[0]), 'wb') as f: + with open(target[0], 'wb') as f: f.write(source[0].read()) env['BUILDERS']['B2'] = Builder(action=create_value) diff --git a/test/Variables/BoolVariable.py b/test/Variables/BoolVariable.py index c63322b29a..aa01a30a14 100644 --- a/test/Variables/BoolVariable.py +++ b/test/Variables/BoolVariable.py @@ -39,10 +39,7 @@ def check(expect): test.write(SConstruct_path, """\ -from SCons.Variables.BoolVariable import BoolVariable - -BV = BoolVariable - +from SCons.Variables.BoolVariable import BoolVariable as BV from SCons.Variables import BoolVariable opts = Variables(args=ARGUMENTS) @@ -51,7 +48,8 @@ def check(expect): BV('profile', 'create profiling informations', False), ) -env = Environment(variables=opts) +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=opts, tools=[]) Help(opts.GenerateHelpText(env)) print(env['warnings']) @@ -69,7 +67,7 @@ def check(expect): expect_stderr = """ scons: *** Error converting option: 'warnings' Invalid value for boolean variable: 'irgendwas' -""" + test.python_file_line(SConstruct_path, 13) +""" + test.python_file_line(SConstruct_path, 11) test.run(arguments='warnings=irgendwas', stderr=expect_stderr, status=2) diff --git a/test/Variables/EnumVariable.py b/test/Variables/EnumVariable.py index 111fff3504..48f24081c9 100644 --- a/test/Variables/EnumVariable.py +++ b/test/Variables/EnumVariable.py @@ -38,9 +38,7 @@ def check(expect): assert result[1:len(expect)+1] == expect, (result[1:len(expect)+1], expect) test.write(SConstruct_path, """\ -from SCons.Variables.EnumVariable import EnumVariable -EV = EnumVariable - +from SCons.Variables.EnumVariable import EnumVariable as EV from SCons.Variables import EnumVariable list_of_libs = Split('x11 gl qt ical') @@ -54,11 +52,12 @@ def check(expect): allowed_values=('motif', 'gtk', 'kde'), map={}, ignorecase=1), # case insensitive EV('some', 'some option', 'xaver', - allowed_values=('xaver', 'eins'), - map={}, ignorecase=2), # make lowercase + allowed_values=('xaver', 'eins', 'zwei wörter'), + map={}, ignorecase=2), # case lowering ) -env = Environment(variables=opts) +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=opts, tools=[]) Help(opts.GenerateHelpText(env)) print(env['debug']) @@ -68,7 +67,8 @@ def check(expect): Default(env.Alias('dummy', None)) """) -test.run(); check(['no', 'gtk', 'xaver']) +test.run() +check(['no', 'gtk', 'xaver']) test.run(arguments='debug=yes guilib=Motif some=xAVER') check(['yes', 'Motif', 'xaver']) @@ -78,22 +78,25 @@ def check(expect): expect_stderr = """ scons: *** Invalid value for enum variable 'debug': 'FULL'. Valid values are: ('yes', 'no', 'full') -""" + test.python_file_line(SConstruct_path, 21) +""" + test.python_file_line(SConstruct_path, 20) test.run(arguments='debug=FULL', stderr=expect_stderr, status=2) expect_stderr = """ scons: *** Invalid value for enum variable 'guilib': 'irgendwas'. Valid values are: ('motif', 'gtk', 'kde') -""" + test.python_file_line(SConstruct_path, 21) +""" + test.python_file_line(SConstruct_path, 20) test.run(arguments='guilib=IrGeNdwas', stderr=expect_stderr, status=2) expect_stderr = """ -scons: *** Invalid value for enum variable 'some': 'irgendwas'. Valid values are: ('xaver', 'eins') -""" + test.python_file_line(SConstruct_path, 21) +scons: *** Invalid value for enum variable 'some': 'irgendwas'. Valid values are: ('xaver', 'eins', 'zwei wörter') +""" + test.python_file_line(SConstruct_path, 20) test.run(arguments='some=IrGeNdwas', stderr=expect_stderr, status=2) +test.run(arguments=['some=zwei Wörter']) +check(['no', 'gtk', 'zwei wörter']) # case-lowering converter + test.pass_test() # Local Variables: diff --git a/test/Variables/ListVariable.py b/test/Variables/ListVariable.py index 2bd032733f..d6356e822b 100644 --- a/test/Variables/ListVariable.py +++ b/test/Variables/ListVariable.py @@ -35,34 +35,38 @@ SConstruct_path = test.workpath('SConstruct') -def check(expect): +def check(expected): result = test.stdout().split('\n') - r = result[1:len(expect)+1] - assert r == expect, (r, expect) - + r = result[1 : len(expected) + 1] + assert r == expected, (r, expected) test.write(SConstruct_path, """\ -from SCons.Variables.ListVariable import ListVariable -LV = ListVariable - +from SCons.Variables.ListVariable import ListVariable as LV from SCons.Variables import ListVariable -list_of_libs = Split('x11 gl qt ical') +list_of_libs = Split('x11 gl qt ical') + ["with space"] optsfile = 'scons.variables' opts = Variables(optsfile, args=ARGUMENTS) opts.AddVariables( - ListVariable('shared', - 'libraries to build as shared libraries', - 'all', - names = list_of_libs, - map = {'GL':'gl', 'QT':'qt'}), - LV('listvariable', 'listvariable help', 'all', names=['l1', 'l2', 'l3']) - ) - -DefaultEnvironment(tools=[]) # test speedup -env = Environment(variables=opts) + ListVariable( + 'shared', + 'libraries to build as shared libraries', + default='all', + names=list_of_libs, + map={'GL': 'gl', 'QT': 'qt'}, + ), + LV( + 'listvariable', + 'listvariable help', + default='all', + names=['l1', 'l2', 'l3'], + ), +) + +_ = DefaultEnvironment(tools=[]) # test speedup +env = Environment(variables=opts, tools=[]) opts.Save(optsfile, env) Help(opts.GenerateHelpText(env)) @@ -73,7 +77,7 @@ def check(expect): else: print('0') -print(" ".join(env['shared'])) +print(",".join(env['shared'])) print(env.subst('$shared')) # Test subst_path() because it's used in $CPPDEFINES expansions. @@ -82,14 +86,27 @@ def check(expect): """) test.run() -check(['all', '1', 'gl ical qt x11', 'gl ical qt x11', - "['gl ical qt x11']"]) - -expect = "shared = 'all'"+os.linesep+"listvariable = 'all'"+os.linesep +check( + [ + 'all', + '1', + 'gl,ical,qt,with space,x11', + 'gl ical qt with space x11', + "['gl ical qt with space x11']", + ] +) + +expect = "shared = 'all'" + os.linesep + "listvariable = 'all'" + os.linesep test.must_match(test.workpath('scons.variables'), expect) - -check(['all', '1', 'gl ical qt x11', 'gl ical qt x11', - "['gl ical qt x11']"]) +check( + [ + 'all', + '1', + 'gl,ical,qt,with space,x11', + 'gl ical qt with space x11', + "['gl ical qt with space x11']", + ] +) test.run(arguments='shared=none') check(['none', '0', '', '', "['']"]) @@ -98,79 +115,80 @@ def check(expect): check(['none', '0', '', '', "['']"]) test.run(arguments='shared=x11,ical') -check(['ical,x11', '1', 'ical x11', 'ical x11', - "['ical x11']"]) +check(['ical,x11', '1', 'ical,x11', 'ical x11', "['ical x11']"]) test.run(arguments='shared=x11,,ical,,') -check(['ical,x11', '1', 'ical x11', 'ical x11', - "['ical x11']"]) +check(['ical,x11', '1', 'ical,x11', 'ical x11', "['ical x11']"]) test.run(arguments='shared=GL') check(['gl', '0', 'gl', 'gl']) test.run(arguments='shared=QT,GL') -check(['gl,qt', '0', 'gl qt', 'gl qt', "['gl qt']"]) +check(['gl,qt', '0', 'gl,qt', 'gl qt', "['gl qt']"]) +#test.run(arguments='shared="with space"') +#check(['with space', '0', 'with space', 'with space', "['with space']"]) expect_stderr = """ -scons: *** Error converting option: 'shared' -Invalid value(s) for option: foo -""" + test.python_file_line(SConstruct_path, 20) +scons: *** Invalid value(s) for variable 'shared': 'foo'. Valid values are: gl,ical,qt,with space,x11,all,none +""" + test.python_file_line(SConstruct_path, 25) test.run(arguments='shared=foo', stderr=expect_stderr, status=2) # be paranoid in testing some more combinations expect_stderr = """ -scons: *** Error converting option: 'shared' -Invalid value(s) for option: foo -""" + test.python_file_line(SConstruct_path, 20) +scons: *** Invalid value(s) for variable 'shared': 'foo'. Valid values are: gl,ical,qt,with space,x11,all,none +""" + test.python_file_line(SConstruct_path, 25) test.run(arguments='shared=foo,ical', stderr=expect_stderr, status=2) expect_stderr = """ -scons: *** Error converting option: 'shared' -Invalid value(s) for option: foo -""" + test.python_file_line(SConstruct_path, 20) +scons: *** Invalid value(s) for variable 'shared': 'foo'. Valid values are: gl,ical,qt,with space,x11,all,none +""" + test.python_file_line(SConstruct_path, 25) test.run(arguments='shared=ical,foo', stderr=expect_stderr, status=2) expect_stderr = """ -scons: *** Error converting option: 'shared' -Invalid value(s) for option: foo -""" + test.python_file_line(SConstruct_path, 20) +scons: *** Invalid value(s) for variable 'shared': 'foo'. Valid values are: gl,ical,qt,with space,x11,all,none +""" + test.python_file_line(SConstruct_path, 25) test.run(arguments='shared=ical,foo,x11', stderr=expect_stderr, status=2) expect_stderr = """ -scons: *** Error converting option: 'shared' -Invalid value(s) for option: foo,bar -""" + test.python_file_line(SConstruct_path, 20) +scons: *** Invalid value(s) for variable 'shared': 'foo,bar'. Valid values are: gl,ical,qt,with space,x11,all,none +""" + test.python_file_line(SConstruct_path, 25) test.run(arguments='shared=foo,x11,,,bar', stderr=expect_stderr, status=2) -test.write('SConstruct', """ +test.write('SConstruct2', """\ from SCons.Variables import ListVariable opts = Variables(args=ARGUMENTS) opts.AddVariables( - ListVariable('gpib', - 'comment', - ['ENET', 'GPIB'], - names = ['ENET', 'GPIB', 'LINUX_GPIB', 'NO_GPIB']), - ) + ListVariable( + 'gpib', + 'comment', + default=['ENET', 'GPIB'], + names=['ENET', 'GPIB', 'LINUX_GPIB', 'NO_GPIB'], + ), +) DefaultEnvironment(tools=[]) # test speedup -env = Environment(variables=opts) +env = Environment(tools=[], variables=opts) Help(opts.GenerateHelpText(env)) print(env['gpib']) Default(env.Alias('dummy', None)) """) -test.run(stdout=test.wrap_stdout(read_str="ENET,GPIB\n", build_str="""\ +test.run( + arguments="-f SConstruct2", + stdout=test.wrap_stdout(read_str="ENET,GPIB\n", + build_str="""\ scons: Nothing to be done for `dummy'. -""")) +""") +) test.pass_test() diff --git a/test/Variables/PackageVariable.py b/test/Variables/PackageVariable.py index 47ee01bcc9..77c04e6088 100644 --- a/test/Variables/PackageVariable.py +++ b/test/Variables/PackageVariable.py @@ -27,6 +27,9 @@ Test the PackageVariable canned Variable type. """ +from __future__ import annotations + +import os import TestSCons @@ -34,25 +37,23 @@ SConstruct_path = test.workpath('SConstruct') -def check(expect): +def check(expect: list[str]) -> None: result = test.stdout().split('\n') + # skip first line and any lines beyond the length of expect assert result[1:len(expect)+1] == expect, (result[1:len(expect)+1], expect) test.write(SConstruct_path, """\ -from SCons.Variables.PackageVariable import PackageVariable -PV = PackageVariable - +from SCons.Variables.PackageVariable import PackageVariable as PV from SCons.Variables import PackageVariable opts = Variables(args=ARGUMENTS) opts.AddVariables( - PackageVariable('x11', - 'use X11 installed here (yes = search some places', - 'yes'), + PackageVariable('x11', 'use X11 installed here (yes = search some places', 'yes'), PV('package', 'help for package', 'yes'), - ) +) -env = Environment(variables=opts) +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=opts, tools=[]) Help(opts.GenerateHelpText(env)) print(env['x11']) @@ -71,12 +72,44 @@ def check(expect): test.run(arguments=['x11=%s' % test.workpath()]) check([test.workpath()]) +space_subdir = test.workpath('space subdir') +test.subdir(space_subdir) +test.run(arguments=[f'x11={space_subdir}']) +check([space_subdir]) + expect_stderr = """ scons: *** Path does not exist for variable 'x11': '/non/existing/path/' -""" + test.python_file_line(SConstruct_path, 14) +""" + test.python_file_line(SConstruct_path, 11) test.run(arguments='x11=/non/existing/path/', stderr=expect_stderr, status=2) +# test that an enabling value produces the default value +# as long as that's a path string +tinycbor_path = test.workpath('path', 'to', 'tinycbor') +test.subdir(tinycbor_path) +SConstruct_pathstr = test.workpath('SConstruct.path') +test.write(SConstruct_pathstr, f"""\ +from SCons.Variables import PackageVariable + +vars = Variables(args=ARGUMENTS) +vars.Add( + PackageVariable( + 'tinycbor', + help="use 'tinycbor' at ", + default=r'{tinycbor_path}' + ) +) + +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=vars, tools=[]) + +print(env['tinycbor']) +Default(env.Alias('dummy', None)) +""") + +test.run(arguments=['-f', 'SConstruct.path', 'tinycbor=yes']) +check([tinycbor_path]) + test.pass_test() # Local Variables: diff --git a/test/Variables/PathVariable.py b/test/Variables/PathVariable.py index 265f48f52f..af9efd3281 100644 --- a/test/Variables/PathVariable.py +++ b/test/Variables/PathVariable.py @@ -40,16 +40,12 @@ def check(expect): result = test.stdout().split('\n') assert result[1:len(expect)+1] == expect, (result[1:len(expect)+1], expect) -#### test PathVariable #### -test.subdir('lib', 'qt', ['qt', 'lib'], 'nolib' ) +test.subdir('lib', 'qt', ['qt', 'lib'], 'nolib') workpath = test.workpath() -libpath = os.path.join(workpath, 'lib') test.write(SConstruct_path, """\ -from SCons.Variables.PathVariable import PathVariable -PV = PathVariable - +from SCons.Variables.PathVariable import PathVariable as PV from SCons.Variables import PathVariable qtdir = r'%s' @@ -60,8 +56,8 @@ def check(expect): PV('qt_libraries', 'where the Qt library is installed', r'%s'), ) -DefaultEnvironment(tools=[]) # test speedup -env = Environment(variables=opts) +_ = DefaultEnvironment(tools=[]) # test speedup +env = Environment(variables=opts, tools=[]) Help(opts.GenerateHelpText(env)) print(env['qtdir']) @@ -69,7 +65,7 @@ def check(expect): print(env.subst('$qt_libraries')) Default(env.Alias('dummy', None)) -""" % (workpath, os.path.join('$qtdir', 'lib') )) +""" % (workpath, os.path.join('$qtdir', 'lib'))) qtpath = workpath libpath = os.path.join(qtpath, 'lib') @@ -92,7 +88,7 @@ def check(expect): check([qtpath, libpath, libpath]) qtpath = os.path.join(workpath, 'non', 'existing', 'path') -SConstruct_file_line = test.python_file_line(test.workpath('SConstruct'), 15)[:-1] +SConstruct_file_line = test.python_file_line(test.workpath('SConstruct'), 13)[:-1] expect_stderr = """ scons: *** Path for variable 'qtdir' does not exist: %(qtpath)s @@ -110,12 +106,19 @@ def check(expect): default_file = test.workpath('default_file') default_subdir = test.workpath('default_subdir') + existing_subdir = test.workpath('existing_subdir') test.subdir(existing_subdir) existing_file = test.workpath('existing_file') test.write(existing_file, "existing_file\n") +space_subdir = test.workpath('space subdir') +test.subdir(space_subdir) + +space_file = test.workpath('space file') +test.write(space_file, "space_file\n") + non_existing_subdir = test.workpath('non_existing_subdir') non_existing_file = test.workpath('non_existing_file') @@ -139,17 +142,22 @@ def check(expect): test.run(arguments=['X=%s' % existing_file]) check([existing_file]) -test.run(arguments=['X=%s' % non_existing_file]) -check([non_existing_file]) - test.run(arguments=['X=%s' % existing_subdir]) check([existing_subdir]) +test.run(arguments=['X=%s' % space_file]) +check([space_file]) + +test.run(arguments=['X=%s' % space_subdir]) +check([space_subdir]) + test.run(arguments=['X=%s' % non_existing_subdir]) check([non_existing_subdir]) +test.must_not_exist(non_existing_subdir) +test.run(arguments=['X=%s' % non_existing_file]) +check([non_existing_file]) test.must_not_exist(non_existing_file) -test.must_not_exist(non_existing_subdir) test.write(SConstruct_path, """\ opts = Variables(args=ARGUMENTS) diff --git a/test/Variables/Variables.py b/test/Variables/Variables.py index 77426c5913..e8a1873705 100644 --- a/test/Variables/Variables.py +++ b/test/Variables/Variables.py @@ -23,12 +23,21 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Test general behavior of Variables including save files. + +Note this test is coded to expect a compiler tool to have run +so that CC and CCFLAGS are set. The first test "run" collects +those values and uses them as a baseline for the actual tests. +We should be able to mock that in some way. +""" + import TestSCons test = TestSCons.TestSCons() test.write('SConstruct', """\ -DefaultEnvironment(tools=[]) # test speedup +_ = DefaultEnvironment(tools=[]) # test speedup env = Environment() print(env['CC']) print(" ".join(env['CCFLAGS'])) @@ -156,7 +165,7 @@ def check(expect): test.run(arguments='DEBUG_BUILD=1') check(['1', '1', cc, (ccflags + ' -O -g').strip(), 'v', 'v']) -test.run(arguments='-h', stdout = """\ +test.run(arguments='-h', stdout="""\ scons: Reading SConscript files ... 1 0 @@ -232,7 +241,7 @@ def checkSave(file, expected): gdict = {} ldict = {} with open(file, 'r') as f: - contents = f.read() + contents = f.read() exec(contents, gdict, ldict) assert expected == ldict, "%s\n...not equal to...\n%s" % (expected, ldict) @@ -277,7 +286,7 @@ def checkSave(file, expected): names = ['a','b','c',]) DefaultEnvironment(tools=[]) # test speedup -env = Environment(variables=opts) +env = Environment(variables=opts, tools=[]) print(env['RELEASE_BUILD']) print(env['DEBUG_BUILD']) @@ -288,18 +297,18 @@ def checkSave(file, expected): # First check for empty output file when nothing is passed on command line test.run() -check(['0','1']) +check(['0', '1']) checkSave('variables.saved', {}) # Now specify one option the same as default and make sure it doesn't write out test.run(arguments='DEBUG_BUILD=1') -check(['0','1']) +check(['0', '1']) checkSave('variables.saved', {}) # Now specify same option non-default and make sure only it is written out test.run(arguments='DEBUG_BUILD=0 LISTOPTION_TEST=a,b') -check(['0','0']) -checkSave('variables.saved',{'DEBUG_BUILD':0, 'LISTOPTION_TEST':'a,b'}) +check(['0', '0']) +checkSave('variables.saved', {'DEBUG_BUILD': 0, 'LISTOPTION_TEST': 'a,b'}) test.write('SConstruct', """ opts = Variables('custom.py') @@ -333,7 +342,7 @@ def compare(a, b): ) """) -test.run(arguments='-h', stdout = """\ +test.run(arguments='-h', stdout="""\ scons: Reading SConscript files ... scons: done reading SConscript files. Variables settable in custom.py or on the command line: @@ -355,7 +364,7 @@ def compare(a, b): actual: None Use scons -H for help about SCons built-in command-line options. -"""%cc) +""" % cc) test.write('SConstruct', """ import SCons.Variables diff --git a/test/Variables/chdir.py b/test/Variables/chdir.py index 2c553e2374..04e10dea54 100644 --- a/test/Variables/chdir.py +++ b/test/Variables/chdir.py @@ -24,7 +24,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -Verify that we can chdir() to the directory in which an Variables +Verify that we can chdir() to the directory in which a Variables file lives by using the __name__ value. """ @@ -43,7 +43,8 @@ SConscript_contents = """\ Import("opts") -env = Environment() +_ = DefaultEnvironment(tools=[]) +env = Environment(tools=[]) opts.Update(env) print("VARIABLE = "+repr(env['VARIABLE'])) """ @@ -66,7 +67,7 @@ VARIABLE = 'opts2.cfg value' """ -test.run(arguments = '-q -Q .', stdout=expect) +test.run(arguments='-q -Q .', stdout=expect) test.pass_test() diff --git a/test/Variables/help.py b/test/Variables/help.py index 186a173d7b..e0addb19d2 100644 --- a/test/Variables/help.py +++ b/test/Variables/help.py @@ -37,7 +37,7 @@ test = TestSCons.TestSCons() workpath = test.workpath() -qtpath = os.path.join(workpath, 'qt') +qtpath = os.path.join(workpath, 'qt') libpath = os.path.join(qtpath, 'lib') libdirvar = os.path.join('$qtdir', 'lib') @@ -92,7 +92,8 @@ PathVariable('qt_libraries', 'where the Qt library is installed', r'%(libdirvar)s'), ) -env = Environment(variables=opts) +_ = DefaultEnvironment(tools=[]) +env = Environment(variables=opts, tools=[]) Help(opts.GenerateHelpText(env)) print(env['warnings']) diff --git a/test/Variables/import.py b/test/Variables/import.py index 8db7624476..85c75fd39c 100644 --- a/test/Variables/import.py +++ b/test/Variables/import.py @@ -24,7 +24,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -Verify that an Variables file in a different directory can import +Verify that a Variables file in a different directory can import a module in that directory. """ @@ -37,6 +37,7 @@ test.subdir('bin', 'subdir') test.write('SConstruct', """\ +_ = DefaultEnvironment(tools=[]) opts = Variables('../bin/opts.cfg', ARGUMENTS) opts.Add('VARIABLE') Export("opts") @@ -45,14 +46,14 @@ SConscript_contents = """\ Import("opts") -env = Environment() +env = Environment(tools=[]) opts.Update(env) print("VARIABLE = %s"%env.get('VARIABLE')) """ test.write(['bin', 'opts.cfg'], """\ from local_options import VARIABLE -""" % locals()) +""") test.write(['bin', 'local_options.py'], """\ VARIABLE = 'bin/local_options.py' @@ -62,7 +63,7 @@ expect = "VARIABLE = bin/local_options.py\n" -test.run(arguments = '-q -Q .', stdout = expect) +test.run(arguments='-q -Q .', stdout=expect) test.pass_test() diff --git a/test/VariantDir/Clean.py b/test/VariantDir/Clean.py index 2e0d4c6827..1fc1269c3e 100644 --- a/test/VariantDir/Clean.py +++ b/test/VariantDir/Clean.py @@ -42,8 +42,7 @@ def build_sample(target, source, env): targetdir = str(target[0].dir) - target = str(target[0]) - with open(target, 'w') as ofd, open(str(source[0]), 'r') as ifd: + with open(target[0], 'w') as ofd, open(source[0], 'r') as ifd: ofd.write(ifd.read()) with open(targetdir+'/sample.junk', 'w') as f: f.write('Side effect!\\n') diff --git a/test/VariantDir/SConscript-variant_dir.py b/test/VariantDir/SConscript-variant_dir.py index 1e28c47a98..36198ceda2 100644 --- a/test/VariantDir/SConscript-variant_dir.py +++ b/test/VariantDir/SConscript-variant_dir.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that specifying a variant_dir argument to SConscript works properly. @@ -58,12 +57,12 @@ var9 = Dir('../build/var9') def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], "wb") as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: ofp.write(ifp.read()) +DefaultEnvironment(tools=[]) # test speedup env = Environment(BUILDERS={'Cat':Builder(action=cat)}, BUILD='build') @@ -138,8 +137,8 @@ def cat(env, source, target): def equal_stats(x,y): x = os.stat(x) y = os.stat(y) - return (stat.S_IMODE(x[stat.ST_MODE]) == stat.S_IMODE(y[stat.ST_MODE]) and - x[stat.ST_MTIME] == y[stat.ST_MTIME]) + return (stat.S_IMODE(x.st_mode) == stat.S_IMODE(y.st_mode) and + x.st_mtime == y.st_mtime) # Make sure we did duplicate the source files in build/var1, # and that their stats are the same: @@ -205,6 +204,7 @@ def equal_stats(x,y): """) test.write(['test2', 'SConscript'], """\ +DefaultEnvironment(tools=[]) # test speedup env = Environment() foo_obj = env.Object('foo.c') env.Program('foo', [foo_obj, 'bar.c']) diff --git a/test/VariantDir/VariantDir.py b/test/VariantDir/VariantDir.py index bd329d8b1b..ef10022ae2 100644 --- a/test/VariantDir/VariantDir.py +++ b/test/VariantDir/VariantDir.py @@ -94,8 +94,8 @@ def buildIt(target, source, env): if not os.path.exists('build'): os.mkdir('build') - f1=open(str(source[0]), 'r') - f2=open(str(target[0]), 'w') + f1=open(source[0], 'r') + f2=open(target[0], 'w') f2.write(f1.read()) f2.close() f1.close() @@ -119,7 +119,7 @@ def buildIt(target, source, env): if fortran and env.Detect(fortran): if sys.platform =='win32': - env_prog = Environment(tools=['mingw'], + env_prog = Environment(tools=['mingw'], # BUILD = env['BUILD'], SRC = ENV['src'], CPPPATH=env['CPPPATH'], FORTRANPATH=env['FORTRANPATH'] ) else: @@ -276,8 +276,8 @@ def blank_output(err): def equal_stats(x,y): x = os.stat(x) y = os.stat(y) - return (stat.S_IMODE(x[stat.ST_MODE]) == stat.S_IMODE(y[stat.ST_MODE]) and - x[stat.ST_MTIME] == y[stat.ST_MTIME]) + return (stat.S_IMODE(x.st_mode) == stat.S_IMODE(y.st_mode) and + x.st_mtime == y.st_mtime) # Make sure we did duplicate the source files in build/var2, # and that their stats are the same: @@ -285,7 +285,7 @@ def equal_stats(x,y): test.must_exist(['work1', 'build', 'var2', 'f2.in']) test.fail_test(not equal_stats(test.workpath('work1', 'build', 'var2', 'f1.c'), test.workpath('work1', 'src', 'f1.c'))) test.fail_test(not equal_stats(test.workpath('work1', 'build', 'var2', 'f2.in'), test.workpath('work1', 'src', 'f2.in'))) - + # Make sure we didn't duplicate the source files in build/var3. test.must_not_exist(['work1', 'build', 'var3', 'f1.c']) test.must_not_exist(['work1', 'build', 'var3', 'f2.in']) @@ -381,7 +381,7 @@ def equal_stats(x,y): test.write( ['work3', 'SConstruct'], """\ SConscriptChdir(0) -VariantDir('build', '.', duplicate=1 ) +VariantDir('build', '.', duplicate=1 ) SConscript( 'build/SConscript' ) """) diff --git a/test/VariantDir/errors.py b/test/VariantDir/errors.py index 26ef4a2312..b50eb318c7 100644 --- a/test/VariantDir/errors.py +++ b/test/VariantDir/errors.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Validate successful handling of errors when duplicating things in @@ -34,9 +33,13 @@ import stat import sys import TestSCons +from TestCmd import IS_ROOT test = TestSCons.TestSCons() +if IS_ROOT: + test.skip_test('SConscript permissions meaningless when running as root; skipping test.\n') + for dir in ['normal', 'ro-dir', 'ro-SConscript', 'ro-src']: test.subdir(dir, [dir, 'src']) @@ -44,7 +47,7 @@ import os.path VariantDir('build', 'src') SConscript(os.path.join('build', 'SConscript')) -""") +""") test.write([dir, 'src', 'SConscript'], """\ def fake_scan(node, env, target): @@ -56,12 +59,12 @@ def fake_scan(node, env, target): return [] def cat(env, source, target): - target = str(target[0]) - with open(target, "w") as f: + with open(target[0], "w") as f: for src in source: - with open(str(src), "r") as f2: + with open(src, "r") as f2: f.write(f2.read()) +DefaultEnvironment(tools=[]) # test speedup env = Environment(BUILDERS={'Build':Builder(action=cat)}, SCANNERS=[Scanner(fake_scan, skeys = ['.in'])]) @@ -85,7 +88,7 @@ def cat(env, source, target): if sys.platform != 'win32': dir = os.path.join('ro-dir', 'build') test.subdir(dir) - os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR) + os.chmod(dir, os.stat(dir).st_mode & ~stat.S_IWUSR) test.run(chdir = 'ro-dir', arguments = ".", @@ -100,16 +103,16 @@ def cat(env, source, target): test.subdir(dir) SConscript = test.workpath(dir, 'SConscript') test.write(SConscript, '') -os.chmod(SConscript, os.stat(SConscript)[stat.ST_MODE] & ~stat.S_IWUSR) +os.chmod(SConscript, os.stat(SConscript).st_mode & ~stat.S_IWUSR) with open(SConscript, 'r'): - os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR) + os.chmod(dir, os.stat(dir).st_mode & ~stat.S_IWUSR) test.run(chdir = 'ro-SConscript', arguments = ".", status = 2, stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied. Stop.\n" % os.path.join('src', 'SConscript')) - os.chmod('ro-SConscript', os.stat('ro-SConscript')[stat.ST_MODE] | stat.S_IWUSR) + os.chmod('ro-SConscript', os.stat('ro-SConscript').st_mode | stat.S_IWUSR) test.run(chdir = 'ro-SConscript', arguments = ".", @@ -127,9 +130,9 @@ def cat(env, source, target): test.write([dir, 'SConscript'], '') file_in = test.workpath(dir, 'file.in') test.write(file_in, '') -os.chmod(file_in, os.stat(file_in)[stat.ST_MODE] & ~stat.S_IWUSR) +os.chmod(file_in, os.stat(file_in).st_mode & ~stat.S_IWUSR) with open(file_in, 'r'): - os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR) + os.chmod(dir, os.stat(dir).st_mode & ~stat.S_IWUSR) test.run(chdir = 'ro-src', arguments = ".", diff --git a/test/Win32/default-drive.py b/test/Win32/default-drive.py index 7d19c57a5d..e1d8d4bf62 100644 --- a/test/Win32/default-drive.py +++ b/test/Win32/default-drive.py @@ -44,10 +44,9 @@ test.write(['src', 'SConstruct'], """ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as ofp: + with open(target[0], "wb") as ofp: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: ofp.write(ifp.read()) env = Environment(BUILDERS={'Build':Builder(action=cat)}) diff --git a/test/YACC/live-check-output-cleaned.py b/test/YACC/live-check-output-cleaned.py index aef47deb28..b4914a892e 100644 --- a/test/YACC/live-check-output-cleaned.py +++ b/test/YACC/live-check-output-cleaned.py @@ -42,7 +42,7 @@ DefaultEnvironment(tools=[]) foo = Environment(YACCFLAGS='-v -d', tools = ['default', 'yacc']) foo.CFile(source = 'foo.y') -""" % locals()) +""") yacc = r""" %%{ diff --git a/test/YACC/live.py b/test/YACC/live.py index 9214369561..e1393cffe2 100644 --- a/test/YACC/live.py +++ b/test/YACC/live.py @@ -65,8 +65,7 @@ return yyparse(); } -int yyerror(s) -char *s; +int yyerror(char *s) { fprintf(stderr, "%%s\n", s); return 0; diff --git a/test/ZIP/ZIP.py b/test/ZIP/ZIP.py index b79173c4a4..69f524e152 100644 --- a/test/ZIP/ZIP.py +++ b/test/ZIP/ZIP.py @@ -35,12 +35,13 @@ test.subdir('sub1') test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(tools = ['zip']) env.Zip(target = 'aaa.zip', source = ['file1', 'file2']) env.Zip(target = 'aaa.zip', source = 'file3') env.Zip(target = 'bbb', source = 'sub1') env.Zip(target = 'bbb', source = 'file4') -""" % locals()) +""") test.write('file1', "file1\n") test.write('file2', "file2\n") @@ -117,9 +118,9 @@ def marker(target, source, env): test.fail_test(test.zipfile_files("f3.xyzzy") != ['file16', 'file17', 'file18']) -f4_size = os.stat('f4.zip')[stat.ST_SIZE] -f4stored_size = os.stat('f4stored.zip')[stat.ST_SIZE] -f4deflated_size = os.stat('f4deflated.zip')[stat.ST_SIZE] +f4_size = os.stat('f4.zip').st_size +f4stored_size = os.stat('f4stored.zip').st_size +f4deflated_size = os.stat('f4deflated.zip').st_size test.fail_test(f4_size != f4deflated_size) test.fail_test(f4stored_size == f4deflated_size) diff --git a/test/ZIP/ZIPROOT.py b/test/ZIP/ZIPROOT.py index 10e01d950f..42579c2ac2 100644 --- a/test/ZIP/ZIPROOT.py +++ b/test/ZIP/ZIPROOT.py @@ -37,10 +37,11 @@ test.subdir(['sub1', 'sub2']) test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(tools = ['zip']) env.Zip(target = 'aaa.zip', source = ['sub1/file1'], ZIPROOT='sub1') env.Zip(target = 'bbb.zip', source = ['sub1/file2', 'sub1/sub2/file2'], ZIPROOT='sub1') -""" % locals()) +""") test.write(['sub1', 'file1'], "file1\n") test.write(['sub1', 'file2'], "file2a\n") diff --git a/test/ZIP/ZIP_OVERRIDE_TIMESTAMP.py b/test/ZIP/ZIP_OVERRIDE_TIMESTAMP.py index 89049445dd..1c9a2c0426 100644 --- a/test/ZIP/ZIP_OVERRIDE_TIMESTAMP.py +++ b/test/ZIP/ZIP_OVERRIDE_TIMESTAMP.py @@ -39,14 +39,15 @@ def zipfile_get_file_datetime(zipfilename, fname): for info in zf.infolist(): if info.filename == fname: return info.date_time - + raise Exception("Unable to find %s" % fname) test.write('SConstruct', """ +_ = DefaultEnvironment(tools=[]) env = Environment(tools = ['zip']) env.Zip(target = 'aaa.zip', source = ['file1'], ZIP_OVERRIDE_TIMESTAMP=(1983,3,11,1,2,2)) -""" % locals()) +""") test.write(['file1'], "file1\n") diff --git a/test/bad-variables.py b/test/bad-variables.py index 11f2be1af7..06f106447c 100644 --- a/test/bad-variables.py +++ b/test/bad-variables.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that setting illegal construction variables fails in ways that are @@ -37,13 +36,14 @@ SConscript_path = test.workpath('SConscript') test.write(SConstruct_path, """\ -env = Environment() +_ = DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['foo-bar'] = 1 """) expect_stderr = """ -scons: *** Illegal construction variable `foo-bar' -""" + test.python_file_line(SConstruct_path, 2) +scons: *** Illegal construction variable 'foo-bar' +""" + test.python_file_line(SConstruct_path, 3) test.run(arguments='.', status=2, stderr=expect_stderr) @@ -54,14 +54,15 @@ """) test.write('SConscript', """\ -env = Environment() +_ = DefaultEnvironment(tools=[]) +env = Environment(tools=[]) env['foo(bar)'] = 1 """) expect_stderr = """ -scons: *** Illegal construction variable `foo(bar)' -""" + test.python_file_line(SConscript_path, 2) +scons: *** Illegal construction variable 'foo(bar)' +""" + test.python_file_line(SConscript_path, 3) test.run(arguments='.', status=2, stderr=expect_stderr) diff --git a/test/chained-build.py b/test/chained-build.py index 10d0b46854..2cc3106771 100644 --- a/test/chained-build.py +++ b/test/chained-build.py @@ -37,7 +37,7 @@ SConstruct1_contents = """\ def build(env, target, source): - with open(str(target[0]), 'wt') as fo, open(str(source[0]), 'rt') as fi: + with open(target[0], 'wt') as fo, open(source[0], 'rt') as fi: fo.write(fi.read()) env=Environment(BUILDERS={'B' : Builder(action=build)}) @@ -46,7 +46,7 @@ def build(env, target, source): SConstruct2_contents = """\ def build(env, target, source): - with open(str(target[0]), 'wt') as fo, open(str(source[0]), 'rt') as fi: + with open(target[0], 'wt') as fo, open(source[0], 'rt') as fi: fo.write(fi.read()) env=Environment(BUILDERS={'B' : Builder(action=build)}) diff --git a/test/duplicate-sources.py b/test/duplicate-sources.py index 3ad29287c4..48a106dc53 100644 --- a/test/duplicate-sources.py +++ b/test/duplicate-sources.py @@ -35,9 +35,9 @@ test.write('SConstruct', """\ def cat(target, source, env): - with open(str(target[0]), 'wb') as t: - for s in source: - with open(str(s), 'rb') as s: + with open(target[0], 'wb') as t: + for src in source: + with open(src, 'rb') as s: t.write(s.read()) env = Environment(BUILDERS = {'Cat' : Builder(action = cat)}) env.Cat('out.txt', ['f1.in', 'f2.in', 'f1.in']) diff --git a/test/emitter.py b/test/emitter.py index d2a8b67b72..e454dc7110 100644 --- a/test/emitter.py +++ b/test/emitter.py @@ -41,7 +41,7 @@ test.write('src/SConscript',""" def build(target, source, env): for t in target: - with open(str(t), "wt") as f: + with open(t, "wt") as f: f.write(str(t)) def emitter(target, source, env): diff --git a/test/fixture/SConstruct_HELP_AddOption.py b/test/fixture/SConstruct_HELP_AddOption.py new file mode 100644 index 0000000000..f4ac237a22 --- /dev/null +++ b/test/fixture/SConstruct_HELP_AddOption.py @@ -0,0 +1,22 @@ + +AddOption( + '--debugging', + dest='debugging', + action='store_true', + default=False, + metavar='BDEBUGGING', + help='Compile with debugging symbols', +) + +vars = Variables() +vars.Add(ListVariable('buildmod', 'List of modules to build', 'none', ['python'])) +DefaultEnvironment(tools=[]) +env = Environment() + +if ARGUMENTS.get('NO_APPEND',False): + Help(vars.GenerateHelpText(env), append=False) +elif ARGUMENTS.get('LOCAL_ONLY',False): + Help(vars.GenerateHelpText(env), append=True, local_only=True) +else: + Help(vars.GenerateHelpText(env), append=True) + diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct.py b/test/fixture/no_msvc/no_msvcs_sconstruct.py index 1aed9ecc1c..8dba65acba 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere env = SCons.Environment.Environment() print('MSVC_VERSION=' + str(env.get('MSVC_VERSION'))) diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py index fc5558b54a..9fe9440321 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_query_toolset_version.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere msvc_version, msvc_toolset_version = SCons.Tool.MSCommon.msvc_query_version_toolset() print(f'msvc_version={msvc_version!r}, msvc_toolset_version={msvc_toolset_version!r}') diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py index 6e7562d390..1880b8bf7a 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_sdk_versions.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere sdk_version_list = SCons.Tool.MSCommon.msvc_sdk_versions() print('sdk_version_list=' + repr(sdk_version_list)) diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py index c2208cd393..35a45c3b62 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct_msvc_toolset_versions.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere toolset_version_list = SCons.Tool.MSCommon.msvc_toolset_versions() print('toolset_version_list=' + repr(toolset_version_list)) diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py b/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py index 2f2f07aedc..baac01bb2c 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct_tools.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere env = SCons.Environment.Environment(tools=['myignoredefaultmsvctool']) diff --git a/test/fixture/no_msvc/no_msvcs_sconstruct_version.py b/test/fixture/no_msvc/no_msvcs_sconstruct_version.py index bd81688e35..3bf15d35e9 100644 --- a/test/fixture/no_msvc/no_msvcs_sconstruct_version.py +++ b/test/fixture/no_msvc/no_msvcs_sconstruct_version.py @@ -7,7 +7,7 @@ DefaultEnvironment(tools=[]) -def DummyVsWhere(msvc_version, env): +def DummyVsWhere(msvc_version, vswhere_exe): # not testing versions with vswhere, so return none return None @@ -16,6 +16,6 @@ def DummyVsWhere(msvc_version, env): (SCons.Util.HKEY_LOCAL_MACHINE, r'') ] -SCons.Tool.MSCommon.vc.find_vc_pdir_vswhere = DummyVsWhere +SCons.Tool.MSCommon.vc._find_vc_pdir_vswhere = DummyVsWhere SCons.Tool.MSCommon.msvc_set_notfound_policy('error') env = SCons.Environment.Environment(MSVC_VERSION='14.3') diff --git a/test/gettext/POTUpdate/UserExamples.py b/test/gettext/POTUpdate/UserExamples.py index 06203fafa7..a3075ebca1 100644 --- a/test/gettext/POTUpdate/UserExamples.py +++ b/test/gettext/POTUpdate/UserExamples.py @@ -1,6 +1,8 @@ -2#!/usr/bin/env python +#!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ End-to-end tests for POTUpdate. Assure, that the examples from user's @@ -44,6 +43,7 @@ test.subdir(['ex1', 'po']) test.write( ['ex1', 'po', 'SConstruct' ], """ +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env.POTUpdate(['foo'], ['../a.cpp', '../b.cpp']) env.POTUpdate(['bar'], ['../c.cpp', '../d.cpp']) @@ -88,6 +88,7 @@ test.subdir(['ex2']) test.write( ['ex2', 'SConstruct'], """ +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env['POTDOMAIN'] = "foo" env.POTUpdate(source = ["a.cpp", "b.cpp"]) # Creates foo.pot ... @@ -121,6 +122,7 @@ test.write( ['ex3', 'po', 'SConstruct'], """ # SConstruct file in 'po/' subdirectory +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env.POTUpdate(XGETTEXTFROM = 'POTFILES.in') """) @@ -147,6 +149,7 @@ test.write( ['ex4', 'po', 'SConstruct'], """ # SConstruct file in 'po/' subdirectory +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH='../') """) @@ -176,6 +179,7 @@ test.write( ['ex5', '0', '1', 'po', 'SConstruct'], """ # SConstruct file in '0/1/po/' subdirectory +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../', '../../']) """) @@ -191,14 +195,15 @@ # scons 'pot-update' creates messages.pot test.run(arguments = 'pot-update', chdir = path.join('ex5', '0', '1', 'po')) test.must_exist( ['ex5', '0', '1', 'po', 'messages.pot']) -test.must_contain( ['ex5', '0', '1', 'po', 'messages.pot'], +test.must_contain( ['ex5', '0', '1', 'po', 'messages.pot'], 'Hello from ../a.cpp', mode='r' ) -test.must_not_contain( ['ex5', '0', '1', 'po', 'messages.pot'], +test.must_not_contain( ['ex5', '0', '1', 'po', 'messages.pot'], 'Hello from ../../a.cpp', mode='r' ) test.write(['ex5', '0', '1', 'po', 'SConstruct'], """ # SConstruct file in '0/1/po/' subdirectory +_ = DefaultEnvironment(tools=[]) env = Environment( tools = ['default', 'xgettext'] ) env.POTUpdate(XGETTEXTFROM = 'POTFILES.in', XGETTEXTPATH=['../../', '../']) """) diff --git a/test/gettext/POUpdate/UserExamples.py b/test/gettext/POUpdate/UserExamples.py index be38996d57..00b2e0c21e 100644 --- a/test/gettext/POUpdate/UserExamples.py +++ b/test/gettext/POUpdate/UserExamples.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,14 +22,17 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ -Make sure, that the examples given in user guide all work. +Make sure the gettext examples given in the user guide all work. """ +# Note the layout here is not ideal, there are three different checks +# for an external program, if missing the test is skipped. Normally +# that means we should be three separate tests, so everyone can run +# the tests they're able to. Let it go this time: msgmerge, +# msginit and gettext are pretty likely to all be installed if one is. + import TestSCons test = TestSCons.TestSCons() @@ -166,10 +171,10 @@ # POUpdate: Example 1 ############################################################################# test.subdir(['ex1']) -test.write( ['ex1', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(['en','pl']) # messages.pot --> [en.po, pl.po] +test.write(['ex1', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(['en', 'pl']) # messages.pot --> [en.po, pl.po] """) test.write(['ex1', 'messages.pot'], pot_contents) test.write(['ex1', 'en.po'], en_po_contents) @@ -178,20 +183,20 @@ # NOTE: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex1', stderr = None) -test.must_exist( ['ex1', 'en.po'] ) -test.must_exist( ['ex1', 'pl.po'] ) -test.must_contain( ['ex1', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex1', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex1', stderr=None) +test.must_exist(['ex1', 'en.po']) +test.must_exist(['ex1', 'pl.po']) +test.must_contain(['ex1', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex1', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 2 ############################################################################# test.subdir(['ex2']) -test.write( ['ex2', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(['en','pl'], ['foo']) # foo.pot --> [en.po, pl.po] +test.write(['ex2', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(['en', 'pl'], ['foo']) # foo.pot --> [en.po, pl.po] """) # test.write(['ex2', 'foo.pot'], pot_contents) @@ -201,20 +206,20 @@ # NOTE: msgmerge(1) prints all messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex2', stderr = None) -test.must_exist( ['ex2', 'en.po'] ) -test.must_exist( ['ex2', 'pl.po'] ) -test.must_contain( ['ex2', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex2', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex2', stderr=None) +test.must_exist(['ex2', 'en.po']) +test.must_exist(['ex2', 'pl.po']) +test.must_contain(['ex2', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex2', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 3 ############################################################################# test.subdir(['ex3']) -test.write( ['ex3', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(['en','pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] +test.write(['ex3', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(['en', 'pl'], POTDOMAIN='foo') # foo.pot --> [en.po, pl.po] """) # test.write(['ex3', 'foo.pot'], pot_contents) @@ -224,24 +229,23 @@ # NOTE: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex3', stderr = None) -test.must_exist( ['ex3', 'en.po'] ) -test.must_exist( ['ex3', 'pl.po'] ) -test.must_contain( ['ex3', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex3', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex3', stderr=None) +test.must_exist(['ex3', 'en.po']) +test.must_exist(['ex3', 'pl.po']) +test.must_contain(['ex3', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex3', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 4 ############################################################################# test.subdir(['ex4']) -test.write( ['ex4', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(LINGUAS_FILE = 1) # needs 'LINGUAS' file +test.write(['ex4', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(LINGUAS_FILE=1) # needs 'LINGUAS' file """) # -test.write(['ex4', 'LINGUAS'], -""" +test.write(['ex4', 'LINGUAS'], """\ en pl """) @@ -253,24 +257,23 @@ # NOTE: msgmerge(1) prints all messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex4', stderr = None) -test.must_exist( ['ex4', 'messages.pot'] ) -test.must_exist( ['ex4', 'en.po'] ) -test.must_exist( ['ex4', 'pl.po'] ) -test.must_contain( ['ex4', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex4', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex4', stderr=None) +test.must_exist(['ex4', 'messages.pot']) +test.must_exist(['ex4', 'en.po']) +test.must_exist(['ex4', 'pl.po']) +test.must_contain(['ex4', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex4', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 5 ############################################################################# test.subdir(['ex5']) -test.write( ['ex5', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(LINGUAS_FILE = 1, source = ['foo']) +test.write(['ex5', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(LINGUAS_FILE=1, source=['foo']) """) -test.write(['ex5', 'LINGUAS'], -""" +test.write(['ex5', 'LINGUAS'], """\ en pl """) @@ -282,23 +285,22 @@ # NOTE: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir= 'ex5', stderr = None) -test.must_exist( ['ex5', 'en.po'] ) -test.must_exist( ['ex5', 'pl.po'] ) -test.must_contain( ['ex5', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex5', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex5', stderr=None) +test.must_exist(['ex5', 'en.po']) +test.must_exist(['ex5', 'pl.po']) +test.must_contain(['ex5', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex5', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 6 ############################################################################# test.subdir(['ex6']) -test.write( ['ex6', 'SConstruct'], -""" -env = Environment( tools = ["default", "msgmerge"] ) -env.POUpdate(['en', 'pl'], LINGUAS_FILE = 1) +test.write(['ex6', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msgmerge"]) +env.POUpdate(['en', 'pl'], LINGUAS_FILE=1) """) -test.write(['ex6', 'LINGUAS'], -""" +test.write(['ex6', 'LINGUAS'], """\ de fr """) @@ -311,15 +313,15 @@ # Note: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex6', stderr = None) -test.must_exist( ['ex6', 'en.po'] ) -test.must_exist( ['ex6', 'pl.po'] ) -test.must_exist( ['ex6', 'de.po'] ) -test.must_exist( ['ex6', 'fr.po'] ) -test.must_contain( ['ex6', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex6', 'pl.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex6', 'de.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex6', 'fr.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex6', stderr=None) +test.must_exist(['ex6', 'en.po']) +test.must_exist(['ex6', 'pl.po']) +test.must_exist(['ex6', 'de.po']) +test.must_exist(['ex6', 'fr.po']) +test.must_contain(['ex6', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex6', 'pl.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex6', 'de.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex6', 'fr.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 7 @@ -331,13 +333,12 @@ test.skip_test("could not find 'msginit'; skipping test(s)\n") ### test.subdir(['ex7']) -test.write( ['ex7', 'SConstruct'], -""" -env = Environment( tools = ["default", "msginit", "msgmerge"] ) -env.POUpdate(LINGUAS_FILE = 1, POAUTOINIT = 1) +test.write(['ex7', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "msginit", "msgmerge"]) +env.POUpdate(LINGUAS_FILE=1, POAUTOINIT=1) """) -test.write(['ex7', 'LINGUAS'], -""" +test.write(['ex7', 'LINGUAS'], """\ en pl """) @@ -347,11 +348,11 @@ # NOTE: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir= 'ex7', stderr = None) -test.must_exist( ['ex7', 'en.po'] ) -test.must_exist( ['ex7', 'pl.po'] ) -test.must_contain( ['ex7', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex7', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex7', stderr=None) +test.must_exist(['ex7', 'en.po']) +test.must_exist(['ex7', 'pl.po']) +test.must_contain(['ex7', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex7', 'pl.po'], "Hello from a.cpp", mode='r') ############################################################################# # POUpdate: Example 8 @@ -363,19 +364,18 @@ test.skip_test("could not find 'xgettext'; skipping test(s)\n") ### test.subdir(['ex8']) -test.write( ['ex8', 'SConstruct'], -""" -env = Environment( tools = ["default", "xgettext", "msginit", "msgmerge"] ) +test.write(['ex8', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) +env = Environment(tools=["default", "xgettext", "msginit", "msgmerge"]) # script-wise settings env['POAUTOINIT'] = 1 env['LINGUAS_FILE'] = 1 env['POTDOMAIN'] = 'foo' -env.POTUpdate(source = 'a.cpp') +env.POTUpdate(source='a.cpp') env.POUpdate() """) -test.write(['ex8', 'LINGUAS'], -""" +test.write(['ex8', 'LINGUAS'], """\ en pl """) @@ -384,12 +384,12 @@ # Note: msgmerge(1) prints its messages to stderr, we must ignore them, # So, stderr=None is crucial here. It is no point to match stderr to some # specific valuse; the messages are internationalized :) ). -test.run(arguments = 'po-update', chdir = 'ex8', stderr = None) -test.must_exist( ['ex8', 'foo.pot'] ) -test.must_exist( ['ex8', 'en.po'] ) -test.must_exist( ['ex8', 'pl.po'] ) -test.must_contain( ['ex8', 'en.po'], "Hello from a.cpp", mode='r' ) -test.must_contain( ['ex8', 'pl.po'], "Hello from a.cpp", mode='r' ) +test.run(arguments='po-update', chdir='ex8', stderr=None) +test.must_exist(['ex8', 'foo.pot']) +test.must_exist(['ex8', 'en.po']) +test.must_exist(['ex8', 'pl.po']) +test.must_contain(['ex8', 'en.po'], "Hello from a.cpp", mode='r') +test.must_contain(['ex8', 'pl.po'], "Hello from a.cpp", mode='r') test.pass_test() diff --git a/test/implicit-cache/DualTargets.py b/test/implicit-cache/DualTargets.py index 8612d1a531..174d39c149 100644 --- a/test/implicit-cache/DualTargets.py +++ b/test/implicit-cache/DualTargets.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Test that --implicit-cache works correctly in conjonction with a @@ -53,9 +52,9 @@ def source_scan(node, env, path): env = Environment(tools=[]) env['BUILDERS']['DualTarget'] = Builder( action = Action( - [ - Copy( '$TARGET', '$SOURCE' ), - Copy( '${TARGET.base}.b', '$SOURCE' ), + [ + Copy( '$TARGET', '$SOURCE' ), + Copy( '${TARGET.base}.b', '$SOURCE' ), ], ), suffix = '.a', @@ -69,7 +68,7 @@ def source_scan(node, env, path): env.Command( 'x.lib', '', Touch('$TARGET') ) env.DualTarget('x.cpp') -""" % locals()) +""") test.must_not_exist('x.cpp') test.must_not_exist('x.lib') diff --git a/test/implicit-cache/basic.py b/test/implicit-cache/basic.py index 7eece88fe8..aa25327f97 100644 --- a/test/implicit-cache/basic.py +++ b/test/implicit-cache/basic.py @@ -62,7 +62,7 @@ SConscript('variant/SConscript', "env") def copy(target, source, env): - with open(str(target[0]), 'wt') as fo, open(str(source[0]), 'rt') as fi: + with open(target[0], 'wt') as fo, open(source[0], 'rt') as fi: fo.write(fi.read()) nodep = env.Command('nodeps.c', 'nodeps.in', action=copy) env.Program('nodeps', 'nodeps.c') diff --git a/test/implicit/changed-node.py b/test/implicit/changed-node.py index d89c14b8af..99ea01b0d7 100644 --- a/test/implicit/changed-node.py +++ b/test/implicit/changed-node.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that we don't throw an exception if a stored implicit @@ -46,13 +45,12 @@ def lister(target, source, env): import os - with open(str(target[0]), 'w') as ofp: - s = str(source[0]) - if os.path.isdir(s): - for l in os.listdir(str(source[0])): + with open(target[0], 'w') as ofp: + if os.path.isdir(source[0]): + for l in os.listdir(source[0]): ofp.write(l + '\\n') else: - ofp.write(s + '\\n') + ofp.write(f'{source[0]}\\n') builder = Builder(action=lister, source_factory=Dir, @@ -77,18 +75,18 @@ def lister(target, source, env): test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) SetOption('implicit_cache', 1) SetOption('max_drift', 1) def lister(target, source, env): import os.path - with open(str(target[0]), 'w') as ofp: - s = str(source[0]) - if os.path.isdir(s): - for l in os.listdir(str(source[0])): + with open(target[0], 'w') as ofp: + if os.path.isdir(source[0]): + for l in os.listdir(source[0]): ofp.write(l + '\\n') else: - ofp.write(s + '\\n') + ofp.write(f'{source[0]}\\n') builder = Builder(action=lister, source_factory=File) diff --git a/test/ninja/build_libraries.py b/test/ninja/build_libraries.py index 0a1941ab5a..9436053c7b 100644 --- a/test/ninja/build_libraries.py +++ b/test/ninja/build_libraries.py @@ -23,8 +23,6 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import os - import TestSCons from TestCmd import IS_WINDOWS, IS_MACOS from TestSCons import _exe, _lib, lib_, _dll, dll_ @@ -36,12 +34,7 @@ except ImportError: test.skip_test("Could not find ninja module. Skipping test.\n") -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/command_line_targets.py b/test/ninja/command_line_targets.py index 9fa77eae47..1a6033b9e0 100644 --- a/test/ninja/command_line_targets.py +++ b/test/ninja/command_line_targets.py @@ -36,12 +36,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/copy_function_command-4731.py b/test/ninja/copy_function_command-4731.py new file mode 100644 index 0000000000..108d82a854 --- /dev/null +++ b/test/ninja/copy_function_command-4731.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Regression test for issue #4731: almost the same as copy_function_command, +but uses a list action where the first command is Copy, +as opposed to a simple function action. This was failing. +""" + +import os + +import TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = TestSCons.NINJA_BINARY + +test.dir_fixture('ninja-fixture') + +test.write('SConstruct', """\ +SetOption('experimental', 'ninja') +DefaultEnvironment(tools=[]) +env = Environment() +env.Tool('ninja') +env.Command('foo2.c', ['foo.c'], [Copy('$TARGET', '$SOURCE'), Chmod('$TARGET', 0x755)]) +env.Program(target='foo', source='foo2.c') +""", +) + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), f'ninja{_exe} -f') +test.run(program=test.workpath('foo'), stdout="foo.c") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed foo2.o', + 'Removed foo2.c', + 'Removed foo' + _exe, + 'Removed build.ninja'] +) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('foo')) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('foo'), stdout="foo.c") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/copy_function_command.py b/test/ninja/copy_function_command.py index 7e999b33e5..ad2b7c6896 100644 --- a/test/ninja/copy_function_command.py +++ b/test/ninja/copy_function_command.py @@ -20,7 +20,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# import os @@ -37,29 +36,25 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') -test.write('SConstruct', """ -SetOption('experimental','ninja') +test.write('SConstruct', """\ +SetOption('experimental', 'ninja') DefaultEnvironment(tools=[]) env = Environment() env.Tool('ninja') env.Command('foo2.c', ['foo.c'], Copy('$TARGET','$SOURCE')) -env.Program(target = 'foo', source = 'foo2.c') -""") +env.Program(target='foo', source='foo2.c') +""", +) # generate simple build test.run(stdout=None) test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) test.must_contain_all(test.stdout(), 'Executing:') -test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.must_contain_all(test.stdout(), f'ninja{_exe} -f') test.run(program=test.workpath('foo'), stdout="foo.c") # clean build and ninja files @@ -68,7 +63,8 @@ 'Removed foo2.o', 'Removed foo2.c', 'Removed foo' + _exe, - 'Removed build.ninja']) + 'Removed build.ninja'] +) # only generate the ninja file test.run(arguments='--disable-execute-ninja', stdout=None) diff --git a/test/ninja/default_targets.py b/test/ninja/default_targets.py index dc7c7c1be2..e809d75379 100644 --- a/test/ninja/default_targets.py +++ b/test/ninja/default_targets.py @@ -20,7 +20,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# import os @@ -32,45 +31,40 @@ try: import ninja except ImportError: - test.skip_test("Could not find module in python") + test.skip_test("Could not find 'ninja'; skipping test.\n") _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') test.file_fixture('ninja_test_sconscripts/sconstruct_default_targets', 'SConstruct') +# this test has had some hangs on the GitHut Windows runner, add timeout. + # generate simple build -test.run(stdout=None) +test.run(stdout=None, timeout=120) test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) test.must_contain_all(test.stdout(), 'Executing:') -test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.must_contain_all(test.stdout(), f'ninja{_exe} -f') test.must_not_exist([test.workpath('out1.txt')]) test.must_exist([test.workpath('out2.txt')]) # clean build and ninja files -test.run(arguments='-c', stdout=None) -test.must_contain_all_lines(test.stdout(), [ - 'Removed out2.txt', - 'Removed build.ninja']) +test.run(arguments='-c', stdout=None, timeout=120) +test.must_contain_all_lines(test.stdout(), ['Removed out2.txt', 'Removed build.ninja']) # only generate the ninja file -test.run(arguments='--disable-execute-ninja', stdout=None) +test.run(arguments='--disable-execute-ninja', stdout=None, timeout=120) test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) test.must_not_exist(test.workpath('out1.txt')) test.must_not_exist(test.workpath('out2.txt')) # run ninja independently program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin -test.run(program=program, stdout=None) +test.run(program=program, stdout=None, timeout=120) test.must_not_exist([test.workpath('out1.txt')]) test.must_exist(test.workpath('out2.txt')) diff --git a/test/ninja/force_scons_callback-4730.py b/test/ninja/force_scons_callback-4730.py new file mode 100644 index 0000000000..acebb9049a --- /dev/null +++ b/test/ninja/force_scons_callback-4730.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Regression test for issue #4730: almost the same as force_scons_callback +function, but uses a target name with a space in it, to make sure +quoting is handled correctly all the way through. +""" + +import os + +import TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = TestSCons.NINJA_BINARY + +test.dir_fixture("ninja-fixture") + +test.file_fixture( + "ninja_test_sconscripts/sconstruct_force_scons_callback-4730", "SConstruct" +) + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ["Generating: build.ninja"]) +test.must_contain_all(test.stdout(), "Executing:") +test.must_contain_all(test.stdout(), "ninja%(_exe)s -f" % locals()) +defers = test.stdout().count("Defer to SCons to build") +if not defers: + test.fail_test(message="Did not see expected 'Defer to SCons' message.") +if defers > 1: + test.fail_test(message=f"Too many 'Defer to SCons' messages {defers}).") +test.must_match("out.txt", "foo.c" + os.linesep) +test.must_match("out words.txt", "test2.cpp" + os.linesep) + +# clean build and ninja files +test.run(arguments="-c", stdout=None) +test.must_contain_all_lines( + test.stdout(), + ["Removed out.txt", "Removed out words.txt", "Removed build.ninja"] +) + +# only generate the ninja file +test.run(arguments="--disable-execute-ninja", stdout=None) +test.must_contain_all_lines(test.stdout(), ["Generating: build.ninja"]) +test.must_not_exist(test.workpath("out.txt")) +test.must_not_exist(test.workpath("out words.txt")) + +# run ninja independently +program = test.workpath("run_ninja_env.bat") if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +defers = test.stdout().count("Defer to SCons to build") +if not defers: + test.fail_test(message=f"Too many 'Defer to SCons' messages {defers}).") +if defers > 1: + test.fail_test(message="Too many 'Defer to SCons' messages.") +test.must_match("out.txt", "foo.c" + os.linesep) +test.must_match("out words.txt", "test2.cpp" + os.linesep) + +# only generate the ninja file with specific NINJA_SCONS_DAEMON_PORT +test.run(arguments="PORT=9999 --disable-execute-ninja", stdout=None) +# Verify that port # propagates to call to ninja_run_daemon.py +test.must_contain(test.workpath("build.ninja"), "PORT = 9999") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/force_scons_callback.py b/test/ninja/force_scons_callback.py index e0da0ddd31..c37c464d9d 100644 --- a/test/ninja/force_scons_callback.py +++ b/test/ninja/force_scons_callback.py @@ -20,7 +20,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# import os @@ -37,9 +36,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath( - os.path.join(ninja.__file__, os.pardir, "data", "bin", "ninja" + _exe) -) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture("ninja-fixture") @@ -52,15 +49,19 @@ test.must_contain_all_lines(test.stdout(), ["Generating: build.ninja"]) test.must_contain_all(test.stdout(), "Executing:") test.must_contain_all(test.stdout(), "ninja%(_exe)s -f" % locals()) -if test.stdout().count("Defer to SCons to build") != 1: - test.fail_test() +defers = test.stdout().count("Defer to SCons to build") +if not defers: + test.fail_test(message="Did not see expected 'Defer to SCons' message.") +if defers > 1: + test.fail_test(message=f"Too many 'Defer to SCons' messages {defers}).") test.must_match("out.txt", "foo.c" + os.linesep) test.must_match("out2.txt", "test2.cpp" + os.linesep) # clean build and ninja files test.run(arguments="-c", stdout=None) test.must_contain_all_lines( - test.stdout(), ["Removed out.txt", "Removed out2.txt", "Removed build.ninja"] + test.stdout(), + ["Removed out.txt", "Removed out2.txt", "Removed build.ninja"] ) # only generate the ninja file @@ -72,8 +73,11 @@ # run ninja independently program = test.workpath("run_ninja_env.bat") if IS_WINDOWS else ninja_bin test.run(program=program, stdout=None) -if test.stdout().count("Defer to SCons to build") != 1: - test.fail_test() +defers = test.stdout().count("Defer to SCons to build") +if not defers: + test.fail_test(message=f"Too many 'Defer to SCons' messages {defers}).") +if defers > 1: + test.fail_test(message="Too many 'Defer to SCons' messages.") test.must_match("out.txt", "foo.c" + os.linesep) test.must_match("out2.txt", "test2.cpp" + os.linesep) diff --git a/test/ninja/generate_and_build.py b/test/ninja/generate_and_build.py index e1c26d4bb0..9d056836fa 100644 --- a/test/ninja/generate_and_build.py +++ b/test/ninja/generate_and_build.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/generate_and_build_cxx.py b/test/ninja/generate_and_build_cxx.py index 074a5cb9af..80faa4bbe1 100644 --- a/test/ninja/generate_and_build_cxx.py +++ b/test/ninja/generate_and_build_cxx.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/generate_source.py b/test/ninja/generate_source.py index 8300176c2e..b848b17c14 100644 --- a/test/ninja/generate_source.py +++ b/test/ninja/generate_source.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/generated_sources_alias.py b/test/ninja/generated_sources_alias.py index 2c4ed36ae0..22ca734b33 100644 --- a/test/ninja/generated_sources_alias.py +++ b/test/ninja/generated_sources_alias.py @@ -37,9 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.BIN_DIR, - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/iterative_speedup.py b/test/ninja/iterative_speedup.py index e5673b0b42..b787958ed2 100644 --- a/test/ninja/iterative_speedup.py +++ b/test/ninja/iterative_speedup.py @@ -39,12 +39,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') @@ -149,7 +144,7 @@ def mod_source_orig(test_num): """ % locals()) -num_source = 200 +num_source = 250 for i in range(1, num_source + 1): generate_source(i - 1, i) diff --git a/test/ninja/mingw_command_generator_action.py b/test/ninja/mingw_command_generator_action.py index 58c5106427..ad7878469a 100644 --- a/test/ninja/mingw_command_generator_action.py +++ b/test/ninja/mingw_command_generator_action.py @@ -52,9 +52,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.BIN_DIR, - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/mingw_depfile_format.py b/test/ninja/mingw_depfile_format.py index 5de7b5fbca..7792440d0e 100644 --- a/test/ninja/mingw_depfile_format.py +++ b/test/ninja/mingw_depfile_format.py @@ -36,9 +36,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.BIN_DIR, - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/mkdir_function_command.py b/test/ninja/mkdir_function_command.py index 8a17623e7f..3fd678a3f8 100644 --- a/test/ninja/mkdir_function_command.py +++ b/test/ninja/mkdir_function_command.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.write('SConstruct', """ SetOption('experimental','ninja') diff --git a/test/ninja/multi_env.py b/test/ninja/multi_env.py index e5da6cf885..7b113ed22b 100644 --- a/test/ninja/multi_env.py +++ b/test/ninja/multi_env.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test = TestSCons.TestSCons() diff --git a/test/ninja/ninja_command_line.py b/test/ninja/ninja_command_line.py index d8e3c08107..8fc232e428 100644 --- a/test/ninja/ninja_command_line.py +++ b/test/ninja/ninja_command_line.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture', 'src') diff --git a/test/ninja/ninja_conftest.py b/test/ninja/ninja_conftest.py index 91d2e03b9b..60c4b033ee 100644 --- a/test/ninja/ninja_conftest.py +++ b/test/ninja/ninja_conftest.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/ninja_file_deterministic.py b/test/ninja/ninja_file_deterministic.py index 9832f226ac..3d226ca4e3 100644 --- a/test/ninja/ninja_file_deterministic.py +++ b/test/ninja/ninja_file_deterministic.py @@ -39,9 +39,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.BIN_DIR, - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/ninja_handle_control_c_rebuild.py b/test/ninja/ninja_handle_control_c_rebuild.py index 9f6b41366c..d187c5f437 100644 --- a/test/ninja/ninja_handle_control_c_rebuild.py +++ b/test/ninja/ninja_handle_control_c_rebuild.py @@ -23,7 +23,7 @@ # """ This test ensures if ninja gets a control-c (or other interrupting signal) while -regenerating the build.ninja, it doesn't remove the build.ninja leaving it +regenerating the build.ninja, it doesn't remove the build.ninja leaving it in an unworkable state. """ import os @@ -40,10 +40,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe - -ninja_bin = os.path.abspath( - os.path.join(ninja.__file__, os.pardir, "data", "bin", "ninja" + _exe) -) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture("ninja-fixture") diff --git a/test/ninja/ninja_test_sconscripts/sconstruct_default_targets b/test/ninja/ninja_test_sconscripts/sconstruct_default_targets index 1a92be3a5a..ebcc563ce1 100644 --- a/test/ninja/ninja_test_sconscripts/sconstruct_default_targets +++ b/test/ninja/ninja_test_sconscripts/sconstruct_default_targets @@ -4,13 +4,14 @@ import SCons -SetOption('experimental','ninja') +SetOption('experimental', 'ninja') DefaultEnvironment(tools=[]) - env = Environment(tools=[]) env.Tool('ninja') -env.Command('out1.txt', 'foo.c', 'echo test > $TARGET' ) -out2_node = env.Command('out2.txt', 'foo.c', 'echo test > $TARGET', NINJA_FORCE_SCONS_BUILD=True) +env.Command('out1.txt', 'foo.c', 'echo test > $TARGET') +out2_node = env.Command( + 'out2.txt', 'foo.c', 'echo test > $TARGET', NINJA_FORCE_SCONS_BUILD=True +) alias = env.Alias('def', out2_node) env.Default(alias) diff --git a/test/ninja/ninja_test_sconscripts/sconstruct_force_scons_callback-4730 b/test/ninja/ninja_test_sconscripts/sconstruct_force_scons_callback-4730 new file mode 100644 index 0000000000..d9b3153a39 --- /dev/null +++ b/test/ninja/ninja_test_sconscripts/sconstruct_force_scons_callback-4730 @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: MIT +# +# Copyright The SCons Foundation + +SetOption("experimental", "ninja") +DefaultEnvironment(tools=[]) + +env = Environment(tools=[]) +daemon_port = ARGUMENTS.get("PORT", False) +if daemon_port: + env["NINJA_SCONS_DAEMON_PORT"] = int(daemon_port) +env.Tool("ninja") + +env.Command("out.txt", "foo.c", "echo $SOURCE> $TARGET") +env.Command("out words.txt", "test2.cpp", "echo $SOURCE> $TARGET", NINJA_FORCE_SCONS_BUILD=True) diff --git a/test/ninja/ninja_test_sconscripts/sconstruct_ninja_determinism b/test/ninja/ninja_test_sconscripts/sconstruct_ninja_determinism index a7982d4067..6d206364ca 100644 --- a/test/ninja/ninja_test_sconscripts/sconstruct_ninja_determinism +++ b/test/ninja/ninja_test_sconscripts/sconstruct_ninja_determinism @@ -5,12 +5,10 @@ import random SetOption('experimental', 'ninja') -SetOption('skip_ninja_regen', True) DefaultEnvironment(tools=[]) - env = Environment(tools=[]) - env.Tool('ninja') +SetOption('skip_ninja_regen', True) # must wait until tool creates the option # make the dependency list vary in order. Ninja tool should sort them to be deterministic. for i in range(1, 10): diff --git a/test/ninja/no_for_sig_subst.py b/test/ninja/no_for_sig_subst.py index a0292ae029..ef2c795991 100644 --- a/test/ninja/no_for_sig_subst.py +++ b/test/ninja/no_for_sig_subst.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/response_file.py b/test/ninja/response_file.py index 3d23c2b2ae..f2561bbfa9 100644 --- a/test/ninja/response_file.py +++ b/test/ninja/response_file.py @@ -42,12 +42,7 @@ _exe = TestSCons._exe _obj = TestSCons._obj -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/shell_command.py b/test/ninja/shell_command.py index a6c48c4288..8b3d134298 100644 --- a/test/ninja/shell_command.py +++ b/test/ninja/shell_command.py @@ -37,12 +37,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath(os.path.join( - ninja.__file__, - os.pardir, - 'data', - 'bin', - 'ninja' + _exe)) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture('ninja-fixture') diff --git a/test/ninja/shutdown_scons_daemon.py b/test/ninja/shutdown_scons_daemon.py index 64ec2c717a..c646f970f8 100644 --- a/test/ninja/shutdown_scons_daemon.py +++ b/test/ninja/shutdown_scons_daemon.py @@ -44,9 +44,7 @@ _python_ = TestSCons._python_ _exe = TestSCons._exe -ninja_bin = os.path.abspath( - os.path.join(ninja.__file__, os.pardir, "data", "bin", "ninja" + _exe) -) +ninja_bin = TestSCons.NINJA_BINARY test.dir_fixture("ninja-fixture") diff --git a/test/no-arguments.py b/test/no-arguments.py index 5ed66abdad..588dabea73 100644 --- a/test/no-arguments.py +++ b/test/no-arguments.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that we use a default target of the current directory when there @@ -30,12 +29,11 @@ arguments, or a null command-line argument. """ - import TestSCons test = TestSCons.TestSCons() -test.write('SConstruct', r""" +test.write('SConstruct', """\ def cat(env, source, target): target = str(target[0]) source = list(map(str, source)) @@ -45,7 +43,8 @@ def cat(env, source, target): with open(src, "rb") as ifp: f.write(ifp.read()) -env = Environment(BUILDERS={'Build':Builder(action=cat)}) +DefaultEnvironment(tools=[]) +env = Environment(BUILDERS={'Build': Builder(action=cat)}, tools=[]) env.Build('aaa.out', 'aaa.in') """) @@ -53,21 +52,18 @@ def cat(env, source, target): up_to_date = test.wrap_stdout("scons: `.' is up to date.\n") -# test.run() test.must_match('aaa.out', "aaa.in\n") test.run(stdout=up_to_date) -# test.unlink('aaa.out') test.must_not_exist('aaa.out') -# test.run(['']) test.must_match('aaa.out', "aaa.in\n") + test.run([''], stdout=up_to_date) -# test.pass_test() # Local Variables: diff --git a/test/no-global-dependencies.py b/test/no-global-dependencies.py index 95761e7de3..541a645ec0 100644 --- a/test/no-global-dependencies.py +++ b/test/no-global-dependencies.py @@ -48,11 +48,12 @@ BoolVariable('duplicate', 'Duplicate sources to variant dir', True) ) +_ = DefaultEnvironment(tools=[]) env = Environment(options=opts) Export('env') SConscript(dirs='.', variant_dir='build', duplicate=env['duplicate']) -""" % locals()) +""") test.write('SConscript', """\ @@ -62,7 +63,7 @@ SConscript(dirs='dir1') SConscript(dirs='dir2') -""" % locals()) +""") test.write('dir1/SConscript', """\ Import('env') @@ -70,13 +71,13 @@ env.Command('x.cpp', '', Touch('$TARGET')) env.Object(env.File('x.cpp')) -""" % locals()) +""") test.write('dir2/SConscript', """\ Import('env') env.Object(env.File('#/build/dir1/x.cpp')) -""" % locals()) +""") test.must_not_exist('build/dir1/x.cpp') @@ -87,36 +88,36 @@ # # Build everything first. -test.run(arguments = 'duplicate=False view_all_dependencies=True .') +test.run(arguments='duplicate=False view_all_dependencies=True .') test.must_exist('build/dir1/x.cpp') test.must_not_contain_any_line(test.stdout(), ["`.' is up to date."]) # Double check that targets are not rebuilt. -test.run(arguments = 'duplicate=False view_all_dependencies=True .') +test.run(arguments='duplicate=False view_all_dependencies=True .') test.must_exist('build/dir1/x.cpp') test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) # Clean-up only the object file -test.run(arguments = 'duplicate=False view_all_dependencies=False -c .') +test.run(arguments='duplicate=False view_all_dependencies=False -c .') test.must_exist('build/dir1/x.cpp') # Rebuild the only object file without seeing all the dependencies. -test.run(arguments = 'duplicate=False view_all_dependencies=False .') +test.run(arguments='duplicate=False view_all_dependencies=False .') test.must_exist('build/dir1/x.cpp') test.must_not_contain_any_line(test.stdout(), ["`.' is up to date."]) # Double check that targets are not rebuilt without and with all the # dependencies. -test.run(arguments = 'duplicate=False view_all_dependencies=False .') +test.run(arguments='duplicate=False view_all_dependencies=False .') test.must_exist('build/dir1/x.cpp') test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) -test.run(arguments = 'duplicate=False view_all_dependencies=True .') +test.run(arguments='duplicate=False view_all_dependencies=True .') test.must_exist('build/dir1/x.cpp') test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) # Clean-up everything. -test.run(arguments = 'duplicate=False view_all_dependencies=True -c .') +test.run(arguments='duplicate=False view_all_dependencies=True -c .') test.must_not_exist('build/dir1/x.cpp') @@ -131,36 +132,36 @@ # # # Build everything first. -# test.run(arguments = 'duplicate=True view_all_dependencies=True .') +# test.run(arguments='duplicate=True view_all_dependencies=True .') # test.must_exist('build/dir1/x.cpp') # test.must_not_contain_any_line(test.stdout(), ["`.' is up to date."]) # # Double check that targets are not rebuilt. -# test.run(arguments = 'duplicate=True view_all_dependencies=True .') +# test.run(arguments='duplicate=True view_all_dependencies=True .') # test.must_exist('build/dir1/x.cpp') # test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) # # Clean-up only the object file -# test.run(arguments = 'duplicate=True view_all_dependencies=False -c .') +# test.run(arguments='duplicate=True view_all_dependencies=False -c .') # test.must_exist('build/dir1/x.cpp') # # Rebuild the only object file without seeing all the dependencies. -# test.run(arguments = 'duplicate=True view_all_dependencies=False .') +# test.run(arguments='duplicate=True view_all_dependencies=False .') # test.must_exist('build/dir1/x.cpp') # test.must_not_contain_any_line(test.stdout(), ["`.' is up to date."]) # # Double check that targets are not rebuilt without and with all the # # dependencies. -# test.run(arguments = 'duplicate=True view_all_dependencies=False .') +# test.run(arguments='duplicate=True view_all_dependencies=False .') # test.must_exist('build/dir1/x.cpp') # test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) -# test.run(arguments = 'duplicate=True view_all_dependencies=True .') +# test.run(arguments='duplicate=True view_all_dependencies=True .') # test.must_exist('build/dir1/x.cpp') # test.must_contain_all_lines(test.stdout(), ["`.' is up to date."]) # # Clean-up everything. -# test.run(arguments = 'duplicate=True view_all_dependencies=True -c .') +# test.run(arguments='duplicate=True view_all_dependencies=True -c .') # test.must_not_exist('build/dir1/x.cpp') diff --git a/test/no-target.py b/test/no-target.py index bf5b94ae35..e16c8c07f7 100644 --- a/test/no-target.py +++ b/test/no-target.py @@ -41,10 +41,9 @@ test.write(subdir_SConscript, r""" def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) b = Builder(action=cat, suffix='.out', src_suffix='.in') diff --git a/test/option/debug-memoizer.py b/test/option/debug-memoizer.py index 01af561b6f..7882646165 100644 --- a/test/option/debug-memoizer.py +++ b/test/option/debug-memoizer.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,26 +22,21 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" -""" -Test calling the --debug=memoizer option. -""" +"""Test calling the --debug=memoizer option.""" import os import TestSCons -test = TestSCons.TestSCons(match = TestSCons.match_re_dotall) - +test = TestSCons.TestSCons(match=TestSCons.match_re_dotall) test.write('SConstruct', """ -DefaultEnvironment(tools=[]) def cat(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as infp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as infp: f.write(infp.read()) + +DefaultEnvironment(tools=[]) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=Action(cat))}) env.Cat('file.out', 'file.in') """) @@ -51,36 +48,35 @@ def cat(target, source, env): # names in the implementation, so if we change them, we'll have to # change this test... expect = [ - "Memoizer (memory cache) hits and misses", - "Base.stat()", + # "Memoizer (memory cache) hits and misses", "Dir.srcdir_list()", + "File.stat()", "File.exists()", "Node._children_get()", ] - -for args in ['-h --debug=memoizer', '--debug=memoizer']: - test.run(arguments = args) - test.must_contain_any_line(test.stdout(), expect) - +test.run(arguments='--debug=memoizer') +test.must_contain_any_line(test.stdout(), expect) test.must_match('file.out', "file.in\n") - - - test.unlink("file.out") +# make sure it also works if memoizer is not the only debug flag +test.run(arguments='--debug=sconscript,memoizer') +test.must_contain_any_line(test.stdout(), expect) +test.must_match('file.out', "file.in\n") +test.unlink("file.out") +# memoization should still report even in help mode +test.run(arguments='-h --debug=memoizer') +test.must_contain_any_line(test.stdout(), expect) +test.must_not_exist("file.out") +# also try setting in SCONSFLAGS os.environ['SCONSFLAGS'] = '--debug=memoizer' - -test.run(arguments = '') - +test.run(arguments='.') test.must_contain_any_line(test.stdout(), expect) - test.must_match('file.out', "file.in\n") - - test.pass_test() # Local Variables: diff --git a/test/option/debug-memory.py b/test/option/debug-memory.py index 2958e0b7b7..1364da5d12 100644 --- a/test/option/debug-memory.py +++ b/test/option/debug-memory.py @@ -45,7 +45,7 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) def cat(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as ifp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as ifp: f.write(ifp.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=Action(cat))}) env.Cat('file.out', 'file.in') diff --git a/test/option/debug-multiple.py b/test/option/debug-multiple.py index 124e033e67..dc86c519ac 100644 --- a/test/option/debug-multiple.py +++ b/test/option/debug-multiple.py @@ -36,7 +36,7 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) def cat(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as infp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as infp: f.write(infp.read()) env = Environment(BUILDERS={'Cat':Builder(action=Action(cat))}) env.Cat('file.out', 'file.in') diff --git a/test/option/debug-objects.py b/test/option/debug-objects.py index 9c8536cb1f..57888460f0 100644 --- a/test/option/debug-objects.py +++ b/test/option/debug-objects.py @@ -36,7 +36,7 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) def cat(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as infp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as infp: f.write(infp.read()) env = Environment(tools=[], BUILDERS={'Cat':Builder(action=Action(cat))}) env.Cat('file.out', 'file.in') diff --git a/test/option/debug-presub.py b/test/option/debug-presub.py index cbd5242eaf..0c5d2e9fe1 100644 --- a/test/option/debug-presub.py +++ b/test/option/debug-presub.py @@ -40,10 +40,9 @@ test.write('SConstruct', """\ DefaultEnvironment(tools=[]) def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as infp: + with open(src, "rb") as infp: f.write(infp.read()) FILE = Builder(action="$FILECOM") TEMP = Builder(action="$TEMPCOM") diff --git a/test/option/fixture/SConstruct_debug_count b/test/option/fixture/SConstruct_debug_count index 332815bde1..1a39d17e5f 100644 --- a/test/option/fixture/SConstruct_debug_count +++ b/test/option/fixture/SConstruct_debug_count @@ -4,7 +4,7 @@ if ARGUMENTS.get('JSON',False): DefaultEnvironment(tools=[]) def cat(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as infp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as infp: f.write(infp.read()) env = Environment(BUILDERS={'Cat':Builder(action=Action(cat))}) env.Cat('file.out', 'file.in') diff --git a/test/option/help-options.py b/test/option/help-options.py index e430572a7f..c84961f83d 100644 --- a/test/option/help-options.py +++ b/test/option/help-options.py @@ -54,9 +54,9 @@ lines = stdout.split('\n') lines = [x for x in lines if x[:3] == ' -'] lines = [x[3:] for x in lines] -lines = [x[0] == '-' and x[1:] or x for x in lines] +lines = [x[1:] if x.startswith('-') else x for x in lines] options = [x.split()[0] for x in lines] -options = [x[-1] == ',' and x[:-1] or x for x in options] +options = [x[:-1] if x.endswith(',') else x for x in options] lowered = [x.lower() for x in options] ordered = sorted(lowered) if lowered != ordered: @@ -65,7 +65,7 @@ test.fail_test() test.pass_test() - + # Local Variables: # tab-width:4 diff --git a/test/option/option--Y.py b/test/option/option--Y.py index e8766fccb0..90d84fbcd9 100644 --- a/test/option/option--Y.py +++ b/test/option/option--Y.py @@ -36,9 +36,10 @@ work1_foo = test.workpath('work1', 'foo' + _exe) work1_foo_c = test.workpath('work1', 'foo.c') -test.write(['repository', 'SConstruct'], r""" +test.write(['repository', 'SConstruct'], """\ +DefaultEnvironment(tools=[]) env = Environment() -env.Program(target= 'foo', source = Split('aaa.c bbb.c foo.c')) +env.Program(target='foo', source=Split('aaa.c bbb.c foo.c')) """) test.write(['repository', 'aaa.c'], r""" @@ -81,16 +82,16 @@ # if we try to write into it accidentally. test.writable('repository', 0) -test.run(chdir = 'work1', options = opts, arguments = '.') +test.run(chdir='work1', options=opts, arguments='.') -test.run(program = work1_foo, stdout = """repository/aaa.c +test.run(program=work1_foo, stdout="""\ +repository/aaa.c repository/bbb.c repository/foo.c """) -test.up_to_date(chdir = 'work1', options = opts, arguments = '.') +test.up_to_date(chdir='work1', options=opts, arguments='.') -# test.write(['work1', 'bbb.c'], r""" #include #include @@ -101,14 +102,15 @@ } """) -test.run(chdir = 'work1', options = opts, arguments = '.') +test.run(chdir='work1', options=opts, arguments='.') -test.run(program = work1_foo, stdout = """repository/aaa.c +test.run(program=work1_foo, stdout="""\ +repository/aaa.c work1/bbb.c repository/foo.c """) -test.up_to_date(chdir = 'work1', options = opts, arguments = '.') +test.up_to_date(chdir='work1', options=opts, arguments='.') # test.write(['work1', 'aaa.c'], r""" @@ -137,47 +139,43 @@ } """) -test.run(chdir = 'work1', options = opts, arguments = '.') +test.run(chdir='work1', options=opts, arguments='.') -test.run(program = work1_foo, stdout = """work1/aaa.c +test.run(program=work1_foo, stdout="""\ +work1/aaa.c work1/bbb.c work1/foo.c """) -test.up_to_date(chdir = 'work1', options = opts, arguments = '.') +test.up_to_date(chdir='work1', options=opts, arguments='.') -# test.unlink(['work1', 'bbb.c']) test.unlink(['work1', 'foo.c']) -test.run(chdir = 'work1', options = opts, arguments = '.') +test.run(chdir='work1', options=opts, arguments='.') -test.run(program = work1_foo, stdout = """work1/aaa.c +test.run(program=work1_foo, stdout="""\ +work1/aaa.c repository/bbb.c repository/foo.c """) -test.up_to_date(chdir = 'work1', options = opts, arguments = '.') - - +test.up_to_date(chdir='work1', options=opts, arguments='.') -# test.subdir('r.NEW', 'r.OLD', 'work2') workpath_r_NEW = test.workpath('r.NEW') workpath_r_OLD = test.workpath('r.OLD') work2_foo = test.workpath('work2', 'foo' + _exe) -SConstruct = """ +SConstruct = """\ DefaultEnvironment(tools=[]) # test speedup env = Environment() -env.Program(target = 'foo', source = 'foo.c') +env.Program(target='foo', source='foo.c') """ test.write(['r.OLD', 'SConstruct'], SConstruct) - test.write(['r.NEW', 'SConstruct'], SConstruct) - test.write(['r.OLD', 'foo.c'], r""" #include #include @@ -197,13 +195,11 @@ test.writable('r.OLD', 0) test.writable('r.NEW', 0) -test.run(chdir = 'work2', options = opts, arguments = '.') +test.run(chdir='work2', options=opts, arguments='.') -test.run(program = work2_foo, stdout = "r.OLD/foo.c\n") +test.run(program=work2_foo, stdout="r.OLD/foo.c\n") +test.up_to_date(chdir='work2', options=opts, arguments='.') -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') - -# test.writable('r.NEW', 1) test.write(['r.NEW', 'foo.c'], r""" @@ -219,14 +215,11 @@ """) test.writable('r.NEW', 0) +test.run(chdir='work2', options=opts, arguments='.') -test.run(chdir = 'work2', options = opts, arguments = '.') - -test.run(program = work2_foo, stdout = "r.NEW/foo.c\n") +test.run(program=work2_foo, stdout="r.NEW/foo.c\n") +test.up_to_date(chdir='work2', options=opts, arguments='.') -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') - -# test.write(['work2', 'foo.c'], r""" #include #include @@ -239,13 +232,11 @@ } """) -test.run(chdir = 'work2', options = opts, arguments = '.') - -test.run(program = work2_foo, stdout = "work2/foo.c\n") +test.run(chdir='work2', options=opts, arguments='.') -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') +test.run(program=work2_foo, stdout="work2/foo.c\n") +test.up_to_date(chdir='work2', options=opts, arguments='.') -# test.writable('r.OLD', 1) test.writable('r.NEW', 1) @@ -276,33 +267,22 @@ test.writable('r.OLD', 0) test.writable('r.NEW', 0) -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') +test.up_to_date(chdir='work2', options=opts, arguments='.') -# test.unlink(['work2', 'foo.c']) +test.run(chdir='work2', options=opts, arguments='.') -test.run(chdir = 'work2', options = opts, arguments = '.') - -test.run(program = work2_foo, stdout = "r.NEW/foo.c 2\n") +test.run(program=work2_foo, stdout="r.NEW/foo.c 2\n") +test.up_to_date(chdir='work2', options=opts, arguments='.') -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') - -# test.writable('r.NEW', 1) - test.unlink(['r.NEW', 'foo.c']) - test.writable('r.NEW', 0) +test.run(chdir='work2', options=opts, arguments='.') -test.run(chdir = 'work2', options = opts, arguments = '.') - -test.run(program = work2_foo, stdout = "r.OLD/foo.c 2\n") - -test.up_to_date(chdir = 'work2', options = opts, arguments = '.') +test.run(program=work2_foo, stdout="r.OLD/foo.c 2\n") +test.up_to_date(chdir='work2', options=opts, arguments='.') - - -# test.pass_test() # Local Variables: diff --git a/test/option/option--duplicate.py b/test/option/option--duplicate.py index 74de64b646..927de86c93 100644 --- a/test/option/option--duplicate.py +++ b/test/option/option--duplicate.py @@ -28,8 +28,6 @@ SConscript settable option. """ -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - import os import sys import stat @@ -85,7 +83,7 @@ } def testLink(file, type): - nl = os.stat(file)[stat.ST_NLINK] + nl = os.stat(file).st_nlink islink = os.path.islink(file) assert criterion[type](nl, islink), \ "Expected %s to be %s (nl %d, islink %d)" \ diff --git a/test/option/option--random.py b/test/option/option--random.py index 1dafca4507..1521e3a44b 100644 --- a/test/option/option--random.py +++ b/test/option/option--random.py @@ -34,10 +34,9 @@ test.write('SConscript', """\ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) env = Environment(BUILDERS={'Cat':Builder(action=cat)}) env.Cat('aaa.out', 'aaa.in') diff --git a/test/option/warn-duplicate-environment.py b/test/option/warn-duplicate-environment.py index 1000647118..f19c533e82 100644 --- a/test/option/warn-duplicate-environment.py +++ b/test/option/warn-duplicate-environment.py @@ -36,9 +36,9 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) WARN = ARGUMENTS.get('WARN') diff --git a/test/option/warn-misleading-keywords.py b/test/option/warn-misleading-keywords.py index 45236bb4b7..5f78e63f94 100644 --- a/test/option/warn-misleading-keywords.py +++ b/test/option/warn-misleading-keywords.py @@ -36,9 +36,9 @@ test.write('SConstruct', """ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wb') as f: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as f: + for src in source: + with open(src, 'rb') as infp: f.write(infp.read()) WARN = ARGUMENTS.get('WARN') @@ -58,7 +58,7 @@ def build(env, target, source): scons: warning: Did you mean to use `(target|source)' instead of `(targets|sources)'\? """ + TestSCons.file_expr -test.run(arguments='.', +test.run(arguments='.', stderr=expect + expect) test.must_match(['file3a'], 'file3a.in\n') diff --git a/test/packaging/tar/bz2_packaging.py b/test/packaging/tar/bz2_packaging.py index 0c8dceac48..9d56b4223e 100644 --- a/test/packaging/tar/bz2_packaging.py +++ b/test/packaging/tar/bz2_packaging.py @@ -29,34 +29,17 @@ - create a tar package from the specified files """ -import TestSCons +import TestSConsTar -import os -import os.path -import sys - -python = TestSCons.python - -test = TestSCons.TestSCons() +test = TestSConsTar.TestSConsTar() tar = test.detect('TAR', 'tar') if not tar: test.skip_test('tar not found, skipping test\n') -bz2 = test.where_is('bzip2') - -if sys.platform == 'win32': - # windows 10 causes fresh problems by supplying a tar, not bzip2 - # but if git is installed, there's a bzip2 there, but can't be used - if not bz2: - test.skip_test('tar found, but helper bzip2 not found, skipping test\n') - bz2 = os.path.splitdrive(bz2)[1] - tar = os.path.splitdrive(test.where_is('tar'))[1] - if tar[:8] != bz2[:8]: # catch one in \WINDOWS, one not - test.skip_test('tar found, but usable bzip2 not, skipping test\n') - -bz2_path = os.path.dirname(bz2) - +is_wintar, is_bz2_supported = TestSConsTar.windows_system_tar_bz2(tar) +if is_wintar and not is_bz2_supported: + test.skip_test('windows tar found; bz2 not supported, skipping test\n') test.subdir('src') @@ -71,14 +54,11 @@ Program( 'src/main.c' ) env=Environment(tools=['packaging', 'filesystem', 'tar']) -# needed for windows to prevent picking up windows tar and thinking non-windows bzip2 would work. -env.PrependENVPath('PATH', r'%s') - env.Package( PACKAGETYPE = 'src_tarbz2', target = 'src.tar.bz2', PACKAGEROOT = 'test', source = [ 'src/main.c', 'SConstruct' ] ) -"""%bz2_path) +""") test.run(arguments='', stderr=None) diff --git a/test/packaging/tar/gz.py b/test/packaging/tar/gz.py index 07d5b52654..d54d6f1f3a 100644 --- a/test/packaging/tar/gz.py +++ b/test/packaging/tar/gz.py @@ -29,16 +29,18 @@ - create a targz package containing the specified files. """ -import TestSCons +import TestSConsTar -python = TestSCons.python - -test = TestSCons.TestSCons() +test = TestSConsTar.TestSConsTar() tar = test.detect('TAR', 'tar') if not tar: test.skip_test('tar not found, skipping test\n') +is_wintar, is_gz_supported = TestSConsTar.windows_system_tar_gz(tar) +if is_wintar and not is_gz_supported: + test.skip_test('windows tar found; gz not supported, skipping test\n') + test.subdir('src') test.write(['src', 'main.c'], r""" diff --git a/test/packaging/tar/xz_packaging.py b/test/packaging/tar/xz_packaging.py index 5144235bd2..dc28325488 100644 --- a/test/packaging/tar/xz_packaging.py +++ b/test/packaging/tar/xz_packaging.py @@ -28,24 +28,16 @@ This tests the SRC xz packager, which does the following: - create a tar package from the specified files """ -import os.path -import TestSCons +import TestSConsTar -python = TestSCons.python - -test = TestSCons.TestSCons() +test = TestSConsTar.TestSConsTar() tar = test.detect('TAR', 'tar') if not tar: test.skip_test('tar not found, skipping test\n') -# Windows 10 now supplies tar, but doesn't support xz compression -# assume it's just okay to check for an xz command, because don't -# want to probe the command itself to see what it supports -xz = test.where_is('xz') -if not xz: - test.skip_test('tar found, but helper xz not found, skipping test\n') - -xz_path = os.path.dirname(xz) +is_wintar, is_xz_supported = TestSConsTar.windows_system_tar_xz(tar) +if is_wintar and not is_xz_supported: + test.skip_test('windows tar found; xz not supported, skipping test\n') test.subdir('src') @@ -60,14 +52,11 @@ Program( 'src/main.c' ) env=Environment(tools=['packaging', 'filesystem', 'tar']) -# needed for windows to prevent picking up windows tar and thinking non-windows bzip2 would work. -env.PrependENVPath('PATH', r'%s') - env.Package( PACKAGETYPE = 'src_tarxz', target = 'src.tar.xz', PACKAGEROOT = 'test', source = [ 'src/main.c', 'SConstruct' ] ) -"""%xz_path) +""") test.run(arguments='', stderr=None) diff --git a/test/runtest/external_tests.py b/test/runtest/external_tests.py new file mode 100644 index 0000000000..622bfbd6e5 --- /dev/null +++ b/test/runtest/external_tests.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test that external subdirs are searched if --external is given: + + python runtest.py --external ext/test/subdir + +""" + +import os + +import TestRuntest + +test = TestRuntest.TestRuntest() +test.subdir('ext/test/subdir') + +pythonstring = TestRuntest.pythonstring +pythonflags = TestRuntest.pythonflags + +one = os.path.join('ext', 'test', 'subdir', 'test_one.py') +two = os.path.join('ext', 'test', 'subdir', 'two.py') +three = os.path.join('ext', 'test', 'test_three.py') + +test.write_passing_test(one) +test.write_passing_test(two) +test.write_passing_test(three) + +expect_stderr_noarg = """\ +usage: runtest.py [OPTIONS] [TEST ...] + +error: no tests matching the specification were found. + See "Test selection options" in the help for details on + how to specify and/or exclude tests. +""" + +expect_stdout = f"""\ +{pythonstring}{pythonflags} {one} +PASSING TEST STDOUT +{pythonstring}{pythonflags} {two} +PASSING TEST STDOUT +""" + +expect_stderr = """\ +PASSING TEST STDERR +PASSING TEST STDERR +""" + +test.run( + arguments='--no-progress ext/test/subdir', + status=1, + stdout=None, + stderr=expect_stderr_noarg, +) + +test.run( + arguments='--no-progress --external ext/test/subdir', + status=0, + stdout=expect_stdout, + stderr=expect_stderr, +) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/sconsign/corrupt.py b/test/sconsign/corrupt.py index fa6a0e9cbc..301fd1b852 100644 --- a/test/sconsign/corrupt.py +++ b/test/sconsign/corrupt.py @@ -47,7 +47,7 @@ SConstruct_contents = """\ def build1(target, source, env): - with open(str(target[0]), 'wb') as ofp, open(str(source[0]), 'rb') as ifp: + with open(target[0], 'wb') as ofp, open(source[0], 'rb') as ifp: ofp.write(ifp.read()) return None diff --git a/test/sconsign/ghost-entries.py b/test/sconsign/ghost-entries.py index 59a1ec2054..2c9c98c035 100644 --- a/test/sconsign/ghost-entries.py +++ b/test/sconsign/ghost-entries.py @@ -51,9 +51,9 @@ test.write('SConstruct', """\ def cat(target, source, env): - with open(str(target[0]), 'wb') as fp: - for s in source: - with open(str(s), 'rb') as infp: + with open(target[0], 'wb') as fp: + for src in source: + with open(src, 'rb') as infp: fp.write(infp.read()) env=Environment() Export('env') diff --git a/test/sconsign/nonwritable.py b/test/sconsign/nonwritable.py index e9520780bd..ee7b13eb95 100644 --- a/test/sconsign/nonwritable.py +++ b/test/sconsign/nonwritable.py @@ -52,16 +52,16 @@ SConstruct_contents = """\ def build1(target, source, env): - with open(str(target[0]), 'wb') as fo, open(str(source[0]), 'rb') as fi: + with open(target[0], 'wb') as fo, open(source[0], 'rb') as fi: fo.write(fi.read()) return None def build2(target, source, env): import os import os.path - with open(str(target[0]), 'wb') as fo, open(str(source[0]), 'rb') as fi: + with open(target[0], 'wb') as fo, open(source[0], 'rb') as fi: fo.write(fi.read()) - dir, file = os.path.split(str(target[0])) + dir, file = os.path.split(target[0]) os.chmod(dir, 0o555) return None diff --git a/test/sconsign/script/Configure.py b/test/sconsign/script/Configure.py index 2e1e9c1455..6b064b1ee8 100644 --- a/test/sconsign/script/Configure.py +++ b/test/sconsign/script/Configure.py @@ -76,16 +76,16 @@ # Value node being printed actually begins with a newline. It would # probably be good to change that to a repr() of the contents. expect = r"""=== .: -SConstruct: None \d+ \d+ +SConstruct: None \d+(\.\d*)? \d+ === .sconf_temp: conftest_%(sig_re)s_0.c: - '.*': + '.*':\s #include "math.h" %(sig_re)s \[.*\] conftest_%(sig_re)s_0_%(sig_re)s%(_obj)s: - %(_sconf_temp_conftest_0_c)s: %(sig_re)s \d+ \d+ + %(_sconf_temp_conftest_0_c)s: %(sig_re)s \d+(\.\d*)? \d+ %(CC)s: %(sig_re)s None None %(sig_re)s \[.*\] === %(CC_dir)s: diff --git a/test/sconsign/script/SConsignFile.py b/test/sconsign/script/SConsignFile.py index d19cfab290..6361ff30e8 100644 --- a/test/sconsign/script/SConsignFile.py +++ b/test/sconsign/script/SConsignFile.py @@ -111,6 +111,7 @@ def process(infp, outfp): ['SConstruct'], f"""\ SConsignFile() +_ = DefaultEnvironment(tools=[]) env1 = Environment( PROGSUFFIX='.exe', OBJSUFFIX='.obj', @@ -155,175 +156,175 @@ def process(infp, outfp): #define STRING2 "inc2.h" """) -test.run(arguments = '--implicit-cache .') +test.run(arguments='--implicit-cache .') sig_re = r'[0-9a-fA-F]{32,64}' database_name = test.get_sconsignname() -test.run_sconsign(arguments = database_name, - stdout = r"""=== .: -SConstruct: None \d+ \d+ -fake_cc\.py: %(sig_re)s \d+ \d+ -fake_link\.py: %(sig_re)s \d+ \d+ +test.run_sconsign(arguments=database_name, + stdout=r"""=== .: +SConstruct: None \d+(\.\d*)? \d+ +fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ +fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ === sub1: -hello.c: %(sig_re)s \d+ \d+ -hello.exe: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +hello.c: %(sig_re)s \d+(\.\d*)? \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] === sub2: -hello.c: %(sig_re)s \d+ \d+ -hello.exe: %(sig_re)s \d+ \d+ - %(sub2_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +hello.c: %(sig_re)s \d+(\.\d*)? \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -inc1.h: %(sig_re)s \d+ \d+ -inc2.h: %(sig_re)s \d+ \d+ +inc1.h: %(sig_re)s \d+(\.\d*)? \d+ +inc2.h: %(sig_re)s \d+(\.\d*)? \d+ """ % locals()) -test.run_sconsign(arguments = "--raw " + database_name, - stdout = r"""=== .: -SConstruct: {'csig': None, 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +test.run_sconsign(arguments="--raw " + database_name, + stdout=r"""=== .: +SConstruct: {'csig': None, 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} === sub1: -hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub1_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub1_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] -hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub1_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub1_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] === sub2: -hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub2_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub2_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] -hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub2_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub2_inc1_h)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub2_inc2_h)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub2_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub2_inc1_h)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub2_inc2_h)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] -inc1.h: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -inc2.h: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +inc1.h: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +inc2.h: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} """ % locals()) expect = r"""=== .: SConstruct: csig: None - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_cc\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_link\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ === sub1: hello.c: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ hello.exe: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub1_hello_obj)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_link\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] hello.obj: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub1_hello_c)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_cc\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] === sub2: hello.c: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ hello.exe: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub2_hello_obj)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_link\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] hello.obj: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub2_hello_c)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ %(sub2_inc1_h)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ %(sub2_inc2_h)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_cc\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] inc1.h: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ inc2.h: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ """ % locals() -test.run_sconsign(arguments = "-v " + database_name, stdout=expect) +test.run_sconsign(arguments="-v " + database_name, stdout=expect) -test.run_sconsign(arguments = "-c -v " + database_name, - stdout = r"""=== .: +test.run_sconsign(arguments="-c -v " + database_name, + stdout=r"""=== .: SConstruct: csig: None fake_cc\.py: @@ -350,8 +351,8 @@ def process(infp, outfp): csig: %(sig_re)s """ % locals()) -test.run_sconsign(arguments = "-s -v " + database_name, - stdout = r"""=== .: +test.run_sconsign(arguments="-s -v " + database_name, + stdout=r"""=== .: SConstruct: size: \d+ fake_cc\.py: @@ -376,94 +377,94 @@ def process(infp, outfp): size: \d+ inc2.h: size: \d+ -""" % locals()) +""") -test.run_sconsign(arguments = "-t -v " + database_name, - stdout = r"""=== .: +test.run_sconsign(arguments="-t -v " + database_name, + stdout=r"""=== .: SConstruct: - timestamp: \d+ + timestamp: \d+(\.\d*)? fake_cc\.py: - timestamp: \d+ + timestamp: \d+(\.\d*)? fake_link\.py: - timestamp: \d+ + timestamp: \d+(\.\d*)? === sub1: hello.c: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.exe: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.obj: - timestamp: \d+ + timestamp: \d+(\.\d*)? === sub2: hello.c: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.exe: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.obj: - timestamp: \d+ + timestamp: \d+(\.\d*)? inc1.h: - timestamp: \d+ + timestamp: \d+(\.\d*)? inc2.h: - timestamp: \d+ -""" % locals()) + timestamp: \d+(\.\d*)? +""") -test.run_sconsign(arguments = "-e hello.obj " + database_name, - stdout = r"""=== .: +test.run_sconsign(arguments="-e hello.obj " + database_name, + stdout=r"""=== .: === sub1: -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] === sub2: -hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals(), - stderr = r"""sconsign: no entry `hello\.obj' in `\.' -""" % locals()) + stderr=r"""sconsign: no entry `hello\.obj' in `\.' +""") -test.run_sconsign(arguments = "-e hello.obj -e hello.exe -e hello.obj " + database_name, - stdout = r"""=== .: +test.run_sconsign(arguments="-e hello.obj -e hello.exe -e hello.obj " + database_name, + stdout=r"""=== .: === sub1: -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.exe: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] === sub2: -hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.exe: %(sig_re)s \d+ \d+ - %(sub2_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals(), - stderr = r"""sconsign: no entry `hello\.obj' in `\.' + stderr=r"""sconsign: no entry `hello\.obj' in `\.' sconsign: no entry `hello\.exe' in `\.' sconsign: no entry `hello\.obj' in `\.' -""" % locals()) +""") -#test.run_sconsign(arguments = "-i -v " + database_name, -# stdout = r"""=== sub1: +#test.run_sconsign(arguments="-i -v " + database_name, +# stdout=r"""=== sub1: #hello.exe: # implicit: # hello.obj: %(sig_re)s diff --git a/test/sconsign/script/Signatures.py b/test/sconsign/script/Signatures.py index 2225f83bbf..a355068de3 100644 --- a/test/sconsign/script/Signatures.py +++ b/test/sconsign/script/Signatures.py @@ -169,26 +169,26 @@ def process(infp, outfp): test.run_sconsign( arguments=f"-e hello.exe -e hello.obj sub1/{database_name}", - stdout=r"""hello.exe: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: None \d+ \d+ + stdout=r"""hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: None \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: None \d+ \d+ - fake_cc\.py: None \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: None \d+(\.\d*)? \d+ + fake_cc\.py: None \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals(), ) test.run_sconsign( arguments=f"-e hello.exe -e hello.obj -r sub1/{database_name}", - stdout=r"""hello.exe: %(sig_re)s '%(date_re)s' \d+ - %(sub1_hello_obj)s: %(sig_re)s '%(date_re)s' \d+ - fake_link\.py: None '%(date_re)s' \d+ + stdout=r"""hello.exe: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + %(sub1_hello_obj)s: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + fake_link\.py: None '%(date_re)s' \d+(\.\d*)? %(sig_re)s \[.*\] -hello.obj: %(sig_re)s '%(date_re)s' \d+ - %(sub1_hello_c)s: None '%(date_re)s' \d+ - fake_cc\.py: None '%(date_re)s' \d+ +hello.obj: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + %(sub1_hello_c)s: None '%(date_re)s' \d+(\.\d*)? + fake_cc\.py: None '%(date_re)s' \d+(\.\d*)? %(sig_re)s \[.*\] """ % locals(), ) diff --git a/test/sconsign/script/dblite.py b/test/sconsign/script/dblite.py index 24d8403c47..0d05d5522c 100644 --- a/test/sconsign/script/dblite.py +++ b/test/sconsign/script/dblite.py @@ -116,24 +116,24 @@ def escape_drive_case(s): manifest = '' expect = r"""=== sub1: -hello%(_exe)s: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - %(LINK)s: None \d+ \d+ +hello%(_exe)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + %(LINK)s: None \d+(\.\d*)? \d+ %(sig_re)s \[.*%(manifest)s\] -hello%(_obj)s: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: None \d+ \d+ - %(CC)s: None \d+ \d+ +hello%(_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: None \d+(\.\d*)? \d+ + %(CC)s: None \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals() expect_r = r"""=== sub1: -hello%(_exe)s: %(sig_re)s '%(date_re)s' \d+ - %(sub1_hello_obj)s: %(sig_re)s '%(date_re)s' \d+ - %(LINK)s: None '%(date_re)s' \d+ +hello%(_exe)s: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + %(sub1_hello_obj)s: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + %(LINK)s: None '%(date_re)s' \d+(\.\d*)? %(sig_re)s \[.*%(manifest)s\] -hello%(_obj)s: %(sig_re)s '%(date_re)s' \d+ - %(sub1_hello_c)s: None '%(date_re)s' \d+ - %(CC)s: None '%(date_re)s' \d+ +hello%(_obj)s: %(sig_re)s '%(date_re)s' \d+(\.\d*)? + %(sub1_hello_c)s: None '%(date_re)s' \d+(\.\d*)? + %(CC)s: None '%(date_re)s' \d+(\.\d*)? %(sig_re)s \[.*\] """ % locals() diff --git a/test/sconsign/script/no-SConsignFile.py b/test/sconsign/script/no-SConsignFile.py index 7a41c72cb2..5760bf230f 100644 --- a/test/sconsign/script/no-SConsignFile.py +++ b/test/sconsign/script/no-SConsignFile.py @@ -168,62 +168,62 @@ def process(infp, outfp): sig_re = r'[0-9a-fA-F]{32,64}' -expect = r"""hello.c: %(sig_re)s \d+ \d+ -hello.exe: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +expect = r"""hello.c: %(sig_re)s \d+(\.\d*)? \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals() test.run_sconsign(arguments = f"sub1/{database_name}", stdout=expect) test.run_sconsign(arguments = f"--raw sub1/{database_name}", - stdout = r"""hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} -hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub1_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} + stdout = r"""hello.c: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} +hello.exe: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub1_hello_obj)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_link\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] -hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - %(sub1_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} - fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+L?, 'size': \d+L?, '_version_id': 2} +hello.obj: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + %(sub1_hello_c)s: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} + fake_cc\.py: {'csig': '%(sig_re)s', 'timestamp': \d+(\.\d*)?L?, 'size': \d+L?, '_version_id': 2} %(sig_re)s \[.*\] """ % locals()) test.run_sconsign(arguments = f"-v sub1/{database_name}", stdout = r"""hello.c: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ hello.exe: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub1_hello_obj)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_link\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] hello.obj: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ implicit: %(sub1_hello_c)s: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ fake_cc\.py: csig: %(sig_re)s - timestamp: \d+ + timestamp: \d+(\.\d*)? size: \d+ action: %(sig_re)s \[.*\] """ % locals()) @@ -248,73 +248,73 @@ def process(infp, outfp): test.run_sconsign(arguments = f"-t -v sub1/{database_name}", stdout = r"""hello.c: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.exe: - timestamp: \d+ + timestamp: \d+(\.\d*)? hello.obj: - timestamp: \d+ + timestamp: \d+(\.\d*)? """ % locals()) test.run_sconsign(arguments = f"-e hello.obj sub1/{database_name}", - stdout = r"""hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ + stdout = r"""hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals()) test.run_sconsign(arguments = f"-e hello.obj -e hello.exe -e hello.obj sub1/{database_name}", - stdout = r"""hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ + stdout = r"""hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.exe: %(sig_re)s \d+ \d+ - %(sub1_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals()) test.run_sconsign(arguments = f"sub2/{database_name}", - stdout = r"""hello.c: %(sig_re)s \d+ \d+ -hello.exe: %(sig_re)s \d+ \d+ - %(sub2_hello_obj)s: %(sig_re)s \d+ \d+ - fake_link\.py: %(sig_re)s \d+ \d+ + stdout = r"""hello.c: %(sig_re)s \d+(\.\d*)? \d+ +hello.exe: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_obj)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_link\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -inc1.h: %(sig_re)s \d+ \d+ -inc2.h: %(sig_re)s \d+ \d+ +inc1.h: %(sig_re)s \d+(\.\d*)? \d+ +inc2.h: %(sig_re)s \d+(\.\d*)? \d+ """ % locals()) #test.run_sconsign(arguments = "-i -v sub2/{}".format(database_name), -# stdout = r"""hello.c: %(sig_re)s \d+ \d+ -#hello.exe: %(sig_re)s \d+ \d+ +# stdout = r"""hello.c: %(sig_re)s \d+(\.\d*)? \d+ +#hello.exe: %(sig_re)s \d+(\.\d*)? \d+ # implicit: -# hello.obj: %(sig_re)s \d+ \d+ -#hello.obj: %(sig_re)s \d+ \d+ +# hello.obj: %(sig_re)s \d+(\.\d*)? \d+ +#hello.obj: %(sig_re)s \d+(\.\d*)? \d+ # implicit: -# hello.c: %(sig_re)s \d+ \d+ -# inc1.h: %(sig_re)s \d+ \d+ -# inc2.h: %(sig_re)s \d+ \d+ +# hello.c: %(sig_re)s \d+(\.\d*)? \d+ +# inc1.h: %(sig_re)s \d+(\.\d*)? \d+ +# inc2.h: %(sig_re)s \d+(\.\d*)? \d+ #""" % locals()) test.run_sconsign(arguments = f"-e hello.obj sub2/{database_name} sub1/{database_name}", - stdout = r"""hello.obj: %(sig_re)s \d+ \d+ - %(sub2_hello_c)s: %(sig_re)s \d+ \d+ - %(sub2_inc1_h)s: %(sig_re)s \d+ \d+ - %(sub2_inc2_h)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ + stdout = r"""hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc1_h)s: %(sig_re)s \d+(\.\d*)? \d+ + %(sub2_inc2_h)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] -hello.obj: %(sig_re)s \d+ \d+ - %(sub1_hello_c)s: %(sig_re)s \d+ \d+ - fake_cc\.py: %(sig_re)s \d+ \d+ +hello.obj: %(sig_re)s \d+(\.\d*)? \d+ + %(sub1_hello_c)s: %(sig_re)s \d+(\.\d*)? \d+ + fake_cc\.py: %(sig_re)s \d+(\.\d*)? \d+ %(sig_re)s \[.*\] """ % locals()) diff --git a/test/srcchange.py b/test/srcchange.py index e72d923a22..2eac09d743 100644 --- a/test/srcchange.py +++ b/test/srcchange.py @@ -52,7 +52,7 @@ def subrevision(target, source ,env): new = re.sub(r'\$REV.*?\$', '$REV: %%s$'%%source[0].get_text_contents().strip(), target[0].get_text_contents()) - with open(str(target[0]),'w') as outf: + with open(target[0], 'w') as outf: outf.write(new) SubRevision = Action(subrevision) diff --git a/test/suffixes.py b/test/suffixes.py index 7684ee83a9..a7a2c9d65f 100644 --- a/test/suffixes.py +++ b/test/suffixes.py @@ -34,10 +34,9 @@ test.write('SConstruct', """ def cat(env, source, target): - target = str(target[0]) - with open(target, "wb") as f: + with open(target[0], "wb") as f: for src in source: - with open(str(src), "rb") as ifp: + with open(src, "rb") as ifp: f.write(ifp.read()) Cat = Builder(action=cat, suffix='.out') env = Environment(BUILDERS = {'Cat':Cat}) diff --git a/test/timestamp-fallback.py b/test/timestamp-fallback.py index b3e812b911..d3f6774a09 100644 --- a/test/timestamp-fallback.py +++ b/test/timestamp-fallback.py @@ -57,7 +57,7 @@ DefaultEnvironment(tools=[]) def build(env, target, source): - with open(str(target[0]), 'wt') as ofp, open(str(source[0]), 'rt') as ifp: + with open(target[0], 'wt') as ofp, open(source[0], 'rt') as ifp: ofp.write(ifp.read()) B = Builder(action = build) diff --git a/test/toolpath/VariantDir.py b/test/toolpath/VariantDir.py index 652dde59b5..94376fc7c0 100644 --- a/test/toolpath/VariantDir.py +++ b/test/toolpath/VariantDir.py @@ -50,7 +50,7 @@ from SCons.Script import Builder def generate(env): def my_copy(target, source, env): - with open(str(target[0]), 'wb') as f, open(str(source[0]), 'rb') as ifp: + with open(target[0], 'wb') as f, open(source[0], 'rb') as ifp: f.write(ifp.read()) env['BUILDERS']['MyCopy'] = Builder(action = my_copy) diff --git a/test/update-release-info/update-release-info.py b/test/update-release-info/update-release-info.py index 2de4713b0c..bebd8a9875 100644 --- a/test/update-release-info/update-release-info.py +++ b/test/update-release-info/update-release-info.py @@ -53,11 +53,24 @@ if not os.path.exists(test.program): test.skip_test("update-release-info.py is not distributed in this package\n") -expected_stderr = """usage: update-release-info.py [-h] [--verbose] [--timestamp TIMESTAMP] +expected_stderr = """\ +usage: update-release-info.py [-h] [--verbose] [--timestamp TIMESTAMP] [{develop,release,post}] update-release-info.py: error: argument mode: invalid choice: 'bad' (choose from 'develop', 'release', 'post') """ -test.run(arguments='bad', stderr=expected_stderr, status=2) +# The way the choices are rendered in help by argparse changed with +# Python 3.12.8, # 3.13.1, 3.14.0a2. Change the test to accept either. +expected_stderr_new = """\ +usage: update-release-info.py [-h] [--verbose] [--timestamp TIMESTAMP] + [{develop,release,post}] +update-release-info.py: error: argument mode: invalid choice: 'bad' (choose from develop, release, post) +""" +test.run(arguments='bad', stderr=None, status=2) +fail_strings = [ + expected_stderr, + expected_stderr_new, +] +test.must_contain_any_line(test.stderr(), fail_strings) # Strings to go in ReleaseConfig combo_strings = [ diff --git a/test/virtualenv/activated/option/enable-virtualenv.py b/test/virtualenv/activated/option/enable-virtualenv.py index ff0658371b..0ec2c92242 100644 --- a/test/virtualenv/activated/option/enable-virtualenv.py +++ b/test/virtualenv/activated/option/enable-virtualenv.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Ensure that the --enable-virtualenv flag works. @@ -36,50 +35,61 @@ test = TestSCons.TestSCons() if SCons.Platform.virtualenv.virtualenv_enabled_by_default: - test.skip_test("Virtualenv support enabled by default, the option --enable-virtualenv is unavailable, skipping\n") + test.skip_test( + "Virtualenv support enabled by default, " + "the option --enable-virtualenv is unavailable, skipping\n" + ) if not SCons.Platform.virtualenv.Virtualenv(): test.skip_test("No virtualenv detected, skipping\n") -if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH','')): - test.skip_test("Virtualenv detected but looks like unactivated, skipping\n") +if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH', '')): + test.skip_test("Virtualenv detected but looks unactivated, skipping\n") -test.write('SConstruct', """ +test.write('SConstruct', """\ import sys import SCons.Platform.virtualenv + env = DefaultEnvironment(tools=[]) -print("sys.executable: %r" % sys.executable) -print("env.WhereIs('python'): %r" % env.WhereIs('python')) +print(f"sys.executable: {sys.executable!r}") +print(f"env.WhereIs('python'): {env.WhereIs('python')!r}") """) test.run(['-Q', '--enable-virtualenv']) s = test.stdout() m = re.search(r"""^sys\.executable:\s*(?P["'][^"']+["'])\s*$""", s, re.MULTILINE) if not m: - test.fail_test(message="""\ + test.fail_test( + message=f"""\ can't determine sys.executable from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") interpreter = eval(m.group('py')) -m = re.search(r"""^\s*env.WhereIs\('python'\):\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE) +m = re.search( + r"""^\s*env.WhereIs\('python'\):\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE +) if not m: - test.fail_test(message=""" + test.fail_test(message=f""" can't determine env.WhereIs('python') from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") python = eval(m.group('py')) -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), - message="sys.executable points outside of virtualenv") -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(python), - message="env.WhereIs('python') points to virtualenv") +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), + message="sys.executable points outside of virtualenv", +) +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(python), + message="env.WhereIs('python') points to virtualenv", +) test.pass_test() diff --git a/test/virtualenv/activated/option/ignore-virtualenv.py b/test/virtualenv/activated/option/ignore-virtualenv.py index b0d482e523..d769ed20be 100644 --- a/test/virtualenv/activated/option/ignore-virtualenv.py +++ b/test/virtualenv/activated/option/ignore-virtualenv.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Ensure that the --ignore-virtualenv flag works. @@ -38,15 +37,16 @@ if not SCons.Platform.virtualenv.Virtualenv(): test.skip_test("No virtualenv detected, skipping\n") -if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH','')): - test.skip_test("Virtualenv detected but looks like unactivated, skipping\n") +if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH', '')): + test.skip_test("Virtualenv detected but looks unactivated, skipping\n") -test.write('SConstruct', """ +test.write('SConstruct', """\ import sys import SCons.Platform.virtualenv + env = DefaultEnvironment(tools=[]) -print("sys.executable: %s" % repr(sys.executable)) -print("env.WhereIs('python'): %s" % repr(env.WhereIs('python'))) +print(f"sys.executable: {sys.executable!r}") +print(f"env.WhereIs('python'): {env.WhereIs('python')!r}") """) os.environ['SCONS_ENABLE_VIRTUALENV'] = '1' @@ -55,30 +55,36 @@ s = test.stdout() m = re.search(r"""^sys\.executable:\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE) if not m: - test.fail_test(message="""\ -can't determine sys.executable from stdout: + test.fail_test( + message=f"""can't determine sys.executable from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") interpreter = eval(m.group('py')) -m = re.search(r"""^\s*env.WhereIs\('python'\):\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE) +m = re.search( + r"""\s*env.WhereIs\('python'\):\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE +) if not m: - test.fail_test(message=""" + test.fail_test(message=f""" can't determine env.WhereIs('python') from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") python = eval(m.group('py')) -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), - message="sys.executable points outside of virtualenv") -test.fail_test(SCons.Platform.virtualenv.IsInVirtualenv(python), - message="env.WhereIs('python') points to virtualenv") +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), + message="sys.executable points outside of virtualenv", +) +test.fail_test( + SCons.Platform.virtualenv.IsInVirtualenv(python), + message="env.WhereIs('python') points to virtualenv", +) test.pass_test() diff --git a/test/virtualenv/activated/virtualenv_activated_python.py b/test/virtualenv/activated/virtualenv_activated_python.py index 4e793daf0b..f685c723d9 100644 --- a/test/virtualenv/activated/virtualenv_activated_python.py +++ b/test/virtualenv/activated/virtualenv_activated_python.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,15 +22,12 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Check which python executable is running scons and which python executable -would be used by scons, when we run under activated virtualenv (i.e. PATH -contains the virtualenv's bin path). This test is skipped when ran in regular -environment or in unactivated virtualenv. +would be used by scons when we run under activated virtualenv (i.e., PATH +contains the virtualenv's bin path). This test is skipped when run in a +regular environment or in an unactivated virtualenv. """ import TestSCons @@ -42,14 +41,15 @@ test.skip_test("No virtualenv detected, skipping\n") if not SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH')): - test.skip_test("Virtualenv detected but looks like unactivated, skipping\n") + test.skip_test("Virtualenv detected but looks unactivated, skipping\n") -test.write('SConstruct', """ +test.write('SConstruct', """\ import sys + env = DefaultEnvironment(tools=[]) -print("sys.executable: %s" % repr(sys.executable)) -print("env.WhereIs('python'): %s" % repr(env.WhereIs('python'))) +print(f"sys.executable: {sys.executable!r}") +print(f"env.WhereIs('python'): {env.WhereIs('python')!r}") """) if SCons.Platform.virtualenv.virtualenv_enabled_by_default: @@ -60,31 +60,40 @@ s = test.stdout() m = re.search(r"""^sys\.executable:\s*(?P["']?[^"']+["']?)\s*$""", s, re.MULTILINE) if not m: - test.fail_test(message="""\ + test.fail_test( + message=f"""\ can't determine sys.executable from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") interpreter = eval(m.group('py')) -m = re.search(r"""^\s*env\.WhereIs\('python'\):\s*(?P["'][^"']+["'])\s*$""", s, re.MULTILINE) +m = re.search( + r"""^\s*env\.WhereIs\('python'\):\s*(?P["'][^"']+["'])\s*$""", s, re.MULTILINE +) if not m: - test.fail_test(message=""" + test.fail_test( + message=f""" can't determine env.WhereIs('python') from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") python = eval(m.group('py')) -# runing in activated virtualenv (after "activate") - PATH includes virtualenv's bin directory -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), - message="sys.executable points outside of virtualenv") -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(python), - message="env.WhereIs('python') points outside of virtualenv") +# running in an activated virtualenv (after "activate") - +# PATH includes the virtualenv bin directory +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), + message="sys.executable points outside of virtualenv", +) +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(python), + message="env.WhereIs('python') points outside of virtualenv", +) test.pass_test() diff --git a/test/virtualenv/activated/virtualenv_detect_virtualenv.py b/test/virtualenv/activated/virtualenv_detect_virtualenv.py index e7b7cb0faa..c7b5be5c31 100644 --- a/test/virtualenv/activated/virtualenv_detect_virtualenv.py +++ b/test/virtualenv/activated/virtualenv_detect_virtualenv.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Check if SCons.Platform.virtualenv.Virtualenv() works in SConscripts. @@ -37,8 +36,9 @@ if not ve: test.skip_test("Virtualenv is not active, skipping\n") -test.write('SConstruct', """ -print("virtualenv: %r" % Virtualenv()) +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +print(f"virtualenv: {Virtualenv()!r}") """) if SCons.Platform.virtualenv.virtualenv_enabled_by_default: @@ -46,7 +46,7 @@ else: test.run(['-Q', '--enable-virtualenv']) -test.must_contain_all_lines(test.stdout(), ['virtualenv: %r' % ve]) +test.must_contain_all_lines(test.stdout(), [f'virtualenv: {ve!r}']) test.pass_test() diff --git a/test/virtualenv/always/virtualenv_global_function.py b/test/virtualenv/always/virtualenv_global_function.py index 8f2c291c9d..537aa98caf 100644 --- a/test/virtualenv/always/virtualenv_global_function.py +++ b/test/virtualenv/always/virtualenv_global_function.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,13 +22,10 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Check which python executable is running scons and which python executable -would be used by scons, when we run under activated virtualenv (i.e. PATH +would be used by scons when we run in an activated virtualenv (i.e., PATH contains the virtualenv's bin path). """ @@ -36,28 +35,32 @@ test = TestSCons.TestSCons() -test.write('SConstruct', """ -print("Virtualenv(): %r" % Virtualenv()) -""") +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +print(f"Virtualenv(): {Virtualenv()!r}") +""", +) test.run(['-Q']) s = test.stdout() m = re.search(r"^Virtualenv\(\):\s*(?P.+\S)\s*$", s, re.MULTILINE) if not m: - test.fail_test(message="""\ + test.fail_test(message=f"""\ can't determine Virtualenv() result from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") scons_ve = m.group('ve') -our_ve = "%r" % SCons.Platform.virtualenv.Virtualenv() +our_ve = f"{SCons.Platform.virtualenv.Virtualenv()!r}" -# runing in activated virtualenv (after "activate") - PATH includes virtualenv's bin directory -test.fail_test(scons_ve != our_ve, - message="Virtualenv() from SCons != Virtualenv() from caller script (%r != %r)" % (scons_ve, our_ve)) +# running in activated virtualenv (after "activate") - PATH includes virtualenv's bin directory +test.fail_test( + scons_ve != our_ve, + message=f"Virtualenv() from SCons != Virtualenv() from caller script ({scons_ve!r} != {our_ve!r})", +) test.pass_test() diff --git a/test/virtualenv/regularenv/virtualenv_detect_regularenv.py b/test/virtualenv/regularenv/virtualenv_detect_regularenv.py index 6856838c93..dcbfc2e4d6 100644 --- a/test/virtualenv/regularenv/virtualenv_detect_regularenv.py +++ b/test/virtualenv/regularenv/virtualenv_detect_regularenv.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Check if SCons.Platform.virtualenv.Virtualenv() works in SConscript. @@ -36,8 +35,9 @@ if SCons.Platform.virtualenv.Virtualenv(): test.skip_test("Virtualenv is active, skipping\n") -test.write('SConstruct', """ -print("virtualenv: %r" % Virtualenv()) +test.write('SConstruct', """\ +DefaultEnvironment(tools=[]) +print(f"virtualenv: {Virtualenv()!r}") """) if SCons.Platform.virtualenv.virtualenv_enabled_by_default: @@ -45,7 +45,7 @@ else: test.run(['-Q', '--enable-virtualenv']) -test.must_contain_all_lines(test.stdout(), ['virtualenv: %r' % None]) +test.must_contain_all_lines(test.stdout(), ["virtualenv: ''"]) test.pass_test() diff --git a/test/virtualenv/unactivated/virtualenv_unactivated_python.py b/test/virtualenv/unactivated/virtualenv_unactivated_python.py index 38d5329874..266156c33b 100644 --- a/test/virtualenv/unactivated/virtualenv_unactivated_python.py +++ b/test/virtualenv/unactivated/virtualenv_unactivated_python.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,21 +22,19 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Check which python executable is running scons and which python executable -would be used by scons, when we run under an unactivated virtualenv (i.e. PATH +would be used by scons when we run under an unactivated virtualenv (i.e., PATH does not contain virtualenv's bin path). This test is skipped if ran in a regular environment or in an activated virtualenv. """ -import TestSCons -import SCons.Platform.virtualenv import os import re +import SCons.Platform.virtualenv + +import TestSCons test = TestSCons.TestSCons() @@ -42,13 +42,14 @@ test.skip_test("No virtualenv detected, skipping\n") if SCons.Platform.virtualenv.select_paths_in_venv(os.getenv('PATH')): - test.skip_test("Virtualenv detected and it looks like activated, skipping\n") + test.skip_test("Virtualenv detected and it looks activated, skipping\n") -test.write('SConstruct', """ +test.write('SConstruct', """\ import sys + env = DefaultEnvironment(tools=[]) -print("sys.executable: %s" % repr(sys.executable)) -print("env.WhereIs('python'): %s" % repr(env.WhereIs('python'))) +print(f"sys.executable: {sys.executable!r}") +print(f"env.WhereIs('python'): {env.WhereIs('python')!r}") """) if SCons.Platform.virtualenv.virtualenv_enabled_by_default: @@ -59,31 +60,41 @@ s = test.stdout() m = re.search(r"""^sys\.executable:\s*(?P["']?[^\"']+["']?)\s*$""", s, re.MULTILINE) if not m: - test.fail_test(message="""\ + test.fail_test( + message=f"""\ can't determine sys.executable from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") interpreter = eval(m.group('py')) -m = re.search(r"""^\s*env\.WhereIs\('python'\):\s*(?P["']?[^"']+[\"']?)\s*$""", s, re.MULTILINE) +m = re.search( + r"""^\s*env\.WhereIs\('python'\):\s*(?P["']?[^"']+[\"']?)\s*$""", + s, + re.MULTILINE, +) if not m: - test.fail_test(message=""" + test.fail_test( + message=f"""\ can't determine env.WhereIs('python') from stdout: ========= STDOUT ========= -%s +{s} ========================== -""" % s) +""") python = eval(m.group('py')) # running without activating virtualenv (by just /path/to/virtualenv/bin/python runtest.py ...). -test.fail_test(not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), - message="sys.executable points outside of virtualenv") -test.fail_test(SCons.Platform.virtualenv.IsInVirtualenv(python), - message="env.WhereIs('python') points to virtualenv") +test.fail_test( + not SCons.Platform.virtualenv.IsInVirtualenv(interpreter), + message="sys.executable points outside of virtualenv", +) +test.fail_test( + SCons.Platform.virtualenv.IsInVirtualenv(python), + message="env.WhereIs('python') points to virtualenv", +) test.pass_test() diff --git a/testing/ci/macos_ci_skip.txt b/testing/ci/macos_ci_skip.txt new file mode 100644 index 0000000000..c9a94f1acb --- /dev/null +++ b/testing/ci/macos_ci_skip.txt @@ -0,0 +1,3 @@ +# temporarily skip this in GitHub MacOS Experimental runner +test/ninja/ninja_handle_control_c_rebuild.py +test/ninja/shutdown_scons_daemon.py diff --git a/testing/ci/msvc_ci_run.txt b/testing/ci/msvc_ci_run.txt new file mode 100644 index 0000000000..7a10309654 --- /dev/null +++ b/testing/ci/msvc_ci_run.txt @@ -0,0 +1 @@ +test/MSVC/msvc.py diff --git a/testing/ci/windows_ci_skip.txt b/testing/ci/windows_ci_skip.txt new file mode 100644 index 0000000000..6f96386f5a --- /dev/null +++ b/testing/ci/windows_ci_skip.txt @@ -0,0 +1,10 @@ +# temporarily skip this in GitHub Windows runner +test/CPPDEFINES/pkg-config.py +test/packaging/msi/explicit-target.py +test/packaging/msi/file-placement.py +test/packaging/msi/package.py +test/scons-time/run/config/python.py +test/scons-time/run/option/python.py +test/sconsign/script/no-SConsignFile.py +test/sconsign/script/SConsignFile.py +test/sconsign/script/Signatures.py diff --git a/testing/ci/windows_msvc_cache.py b/testing/ci/windows_msvc_cache.py new file mode 100644 index 0000000000..429b119e78 --- /dev/null +++ b/testing/ci/windows_msvc_cache.py @@ -0,0 +1,48 @@ +import argparse +import os +import sys +import time + +def _scons_msvc_cache(skip_populate=False, skip_display=False): + + # /testing/ci/script.py + syspathdir = os.path.join(os.path.dirname(__file__), '..', '..') + sys.path.insert(0, syspathdir) + + from SCons.Environment import Environment + from SCons.Tool.MSCommon.common import CONFIG_CACHE + + if not skip_populate: + Environment(tools=["msvc"]) + + if not skip_display: + print(f"SCONS_CACHE_MSVC_CONFIG={CONFIG_CACHE!r}") + if os.path.exists(CONFIG_CACHE): + with open(CONFIG_CACHE, "r") as fh: + for line in fh: + sys.stdout.write(line) + print() + +def _setup(): + + if not sys.platform.startswith("win32"): + return + + if not os.environ.get("SCONS_CACHE_MSVC_CONFIG"): + return + + parser = argparse.ArgumentParser() + parser.add_argument("--skip-populate", action="store_true", default=False, dest="skip_populate") + parser.add_argument("--skip-display", action="store_true", default=False, dest="skip_display") + + args = parser.parse_args() + + try: + _scons_msvc_cache(skip_populate=args.skip_populate, skip_display=args.skip_display) + except Exception as e: + print(f"exception: {type(e).__name__}") + +if __name__ == "__main__": + beg_time = time.time() + _setup() + print(f"Execution time: {time.time() - beg_time:.1f} seconds") diff --git a/testing/framework/TestCmd.py b/testing/framework/TestCmd.py index fba0b755bb..6ac1275fc3 100644 --- a/testing/framework/TestCmd.py +++ b/testing/framework/TestCmd.py @@ -1,3 +1,22 @@ +# Copyright 2000-2024 Steven Knight +# +# This module is free software, and you may redistribute it and/or modify +# it under the same terms as Python itself, so long as this copyright message +# and disclaimer are retained in their original form. +# +# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +# +# Python License: https://docs.python.org/3/license.html#psf-license + """ A testing framework for commands and scripts. @@ -276,21 +295,7 @@ TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4') """ -# Copyright 2000-2010 Steven Knight -# This module is free software, and you may redistribute it and/or modify -# it under the same terms as Python itself, so long as this copyright message -# and disclaimer are retained in their original form. -# -# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, -# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF -# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -# DAMAGE. -# -# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, -# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, -# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +from __future__ import annotations __author__ = "Steven Knight " __revision__ = "TestCmd.py 1.3.D001 2010/06/03 12:58:27 knight" @@ -302,6 +307,7 @@ import hashlib import os import re + try: import psutil except ImportError: @@ -314,20 +320,24 @@ import subprocess import sys import tempfile -import threading import time import traceback from collections import UserList, UserString from pathlib import Path from subprocess import PIPE, STDOUT -from typing import Optional +from typing import Callable IS_WINDOWS = sys.platform == 'win32' IS_MACOS = sys.platform == 'darwin' IS_64_BIT = sys.maxsize > 2**32 IS_PYPY = hasattr(sys, 'pypy_translation_info') +try: + IS_ROOT = os.geteuid() == 0 +except AttributeError: + IS_ROOT = False NEED_HELPER = os.environ.get('SCONS_NO_DIRECT_SCRIPT') + # sentinel for cases where None won't do _Null = object() @@ -367,6 +377,7 @@ def to_str(s): def is_String(e): return isinstance(e, (str, UserString)) + testprefix = 'testcmd.' if os.name in ('posix', 'nt'): testprefix += f"{os.getpid()}." @@ -427,7 +438,13 @@ def clean_up_ninja_daemon(self, result_type) -> None: shutil.rmtree(daemon_dir) -def fail_test(self=None, condition: bool=True, function=None, skip: int=0, message=None) -> None: +def fail_test( + self=None, + condition: bool = True, + function: Callable | None = None, + skip: int = 0, + message: str = "", +) -> None: """Causes a test to exit with a fail. Reports that the test FAILED and exits with a status of 1, unless @@ -468,7 +485,7 @@ def fail_test(self=None, condition: bool=True, function=None, skip: int=0, messa sys.exit(1) -def no_result(self=None, condition: bool=True, function=None, skip: int=0) -> None: +def no_result(self=None, condition: bool = True, function=None, skip: int = 0) -> None: """Causes a test to exit with a no result. In testing parlance NO RESULT means the test could not be completed @@ -510,7 +527,7 @@ def no_result(self=None, condition: bool=True, function=None, skip: int=0) -> No sys.exit(2) -def pass_test(self=None, condition: bool=True, function=None) -> None: +def pass_test(self=None, condition: bool = True, function=None) -> None: """Causes a test to exit with a pass. Reports that the test PASSED and exits with a status of 0, unless @@ -605,7 +622,7 @@ def match_re(lines=None, res=None): print(f"match_re: expected {len(res)} lines, found {len(lines)}") return None for i, (line, regex) in enumerate(zip(lines, res)): - s = fr"^{regex}$" + s = rf"^{regex}$" try: expr = re.compile(s) except re.error as e: @@ -635,7 +652,7 @@ def match_re_dotall(lines=None, res=None): lines = "\n".join(lines) if not isinstance(res, str): res = "\n".join(res) - s = fr"^{res}$" + s = rf"^{res}$" try: expr = re.compile(s, re.DOTALL) except re.error as e: @@ -644,8 +661,16 @@ def match_re_dotall(lines=None, res=None): return expr.match(lines) -def simple_diff(a, b, fromfile: str='', tofile: str='', - fromfiledate: str='', tofiledate: str='', n: int=0, lineterm: str=''): +def simple_diff( + a, + b, + fromfile: str = '', + tofile: str = '', + fromfiledate: str = '', + tofiledate: str = '', + n: int = 0, + lineterm: str = '', +): r"""Compare two sequences of lines; generate the delta as a simple diff. Similar to difflib.context_diff and difflib.unified_diff but @@ -674,7 +699,7 @@ def simple_diff(a, b, fromfile: str='', tofile: str='', sm = difflib.SequenceMatcher(None, a, b) def comma(x1, x2): - return x1 + 1 == x2 and str(x2) or f'{x1 + 1},{x2}' + return str(x2) if x1 + 1 == x2 else f'{x1 + 1},{x2}' for op, a1, a2, b1, b2 in sm.get_opcodes(): if op == 'delete': @@ -694,8 +719,16 @@ def comma(x1, x2): yield f"> {l}" -def diff_re(a, b, fromfile: str='', tofile: str='', - fromfiledate: str='', tofiledate: str='', n: int=3, lineterm: str='\n'): +def diff_re( + a, + b, + fromfile: str = '', + tofile: str = '', + fromfiledate: str = '', + tofiledate: str = '', + n: int = 3, + lineterm: str = '\n', +): """Compare a and b (lists of strings) where a are regular expressions. A simple "diff" of two sets of lines when the expected lines @@ -714,7 +747,7 @@ def diff_re(a, b, fromfile: str='', tofile: str='', elif diff > 0: b = b + [''] * diff for i, (aline, bline) in enumerate(zip(a, b)): - s = fr"^{aline}$" + s = rf"^{aline}$" try: expr = re.compile(s) except re.error as e: @@ -729,6 +762,7 @@ def diff_re(a, b, fromfile: str='', tofile: str='', if os.name == 'posix': + def escape(arg): """escape shell special characters""" slash = '\\' @@ -748,6 +782,7 @@ def escape(arg): arg = f"\"{arg}\"" return arg + if os.name == 'java': python = os.path.join(sys.prefix, 'jython') else: @@ -755,7 +790,6 @@ def escape(arg): _python_ = escape(python) if sys.platform == 'win32': - default_sleep_seconds = 2 def where_is(file, path=None, pathext=None): @@ -768,7 +802,7 @@ def where_is(file, path=None, pathext=None): if is_String(pathext): pathext = pathext.split(os.pathsep) for ext in pathext: - if ext.casefold() == file[-len(ext):].casefold(): + if ext.casefold() == file[-len(ext) :].casefold(): pathext = [''] break for dir in path: @@ -793,7 +827,7 @@ def where_is(file, path=None, pathext=None): st = os.stat(f) except OSError: continue - if stat.S_IMODE(st[stat.ST_MODE]) & 0o111: + if stat.S_IMODE(st.st_mode) & stat.S_IXUSR: return f return None @@ -819,7 +853,8 @@ def ReadFile(hFile, bufSize, ol=None): lpBuffer = ctypes.create_string_buffer(bufSize) bytesRead = DWORD() bErr = ctypes.windll.kernel32.ReadFile( - hFile, lpBuffer, bufSize, ctypes.byref(bytesRead), ol) + hFile, lpBuffer, bufSize, ctypes.byref(bytesRead), ol + ) if not bErr: raise ctypes.WinError() return (0, ctypes.string_at(lpBuffer, bytesRead.value)) @@ -828,7 +863,8 @@ def WriteFile(hFile, data, ol=None): assert ol is None bytesWritten = DWORD() bErr = ctypes.windll.kernel32.WriteFile( - hFile, data, len(data), ctypes.byref(bytesWritten), ol) + hFile, data, len(data), ctypes.byref(bytesWritten), ol + ) if not bErr: raise ctypes.WinError() return (0, bytesWritten.value) @@ -837,10 +873,12 @@ def PeekNamedPipe(hPipe, size): assert size == 0 bytesAvail = DWORD() bErr = ctypes.windll.kernel32.PeekNamedPipe( - hPipe, None, size, None, ctypes.byref(bytesAvail), None) + hPipe, None, size, None, ctypes.byref(bytesAvail), None + ) if not bErr: raise ctypes.WinError() return ("", bytesAvail.value, None) + import msvcrt else: import select @@ -864,7 +902,7 @@ def recv(self, maxsize=None): def recv_err(self, maxsize=None): return self._recv('stderr', maxsize) - def send_recv(self, input: str='', maxsize=None): + def send_recv(self, input: str = '', maxsize=None): return self.send(input), self.recv(maxsize), self.recv_err(maxsize) def get_conn_maxsize(self, which, maxsize): @@ -879,6 +917,7 @@ def _close(self, which) -> None: setattr(self, which, None) if sys.platform == 'win32': # and subprocess.mswindows: + def send(self, input): input = to_bytes(input) if not self.stdin: @@ -920,6 +959,7 @@ def _recv(self, which, maxsize): return read else: + def send(self, input): if not self.stdin: return None @@ -928,8 +968,7 @@ def send(self, input): return 0 try: - written = os.write(self.stdin.fileno(), - bytearray(input, 'utf-8')) + written = os.write(self.stdin.fileno(), bytearray(input, 'utf-8')) except OSError as why: if why.args[0] == errno.EPIPE: # broken pipe return self._close('stdin') @@ -969,7 +1008,7 @@ def _recv(self, which, maxsize): disconnect_message = "Other end disconnected!" -def recv_some(p, t: float=.1, e: int=1, tr: int=5, stderr: int=0): +def recv_some(p, t: float = 0.1, e: int = 1, tr: int = 5, stderr: int = 0): if tr < 1: tr = 1 x = time.time() + t @@ -1023,7 +1062,7 @@ def __init__( interpreter=None, workdir=None, subdir=None, - verbose=None, + verbose: int = -1, match=None, match_stdout=None, match_stderr=None, @@ -1031,15 +1070,15 @@ def __init__( diff_stdout=None, diff_stderr=None, combine: bool = False, - universal_newlines: Optional[bool] = True, - timeout: Optional[float] = None, + universal_newlines: bool | None = True, + timeout: float | None = None, ) -> None: self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0) self._cwd = os.getcwd() self.description_set(description) self.program_set(program) self.interpreter_set(interpreter) - if verbose is None: + if verbose == -1: try: verbose = max(0, int(os.environ.get('TESTCMD_VERBOSE', 0))) except ValueError: @@ -1047,7 +1086,7 @@ def __init__( self.verbose_set(verbose) self.combine = combine self.universal_newlines = universal_newlines - self.process = None + self.process: Popen | None = None # Two layers of timeout: one at the test class instance level, # one set on an individual start() call (usually via a run() call) self.timeout = timeout @@ -1055,29 +1094,25 @@ def __init__( self.set_match_function(match, match_stdout, match_stderr) self.set_diff_function(diff, diff_stdout, diff_stderr) self._dirlist = [] - self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0} + self._preserve: dict[str, str | bool] = { + 'pass_test': False, + 'fail_test': False, + 'no_result': False, + } preserve_value = os.environ.get('PRESERVE', False) if preserve_value not in [0, '0', 'False']: self._preserve['pass_test'] = os.environ['PRESERVE'] self._preserve['fail_test'] = os.environ['PRESERVE'] self._preserve['no_result'] = os.environ['PRESERVE'] else: - try: - self._preserve['pass_test'] = os.environ['PRESERVE_PASS'] - except KeyError: - pass - try: - self._preserve['fail_test'] = os.environ['PRESERVE_FAIL'] - except KeyError: - pass - try: - self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT'] - except KeyError: - pass + self._preserve['pass_test'] = os.environ.get('PRESERVE_PASS', False) + self._preserve['fail_test'] = os.environ.get('PRESERVE_FAIL', False) + self._preserve['no_result'] = os.environ.get('PRESERVE_NO_RESULT', False) self._stdout = [] self._stderr = [] - self.status = None + self.status: int | None = None self.condition = 'no_result' + self.workdir: str | None self.workdir_set(workdir) self.subdir(subdir) @@ -1086,7 +1121,6 @@ def __init__( except KeyError: self.fixture_dirs = [] - def __del__(self) -> None: self.cleanup() @@ -1144,8 +1178,8 @@ def cleanup(self, condition=None) -> None: list = self._dirlist[:] list.reverse() for dir in list: - self.writable(dir, 1) - shutil.rmtree(dir, ignore_errors=1) + self.writable(dir, True) + shutil.rmtree(dir, ignore_errors=True) self._dirlist = [] global _Cleanup @@ -1178,12 +1212,15 @@ def command_args(self, program=None, interpreter=None, arguments=None): cmd.extend([f"{k}={v}" for k, v in arguments.items()]) return cmd if isinstance(arguments, str): + # Split into a list for passing to SCons. This *will* + # break if the string has embedded spaces as part of a substing - + # use a # list to pass those to avoid the problem. arguments = arguments.split() cmd.extend(arguments) return cmd def description_set(self, description) -> None: - """Set the description of the functionality being tested. """ + """Set the description of the functionality being tested.""" self.description = description def set_diff_function(self, diff=_Null, stdout=_Null, stderr=_Null) -> None: @@ -1207,9 +1244,9 @@ def diff(self, a, b, name=None, diff_function=None, *args, **kw) -> None: print(self.banner(name)) if not is_List(a): - a=a.splitlines() + a = a.splitlines() if not is_List(b): - b=b.splitlines() + b = b.splitlines() args = (a, b) + args for line in diff_function(*args, **kw): @@ -1239,16 +1276,24 @@ def diff_stdout(self, a, b, *args, **kw): unified_diff = staticmethod(difflib.unified_diff) - def fail_test(self, condition: bool=True, function=None, skip: int=0, message=None) -> None: + def fail_test( + self, + condition: bool = True, + function: Callable | None = None, + skip: int = 0, + message: str = "", + ) -> None: """Cause the test to fail.""" if not condition: return self.condition = 'fail_test' - fail_test(self=self, - condition=condition, - function=function, - skip=skip, - message=message) + fail_test( + self=self, + condition=condition, + function=function, + skip=skip, + message=message, + ) def interpreter_set(self, interpreter) -> None: """Set the program to be used to interpret the program @@ -1257,7 +1302,7 @@ def interpreter_set(self, interpreter) -> None: self.interpreter = interpreter def set_match_function(self, match=_Null, stdout=_Null, stderr=_Null) -> None: - """Sets the specified match functions. """ + """Sets the specified match functions.""" if match is not _Null: self._match_function = match if stdout is not _Null: @@ -1306,17 +1351,14 @@ def match_stdout(self, lines, matches): match_re_dotall = staticmethod(match_re_dotall) - def no_result(self, condition: bool=True, function=None, skip: int=0) -> None: + def no_result(self, condition: bool = True, function=None, skip: int = 0) -> None: """Report that the test could not be run.""" if not condition: return self.condition = 'no_result' - no_result(self=self, - condition=condition, - function=function, - skip=skip) + no_result(self=self, condition=condition, function=function, skip=skip) - def pass_test(self, condition: bool=True, function=None) -> None: + def pass_test(self, condition: bool = True, function=None) -> None: """Cause the test to pass.""" if not condition: return @@ -1335,7 +1377,7 @@ def preserve(self, *conditions) -> None: if not conditions: conditions = ('pass_test', 'fail_test', 'no_result') for cond in conditions: - self._preserve[cond] = 1 + self._preserve[cond] = True def program_set(self, program) -> None: """Sets the executable program or script to be tested.""" @@ -1344,7 +1386,7 @@ def program_set(self, program) -> None: program = os.path.join(self._cwd, program) self.program = program - def read(self, file, mode: str='rb', newline=None): + def read(self, file, mode: str = 'rb', newline=None): """Reads and returns the contents of the specified file name. The file name may be a list, in which case the elements are @@ -1376,8 +1418,7 @@ def rmdir(self, dir) -> None: dir = self.canonicalize(dir) os.rmdir(dir) - - def parse_path(self, path, suppress_current: bool=False): + def parse_path(self, path, suppress_current: bool = False): """Return a list with the single path components of path.""" head, tail = os.path.split(path) result = [] @@ -1396,7 +1437,7 @@ def parse_path(self, path, suppress_current: bool=False): return result def dir_fixture(self, srcdir, dstdir=None) -> None: - """ Copies the contents of the fixture directory to the test directory. + """Copies the contents of the fixture directory to the test directory. If srcdir is an absolute path, it is tried directly, else the fixture_dirs are searched in order to find the named fixture @@ -1413,6 +1454,9 @@ def dir_fixture(self, srcdir, dstdir=None) -> None: """ if is_List(srcdir): srcdir = os.path.join(*srcdir) + if is_List(dstdir): + dstdir = os.path.join(*dstdir) + spath = srcdir if srcdir and self.fixture_dirs and not os.path.isabs(srcdir): for dir in self.fixture_dirs: @@ -1445,7 +1489,7 @@ def dir_fixture(self, srcdir, dstdir=None) -> None: shutil.copy(epath, dpath) def file_fixture(self, srcfile, dstfile=None) -> None: - """ Copies a fixture file to the test directory, optionally renaming. + """Copies a fixture file to the test directory, optionally renaming. If srcfile is an absolute path, it is tried directly, else the fixture_dirs are searched in order to find the named fixture @@ -1463,6 +1507,8 @@ def file_fixture(self, srcfile, dstfile=None) -> None: """ if is_List(srcfile): srcfile = os.path.join(*srcfile) + if is_List(dstfile): + dstfile = os.path.join(*dstfile) srcpath, srctail = os.path.split(srcfile) spath = srcfile @@ -1498,13 +1544,16 @@ def file_fixture(self, srcfile, dstfile=None) -> None: shutil.copy(spath, dpath) - def start(self, program=None, - interpreter=None, - arguments=None, - universal_newlines=None, - timeout=None, - **kw) -> Popen: - """ Starts a program or script for the test environment. + def start( + self, + program=None, + interpreter=None, + arguments=None, + universal_newlines=None, + timeout=None, + **kw, + ) -> Popen: + """Starts a program or script for the test environment. The specified program will have the original directory prepended unless it is enclosed in a [list]. @@ -1543,12 +1592,14 @@ def start(self, program=None, # It seems that all pythons up to py3.6 still set text mode if you set encoding. # TODO: File enhancement request on python to propagate universal_newlines even # if encoding is set.hg c - p = Popen(cmd, - stdin=stdin, - stdout=PIPE, - stderr=stderr_value, - env=os.environ, - universal_newlines=False) + p = Popen( + cmd, + stdin=stdin, + stdout=PIPE, + stderr=stderr_value, + env=os.environ, + universal_newlines=False, + ) self.process = p return p @@ -1576,7 +1627,7 @@ def fix_binary_stream(stream): return stream.replace('\r\n', '\n') def finish(self, popen=None, **kw) -> None: - """ Finishes and waits for the process. + """Finishes and waits for the process. Process being run under control of the specified popen argument is waited for, recording the exit status, output and error output. @@ -1614,13 +1665,16 @@ def finish(self, popen=None, **kw) -> None: self._stdout.append(stdout or '') self._stderr.append(stderr or '') - def run(self, program=None, - interpreter=None, - arguments=None, - chdir=None, - stdin=None, - universal_newlines=None, - timeout=None) -> None: + def run( + self, + program=None, + interpreter=None, + arguments=None, + chdir=None, + stdin=None, + universal_newlines=None, + timeout=None, + ) -> None: """Runs a test of the program or script for the test environment. Output and error output are saved for future retrieval via @@ -1650,12 +1704,14 @@ def run(self, program=None, os.chdir(chdir) if not timeout: timeout = self.timeout - p = self.start(program=program, - interpreter=interpreter, - arguments=arguments, - universal_newlines=universal_newlines, - timeout=timeout, - stdin=stdin) + p = self.start( + program=program, + interpreter=interpreter, + arguments=arguments, + universal_newlines=universal_newlines, + timeout=timeout, + stdin=stdin, + ) if is_List(stdin): stdin = ''.join(stdin) @@ -1698,12 +1754,12 @@ def run(self, program=None, if self.verbose >= 2: write = sys.stdout.write write('============ STATUS: %d\n' % self.status) - out = self.stdout() + out = self.stdout() or "" if out or self.verbose >= 3: write(f'============ BEGIN STDOUT (len={len(out)}):\n') write(out) write('============ END STDOUT\n') - err = self.stderr() + err = self.stderr() or "" if err or self.verbose >= 3: write(f'============ BEGIN STDERR (len={len(err)})\n') write(err) @@ -1718,7 +1774,7 @@ def sleep(self, seconds=default_sleep_seconds) -> None: """ time.sleep(seconds) - def stderr(self, run=None) -> Optional[str]: + def stderr(self, run=None) -> str | None: """Returns the stored standard error output from a given run. Args: @@ -1740,7 +1796,7 @@ def stderr(self, run=None) -> Optional[str]: except IndexError: return None - def stdout(self, run=None) -> Optional[str]: + def stdout(self, run=None) -> str | None: """Returns the stored standard output from a given run. Args: @@ -1788,7 +1844,6 @@ def subdir(self, *subdirs): return count - def symlink(self, target, link) -> None: """Creates a symlink to the specified target. @@ -1807,7 +1862,7 @@ def symlink(self, target, link) -> None: try: os.symlink(target, link) except AttributeError: - pass # Windows has no symlink + pass # Windows has no symlink def tempdir(self, path=None): """Creates a temporary directory. @@ -1932,7 +1987,7 @@ def workpath(self, *args): """ return os.path.join(self.workdir, *args) - def readable(self, top, read: bool=True) -> None: + def readable(self, top, read: bool = True) -> None: """Makes the specified directory tree readable or unreadable. Tree is made readable if `read` evaluates True (the default), @@ -1946,23 +2001,23 @@ def readable(self, top, read: bool=True) -> None: return if read: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE( - st[stat.ST_MODE] | stat.S_IREAD)) + os.chmod(fname, stat.S_IMODE(st.st_mode | stat.S_IREAD)) else: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE( - st[stat.ST_MODE] & ~stat.S_IREAD)) + os.chmod(fname, stat.S_IMODE(st.st_mode & ~stat.S_IREAD)) if os.path.isfile(top): # If it's a file, that's easy, just chmod it. @@ -1982,27 +2037,32 @@ def do_chmod(fname) -> None: # in the tree bottom-up, lest disabling read permission from # the top down get in the way of being able to get at lower # parts of the tree. - for dirpath, dirnames, filenames in os.walk(top, topdown=0): + for dirpath, dirnames, filenames in os.walk(top, topdown=False): for name in dirnames + filenames: do_chmod(os.path.join(dirpath, name)) do_chmod(top) - def writable(self, top, write: bool=True) -> None: + def writable(self, top, write: bool = True) -> None: """Make the specified directory tree writable or unwritable. Tree is made writable if `write` evaluates True (the default), else it is made not writable. + + Note on Windows the only thing we can do is and/remove the + "readable" setting without resorting to PyWin32 - and that, + only as Administrator, so this is kind of pointless there. """ if sys.platform == 'win32': - if write: + def do_chmod(fname) -> None: try: os.chmod(fname, stat.S_IWRITE) except OSError: pass else: + def do_chmod(fname) -> None: try: os.chmod(fname, stat.S_IREAD) @@ -2010,34 +2070,34 @@ def do_chmod(fname) -> None: pass else: - if write: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE] | 0o200)) + os.chmod(fname, stat.S_IMODE(st.st_mode | stat.S_IWRITE)) else: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE( - st[stat.ST_MODE] & ~0o200)) + os.chmod(fname, stat.S_IMODE(st.st_mode & ~stat.S_IWRITE)) if os.path.isfile(top): do_chmod(top) else: do_chmod(top) - for dirpath, dirnames, filenames in os.walk(top, topdown=0): + for dirpath, dirnames, filenames in os.walk(top, topdown=False): for name in dirnames + filenames: do_chmod(os.path.join(dirpath, name)) - def executable(self, top, execute: bool=True) -> None: + def executable(self, top, execute: bool = True) -> None: """Make the specified directory tree executable or not executable. Tree is made executable if `execute` evaluates True (the default), @@ -2051,23 +2111,23 @@ def executable(self, top, execute: bool=True) -> None: return if execute: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE( - st[stat.ST_MODE] | stat.S_IEXEC)) + os.chmod(fname, stat.S_IMODE(st.st_mode | stat.S_IEXEC)) else: + def do_chmod(fname) -> None: try: st = os.stat(fname) except OSError: pass else: - os.chmod(fname, stat.S_IMODE( - st[stat.ST_MODE] & ~stat.S_IEXEC)) + os.chmod(fname, stat.S_IMODE(st.st_mode & ~stat.S_IEXEC)) if os.path.isfile(top): # If it's a file, that's easy, just chmod it. @@ -2087,12 +2147,12 @@ def do_chmod(fname) -> None: # in the tree bottom-up, lest disabling execute permission from # the top down get in the way of being able to get at lower # parts of the tree. - for dirpath, dirnames, filenames in os.walk(top, topdown=0): + for dirpath, dirnames, filenames in os.walk(top, topdown=False): for name in dirnames + filenames: do_chmod(os.path.join(dirpath, name)) do_chmod(top) - def write(self, file, content, mode: str='wb'): + def write(self, file, content, mode: str = 'wb'): """Writes data to file. The file is created under the temporary working directory. @@ -2117,6 +2177,7 @@ def write(self, file, content, mode: str='wb'): except TypeError as e: f.write(bytes(content, 'utf-8')) + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/testing/framework/TestCmdTests.py b/testing/framework/TestCmdTests.py index 4340d90b89..8c286f55f5 100644 --- a/testing/framework/TestCmdTests.py +++ b/testing/framework/TestCmdTests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -""" -Unit tests for the TestCmd.py module. -""" - +# # Copyright 2000-2010 Steven Knight +# # This module is free software, and you may redistribute it and/or modify # it under the same terms as Python itself, so long as this copyright message # and disclaimer are retained in their original form. @@ -18,7 +16,12 @@ # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +# +# Python License: https://docs.python.org/3/license.html#psf-license +""" +Unit tests for the TestCmd.py module. +""" import os import shutil @@ -42,17 +45,21 @@ import TestCmd from TestCmd import _python_ + def _is_readable(path): - # XXX this doesn't take into account UID, it assumes it's our file - return os.stat(path)[stat.ST_MODE] & stat.S_IREAD + # TODO this doesn't take into account UID, it assumes it's our file + return os.stat(path).st_mode & stat.S_IREAD + def _is_writable(path): - # XXX this doesn't take into account UID, it assumes it's our file - return os.stat(path)[stat.ST_MODE] & stat.S_IWRITE + # TODO this doesn't take into account UID, it assumes it's our file + return os.stat(path).st_mode & stat.S_IWRITE + def _is_executable(path): - # XXX this doesn't take into account UID, it assumes it's our file - return os.stat(path)[stat.ST_MODE] & stat.S_IEXEC + # TODO this doesn't take into account UID, it assumes it's our file + return os.stat(path).st_mode & stat.S_IEXEC + def _clear_dict(dict, *keys) -> None: for key in keys: @@ -65,6 +72,7 @@ def _clear_dict(dict, *keys) -> None: class ExitError(Exception): pass + class TestCmdTestCase(unittest.TestCase): """Base class for TestCmd test cases, with fixture and utility methods.""" @@ -85,13 +93,19 @@ class T: t.script1 = 'script_1.txt' t.scriptout = 'scriptout' t.scripterr = 'scripterr' - fmt = "import os, sys; cwd = os.getcwd(); " + \ - "sys.stdout.write('%s: STDOUT: %%s: %%s\\n' %% (cwd, sys.argv[1:])); " + \ - "sys.stderr.write('%s: STDERR: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" - fmtout = "import os, sys; cwd = os.getcwd(); " + \ - "sys.stdout.write('%s: STDOUT: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" - fmterr = "import os, sys; cwd = os.getcwd(); " + \ - "sys.stderr.write('%s: STDERR: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" + fmt = ( + "import os, sys; cwd = os.getcwd(); " + "sys.stdout.write('%s: STDOUT: %%s: %%s\\n' %% (cwd, sys.argv[1:])); " + "sys.stderr.write('%s: STDERR: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" + ) + fmtout = ( + "import os, sys; cwd = os.getcwd(); " + "sys.stdout.write('%s: STDOUT: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" + ) + fmterr = ( + "import os, sys; cwd = os.getcwd(); " + + "sys.stderr.write('%s: STDERR: %%s: %%s\\n' %% (cwd, sys.argv[1:]))" + ) text = fmt % (t.script, t.script) textx = fmt % (t.scriptx, t.scriptx) if sys.platform == 'win32': @@ -103,7 +117,7 @@ class T: textout = fmtout % t.scriptout texterr = fmterr % t.scripterr - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') run_env.subdir('sub dir') t.run_env = run_env @@ -145,7 +159,14 @@ def call_python(self, indata, python=None): stderr = self.translate_newlines(to_str(cp.stderr)) return stdout, stderr, cp.returncode - def popen_python(self, indata, status: int=0, stdout: str="", stderr: str="", python=None) -> None: + def popen_python( + self, + indata: str, + status: int = 0, + stdout: str = "", + stderr: str = "", + python: str = None, + ) -> None: if python is None: python = sys.executable _stdout, _stderr, _status = self.call_python(indata, python) @@ -163,6 +184,7 @@ def popen_python(self, indata, status: int=0, stdout: str="", stderr: str="", py f"Expected STDERR ==========\n{stderr}" f"Actual STDERR ============\n{_stderr}" ) + self.assertEqual(stderr, _stderr) def run_match(self, content, *args) -> None: expect = "%s: %s: %s: %s\n" % args @@ -173,17 +195,15 @@ def run_match(self, content, *args) -> None: ) - class __init__TestCase(TestCmdTestCase): def test_init(self) -> None: """Test init()""" test = TestCmd.TestCmd() - test = TestCmd.TestCmd(description = 'test') - test = TestCmd.TestCmd(description = 'test', program = 'foo') - test = TestCmd.TestCmd(description = 'test', - program = 'foo', - universal_newlines=None) - + test = TestCmd.TestCmd(description='test') + test = TestCmd.TestCmd(description='test', program='foo') + test = TestCmd.TestCmd( + description='test', program='foo', universal_newlines=None + ) class basename_TestCase(TestCmdTestCase): @@ -192,11 +212,10 @@ def test_basename(self) -> None: assert 1 == 1 - class cleanup_TestCase(TestCmdTestCase): def test_cleanup(self) -> None: """Test cleanup()""" - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir test.write('file1', "Test file #1\n") test.cleanup() @@ -204,7 +223,7 @@ def test_cleanup(self) -> None: def test_writable(self) -> None: """Test cleanup() when the directory isn't writable""" - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir test.write('file2', "Test file #2\n") os.chmod(test.workpath('file2'), 0o400) @@ -214,15 +233,18 @@ def test_writable(self) -> None: def test_shutil(self) -> None: """Test cleanup() when used with shutil""" - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir os.chdir(wdir) import shutil + save_rmtree = shutil.rmtree - def my_rmtree(dir, ignore_errors: int=0, wdir=wdir, _rmtree=save_rmtree): + + def my_rmtree(dir, ignore_errors: bool = False, wdir=wdir, _rmtree=save_rmtree): assert os.getcwd() != wdir return _rmtree(dir, ignore_errors=ignore_errors) + try: shutil.rmtree = my_rmtree test.cleanup() @@ -231,7 +253,8 @@ def my_rmtree(dir, ignore_errors: int=0, wdir=wdir, _rmtree=save_rmtree): def test_atexit(self) -> None: """Test cleanup when atexit is used""" - self.popen_python(f"""\ + self.popen_python( + indata=f"""\ import atexit import sys import TestCmd @@ -244,13 +267,15 @@ def cleanup(): result = TestCmd.TestCmd(workdir='') sys.exit(0) -""", stdout='cleanup()\n') +""", + stdout='cleanup()\n', + ) class chmod_TestCase(TestCmdTestCase): def test_chmod(self) -> None: """Test chmod()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'sub') + test = TestCmd.TestCmd(workdir='', subdir='sub') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_sub_file2 = os.path.join(test.workdir, 'sub', 'file2') @@ -261,71 +286,76 @@ def test_chmod(self) -> None: f.write("") if sys.platform == 'win32': - test.chmod(wdir_file1, stat.S_IREAD) test.chmod(['sub', 'file2'], stat.S_IWRITE) - file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE]) + file1_mode = stat.S_IMODE(os.stat(wdir_file1).st_mode) assert file1_mode == 0o444, f'0{file1_mode:o}' - file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE]) + file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2).st_mode) assert file2_mode == 0o666, f'0{file2_mode:o}' test.chmod('file1', stat.S_IWRITE) test.chmod(wdir_sub_file2, stat.S_IREAD) - file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE]) + file1_mode = stat.S_IMODE(os.stat(wdir_file1).st_mode) assert file1_mode == 0o666, f'0{file1_mode:o}' - file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE]) + file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2).st_mode) assert file2_mode == 0o444, f'0{file2_mode:o}' else: - test.chmod(wdir_file1, 0o700) test.chmod(['sub', 'file2'], 0o760) - file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE]) + file1_mode = stat.S_IMODE(os.stat(wdir_file1).st_mode) assert file1_mode == 0o700, f'0{file1_mode:o}' - file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE]) + file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2).st_mode) assert file2_mode == 0o760, f'0{file2_mode:o}' test.chmod('file1', 0o765) test.chmod(wdir_sub_file2, 0o567) - file1_mode = stat.S_IMODE(os.stat(wdir_file1)[stat.ST_MODE]) + file1_mode = stat.S_IMODE(os.stat(wdir_file1).st_mode) assert file1_mode == 0o765, f'0{file1_mode:o}' - file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2)[stat.ST_MODE]) + file2_mode = stat.S_IMODE(os.stat(wdir_sub_file2).st_mode) assert file2_mode == 0o567, f'0{file2_mode:o}' - class combine_TestCase(TestCmdTestCase): def test_combine(self): """Test combining stdout and stderr""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run1', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run1', + """\ +import sys + sys.stdout.write("run1 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run1 STDOUT second line\\n") sys.stderr.write("run1 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run1 STDERR second line\\n") sys.stdout.write("run1 STDOUT third line\\n") sys.stderr.write("run1 STDERR third line\\n") -""") - run_env.write('run2', """import sys +""", + ) + run_env.write( + 'run2', + """\ +import sys + sys.stdout.write("run2 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run2 STDOUT second line\\n") sys.stderr.write("run2 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run2 STDERR second line\\n") sys.stdout.write("run2 STDOUT third line\\n") sys.stderr.write("run2 STDERR third line\\n") -""") +""", + ) cwd = os.getcwd() os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. try: - test = TestCmd.TestCmd(interpreter = 'python', - workdir = '', - combine = 1) + test = TestCmd.TestCmd(interpreter='python', workdir='', combine=1) output = test.stdout() if output is not None: raise IndexError(f"got unexpected output:\n\t`{output}'\n") @@ -334,7 +364,7 @@ def test_combine(self): # stdout and stderr in different orders, so we accomodate both. test.program_set('run1') - test.run(arguments = 'foo bar') + test.run(arguments='foo bar') stdout_lines = """\ run1 STDOUT ['foo', 'bar'] run1 STDOUT second line @@ -345,11 +375,10 @@ def test_combine(self): run1 STDERR second line run1 STDERR third line """ - foo_bar_expect = (stdout_lines + stderr_lines, - stderr_lines + stdout_lines) + foo_bar_expect = (stdout_lines + stderr_lines, stderr_lines + stdout_lines) test.program_set('run2') - test.run(arguments = 'snafu') + test.run(arguments='snafu') stdout_lines = """\ run2 STDOUT ['snafu'] run2 STDOUT second line @@ -360,8 +389,7 @@ def test_combine(self): run2 STDERR second line run2 STDERR third line """ - snafu_expect = (stdout_lines + stderr_lines, - stderr_lines + stdout_lines) + snafu_expect = (stdout_lines + stderr_lines, stderr_lines + stdout_lines) # XXX SHOULD TEST ABSOLUTE NUMBER AS WELL output = test.stdout() @@ -370,7 +398,7 @@ def test_combine(self): error = test.stderr() assert error == '', error - output = test.stdout(run = -1) + output = test.stdout(run=-1) output = self.translate_newlines(output) assert output in foo_bar_expect, output error = test.stderr(-1) @@ -379,19 +407,17 @@ def test_combine(self): os.chdir(cwd) - class description_TestCase(TestCmdTestCase): def test_description(self) -> None: """Test description()""" test = TestCmd.TestCmd() assert test.description is None, 'initialized description?' - test = TestCmd.TestCmd(description = 'test') + test = TestCmd.TestCmd(description='test') assert test.description == 'test', 'uninitialized description' test.description_set('foo') assert test.description == 'foo', 'did not set description' - class diff_TestCase(TestCmdTestCase): def test_diff_re(self) -> None: """Test diff_re()""" @@ -407,51 +433,57 @@ def test_diff_re(self) -> None: def test_diff_custom_function(self) -> None: """Test diff() using a custom function""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def my_diff(a, b): - return [ - '*****', - a, - '*****', - b, - '*****', - ] -test = TestCmd.TestCmd(diff = my_diff) + return '*****', a, '*****', b, '*****' + +test = TestCmd.TestCmd(diff=my_diff) test.diff("a\\nb1\\nc\\n", "a\\nb2\\nc\\n", "STDOUT") sys.exit(0) """, - stdout = """\ + stdout="""\ STDOUT========================================================================== ***** ['a', 'b1', 'c'] ***** ['a', 'b2', 'c'] ***** -""") +""", + ) def test_diff_string(self) -> None: - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(diff = 'diff_re') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(diff='diff_re') test.diff("a\\nb1\\nc\\n", "a\\nb2\\nc\\n", 'STDOUT') sys.exit(0) """, - stdout = """\ + stdout="""\ STDOUT========================================================================== 2c2 < 'b1' --- > 'b2' -""") +""", + ) def test_error(self) -> None: """Test handling a compilation error in TestCmd.diff_re()""" - script_input = f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + script_input = f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path assert TestCmd.diff_re([r"a.*(e"], ["abcde"]) sys.exit(0) """ @@ -459,29 +491,39 @@ def test_error(self) -> None: assert status == 1, status expect1 = "Regular expression error in '^a.*(e$': missing )" expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis" - assert (stderr.find(expect1) != -1 or - stderr.find(expect2) != -1), repr(stderr) + assert stderr.find(expect1) != -1 or stderr.find(expect2) != -1, repr(stderr) def test_simple_diff_static_method(self) -> None: """Test calling the TestCmd.TestCmd.simple_diff() static method""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -result = TestCmd.TestCmd.simple_diff(['a', 'b', 'c', 'e', 'f1'], - ['a', 'c', 'd', 'e', 'f2']) + +sys.path = [r'{self.orig_cwd}'] + sys.path + +result = TestCmd.TestCmd.simple_diff( + ['a', 'b', 'c', 'e', 'f1'], ['a', 'c', 'd', 'e', 'f2'] +) result = list(result) expect = ['2d1', '< b', '3a3', '> d', '5c5', '< f1', '---', '> f2'] assert result == expect, result sys.exit(0) -""") +""", + ) def test_context_diff_static_method(self) -> None: """Test calling the TestCmd.TestCmd.context_diff() static method""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -result = TestCmd.TestCmd.context_diff(['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'], - ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n']) + +sys.path = [r'{self.orig_cwd}'] + sys.path +result = TestCmd.TestCmd.context_diff( + ['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'], + ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n' ], +) result = list(result) expect = [ '*** \\n', @@ -502,15 +544,21 @@ def test_context_diff_static_method(self) -> None: ] assert result == expect, result sys.exit(0) -""") +""", + ) def test_unified_diff_static_method(self) -> None: """Test calling the TestCmd.TestCmd.unified_diff() static method""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -result = TestCmd.TestCmd.unified_diff(['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'], - ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n']) + +sys.path = [r'{self.orig_cwd}'] + sys.path +result = TestCmd.TestCmd.unified_diff( + ['a\\n', 'b\\n', 'c\\n', 'e\\n', 'f1\\n'], + ['a\\n', 'c\\n', 'd\\n', 'e\\n', 'f2\\n'], +) result = list(result) expect = [ '--- \\n', @@ -522,19 +570,25 @@ def test_unified_diff_static_method(self) -> None: '+d\\n', ' e\\n', '-f1\\n', - '+f2\\n' + '+f2\\n', ] assert result == expect, result sys.exit(0) -""") +""", + ) def test_diff_re_static_method(self) -> None: """Test calling the TestCmd.TestCmd.diff_re() static method""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -result = TestCmd.TestCmd.diff_re(['a', 'b', 'c', '.', 'f1'], - ['a', 'c', 'd', 'e', 'f2']) + +sys.path = [r'{self.orig_cwd}'] + sys.path +result = TestCmd.TestCmd.diff_re( + ['a', 'b', 'c', '.', 'f1'], + ['a', 'c', 'd', 'e', 'f2'], +) result = list(result) expect = [ '2c2', @@ -548,255 +602,309 @@ def test_diff_re_static_method(self) -> None: '5c5', "< 'f1'", '---', - "> 'f2'" + "> 'f2'", ] assert result == expect, result sys.exit(0) -""") - +""", + ) class diff_stderr_TestCase(TestCmdTestCase): def test_diff_stderr_default(self) -> None: """Test diff_stderr() default behavior""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + test = TestCmd.TestCmd() -test.diff_stderr('a\nb1\nc\n', 'a\nb2\nc\n') +test.diff_stderr('a\\nb1\\nc\\n', 'a\\nb2\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 2c2 < b1 --- > b2 -""") +""", + ) def test_diff_stderr_not_affecting_diff_stdout(self) -> None: """Test diff_stderr() not affecting diff_stdout() behavior""" - self.popen_python(fr""" + self.popen_python( + indata=f"""\ import sys -sys.path = [r'{self.orig_cwd}'] + sys.path import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stderr='diff_re') print("diff_stderr:") -test.diff_stderr('a\nb.\nc\n', 'a\nbb\nc\n') +test.diff_stderr('a\\nb.\\nc\\n', 'a\\nbb\\nc\\n') print("diff_stdout:") -test.diff_stdout('a\nb.\nc\n', 'a\nbb\nc\n') +test.diff_stdout('a\\nb.\\nc\\n', 'a\\nbb\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ diff_stderr: diff_stdout: 2c2 < b. --- > bb -""") +""", + ) def test_diff_stderr_custom_function(self) -> None: """Test diff_stderr() using a custom function""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def my_diff(a, b): return ["a:"] + a + ["b:"] + b + test = TestCmd.TestCmd(diff_stderr=my_diff) test.diff_stderr('abc', 'def') sys.exit(0) """, - stdout="""\ + stdout="""\ a: abc b: def -""") +""", + ) def test_diff_stderr_TestCmd_function(self) -> None: """Test diff_stderr() using a TestCmd function""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(diff_stderr = TestCmd.diff_re) -test.diff_stderr('a\n.\n', 'b\nc\n') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(diff_stderr=TestCmd.diff_re) +test.diff_stderr('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") +""", + ) def test_diff_stderr_static_method(self) -> None: """Test diff_stderr() using a static method""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stderr=TestCmd.TestCmd.diff_re) -test.diff_stderr('a\n.\n', 'b\nc\n') +test.diff_stderr('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") +""", + ) def test_diff_stderr_string(self) -> None: """Test diff_stderr() using a string to fetch the diff method""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stderr='diff_re') -test.diff_stderr('a\n.\n', 'b\nc\n') +test.diff_stderr('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") - +""", + ) class diff_stdout_TestCase(TestCmdTestCase): def test_diff_stdout_default(self) -> None: """Test diff_stdout() default behavior""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd() -test.diff_stdout('a\nb1\nc\n', 'a\nb2\nc\n') +test.diff_stdout('a\\nb1\\nc\\n', 'a\\nb2\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 2c2 < b1 --- > b2 -""") +""", + ) def test_diff_stdout_not_affecting_diff_stderr(self) -> None: """Test diff_stdout() not affecting diff_stderr() behavior""" - self.popen_python(fr""" + self.popen_python( + indata=f"""\ import sys -sys.path = [r'{self.orig_cwd}'] + sys.path import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stdout='diff_re') print("diff_stdout:") -test.diff_stdout('a\nb.\nc\n', 'a\nbb\nc\n') +test.diff_stdout('a\\nb.\\nc\\n', 'a\\nbb\\nc\\n') print("diff_stderr:") -test.diff_stderr('a\nb.\nc\n', 'a\nbb\nc\n') +test.diff_stderr('a\\nb.\\nc\\n', 'a\\nbb\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ diff_stdout: diff_stderr: 2c2 < b. --- > bb -""") +""", + ) def test_diff_stdout_custom_function(self) -> None: """Test diff_stdout() using a custom function""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def my_diff(a, b): return ["a:"] + a + ["b:"] + b + test = TestCmd.TestCmd(diff_stdout=my_diff) test.diff_stdout('abc', 'def') sys.exit(0) """, - stdout="""\ + stdout="""\ a: abc b: def -""") +""", + ) def test_diff_stdout_TestCmd_function(self) -> None: """Test diff_stdout() using a TestCmd function""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(diff_stdout = TestCmd.diff_re) -test.diff_stdout('a\n.\n', 'b\nc\n') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(diff_stdout=TestCmd.diff_re) +test.diff_stdout('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") +""", + ) def test_diff_stdout_static_method(self) -> None: """Test diff_stdout() using a static method""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stdout=TestCmd.TestCmd.diff_re) -test.diff_stdout('a\n.\n', 'b\nc\n') +test.diff_stdout('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") +""", + ) def test_diff_stdout_string(self) -> None: """Test diff_stdout() using a string to fetch the diff method""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd(diff_stdout='diff_re') -test.diff_stdout('a\n.\n', 'b\nc\n') +test.diff_stdout('a\\n.\\n', 'b\\nc\\n') sys.exit(0) """, - stdout="""\ + stdout="""\ 1c1 < 'a' --- > 'b' -""") - +""", + ) class exit_TestCase(TestCmdTestCase): def test_exit(self) -> None: """Test exit()""" + def _test_it(cwd, tempdir, condition, preserved): close_true = {'pass_test': 1, 'fail_test': 0, 'no_result': 0} exit_status = {'pass_test': 0, 'fail_test': 1, 'no_result': 2} - result_string = {'pass_test': "PASSED\n", - 'fail_test': "FAILED test at line 5 of \n", - 'no_result': "NO RESULT for test at line 5 of \n"} + result_string = { + 'pass_test': "PASSED\n", + 'fail_test': "FAILED test at line 6 of \n", + 'no_result': "NO RESULT for test at line 6 of \n", + } global ExitError - input = f"""import sys -sys.path = [r'{cwd}'] + sys.path + input = f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(workdir = '{tempdir}') + +sys.path = [r'{cwd}'] + sys.path +test = TestCmd.TestCmd(workdir='{tempdir}') test.{condition}() """ stdout, stderr, status = self.call_python(input, python="python") if close_true[condition]: - unexpected = (status != 0) + unexpected = status != 0 else: - unexpected = (status == 0) + unexpected = status == 0 if unexpected: msg = "Unexpected exit status from python: %s\n" raise ExitError(msg % status + stdout + stderr) if status != exit_status[condition]: - msg = "Expected exit status %d, got %d\n" - raise ExitError(msg % (exit_status[condition], status)) + msg = "Expected exit status %d, got %d\n" + raise ExitError(msg % (exit_status[condition], status)) if stderr != result_string[condition]: msg = "Expected error output:\n%s\nGot error output:\n%s" raise ExitError(msg % (result_string[condition], stderr)) @@ -809,13 +917,19 @@ def _test_it(cwd, tempdir, condition, preserved): msg = "Working directory %s was mistakenly preserved\n" raise ExitError(msg % tempdir + stdout) - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. try: cwd = self.orig_cwd - _clear_dict(os.environ, 'PRESERVE', 'PRESERVE_PASS', 'PRESERVE_FAIL', 'PRESERVE_NO_RESULT') + _clear_dict( + os.environ, + 'PRESERVE', + 'PRESERVE_PASS', + 'PRESERVE_FAIL', + 'PRESERVE_NO_RESULT', + ) _test_it(cwd, 'dir01', 'pass_test', 0) _test_it(cwd, 'dir02', 'fail_test', 0) _test_it(cwd, 'dir03', 'no_result', 0) @@ -840,89 +954,168 @@ def _test_it(cwd, tempdir, condition, preserved): _test_it(cwd, 'dir15', 'no_result', 1) del os.environ['PRESERVE_NO_RESULT'] finally: - _clear_dict(os.environ, 'PRESERVE', 'PRESERVE_PASS', 'PRESERVE_FAIL', 'PRESERVE_NO_RESULT') - + _clear_dict( + os.environ, + 'PRESERVE', + 'PRESERVE_PASS', + 'PRESERVE_FAIL', + 'PRESERVE_NO_RESULT', + ) class fail_test_TestCase(TestCmdTestCase): def test_fail_test(self) -> None: """Test fail_test()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run', + """\ +import sys + sys.stdout.write("run: STDOUT\\n") sys.stderr.write("run: STDERR\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -TestCmd.fail_test(condition = 1) -""", status = 1, stderr = "FAILED test at line 4 of \n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +TestCmd.fail_test(condition=1) +""", + status=1, + stderr="FAILED test at line 5 of \n", + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') -test.run() -test.fail_test(condition = (test.status == 0)) -""", status = 1, stderr = f"FAILED test of {run_env.workpath('run')}\n\tat line 6 of \n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path -import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', description = 'xyzzy', workdir = '') +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') test.run() -test.fail_test(condition = (test.status == 0)) -""", status = 1, stderr = f"FAILED test of {run_env.workpath('run')} [xyzzy]\n\tat line 6 of \n") +test.fail_test(condition=(test.status == 0)) +""", + status=1, + stderr=( + f"FAILED test of {run_env.workpath('run')}\n" + "\tat line 7 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys +import TestCmd - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd( + program='run', interpreter='python', description='xyzzy', workdir='' +) +test.run() +test.fail_test(condition=(test.status == 0)) +""", + status=1, + stderr=( + f"FAILED test of {run_env.workpath('run')} [xyzzy]\n" + "\tat line 9 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') test.run() + def xxx(): sys.stderr.write("printed on failure\\n") -test.fail_test(condition = (test.status == 0), function = xxx) -""", status = 1, stderr = f"printed on failure\nFAILED test of {run_env.workpath('run')}\n\tat line 8 of \n") - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path +test.fail_test(condition=(test.status == 0), function=xxx) +""", + status=1, + stderr=( + "printed on failure\n" + f"FAILED test of {run_env.workpath('run')}\n" + "\tat line 11 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def test1(self): self.run() - self.fail_test(condition = (self.status == 0)) + self.fail_test(condition=(self.status == 0)) + def test2(self): test1(self) -test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')) -""", status = 1, stderr = f"FAILED test of {run_env.workpath('run')}\n\tat line 6 of (test1)\n\tfrom line 8 of (test2)\n\tfrom line 9 of \n") - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path +test2(TestCmd.TestCmd(program='run', interpreter='python', workdir='')) +""", + status=1, + stderr=( + f"FAILED test of {run_env.workpath('run')}\n" + "\tat line 8 of (test1)\n" + "\tfrom line 11 of (test2)\n" + "\tfrom line 13 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def test1(self): self.run() - self.fail_test(condition = (self.status == 0), skip = 1) + self.fail_test(condition=(self.status == 0), skip=1) + def test2(self): test1(self) -test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')) -""", status = 1, stderr = f"FAILED test of {run_env.workpath('run')}\n\tat line 8 of (test2)\n\tfrom line 9 of \n") +test2(TestCmd.TestCmd(program='run', interpreter='python', workdir='')) +""", + status=1, + stderr=( + f"FAILED test of {run_env.workpath('run')}\n" + "\tat line 11 of (test2)\n" + "\tfrom line 13 of \n" + ), + ) class interpreter_TestCase(TestCmdTestCase): def test_interpreter(self) -> None: """Test interpreter()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run', + """\ +import sys + sys.stdout.write("run: STDOUT\\n") sys.stderr.write("run: STDERR\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - test = TestCmd.TestCmd(program = 'run', workdir = '') + test = TestCmd.TestCmd(program='run', workdir='') test.interpreter_set('foo') assert test.interpreter == 'foo', 'did not set interpreter' test.interpreter_set('python') @@ -930,7 +1123,6 @@ def test_interpreter(self) -> None: test.run() - class match_TestCase(TestCmdTestCase): def test_match_default(self) -> None: """Test match() default behavior""" @@ -943,8 +1135,10 @@ def test_match_default(self) -> None: def test_match_custom_function(self) -> None: """Test match() using a custom function""" + def match_length(lines, matches): return len(lines) == len(matches) + test = TestCmd.TestCmd(match=match_length) assert not test.match("123\n", "1\n") assert test.match("123\n", "111\n") @@ -952,11 +1146,11 @@ def match_length(lines, matches): assert test.match("123\n123\n", "111\n111\n") lines = ["123\n", "123\n"] regexes = ["1\n", "1\n"] - assert test.match(lines, regexes) # due to equal numbers of lines + assert test.match(lines, regexes) # due to equal numbers of lines def test_match_TestCmd_function(self) -> None: """Test match() using a TestCmd function""" - test = TestCmd.TestCmd(match = TestCmd.match_exact) + test = TestCmd.TestCmd(match=TestCmd.match_exact) assert not test.match("abcde\n", "a.*e\n") assert test.match("abcde\n", "abcde\n") assert not test.match("12345\nabcde\n", "1\\d+5\na.*e\n") @@ -991,7 +1185,6 @@ def test_match_string(self) -> None: assert test.match(lines, lines) - class match_exact_TestCase(TestCmdTestCase): def test_match_exact_function(self) -> None: """Test calling the TestCmd.match_exact() function""" @@ -1016,14 +1209,18 @@ def test_evaluation(self) -> None: assert test.match_exact("abcde\n", "abcde\n") assert not test.match_exact(["12345\n", "abcde\n"], ["1[0-9]*5\n", "a.*e\n"]) assert test.match_exact(["12345\n", "abcde\n"], ["12345\n", "abcde\n"]) - assert not test.match_exact(UserList(["12345\n", "abcde\n"]), - ["1[0-9]*5\n", "a.*e\n"]) - assert test.match_exact(UserList(["12345\n", "abcde\n"]), - ["12345\n", "abcde\n"]) - assert not test.match_exact(["12345\n", "abcde\n"], - UserList(["1[0-9]*5\n", "a.*e\n"])) - assert test.match_exact(["12345\n", "abcde\n"], - UserList(["12345\n", "abcde\n"])) + assert not test.match_exact( + UserList(["12345\n", "abcde\n"]), ["1[0-9]*5\n", "a.*e\n"] + ) + assert test.match_exact( + UserList(["12345\n", "abcde\n"]), ["12345\n", "abcde\n"] + ) + assert not test.match_exact( + ["12345\n", "abcde\n"], UserList(["1[0-9]*5\n", "a.*e\n"]) + ) + assert test.match_exact( + ["12345\n", "abcde\n"], UserList(["12345\n", "abcde\n"]) + ) assert not test.match_exact("12345\nabcde\n", "1[0-9]*5\na.*e\n") assert test.match_exact("12345\nabcde\n", "12345\nabcde\n") lines = ["vwxyz\n", "67890\n"] @@ -1032,7 +1229,6 @@ def test_evaluation(self) -> None: assert test.match_exact(lines, lines) - class match_re_dotall_TestCase(TestCmdTestCase): def test_match_re_dotall_function(self) -> None: """Test calling the TestCmd.match_re_dotall() function""" @@ -1049,7 +1245,7 @@ def test_match_re_dotall_static_method(self) -> None: def test_error(self) -> None: """Test handling a compilation error in TestCmd.match_re_dotall()""" - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') cwd = os.getcwd() os.chdir(run_env.workdir) # Everything before this prepared our "source directory." @@ -1065,8 +1261,9 @@ def test_error(self) -> None: assert status == 1, status expect1 = "Regular expression error in '^a.*(e$': missing )" expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis" - assert (stderr.find(expect1) != -1 or - stderr.find(expect2) != -1), repr(stderr) + assert stderr.find(expect1) != -1 or stderr.find(expect2) != -1, repr( + stderr + ) finally: os.chdir(cwd) @@ -1076,36 +1273,44 @@ def test_evaluation(self) -> None: assert test.match_re_dotall("abcde\nfghij\n", r"a.*e\nf.*j\n") assert test.match_re_dotall("abcde\nfghij\n", r"a[^j]*j\n") assert test.match_re_dotall("abcde\nfghij\n", r"abcde\nfghij\n") - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - [r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"]) - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - [r"1.*j\n"]) - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - [r"12345\n", r"abcde\n", r"fghij\n"]) - assert test.match_re_dotall(UserList(["12345\n", "abcde\n", "fghij\n"]), - [r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"]) - assert test.match_re_dotall(UserList(["12345\n", "abcde\n", "fghij\n"]), - [r"1.*j\n"]) - assert test.match_re_dotall(UserList(["12345\n", "abcde\n", "fghij\n"]), - [r"12345\n", r"abcde\n", r"fghij\n"]) - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - UserList([r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"])) - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - UserList([r"1.*j\n"])) - assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], - UserList([r"12345\n", r"abcde\n", r"fghij\n"])) - assert test.match_re_dotall("12345\nabcde\nfghij\n", - r"1[0-9]*5\na.*e\nf.*j\n") + assert test.match_re_dotall( + ["12345\n", "abcde\n", "fghij\n"], [r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"] + ) + assert test.match_re_dotall(["12345\n", "abcde\n", "fghij\n"], [r"1.*j\n"]) + assert test.match_re_dotall( + ["12345\n", "abcde\n", "fghij\n"], [r"12345\n", r"abcde\n", r"fghij\n"] + ) + assert test.match_re_dotall( + UserList(["12345\n", "abcde\n", "fghij\n"]), + [r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"], + ) + assert test.match_re_dotall( + UserList(["12345\n", "abcde\n", "fghij\n"]), [r"1.*j\n"] + ) + assert test.match_re_dotall( + UserList(["12345\n", "abcde\n", "fghij\n"]), + [r"12345\n", r"abcde\n", r"fghij\n"], + ) + assert test.match_re_dotall( + ["12345\n", "abcde\n", "fghij\n"], + UserList([r"1[0-9]*5\n", r"a.*e\n", r"f.*j\n"]), + ) + assert test.match_re_dotall( + ["12345\n", "abcde\n", "fghij\n"], UserList([r"1.*j\n"]) + ) + assert test.match_re_dotall( + ["12345\n", "abcde\n", "fghij\n"], + UserList([r"12345\n", r"abcde\n", r"fghij\n"]), + ) + assert test.match_re_dotall("12345\nabcde\nfghij\n", r"1[0-9]*5\na.*e\nf.*j\n") assert test.match_re_dotall("12345\nabcde\nfghij\n", r"1.*j\n") - assert test.match_re_dotall("12345\nabcde\nfghij\n", - r"12345\nabcde\nfghij\n") + assert test.match_re_dotall("12345\nabcde\nfghij\n", r"12345\nabcde\nfghij\n") lines = ["vwxyz\n", "67890\n"] regexes = [r"v[^a-u]*z\n", r"6[^ ]+0\n"] assert test.match_re_dotall(lines, regexes) assert test.match_re_dotall(lines, lines) - class match_re_TestCase(TestCmdTestCase): def test_match_re_function(self) -> None: """Test calling the TestCmd.match_re() function""" @@ -1122,7 +1327,7 @@ def test_match_re_static_method(self) -> None: def test_error(self) -> None: """Test handling a compilation error in TestCmd.match_re()""" - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') cwd = os.getcwd() os.chdir(run_env.workdir) # Everything before this prepared our "source directory." @@ -1140,8 +1345,9 @@ def test_error(self) -> None: assert status == 1, status expect1 = "Regular expression error in '^a.*(e$': missing )" expect2 = "Regular expression error in '^a.*(e$': unbalanced parenthesis" - assert (stderr.find(expect1) != -1 or - stderr.find(expect2) != -1), repr(stderr) + assert stderr.find(expect1) != -1 or stderr.find(expect2) != -1, repr( + stderr + ) finally: os.chdir(cwd) @@ -1152,14 +1358,10 @@ def test_evaluation(self) -> None: assert test.match_re("abcde\n", "abcde\n") assert test.match_re(["12345\n", "abcde\n"], ["1[0-9]*5\n", "a.*e\n"]) assert test.match_re(["12345\n", "abcde\n"], ["12345\n", "abcde\n"]) - assert test.match_re(UserList(["12345\n", "abcde\n"]), - ["1[0-9]*5\n", "a.*e\n"]) - assert test.match_re(UserList(["12345\n", "abcde\n"]), - ["12345\n", "abcde\n"]) - assert test.match_re(["12345\n", "abcde\n"], - UserList(["1[0-9]*5\n", "a.*e\n"])) - assert test.match_re(["12345\n", "abcde\n"], - UserList(["12345\n", "abcde\n"])) + assert test.match_re(UserList(["12345\n", "abcde\n"]), ["1[0-9]*5\n", "a.*e\n"]) + assert test.match_re(UserList(["12345\n", "abcde\n"]), ["12345\n", "abcde\n"]) + assert test.match_re(["12345\n", "abcde\n"], UserList(["1[0-9]*5\n", "a.*e\n"])) + assert test.match_re(["12345\n", "abcde\n"], UserList(["12345\n", "abcde\n"])) assert test.match_re("12345\nabcde\n", "1[0-9]*5\na.*e\n") assert test.match_re("12345\nabcde\n", "12345\nabcde\n") lines = ["vwxyz\n", "67890\n"] @@ -1168,7 +1370,6 @@ def test_evaluation(self) -> None: assert test.match_re(lines, lines) - class match_stderr_TestCase(TestCmdTestCase): def test_match_stderr_default(self) -> None: """Test match_stderr() default behavior""" @@ -1200,8 +1401,10 @@ def test_match_stderr_not_affecting_match_stdout(self) -> None: def test_match_stderr_custom_function(self) -> None: """Test match_stderr() using a custom function""" + def match_length(lines, matches): return len(lines) == len(matches) + test = TestCmd.TestCmd(match_stderr=match_length) assert not test.match_stderr("123\n", "1\n") assert test.match_stderr("123\n", "111\n") @@ -1209,11 +1412,11 @@ def match_length(lines, matches): assert test.match_stderr("123\n123\n", "111\n111\n") lines = ["123\n", "123\n"] regexes = [r"1\n", r"1\n"] - assert test.match_stderr(lines, regexes) # equal numbers of lines + assert test.match_stderr(lines, regexes) # equal numbers of lines def test_match_stderr_TestCmd_function(self) -> None: """Test match_stderr() using a TestCmd function""" - test = TestCmd.TestCmd(match_stderr = TestCmd.match_exact) + test = TestCmd.TestCmd(match_stderr=TestCmd.match_exact) assert not test.match_stderr("abcde\n", "a.*e\n") assert test.match_stderr("abcde\n", "abcde\n") assert not test.match_stderr("12345\nabcde\n", "1\\d+5\na.*e\n") @@ -1248,7 +1451,6 @@ def test_match_stderr_string(self) -> None: assert test.match_stderr(lines, lines) - class match_stdout_TestCase(TestCmdTestCase): def test_match_stdout_default(self) -> None: """Test match_stdout() default behavior""" @@ -1280,8 +1482,10 @@ def test_match_stdout_not_affecting_match_stderr(self) -> None: def test_match_stdout_custom_function(self) -> None: """Test match_stdout() using a custom function""" + def match_length(lines, matches): return len(lines) == len(matches) + test = TestCmd.TestCmd(match_stdout=match_length) assert not test.match_stdout("123\n", "1\n") assert test.match_stdout("123\n", "111\n") @@ -1289,11 +1493,11 @@ def match_length(lines, matches): assert test.match_stdout("123\n123\n", "111\n111\n") lines = ["123\n", "123\n"] regexes = [r"1\n", r"1\n"] - assert test.match_stdout(lines, regexes) # equal numbers of lines + assert test.match_stdout(lines, regexes) # equal numbers of lines def test_match_stdout_TestCmd_function(self) -> None: """Test match_stdout() using a TestCmd function""" - test = TestCmd.TestCmd(match_stdout = TestCmd.match_exact) + test = TestCmd.TestCmd(match_stdout=TestCmd.match_exact) assert not test.match_stdout("abcde\n", "a.*e\n") assert test.match_stdout("abcde\n", "abcde\n") assert not test.match_stdout("12345\nabcde\n", "1\\d+5\na.*e\n") @@ -1328,117 +1532,207 @@ def test_match_stdout_string(self) -> None: assert test.match_stdout(lines, lines) - class no_result_TestCase(TestCmdTestCase): def test_no_result(self) -> None: """Test no_result()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run', + """\ +import sys + sys.stdout.write("run: STDOUT\\n") sys.stderr.write("run: STDERR\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -TestCmd.no_result(condition = 1) -""", status = 2, stderr = "NO RESULT for test at line 4 of \n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +TestCmd.no_result(condition=1) +""", + status=2, + stderr="NO RESULT for test at line 5 of \n", + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') -test.run() -test.no_result(condition = (test.status == 0)) -""", status = 2, stderr = f"NO RESULT for test of {run_env.workpath('run')}\n\tat line 6 of \n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path -import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', description = 'xyzzy', workdir = '') +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') test.run() -test.no_result(condition = (test.status == 0)) -""", status = 2, stderr = f"NO RESULT for test of {run_env.workpath('run')} [xyzzy]\n\tat line 6 of \n") +test.no_result(condition=(test.status == 0)) +""", + status=2, + stderr=( + f"NO RESULT for test of {run_env.workpath('run')}\n" + "\tat line 7 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys +import TestCmd - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd( + program='run', interpreter='python', description='xyzzy', workdir='' +) +test.run() +test.no_result(condition=(test.status == 0)) +""", + status=2, + stderr=( + f"NO RESULT for test of {run_env.workpath('run')} [xyzzy]\n" + "\tat line 9 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') test.run() + def xxx(): sys.stderr.write("printed on no result\\n") -test.no_result(condition = (test.status == 0), function = xxx) -""", status = 2, stderr = f"printed on no result\nNO RESULT for test of {run_env.workpath('run')}\n\tat line 8 of \n") - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path +test.no_result(condition=(test.status == 0), function=xxx) +""", + status=2, + stderr=( + "printed on no result\n" + f"NO RESULT for test of {run_env.workpath('run')}\n" + "\tat line 11 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def test1(self): self.run() - self.no_result(condition = (self.status == 0)) + self.no_result(condition=(self.status == 0)) + def test2(self): test1(self) -test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')) -""", status = 2, stderr = f"NO RESULT for test of {run_env.workpath('run')}\n\tat line 6 of (test1)\n\tfrom line 8 of (test2)\n\tfrom line 9 of \n") - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path +test2(TestCmd.TestCmd(program='run', interpreter='python', workdir='')) +""", + status=2, + stderr=( + f"NO RESULT for test of {run_env.workpath('run')}\n" + "\tat line 8 of (test1)\n" + "\tfrom line 11 of (test2)\n" + "\tfrom line 13 of \n" + ), + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path + def test1(self): self.run() - self.no_result(condition = (self.status == 0), skip = 1) + self.no_result(condition=(self.status == 0), skip=1) + def test2(self): test1(self) -test2(TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '')) -""", status = 2, stderr = f"NO RESULT for test of {run_env.workpath('run')}\n\tat line 8 of (test2)\n\tfrom line 9 of \n") +test2(TestCmd.TestCmd(program='run', interpreter='python', workdir='')) +""", + status=2, + stderr=( + f"NO RESULT for test of {run_env.workpath('run')}\n" + "\tat line 11 of (test2)\n" + "\tfrom line 13 of \n" + ), + ) class pass_test_TestCase(TestCmdTestCase): def test_pass_test(self) -> None: """Test pass_test()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run', + """\ +import sys + sys.stdout.write("run: STDOUT\\n") sys.stderr.write("run: STDERR\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd -TestCmd.pass_test(condition = 1) -""", stderr = "PASSED\n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +TestCmd.pass_test(condition=1) +""", + stderr="PASSED\n", + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') -test.run() -test.pass_test(condition = (test.status == 0)) -""", stderr = "PASSED\n") - self.popen_python(f"""import sys sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') +test.run() +test.pass_test(condition=(test.status == 0)) +""", + stderr="PASSED\n", + ) + + self.popen_python( + indata=f"""\ +import sys import TestCmd -test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') + +sys.path = [r'{self.orig_cwd}'] + sys.path +test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') test.run() + def brag(): sys.stderr.write("printed on success\\n") -test.pass_test(condition = (test.status == 0), function = brag) -""", stderr = "printed on success\nPASSED\n") - # TODO(sgk): SHOULD ALSO TEST FAILURE CONDITIONS +test.pass_test(condition=(test.status == 0), function=brag) +""", + stderr="printed on success\nPASSED\n", + ) + # TODO(sgk): SHOULD ALSO TEST FAILURE CONDITIONS class preserve_TestCase(TestCmdTestCase): def test_preserve(self) -> None: """Test preserve()""" - def cleanup_test(test, cond=None, stdout: str="") -> None: + + def cleanup_test(test, cond=None, stdout: str = "") -> None: save = sys.stdout with closing(StringIO()) as io: sys.stdout = io @@ -1456,12 +1750,14 @@ def cleanup_test(test, cond=None, stdout: str="") -> None: wdir = test.workdir try: test.write('file1', "Test file #1\n") - #test.cleanup() - cleanup_test(test, ) + # test.cleanup() + cleanup_test( + test, + ) assert not os.path.exists(wdir) finally: if os.path.exists(wdir): - shutil.rmtree(wdir, ignore_errors=1) + shutil.rmtree(wdir, ignore_errors=True) test._dirlist.remove(wdir) test = TestCmd.TestCmd(workdir='') @@ -1475,10 +1771,10 @@ def cleanup_test(test, cond=None, stdout: str="") -> None: assert not os.path.exists(wdir) finally: if os.path.exists(wdir): - shutil.rmtree(wdir, ignore_errors = 1) + shutil.rmtree(wdir, ignore_errors=True) test._dirlist.remove(wdir) - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir try: test.write('file3', "Test file #3\n") @@ -1489,10 +1785,10 @@ def cleanup_test(test, cond=None, stdout: str="") -> None: assert not os.path.exists(wdir) finally: if os.path.exists(wdir): - shutil.rmtree(wdir, ignore_errors = 1) + shutil.rmtree(wdir, ignore_errors=True) test._dirlist.remove(wdir) - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir try: test.write('file4', "Test file #4\n") @@ -1505,10 +1801,10 @@ def cleanup_test(test, cond=None, stdout: str="") -> None: assert not os.path.exists(wdir) finally: if os.path.exists(wdir): - shutil.rmtree(wdir, ignore_errors = 1) + shutil.rmtree(wdir, ignore_errors=True) test._dirlist.remove(wdir) - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wdir = test.workdir try: test.preserve() @@ -1520,27 +1816,27 @@ def cleanup_test(test, cond=None, stdout: str="") -> None: assert os.path.isdir(wdir) finally: if os.path.exists(wdir): - shutil.rmtree(wdir, ignore_errors = 1) + shutil.rmtree(wdir, ignore_errors=True) test._dirlist.remove(wdir) - class program_TestCase(TestCmdTestCase): def test_program(self) -> None: """Test program()""" test = TestCmd.TestCmd() assert test.program is None, 'initialized program?' - test = TestCmd.TestCmd(program = 'test') - assert test.program == os.path.join(os.getcwd(), 'test'), 'uninitialized program' + test = TestCmd.TestCmd(program='test') + assert test.program == os.path.join( + os.getcwd(), 'test' + ), 'uninitialized program' test.program_set('foo') assert test.program == os.path.join(os.getcwd(), 'foo'), 'did not set program' - class read_TestCase(TestCmdTestCase): def test_read(self): """Test read()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_file2 = os.path.join(test.workdir, 'file2') wdir_foo_file3 = os.path.join(test.workdir, 'foo', 'file3') @@ -1566,7 +1862,7 @@ def test_read(self): raise try: - test.read(test.workpath('file_x'), mode = 'w') + test.read(test.workpath('file_x'), mode='w') except ValueError: # expect "mode must begin with 'r' pass except: @@ -1574,30 +1870,33 @@ def test_read(self): def _file_matches(file, contents, expected) -> None: contents = to_str(contents) - assert contents == expected, \ - "Expected contents of " + str(file) + "==========\n" + \ - expected + \ - "Actual contents of " + str(file) + "============\n" + \ - contents + assert contents == expected, ( + "Expected contents of " + + str(file) + + "==========\n" + + expected + + "Actual contents of " + + str(file) + + "============\n" + + contents + ) _file_matches(wdir_file1, test.read('file1'), "") _file_matches(wdir_file2, test.read('file2'), "Test\nfile\n#2.\n") - _file_matches(wdir_foo_file3, test.read(['foo', 'file3']), - "Test\nfile\n#3.\n") - _file_matches(wdir_foo_file3, - test.read(UserList(['foo', 'file3'])), - "Test\nfile\n#3.\n") - _file_matches(wdir_file4, test.read('file4', mode = 'r'), - "Test\nfile\n#4.\n") - _file_matches(wdir_file5, test.read('file5', mode = 'rb'), - "Test\r\nfile\r\n#5.\r\n") - + _file_matches(wdir_foo_file3, test.read(['foo', 'file3']), "Test\nfile\n#3.\n") + _file_matches( + wdir_foo_file3, test.read(UserList(['foo', 'file3'])), "Test\nfile\n#3.\n" + ) + _file_matches(wdir_file4, test.read('file4', mode='r'), "Test\nfile\n#4.\n") + _file_matches( + wdir_file5, test.read('file5', mode='rb'), "Test\r\nfile\r\n#5.\r\n" + ) class rmdir_TestCase(TestCmdTestCase): def test_rmdir(self): """Test rmdir()""" - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') try: test.rmdir(['no', 'such', 'dir']) @@ -1606,9 +1905,7 @@ def test_rmdir(self): else: raise Exception("did not catch expected FileNotFoundError") - test.subdir(['sub'], - ['sub', 'dir'], - ['sub', 'dir', 'one']) + test.subdir(['sub'], ['sub', 'dir'], ['sub', 'dir', 'one']) s = test.workpath('sub') s_d = test.workpath('sub', 'dir') @@ -1656,58 +1953,64 @@ def test_run(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) test.run() - self.run_match(test.stdout(), t.script, "STDOUT", t.workdir, - repr([])) - self.run_match(test.stderr(), t.script, "STDERR", t.workdir, - repr([])) - - test.run(arguments = 'arg1 arg2 arg3') - self.run_match(test.stdout(), t.script, "STDOUT", t.workdir, - repr(['arg1', 'arg2', 'arg3'])) - self.run_match(test.stderr(), t.script, "STDERR", t.workdir, - repr(['arg1', 'arg2', 'arg3'])) - - test.run(program = t.scriptx, arguments = 'foo') - self.run_match(test.stdout(), t.scriptx, "STDOUT", t.workdir, - repr(['foo'])) - self.run_match(test.stderr(), t.scriptx, "STDERR", t.workdir, - repr(['foo'])) - - test.run(chdir = os.curdir, arguments = 'x y z') - self.run_match(test.stdout(), t.script, "STDOUT", test.workdir, - repr(['x', 'y', 'z'])) - self.run_match(test.stderr(), t.script, "STDERR", test.workdir, - repr(['x', 'y', 'z'])) - - test.run(chdir = 'script_subdir') + self.run_match(test.stdout(), t.script, "STDOUT", t.workdir, repr([])) + self.run_match(test.stderr(), t.script, "STDERR", t.workdir, repr([])) + + test.run(arguments='arg1 arg2 arg3') + self.run_match( + test.stdout(), + t.script, + "STDOUT", + t.workdir, + repr(['arg1', 'arg2', 'arg3']), + ) + self.run_match( + test.stderr(), + t.script, + "STDERR", + t.workdir, + repr(['arg1', 'arg2', 'arg3']), + ) + + test.run(program=t.scriptx, arguments='foo') + self.run_match(test.stdout(), t.scriptx, "STDOUT", t.workdir, repr(['foo'])) + self.run_match(test.stderr(), t.scriptx, "STDERR", t.workdir, repr(['foo'])) + + test.run(chdir=os.curdir, arguments='x y z') + self.run_match( + test.stdout(), t.script, "STDOUT", test.workdir, repr(['x', 'y', 'z']) + ) + self.run_match( + test.stderr(), t.script, "STDERR", test.workdir, repr(['x', 'y', 'z']) + ) + + test.run(chdir='script_subdir') script_subdir = test.workpath('script_subdir') - self.run_match(test.stdout(), t.script, "STDOUT", script_subdir, - repr([])) - self.run_match(test.stderr(), t.script, "STDERR", script_subdir, - repr([])) + self.run_match(test.stdout(), t.script, "STDOUT", script_subdir, repr([])) + self.run_match(test.stderr(), t.script, "STDERR", script_subdir, repr([])) - test.run(program = t.script1, interpreter = ['python', '-x']) - self.run_match(test.stdout(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(test.stderr(), t.script1, "STDERR", t.workdir, - repr([])) + test.run(program=t.script1, interpreter=['python', '-x']) + self.run_match(test.stdout(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(test.stderr(), t.script1, "STDERR", t.workdir, repr([])) try: - test.run(chdir = 'no_subdir') + test.run(chdir='no_subdir') except OSError: pass - test.run(program = 'no_script', interpreter = 'python') + test.run(program='no_script', interpreter='python') assert test.status is not None, test.status try: - test.run(program = 'no_script', interpreter = 'no_interpreter') + test.run(program='no_script', interpreter='no_interpreter') except OSError: # Python versions that use subprocess throw an OSError # exception when they try to execute something that @@ -1719,61 +2022,59 @@ def test_run(self) -> None: # a non-zero exit status. assert test.status is not None, test.status - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '', - subdir = 't.scriptx_subdir') + testx = TestCmd.TestCmd( + program=t.scriptx, workdir='', subdir='t.scriptx_subdir' + ) testx.run() - self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.workdir, - repr([])) - self.run_match(testx.stderr(), t.scriptx, "STDERR", t.workdir, - repr([])) - - testx.run(arguments = 'foo bar') - self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.workdir, - repr(['foo', 'bar'])) - self.run_match(testx.stderr(), t.scriptx, "STDERR", t.workdir, - repr(['foo', 'bar'])) - - testx.run(program = t.script, interpreter = 'python', arguments = 'bar') - self.run_match(testx.stdout(), t.script, "STDOUT", t.workdir, - repr(['bar'])) - self.run_match(testx.stderr(), t.script, "STDERR", t.workdir, - repr(['bar'])) - - testx.run(chdir = os.curdir, arguments = 'baz') - self.run_match(testx.stdout(), t.scriptx, "STDOUT", testx.workdir, - repr(['baz'])) - self.run_match(testx.stderr(), t.scriptx, "STDERR", testx.workdir, - repr(['baz'])) - - testx.run(chdir = 't.scriptx_subdir') + self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.workdir, repr([])) + self.run_match(testx.stderr(), t.scriptx, "STDERR", t.workdir, repr([])) + + testx.run(arguments='foo bar') + self.run_match( + testx.stdout(), t.scriptx, "STDOUT", t.workdir, repr(['foo', 'bar']) + ) + self.run_match( + testx.stderr(), t.scriptx, "STDERR", t.workdir, repr(['foo', 'bar']) + ) + + testx.run(program=t.script, interpreter='python', arguments='bar') + self.run_match(testx.stdout(), t.script, "STDOUT", t.workdir, repr(['bar'])) + self.run_match(testx.stderr(), t.script, "STDERR", t.workdir, repr(['bar'])) + + testx.run(chdir=os.curdir, arguments='baz') + self.run_match( + testx.stdout(), t.scriptx, "STDOUT", testx.workdir, repr(['baz']) + ) + self.run_match( + testx.stderr(), t.scriptx, "STDERR", testx.workdir, repr(['baz']) + ) + + testx.run(chdir='t.scriptx_subdir') t.scriptx_subdir = testx.workpath('t.scriptx_subdir') - self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.scriptx_subdir, - repr([])) - self.run_match(testx.stderr(), t.scriptx, "STDERR", t.scriptx_subdir, - repr([])) + self.run_match( + testx.stdout(), t.scriptx, "STDOUT", t.scriptx_subdir, repr([]) + ) + self.run_match( + testx.stderr(), t.scriptx, "STDERR", t.scriptx_subdir, repr([]) + ) - testx.run(program = t.script1, interpreter = ('python', '-x')) - self.run_match(testx.stdout(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(testx.stderr(), t.script1, "STDERR", t.workdir, - repr([])) + testx.run(program=t.script1, interpreter=('python', '-x')) + self.run_match(testx.stdout(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(testx.stderr(), t.script1, "STDERR", t.workdir, repr([])) s = os.path.join('.', t.scriptx) - testx.run(program = [s]) - self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.workdir, - repr([])) - self.run_match(testx.stderr(), t.scriptx, "STDERR", t.workdir, - repr([])) + testx.run(program=[s]) + self.run_match(testx.stdout(), t.scriptx, "STDOUT", t.workdir, repr([])) + self.run_match(testx.stderr(), t.scriptx, "STDERR", t.workdir, repr([])) try: - testx.run(chdir = 'no_subdir') + testx.run(chdir='no_subdir') except OSError: pass try: - testx.run(program = 'no_program') + testx.run(program='no_program') except OSError: # Python versions that use subprocess throw an OSError # exception when they try to execute something that @@ -1785,15 +2086,13 @@ def test_run(self) -> None: # a non-zero exit status. assert test.status is not None - test1 = TestCmd.TestCmd(program = t.script1, - interpreter = ['python', '-x'], - workdir = '') + test1 = TestCmd.TestCmd( + program=t.script1, interpreter=['python', '-x'], workdir='' + ) test1.run() - self.run_match(test1.stdout(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(test1.stderr(), t.script1, "STDERR", t.workdir, - repr([])) + self.run_match(test1.stdout(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(test1.stderr(), t.script1, "STDERR", t.workdir, repr([])) finally: os.chdir(t.orig_cwd) @@ -1811,16 +2110,16 @@ def start(self, additional_argument=None, **kw): return TestCmd.TestCmd.start(self, **kw) try: - test = MyTestCmdSubclass(program = t.script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = MyTestCmdSubclass( + program=t.script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) test.run() - self.run_match(test.stdout(), t.script, "STDOUT", t.workdir, - repr([])) - self.run_match(test.stderr(), t.script, "STDERR", t.workdir, - repr([])) + self.run_match(test.stdout(), t.script, "STDOUT", t.workdir, repr([])) + self.run_match(test.stderr(), t.script, "STDERR", t.workdir, repr([])) finally: os.chdir(t.orig_cwd) @@ -1838,26 +2137,23 @@ def test_run_verbose(self) -> None: try: # Test calling TestCmd() with an explicit verbose = 1. - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - verbose = 1) + test = TestCmd.TestCmd( + program=t.script, interpreter='python', workdir='', verbose=1 + ) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) o = sys.stdout.getvalue() assert o == '', o e = sys.stderr.getvalue() - expect = f'python "{t.script_path}" "arg1 arg2\"\n' - assert expect == e, (expect, e) + expect = f'python "{t.script_path}" "arg1 arg2"\n' + self.assertEqual(expect, e) - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '', - verbose = 1) + testx = TestCmd.TestCmd(program=t.scriptx, workdir='', verbose=1) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - testx.run(arguments = ['arg1 arg2']) - expect = f'"{t.scriptx_path}" "arg1 arg2\"\n' + testx.run(arguments=['arg1 arg2']) + expect = f'"{t.scriptx_path}" "arg1 arg2"\n' o = sys.stdout.getvalue() assert o == '', o e = sys.stderr.getvalue() @@ -1885,54 +2181,58 @@ def test_run_verbose(self) -> None: %s============ END STDERR """ - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - verbose = 2) + test = TestCmd.TestCmd( + program=t.script, interpreter='python', workdir='', verbose=2 + ) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) line_fmt = "script: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) stderr_line = line_fmt % ('STDERR', t.sub_dir) - expect = outerr_fmt % (len(stdout_line), stdout_line, - len(stderr_line), stderr_line) + expect = outerr_fmt % ( + len(stdout_line), + stdout_line, + len(stderr_line), + stderr_line, + ) o = sys.stdout.getvalue() assert expect == o, (expect, o) - expect = f'python "{t.script_path}" "arg1 arg2\"\n' + expect = f'python "{t.script_path}" "arg1 arg2"\n' e = sys.stderr.getvalue() assert e == expect, (e, expect) - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '', - verbose = 2) + testx = TestCmd.TestCmd(program=t.scriptx, workdir='', verbose=2) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - testx.run(arguments = ['arg1 arg2']) + testx.run(arguments=['arg1 arg2']) line_fmt = "scriptx.bat: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) stderr_line = line_fmt % ('STDERR', t.sub_dir) - expect = outerr_fmt % (len(stdout_line), stdout_line, - len(stderr_line), stderr_line) + expect = outerr_fmt % ( + len(stdout_line), + stdout_line, + len(stderr_line), + stderr_line, + ) o = sys.stdout.getvalue() assert expect == o, (expect, o) - expect = f'"{t.scriptx_path}" "arg1 arg2\"\n' + expect = f'"{t.scriptx_path}" "arg1 arg2"\n' e = sys.stderr.getvalue() assert e == expect, (e, expect) # Test calling TestCmd() with an explicit verbose = 3. - test = TestCmd.TestCmd(program = t.scriptout, - interpreter = 'python', - workdir = '', - verbose = 2) + test = TestCmd.TestCmd( + program=t.scriptout, interpreter='python', workdir='', verbose=2 + ) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) line_fmt = "scriptout: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) @@ -1941,66 +2241,69 @@ def test_run_verbose(self) -> None: assert expect == o, (expect, o) e = sys.stderr.getvalue() - expect = f'python "{t.scriptout_path}" "arg1 arg2\"\n' + expect = f'python "{t.scriptout_path}" "arg1 arg2"\n' assert e == expect, (e, expect) - test = TestCmd.TestCmd(program = t.scriptout, - interpreter = 'python', - workdir = '', - verbose = 3) + test = TestCmd.TestCmd( + program=t.scriptout, interpreter='python', workdir='', verbose=3 + ) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) line_fmt = "scriptout: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) - expect = outerr_fmt % (len(stdout_line), stdout_line, - '0', '') + expect = outerr_fmt % (len(stdout_line), stdout_line, '0', '') o = sys.stdout.getvalue() assert expect == o, (expect, o) e = sys.stderr.getvalue() - expect = f'python "{t.scriptout_path}" "arg1 arg2\"\n' + expect = f'python "{t.scriptout_path}" "arg1 arg2"\n' assert e == expect, (e, expect) # Test letting TestCmd() pick up verbose = 2 from the environment. os.environ['TESTCMD_VERBOSE'] = '2' - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '') + test = TestCmd.TestCmd(program=t.script, interpreter='python', workdir='') with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) line_fmt = "script: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) stderr_line = line_fmt % ('STDERR', t.sub_dir) - expect = outerr_fmt % (len(stdout_line), stdout_line, - len(stderr_line), stderr_line) + expect = outerr_fmt % ( + len(stdout_line), + stdout_line, + len(stderr_line), + stderr_line, + ) o = sys.stdout.getvalue() assert expect == o, (expect, o) - expect = f'python "{t.script_path}" "arg1 arg2\"\n' + expect = f'python "{t.script_path}" "arg1 arg2"\n' e = sys.stderr.getvalue() assert e == expect, (e, expect) - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '') + testx = TestCmd.TestCmd(program=t.scriptx, workdir='') with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - testx.run(arguments = ['arg1 arg2']) + testx.run(arguments=['arg1 arg2']) line_fmt = "scriptx.bat: %s: %s: ['arg1 arg2']\n" stdout_line = line_fmt % ('STDOUT', t.sub_dir) stderr_line = line_fmt % ('STDERR', t.sub_dir) - expect = outerr_fmt % (len(stdout_line), stdout_line, - len(stderr_line), stderr_line) + expect = outerr_fmt % ( + len(stdout_line), + stdout_line, + len(stderr_line), + stderr_line, + ) o = sys.stdout.getvalue() assert expect == o, (expect, o) - expect = f'"{t.scriptx_path}" "arg1 arg2\"\n' + expect = f'"{t.scriptx_path}" "arg1 arg2"\n' e = sys.stderr.getvalue() assert e == expect, (e, expect) @@ -2008,30 +2311,27 @@ def test_run_verbose(self) -> None: os.environ['TESTCMD_VERBOSE'] = '1' - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - verbose = 1) + test = TestCmd.TestCmd( + program=t.script, interpreter='python', workdir='', verbose=1 + ) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - test.run(arguments = ['arg1 arg2']) + test.run(arguments=['arg1 arg2']) o = sys.stdout.getvalue() assert o == '', o e = sys.stderr.getvalue() - expect = f'python "{t.script_path}" "arg1 arg2\"\n' + expect = f'python "{t.script_path}" "arg1 arg2"\n' assert expect == e, (expect, e) - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '', - verbose = 1) + testx = TestCmd.TestCmd(program=t.scriptx, workdir='', verbose=1) with closing(StringIO()) as sys.stdout, closing(StringIO()) as sys.stderr: - testx.run(arguments = ['arg1 arg2']) - expect = f'"{t.scriptx_path}" "arg1 arg2\"\n' + testx.run(arguments=['arg1 arg2']) o = sys.stdout.getvalue() assert o == '', o e = sys.stderr.getvalue() - assert expect == e, (expect, e) + expect = f'"{t.scriptx_path}" "arg1 arg2"\n' + self.assertEqual(expect, e) finally: sys.stdout = save_stdout @@ -2040,25 +2340,31 @@ def test_run_verbose(self) -> None: os.environ['TESTCMD_VERBOSE'] = '' - class set_diff_function_TestCase(TestCmdTestCase): def test_set_diff_function(self) -> None: """Test set_diff_function()""" - self.popen_python(fr"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd() -test.diff("a\n", "a\n") +test.diff("a\\n", "a\\n") test.set_diff_function('diff_re') -test.diff(".\n", "a\n") +test.diff(".\\n", "a\\n") sys.exit(0) -""") +""", + ) def test_set_diff_function_stdout(self) -> None: """Test set_diff_function(): stdout""" - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + self.popen_python( + indata=f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd() print("diff:") test.diff("a\\n", "a\\n") @@ -2071,7 +2377,7 @@ def test_set_diff_function_stdout(self) -> None: test.diff_stdout(".\\n", "a\\n") sys.exit(0) """, - stdout="""\ + stdout="""\ diff: diff_stdout: diff: @@ -2080,13 +2386,17 @@ def test_set_diff_function_stdout(self) -> None: --- > a diff_stdout: -""") +""", + ) def test_set_diff_function_stderr(self) -> None: - """Test set_diff_function(): stderr """ - self.popen_python(f"""import sys -sys.path = [r'{self.orig_cwd}'] + sys.path + """Test set_diff_function(): stderr""" + self.popen_python( + f"""\ +import sys import TestCmd + +sys.path = [r'{self.orig_cwd}'] + sys.path test = TestCmd.TestCmd() print("diff:") test.diff("a\\n", "a\\n") @@ -2099,7 +2409,7 @@ def test_set_diff_function_stderr(self) -> None: test.diff_stderr(".\\n", "a\\n") sys.exit(0) """, - stdout="""\ + stdout="""\ diff: diff_stderr: diff: @@ -2108,8 +2418,8 @@ def test_set_diff_function_stderr(self) -> None: --- > a diff_stderr: -""") - +""", + ) class set_match_function_TestCase(TestCmdTestCase): @@ -2125,7 +2435,7 @@ def test_set_match_function(self) -> None: assert test.match("abcde\n", "abcde\n") def test_set_match_function_stdout(self) -> None: - """Test set_match_function(): stdout """ + """Test set_match_function(): stdout""" test = TestCmd.TestCmd() assert test.match("abcde\n", "a.*e\n") assert test.match("abcde\n", "abcde\n") @@ -2140,7 +2450,7 @@ def test_set_match_function_stdout(self) -> None: assert test.match_stdout("abcde\n", "abcde\n") def test_set_match_function_stderr(self) -> None: - """Test set_match_function(): stderr """ + """Test set_match_function(): stderr""" test = TestCmd.TestCmd() assert test.match("abcde\n", "a.*e\n") assert test.match("abcde\n", "abcde\n") @@ -2155,7 +2465,6 @@ def test_set_match_function_stderr(self) -> None: assert test.match_stderr("abcde\n", "abcde\n") - class sleep_TestCase(TestCmdTestCase): def test_sleep(self) -> None: """Test sleep()""" @@ -2165,36 +2474,49 @@ def test_sleep(self) -> None: test.sleep() end = time.perf_counter() diff = end - start - assert diff > 0.9, f"only slept {diff:f} seconds (start {start:f}, end {end:f}), not default" + assert ( + diff > 0.9 + ), f"only slept {diff:f} seconds (start {start:f}, end {end:f}), not default" start = time.perf_counter() test.sleep(3) end = time.perf_counter() diff = end - start - assert diff > 2.9, f"only slept {diff:f} seconds (start {start:f}, end {end:f}), not 3" - + assert ( + diff > 2.9 + ), f"only slept {diff:f} seconds (start {start:f}, end {end:f}), not 3" class stderr_TestCase(TestCmdTestCase): def test_stderr(self): """Test stderr()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run1', """import sys + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run1', + """\ +import sys + sys.stdout.write("run1 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run1 STDOUT second line\\n") sys.stderr.write("run1 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run1 STDERR second line\\n") -""") - run_env.write('run2', """import sys +""", + ) + run_env.write( + 'run2', + """\ +import sys + sys.stdout.write("run2 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run2 STDOUT second line\\n") sys.stderr.write("run2 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run2 STDERR second line\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - test = TestCmd.TestCmd(interpreter = 'python', workdir = '') + test = TestCmd.TestCmd(interpreter='python', workdir='') try: output = test.stderr() except IndexError: @@ -2203,81 +2525,88 @@ def test_stderr(self): if output is not None: raise IndexError(f"got unexpected output:\n{output}") test.program_set('run1') - test.run(arguments = 'foo bar') + test.run(arguments='foo bar') test.program_set('run2') - test.run(arguments = 'snafu') + test.run(arguments='snafu') # XXX SHOULD TEST ABSOLUTE NUMBER AS WELL output = test.stderr() assert output == "run2 STDERR ['snafu']\nrun2 STDERR second line\n", output - output = test.stderr(run = -1) + output = test.stderr(run=-1) assert output == "run1 STDERR ['foo', 'bar']\nrun1 STDERR second line\n", output - class command_args_TestCase(TestCmdTestCase): def test_command_args(self) -> None: """Test command_args()""" - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') r = test.command_args('prog') expect = [run_env.workpath('prog')] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args(test.workpath('new_prog')) expect = [test.workpath('new_prog')] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args('prog', 'python') expect = ['python', run_env.workpath('prog')] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args('prog', 'python', 'arg1 arg2') expect = ['python', run_env.workpath('prog'), 'arg1', 'arg2'] - assert r == expect, (expect, r) + self.assertEqual(expect, r) + + r = test.command_args('prog', 'python', 'arg1 arg2=value') + expect = ['python', run_env.workpath('prog'), 'arg1', 'arg2=value'] + with self.subTest(): + self.assertEqual(expect, r) + + r = test.command_args('prog', 'python', ['arg1', 'arg2=with space']) + expect = ['python', run_env.workpath('prog'), 'arg1', 'arg2=with space'] + with self.subTest(): + self.assertEqual(expect, r) test.program_set('default_prog') default_prog = run_env.workpath('default_prog') r = test.command_args() expect = [default_prog] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args(interpreter='PYTHON') expect = ['PYTHON', default_prog] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args(interpreter='PYTHON', arguments='arg3 arg4') expect = ['PYTHON', default_prog, 'arg3', 'arg4'] - assert r == expect, (expect, r) + self.assertEqual(expect, r) # Test arguments = dict - r = test.command_args(interpreter='PYTHON', arguments={'VAR1':'1'}) + r = test.command_args(interpreter='PYTHON', arguments={'VAR1': '1'}) expect = ['PYTHON', default_prog, 'VAR1=1'] - assert r == expect, (expect, r) - + self.assertEqual(expect, r) test.interpreter_set('default_python') r = test.command_args() expect = ['default_python', default_prog] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args(arguments='arg5 arg6') expect = ['default_python', default_prog, 'arg5', 'arg6'] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args('new_prog_1') expect = [run_env.workpath('new_prog_1')] - assert r == expect, (expect, r) + self.assertEqual(expect, r) r = test.command_args(program='new_prog_2') expect = [run_env.workpath('new_prog_2')] - assert r == expect, (expect, r) - + self.assertEqual(expect, r) class start_TestCase(TestCmdTestCase): @@ -2286,21 +2615,23 @@ def setup_run_scripts(self): t.recv_script = 'script_recv' t.recv_script_path = t.run_env.workpath(t.sub_dir, t.recv_script) t.recv_out_path = t.run_env.workpath('script_recv.out') - text = f"""import os + text = f"""\ +import os import sys class Unbuffered: def __init__(self, file): self.file = file + def write(self, arg): self.file.write(arg) self.file.flush() + def __getattr__(self, attr): return getattr(self.file, attr) sys.stdout = Unbuffered(sys.stdout) sys.stderr = Unbuffered(sys.stderr) - sys.stdout.write('script_recv: STDOUT\\n') sys.stderr.write('script_recv: STDERR\\n') with open(r'{t.recv_out_path}', 'w') as logfp: @@ -2331,40 +2662,50 @@ def test_start(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) p = test.start() - self.run_match(p.stdout.read(), t.script, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.script, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.script, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.script, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) p = test.start(arguments='arg1 arg2 arg3') - self.run_match(p.stdout.read(), t.script, "STDOUT", t.workdir, - repr(['arg1', 'arg2', 'arg3'])) - self.run_match(p.stderr.read(), t.script, "STDERR", t.workdir, - repr(['arg1', 'arg2', 'arg3'])) + self.run_match( + p.stdout.read(), + t.script, + "STDOUT", + t.workdir, + repr(['arg1', 'arg2', 'arg3']), + ) + self.run_match( + p.stderr.read(), + t.script, + "STDERR", + t.workdir, + repr(['arg1', 'arg2', 'arg3']), + ) p.wait() self._cleanup(p) p = test.start(program=t.scriptx, arguments='foo') - self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, - repr(['foo'])) - self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, - repr(['foo'])) + self.run_match( + p.stdout.read(), t.scriptx, "STDOUT", t.workdir, repr(['foo']) + ) + self.run_match( + p.stderr.read(), t.scriptx, "STDERR", t.workdir, repr(['foo']) + ) p.wait() self._cleanup(p) p = test.start(program=t.script1, interpreter=['python', '-x']) - self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) @@ -2388,48 +2729,46 @@ def test_start(self) -> None: # a non-zero exit status. assert status is not None, status - testx = TestCmd.TestCmd(program = t.scriptx, - workdir = '', - subdir = 't.scriptx_subdir') + testx = TestCmd.TestCmd( + program=t.scriptx, workdir='', subdir='t.scriptx_subdir' + ) p = testx.start() - self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) p = testx.start(arguments='foo bar') - self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, - repr(['foo', 'bar'])) - self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, - repr(['foo', 'bar'])) + self.run_match( + p.stdout.read(), t.scriptx, "STDOUT", t.workdir, repr(['foo', 'bar']) + ) + self.run_match( + p.stderr.read(), t.scriptx, "STDERR", t.workdir, repr(['foo', 'bar']) + ) p.wait() self._cleanup(p) p = testx.start(program=t.script, interpreter='python', arguments='bar') - self.run_match(p.stdout.read(), t.script, "STDOUT", t.workdir, - repr(['bar'])) - self.run_match(p.stderr.read(), t.script, "STDERR", t.workdir, - repr(['bar'])) + self.run_match( + p.stdout.read(), t.script, "STDOUT", t.workdir, repr(['bar']) + ) + self.run_match( + p.stderr.read(), t.script, "STDERR", t.workdir, repr(['bar']) + ) p.wait() self._cleanup(p) p = testx.start(program=t.script1, interpreter=('python', '-x')) - self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) s = os.path.join('.', t.scriptx) p = testx.start(program=[s]) - self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.scriptx, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.scriptx, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) @@ -2451,15 +2790,13 @@ def test_start(self) -> None: except OSError: pass - test1 = TestCmd.TestCmd(program = t.script1, - interpreter = ['python', '-x'], - workdir = '') + test1 = TestCmd.TestCmd( + program=t.script1, interpreter=['python', '-x'], workdir='' + ) p = test1.start() - self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, - repr([])) - self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, - repr([])) + self.run_match(p.stdout.read(), t.script1, "STDOUT", t.workdir, repr([])) + self.run_match(p.stderr.read(), t.script1, "STDERR", t.workdir, repr([])) p.wait() self._cleanup(p) @@ -2474,11 +2811,12 @@ def test_finish(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - - test = TestCmd.TestCmd(program = t.recv_script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.recv_script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) test.start(stdin=1) test.finish() @@ -2535,19 +2873,21 @@ def test_recv(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) p = test.start() stdout = p.recv() while stdout == '': import time + time.sleep(1) stdout = p.recv() - self.run_match(stdout, t.script, "STDOUT", t.workdir, - repr([])) + self.run_match(stdout, t.script, "STDOUT", t.workdir, repr([])) p.wait() finally: @@ -2561,23 +2901,23 @@ def test_recv_err(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - - test = TestCmd.TestCmd(program = t.script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) p = test.start() stderr = p.recv_err() while stderr == '': import time + time.sleep(1) stderr = p.recv_err() - self.run_match(stderr, t.script, "STDERR", t.workdir, - repr([])) + self.run_match(stderr, t.script, "STDERR", t.workdir, repr([])) p.wait() - finally: os.chdir(t.orig_cwd) @@ -2589,11 +2929,12 @@ def test_send(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - - test = TestCmd.TestCmd(program = t.recv_script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.recv_script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) p = test.start(stdin=1) input = 'stdin.write() input to the receive script\n' @@ -2603,7 +2944,9 @@ def test_send(self) -> None: with open(t.recv_out_path, 'r') as f: result = to_str(f.read()) expect = f"script_recv: {input}" - assert result == expect, f"Result:[{result}] should match\nExpected:[{expect}]" + assert ( + result == expect + ), f"Result:[{result}] should match\nExpected:[{expect}]" # TODO: Python 3.6+ ResourceWarning: unclosed file <_io.BufferedReader name=9> p = test.start(stdin=1) @@ -2628,11 +2971,12 @@ def __FLAKY__test_send_recv(self) -> None: # Everything before this prepared our "source directory." # Now do the real test. try: - - test = TestCmd.TestCmd(program = t.recv_script, - interpreter = 'python', - workdir = '', - subdir = 'script_subdir') + test = TestCmd.TestCmd( + program=t.recv_script, + interpreter='python', + workdir='', + subdir='script_subdir', + ) def do_send_recv(p, input): send, stdout, stderr = p.send_recv(input) @@ -2680,67 +3024,78 @@ def do_send_recv(p, input): os.chdir(t.orig_cwd) - class stdin_TestCase(TestCmdTestCase): def test_stdin(self) -> None: """Test stdin()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run', """\ + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run', + """\ import fileinput + for line in fileinput.input(): print('Y'.join(line[:-1].split('X'))) -""") +""", + ) run_env.write('input', "X on X this X line X\n") os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - test = TestCmd.TestCmd(program = 'run', interpreter = 'python', workdir = '') - test.run(arguments = 'input') + test = TestCmd.TestCmd(program='run', interpreter='python', workdir='') + test.run(arguments='input') assert test.stdout() == "Y on Y this Y line Y\n" - test.run(stdin = "X is X here X tooX\n") + test.run(stdin="X is X here X tooX\n") assert test.stdout() == "Y is Y here Y tooY\n" - test.run(stdin = """X here X + test.run( + stdin="""X here X X there X -""") +""" + ) assert test.stdout() == "Y here Y\nY there Y\n" - test.run(stdin = ["X line X\n", "X another X\n"]) + test.run(stdin=["X line X\n", "X another X\n"]) assert test.stdout() == "Y line Y\nY another Y\n" - class stdout_TestCase(TestCmdTestCase): def test_stdout(self): """Test stdout()""" - run_env = TestCmd.TestCmd(workdir = '') - run_env.write('run1', """\ + run_env = TestCmd.TestCmd(workdir='') + run_env.write( + 'run1', + """\ import sys + sys.stdout.write("run1 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run1 STDOUT second line\\n") sys.stderr.write("run1 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run1 STDERR second line\\n") -""") - run_env.write('run2', """\ +""", + ) + run_env.write( + 'run2', + """\ import sys sys.stdout.write("run2 STDOUT %s\\n" % sys.argv[1:]) sys.stdout.write("run2 STDOUT second line\\n") sys.stderr.write("run2 STDERR %s\\n" % sys.argv[1:]) sys.stderr.write("run2 STDERR second line\\n") -""") +""", + ) os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. - test = TestCmd.TestCmd(interpreter = 'python', workdir = '') + test = TestCmd.TestCmd(interpreter='python', workdir='') output = test.stdout() if output is not None: raise IndexError(f"got unexpected output:\n\t`{output}'\n") test.program_set('run1') - test.run(arguments = 'foo bar') + test.run(arguments='foo bar') test.program_set('run2') - test.run(arguments = 'snafu') + test.run(arguments='snafu') # XXX SHOULD TEST ABSOLUTE NUMBER AS WELL output = test.stdout() assert output == "run2 STDOUT ['snafu']\nrun2 STDOUT second line\n", output - output = test.stdout(run = -1) + output = test.stdout(run=-1) assert output == "run1 STDOUT ['foo', 'bar']\nrun1 STDOUT second line\n", output @@ -2790,7 +3145,7 @@ class symlink_TestCase(TestCmdTestCase): @unittest.skipIf(sys.platform == 'win32', "Skip symlink test on win32") def test_symlink(self) -> None: """Test symlink()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_target1 = os.path.join(test.workdir, 'target1') wdir_foo_file2 = os.path.join(test.workdir, 'foo', 'file2') @@ -2815,7 +3170,6 @@ def test_symlink(self) -> None: assert os.path.exists(wdir_foo_file2) - class tempdir_TestCase(TestCmdTestCase): def setUp(self) -> None: TestCmdTestCase.setUp(self) @@ -2849,6 +3203,7 @@ def test_tempdir(self) -> None: timeout_script = """\ import sys import time + seconds = int(sys.argv[1]) sys.stdout.write('sleeping %s\\n' % seconds) sys.stdout.flush() @@ -2858,6 +3213,7 @@ def test_tempdir(self) -> None: sys.exit(0) """ + class timeout_TestCase(TestCmdTestCase): def test_initialization(self) -> None: """Test initializating a TestCmd with a timeout""" @@ -2903,7 +3259,7 @@ def test_run(self) -> None: assert test.stdout() == 'sleeping 6\n', test.stdout() # This method has been removed - #def test_set_timeout(self): + # def test_set_timeout(self): # """Test set_timeout()""" # test = TestCmd.TestCmd(workdir='', timeout=2) # test.write('sleep.py', timeout_script) @@ -2929,11 +3285,10 @@ def test_run(self) -> None: # assert test.stdout() == 'sleeping 8\n', test.stdout() - class unlink_TestCase(TestCmdTestCase): def test_unlink(self): """Test unlink()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_file2 = os.path.join(test.workdir, 'file2') wdir_foo_file3a = os.path.join(test.workdir, 'foo', 'file3a') @@ -2956,7 +3311,7 @@ def test_unlink(self): try: contents = test.unlink('no_file') - except OSError: # expect "No such file or directory" + except OSError: # expect "No such file or directory" pass except: raise @@ -2985,7 +3340,7 @@ def test_unlink(self): try: try: test.unlink('file5') - except OSError: # expect "Permission denied" + except OSError: # expect "Permission denied" pass except: raise @@ -2997,7 +3352,7 @@ def test_unlink(self): class unlink_files_TestCase(TestCmdTestCase): def test_unlink_files(self): """Test unlink_files()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_file2 = os.path.join(test.workdir, 'file2') wdir_foo_file3a = os.path.join(test.workdir, 'foo', 'file3a') @@ -3033,43 +3388,24 @@ def test_unlink_files(self): with open(wdir_file5, 'w') as f: f.write("") - test.unlink_files('', [ - 'no_file_a', - 'no_file_b', - ]) - - test.unlink_files('', [ - 'file1', - 'file2', - ]) + test.unlink_files('', ['no_file_a', 'no_file_b']) + test.unlink_files('', ['file1', 'file2']) assert not os.path.exists(wdir_file1) assert not os.path.exists(wdir_file2) - test.unlink_files('foo', [ - 'file3a', - 'file3b', - ]) + test.unlink_files('foo', ['file3a', 'file3b']) assert not os.path.exists(wdir_foo_file3a) assert not os.path.exists(wdir_foo_file3b) - test.unlink_files(['foo'], [ - 'file3c', - 'file3d', - ]) + test.unlink_files(['foo'], ['file3c', 'file3d']) assert not os.path.exists(wdir_foo_file3c) assert not os.path.exists(wdir_foo_file3d) - test.unlink_files('', [ - ['foo', 'file4a'], - ['foo', 'file4b'], - ]) + test.unlink_files('', [['foo', 'file4a'], ['foo', 'file4b']]) assert not os.path.exists(wdir_foo_file4a) assert not os.path.exists(wdir_foo_file4b) - test.unlink_files([''], [ - ['foo', 'file4c'], - ['foo', 'file4d'], - ]) + test.unlink_files([''], [['foo', 'file4c'], ['foo', 'file4d']]) assert not os.path.exists(wdir_foo_file4c) assert not os.path.exists(wdir_foo_file4d) @@ -3082,7 +3418,7 @@ def test_unlink_files(self): try: try: test.unlink_files('', ['file5']) - except OSError: # expect "Permission denied" + except OSError: # expect "Permission denied" pass except: raise @@ -3094,7 +3430,7 @@ def test_unlink_files(self): class touch_TestCase(TestCmdTestCase): def test_touch(self) -> None: """Test touch()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'sub') + test = TestCmd.TestCmd(workdir='', subdir='sub') wdir_file1 = os.path.join(test.workdir, 'file1') wdir_sub_file2 = os.path.join(test.workdir, 'sub', 'file2') @@ -3127,49 +3463,47 @@ def test_touch(self) -> None: assert file2_new_time > file2_old_time - class verbose_TestCase(TestCmdTestCase): def test_verbose(self) -> None: """Test verbose()""" test = TestCmd.TestCmd() assert test.verbose == 0, 'verbose already initialized?' - test = TestCmd.TestCmd(verbose = 1) + test = TestCmd.TestCmd(verbose=1) assert test.verbose == 1, 'did not initialize verbose' test.verbose = 2 assert test.verbose == 2, 'did not set verbose' - class workdir_TestCase(TestCmdTestCase): def test_workdir(self): """Test workdir()""" - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') os.chdir(run_env.workdir) # Everything before this prepared our "source directory." # Now do the real test. test = TestCmd.TestCmd() assert test.workdir is None - test = TestCmd.TestCmd(workdir = None) + test = TestCmd.TestCmd(workdir=None) assert test.workdir is None - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') assert test.workdir is not None assert os.path.isdir(test.workdir) - test = TestCmd.TestCmd(workdir = 'dir') + test = TestCmd.TestCmd(workdir='dir') assert test.workdir is not None assert os.path.isdir(test.workdir) no_such_subdir = os.path.join('no', 'such', 'subdir') try: - test = TestCmd.TestCmd(workdir = no_such_subdir) + test = TestCmd.TestCmd(workdir=no_such_subdir) except OSError: # expect "No such file or directory" pass except: raise - test = TestCmd.TestCmd(workdir = 'foo') + test = TestCmd.TestCmd(workdir='foo') workdir_foo = test.workdir assert workdir_foo is not None @@ -3189,7 +3523,6 @@ def test_workdir(self): assert os.path.isdir(workdir_bar) - class workdirs_TestCase(TestCmdTestCase): def test_workdirs(self) -> None: """Test workdirs()""" @@ -3206,14 +3539,13 @@ def test_workdirs(self) -> None: assert not os.path.exists(wdir2) - class workpath_TestCase(TestCmdTestCase): def test_workpath(self) -> None: """Test workpath()""" test = TestCmd.TestCmd() assert test.workdir is None - test = TestCmd.TestCmd(workdir = '') + test = TestCmd.TestCmd(workdir='') wpath = test.workpath('foo', 'bar') assert wpath == os.path.join(test.workdir, 'foo', 'bar') @@ -3222,7 +3554,7 @@ class readable_TestCase(TestCmdTestCase): @unittest.skipIf(sys.platform == 'win32', "Skip permission fiddling on win32") def test_readable(self) -> None: """Test readable()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') test.write('file1', "Test file #1\n") test.write(['foo', 'file2'], "Test file #2\n") os.symlink('no_such_file', test.workpath('dangling_symlink')) @@ -3256,12 +3588,11 @@ def test_readable(self) -> None: test.readable(test.workdir, 1) - class writable_TestCase(TestCmdTestCase): @unittest.skipIf(sys.platform == 'win32', "Skip permission fiddling on win32") def test_writable(self) -> None: """Test writable()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') test.write('file1', "Test file #1\n") test.write(['foo', 'file2'], "Test file #2\n") os.symlink('no_such_file', test.workpath('dangling_symlink')) @@ -3297,18 +3628,18 @@ class executable_TestCase(TestCmdTestCase): @unittest.skipIf(sys.platform == 'win32', "Skip permission fiddling on win32") def test_executable(self) -> None: """Test executable()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') test.write('file1', "Test file #1\n") test.write(['foo', 'file2'], "Test file #2\n") os.symlink('no_such_file', test.workpath('dangling_symlink')) def make_executable(fname) -> None: st = os.stat(fname) - os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0o100)) + os.chmod(fname, stat.S_IMODE(st.st_mode | stat.S_IXUSR)) def make_non_executable(fname) -> None: st = os.stat(fname) - os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0o100)) + os.chmod(fname, stat.S_IMODE(st.st_mode & ~stat.S_IXUSR)) test.executable(test.workdir, 0) # XXX skip these tests if euid == 0? @@ -3345,11 +3676,10 @@ def make_non_executable(fname) -> None: test.executable(test.workdir, 1) - class write_TestCase(TestCmdTestCase): def test_write(self): """Test write()""" - test = TestCmd.TestCmd(workdir = '', subdir = 'foo') + test = TestCmd.TestCmd(workdir='', subdir='foo') test.write('file1', "Test file #1\n") test.write(['foo', 'file2'], "Test file #2\n") try: @@ -3361,21 +3691,23 @@ def test_write(self): test.write(test.workpath('file4'), "Test file #4.\n") test.write(test.workpath('foo', 'file5'), "Test file #5.\n") try: - test.write(test.workpath('bar', 'file6'), "Test file #6 (should not get created)\n") + test.write( + test.workpath('bar', 'file6'), "Test file #6 (should not get created)\n" + ) except IOError: # expect "No such file or directory" pass except: raise try: - test.write('file7', "Test file #8.\n", mode = 'r') - except ValueError: # expect "mode must begin with 'w' + test.write('file7', "Test file #8.\n", mode='r') + except ValueError: # expect "mode must begin with 'w' pass except: raise - test.write('file8', "Test file #8.\n", mode = 'w') - test.write('file9', "Test file #9.\r\n", mode = 'wb') + test.write('file8', "Test file #8.\n", mode='w') + test.write('file9', "Test file #9.\r\n", mode='wb') if os.name != "nt": os.chmod(test.workdir, 0o500) @@ -3411,7 +3743,7 @@ def test_write(self): class variables_TestCase(TestCmdTestCase): def test_variables(self) -> None: """Test global variables""" - run_env = TestCmd.TestCmd(workdir = '') + run_env = TestCmd.TestCmd(workdir='') variables = [ 'fail_test', @@ -3425,14 +3757,16 @@ def test_variables(self) -> None: 'TestCmd', ] - script = "import TestCmd\n" + \ - '\n'.join([ "print(TestCmd.%s\n)" % v for v in variables ]) + script = "import TestCmd\n" + '\n'.join( + [f"print(TestCmd.{v}\n)" for v in variables] + ) run_env.run(program=sys.executable, stdin=script) stderr = run_env.stderr() assert stderr == "", stderr - script = "from TestCmd import *\n" + \ - '\n'.join([ "print(%s)" % v for v in variables ]) + script = "from TestCmd import *\n" + '\n'.join( + [f"print({v})" for v in variables] + ) run_env.run(program=sys.executable, stdin=script) stderr = run_env.stderr() assert stderr == "", stderr diff --git a/testing/framework/TestCommon.py b/testing/framework/TestCommon.py index 993dc02ed7..9a0cb8beb4 100644 --- a/testing/framework/TestCommon.py +++ b/testing/framework/TestCommon.py @@ -1,3 +1,22 @@ +# Copyright 2000-2010 Steven Knight +# +# This module is free software, and you may redistribute it and/or modify +# it under the same terms as Python itself, so long as this copyright message +# and disclaimer are retained in their original form. +# +# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +# +# Python License: https://docs.python.org/3/license.html#psf-license + """ A testing framework for commands and scripts with commonly useful error handling @@ -39,26 +58,37 @@ test.must_contain_all_lines(output, lines, ['title', find]) + test.must_contain_single_instance_of(output, lines, ['title']) + test.must_contain_any_line(output, lines, ['title', find]) test.must_contain_exactly_lines(output, lines, ['title', find]) - test.must_exist('file1', ['file2', ...]) + test.must_exist('file1', ['file2', ...], ['message']) + + test.must_exist_one_of(files, ['message']) test.must_match('file', "expected contents\n") + test.must_match_file(file, golden_file, ['message', 'newline']) + test.must_not_be_writable('file1', ['file2', ...]) test.must_not_contain('file', 'banned text\n') test.must_not_contain_any_line(output, lines, ['title', find]) + test.must_not_contain_lines(lines, output, ['title', find]): + test.must_not_exist('file1', ['file2', ...]) + test.must_not_exist_any_of(files) + test.must_not_be_empty('file') test.run( options="options to be prepended to arguments", + arguments="string or list of arguments", stdout="expected standard output from the program", stderr="expected error output from the program", status=expected_status, @@ -80,21 +110,7 @@ """ -# Copyright 2000-2010 Steven Knight -# This module is free software, and you may redistribute it and/or modify -# it under the same terms as Python itself, so long as this copyright message -# and disclaimer are retained in their original form. -# -# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, -# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF -# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -# DAMAGE. -# -# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, -# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, -# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +from __future__ import annotations __author__ = "Steven Knight " __revision__ = "TestCommon.py 1.3.D001 2010/06/03 12:58:27 knight" @@ -107,6 +123,7 @@ import sysconfig from collections import UserList +from typing import Callable from TestCmd import * from TestCmd import __all__ @@ -133,74 +150,79 @@ else: obj_suffix = '.obj' shobj_suffix = '.obj' - exe_suffix = '.exe' + exe_suffix = '.exe' shobj_prefix = '' - lib_prefix = '' + lib_prefix = '' # TODO: for mingw, is this .lib or .a? - lib_suffix = '.lib' - dll_prefix = '' - dll_suffix = '.dll' + lib_suffix = '.lib' + dll_prefix = '' + dll_suffix = '.dll' elif sys.platform == 'cygwin': - exe_suffix = '.exe' - obj_suffix = '.o' + exe_suffix = '.exe' + obj_suffix = '.o' shobj_suffix = '.os' shobj_prefix = '' - lib_prefix = 'lib' - lib_suffix = '.a' - dll_prefix = 'cyg' - dll_suffix = '.dll' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'cyg' + dll_suffix = '.dll' elif sys.platform.find('irix') != -1: - exe_suffix = '' - obj_suffix = '.o' + exe_suffix = '' + obj_suffix = '.o' shobj_suffix = '.o' shobj_prefix = '' - lib_prefix = 'lib' - lib_suffix = '.a' - dll_prefix = 'lib' - dll_suffix = '.so' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.so' elif sys.platform.find('darwin') != -1: - exe_suffix = '' - obj_suffix = '.o' + exe_suffix = '' + obj_suffix = '.o' shobj_suffix = '.os' shobj_prefix = '' - lib_prefix = 'lib' - lib_suffix = '.a' - dll_prefix = 'lib' - dll_suffix = '.dylib' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.dylib' elif sys.platform.find('sunos') != -1: - exe_suffix = '' - obj_suffix = '.o' + exe_suffix = '' + obj_suffix = '.o' shobj_suffix = '.pic.o' shobj_prefix = '' - lib_prefix = 'lib' - lib_suffix = '.a' - dll_prefix = 'lib' - dll_suffix = '.so' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.so' else: - exe_suffix = '' - obj_suffix = '.o' + exe_suffix = '' + obj_suffix = '.o' shobj_suffix = '.os' shobj_prefix = '' - lib_prefix = 'lib' - lib_suffix = '.a' - dll_prefix = 'lib' - dll_suffix = '.so' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.so' + def is_List(e): return isinstance(e, (list, UserList)) + def is_Tuple(e): return isinstance(e, tuple) + def is_Sequence(e): - return (not hasattr(e, "strip") and - hasattr(e, "__getitem__") or - hasattr(e, "__iter__")) + return ( + not hasattr(e, "strip") and hasattr(e, "__getitem__") or hasattr(e, "__iter__") + ) + def is_writable(f): - mode = os.stat(f)[stat.ST_MODE] + mode = os.stat(f).st_mode return mode & stat.S_IWUSR + def separate_files(flist): existing = [] missing = [] @@ -211,15 +233,16 @@ def separate_files(flist): missing.append(f) return existing, missing -def contains(seq, subseq, find) -> bool: - # Returns True or False. + +def contains(seq, subseq, find: Callable | None = None) -> bool: if find is None: return subseq in seq else: f = find(seq, subseq) return f not in (None, -1) and f is not False -def find_index(seq, subseq, find): + +def find_index(seq, subseq, find: Callable | None = None) -> int | None: # Returns either an index of the subseq within the seq, or None. # Accepts a function find(seq, subseq), which returns an integer on success # and either: None, False, or -1, on failure. @@ -234,21 +257,24 @@ def find_index(seq, subseq, find): if os.name == 'posix': + def _failed(self, status: int = 0): if self.status is None or status is None: return None return _status(self) != status + def _status(self): return self.status elif os.name == 'nt': + def _failed(self, status: int = 0): - return not (self.status is None or status is None) and \ - self.status != status + return not (self.status is None or status is None) and self.status != status + def _status(self): return self.status -class TestCommon(TestCmd): +class TestCommon(TestCmd): # Additional methods from the Perl Test::Cmd::Common module # that we may wish to add in the future: # @@ -264,8 +290,14 @@ def __init__(self, **kw) -> None: super().__init__(**kw) os.chdir(self.workdir) - def options_arguments(self, options, arguments): + def options_arguments( + self, + options: str | list[str], + arguments: str | list[str], + ): """Merges the "options" keyword argument with the arguments.""" + # TODO: this *doesn't* split unless both are non-empty strings. + # Did we want to always split strings? if options: if arguments is None: return options @@ -282,14 +314,15 @@ def options_arguments(self, options, arguments): return arguments def must_be_writable(self, *files) -> None: - """Ensures that the specified file(s) exist and are writable. + """Ensure that the specified file(s) exist and are writable. + An individual file can be specified as a list of directory names, in which case the pathname will be constructed by concatenating them. Exits FAILED if any of the files does not exist or is not writable. """ - files = [is_List(x) and os.path.join(*x) or x for x in files] - existing, missing = separate_files(files) + flist = [os.path.join(*x) if is_List(x) else x for x in files] + existing, missing = separate_files(flist) unwritable = [x for x in existing if not is_writable(x)] if missing: print("Missing files: `%s'" % "', `".join(missing)) @@ -297,17 +330,23 @@ def must_be_writable(self, *files) -> None: print("Unwritable files: `%s'" % "', `".join(unwritable)) self.fail_test(missing + unwritable) - def must_contain(self, file, required, mode: str='rb', find=None) -> None: + def must_contain( + self, + file: str, + required: str, + mode: str = 'rb', + find: Callable | None = None, + ) -> None: """Ensures specified file contains the required text. Args: - file (string): name of file to search in. + file: name of file to search in. required (string): text to search for. For the default find function, type must match the return type from reading the file; current implementation will convert. - mode (string): file open mode. - find (func): optional custom search routine. Must take the - form "find(output, line)" non-negative integer on success + mode: file open mode. + find: optional custom search routine. Must take the + form ``find(output, line)``, returning non-negative integer on success and None, False, or -1, on failure. Calling test exits FAILED if search result is false @@ -326,7 +365,9 @@ def must_contain(self, file, required, mode: str='rb', find=None) -> None: print(file_contents) self.fail_test() - def must_contain_all(self, output, input, title=None, find=None) -> None: + def must_contain_all( + self, output, input, title: str = "", find: Callable | None = None + ) -> None: """Ensures that the specified output string (first argument) contains all of the specified input as a block (second argument). @@ -338,10 +379,10 @@ def must_contain_all(self, output, input, title=None, find=None) -> None: for lines in the output. """ if is_List(output): - output = os.newline.join(output) + output = '\n'.join(output) if not contains(output, input, find): - if title is None: + if not title: title = 'output' print(f'Missing expected input from {title}:') print(input) @@ -349,7 +390,9 @@ def must_contain_all(self, output, input, title=None, find=None) -> None: print(output) self.fail_test() - def must_contain_all_lines(self, output, lines, title=None, find=None) -> None: + def must_contain_all_lines( + self, output, lines, title: str = "", find: Callable | None = None + ) -> None: """Ensures that the specified output string (first argument) contains all of the specified lines (second argument). @@ -365,7 +408,7 @@ def must_contain_all_lines(self, output, lines, title=None, find=None) -> None: missing = [line for line in lines if not contains(output, line, find)] if missing: - if title is None: + if not title: title = 'output' sys.stdout.write(f"Missing expected lines from {title}:\n") for line in missing: @@ -374,13 +417,12 @@ def must_contain_all_lines(self, output, lines, title=None, find=None) -> None: sys.stdout.write(output) self.fail_test() - def must_contain_single_instance_of(self, output, lines, title=None) -> None: + def must_contain_single_instance_of(self, output, lines, title: str = "") -> None: """Ensures that the specified output string (first argument) contains one instance of the specified lines (second argument). An optional third argument can be used to describe the type of output being searched, and only shows up in failure output. - """ if is_List(output): output = '\n'.join(output) @@ -392,7 +434,7 @@ def must_contain_single_instance_of(self, output, lines, title=None) -> None: counts[line] = count if counts: - if title is None: + if not title: title = 'output' sys.stdout.write(f"Unexpected number of lines from {title}:\n") for line in counts: @@ -401,7 +443,9 @@ def must_contain_single_instance_of(self, output, lines, title=None) -> None: sys.stdout.write(output) self.fail_test() - def must_contain_any_line(self, output, lines, title=None, find=None) -> None: + def must_contain_any_line( + self, output, lines, title: str = "", find: Callable | None = None + ) -> None: """Ensures that the specified output string (first argument) contains at least one of the specified lines (second argument). @@ -416,7 +460,7 @@ def must_contain_any_line(self, output, lines, title=None, find=None) -> None: if contains(output, line, find): return - if title is None: + if not title: title = 'output' sys.stdout.write(f"Missing any expected line from {title}:\n") for line in lines: @@ -425,7 +469,9 @@ def must_contain_any_line(self, output, lines, title=None, find=None) -> None: sys.stdout.write(output) self.fail_test() - def must_contain_exactly_lines(self, output, expect, title=None, find=None) -> None: + def must_contain_exactly_lines( + self, output, expect, title: str = "", find: Callable | None = None + ) -> None: """Ensures that the specified output string (first argument) contains all of the lines in the expected string (second argument) with none left over. @@ -440,7 +486,7 @@ def must_contain_exactly_lines(self, output, expect, title=None, find=None) -> N """ out = output.splitlines() if is_List(expect): - exp = [ e.rstrip('\n') for e in expect ] + exp = [e.rstrip('\n') for e in expect] else: exp = expect.splitlines() if sorted(out) == sorted(exp): @@ -458,7 +504,7 @@ def must_contain_exactly_lines(self, output, expect, title=None, find=None) -> N # all lines were matched return - if title is None: + if not title: title = 'output' if missing: sys.stdout.write(f"Missing expected lines from {title}:\n") @@ -473,23 +519,25 @@ def must_contain_exactly_lines(self, output, expect, title=None, find=None) -> N sys.stdout.flush() self.fail_test() - def must_contain_lines(self, lines, output, title=None, find = None): + def must_contain_lines( + self, lines, output, title: str = "", find: Callable | None = None + ) -> None: # Deprecated; retain for backwards compatibility. - return self.must_contain_all_lines(output, lines, title, find) + self.must_contain_all_lines(output, lines, title, find) - def must_exist(self, *files) -> None: + def must_exist(self, *files, message: str = "") -> None: """Ensures that the specified file(s) must exist. An individual file be specified as a list of directory names, in which case the pathname will be constructed by concatenating them. Exits FAILED if any of the files does not exist. """ - files = [is_List(x) and os.path.join(*x) or x for x in files] - missing = [x for x in files if not os.path.exists(x) and not os.path.islink(x) ] + flist = [os.path.join(*x) if is_List(x) else x for x in files] + missing = [x for x in flist if not os.path.exists(x) and not os.path.islink(x)] if missing: print("Missing files: `%s'" % "', `".join(missing)) - self.fail_test(missing) + self.fail_test(bool(missing), message=message) - def must_exist_one_of(self, files) -> None: + def must_exist_one_of(self, files, message: str = "") -> None: """Ensures that at least one of the specified file(s) exists. The filenames can be given as a list, where each entry may be a single path string, or a tuple of folder names and the final @@ -502,14 +550,22 @@ def must_exist_one_of(self, files) -> None: if is_List(x) or is_Tuple(x): xpath = os.path.join(*x) else: - xpath = is_Sequence(x) and os.path.join(x) or x + xpath = os.path.join(x) if is_Sequence(x) else x if glob.glob(xpath): return missing.append(xpath) print("Missing one of: `%s'" % "', `".join(missing)) - self.fail_test(missing) - - def must_match(self, file, expect, mode: str = 'rb', match=None, message=None, newline=None): + self.fail_test(bool(missing), message=message) + + def must_match( + self, + file, + expect, + mode: str = 'rb', + match: Callable | None = None, + message: str = "", + newline=None, + ): """Matches the contents of the specified file (first argument) against the expected contents (second argument). The expected contents are a list of lines or a string which will be split @@ -519,7 +575,10 @@ def must_match(self, file, expect, mode: str = 'rb', match=None, message=None, n if not match: match = self.match try: - self.fail_test(not match(to_str(file_contents), to_str(expect)), message=message) + self.fail_test( + not match(to_str(file_contents), to_str(expect)), + message=message, + ) except KeyboardInterrupt: raise except: @@ -527,7 +586,15 @@ def must_match(self, file, expect, mode: str = 'rb', match=None, message=None, n self.diff(expect, file_contents, 'contents ') raise - def must_match_file(self, file, golden_file, mode: str='rb', match=None, message=None, newline=None): + def must_match_file( + self, + file, + golden_file, + mode: str = 'rb', + match: Callable | None = None, + message: str = "", + newline=None, + ) -> None: """Matches the contents of the specified file (first argument) against the expected contents (second argument). The expected contents are a list of lines or a string which will be split @@ -540,17 +607,19 @@ def must_match_file(self, file, golden_file, mode: str='rb', match=None, message match = self.match try: - self.fail_test(not match(to_str(file_contents), to_str(golden_file_contents)), message=message) + self.fail_test( + not match(to_str(file_contents), to_str(golden_file_contents)), + message=message, + ) except KeyboardInterrupt: raise except: - print("Unexpected contents of `%s'" % file) + print(f"Unexpected contents of `{file}'") self.diff(golden_file_contents, file_contents, 'contents ') raise - def must_not_contain(self, file, banned, mode: str = 'rb', find = None) -> None: - """Ensures that the specified file doesn't contain the banned text. - """ + def must_not_contain(self, file, banned, mode: str = 'rb', find=None) -> None: + """Ensures that the specified file doesn't contain the banned text.""" file_contents = self.read(file, mode) if contains(file_contents, banned, find): @@ -561,7 +630,9 @@ def must_not_contain(self, file, banned, mode: str = 'rb', find = None) -> None: print(file_contents) self.fail_test() - def must_not_contain_any_line(self, output, lines, title=None, find=None) -> None: + def must_not_contain_any_line( + self, output, lines, title: str = "", find: Callable | None = None + ) -> None: """Ensures that the specified output string (first argument) does not contain any of the specified lines (second argument). @@ -578,7 +649,7 @@ def must_not_contain_any_line(self, output, lines, title=None, find=None) -> Non unexpected.append(line) if unexpected: - if title is None: + if not title: title = 'output' sys.stdout.write(f"Unexpected lines in {title}:\n") for line in unexpected: @@ -587,8 +658,10 @@ def must_not_contain_any_line(self, output, lines, title=None, find=None) -> Non sys.stdout.write(output) self.fail_test() - def must_not_contain_lines(self, lines, output, title=None, find=None): - return self.must_not_contain_any_line(output, lines, title, find) + def must_not_contain_lines( + self, lines, output, title: str = "", find: Callable | None = None + ) -> None: + self.must_not_contain_any_line(output, lines, title, find) def must_not_exist(self, *files) -> None: """Ensures that the specified file(s) must not exist. @@ -596,11 +669,11 @@ def must_not_exist(self, *files) -> None: which case the pathname will be constructed by concatenating them. Exits FAILED if any of the files exists. """ - files = [is_List(x) and os.path.join(*x) or x for x in files] - existing = [x for x in files if os.path.exists(x) or os.path.islink(x)] + flist = [os.path.join(*x) if is_List(x) else x for x in files] + existing = [x for x in flist if os.path.exists(x) or os.path.islink(x)] if existing: print("Unexpected files exist: `%s'" % "', `".join(existing)) - self.fail_test(existing) + self.fail_test(bool(existing)) def must_not_exist_any_of(self, files) -> None: """Ensures that none of the specified file(s) exists. @@ -615,12 +688,12 @@ def must_not_exist_any_of(self, files) -> None: if is_List(x) or is_Tuple(x): xpath = os.path.join(*x) else: - xpath = is_Sequence(x) and os.path.join(x) or x + xpath = os.path.join(x) if is_Sequence(x) else x if glob.glob(xpath): existing.append(xpath) if existing: print("Unexpected files exist: `%s'" % "', `".join(existing)) - self.fail_test(existing) + self.fail_test(bool(existing)) def must_not_be_empty(self, file) -> None: """Ensures that the specified file exists, and that it is not empty. @@ -646,8 +719,8 @@ def must_not_be_writable(self, *files) -> None: them. Exits FAILED if any of the files does not exist or is writable. """ - files = [is_List(x) and os.path.join(*x) or x for x in files] - existing, missing = separate_files(files) + flist = [os.path.join(*x) if is_List(x) else x for x in files] + existing, missing = separate_files(flist) writable = [file for file in existing if is_writable(file)] if missing: print("Missing files: `%s'" % "', `".join(missing)) @@ -655,8 +728,15 @@ def must_not_be_writable(self, *files) -> None: print("Writable files: `%s'" % "', `".join(writable)) self.fail_test(missing + writable) - def _complete(self, actual_stdout, expected_stdout, - actual_stderr, expected_stderr, status, match) -> None: + def _complete( + self, + actual_stdout, + expected_stdout, + actual_stderr, + expected_stderr, + status, + match, + ) -> None: """ Post-processes running a subcommand, checking for failure status and displaying output appropriately. @@ -671,34 +751,36 @@ def _complete(self, actual_stdout, expected_stdout, print(self.banner('STDERR ')) print(actual_stderr) self.fail_test() - if (expected_stdout is not None - and not match(actual_stdout, expected_stdout)): + if expected_stdout is not None and not match(actual_stdout, expected_stdout): self.diff(expected_stdout, actual_stdout, 'STDOUT ') if actual_stderr: print(self.banner('STDERR ')) print(actual_stderr) self.fail_test() - if (expected_stderr is not None - and not match(actual_stderr, expected_stderr)): + if expected_stderr is not None and not match(actual_stderr, expected_stderr): print(self.banner('STDOUT ')) print(actual_stdout) self.diff(expected_stderr, actual_stderr, 'STDERR ') self.fail_test() - def start(self, program = None, - interpreter = None, - options = None, - arguments = None, - universal_newlines = None, - **kw): + def start( + self, + program=None, + interpreter=None, + options=None, + arguments=None, + universal_newlines=None, + **kw, + ): """ Starts a program or script for the test environment, handling any exceptions. """ arguments = self.options_arguments(options, arguments) try: - return super().start(program, interpreter, arguments, - universal_newlines, **kw) + return super().start( + program, interpreter, arguments, universal_newlines, **kw + ) except KeyboardInterrupt: raise except Exception as e: @@ -716,50 +798,61 @@ def start(self, program = None, sys.stderr.write(f'Exception trying to execute: {cmd_args}\n') raise e - def finish(self, popen, stdout = None, stderr: str = '', status: int = 0, **kw) -> None: - """ - Finishes and waits for the process being run under control of - the specified popen argument. Additional arguments are similar - to those of the run() method: + def finish( + self, + popen, + stdout: str | None = None, + stderr: str | None = '', + status: int | None = 0, + **kw, + ) -> None: + """Finish and wait for the process being run. - stdout The expected standard output from - the command. A value of None means - don't test standard output. + The *popen* argument describes a ``Popen`` object controlling + the process. - stderr The expected error output from - the command. A value of None means - don't test error output. + The default behavior is to expect a successful exit, to not test + standard output, and to expect error output to be empty. - status The expected exit status from the - command. A value of None means don't - test exit status. + Only arguments extending :meth:`TestCmd.finish` are shown. + + Args: + stdout: The expected standard output from the command. + A value of ``None`` means don't test standard output. + stderr: The expected error output from the command. + A value of ``None`` means don't test error output. + status: The expected exit status from the command. + A value of ``None`` means don't test exit status. """ super().finish(popen, **kw) match = kw.get('match', self.match) - self._complete(self.stdout(), stdout, - self.stderr(), stderr, status, match) - - def run(self, options = None, arguments = None, - stdout = None, stderr: str = '', status: int = 0, **kw) -> None: + self._complete(self.stdout(), stdout, self.stderr(), stderr, status, match) + + def run( + self, + options=None, + arguments=None, + stdout: str | None = None, + stderr: str | None = '', + status: int | None = 0, + **kw, + ) -> None: """Runs the program under test, checking that the test succeeded. - The parameters are the same as the base TestCmd.run() method, - with the addition of: - - options Extra options that get prepended to the beginning - of the arguments. + The default behavior is to expect a successful exit, not test + standard output, and expects error output to be empty. - stdout The expected standard output from - the command. A value of None means - don't test standard output. + Only arguments extending :meth:`TestCmd.run` are shown. - stderr The expected error output from - the command. A value of None means - don't test error output. - - status The expected exit status from the - command. A value of None means don't - test exit status. + Args: + options: Extra options that get prepended to the beginning + of the arguments. + stdout: The expected standard output from the command. + A value of ``None`` means don't test standard output. + stderr: The expected error output from the command. + A value of ``None`` means don't test error output. + status: The expected exit status from the command. + A value of ``None`` means don't test exit status. By default, this expects a successful exit (status = 0), does not test standard output (stdout = None), and expects that error @@ -767,15 +860,15 @@ def run(self, options = None, arguments = None, """ kw['arguments'] = self.options_arguments(options, arguments) try: - match = kw['match'] - del kw['match'] + match = kw.pop('match') except KeyError: match = self.match super().run(**kw) - self._complete(self.stdout(), stdout, - self.stderr(), stderr, status, match) + self._complete(self.stdout(), stdout, self.stderr(), stderr, status, match) - def skip_test(self, message: str="Skipping test.\n", from_fw: bool=False) -> None: + def skip_test( + self, message: str = "Skipping test.\n", from_fw: bool = False + ) -> None: """Skips a test. Proper test-skipping behavior is dependent on the external @@ -824,9 +917,8 @@ def detailed_diff(value, expect) -> str: if len(v_split) != len(e_split): print(f"different number of lines:{len(v_split)} {len(e_split)}") - # breakpoint() for v, e in zip(v_split, e_split): - # print("%s:%s"%(v,e)) + # print(f"{v}:{e}") if v != e: print(f"\n[{v}]\n[{e}]") diff --git a/testing/framework/TestCommonTests.py b/testing/framework/TestCommonTests.py index 31e13e9b51..3beca80102 100644 --- a/testing/framework/TestCommonTests.py +++ b/testing/framework/TestCommonTests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -""" -Unit tests for the TestCommon.py module. -""" - +# # Copyright 2000-2010 Steven Knight +# # This module is free software, and you may redistribute it and/or modify # it under the same terms as Python itself, so long as this copyright message # and disclaimer are retained in their original form. @@ -18,6 +16,12 @@ # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +# +# Python License: https://docs.python.org/3/license.html#psf-license + +""" +Unit tests for the TestCommon.py module. +""" import os import re @@ -32,10 +36,12 @@ import TestCmd import TestCommon + # this used to be a custom function, now use the stdlib equivalent def lstrip(s): return dedent(s) + expected_newline = '\\n' @@ -58,12 +64,13 @@ def assert_display(expect, result, error=None): class TestCommonTestCase(unittest.TestCase): """Base class for TestCommon test cases, fixture and utility methods.""" + create_run_env = True def setUp(self) -> None: self.orig_cwd = os.getcwd() if self.create_run_env: - self.run_env = TestCmd.TestCmd(workdir = '') + self.run_env = TestCmd.TestCmd(workdir='') def tearDown(self) -> None: os.chdir(self.orig_cwd) @@ -109,12 +116,15 @@ def set_up_execution_scripts(self) -> None: run_env.write(self.signal_script, wrapper % signal_body) - stdin_body = lstrip("""\ + stdin_body = lstrip( + """\ import sys input = sys.stdin.read()[:-1] sys.stdout.write(r'%s: STDOUT: ' + repr(input) + '\\n') sys.stderr.write(r'%s: STDERR: ' + repr(input) + '\\n') - """ % (self.stdin_script, self.stdin_script)) + """ + % (self.stdin_script, self.stdin_script) + ) run_env.write(self.stdin_script, wrapper % stdin_body) @@ -134,19 +144,15 @@ def run_execution_test(self, script, expect_stdout, expect_stderr) -> None: stderr = run_env.stderr() expect_stdout = expect_stdout % self.__dict__ - assert stdout == expect_stdout, assert_display(expect_stdout, - stdout, - stderr) + assert stdout == expect_stdout, assert_display(expect_stdout, stdout, stderr) try: match = expect_stderr.match except AttributeError: expect_stderr = expect_stderr % self.__dict__ - assert stderr == expect_stderr, assert_display(expect_stderr, - stderr) + assert stderr == expect_stderr, assert_display(expect_stderr, stderr) else: - assert expect_stderr.match(stderr), assert_display(expect_stderr, - stderr) + assert expect_stderr.match(stderr), assert_display(expect_stderr, stderr) class __init__TestCase(TestCommonTestCase): @@ -170,25 +176,30 @@ def test___init__(self) -> None: class banner_TestCase(TestCommonTestCase): create_run_env = False + def test_banner(self) -> None: """Test banner()""" tc = TestCommon.TestCommon(workdir='') - b = tc.banner('xyzzy ') - assert b == "xyzzy ==========================================================================", b + text = 'xyzzy ' + b = tc.banner(text) + expect = f"{text}{tc.banner_char * (tc.banner_width - len(text))}" + assert b == expect, b tc.banner_width = 10 + b = tc.banner(text) + expect = f"{text}{tc.banner_char * (tc.banner_width - len(text))}" + assert b == expect, b - b = tc.banner('xyzzy ') - assert b == "xyzzy ====", b - - b = tc.banner('xyzzy ', 20) - assert b == "xyzzy ==============", b + b = tc.banner(text, 20) + expect = f"{text}{tc.banner_char * (20 - len(text))}" + assert b == expect, b tc.banner_char = '-' + b = tc.banner(text) + expect = f"{text}{tc.banner_char * (tc.banner_width - len(text))}" + assert b == expect, b - b = tc.banner('xyzzy ') - assert b == "xyzzy ----", b class must_be_writable_TestCase(TestCommonTestCase): def test_file_does_not_exists(self) -> None: @@ -218,7 +229,7 @@ def test_writable_file_exists(self) -> None: tc = TestCommon(workdir='') tc.write('file1', "file1\\n") f1 = tc.workpath('file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode | stat.S_IWUSR) tc.must_be_writable('file1') tc.pass_test() @@ -240,7 +251,7 @@ def test_non_writable_file_exists(self) -> None: tc = TestCommon(workdir='') tc.write('file1', "file1\\n") f1 = tc.workpath('file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode & ~stat.S_IWUSR) tc.must_be_writable('file1') tc.pass_test() @@ -263,7 +274,7 @@ def test_file_specified_as_list(self) -> None: tc.subdir('sub') tc.write(['sub', 'file1'], "sub/file1\\n") f1 = tc.workpath('sub', 'file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode | stat.S_IWUSR) tc.must_be_writable(['sub', 'file1']) tc.pass_test() @@ -370,7 +381,6 @@ def test_mode(self) -> None: assert stderr == "PASSED\n", stderr - class must_contain_all_lines_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_contain_all_lines(): success""" @@ -428,14 +438,17 @@ def test_failure(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Missing expected lines from output: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' output ========================================================================= www zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -466,6 +479,7 @@ def test_find(self) -> None: def re_search(output, line): return re.compile(line, re.S).search(output) + test.must_contain_all_lines(output, lines, find=re_search) test.pass_test() @@ -500,14 +514,17 @@ def test_title(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Missing expected lines from STDERR: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' STDERR ========================================================================= www zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -516,7 +533,6 @@ def test_title(self) -> None: assert stderr.find("FAILED") != -1, stderr - class must_contain_any_line_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_contain_any_line(): success""" @@ -574,14 +590,17 @@ def test_failure(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Missing any expected line from output: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' output ========================================================================= www zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -646,14 +665,17 @@ def test_title(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Missing any expected line from STDOUT: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' STDOUT ========================================================================= www zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -662,7 +684,6 @@ def test_title(self) -> None: assert stderr.find("FAILED") != -1, stderr - class must_contain_exactly_lines_TestCase(TestCommonTestCase): def test_success_list(self) -> None: """Test must_contain_exactly_lines(): success (input list)""" @@ -762,7 +783,7 @@ def test_failure(self) -> None: 'www' 'zzz' Extra output =================================================================== - """ % globals()) + """) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -848,7 +869,7 @@ def test_title(self) -> None: 'www' 'zzz' Extra STDOUT =================================================================== - """ % globals()) + """) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -857,7 +878,6 @@ def test_title(self) -> None: assert stderr.find("FAILED") != -1, stderr - class must_contain_lines_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_contain_lines(): success""" @@ -913,14 +933,17 @@ def test_failure(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Missing expected lines from output: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' output ========================================================================= www zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -929,7 +952,6 @@ def test_failure(self) -> None: assert stderr.find("FAILED") != -1, stderr - class must_exist_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_exist(): success""" @@ -964,6 +986,22 @@ def test_failure(self) -> None: stderr = run_env.stderr() assert stderr.find("FAILED") != -1, stderr + def test_failure_message(self) -> None: + """Test must_exist(): failure with extra message""" + run_env = self.run_env + + script = lstrip("""\ + from TestCommon import TestCommon + tc = TestCommon(workdir='') + tc.must_exist('file1', message="Extra Info") + tc.pass_test() + """) + run_env.run(program=sys.executable, stdin=script) + stdout = run_env.stdout() + assert stdout == "Missing files: `file1'\n", stdout + stderr = run_env.stderr() + assert stderr.find("Extra Info") != -1, stderr + def test_file_specified_as_list(self) -> None: """Test must_exist(): file specified as list""" run_env = self.run_env @@ -983,7 +1021,7 @@ def test_file_specified_as_list(self) -> None: assert stderr == "PASSED\n", stderr @unittest.skipIf(sys.platform == 'win32', "Skip symlink test on win32") - def test_broken_link(self) -> None : + def test_broken_link(self) -> None: """Test must_exist(): exists but it is a broken link""" run_env = self.run_env @@ -1000,6 +1038,7 @@ def test_broken_link(self) -> None : stderr = run_env.stderr() assert stderr == "PASSED\n", stderr + class must_exist_one_of_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_exist_one_of(): success""" @@ -1034,6 +1073,22 @@ def test_failure(self) -> None: stderr = run_env.stderr() assert stderr.find("FAILED") != -1, stderr + def test_failure_message(self) -> None: + """Test must_exist_one_of(): failure with extra message""" + run_env = self.run_env + + script = lstrip("""\ + from TestCommon import TestCommon + tc = TestCommon(workdir='') + tc.must_exist_one_of(['file1'], message="Extra Info") + tc.pass_test() + """) + run_env.run(program=sys.executable, stdin=script) + stdout = run_env.stdout() + assert stdout == "Missing one of: `file1'\n", stdout + stderr = run_env.stderr() + assert stderr.find("Extra Info") != -1, stderr + def test_files_specified_as_list(self) -> None: """Test must_exist_one_of(): files specified as list""" run_env = self.run_env @@ -1077,8 +1132,7 @@ def test_file_given_as_list(self) -> None: tc = TestCommon(workdir='') tc.subdir('sub') tc.write(['sub', 'file1'], "sub/file1\\n") - tc.must_exist_one_of(['file2', - ['sub', 'file1']]) + tc.must_exist_one_of(['file2', ['sub', 'file1']]) tc.pass_test() """) run_env.run(program=sys.executable, stdin=script) @@ -1096,8 +1150,7 @@ def test_file_given_as_sequence(self) -> None: tc = TestCommon(workdir='') tc.subdir('sub') tc.write(['sub', 'file1'], "sub/file1\\n") - tc.must_exist_one_of(['file2', - ('sub', 'file1')]) + tc.must_exist_one_of(['file2', ('sub', 'file1')]) tc.pass_test() """) run_env.run(program=sys.executable, stdin=script) @@ -1106,6 +1159,7 @@ def test_file_given_as_sequence(self) -> None: stderr = run_env.stderr() assert stderr == "PASSED\n", stderr + class must_match_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_match(): success""" @@ -1189,7 +1243,6 @@ def test_mode(self) -> None: assert stderr == "PASSED\n", stderr - class must_not_be_writable_TestCase(TestCommonTestCase): def test_file_does_not_exists(self) -> None: """Test must_not_be_writable(): file does not exist""" @@ -1218,7 +1271,7 @@ def test_writable_file_exists(self) -> None: tc = TestCommon(workdir='') tc.write('file1', "file1\\n") f1 = tc.workpath('file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode | stat.S_IWUSR) tc.must_not_be_writable('file1') tc.pass_test() @@ -1240,7 +1293,7 @@ def test_non_writable_file_exists(self) -> None: tc = TestCommon(workdir='') tc.write('file1', "file1\\n") f1 = tc.workpath('file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode & ~stat.S_IWUSR) tc.must_not_be_writable('file1') tc.pass_test() @@ -1263,7 +1316,7 @@ def test_file_specified_as_list(self) -> None: tc.subdir('sub') tc.write(['sub', 'file1'], "sub/file1\\n") f1 = tc.workpath('sub', 'file1') - mode = os.stat(f1)[stat.ST_MODE] + mode = os.stat(f1).st_mode os.chmod(f1, mode & ~stat.S_IWUSR) tc.must_not_be_writable(['sub', 'file1']) tc.pass_test() @@ -1275,7 +1328,6 @@ def test_file_specified_as_list(self) -> None: assert stderr == "PASSED\n", stderr - class must_not_contain_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_not_contain(): success""" @@ -1380,7 +1432,6 @@ def test_mode(self) -> None: assert stderr == "PASSED\n", stderr - class must_not_contain_any_line_TestCase(TestCommonTestCase): def test_failure(self) -> None: """Test must_not_contain_any_line(): failure""" @@ -1408,7 +1459,8 @@ def test_failure(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Unexpected lines in output: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' @@ -1418,7 +1470,9 @@ def test_failure(self) -> None: xxx yyy zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -1512,7 +1566,8 @@ def test_title(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Unexpected lines in XYZZY: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' @@ -1521,7 +1576,9 @@ def test_title(self) -> None: xxx yyy zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -1530,7 +1587,6 @@ def test_title(self) -> None: assert stderr.find("FAILED") != -1, stderr - class must_not_contain_lines_TestCase(TestCommonTestCase): def test_failure(self) -> None: """Test must_not_contain_lines(): failure""" @@ -1557,7 +1613,8 @@ def test_failure(self) -> None: test.pass_test() """) - expect = lstrip("""\ + expect = lstrip( + """\ Unexpected lines in output: 'xxx%(expected_newline)s' 'yyy%(expected_newline)s' @@ -1566,7 +1623,9 @@ def test_failure(self) -> None: xxx yyy zzz - """ % globals()) + """ + % globals() + ) run_env.run(program=sys.executable, stdin=script) stdout = run_env.stdout() @@ -1604,7 +1663,6 @@ def test_success(self) -> None: assert stderr == "PASSED\n", stderr - class must_not_exist_TestCase(TestCommonTestCase): def test_failure(self) -> None: """Test must_not_exist(): failure""" @@ -1674,6 +1732,7 @@ def test_existing_broken_link(self) -> None: stderr = run_env.stderr() assert stderr.find("FAILED") != -1, stderr + class must_not_exist_any_of_TestCase(TestCommonTestCase): def test_success(self) -> None: """Test must_not_exist_any_of(): success""" @@ -1779,6 +1838,7 @@ def test_file_given_as_sequence(self) -> None: stderr = run_env.stderr() assert stderr == "PASSED\n", stderr + class must_not_be_empty_TestCase(TestCommonTestCase): def test_failure(self) -> None: """Test must_not_be_empty(): failure""" @@ -1830,6 +1890,7 @@ def test_file_doesnt_exist(self) -> None: stderr = run_env.stderr() assert stderr.find("FAILED") != -1, stderr + class run_TestCase(TestCommonTestCase): def test_argument_handling(self) -> None: """Test run(): argument handling""" @@ -1940,52 +2001,51 @@ def raise_exception(*args, **kw): """) expect_stderr = lstrip( - fr"""Exception trying to execute: \[{re.escape(repr(sys.executable))}, '[^']*pass'\] + rf"""Exception trying to execute: \[{re.escape(repr(sys.executable))}, '[^']*pass'\] Traceback \(most recent call last\): File "", line \d+, in (\?|) - File "[^"]+TestCommon.py", line \d+, in run - super\(\).run\(\*\*kw\) - File "[^"]+TestCmd.py", line \d+, in run - p = self.start\(program=program, -(?:\s*\^*\s)? File \"[^\"]+TestCommon.py\", line \d+, in start + File "[^"]+TestCommon\.py", line \d+, in run + super\(\)\.run\(\*\*kw\) + File "[^"]+TestCmd\.py", line \d+, in run + p = self\.start\( +(?:\s*\^*\s)? File \"[^\"]+TestCommon\.py\", line \d+, in start raise e - File "[^"]+TestCommon.py", line \d+, in start - return super\(\).start\(program, interpreter, arguments, + File "[^"]+TestCommon\.py", line \d+, in start + return super\(\)\.start\( (?:\s*\^*\s)? File \"\", line \d+, in raise_exception TypeError: forced TypeError -""") - expect_stderr = re.compile(expect_stderr, re.M) +""" + ) - # TODO: Python 3.13+ expanded error msgs again. This doesn't work yet. + # Python 3.13+ expanded error msgs again, not in a way we can easily + # accomodate with the other regex. + # TODO: broken again after reformat work expect_enhanced_stderr = lstrip( - fr"""Exception trying to execute: \[{re.escape(repr(sys.executable))}, '[^']*pass'\] -Traceback (most recent call last): + rf"""Exception trying to execute: \[{re.escape(repr(sys.executable))}, '[^']*pass'\] +Traceback \(most recent call last\): File "", line \d+, in (\?|) - File "[^"]+TestCommon.py", line \d+, in run - super\(\).run\(\*\*kw\) - ~~~~~~~~~~~^^^^^^ - File "[^"]+TestCmd.py", line \d+, in run - p = self.start\(program=program, - ~~~~~~~~~~^^^^^^^^^^^^^^^^^ - interpreter=interpreter, - ^^^^^^^^^^^^^^^^^^^^^^^^ - ...<2 lines>... - timeout=timeout, - ^^^^^^^^^^^^^^^^ - stdin=stdin\) - ^^^^^^^^^^^^ -(?:\s*\^*\s)? File \"[^\"]+TestCommon.py\", line \d+, in start + File "[^"]+TestCommon\.py", line \d+, in run + super\(\)\.run\(\*\*kw\) +(?:\s*[~\^]*\s*)?File "[^"]+TestCmd\.py", line \d+, in run + p = self\.start\( + program=program, + \.\.\.<4 lines>\.\.\. + stdin=stdin, + \) +(?:\s*[~\^]*\s*)?File \"[^\"]+TestCommon\.py\", line \d+, in start raise e - File "[^"]+TestCommon.py", line \d+, in start - return super\(\).start\(program, interpreter, arguments, - ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - universal_newlines, \*\*kw\) - ^^^^^^^^^^^^^^^^^^^^^^^^^ -(?:\s*\^*\s)? File \"\", line \d+, in raise_exception + File "[^"]+TestCommon\.py", line \d+, in start + return super\(\)\.start\( +(?:\s*[~\^]*\s*)?program, interpreter, arguments, universal_newlines, \*\*kw +(?:\s*[~\^]*\s*)?\) +(?:\s*[~\^]*\s*)?File \"\", line \d+, in raise_exception TypeError: forced TypeError -""") - expect_enhanced_stderr = re.compile(expect_enhanced_stderr, re.M) - +""" + ) + if sys.version_info[:2] > (3, 12): + expect_stderr = re.compile(expect_enhanced_stderr, re.M) + else: + expect_stderr = re.compile(expect_stderr, re.M) self.run_execution_test(script, expect_stdout, expect_stderr) def test_ignore_stderr(self) -> None: @@ -2234,10 +2294,14 @@ def test_options_plus_arguments(self) -> None: self.run_execution_test(script, "", "") def test_signal_handling(self) -> None: - """Test run(): signal handling""" + """Test run(): signal handling. + Only strange platforms unlikely to support SCons like the + webassembly ones don't support kill(), but keep the test + in place anyway. + """ try: - os.kill + _ = os.kill except AttributeError: sys.stderr.write('can not test, no os.kill ... ') return @@ -2297,7 +2361,6 @@ def test_stdin(self) -> None: self.run_execution_test(script, "", "") - class start_TestCase(TestCommonTestCase): def test_option_handling(self) -> None: """Test start(): option handling""" @@ -2333,7 +2396,6 @@ def test_options_plus_arguments(self) -> None: self.run_execution_test(script, "", "") - class skip_test_TestCase(TestCommonTestCase): def test_skip_test(self) -> None: """Test skip_test()""" @@ -2370,6 +2432,7 @@ def test_skip_test(self) -> None: assert stderr in expect, repr(stderr) import os + os.environ['TESTCOMMON_PASS_SKIPS'] = '1' try: @@ -2388,7 +2451,6 @@ def test_skip_test(self) -> None: del os.environ['TESTCOMMON_PASS_SKIPS'] - class variables_TestCase(TestCommonTestCase): def test_variables(self) -> None: """Test global variables""" @@ -2404,7 +2466,6 @@ def test_variables(self) -> None: 'python', '_python_', 'TestCmd', - 'TestCommon', 'exe_suffix', 'obj_suffix', @@ -2416,20 +2477,21 @@ def test_variables(self) -> None: 'dll_suffix', ] - script = "import TestCommon\n" + \ - '\n'.join([f"print(TestCommon.{v})\n" for v in variables]) + script = "import TestCommon\n" + '\n'.join( + [f"print(TestCommon.{v})\n" for v in variables] + ) run_env.run(program=sys.executable, stdin=script) stderr = run_env.stderr() assert stderr == "", stderr - script = "from TestCommon import *\n" + \ - '\n'.join([f"print({v})" for v in variables]) + script = "from TestCommon import *\n" + '\n'.join( + [f"print({v})" for v in variables] + ) run_env.run(program=sys.executable, stdin=script) stderr = run_env.stderr() assert stderr == "", stderr - if __name__ == "__main__": unittest.main() diff --git a/testing/framework/TestRuntest.py b/testing/framework/TestRuntest.py index 5277690867..d277cc16e0 100644 --- a/testing/framework/TestRuntest.py +++ b/testing/framework/TestRuntest.py @@ -43,13 +43,7 @@ from TestCommon import * from TestCommon import __all__ -__all__.extend( - [ - 'TestRuntest', - 'pythonstring', - 'pythonflags', - ] -) +__all__.extend(['TestRuntest', 'pythonstring', 'pythonflags']) pythonstring = python pythonflags = '' @@ -91,6 +85,7 @@ __developer__ = 'John Doe' """ + class TestRuntest(TestCommon): """Class for testing the runtest.py script. @@ -122,7 +117,9 @@ def __init__(self, **kw) -> None: if 'program' not in kw: kw['program'] = 'runtest.py' if 'interpreter' not in kw: - kw['interpreter'] = [python,] + kw['interpreter'] = [ + python, + ] if 'match' not in kw: kw['match'] = match_exact if 'workdir' not in kw: @@ -176,6 +173,7 @@ def write_no_result_test(self, name) -> None: def write_passing_test(self, name) -> None: self.write(name, passing_test_template) + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/testing/framework/TestSCons.py b/testing/framework/TestSCons.py index 2b40a361de..878eb9347a 100644 --- a/testing/framework/TestSCons.py +++ b/testing/framework/TestSCons.py @@ -34,49 +34,51 @@ attributes defined in this subclass. """ +from __future__ import annotations + import os import re import shutil +import subprocess as sp import sys import time -import subprocess as sp import zipfile from collections import namedtuple +from SCons.Util import get_hash_format, get_current_hash_algorithm_used from TestCommon import * from TestCommon import __all__, _python_ -from SCons.Util import get_hash_format, get_current_hash_algorithm_used - -from TestCmd import Popen -from TestCmd import PIPE # Some tests which verify that SCons has been packaged properly need to # look for specific version file names. Replicating the version number # here provides some independent verification that what we packaged # conforms to what we expect. -default_version = '4.7.1ayyyymmdd' +default_version = '4.9.2ayyyymmdd' # TODO: these need to be hand-edited when there are changes -python_version_unsupported = (3, 6, 0) -python_version_deprecated = (3, 7, 0) # lowest non-deprecated Python +python_version_unsupported = (3, 7, 0) +python_version_deprecated = (3, 7, 0) python_version_supported_str = "3.7.0" # str of lowest non-deprecated Python SConsVersion = default_version -__all__.extend([ - 'TestSCons', - 'machine', - 'python', - '_exe', - '_obj', - '_shobj', - 'shobj_', - 'lib_', - '_lib', - 'dll_', - '_dll' -]) +__all__.extend( + [ + 'TestSCons', + 'machine', + 'python', + '_exe', + '_obj', + '_shobj', + 'shobj_', + 'lib_', + '_lib', + 'dll_', + '_dll', + 'NINJA_BINARY', + ] +) machine_map = { 'i686': 'i386', @@ -104,14 +106,30 @@ _dll = dll_suffix dll_ = dll_prefix +try: + # Note: if the ninja python package is not installed, this import statement can end + # up finding the scons/test/ninja directory instead, and successfully importing + # that directory as an implicit namespace package. Therefore if ninja is + # unavailable, we may not get an ImportError here, but can instead get an + # AttributeError when attempting to access ninja.BIN_DIR below. This happens + # when running individual test files in the test/ directory, since the test/ + # directory will then be listed as the first entry in sys.path + import ninja + + NINJA_BINARY = os.path.abspath(os.path.join(ninja.BIN_DIR, 'ninja' + _exe)) +except (ImportError, AttributeError): + NINJA_BINARY = None + if sys.platform == 'cygwin': # On Cygwin, os.path.normcase() lies, so just report back the # fact that the underlying Win32 OS is case-insensitive. def case_sensitive_suffixes(s1, s2) -> int: return 0 else: + def case_sensitive_suffixes(s1, s2): - return (os.path.normcase(s1) != os.path.normcase(s2)) + return os.path.normcase(s1) != os.path.normcase(s2) + file_expr = r"""File "[^"]*", line \d+, in [^\n]+ """ @@ -129,8 +147,8 @@ def re_escape(str): # when searching for special strings in stdout/stderr. # def search_re(out, l): - """ Search the regular expression 'l' in the output 'out' - and return the start index when successful. + """Search the regular expression 'l' in the output 'out' + and return the start index when successful. """ m = re.search(l, out) if m: @@ -140,9 +158,9 @@ def search_re(out, l): def search_re_in_list(out, l): - """ Search the regular expression 'l' in each line of - the given string list 'out' and return the line's index - when successful. + """Search the regular expression 'l' in each line of + the given string list 'out' and return the line's index + when successful. """ for idx, o in enumerate(out): m = re.search(l, o) @@ -204,8 +222,7 @@ def initialize_sconsflags(ignore_python_version): # (The intended use case is to set it to null when running # timing tests of earlier versions of SCons which don't # support the --warn=no-visual-c-missing warning.) - visual_c = os.environ.get('TESTSCONS_SCONSFLAGS', - '--warn=no-visual-c-missing') + visual_c = os.environ.get('TESTSCONS_SCONSFLAGS', '--warn=no-visual-c-missing') if visual_c and visual_c not in sconsflags: sconsflags.append(visual_c) os.environ['SCONSFLAGS'] = ' '.join(sconsflags) @@ -220,18 +237,22 @@ def restore_sconsflags(sconsflags) -> None: # Helpers for Configure()'s config.log processing -ConfigCheckInfo = namedtuple('ConfigCheckInfo', - ['check_string', 'result', 'cached', 'temp_filename']) -# check_string: the string output to for this checker -# results : The expected results for each check -# cached : If the corresponding check is expected to be cached -# temp_filename : The name of the generated tempfile for this check +ConfigCheckInfo = namedtuple( + 'ConfigCheckInfo', + [ + 'check_string', # string output to for this checker + 'result', # expected results for each check + 'cached', # If the corresponding check is expected to be cached + 'temp_filename', # name of the generated tempfile for this check + ], +) class NoMatch(Exception): """ Exception for matchPart to indicate there was no match found in the passed logfile """ + def __init__(self, p) -> None: self.pos = p @@ -300,7 +321,7 @@ def __init__(self, **kw) -> None: elif not self.external and not os.path.isabs(kw['program']): kw['program'] = os.path.join(self.orig_cwd, kw['program']) if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'): - kw['interpreter'] = [python, ] + kw['interpreter'] = [python] if 'match' not in kw: kw['match'] = match_exact if 'workdir' not in kw: @@ -319,6 +340,7 @@ def __init__(self, **kw) -> None: if not self.external: import SCons.Node.FS + if SCons.Node.FS.default_fs is None: SCons.Node.FS.default_fs = SCons.Node.FS.FS() @@ -335,6 +357,7 @@ def Environment(self, ENV=None, *args, **kw): if not self.external: import SCons.Environment import SCons.Errors + if ENV is not None: kw['ENV'] = ENV try: @@ -411,12 +434,15 @@ def where_is(self, prog, path=None, pathext=None): return os.path.normpath(result) else: import SCons.Environment + env = SCons.Environment.Environment() return env.WhereIs(prog, path, pathext) return None - def wrap_stdout(self, build_str: str="", read_str: str="", error: int=0, cleaning: int=0) -> str: + def wrap_stdout( + self, build_str: str = "", read_str: str = "", error: int = 0, cleaning: int = 0 + ) -> str: """Wraps "expect" strings in SCons boilerplate. Given strings of expected output specific to a test, @@ -426,27 +452,28 @@ def wrap_stdout(self, build_str: str="", read_str: str="", error: int=0, cleanin Makes a complete message to match against. Args: - read_str: the message for the execution part of the output. + build_str: the message for the execution part of the output. If non-empty, needs to be newline-terminated. read_str: the message for the reading-sconscript part of the output. If non-empty, needs to be newline-terminated. error: if true, expect a fail message rather than a done message. - cleaning (int): index into type messages, if 0 selects + cleaning: index into type messages, if 0 selects build messages, if 1 selects clean messages. """ - cap, lc = [('Build', 'build'), - ('Clean', 'clean')][cleaning] + cap, lc = [('Build', 'build'), ('Clean', 'clean')][cleaning] if error: term = f"scons: {lc}ing terminated because of errors.\n" else: term = f"scons: done {lc}ing targets.\n" - return "scons: Reading SConscript files ...\n" + \ - read_str + \ - "scons: done reading SConscript files.\n" + \ - f"scons: {cap}ing targets ...\n" + \ - build_str + \ - term + return ( + "scons: Reading SConscript files ...\n" + + read_str + + "scons: done reading SConscript files.\n" + + f"scons: {cap}ing targets ...\n" + + build_str + + term + ) def run(self, *args, **kw) -> None: """ @@ -482,7 +509,7 @@ def run(self, *args, **kw) -> None: # kw['options'] = ' '.join(options) # TestCommon.run(self, *args, **kw) - def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: + def up_to_date(self, arguments: str = '.', read_str: str = "", **kw) -> None: """Asserts that all of the targets listed in arguments is up to date, but does not make any assumptions on other targets. This function is most useful in conjunction with the -n option. @@ -498,7 +525,7 @@ def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: kw['match'] = self.match_re_dotall self.run(**kw) - def not_up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: + def not_up_to_date(self, arguments: str = '.', read_str: str = "", **kw) -> None: """Asserts that none of the targets listed in arguments is up to date, but does not make any assumptions on other targets. This function is most useful in conjunction with the -n option. @@ -508,7 +535,9 @@ def not_up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: s = f"{s}(?!scons: `{re.escape(arg)}' is up to date.)" s = f"({s}[^\n]*\n)*" kw['arguments'] = arguments - stdout = re.escape(self.wrap_stdout(read_str=read_str, build_str='ARGUMENTSGOHERE')) + stdout = re.escape( + self.wrap_stdout(read_str=read_str, build_str='ARGUMENTSGOHERE') + ) kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s) kw['match'] = self.match_re_dotall self.run(**kw) @@ -549,11 +578,16 @@ def deprecated_fatal(self, warn, msg): TODO: Actually detect that it's now an error. We don't have any cases yet, so there's no way to test it. """ - self.write('SConstruct', """if True: - WARN = ARGUMENTS.get('WARN') - if WARN: SetOption('warn', WARN) - SConscript('SConscript') - """) + self.write( + 'SConstruct', + """\ +if True: + WARN = ARGUMENTS.get('WARN') + if WARN: + SetOption('warn', WARN) + SConscript('SConscript') +""", + ) def err_out(): # TODO calculate stderr for fatal error @@ -573,8 +607,9 @@ def err_out(): self.pass_test() else: # test failed; have to do this by hand... + stdout = self.stdout() or "" print(self.banner('STDOUT ')) - print(self.stdout()) + print(stdout) print(self.diff(warning, stderr, 'STDERR ')) self.fail_test() @@ -595,14 +630,18 @@ def deprecated_warning(self, warn, msg): def RunPair(option, expected) -> None: # run the same test with the option on the command line and # then with the option passed via SetOption(). - self.run(options=f"--warn={option}", - arguments='.', - stderr=expected, - match=match_re_dotall) - self.run(options=f"WARN={option}", - arguments='.', - stderr=expected, - match=match_re_dotall) + self.run( + options=f"--warn={option}", + arguments='.', + stderr=expected, + match=match_re_dotall, + ) + self.run( + options=f"WARN={option}", + arguments='.', + stderr=expected, + match=match_re_dotall, + ) # all warnings off, should get no output RunPair('no-deprecated', '') @@ -611,21 +650,23 @@ def RunPair(option, expected) -> None: RunPair(warn, warning) # warning disabled, should get either nothing or mandatory message - expect = f"""()|(Can not disable mandataory warning: 'no-{warn}'\n\n{warning})""" + expect = ( + f"""()|(Can not disable mandataory warning: 'no-{warn}'\n\n{warning})""" + ) RunPair(f"no-{warn}", expect) return warning - def diff_substr(self, expect, actual, prelen: int=20, postlen: int=40) -> str: + def diff_substr(self, expect, actual, prelen: int = 20, postlen: int = 40) -> str: i = 0 for x, y in zip(expect, actual): if x != y: - return "Actual did not match expect at char %d:\n" \ - " Expect: %s\n" \ - " Actual: %s\n" \ - % (i, repr(expect[i - prelen:i + postlen]), - repr(actual[i - prelen:i + postlen])) - i = i + 1 + return ( + f"Actual did not match expect at char {i}:\n" + f" Expect: {expect[i - prelen : i + postlen]!r}\n" + f" Actual: {actual[i - prelen : i + postlen]!r}\n" + ) + i += 1 return "Actual matched the expected output???" def python_file_line(self, file, line): @@ -656,19 +697,17 @@ def python_file_line(self, file, line): return x def normalize_ps(self, s): - s = re.sub(r'(Creation|Mod)Date: .*', - r'\1Date XXXX', s) - s = re.sub(r'%DVIPSSource:\s+TeX output\s.*', - r'%DVIPSSource: TeX output XXXX', s) - s = re.sub(r'/(BaseFont|FontName) /[A-Z0-9]{6}', - r'/\1 /XXXXXX', s) - s = re.sub(r'BeginFont: [A-Z0-9]{6}', - r'BeginFont: XXXXXX', s) + s = re.sub(r'(Creation|Mod)Date: .*', r'\1Date XXXX', s) + s = re.sub( + r'%DVIPSSource:\s+TeX output\s.*', r'%DVIPSSource: TeX output XXXX', s + ) + s = re.sub(r'/(BaseFont|FontName) /[A-Z0-9]{6}', r'/\1 /XXXXXX', s) + s = re.sub(r'BeginFont: [A-Z0-9]{6}', r'BeginFont: XXXXXX', s) return s @staticmethod - def to_bytes_re_sub(pattern, repl, string, count: int=0, flags: int=0): + def to_bytes_re_sub(pattern, repl, string, count: int = 0, flags: int = 0): """ Wrapper around re.sub to change pattern and repl to bytes to work with both python 2 & 3 @@ -678,14 +717,18 @@ def to_bytes_re_sub(pattern, repl, string, count: int=0, flags: int=0): return re.sub(pattern, repl, string, count=count, flags=flags) def normalize_pdf(self, s): - s = self.to_bytes_re_sub(r'/(Creation|Mod)Date \(D:[^)]*\)', - r'/\1Date (D:XXXX)', s) - s = self.to_bytes_re_sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]', - r'/ID [ ]', s) - s = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}', - r'/\1 /XXXXXX', s) - s = self.to_bytes_re_sub(r'/Length \d+ *\n/Filter /FlateDecode\n', - r'/Length XXXX\n/Filter /FlateDecode\n', s) + s = self.to_bytes_re_sub( + r'/(Creation|Mod)Date \(D:[^)]*\)', r'/\1Date (D:XXXX)', s + ) + s = self.to_bytes_re_sub( + r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]', r'/ID [ ]', s + ) + s = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}', r'/\1 /XXXXXX', s) + s = self.to_bytes_re_sub( + r'/Length \d+ *\n/Filter /FlateDecode\n', + r'/Length XXXX\n/Filter /FlateDecode\n', + s, + ) try: import zlib @@ -708,12 +751,19 @@ def normalize_pdf(self, s): for b, e in encoded: r.append(s[x:b]) d = zlib.decompress(s[b:e]) - d = self.to_bytes_re_sub(r'%%CreationDate: [^\n]*\n', - r'%%CreationDate: 1970 Jan 01 00:00:00\n', d) - d = self.to_bytes_re_sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d', - r'%DVIPSSource: TeX output 1970.01.01:0000', d) - d = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}', - r'/\1 /XXXXXX', d) + d = self.to_bytes_re_sub( + r'%%CreationDate: [^\n]*\n', + r'%%CreationDate: 1970 Jan 01 00:00:00\n', + d, + ) + d = self.to_bytes_re_sub( + r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d', + r'%DVIPSSource: TeX output 1970.01.01:0000', + d, + ) + d = self.to_bytes_re_sub( + r'/(BaseFont|FontName) /[A-Z]{6}', r'/\1 /XXXXXX', d + ) r.append(d) x = e r.append(s[x:]) @@ -723,13 +773,15 @@ def normalize_pdf(self, s): def paths(self, patterns): import glob + result = [] for p in patterns: result.extend(sorted(glob.glob(p))) return result - def get_sconsignname(self): + def get_sconsignname(self) -> str: """Get the scons database name used, and return both the prefix and full filename. + if the user left the options defaulted AND the default algorithm set by SCons is md5, then set the database name to be the special default name @@ -739,26 +791,36 @@ def get_sconsignname(self): Returns: a pair containing: the current dbname, the dbname.dblite filename + + TODO: docstring is not truthful about returning "both" - but which to fix? + Say it returns just basename, or return both? + TODO: has no way to account for an ``SConsignFile()`` call which might assign + a different dbname. Document that it's only useful for hash testing? """ hash_format = get_hash_format() current_hash_algorithm = get_current_hash_algorithm_used() if hash_format is None and current_hash_algorithm == 'md5': return ".sconsign" else: - database_prefix=f".sconsign_{current_hash_algorithm}" + database_prefix = f".sconsign_{current_hash_algorithm}" return database_prefix - - def unlink_sconsignfile(self, name: str='.sconsign.dblite') -> None: + def unlink_sconsignfile(self, name: str = '.sconsign.dblite') -> None: """Delete the sconsign file. + Provides a hook to do special things for the sconsign DB, + although currently it just calls unlink. + Args: name: expected name of sconsign file + + TODO: deal with suffix if :meth:`getsconsignname` does not provide it. + How do we know, since multiple formats are allowed? """ return self.unlink(name) - def java_ENV(self, version=None): - """ Initialize JAVA SDK environment. + def java_ENV(self, version: str | None = None): + """Initialize JAVA SDK environment. Initialize with a default external environment that uses a local Java SDK in preference to whatever's found in the default PATH. @@ -778,6 +840,7 @@ def java_ENV(self, version=None): pass import SCons.Environment + env = SCons.Environment.Environment() self._java_env[version] = env @@ -811,8 +874,8 @@ def java_ENV(self, version=None): return None - def java_where_includes(self, version=None): - """ Find include path needed for compiling java jni code. + def java_where_includes(self, version: str | None = None) -> list[str] | None: + """Find include path needed for compiling java jni code. Args: version: if set, match only that version @@ -831,14 +894,22 @@ def java_where_includes(self, version=None): if not version: version = '' - jni_dirs = ['/System/Library/Frameworks/JavaVM.framework/Headers/jni.h', - '/usr/lib/jvm/default-java/include/jni.h', - '/usr/lib/jvm/java-*-oracle/include/jni.h'] + jni_dirs = [ + '/System/Library/Frameworks/JavaVM.framework/Headers/jni.h', + '/usr/lib/jvm/default-java/include/jni.h', + '/usr/lib/jvm/java-*-oracle/include/jni.h', + ] else: - jni_dirs = [f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}*/Headers/jni.h'] - jni_dirs.extend([f'/usr/lib/jvm/java-*-sun-{version}*/include/jni.h', - f'/usr/lib/jvm/java-{version}*-openjdk*/include/jni.h', - f'/usr/java/jdk{version}*/include/jni.h']) + jni_dirs = [ + f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}*/Headers/jni.h' + ] + jni_dirs.extend( + [ + f'/usr/lib/jvm/java-*-sun-{version}*/include/jni.h', + f'/usr/lib/jvm/java-{version}*-openjdk*/include/jni.h', + f'/usr/java/jdk{version}*/include/jni.h', + ] + ) dirs = self.paths(jni_dirs) if not dirs: return None @@ -851,8 +922,8 @@ def java_where_includes(self, version=None): result.append(os.path.join(d, 'linux')) return result - def java_where_java_home(self, version=None) -> str: - """ Find path to what would be JAVA_HOME. + def java_where_java_home(self, version: str | None = None) -> str | None: + """Find path to what would be JAVA_HOME. SCons does not read JAVA_HOME from the environment, so deduce it. @@ -878,7 +949,7 @@ def java_where_java_home(self, version=None) -> str: for home in [ '/System/Library/Frameworks/JavaVM.framework/Home', # osx 10.10 - '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home' + '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home', ]: if os.path.exists(home): return home @@ -888,7 +959,7 @@ def java_where_java_home(self, version=None) -> str: for home in [ f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}/Home', # osx 10.10 - '/System/Library/Frameworks/JavaVM.framework/Versions/Current/' + '/System/Library/Frameworks/JavaVM.framework/Versions/Current/', ]: if os.path.exists(home): return home @@ -905,6 +976,7 @@ def java_where_java_home(self, version=None) -> str: "Could not run Java: unable to detect valid JAVA_HOME, skipping test.\n", from_fw=True, ) + return None def java_mac_check(self, where_java_bin, java_bin_name) -> None: """Extra check for Java on MacOS. @@ -925,8 +997,8 @@ def java_mac_check(self, where_java_bin, java_bin_name) -> None: from_fw=True, ) - def java_where_jar(self, version=None) -> str: - """ Find java archiver jar. + def java_where_jar(self, version: str | None = None) -> str: + """Find java archiver jar. Args: version: if set, match only that version @@ -946,8 +1018,8 @@ def java_where_jar(self, version=None) -> str: return where_jar - def java_where_java(self, version=None) -> str: - """ Find java executable. + def java_where_java(self, version: str | None = None) -> str: + """Find java executable. Args: version: if set, match only that version @@ -959,14 +1031,16 @@ def java_where_java(self, version=None) -> str: where_java = self.where_is('java', ENV['PATH']) if not where_java: - self.skip_test("Could not find Java java, skipping test(s).\n", from_fw=True) + self.skip_test( + "Could not find Java java, skipping test(s).\n", from_fw=True + ) elif sys.platform == "darwin": self.java_mac_check(where_java, 'java') return where_java - def java_where_javac(self, version=None) -> str: - """ Find java compiler. + def java_where_javac(self, version: str | None = None) -> tuple[str, str]: + """Find java compiler. Args: version: if set, match only that version @@ -980,30 +1054,33 @@ def java_where_javac(self, version=None) -> str: else: where_javac = self.where_is('javac', ENV['PATH']) if not where_javac: - self.skip_test("Could not find Java javac, skipping test(s).\n", from_fw=True) + self.skip_test( + "Could not find Java javac, skipping test(s).\n", from_fw=True + ) elif sys.platform == "darwin": self.java_mac_check(where_javac, 'javac') - self.run(program=where_javac, - arguments='-version', - stderr=None, - status=None) + self.run(program=where_javac, arguments='-version', stderr=None, status=None) # Note recent versions output version info to stdout instead of stderr + stdout = self.stdout() or "" + stderr = self.stderr() or "" if version: verf = f'javac {version}' - if self.stderr().find(verf) == -1 and self.stdout().find(verf) == -1: + if stderr.find(verf) == -1 and stdout.find(verf) == -1: fmt = "Could not find javac for Java version %s, skipping test(s).\n" self.skip_test(fmt % version, from_fw=True) else: version_re = r'javac (\d*\.*\d)' - m = re.search(version_re, self.stderr()) + m = re.search(version_re, stderr) if not m: - m = re.search(version_re, self.stdout()) + m = re.search(version_re, stdout) if m: version = m.group(1) self.javac_is_gcj = False - elif self.stderr().find('gcj') != -1: + return where_javac, version + + if stderr.find('gcj') != -1: version = '1.2' self.javac_is_gcj = True else: @@ -1011,8 +1088,8 @@ def java_where_javac(self, version=None) -> str: self.javac_is_gcj = False return where_javac, version - def java_where_javah(self, version=None) -> str: - """ Find java header generation tool. + def java_where_javah(self, version: str | None = None) -> str: + """Find java header generation tool. TODO issue #3347 since JDK10, there is no separate javah command, 'javac -h' is used. We should not return a javah from a different @@ -1030,11 +1107,13 @@ def java_where_javah(self, version=None) -> str: else: where_javah = self.where_is('javah', ENV['PATH']) if not where_javah: - self.skip_test("Could not find Java javah, skipping test(s).\n", from_fw=True) + self.skip_test( + "Could not find Java javah, skipping test(s).\n", from_fw=True + ) return where_javah - def java_where_rmic(self, version=None) -> str: - """ Find java rmic tool. + def java_where_rmic(self, version: str | None = None) -> str: + """Find java rmic tool. Args: version: if set, match only that version @@ -1048,7 +1127,10 @@ def java_where_rmic(self, version=None) -> str: else: where_rmic = self.where_is('rmic', ENV['PATH']) if not where_rmic: - self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n", from_fw=True) + self.skip_test( + "Could not find Java rmic, skipping non-simulated test(s).\n", + from_fw=True, + ) return where_rmic def java_get_class_files(self, dir): @@ -1059,23 +1141,29 @@ def java_get_class_files(self, dir): result.append(os.path.join(dirpath, fname)) return sorted(result) - def Qt_dummy_installation(self, dir: str='qt') -> None: + def Qt_dummy_installation(self, dir: str = 'qt') -> None: # create a dummy qt installation self.subdir(dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib']) - self.write([dir, 'bin', 'mymoc.py'], """\ + self.write( + [dir, 'bin', 'mymoc.py'], + """\ import getopt import sys import re + # -w and -z are fake options used in test/QT/QTFLAGS.py cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', []) impl = 0 opt_string = '' for opt, arg in cmd_opts: - if opt == '-o': outfile = arg - elif opt == '-i': impl = 1 - else: opt_string = opt_string + ' ' + opt + if opt == '-o': + outfile = arg + elif opt == '-i': + impl = 1 + else: + opt_string = opt_string + ' ' + opt with open(outfile, 'w') as ofp: ofp.write("/* mymoc.py%s */\\n" % opt_string) @@ -1088,12 +1176,16 @@ def Qt_dummy_installation(self, dir: str='qt') -> None: contents = re.sub(r'#include.*', '', contents) ofp.write(contents.replace('Q_OBJECT', subst)) sys.exit(0) -""") +""", + ) - self.write([dir, 'bin', 'myuic.py'], """\ + self.write( + [dir, 'bin', 'myuic.py'], + """\ import os.path import re import sys + output_arg = 0 impl_arg = 0 impl = None @@ -1129,22 +1221,31 @@ def Qt_dummy_installation(self, dir: str='qt') -> None: else: ofp.write('#include "my_qobject.h"\\n' + ifp.read() + " Q_OBJECT \\n") sys.exit(0) -""") +""", + ) - self.write([dir, 'include', 'my_qobject.h'], r""" + self.write( + [dir, 'include', 'my_qobject.h'], + """\ #define Q_OBJECT ; void my_qt_symbol(const char *arg); -""") +""", + ) - self.write([dir, 'lib', 'my_qobject.cpp'], r""" + self.write( + [dir, 'lib', 'my_qobject.cpp'], + """\ #include "../include/my_qobject.h" #include void my_qt_symbol(const char *arg) { fputs(arg, stdout); } -""") +""", + ) - self.write([dir, 'lib', 'SConstruct'], r""" + self.write( + [dir, 'lib', 'SConstruct'], + r""" import sys DefaultEnvironment(tools=[]) # test speedup env = Environment() @@ -1152,12 +1253,15 @@ def Qt_dummy_installation(self, dir: str='qt') -> None: env.StaticLibrary('myqt', 'my_qobject.cpp') else: env.SharedLibrary('myqt', 'my_qobject.cpp') -""") +""", + ) - self.run(chdir=self.workpath(dir, 'lib'), - arguments='.', - stderr=noisy_ar, - match=self.match_re_dotall) + self.run( + chdir=self.workpath(dir, 'lib'), + arguments='.', + stderr=noisy_ar, + match=self.match_re_dotall, + ) self.QT = self.workpath(dir) self.QT_LIB = 'myqt' @@ -1165,20 +1269,25 @@ def Qt_dummy_installation(self, dir: str='qt') -> None: self.QT_UIC = f"{_python_} {self.workpath(dir, 'bin', 'myuic.py')}" self.QT_LIB_DIR = self.workpath(dir, 'lib') - def Qt_create_SConstruct(self, place, qt_tool: str='qt3') -> None: + def Qt_create_SConstruct(self, place, qt_tool: str = 'qt3') -> None: if isinstance(place, list): - place = test.workpath(*place) + place = self.workpath(*place) - var_prefix=qt_tool.upper() - self.write(place, f"""\ + var_prefix = qt_tool.upper() + self.write( + place, + f"""\ if ARGUMENTS.get('noqtdir', 0): {var_prefix}DIR = None else: {var_prefix}DIR = r'{self.QT}' DefaultEnvironment(tools=[]) # test speedup env = Environment( - {var_prefix}DIR={var_prefix}DIR, {var_prefix}_LIB=r'{self.QT_LIB}', {var_prefix}_MOC=r'{self.QT_MOC}', - {var_prefix}_UIC=r'{self.QT_UIC}', tools=['default', '{qt_tool}'] + {var_prefix}DIR={var_prefix}DIR, + {var_prefix}_LIB=r'{self.QT_LIB}', + {var_prefix}_MOC=r'{self.QT_MOC}', + {var_prefix}_UIC=r'{self.QT_UIC}', + tools=['default', '{qt_tool}'], ) dup = 1 if ARGUMENTS.get('variant_dir', 0): @@ -1199,7 +1308,8 @@ def Qt_create_SConstruct(self, place, qt_tool: str='qt3') -> None: sconscript = File('SConscript') Export("env dup") SConscript(sconscript) -""") +""", + ) NCR = 0 # non-cached rebuild CR = 1 # cached rebuild (up to date) @@ -1215,12 +1325,11 @@ def Qt_create_SConstruct(self, place, qt_tool: str='qt3') -> None: # Configure_lib = 'm' def coverage_run(self) -> bool: - """ Check if the the tests are being run under coverage. - """ + """Check if the the tests are being run under coverage.""" return 'COVERAGE_PROCESS_START' in os.environ or 'COVERAGE_FILE' in os.environ - def skip_if_not_msvc(self, check_platform: bool=True) -> None: - """ Skip test if MSVC is not available. + def skip_if_not_msvc(self, check_platform: bool = True) -> None: + """Skip test if MSVC is not available. Check whether we are on a Windows platform and skip the test if not. This check can be omitted by setting check_platform to False. @@ -1237,18 +1346,23 @@ def skip_if_not_msvc(self, check_platform: bool=True) -> None: try: import SCons.Tool.MSCommon as msc + if not msc.msvc_exists(): msg = "No MSVC toolchain found...skipping test\n" self.skip_test(msg, from_fw=True) except Exception: pass - def checkConfigureLogAndStdout(self, checks, - logfile: str='config.log', - sconf_dir: str='.sconf_temp', - sconstruct: str="SConstruct", - doCheckLog: bool=True, doCheckStdout: bool=True): - """ Verify expected output from Configure. + def checkConfigureLogAndStdout( + self, + checks, + logfile: str = 'config.log', + sconf_dir: str = '.sconf_temp', + sconstruct: str = "SConstruct", + doCheckLog: bool = True, + doCheckStdout: bool = True, + ): + """Verify expected output from Configure. Used to verify the expected output from using Configure() via the contents of one or both of stdout or config.log file. @@ -1277,8 +1391,13 @@ def checkConfigureLogAndStdout(self, checks, # Some debug code to keep around.. # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile)) - if (doCheckLog and - logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0): + if ( + doCheckLog + and logfile.find( + "scons: warning: The stored build information has an unexpected class." + ) + >= 0 + ): self.fail_test() log = r'file \S*%s\,line \d+:' % re.escape(sconstruct) + ls @@ -1301,7 +1420,7 @@ def checkConfigureLogAndStdout(self, checks, result_cached = 1 for bld_desc in check_info.cached: # each TryXXX for ext, flag in bld_desc: # each file in TryBuild - conf_filename = re.escape(check_info.temp_filename%ext) + conf_filename = re.escape(check_info.temp_filename % ext) if flag == self.NCR: # NCR = Non Cached Rebuild @@ -1315,12 +1434,21 @@ def checkConfigureLogAndStdout(self, checks, if flag == self.CR: # CR = cached rebuild (up to date)s # up to date - log = log + \ - re.escape("scons: Configure: \"") + \ - conf_filename + \ - re.escape("\" is up to date.") + ls - log = log + re.escape("scons: Configure: The original builder " - "output was:") + ls + log = ( + log + + re.escape("scons: Configure: \"") + + conf_filename + + re.escape("\" is up to date.") + + ls + ) + log = ( + log + + re.escape( + "scons: Configure: The original builder " + "output was:" + ) + + ls + ) log = f"{log}( \\|.*{ls})+" if flag == self.NCF: # non-cached rebuild failure @@ -1328,11 +1456,22 @@ def checkConfigureLogAndStdout(self, checks, result_cached = 0 if flag == self.CF: # cached rebuild failure - log = log + \ - re.escape("scons: Configure: Building \"") + \ - conf_filename + \ - re.escape("\" failed in a previous run and all its sources are up to date.") + ls - log = log + re.escape("scons: Configure: The original builder output was:") + ls + log = ( + log + + re.escape("scons: Configure: Building \"") + + conf_filename + + re.escape( + "\" failed in a previous run and all its sources are up to date." + ) + + ls + ) + log = ( + log + + re.escape( + "scons: Configure: The original builder output was:" + ) + + ls + ) log = f"{log}( \\|.*{ls})+" if result_cached: result = f"(cached) {check_info.result}" @@ -1356,7 +1495,7 @@ def checkConfigureLogAndStdout(self, checks, print("Cannot match log file against log regexp.") print("log file: ") print("------------------------------------------------------") - print(logfile[m.pos:]) + print(logfile[m.pos :]) print("------------------------------------------------------") print("log regexp: ") print("------------------------------------------------------") @@ -1366,21 +1505,28 @@ def checkConfigureLogAndStdout(self, checks, if doCheckStdout: exp_stdout = self.wrap_stdout(".*", rdstr) - if not self.match_re_dotall(self.stdout(), exp_stdout): + stdout = self.stdout() or "" + if not self.match_re_dotall(stdout, exp_stdout): print("Unexpected stdout: ") print("-----------------------------------------------------") - print(repr(self.stdout())) + print(repr(stdout)) print("-----------------------------------------------------") print(repr(exp_stdout)) print("-----------------------------------------------------") self.fail_test() - - - def checkLogAndStdout(self, checks, results, cached, - logfile, sconf_dir, sconstruct, - doCheckLog: bool=True, doCheckStdout: bool=True): - """ Verify expected output from Configure. + def checkLogAndStdout( + self, + checks, + results, + cached, + logfile, + sconf_dir, + sconstruct, + doCheckLog: bool = True, + doCheckStdout: bool = True, + ): + """Verify expected output from Configure. Used to verify the expected output from using Configure() via the contents of one or both of stdout or config.log file. @@ -1401,7 +1547,6 @@ def checkLogAndStdout(self, checks, results, cached, doCheckStdout: Check stdout, defaults to true """ try: - ls = '\n' nols = '([^\n])' lastEnd = 0 @@ -1412,8 +1557,13 @@ def checkLogAndStdout(self, checks, results, cached, # Some debug code to keep around.. # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile)) - if (doCheckLog and - logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0): + if ( + doCheckLog + and logfile.find( + "scons: warning: The stored build information has an unexpected class." + ) + >= 0 + ): self.fail_test() sconf_dir = sconf_dir @@ -1441,23 +1591,31 @@ def checkLogAndStdout(self, checks, results, cached, for bld_desc in cache_desc: # each TryXXX for ext, flag in bld_desc: # each file in TryBuild if ext in ['.c', '.cpp']: - conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\ - r'_[a-z0-9]{32,64}_\d+%s' % re.escape(ext) + conf_filename = re.escape( + os.path.join(sconf_dir, "conftest") + ) + r'_[a-z0-9]{32,64}_\d+%s' % re.escape(ext) elif ext == '': - conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\ - r'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?' + conf_filename = ( + re.escape(os.path.join(sconf_dir, "conftest")) + + r'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?' + ) else: - # We allow the second hash group to be optional because - # TryLink() will create a c file, then compile to obj, then link that - # The intermediate object file will not get the action hash - # But TryCompile()'s where the product is the .o will get the - # action hash. Rather than add a ton of complications to this logic - # this shortcut should be sufficient. - # TODO: perhaps revisit and/or fix file naming for intermediate files in - # Configure context logic - conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\ - r'_[a-z0-9]{32,64}_\d+(_[a-z0-9]{32,64})?%s' % re.escape(ext) + # We allow the second hash group to be optional + # because TryLink() will create a c file, then + # compile to obj, then link that The intermediate + # object file will not get the action hash But + # TryCompile()'s where the product is the .o will + # get the action hash. Rather than add a ton of + # complications to this logic this shortcut should + # be sufficient. TODO: perhaps revisit and/or fix + # file naming for intermediate files in Configure + # context logic + conf_filename = ( + re.escape(os.path.join(sconf_dir, "conftest")) + + r'_[a-z0-9]{32,64}_\d+(_[a-z0-9]{32,64})?%s' + % re.escape(ext) + ) if flag == self.NCR: # NCR = Non Cached Rebuild @@ -1471,12 +1629,21 @@ def checkLogAndStdout(self, checks, results, cached, if flag == self.CR: # CR = cached rebuild (up to date)s # up to date - log = log + \ - re.escape("scons: Configure: \"") + \ - conf_filename + \ - re.escape("\" is up to date.") + ls - log = log + re.escape("scons: Configure: The original builder " - "output was:") + ls + log = ( + log + + re.escape("scons: Configure: \"") + + conf_filename + + re.escape("\" is up to date.") + + ls + ) + log = ( + log + + re.escape( + "scons: Configure: The original builder " + "output was:" + ) + + ls + ) log = f"{log}( \\|.*{ls})+" if flag == self.NCF: # non-cached rebuild failure @@ -1484,11 +1651,22 @@ def checkLogAndStdout(self, checks, results, cached, result_cached = 0 if flag == self.CF: # cached rebuild failure - log = log + \ - re.escape("scons: Configure: Building \"") + \ - conf_filename + \ - re.escape("\" failed in a previous run and all its sources are up to date.") + ls - log = log + re.escape("scons: Configure: The original builder output was:") + ls + log = ( + log + + re.escape("scons: Configure: Building \"") + + conf_filename + + re.escape( + "\" failed in a previous run and all its sources are up to date." + ) + + ls + ) + log = ( + log + + re.escape( + "scons: Configure: The original builder output was:" + ) + + ls + ) log = f"{log}( \\|.*{ls})+" # cnt = cnt + 1 if result_cached: @@ -1512,7 +1690,7 @@ def checkLogAndStdout(self, checks, results, cached, print("Cannot match log file against log regexp.") print("log file: ") print("------------------------------------------------------") - print(logfile[m.pos:]) + print(logfile[m.pos :]) print("------------------------------------------------------") print("log regexp: ") print("------------------------------------------------------") @@ -1522,17 +1700,18 @@ def checkLogAndStdout(self, checks, results, cached, if doCheckStdout: exp_stdout = self.wrap_stdout(".*", rdstr) - if not self.match_re_dotall(self.stdout(), exp_stdout): + stdout = self.stdout() or "" + if not self.match_re_dotall(stdout, exp_stdout): print("Unexpected stdout: ") print("----Actual-------------------------------------------") - print(repr(self.stdout())) + print(repr(stdout)) print("----Expected-----------------------------------------") print(repr(exp_stdout)) print("-----------------------------------------------------") self.fail_test() def get_python_version(self) -> str: - """ Returns the Python version. + """Returns the Python version. Convenience function so everyone doesn't have to hand-code slicing the right number of characters @@ -1540,7 +1719,7 @@ def get_python_version(self) -> str: # see also sys.prefix documentation return python_minor_version_string() - def get_platform_python_info(self, python_h_required: bool=False): + def get_platform_python_info(self, python_h_required: bool = False): """Return information about Python. Returns a path to a Python executable suitable for testing on @@ -1555,13 +1734,17 @@ def get_platform_python_info(self, python_h_required: bool=False): """ python = os.environ.get('python_executable', self.where_is('python')) if not python: - self.skip_test('Can not find installed "python", skipping test.\n', from_fw=True) + self.skip_test( + 'Can not find installed "python", skipping test.\n', from_fw=True + ) # construct a program to run in the intended environment # in order to fetch the characteristics of that Python. # Windows Python doesn't store all the info in config vars. if sys.platform == 'win32': - self.run(program=python, stdin="""\ + self.run( + program=python, + stdin="""\ import sysconfig, sys, os.path py_ver = 'python%d%d' % sys.version_info[:2] try: @@ -1593,9 +1776,12 @@ def venv_path(): print(Python_h) else: print("False") -""") +""", + ) else: - self.run(program=python, stdin="""\ + self.run( + program=python, + stdin="""\ import sys, sysconfig, os.path include = sysconfig.get_config_var("INCLUDEPY") print(include) @@ -1609,10 +1795,14 @@ def venv_path(): print(Python_h) else: print("False") -""") - incpath, libpath, libname, python_h = self.stdout().strip().split('\n') +""", + ) + stdout = self.stdout() or "" + incpath, libpath, libname, python_h = stdout.strip().split('\n') if python_h == "False" and python_h_required: - self.skip_test('Can not find required "Python.h", skipping test.\n', from_fw=True) + self.skip_test( + 'Can not find required "Python.h", skipping test.\n', from_fw=True + ) return (python, incpath, libpath, libname + _lib) @@ -1633,7 +1823,7 @@ def start(self, *args, **kw): restore_sconsflags(sconsflags) return p - def wait_for(self, fname, timeout: float=20.0, popen=None) -> None: + def wait_for(self, fname, timeout: float = 60.0, popen=None) -> None: """ Waits for the specified file name to exist. """ @@ -1705,24 +1895,34 @@ def __init__(self, name, units, expression, convert=None) -> None: StatList = [ - Stat('memory-initial', 'kbytes', - r'Memory before reading SConscript files:\s+(\d+)', - convert=lambda s: int(s) // 1024), - Stat('memory-prebuild', 'kbytes', - r'Memory before building targets:\s+(\d+)', - convert=lambda s: int(s) // 1024), - Stat('memory-final', 'kbytes', - r'Memory after building targets:\s+(\d+)', - convert=lambda s: int(s) // 1024), - - Stat('time-sconscript', 'seconds', - r'Total SConscript file execution time:\s+([\d.]+) seconds'), - Stat('time-scons', 'seconds', - r'Total SCons execution time:\s+([\d.]+) seconds'), - Stat('time-commands', 'seconds', - r'Total command execution time:\s+([\d.]+) seconds'), - Stat('time-total', 'seconds', - r'Total build time:\s+([\d.]+) seconds'), + Stat( + 'memory-initial', + 'kbytes', + r'Memory before reading SConscript files:\s+(\d+)', + convert=lambda s: int(s) // 1024, + ), + Stat( + 'memory-prebuild', + 'kbytes', + r'Memory before building targets:\s+(\d+)', + convert=lambda s: int(s) // 1024, + ), + Stat( + 'memory-final', + 'kbytes', + r'Memory after building targets:\s+(\d+)', + convert=lambda s: int(s) // 1024, + ), + Stat( + 'time-sconscript', + 'seconds', + r'Total SConscript file execution time:\s+([\d.]+) seconds', + ), + Stat('time-scons', 'seconds', r'Total SCons execution time:\s+([\d.]+) seconds'), + Stat( + 'time-commands', 'seconds', r'Total command execution time:\s+([\d.]+) seconds' + ), + Stat('time-total', 'seconds', r'Total build time:\s+([\d.]+) seconds'), ] @@ -1737,7 +1937,7 @@ def __init__(self, *args, **kw) -> None: directory containing the executing script to the temporary working directory. """ - self.variables = kw.get('variables') + self.variables: dict = kw.get('variables') default_calibrate_variables = [] if self.variables is not None: for variable, value in self.variables.items(): @@ -1817,11 +2017,7 @@ def trace(self, graph, name, value, units, sort=None) -> None: sys.stdout.flush() def report_traces(self, trace, stats) -> None: - self.trace('TimeSCons-elapsed', - trace, - self.elapsed_time(), - "seconds", - sort=0) + self.trace('TimeSCons-elapsed', trace, self.elapsed_time(), "seconds", sort=0) for name, args in stats.items(): self.trace(name, trace, **args) @@ -1872,8 +2068,9 @@ def startup(self, *args, **kw) -> None: # the full and null builds. kw['status'] = None self.run(*args, **kw) - sys.stdout.write(self.stdout()) - stats = self.collect_stats(self.stdout()) + stdout = self.stdout() or "" + sys.stdout.write(stdout) + stats = self.collect_stats(stdout) # Delete the time-commands, since no commands are ever # executed on the help run and it is (or should be) always 0.0. del stats['time-commands'] @@ -1885,8 +2082,9 @@ def full(self, *args, **kw) -> None: """ self.add_timing_options(kw) self.run(*args, **kw) - sys.stdout.write(self.stdout()) - stats = self.collect_stats(self.stdout()) + stdout = self.stdout() or "" + sys.stdout.write(stdout) + stats = self.collect_stats(stdout) self.report_traces('full', stats) self.trace('full-memory', 'initial', **stats['memory-initial']) self.trace('full-memory', 'prebuild', **stats['memory-prebuild']) @@ -1917,8 +2115,9 @@ def null(self, *args, **kw) -> None: # SConscript:/private/var/folders/ng/48pttrpj239fw5rmm3x65pxr0000gn/T/testcmd.12081.pk1bv5i5/SConstruct took 533.646 ms read_str = 'SConscript:.*\n' self.up_to_date(arguments='.', read_str=read_str, **kw) - sys.stdout.write(self.stdout()) - stats = self.collect_stats(self.stdout()) + stdout = self.stdout() or "" + sys.stdout.write(stdout) + stats = self.collect_stats(stdout) # time-commands should always be 0.0 on a null build, because # no commands should be executed. Remove it from the stats # so we don't trace it, but only if it *is* 0 so that we'll @@ -1978,7 +2177,7 @@ def copy_timing_configuration(self, source_dir, dest_dir) -> None: destination = source.replace(source_dir, dest_dir) shutil.copy2(source, destination) - def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: + def up_to_date(self, arguments: str = '.', read_str: str = "", **kw) -> None: """Asserts that all of the targets listed in arguments is up to date, but does not make any assumptions on other targets. This function is most useful in conjunction with the -n option. @@ -1999,7 +2198,6 @@ def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None: self.run(**kw) - # In some environments, $AR will generate a warning message to stderr # if the library doesn't previously exist and is being created. One # way to fix this is to tell AR to be quiet (sometimes the 'c' flag), diff --git a/testing/framework/TestSConsMSVS.py b/testing/framework/TestSConsMSVS.py index 8c34372b85..88026e141b 100644 --- a/testing/framework/TestSConsMSVS.py +++ b/testing/framework/TestSConsMSVS.py @@ -40,6 +40,7 @@ import platform import traceback from xml.etree import ElementTree + try: import winreg except ImportError: @@ -50,6 +51,13 @@ from TestSCons import __all__ +PROJECT_GUID = "{00000000-0000-0000-0000-000000000000}" +PROJECT_GUID_1 = "{11111111-1111-1111-1111-111111111111}" +PROJECT_GUID_2 = "{22222222-2222-2222-2222-222222222222}" + +SOLUTION_GUID_1 = "{88888888-8888-8888-8888-888888888888}" +SOLUTION_GUID_2 = "{99999999-9999-9999-9999-999999999999}" + expected_dspfile_6_0 = '''\ # Microsoft Developer Studio Project File - Name="Test" - Package Owner=<4> # Microsoft Developer Studio Generated Build File, Format Version 6.00 @@ -189,8 +197,9 @@ ''' SConscript_contents_6_0 = """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='6.0',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='6.0', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test.c'] testincs = ['sdk.h'] @@ -209,7 +218,6 @@ """ - expected_slnfile_7_0 = """\ Microsoft Visual Studio Solution File, Format Version 7.00 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "" @@ -310,8 +318,9 @@ """ SConscript_contents_7_0 = """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='7.0',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='7.0', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -320,6 +329,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID = '%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -331,7 +341,6 @@ """ - expected_slnfile_7_1 = """\ Microsoft Visual Studio Solution File, Format Version 8.00 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "Test.vcproj", "" @@ -436,8 +445,9 @@ """ SConscript_contents_7_1 = """\ -env=Environment(platform='win32', tools=['msvs'], - MSVS_VERSION='7.1',HOST_ARCH='%(HOST_ARCH)s') +env=Environment(tools=['msvs'], + MSVS_VERSION='7.1', + HOST_ARCH='%(HOST_ARCH)s') testsrc = ['test1.cpp', 'test2.cpp'] testincs = ['sdk.h'] @@ -446,6 +456,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = 'Test.vcproj', + MSVS_PROJECT_GUID = '%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -482,9 +493,9 @@ \tKeyword="MakeFileProj"> \t @@ -502,10 +513,10 @@ \t\t\t> \t\t\t \t \t -\t\t{39A97E1F-1A52-8954-A0B1-A10A8487545E} +\t\t%(PROJECT_GUID)s -\t\tTest +\t\t%(PROJECT_BASENAME)s \t\tMakeFileProj \t\tNoUpgrade \t @@ -585,7 +596,7 @@ \t \t\tMakefile \t\tfalse -\t\tv100 +\t\t%(PLATFORM_TOOLSET)s \t \t \t @@ -596,15 +607,16 @@ \t \t \t<_ProjectFileVersion>10.0.30319.1 -\t\techo Starting SCons && "" -c "" -C "" -f SConstruct "Test.exe" -\t\techo Starting SCons && "" -c "" -C "" -f SConstruct "Test.exe" -\t\techo Starting SCons && "" -c "" -C "" -f SConstruct -c "Test.exe" -\t\tTest.exe +\t\techo Starting SCons && "" -c "" -C "" -f SConstruct "%(PROJECT_BASENAME)s.exe" +\t\techo Starting SCons && "" -c "" -C "" -f SConstruct "%(PROJECT_BASENAME)s.exe" +\t\techo Starting SCons && "" -c "" -C "" -f SConstruct -c "%(PROJECT_BASENAME)s.exe" +\t\t%(PROJECT_BASENAME)s.exe \t\tDEF1;DEF2;DEF3=1234 \t\t%(INCLUDE_DIRS)s \t\t$(NMakeForcedIncludes) \t\t$(NMakeAssemblySearchPath) \t\t$(NMakeForcedUsingAssemblies) +\t\t \t \t \t\t @@ -632,7 +644,8 @@ """ SConscript_contents_fmt = """\ -env=Environment(platform='win32', tools=['msvs'], MSVS_VERSION='%(MSVS_VERSION)s', +env=Environment(tools=['msvs'], + MSVS_VERSION='%(MSVS_VERSION)s', CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], CPPPATH=['inc1', 'inc2'], HOST_ARCH='%(HOST_ARCH)s') @@ -644,6 +657,7 @@ testmisc = ['readme.txt'] env.MSVSProject(target = '%(PROJECT_FILE)s', + MSVS_PROJECT_GUID = '%(PROJECT_GUID)s', slnguid = '{SLNGUID}', srcs = testsrc, incs = testincs, @@ -654,6 +668,162 @@ variant = 'Release') """ +expected_projects_slnfile_fmt = """\ +Microsoft Visual Studio Solution File, Format Version %(FORMAT_VERSION)s +# Visual Studio %(VS_NUMBER)s +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(PROJECT_NAME_1)s", "%(PROJECT_FILE_1)s", "" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(PROJECT_NAME_2)s", "%(PROJECT_FILE_2)s", "" +EndProject +Global + +\tGlobalSection(SolutionConfigurationPlatforms) = preSolution +\t\tRelease|Win32 = Release|Win32 +\tEndGlobalSection +\tGlobalSection(ProjectConfigurationPlatforms) = postSolution +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\tEndGlobalSection +\tGlobalSection(SolutionProperties) = preSolution +\t\tHideSolutionNode = FALSE +\tEndGlobalSection +EndGlobal +""" + +expected_projects_slnfile_fmt_slnnodes = """\ +Microsoft Visual Studio Solution File, Format Version %(FORMAT_VERSION)s +# Visual Studio %(VS_NUMBER)s +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(PROJECT_NAME_1)s", "%(PROJECT_FILE_1)s", "" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(SOLUTION_FILE_1)s", "%(SOLUTION_FILE_1)s", "" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(PROJECT_NAME_2)s", "%(PROJECT_FILE_2)s", "" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%(SOLUTION_FILE_2)s", "%(SOLUTION_FILE_2)s", "" +EndProject +Global + +\tGlobalSection(SolutionConfigurationPlatforms) = preSolution +\t\tRelease|Win32 = Release|Win32 +\tEndGlobalSection +\tGlobalSection(ProjectConfigurationPlatforms) = postSolution +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\t\t.Release|Win32.ActiveCfg = Release|Win32 +\t\t.Release|Win32.Build.0 = Release|Win32 +\tEndGlobalSection +\tGlobalSection(SolutionProperties) = preSolution +\t\tHideSolutionNode = FALSE +\tEndGlobalSection +EndGlobal +""" + +SConscript_projects_contents_fmt = """\ +env=Environment( + tools=['msvs'], + MSVS_VERSION='%(MSVS_VERSION)s', + CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], + CPPPATH=['inc1', 'inc2'], + HOST_ARCH='%(HOST_ARCH)s', +) + +testsrc = ['test1.cpp', 'test2.cpp'] +testincs = [r'sdk_dir\\sdk.h'] +testlocalincs = ['test.h'] +testresources = ['test.rc'] +testmisc = ['readme.txt'] + +p1 = env.MSVSProject( + target = '%(PROJECT_FILE_1)s', + MSVS_PROJECT_GUID = '%(PROJECT_GUID_1)s', + slnguid = '{SLNGUID}', + srcs = testsrc, + incs = testincs, + localincs = testlocalincs, + resources = testresources, + misc = testmisc, + buildtarget = 'Test_1.exe', + variant = 'Release', + auto_build_solution = %(AUTOBUILD_SOLUTION)s, +) + +p2 = env.MSVSProject( + target = '%(PROJECT_FILE_2)s', + MSVS_PROJECT_GUID = '%(PROJECT_GUID_2)s', + slnguid = '{SLNGUID}', + srcs = testsrc, + incs = testincs, + localincs = testlocalincs, + resources = testresources, + misc = testmisc, + buildtarget = 'Test_2.exe', + variant = 'Release', + auto_build_solution = %(AUTOBUILD_SOLUTION)s, +) + +env.MSVSSolution( + target = '%(SOLUTION_FILE)s', + projects = [p1, p2], + variant = 'Release', + auto_filter_projects = %(AUTOFILTER_PROJECTS)s, +) +""" + +SConscript_projects_defaultguids_contents_fmt = """\ +env=Environment( + tools=['msvs'], + MSVS_VERSION='%(MSVS_VERSION)s', + CPPDEFINES=['DEF1', 'DEF2',('DEF3','1234')], + CPPPATH=['inc1', 'inc2'], + HOST_ARCH='%(HOST_ARCH)s', +) + +testsrc = ['test1.cpp', 'test2.cpp'] +testincs = [r'sdk_dir\\sdk.h'] +testlocalincs = ['test.h'] +testresources = ['test.rc'] +testmisc = ['readme.txt'] + +p1 = env.MSVSProject( + target = '%(PROJECT_FILE_1)s', + slnguid = '{SLNGUID}', + srcs = testsrc, + incs = testincs, + localincs = testlocalincs, + resources = testresources, + misc = testmisc, + buildtarget = 'Test_1.exe', + variant = 'Release', + auto_build_solution = %(AUTOBUILD_SOLUTION)s, +) + +p2 = env.MSVSProject( + target = '%(PROJECT_FILE_2)s', + slnguid = '{SLNGUID}', + srcs = testsrc, + incs = testincs, + localincs = testlocalincs, + resources = testresources, + misc = testmisc, + buildtarget = 'Test_2.exe', + variant = 'Release', + auto_build_solution = %(AUTOBUILD_SOLUTION)s, +) + +env.MSVSSolution( + target = '%(SOLUTION_FILE)s', + projects = [p1, p2], + variant = 'Release', + auto_filter_projects = %(AUTOFILTER_PROJECTS)s, +) +""" + def get_tested_proj_file_vc_versions(): """ @@ -667,7 +837,6 @@ class TestSConsMSVS(TestSCons): def msvs_versions(self): if not hasattr(self, '_msvs_versions'): - # Determine the SCons version and the versions of the MSVS # environments installed on the test machine. # @@ -676,21 +845,23 @@ def msvs_versions(self): # we can just exec(). We construct the SCons.__"version"__ # string in the input here so that the SCons build itself # doesn't fill it in when packaging SCons. - input = """\ + input = ( + """\ import SCons import SCons.Tool.MSCommon print("self.scons_version =%%s"%%repr(SCons.__%s__)) -print("self._msvs_versions =%%s"%%str(SCons.Tool.MSCommon.query_versions())) -""" % 'version' +print("self._msvs_versions =%%s"%%str(SCons.Tool.MSCommon.query_versions(env=None))) +""" + % 'version' + ) - self.run(arguments = '-n -q -Q -f -', stdin = input) + self.run(arguments='-n -q -Q -f -', stdin=input) exec(self.stdout()) return self._msvs_versions def vcproj_sys_path(self, fname) -> None: - """ - """ + """ """ orig = 'sys.path = [ join(sys' enginepath = repr(os.path.join(self._cwd, '..', 'engine')) @@ -700,11 +871,17 @@ def vcproj_sys_path(self, fname) -> None: contents = contents.replace(orig, replace) self.write(fname, contents) - def msvs_substitute(self, input, msvs_ver, - subdir=None, sconscript=None, - python=None, - project_guid=None, - vcproj_sccinfo: str='', sln_sccinfo: str=''): + def msvs_substitute( + self, + input, + msvs_ver, + subdir=None, + sconscript=None, + python=None, + project_guid=None, + vcproj_sccinfo: str = '', + sln_sccinfo: str = '', + ): if not hasattr(self, '_msvs_versions'): self.msvs_versions() @@ -720,7 +897,7 @@ def msvs_substitute(self, input, msvs_ver, python = sys.executable if project_guid is None: - project_guid = "{B0CC4EE9-0174-51CD-A06A-41D0713E928A}" + project_guid = PROJECT_GUID if 'SCONS_LIB_DIR' in os.environ: exec_script_main = f"from os.path import join; import sys; sys.path = [ r'{os.environ['SCONS_LIB_DIR']}' ] + sys.path; import SCons.Script; SCons.Script.main()" @@ -738,13 +915,13 @@ def msvs_substitute(self, input, msvs_ver, result = result.replace('\n', sln_sccinfo) return result - def get_msvs_executable(self, version): + def get_msvs_executable(self, version, env=None): """Returns a full path to the executable (MSDEV or devenv) for the specified version of Visual Studio. """ from SCons.Tool.MSCommon import get_vs_by_version - msvs = get_vs_by_version(version) + msvs = get_vs_by_version(version, env) if not msvs: return None return msvs.get_executable() @@ -767,23 +944,23 @@ def run(self, *args, **kw): return result def get_vs_host_arch(self): - """ Returns an MSVS, SDK, and/or MSVS acceptable platform arch. """ + """Returns an MSVS, SDK, and/or MSVS acceptable platform arch.""" # Dict to 'canonicalize' the arch (synchronize with MSCommon\vc.py) _ARCH_TO_CANONICAL = { - "amd64" : "amd64", - "emt64" : "amd64", - "i386" : "x86", - "i486" : "x86", - "i586" : "x86", - "i686" : "x86", - "ia64" : "ia64", # deprecated - "itanium" : "ia64", # deprecated - "x86" : "x86", - "x86_64" : "amd64", - "arm" : "arm", - "arm64" : "arm64", - "aarch64" : "arm64", + "amd64": "amd64", + "emt64": "amd64", + "i386": "x86", + "i486": "x86", + "i586": "x86", + "i686": "x86", + "ia64": "ia64", # deprecated + "itanium": "ia64", # deprecated + "x86": "x86", + "x86_64": "amd64", + "arm": "arm", + "arm64": "arm64", + "aarch64": "arm64", } host_platform = None @@ -792,7 +969,7 @@ def get_vs_host_arch(self): try: winkey = winreg.OpenKeyEx( winreg.HKEY_LOCAL_MACHINE, - r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' + r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', ) host_platform, _ = winreg.QueryValueEx(winkey, 'PROCESSOR_ARCHITECTURE') except OSError: @@ -809,7 +986,7 @@ def get_vs_host_arch(self): return host - def validate_msvs_file(self, file) -> None: + def validate_msvs_file(self, file) -> None: try: x = ElementTree.parse(file) except: @@ -868,6 +1045,8 @@ def _get_solution_file_vs_number(self, vc_version) -> str: return '15' elif major == 14 and minor == 2: return '16' + elif major == 14 and minor == 3: + return '17' else: raise SCons.Errors.UserError(f'Received unexpected VC version {vc_version}') @@ -902,6 +1081,15 @@ def _get_vcxproj_file_tools_version(self, vc_version) -> str: else: raise SCons.Errors.UserError(f'Received unexpected VC version {vc_version}') + def _get_vcxproj_file_platform_toolset(self, vc_version) -> str: + """ + Returns the version entry expected in the project file. + For .vcxproj files, this goes is PlatformToolset. + For .vcproj files, not applicable. + """ + major, minor = self.parse_vc_version(vc_version) + return f"v{major}{minor}" + def _get_vcxproj_file_cpp_path(self, dirs): """Returns the include paths expected in the .vcxproj file""" return ';'.join([self.workpath(dir) for dir in dirs]) @@ -925,18 +1113,165 @@ def get_expected_proj_file_contents(self, vc_version, dirs, project_file): fmt = expected_vcxprojfile_fmt else: fmt = expected_vcprojfile_fmt + project_filename = os.path.split(project_file)[-1] + project_basename = os.path.splitext(project_filename)[0] return fmt % { + 'PROJECT_BASENAME': project_basename, + 'PROJECT_GUID': PROJECT_GUID, 'TOOLS_VERSION': self._get_vcxproj_file_tools_version(vc_version), 'INCLUDE_DIRS': self._get_vcxproj_file_cpp_path(dirs), + 'PLATFORM_TOOLSET': self._get_vcxproj_file_platform_toolset(vc_version), } def get_expected_sconscript_file_contents(self, vc_version, project_file): return SConscript_contents_fmt % { 'HOST_ARCH': self.get_vs_host_arch(), 'MSVS_VERSION': vc_version, + 'PROJECT_GUID': PROJECT_GUID, 'PROJECT_FILE': project_file, } + def msvs_substitute_projects( + self, + input, + *, + subdir=None, + sconscript=None, + python=None, + project_guid_1=None, + project_guid_2=None, + solution_guid_1=None, + solution_guid_2=None, + vcproj_sccinfo: str = '', + sln_sccinfo: str = '', + ): + if not hasattr(self, '_msvs_versions'): + self.msvs_versions() + + if subdir: + workpath = self.workpath(subdir) + else: + workpath = self.workpath() + + if sconscript is None: + sconscript = self.workpath('SConstruct') + + if python is None: + python = sys.executable + + if project_guid_1 is None: + project_guid_1 = PROJECT_GUID_1 + + if project_guid_2 is None: + project_guid_2 = PROJECT_GUID_2 + + if solution_guid_1 is None: + solution_guid_1 = SOLUTION_GUID_1 + + if solution_guid_2 is None: + solution_guid_2 = SOLUTION_GUID_2 + + if 'SCONS_LIB_DIR' in os.environ: + exec_script_main = f"from os.path import join; import sys; sys.path = [ r'{os.environ['SCONS_LIB_DIR']}' ] + sys.path; import SCons.Script; SCons.Script.main()" + else: + exec_script_main = f"from os.path import join; import sys; sys.path = [ join(sys.prefix, 'Lib', 'site-packages', 'scons-{self.scons_version}'), join(sys.prefix, 'scons-{self.scons_version}'), join(sys.prefix, 'Lib', 'site-packages', 'scons'), join(sys.prefix, 'scons') ] + sys.path; import SCons.Script; SCons.Script.main()" + exec_script_main_xml = exec_script_main.replace("'", "'") + + result = input.replace(r'', workpath) + result = result.replace(r'', python) + result = result.replace(r'', sconscript) + result = result.replace(r'', exec_script_main) + result = result.replace(r'', exec_script_main_xml) + result = result.replace(r'', project_guid_1) + result = result.replace(r'', project_guid_2) + result = result.replace(r'', solution_guid_1) + result = result.replace(r'', solution_guid_2) + result = result.replace('\n', vcproj_sccinfo) + result = result.replace('\n', sln_sccinfo) + return result + + def get_expected_projects_proj_file_contents( + self, vc_version, dirs, project_file, project_guid + ): + """Returns the expected .vcxproj file contents""" + if project_file.endswith('.vcxproj'): + fmt = expected_vcxprojfile_fmt + else: + fmt = expected_vcprojfile_fmt + project_filename = os.path.split(project_file)[-1] + project_basename = os.path.splitext(project_filename)[0] + return fmt % { + 'PROJECT_BASENAME': project_basename, + 'PROJECT_GUID': project_guid, + 'TOOLS_VERSION': self._get_vcxproj_file_tools_version(vc_version), + 'INCLUDE_DIRS': self._get_vcxproj_file_cpp_path(dirs), + 'PLATFORM_TOOLSET': self._get_vcxproj_file_platform_toolset(vc_version), + } + + def get_expected_projects_sln_file_contents( + self, + vc_version, + project_file_1, + project_file_2, + have_solution_project_nodes=False, + autofilter_solution_project_nodes=None, + ): + if not have_solution_project_nodes or autofilter_solution_project_nodes: + rval = expected_projects_slnfile_fmt % { + 'FORMAT_VERSION': self._get_solution_file_format_version(vc_version), + 'VS_NUMBER': self._get_solution_file_vs_number(vc_version), + 'PROJECT_NAME_1': project_file_1.split('.')[0], + 'PROJECT_FILE_1': project_file_1, + 'PROJECT_NAME_2': project_file_2.split('.')[0], + 'PROJECT_FILE_2': project_file_2, + } + else: + rval = expected_projects_slnfile_fmt_slnnodes % { + 'FORMAT_VERSION': self._get_solution_file_format_version(vc_version), + 'VS_NUMBER': self._get_solution_file_vs_number(vc_version), + 'PROJECT_NAME_1': project_file_1.split('.')[0], + 'PROJECT_FILE_1': project_file_1, + 'PROJECT_NAME_2': project_file_2.split('.')[0], + 'PROJECT_FILE_2': project_file_2, + 'SOLUTION_FILE_1': project_file_1.split('.')[0] + ".sln", + 'SOLUTION_FILE_2': project_file_2.split('.')[0] + ".sln", + } + return rval + + def get_expected_projects_sconscript_file_contents( + self, + vc_version, + project_file_1, + project_file_2, + solution_file, + autobuild_solution=0, + autofilter_projects=None, + default_guids=False, + ): + values = { + 'HOST_ARCH': self.get_vs_host_arch(), + 'MSVS_VERSION': vc_version, + 'PROJECT_FILE_1': project_file_1, + 'PROJECT_FILE_2': project_file_2, + 'SOLUTION_FILE': solution_file, + "AUTOBUILD_SOLUTION": autobuild_solution, + "AUTOFILTER_PROJECTS": autofilter_projects, + } + + if default_guids: + format = SConscript_projects_defaultguids_contents_fmt + else: + format = SConscript_projects_contents_fmt + + values.update( + { + 'PROJECT_GUID_1': PROJECT_GUID_1, + 'PROJECT_GUID_2': PROJECT_GUID_2, + } + ) + return format % values + + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/testing/framework/TestSConsTar.py b/testing/framework/TestSConsTar.py new file mode 100644 index 0000000000..11b4524135 --- /dev/null +++ b/testing/framework/TestSConsTar.py @@ -0,0 +1,173 @@ +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +A testing framework for the SCons software construction tool. + +A TestSConsTar environment object is created via the usual invocation: + + test = TestSConsTar() + +TestSConsTar is a subsclass of TestSCons, which is in turn a subclass +of TestCommon, which is in turn is a subclass of TestCmd), and hence +has available all of the methods and attributes from those classes, +as well as any overridden or additional methods or attributes defined +in this subclass. +""" + +import os +import os.path +import subprocess +import sys + +from TestSCons import * +from TestSCons import __all__ + +__all__.extend([ + 'TestSConsTar', + 'windows_system_tar_gz', + 'windows_system_tar_bz2', + 'windows_system_tar_xz', + 'windows_system_tar_lzma', +]) + +if sys.platform == 'win32': + + # Windows 10 and later supplies windows/system32/tar.exe (bsdtar). + # Not all versions support all compression types. Check the version + # string for library support. + + # windows tar.exe: + # %systemroot%/system32/tar.exe + + # tar.exe --version (Windows 10, GH windows-2022): + # bsdtar 3.5.2 - libarchive 3.5.2 zlib/1.2.5.f-ipp + + # tar.exe --version (Windows 11): + # bsdtar 3.7.7 - libarchive 3.7.7 zlib/1.2.5.f-ipp liblzma/5.4.3 bz2lib/1.0.8 libzstd/1.5.4 + + # tar.exe --version (GH windows-2025): + # bsdtar 3.7.7 - libarchive 3.7.7 zlib/1.2.13.1-motley liblzma/5.4.3 bz2lib/1.0.8 libzstd/1.5.5 + + def _is_windows_system_tar(tar): + + if not tar: + return False + + tar = os.path.normcase(os.path.abspath(tar)) + if not os.path.exists(tar): + return False + + if not tar.endswith('tar.exe'): + return False + + windir = os.environ.get("SystemRoot") + if not windir: + windir = os.environ.get("windir") + + if not windir: + windir = os.path.join(os.environ.get("SystemDrive", "C:") + os.path.sep, "Windows") + + windows_tar = os.path.normcase(os.path.abspath(os.path.join(windir, "System32", "tar.exe"))) + rval = bool(tar == windows_tar) + + return rval + + def _windows_system_tar_have_library(tar, libname): + + is_wintar = _is_windows_system_tar(tar) + if not is_wintar: + return is_wintar, None + + try: + result = subprocess.run([f"{tar}", "--version"], capture_output=True, text=True, check=True) + version_str = result.stdout.strip() + except: + version_str = None + + if not version_str: + return is_wintar, None + + # print(f"{tar} --version => {version_str!r}") + + version_comps = version_str.split() + if len(version_comps) < 5: + return is_wintar, None + + for indx, expected in [ + (0, "bsdtar"), (2, "-"), (3, "libarchive"), + ]: + if version_comps[indx].lower() != expected: + return is_wintar, None + + # bsdtar_version = version_comps[1] + # libarchive_version = version_comps[4] + + kind = libname + "/" + + have_library = False + for component in version_comps[5:]: + if component.lower().startswith(kind): + have_library = True + break + + return is_wintar, have_library + + def windows_system_tar_gz(tar): + is_wintar, have_library = _windows_system_tar_have_library(tar, 'zlib') + return is_wintar, have_library + + def windows_system_tar_bz2(tar): + is_wintar, have_library = _windows_system_tar_have_library(tar, 'bz2lib') + return is_wintar, have_library + + def windows_system_tar_xz(tar): + is_wintar, have_library = _windows_system_tar_have_library(tar, 'liblzma') + return is_wintar, have_library + + def windows_system_tar_lzma(tar): + is_wintar, have_library = _windows_system_tar_have_library(tar, 'liblzma') + return is_wintar, have_library + +else: + + def windows_system_tar_gz(tar): + return False, None + + def windows_system_tar_bz2(tar): + return False, None + + def windows_system_tar_xz(tar): + return False, None + + def windows_system_tar_lzma(tar): + return False, None + +class TestSConsTar(TestSCons): + pass + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/testing/framework/TestSCons_time.py b/testing/framework/TestSCons_time.py index d2f061a9a5..7748c7c7ce 100644 --- a/testing/framework/TestSCons_time.py +++ b/testing/framework/TestSCons_time.py @@ -40,10 +40,12 @@ from TestCommon import * from TestCommon import __all__ +from TestCmd import IS_WINDOWS + # some of the scons_time tests may need regex-based matching: from TestSCons import search_re, search_re_in_list -__all__.extend(['TestSCons_time',]) +__all__.extend(['TestSCons_time']) SConstruct = """\ import os @@ -189,7 +191,9 @@ def __init__(self, **kw) -> None: kw['program'] = p if 'interpreter' not in kw: - kw['interpreter'] = [python,] + kw['interpreter'] = [ + python, + ] if 'match' not in kw: kw['match'] = match_exact @@ -205,20 +209,20 @@ def archive_split(self, path): else: return os.path.splitext(path) - def fake_logfile(self, logfile_name, index: int=0) -> None: + def fake_logfile(self, logfile_name, index: int = 0) -> None: self.write(self.workpath(logfile_name), logfile_contents % locals()) def profile_data(self, profile_name, python_name, call, body) -> None: profile_name = self.workpath(profile_name) python_name = self.workpath(python_name) d = { - 'profile_name' : profile_name, - 'python_name' : python_name, - 'call' : call, - 'body' : body, + 'profile_name': profile_name, + 'python_name': python_name, + 'call': call, + 'body': body, } self.write(python_name, profile_py % d) - self.run(program = python_name, interpreter = sys.executable) + self.run(program=python_name, interpreter=sys.executable) def tempdir_re(self, *args): """ @@ -236,9 +240,16 @@ def tempdir_re(self, *args): except AttributeError: pass else: - tempdir = realpath(tempdir) - - args = (tempdir, 'scons-time-',) + args + # Don't realpath on Windows, tempdir could contain 8+3 path + # E.g. username on GitHub runner is "runneradmin" -> "RUNNER~1" + # We don't want to convert that back! + if not IS_WINDOWS: + tempdir = realpath(tempdir) + + args = ( + tempdir, + 'scons-time-', + ) + args x = os.path.join(*args) x = re.escape(x) x = x.replace('time\\-', f'time\\-[^{sep}]*') @@ -274,19 +285,20 @@ def write_sample_directory(self, archive, dir, files): def write_sample_tarfile(self, archive, dir, files): import shutil import tarfile + base, suffix = self.archive_split(archive) mode = { - '.tar' : 'w', - '.tar.gz' : 'w:gz', - '.tgz' : 'w:gz', + '.tar': 'w', + '.tar.gz': 'w:gz', + '.tgz': 'w:gz', } with tarfile.open(archive, mode[suffix]) as tar: for name, content in files: path = os.path.join(dir, name) with open(path, 'wb') as f: - f.write(bytearray(content,'utf-8')) + f.write(bytearray(content, 'utf-8')) tarinfo = tar.gettarinfo(path, path) tarinfo.uid = 111 tarinfo.gid = 111 @@ -300,6 +312,7 @@ def write_sample_tarfile(self, archive, dir, files): def write_sample_zipfile(self, archive, dir, files): import shutil import zipfile + with zipfile.ZipFile(archive, 'w') as zip: for name, content in files: path = os.path.join(dir, name) @@ -310,17 +323,17 @@ def write_sample_zipfile(self, archive, dir, files): return self.workpath(archive) sample_project_files = [ - ('SConstruct', SConstruct), + ('SConstruct', SConstruct), ] def write_sample_project(self, archive, dir=None): base, suffix = self.archive_split(archive) write_sample = { - '.tar' : self.write_sample_tarfile, - '.tar.gz' : self.write_sample_tarfile, - '.tgz' : self.write_sample_tarfile, - '.zip' : self.write_sample_zipfile, + '.tar': self.write_sample_tarfile, + '.tar.gz': self.write_sample_tarfile, + '.tgz': self.write_sample_tarfile, + '.zip': self.write_sample_zipfile, }.get(suffix, self.write_sample_directory) if not dir: @@ -331,6 +344,7 @@ def write_sample_project(self, archive, dir=None): return path + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/testing/framework/TestSConsign.py b/testing/framework/TestSConsign.py index 9f02b4937a..18dd333dbd 100644 --- a/testing/framework/TestSConsign.py +++ b/testing/framework/TestSConsign.py @@ -41,7 +41,8 @@ from TestSCons import * from TestSCons import __all__ -__all__.extend([ 'TestSConsign', ]) +__all__.extend(['TestSConsign']) + class TestSConsign(TestSCons): """Class for testing the sconsign.py script. @@ -55,6 +56,7 @@ class TestSConsign(TestSCons): "scons" itself, since we need to run scons to generate the .sconsign files that we want the sconsign script to read. """ + def __init__(self, *args, **kw) -> None: try: script_dir = os.environ['SCONS_SCRIPT_DIR'] @@ -67,7 +69,7 @@ def __init__(self, *args, **kw) -> None: super().__init__(*args, **kw) self.my_kw = { - 'interpreter' : python, # imported from TestSCons + 'interpreter': python, # imported from TestSCons } if 'program' not in kw: @@ -99,6 +101,7 @@ def run_sconsign(self, *args, **kw): kw.update(self.my_kw) return self.run(*args, **kw) + # Local Variables: # tab-width:4 # indent-tabs-mode:nil diff --git a/testing/framework/TestUnit/cli.py b/testing/framework/TestUnit/cli.py index defe5a1eff..b46951fcec 100644 --- a/testing/framework/TestUnit/cli.py +++ b/testing/framework/TestUnit/cli.py @@ -10,8 +10,11 @@ def get_runner(): parser = optparse.OptionParser() - parser.add_option('--runner', default='unittest.TextTestRunner', - help='name of test runner class to use') + parser.add_option( + '--runner', + default='unittest.TextTestRunner', + help='name of test runner class to use', + ) opts, args = parser.parse_args() fromsplit = opts.runner.rsplit('.', 1) diff --git a/testing/framework/TestUnit/taprunner.py b/testing/framework/TestUnit/taprunner.py index b52c762a82..c4e6f41775 100644 --- a/testing/framework/TestUnit/taprunner.py +++ b/testing/framework/TestUnit/taprunner.py @@ -15,6 +15,7 @@ from unittest import TextTestRunner + try: from unittest import TextTestResult except ImportError: @@ -23,19 +24,18 @@ class TAPTestResult(TextTestResult): - - def _process(self, test, msg, failtype = None, directive = None) -> None: - """ increase the counter, format and output TAP info """ + def _process(self, test, msg, failtype=None, directive=None) -> None: + """increase the counter, format and output TAP info""" # counterhack: increase test counter test.suite.tap_counter += 1 msg = "%s %d" % (msg, test.suite.tap_counter) if "not" not in msg: msg += " " # justify - self.stream.write("%s - " % msg) + self.stream.write(f"{msg} - ") if failtype: - self.stream.write("%s - " % failtype) - self.stream.write("%s" % test.__class__.__name__) - self.stream.write(".%s" % test._testMethodName) + self.stream.write(f"{failtype} - ") + self.stream.write(f"{test.__class__.__name__}") + self.stream.write(f".{test._testMethodName}") if directive: self.stream.write(directive) self.stream.write("\n") @@ -58,7 +58,7 @@ def addError(self, test, err) -> None: def addSkip(self, test, reason) -> None: super().addSkip(test, reason) - self._process(test, "ok", directive=(" # SKIP %s" % reason)) + self._process(test, "ok", directive=f" # SKIP {reason}") def addExpectedFailure(self, test, err) -> None: super().addExpectedFailure(test, err) @@ -82,7 +82,7 @@ def run(self, test): # [ ] add commented block with test suite __doc__ # [ ] check call with a single test # if isinstance(test, suite.TestSuite): - self.stream.write("1..%s\n" % len(list(test))) + self.stream.write(f"1..{len(list(test))}\n") # counterhack: inject test counter into test suite test.tap_counter = 0 @@ -98,33 +98,40 @@ def run(self, test): import unittest class Test(unittest.TestCase): - def test_ok(self) -> None: - pass - def test_fail(self) -> None: - self.assertTrue(False) - def test_error(self) -> None: - bad_symbol - @unittest.skip("skipin'") - def test_skip(self) -> None: - pass - @unittest.expectedFailure - def test_not_ready(self) -> None: - self.fail() - @unittest.expectedFailure - def test_invalid_fail_mark(self) -> None: - pass - def test_another_ok(self) -> None: - pass - - - suite = unittest.TestSuite([ - Test('test_ok'), - Test('test_fail'), - Test('test_error'), - Test('test_skip'), - Test('test_not_ready'), - Test('test_invalid_fail_mark'), - Test('test_another_ok') - ]) + def test_ok(self) -> None: + pass + + def test_fail(self) -> None: + self.assertTrue(False) + + def test_error(self) -> None: + bad_symbol + + @unittest.skip("skipin'") + def test_skip(self) -> None: + pass + + @unittest.expectedFailure + def test_not_ready(self) -> None: + self.fail() + + @unittest.expectedFailure + def test_invalid_fail_mark(self) -> None: + pass + + def test_another_ok(self) -> None: + pass + + suite = unittest.TestSuite( + [ + Test('test_ok'), + Test('test_fail'), + Test('test_error'), + Test('test_skip'), + Test('test_not_ready'), + Test('test_invalid_fail_mark'), + Test('test_another_ok'), + ] + ) if not TAPTestRunner().run(suite).wasSuccessful(): - sys.exit(1) + sys.exit(1) diff --git a/testing/framework/test-framework.rst b/testing/framework/test-framework.rst index 39ec6e3752..e446a52e20 100644 --- a/testing/framework/test-framework.rst +++ b/testing/framework/test-framework.rst @@ -8,38 +8,48 @@ SCons Testing Framework Introduction ============ -SCons uses extensive automated tests to ensure quality. The primary goal -is that users be able to upgrade from version to version without -any surprise changes in behavior. +SCons uses extensive automated tests to ensure quality, so users can +safely upgrade from version to version without any surprise changes in behavior. + +Changes to SCons code, whether fix or new feature, need testing support before +being merged. Sometimes this might mean writing tests for an area that didn't +have adequate, or any, testing before. Having tests isn't an +*iron-clad policy*, but exceptions need to be okayed by the core team - +it's good to discuss ahead of time. When in doubt, test it! + +This guide should provide enough information to get started. Use it together +with existing tests to "see how it's done". Note if you're coding in an IDE, +you may need to add the path ``testing/framework`` to the known paths, +or it won't recognize some symbols. -In general, no change goes into SCons unless it has one or more new -or modified tests that demonstrably exercise the bug being fixed or -the feature being added. There are exceptions to this guideline, but -they should be just that, *exceptions*. When in doubt, make sure -it's tested. Test organization ================= -There are three types of SCons tests: - -*End-to-End Tests* - End-to-end tests of SCons are Python scripts (``*.py``) underneath the - ``test/`` subdirectory. They use the test infrastructure modules in - the ``testing/framework`` subdirectory. They set up small complete - projects and call SCons to execute them, checking that the behavior is - as expected. +There are three categories of SCons tests: *Unit Tests* Unit tests for individual SCons modules live underneath the - ``SCons/`` subdirectory and are the same base name as the module - to be tested, with ``Tests`` appended to the basename. For example, - the unit tests for the ``Builder.py`` module are in the - ``BuilderTests.py`` script. Unit tests tend to be based on assertions. + ``SCons/`` subdirectory (that is alongside the code they are testing) + and use the Python ``unittest`` module to perform assertion-based + testing of individual routines, classes and methods in the SCons code base. + +*End-to-End Tests* + End-to-end tests of SCons are small self-contained projects that are + run by calling ``scons`` (or in a few cases, ``sconsign`` or ``scons-time``) + to execute them, and thus simulate the results of running all the + way through building a project - thus "end to end". + These tests need a Python script to drive them - setup, issuing + the correct command line, checking and recording results; + these operations are aided by using the test infrastructure modules in + the ``testing/framework`` subdirectory. + The scripts need a test runner to find end execute them + (the runner specifically requires a ``.py`` filename suffix on these). + The standard tests are located in the ``test/`` subdirectory, *External Tests* For the support of external Tools (in the form of packages, preferably), - the testing framework is extended so it can run in standalone mode. + the testing framework can also run in standalone mode. You can start it from the top-level directory of your Tool's source tree, where it then finds all Python scripts (``*.py``) underneath the local ``test/`` directory. This implies that Tool tests have to be kept in @@ -49,46 +59,68 @@ There are three types of SCons tests: Contrasting end-to-end and unit tests ------------------------------------- -In general, end-to-end tests verify hardened parts of the public interface: -interfaces documented in the manpage that a user might use in their -project. These cannot be broken (of course, errors can be corrected, -though sometimes a transition period may be required). -Unit tests are now considered for testing internal interfaces, which do -not themselves directly have API guarantees. An example could be using -and end-to-end test to verify that things added by env.Append() actually -appear correctly in issued command lines, while unit tests assure -correct behavior given various inputs of internal routines that -Append() may make use of. If a reported error can be tested by adding a new -case to an existing unit test, by all means, do that, as it tends to be -simpler and cleaner. On the other hand, reported problems that come with -a reproducer are by their nature more like an e2e test - this is something -a user has tried in their SConscripts that didn't have the expected result. +End-to-end tests exist to verify hardened parts of the public interface: +features documented in the reference manual that are available +to use in a project. They accomplish this by themselves being +complete (though usually very small) SCons projects. +Unit tests are more for testing internal interfaces which may +not themselves directly have public API guarantees. +They let you hone in on the point of error much more precisely. +As an example, an end-to-end test verifies that +variable contents added by ``env.Append`` actually +appear correctly in issued command lines, +while unit tests verify that internal routine +``SCons.Environment._add_cppdefines`` properly handles the +complexities of combining different types of arguments +and still produce the expected contents of the ``CPPDEFINES`` +variable being added to by the ``env.Append`` interface.. + +If you're pursuing a fix and it can be tested by adding a new +case to a unit test, that is often simpler and more direct. +However, bug reports tend describe end-to-end type behavior +("I put this in my SConscripts, and something went wrong"), +so it's often useful to code an e2e test to match a bug report, +and develop a solution that turns that to a pass. +So pick the testing strategy that makes sense (sometimes both do) - +and if it's possible to reduce an end-to-end test to a unit test, please do End-to-end tests are by their nature harder to debug. For the unit tests, you're running a test program directly, so you can drop straight into the Python debugger by calling ``runtest.py`` with the ``-d / --debug`` -option and setting breakpoints to help examine the internal state as -the test is running. The e2e tests are each mini SCons projects executed -by an instance of scons in a subprocess, and the Python debugger isn't -particularly useful in this context. -There's a separate section of this document on that topic: see `Debugging -end-to-end tests`_. +option, set some breakpoints and otherwise step through examining +internal state and how it changes as the test is running. +The end-to-end tests are each small SCons projects executed +by an instance of SCons in a subprocess, so the Python debugger is +harder to bring into play in this context. +There's a separate section devoted to debugging ideas: +`Debugging end-to-end tests`_. Naming conventions ------------------ +The unit tests have the same base name as the module to be tested, +with ``Tests`` appended. For packages, it's conventional to put the +test file in the package directory, and there's typically only one test file +for the whole package directory, but feel free to use multiples +if it makes more sense. For example, there's an +``SCons/Platform/PlatformTests.py`` which covers most of the +Platform code, but also ``SCons/Platform/virtualenvTests.py`` +for testing the virtualenv (sub-)module. + The end-to-end tests, more or less, follow this naming convention: -#. All tests end with a ``.py`` suffix. -#. In the *General* form we use +* Tests are Python scripts, so they end with a ``.py`` suffix. +* In the *General* form we use ``Feature.py`` - for the test of a specified feature; try to keep this description + for the test of a specified feature; try to keep this name reasonably short ``Feature-x.py`` for the test of a specified feature using option ``x`` -#. The *command line option* tests take the form + ``Feature-live.py`` + for a test which explicitly uses an external tool to operate. +* The *command line option* tests take the form ``option-x.py`` for a lower-case single-letter option @@ -98,8 +130,8 @@ The end-to-end tests, more or less, follow this naming convention: ``option--lo.py`` long option; you can abbreviate the long option name to a few characters (the abbreviation must be unique, of course). -#. Use a suitably named subdirectory if there's a whole group of - related test files. +* Use a suitably named subdirectory if there's a whole group of + related test files, this allows easier selection. Testing Architecture @@ -107,13 +139,14 @@ Testing Architecture The test framework provides a lot of useful functions for use within a test program. This includes test setup, parameterization, running tests, -looking at results and reporting outcomes. You can run a particular test +examining results and reporting outcomes. You can run a particular unit test directly by making sure the Python interpreter can find the framework:: $ PYTHON_PATH=testing/framework python SCons/ActionTests.py -The framework does *not* provide facilities for handling a collection of -test programs. For that, SCons provides a driver script ``runtest.py``. +The framework does *not* provide facilities for test selection, +or otherwise dealing with collections of tests. +For that, SCons provides a runner script ``runtest.py``. Help is available through the ``-h`` option:: $ python runtest.py -h @@ -165,7 +198,7 @@ to the screen to a file named by ``FILE``. There is also an option to save the results in a custom XML format. The above invocations all test against the SCons files in the current -directory (that is, in ``./SCons``, and do not require that a packaging +directory (that is, in ``./SCons``), and do not require that a packaging build of SCons be performed first. This is the most common mode: make some changes, and test the effects in place. The ``runtest.py`` script supports additional options to run tests against unpacked packages in the @@ -228,16 +261,17 @@ When using fixtures, you may end up in a situation where you have supporting Python script files in a subdirectory which shouldn't be picked up as test scripts of their own. There are two options here: -#. Add a file with the name ``sconstest.skip`` to your subdirectory. This - tells ``runtest.py`` to skip the contents of the directory completely. -#. Create a file ``.exclude_tests`` in each directory in question, and in - it list line-by-line the files to exclude from testing. + * Add a file with the name ``sconstest.skip`` to your subdirectory. This + tells ``runtest.py`` to skip the contents of the directory completely. + * Create a file ``.exclude_tests`` in your subdirectory, and in + it list line-by-line the files to exclude from testing - the rest + will still be picked up as long as they meet the selection criteria. The same rules apply when testing external Tools when using the ``-e`` option. -Example End-to-End test script +Example end-to-end test script ============================== To illustrate how the end-to-end test scripts work, let's walk through @@ -269,14 +303,14 @@ a simple *Hello, world!* example:: test.pass_test() -Explanation ------------ +Line by line explanation of example +----------------------------------- ``import TestSCons`` Imports the main infrastructure for SCons tests. This is normally the only part of the infrastructure that needs importing. - Sometimes other Python modules are necessary or helpful, and get - imported before this line. + If you need Python standard library modules in your code, + the convention it to import those before the framework. ``test = TestSCons.TestSCons()`` Initializes an object for testing. A fair amount happens under @@ -327,6 +361,7 @@ Explanation the test passed. As a side effect of destroying the ``test`` object, the created temporary directory will be removed. + Working with fixtures ===================== @@ -336,24 +371,21 @@ method, plus a string holding its contents, and it gets written to the test directory right before starting. This simple technique can be seen throughout most of the end-to-end -tests as it was the original technique provided to test developers, +tests as it was the original technique provided for test developers, but it is no longer the preferred way to write a new test. To develop this way, you first need to create the necessary files and get them to work, then convert them to an embedded string form, which may -involve lots of extra escaping. These embedded files are then tricky -to maintain. As a test grows multiple steps, it becomes less easy to -read, since many if the embedded strings aren't quite the final files, -and the volume of test code obscures the flow of the testing steps. -Additionally, as SCons moves more to the use of automated code checkers -and formatters to detect problems and keep a standard coding style for -better readability, note that such tools don't look inside strings -for code, so the effect is lost on them. +involve escaping, using raw strings, and other fiddly details. +These embedded files are then tricky to maintain - they're not +recognized as code by editors, static checkers, or formatters. +Readability is further hurt if the test script grows large - +lots of files-in-strings obscure the flow of the actual testing logic. In testing parlance, a fixture is a repeatable test setup. The SCons -test harness allows the use of saved files or directories to be used -in that sense: *the fixture for this test is foo*, instead of writing -a whole bunch of strings to create files. Since these setups can be -reusable across multiple tests, the *fixture* terminology applies well. +test harness allows the use of saved files as well as collections of +files in named directories to be used +in that sense: *the fixture for this test is foo*. Since these setups can be +reused across multiple tests, the *fixture* terminology applies well. Note: fixtures must not be treated by SCons as runnable tests. To exclude them, see instructions in the above section named `Selecting tests`_. @@ -361,37 +393,39 @@ them, see instructions in the above section named `Selecting tests`_. Directory fixtures ------------------ -The test harness method ``dir_fixture(srcdir, [dstdir])`` +The test method ``dir_fixture(srcdir, [dstdir])`` copies the contents of the specified directory ``srcdir`` from the directory of the called test script to the current temporary test -directory. The ``srcdir`` name may be a list, in which case the elements -are concatenated into a path first. The optional ``dstdir`` is +directory. The optional ``dstdir`` is used as a destination path under the temporary working directory. -``distdir`` is created automatically, if it does not already exist. +``dstdir`` is created automatically if it does not already exist. +The ``srcdir`` and ``dstdir`` parameters may each be a list, +which will be concatenated into a path string. If ``srcdir`` represents an absolute path, it is used as-is. Otherwise, if the harness was invoked with the environment variable ``FIXTURE_DIRS`` set (which ``runtest.py`` does by default), the test instance will present that list of directories to search -as ``self.fixture_dirs``, each of these are additionally searched for -a directory with the name of ``srcdir``. +as ``self.fixture_dirs``. Each of these are additionally searched for +a directory with the name given by ``srcdir``. -A short syntax example:: +A short example showing the syntax:: test = TestSCons.TestSCons() - test.dir_fixture('image') + test.dir_fixture('fixture') test.run() -would copy all files and subdirectories from the local ``image`` directory -to the temporary directory for the current test, then run it. +This copies all files and subdirectories from the local ``fixture`` directory, +or if not found, from a ``fixture`` located in one of the fixture dirs, +to the temporary directory for the current test, before running the test. -To see a real example for this in action, refer to the test named +To see an example in action, refer to the test named ``test/packaging/convenience-functions/convenience-functions.py``. + File fixtures ------------- - -The method ``file_fixture(srcfile, [dstfile])`` +The test method ``file_fixture(srcfile, dstfile)`` copies the file ``srcfile`` from the directory of the called script to the temporary test directory. The optional ``dstfile`` is used as a destination file name @@ -399,17 +433,17 @@ under the temporary working directory, unless it is an absolute path name. If ``dstfile`` includes directory elements, they are created automatically if they don't already exist. The ``srcfile`` and ``dstfile`` parameters may each be a list, -which will be concatenated into a path. +which will be concatenated into a path string. If ``srcfile`` represents an absolute path, it is used as-is. Otherwise, any passed in fixture directories are used as additional places to search for the fixture file, as for the ``dir_fixture`` case. -With the following code:: +As an example, with the following code:: test = TestSCons.TestSCons() test.file_fixture('SConstruct') - test.file_fixture(['src', 'main.cpp'], ['src', 'main.cpp']) + test.file_fixture('src/main.cpp', 'src/main.cpp') test.run() The files ``SConstruct`` and ``src/main.cpp`` are copied to the @@ -432,13 +466,20 @@ How to convert old tests to use fixtures ---------------------------------------- Tests using the inline ``TestSCons.write()`` method can fairly easily be -converted to the fixture based approach. For this, we need to get at the -files as they are written to each temporary test directory, -which we can do by taking advantage of an existing debugging aid, -namely that ``runtest.py`` checks for the existence of an environment -variable named ``PRESERVE``. If it is set to a non-zero value, the testing -framework preserves the test directory instead of deleting it, and prints -a message about its name to the screen. +converted to the fixture based approach via a trick: you can capture +the test directory as it is created, which will contain the files +in their final form. To do this, set the environment variable +``PRESERVE`` to a non-zero value when calling ``runtest.py`` +to run the test, +and it will preserve the directory rather than getting rid of it, +and report the path. +A thing to keep in mind is some tests rewrite files while +running - for example some tests create an `SConstruct``, +then write a new one for another part of the test, +then possibly do so again - "preserving" will only keep the state +of the test as it exits. For this and debugging reasons, +it is preferred not to have tests replace the contents of key files +during a run. So, you should be able to give the commands:: @@ -454,7 +495,7 @@ The output will then look something like this:: PASSED preserved directory /tmp/testcmd.4060.twlYNI -You can now copy the files from that directory to your new +You can copy the files from that directory to your new *fixture* directory. Then, in the test script you simply remove all the tedious ``TestSCons.write()`` statements and replace them with a single ``TestSCons.dir_fixture()`` call. @@ -472,20 +513,26 @@ final name as needed:: test.file_fixture('fixture/SConstruct.part2', 'SConstruct') # run new test +As mentioned earlier, this isn't really ideal and it's +preferred in such cases to keep the separate names in the test +directory, and instead vary how the tests are executed, like:: + + test.run(arguments=['-f', 'SConstruct.part1']) + test.run(arguments=['-f', 'SConstruct.part2']) + When not to use a fixture ------------------------- -Note that some files are not appropriate for use in a fixture as-is: -fixture files should be static. If the creation of the file involves -interpolating data discovered during the run of the test script, -that process should stay in the script. Here is an example of this -kind of usage that does not lend itself easily to a fixture:: +Static test files are well suited to fixtures, you just copy them over. +Files with dynamically created content - usually to interpolate +information discovered during test setup, are more problematic. +Here's an example of a rather common pattern:: import TestSCons _python_ = TestSCons._python_ - test.write('SConstruct', f""" + test.write('SConstruct', f"""\ cc = Environment().Dictionary('CC') env = Environment( LINK=r'{_python_} mylink.py', @@ -497,39 +544,79 @@ kind of usage that does not lend itself easily to a fixture:: env.Program(target='test1', source='test1.c') """ -Here the value of ``_python_`` from the test program is -pasted in via f-string formatting. A fixture would be hard to use -here because we don't know the value of ``_python_`` until runtime -(also note that as it will be an absolute pathname, it's entered using -Python raw string notation to avoid interpretation problems on Windows, +Here the value of ``_python_`` (the path to the Python executable +actually in use for the test) is obtained by the test program from +the framework, and pasted in via f-string formatting in setting up +the string that will make up the contents of the ``SConstruct``. +A simple fixture isn't useful here because the value of ``_python_`` +isn't known until runtime (also note that as it will be an +absolute pathname, it is entered using Python raw string notation +to avoid interpretation problems on Windows, where the path separator is a backslash). The other files created in this test may still be candidates for use as fixture files, however. +There's another approach that can be used in this case, +letting you still use a fixture file: +instead of using string interpolation at setup time, +consider passing values at run-time via command-line arguments. +In the example above, you can replace the substitution +of ``_python_`` at file-writing time with a check for a variable +from the command line, and substitute that at run-time, +so instead of the above sequence in the test script, +put this in a new file ``fixture/SConstruct``:: + + python = ARGUMENTS.get('PYTHON', 'python') + cc = Environment().Dictionary('CC') + env = Environment( + LINK=rf'{python} mylink.py', + LINKFLAGS=[], + CC=rf'{python} mycc.py', + CXX=cc, + CXXFLAGS=[], + ) + env.Program(target='test1', source='test1.c') + +Read this in as a file fixture:: + + test.file_fixture(srcfile='fixture/SConstruct') + +For this to work, you have to supply ``PYTHON`` in the argument list, +so it appears in ``ARGUMENTS`` at run-time:: + + test.run(arguments=rf'PYTHON={_python_}') + Debugging end-to-end tests ========================== -The end-to-end tests are hand-crafted SCons projects, so testing -involves running an instance of scons with those inputs. The -tests treat the SCons invocation as a *black box*, +An end-to-end tests is a hand-crafted SCons project, +so testing involves building (or cleaning) that project +with suitable arguments to control the behavior. +The tests treat the SCons invocation as a *black box*, usually looking for *external* effects of the test - targets are -created, created files have expected contents, files properly +created, generated files have expected contents, files are properly removed on clean, etc. They often also look for -the flow of messages from SCons. +the flow of messages from SCons, which is unfortunately a bit fragile +(many a test has been broken by a new Python version changing +the precise format of an exception message, for example). +Some tests do have test code inside the generated files, +and based on the result emit special known strings that +the test can look for. Simple tricks like inserting ``print`` statements in the SCons code -itself don't really help as they end up disrupting those external -effects (e.g. ``test.run(stdout="Some text")``, but with the -``print``, ``stdout`` contains the extra print output and the -result doesn't match). +itself don't really help as they end up disrupting expected output. +For example, ``test.run(stdout="Some text")`` +expects a simple string on the standard output stream, +but the presence of a ``print`` in the code means that appears +in the output, too, and the string matching will fail the test. Even more irritatingly, added text can cause other tests to fail and obscure the error you're looking for. Say you have three different tests in a script exercising different code paths for the same feature, and the third one is unexpectedly failing. You add some debug prints to -the affected part of scons, and now the first test of the three starts +the affected part of SCons, and now the first test of the three starts failing, aborting the test run before it even gets to the third test - the one you were trying to debug. @@ -582,22 +669,28 @@ Test infrastructure The main end-to-end test API is defined in the ``TestSCons`` class. ``TestSCons`` is a subclass of ``TestCommon``, which is a subclass of ``TestCmd``. -``TestSCons`` provides the help for using an instance of SCons during -the run. - -The unit tests do not run an instance of SCons separately, but instead -import the modules of SCons that they intend to test. Those tests -should use the ``TestCmd`` class - it is intended for runnable scripts. - +``TestCmd`` provides facilities for generically "running a command". +``TestCommon`` wraps this with features for result and error handling. +``TestSCons`` specializes that into support for +specifically running the command ``scons`` +(there are related classes ``TestSConsign`` for runing ``sconsign`` +and ``TestSCons_time`` for running timing tests using ``bin/scons_time.py``. Those classes are defined in Python files of the same name in ``testing/framework``. -Start in ``testing/framework/TestCmd.py`` for the base API definitions, like how -to create files (``test.write()``) and run commands (``test.run()``). +Start in ``testing/framework/TestCmd.py`` for the base API definitions, +like how to create files (``test.write()``) +and run commands (``test.run()``). + +The unit tests do not run a separate instance of SCons, but instead +import the SCons module that they intend to test. Those tests +can usually use the ``TestCmd`` class for testing infrastructure +(temporary directory, file creation, etc.), while the test classes +themselves normally derive from ``unittest.TestCase``. The match functions work like this: ``TestSCons.match_re`` - match each line with an RE + match each line with a regular expression. * Splits the lines into a list (unless they already are) * splits the REs at newlines (unless already a list) @@ -606,7 +699,7 @@ The match functions work like this: REs as lines. ``TestSCons.match_re_dotall`` - match all the lines against a single RE + match all the lines against a single regular expression. * Joins the lines with newline (unless already a string) * joins the REs with newline (unless it's a string) and puts ``^..$`` @@ -621,8 +714,20 @@ or:: test.must_match(..., match=TestSCons.match_re, ...) -Avoiding tests based on tool existence -====================================== +Often you want to supply arguments to SCons when it is invoked +to run a test, which you can do using an *arguments* parameter:: + + test.run(arguments="-O -v FOO=BAR") + +One caveat here: the way the parameter is processed is unavoidably +different from typing on the command line - if you need it not to +be split on spaces, pre-split it yourself, and pass the list, like:: + + test.run(arguments=["-f", "SConstruct2", "FOO=Two Words"]) + + +Avoiding tests based on tool (non-)existence +============================================ For many tests, if the tool being tested is backed by an external program which is not installed on the machine under test, it may not be worth @@ -630,6 +735,9 @@ proceeding with the test. For example, it's hard to test compiling code with a C compiler if no C compiler exists. In this case, the test should be skipped. +End-to-end +---------- + Here's a simple example for end-to-end tests:: intelc = test.detect_tool('intelc', prog='icpc') @@ -640,11 +748,28 @@ See ``testing/framework/TestSCons.py`` for the ``detect_tool()`` method. It calls the tool's ``generate()`` method, and then looks for the given program (tool name by default) in ``env['ENV']['PATH']``. -The ``where_is()`` method can be used to look for programs that -are do not have tool specifications. The existing test code +``test.where_is()`` can be used to look for programs that +do not have tool specifications (or you just don't want to +involve a specific tool). The existing test code will have many samples of using either or both of these to detect if it is worth even proceeding with a test. +There's an additional consideration for e2e tests: when a project +developer needs a tool that requires some unique setup +(in particular, the path to find an external executable), +they can just adjust their build to make it work in their environment. +It's not practical to change a bunch of tests in the test suite to do a +similar thing. Calling ``test.where_is()`` might return a positive +response based on searching the shell's ``PATH`` environment +variable (which it checks if no specific paths to search are given), +but that does not guarantee the copy of SCons launched to run +the actual testcase will find it, so it may be necessary to +pass the path to the test, perhaps via an argument to ``test.run()``. + + +Unit Tests +---------- + For the unit tests, there are decorators for conditional skipping and other actions that will produce the correct output display and statistics in abnormal situations. @@ -673,84 +798,110 @@ plumbed into the environment. These things can be tested by mocking the behavior of the executable. Many examples of this can be found in the ``test`` directory. See for example ``test/subdivide.py``. -Testing DOs and DONTs -===================== -There's no question that having to write tests in order to get a change -approved - even an apparently trivial change - does make it a little harder -to contribute to the SCons code base - but the requirement to have features -and bugfixes testable is a necessary part of ensuring SCons quality. -Thinking of SCons development in terms of the red/green model from -Test Driven Development should make things a little easier. - -If you are working on an SCons bug, try to come up with a simple -reproducer first. Bug reports (even your own!) are often like *I tried -to do this but it surprisingly failed*, and a reproducer is normally an -``SConstruct`` along with, probably, some supporting files such as source -files, data files, subsidiary SConscripts, etc. Try to make this example -as simple and clean as possible. No, this isn't necessarily easy to do, -but winnowing down what triggers a problem and removing the stuff that -doesn't actually contribute to triggering the problem it is a step that -lets you (and later readers) more clearly understand what is going on. -You don't have to turn this into a formal testcase yet, but keep this -reproducer around, and document with it what you expect to happen, -and what actually happens. This material will help produce an E2E -test later, and this is something you *may* be able to get help with, -if the way the tests are usually written and the test harness proves -too confusing. With a clean test in hand (make sure it's failing!) -you can go ahead an code up a fix and make sure it passes with the fix -in place. Jumping straight to a fix without working on a testcase like -this will often lead to a disappointing *how do I come up with a test -so the maintainer will be willing to merge* phase. Asking questions on -a public forum can be productive here. - -E2E-specific Suggestions: - -* Do not require the use of an external tool unless necessary. +Testing DOs and DONT'S +====================== + +We know that needing to write tests makes the job of contributing +code to SCons more cumbersome. But as noted in the introduction, +the testing strategy is extremely important to SCons, it has allowed +the project to serve many users with very few nasty surprises +(won't lie and say there has *never* been a surprise!) +for over two decades. We suggest thinking in terms of test-driven +development for your contribution: you'll need something to show +that your change actually works anyway. If it's a bug report, +this may be the minimum viable reproducer; if it's a new feature, +you still want to show how something that couldn't be done +before can now be done. Code that up first, and document your +expectations (for yourself as much as anyone), and use it when +developing the fix/feature. Often that code can be converted into +a test that will fit into the SCons testuite without doing too +much extra work. If that work looks too daunting, please ask +for help - tips, advice, and coding help may all be available. + + +E2E-specific suggestions +------------------------ + +* **DO** group tests by topic area. This makes selection easier, + for example the tests specific to the ``yacc`` tool can be run using + ``runtest.py test/YACC`` +* **DO** keep tests simple. Some tests are by their nature complex, + but narrowing in on a specific feature makes for easier debugging - + more simpler test files is easier on future maintainers than a huge + compilcated one. +* **DON'T** gang too many things together in one file (related to + the previous item). It's cleaner if + they're split into different files unless they share complex + infrastructure. This helps avoid the problem of "fail fast": + a test aborts when it detects a failure condition, + and the other tests in the same file don't ever run, + which may keep you from seeing a pattern exposed + when several related tests all fail. +* **DON'T** require the use of an external executable "unless necessary". Usually the SCons behavior is the thing we want to test, - not the behavior of the external tool. *Necessary* is not a precise term - - sometimes it would be too time-consuming to write a script to mock - a compiler with an extensive set of options, and sometimes it's - not a good idea to assume you know what all those will do vs what - the real tool does; there may be other good reasons for just going - ahead and calling the external tool. -* If using an external tool, be prepared to skip the test if it is unavailable. -* Do not combine tests that need an external tool with ones that - do not - split these into separate test files. There is no concept - of partial skip for e2e tests, so if you successfully complete seven + not the behavior of the external tool. *Unless necessary* is + intentionally vague, use your judgement. If it's a ton of work to + mock an executable's behavior, perhaps in the combinations of + different flags, don't. However, if you don't actually need the + output (files or stderr/stdout) of an external, try to avoid. +* **DO** be prepared to skip a test using an external tool + if it is unavailable. We want the tests to be runnable in many + configurations, and not produce tons of fails jut because + that configuration didn't install some things. Yes, we know + tons of things will fail if you don't have a findable C compiler - + sorry! +* **DON'T** combine tests that need an external tool with ones that + do not, split them into separate test files. e2e tests can't do a + partial skip, so if you successfully complete seven of eight tests, and then come to a conditional "skip if tool missing" - or "skip if on Windows", and that branch is taken, then the - whole test file ends up skipped, and the seven that ran will - never be recorded. Some tests follow the convention of creating a - second test file with the ending ``-live`` for the part that requires - actually running the external tool. -* In testing, *fail fast* is not always the best policy - if you can think - of many scenarios that could go wrong and they are all run linearly in - a single test file, then you only hear about the first one that fails. - In some cases it may make sense to split them out a bit more, so you - can see several fails at once, which may show a helpful failure pattern - you wouldn't spot from a single fail. -* Use test fixtures where it makes sense, and in particular, try to - make use of shareable mocked tools, which, by getting lots of use, - will be better debugged (that is, don't have each test produce its - own ``myfortan.py`` or ``mylex.py`` etc. unless they need drastically - different behaviors). - -Unittest-specific hints: - -- Let the ``unittest`` module help! Lots of the existing tests just - use a bare ``assert`` call for checks, which works fine, but then - you are responsible for preparing the message if it fails. The base - ``TestCase`` class has methods which know how to display many things, - for example ``self.assertEqual()`` displays in what way the two arguments - differ if they are *not* equal. Checking for am expected exception can - be done with ``self.assertRaises()`` rather than crafting a stub of - code using a try block for this situation. -- The *fail fast* consideration applies here, too: try not to fail a whole - testcase on the first problem, if there are more checks to go. - Again, existing tests may use elaborate tricks for this, but modern - ``unittest`` has a ``subTest`` context manager that can be used to wrap - each distinct piece and not abort the testcase for a failing subtest - (to be fair, this functionality is a recent addition, after most SCons - unit tests were written - but it should be used going forward). + or "skip if on Windows", then the whole test file ends up marked as a skip. + On the other side, if you have a platform or tool-specific condition + that does not issue a ``skip_test``, then part of your test may be + skipped and you'll see no indication of that in the output log. + Splitting gives you a more complete picture. +* **DO** leave hints when a test requires external executables. + The current convention is to use the word "live" in the test name, + either as an ending ( (e.g. ``test/AS/as-live.py``) + or use it as the entire name of the test (e.g. ``test/SWIG/live.py``). +* **DO** use test fixtures where it makes sense. Real files are easier + to read than strings embedded in a test script used to create those + files - not just by humans, but by editors, checkers and formatters. + And in particular, try to make use of shareable mocked tools, which, + by getting lots of use, will be better debugged than single-use ones + (e.g., try to avoid each Fortran test containing its own mock compiler + ``myfortan.py`` - all those copies will have to be maintained). + +Unittest-specific hints +----------------------- + +* **DO** test at an appropriate level. A "unit" of SCons behavior is + something with predictable outcomes, which has multiple consumers. + A convenience function used by only one other function may not need + its own tests, as long as the caller is suitably tested. +* **DO** keep tests independent. This is just standard testing practice - + a test in one function should not depend on the results of an earlier + test in the same function. Make sure they have independent setup, + either by repeating the setup, or splitting into a separate function. +* **DO** consider whether "fail fast" is appropriate. Tests within a + test function can be made independent by using the ``unittest`` + module's ``subTest`` method - if one subtest fails, results will be + collected and execution continues, which may be more helpful in some + cases. This is a comparatively recent addition to ``unittest`` (Python + 3.4), so much of SCons' body of unit tests was written without the + advantage of that feature. +* **DO** make use of helpful ``unittest`` features. In particular, + using basic ``assert`` statements leaves you responsible for the output + if the test fails. Even in simple cases this tends to look awkward. + The various assert methods, on the other hand, provide decent formatting + of output on failure, often showing where two complex elements differ, + and you only need to add something for output if it needs specialization. + Compare:: + + assert out, "expected string", out + self.assertEqual(out, "expected string") + + There is an assert method for checking that an exception happens + (``self.assertRaises``), which is more readable than hand-coding something + with a ``try`` block to check the exception was raised. Please use this! diff --git a/timings/hundred/SConstruct b/timings/hundred/SConstruct index b321d10854..dce9ca79d3 100644 --- a/timings/hundred/SConstruct +++ b/timings/hundred/SConstruct @@ -25,7 +25,7 @@ target_count = int(ARGUMENTS['TARGET_COUNT']) def copy_files( env, target, source ): for t, s in zip(target, source): - open(str(t), 'wb').write(open(str(s), 'rb').read()) + open(t, 'wb').write(open(s, 'rb').read()) source_list = ['source_%04d' % t for t in range(target_count)] target_list = ['target_%04d' % t for t in range(target_count)] diff --git a/windows_ci_skip.txt b/windows_ci_skip.txt deleted file mode 100644 index 24a2b6c8a8..0000000000 --- a/windows_ci_skip.txt +++ /dev/null @@ -1,41 +0,0 @@ -# temporarily skip this in GitHub Windows runner -test/CPPDEFINES/pkg-config.py -test/Interactive/added-include.py -test/Interactive/Alias.py -test/Interactive/basic.py -test/Interactive/cache-debug.py -test/Interactive/cache-disable.py -test/Interactive/cache-force.py -test/Interactive/cache-show.py -test/Interactive/clean.py -test/Interactive/configure.py -test/Interactive/Default-None.py -test/Interactive/Default.py -test/Interactive/exit.py -test/Interactive/failure.py -test/Interactive/help.py -test/Interactive/implicit-VariantDir.py -test/Interactive/option--Q.py -test/Interactive/option-i.py -test/Interactive/option-j.py -test/Interactive/option-k.py -test/Interactive/option-n.py -test/Interactive/option-s.py -test/Interactive/repeat-line.py -test/Interactive/shell.py -test/Interactive/tree.py -test/Interactive/unknown-command.py -test/Interactive/variant_dir.py -test/MSVC/msvc.py -test/packaging/msi/explicit-target.py -test/packaging/msi/file-placement.py -test/packaging/msi/package.py -test/packaging/tar/xz_packaging.py -test/scons-time/run/config/python.py -test/scons-time/run/option/python.py -test/scons-time/run/option/quiet.py -test/scons-time/run/option/verbose.py -test/sconsign/script/no-SConsignFile.py -test/sconsign/script/SConsignFile.py -test/sconsign/script/Signatures.py -