diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4329b4309e..3539e014bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,10 @@ on: push: pull_request: workflow_dispatch: + release: + types: + - prereleased + - released schedule: # 5AM every Monday, to catch breaks due to changes in dependencies - cron: "0 5 * * 1" @@ -119,7 +123,7 @@ jobs: name: ${{ matrix.os }}-PerformanceTestResults.txt path: ./out/SoarPerformanceTests/PerformanceTestResults.txt -# Using powershell means we need to explicitly stop on failure + # Using powershell means we need to explicitly stop on failure Windows: name: build-windows runs-on: [windows-latest] @@ -260,3 +264,144 @@ jobs: with: name: Windows-PerformanceTestResults.txt path: ./out/SoarPerformanceTests/PerformanceTestResults.txt + + python_wheels: + name: wheel-*nix + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ + ubuntu-latest, + # latest available X86_64 target + macos-12, + # latest is ARM + macos-latest, + + windows-latest, + ] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Do a complete clone, not a shallow one. + # https://github.com/actions/checkout/#usage + # + # We need a complete clone, as versioningit uses `git describe`, + # which requires a commit tree to determine the latest tag. + fetch-depth: 0 + + - name: Show tags + run: git describe --long --dirty --always --tags + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + # - name: Setup tcl (ubuntu) + # if: matrix.os == 'ubuntu-latest' + # run: sudo apt-get update && sudo apt-get install tcl-dev + + # - name: Setup tcl (macos-latest) + # if: matrix.os == 'macos-latest' + # run: brew install tcl-tk + + - name: Setup SWIG (macos-latest) + if: matrix.os == 'macos-latest' + run: brew install swig + + - name: Build wheels + uses: pypa/cibuildwheel@v2.17.0 + with: + package-dir: Core/ClientSMLSWIG/Python/ + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl + + python_push_dev: + name: Publish to test.pypi.org + runs-on: ubuntu-latest + # - Upload only every monday to prevent spamming the index with too many build artifacts. + # - Allow uploading on manual workflow dispatch, to enable quick dev releases. + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + # Alternative, to upload on every commit to development; + # if: github.event_name != 'schedule' && github.event_name != 'release' + needs: + # We depend on the python wheel builds because we need their artifacts + - python_wheels + + # We depend on the builds themselves to gatekeep the upload if build + testing fails, + # usually then also the python wheel build should fail, but the normal builds do more thorough checking. + - Posix + - Windows + + # We use Trusted Publishing to manage our access to pypi: + # https://github.com/marketplace/actions/pypi-publish#trusted-publishing + environment: + name: test-pypi + url: https://test.pypi.org/p/soar-sml/ + permissions: + id-token: write + + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - uses: actions/download-artifact@v4 + with: + pattern: cibw-wheels-* + path: wheelhouse/ + merge-multiple: true + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: wheelhouse + skip-existing: true + verbose: true + + python_push_release: + name: Publish to pypi.org + runs-on: ubuntu-latest + if: github.event_name == 'release' + needs: + # We depend on the python wheel builds because we need their artifacts + - python_wheels + + # We depend on the builds themselves to gatekeep the upload if build + testing fails, + # usually then also the python wheel build should fail, but the normal builds do more thorough checking. + - Posix + - Windows + + # We use Trusted Publishing to manage our access to pypi: + # https://github.com/marketplace/actions/pypi-publish#trusted-publishing + environment: + name: pypi + url: https://pypi.org/p/soar-sml/ + permissions: + id-token: write + + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - uses: actions/download-artifact@v4 + with: + pattern: cibw-wheels-* + path: wheelhouse/ + merge-multiple: true + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: wheelhouse + skip-existing: true + verbose: true diff --git a/.gitignore b/.gitignore index 4cb8fd4ace..e153dd690c 100644 --- a/.gitignore +++ b/.gitignore @@ -503,6 +503,12 @@ local.properties # TeXlipse plugin .texlipse +# cibuildwheel output +wheelhouse/ + +# pip/enscons build output +soar_sml/ + ### Soar ### out/ build/ diff --git a/Core/ClientSMLSWIG/Python/DEVELOPING.md b/Core/ClientSMLSWIG/Python/DEVELOPING.md new file mode 100644 index 0000000000..8cea58705f --- /dev/null +++ b/Core/ClientSMLSWIG/Python/DEVELOPING.md @@ -0,0 +1,81 @@ +# Developing `soar-sml` + +Before installing development releases, it is always recommended to have an updated `pip`: + +``` +$ pip install --upgrade pip +``` + +## Instant: Installing from the latest commit + +To install `soar-sml` *directly* from the latest `development` branch commit, without cloning it into a project directory, +run the following command: + +``` +$ pip install "git+https://github.com/SoarGroup/Soar#subdirectory=Core/ClientSMLSWIG/Python" +``` + +## Local: Installing from a project directory + +To make `soar-sml` available for import in python, while actively developing the source code for it, +the following command is more suitable. + +Assuming `Soar` has been cloned under `soar/`: +```bash +$ pip install -e soar/Core/ClientSMLSWIG/Python +``` + +The `-e` stands for "editable", this means pip will have python redirect the "actual" location of the `soar_sml` +package to be inside this project directory. Specifically, in this case, to the project root directory (`soar/`). + +This command will install a python package directory under the project root (`soar/soar_sml/`), where it copies +build artifacts into, into a format that python expects. + +During development, other than running the `pip` command again, +these files can be updated from builds with the `sml_python_dev` target: + +```bash +$ scons sml_python_dev +``` + +Running the above command, and then re-running your python scripts, +will automatically incorporate any compiled changes. + +## Remote: Installing a development version from the package index + +Weekly development versions are uploaded to [`test.pypi.org/p/soar-sml`](https://test.pypi.org/p/soar-sml), +the latest of which can be installed with the following command: + +```bash +$ pip install --pre -i https://test.pypi.org/simple/ soar-sml +``` + +## Packaging + +Versioning is dynamic, and will calculate a version identifier according to the latest git tag. +- If the latest git tag is the current commit, it will version with that tag. +- If the latest git tag is from a past commit, it will increment the smallest segment (e.g. `.2` in `9.6.2`) and + add miscellaneous metadata (such as the commit datetime, the amount of commits since the last tag, and wether the + workspace repository was "dirty" or not: if there were uncommitted changes.). + +`build.yml` will automatically build wheels for `soar-sml` on every commit, utilizing +[cibuildwheel](https://cibuildwheel.pypa.io/) to ease the process, and build for many python versions at once. It will +do a quick test on every build, importing `soar_sml` and running hello world, to ensure that the wheel passes basic +checks, and "works". + +`build.yml` also contains job definitions to upload to PyPI, the Python Package Index. + +On a GitHub release, a workflow is triggered to build the final version of wheels, and mark them +**with the version given in the corresponding git tag**. +(So if the release is named "Version 9.6.2", and the tag is "v9.6.2", then the auto-version script will see "v9.6.2") +The string `releases/` is also removed from the git tag before parsing. + +*Do not release multiple versions pointing to the same commit*. Git tags do not have ordering compatible +with the dynamic versioning, and so with the tags `9.6.2-rc2` and `9.6.2` pointing to the same commit, +it may pick up on the `rc` tag, and version the build with that. + +Development versions are built and uploaded to [`test.pypi.org/p/soar-sml`](https://test.pypi.org/p/soar-sml) weekly. +These will also upload on manual triggers. + +If needed, uploading to pypi can be done manually using the +[twine CLI tool](https://twine.readthedocs.io/en/stable/#using-twine). diff --git a/Core/ClientSMLSWIG/Python/README.md b/Core/ClientSMLSWIG/Python/README.md new file mode 100644 index 0000000000..9dda36a0dc --- /dev/null +++ b/Core/ClientSMLSWIG/Python/README.md @@ -0,0 +1,77 @@ +# `soar-sml` + +This directory contains the SWIG bindings to Soar's SML interface. + +This project is also published on PyPI as [soar-sml](https://pypi.org/project/soar-sml/), +for ease of distribution and installation. + +Versions on PyPI will follow Soar's versioning methodology. + +`soar-sml` effectively bundles Soar with its distribution; you can download it, import it into Python, +and have a fully-working Soar kernel up and running. + +## Importing + +The soar-sml package can be imported like so: + +```Python +import soar_sml +``` + +## `Python_sml_ClientInterface` compatibility + +The raw build artifacts of this SWIG interface exposed these bindings under a `Python_sml_ClientInterface` namespace +in the past, we've switched to using `soar_sml` to be more in line with python's packaging ideology. However, +we did not want to break compatibility with all scripts by doing so, and have also published a compatibility shim. + +Under `compat/`, there exists a small project `soar-compat` that re-exports the classes and functions of under a +`Python_sml_ClientInterface` namespace, effectively making every existing project compatible with the new `soar-sml` +package. + +This means that code like thus will still continue to work: + +```Python +import sys +sys.path.append('/home/user/SoarSuite/bin') +import Python_sml_ClientInterface as sml + +k = sml.Kernel.CreateKernelInNewThread() +a = k.CreateAgent('soar') +print(a.ExecuteCommandLine('echo hello world')) +``` + +In this case, the `sys.path.append` line is redundant (it accomplishes nothing; python already properly imports +the package), and can be removed. + +This compatibility package is available on PyPI, and can be installed directly like so: + +```bash +$ pip install soar-compat --no-deps +``` +(the `--no-deps` flag is added to prevent `soar-sml` being pulled from pypi, as that is a dependency of +`soar-compat`) + +However, for ease of installation, it can be installed as an ["extra"](https://stackoverflow.com/a/52475030/8700553) +like so: + +```bash +$ pip install "soar-sml[compat]" +``` + +Running this above command will make every Soar SML Python script on your system (or virtual environment) that uses +`import Python_sml_ClientInterface` functional, portable, with no further modifications necessary. + +## Building locally + +Building and installing this package via pip, locally, is easy: + +```BASH +$ pip install soar/Core/ClientSMLSWIG/Python +``` + +This will generate all the required build artifacts, install them to your python installation (system-wide, or +inside a virtual environment), and prepare it for `import soar_sml` statements in your scripts/programs. + +## Developing and Packaging + +Notes for developing and packaging `soar-sml` can be found in [`DEVELOPING.md`](./DEVELOPING.md) diff --git a/Core/ClientSMLSWIG/Python/SConscript b/Core/ClientSMLSWIG/Python/SConscript index b1d4c5f8b7..7ce28417a9 100644 --- a/Core/ClientSMLSWIG/Python/SConscript +++ b/Core/ClientSMLSWIG/Python/SConscript @@ -17,14 +17,27 @@ python_sml_alias = clone['SML_PYTHON_ALIAS'] inc_path = sysconfig.get_path('include') if os.name == 'nt': - lib_path = os.path.join(sysconfig.get_config_vars('BINDIR')[0], 'libs') + lib_path = [ + os.path.join(sysconfig.get_config_vars('BINDIR')[0], 'libs'), + # Finds the correct library path for Github Runner environments (cibuildwheel) + os.path.join(sysconfig.get_config_vars('LIBDEST')[0], '..\\libs'), + ] pylib = 'python' + sysconfig.get_config_vars('VERSION')[0] else: lib_path = sysconfig.get_config_vars('LIBDIR')[0] pylib = sysconfig.get_config_vars('LIBRARY')[0] lib_install_dir = clone['OUT_DIR'] -clone.Append(CPPPATH = inc_path, LIBPATH = lib_path, LIBS = pylib) +clone.Append(CPPPATH = inc_path) + +# linux: manylinux containers do not ship with python's libraries: +# https://github.com/pypa/manylinux/issues/191#issuecomment-386489125 +# macos: python libraries (only their executables) do not appear to be present when installing python on +# GHA Runners. (python installations are handled by cibuildwheel) +# +# Omitting python's libraries is safe, as python itself injects its symbols when importing the library. +if (not env['ENSCONS_ACTIVE']) or os.name == 'nt': + clone.Append(LIBPATH = lib_path, LIBS = pylib) if os.name == 'posix': clone.Append(CPPFLAGS = Split('-Wno-unused -fno-strict-aliasing')) @@ -46,4 +59,13 @@ install_source = env.Install(lib_install_dir, source) install_lib = env.Install(lib_install_dir, shlib) install_test = env.Install(lib_install_dir, env.File('TestPythonSML.py')) -env.Alias(python_sml_alias, install_lib + install_source + install_test) +# We add soarlib to the python_sml explicitly, as some operating systems don't pick up on this dependency, +# and crash the build. +Import('soarlib') + +env.Alias(python_sml_alias, soarlib + install_lib + install_source + install_test) + +python_shlib = shlib +python_source = source +Export('python_shlib') +Export('python_source') diff --git a/Core/ClientSMLSWIG/Python/compat/Python_sml_ClientInterface.py b/Core/ClientSMLSWIG/Python/compat/Python_sml_ClientInterface.py new file mode 100644 index 0000000000..e06b29cfdc --- /dev/null +++ b/Core/ClientSMLSWIG/Python/compat/Python_sml_ClientInterface.py @@ -0,0 +1 @@ +from soar_sml import * diff --git a/Core/ClientSMLSWIG/Python/compat/README.md b/Core/ClientSMLSWIG/Python/compat/README.md new file mode 100644 index 0000000000..cabb33923d --- /dev/null +++ b/Core/ClientSMLSWIG/Python/compat/README.md @@ -0,0 +1,9 @@ +# `soar-compat` + +A small shim library that provides compatibility with packaging/distribution methods for Soar's SML Bindings. + +The raw bindings, as built in Soar, are imported as `import Python_sml_ClientInterface`, +while the pypi package `soar-sml` imports as `soar_sml`. + +While it possible to do `import soar_sml as Python_sml_ClientInterface`, this small library avoids that need, +and allows all existing scripts to import Soar's SML Bindings in the way they're used to. diff --git a/Core/ClientSMLSWIG/Python/compat/pyproject.toml b/Core/ClientSMLSWIG/Python/compat/pyproject.toml new file mode 100644 index 0000000000..b3e9786276 --- /dev/null +++ b/Core/ClientSMLSWIG/Python/compat/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "soar-compat" +description = "Compatibility layer with soar-sml" +version = "1.0" +license = { text = "BSD" } +authors = [ + {name = "Jonathan de Jong", email = "jo@jo.wtf"} +] +readme = "README.md" +dependencies = ["soar-sml"] +keywords = ["soar"] +classifiers = [ + "Programming Language :: Python :: 3", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/Core/ClientSMLSWIG/Python/pyproject.toml b/Core/ClientSMLSWIG/Python/pyproject.toml new file mode 100644 index 0000000000..e146b55a19 --- /dev/null +++ b/Core/ClientSMLSWIG/Python/pyproject.toml @@ -0,0 +1,158 @@ +[project] +name = "soar-sml" +description = "Raw SML Bindings to the Soar Cognitive Architecture" +dynamic = ["version"] +license = "BSD" +maintainers = [ + { name = "Nathan Glenn", email = "garfieldnate@gmail.com" }, + { name = "Center for Integrated Cognition" } +] +authors = [ + { name = "John E. Laird" }, + { name = "Allen Newell" }, + { name = "Paul Rosenbloom" }, + + { name = "A. Deschamps" }, + { name = "A. Mininger" }, + { name = "A. Nickels" }, + { name = "A. Nuxoll" }, + { name = "A. Turner" }, + { name = "B. Stearns" }, + { name = "C. Dettmering" }, + { name = "D. Pearson" }, + { name = "D. Ray" }, + { name = "I. Hines" }, + { name = "J. Boggs" }, + { name = "J. Crossman" }, + { name = "J. Kirk" }, + { name = "J. Li" }, + { name = "J. Voigt" }, + { name = "J. Xu" }, + { name = "K. Coulter" }, + { name = "L. Goeddel" }, + { name = "M. Assanie" }, + { name = "M. Bloch" }, + { name = "M. Lanting" }, + { name = "M. Schmidt" }, + { name = "M. Tinkerhess" }, + { name = "N. Derbinsky" }, + { name = "N. Glenn" }, + { name = "N. Gorski" }, + { name = "N. Timpko" }, + { name = "R. Marinier" }, + { name = "R. Wray" }, + { name = "S. Jones" }, + { name = "S. Lathrop" }, + { name = "S. Mohan" }, + { name = "S. Nason" }, + { name = "S. Wallace" }, + { name = "S. Wintermute" }, + { name = "Y. Wang" }, +] +readme = "README.md" +requires-python = ">=3.8" +dependencies = [] +keywords = ["soar", "sml", "cog-arch", "cognitive architecture", "cognitive", "soar-sml", "ai"] +classifiers = [ + "Programming Language :: Python :: 3", +] + +[project.urls] +Homepage = "https://soar.eecs.umich.edu/" +Repository = "https://github.com/SoarGroup/Soar" + +[project.optional-dependencies] +compat = ["soar-compat"] + +[build-system] +requires = [ + # Enscons provides an interface with scons, allowing it to be puppetted by cibuildwheel, + # and other python build/install frontends (such as pip itself). + # + # We use a forked version of enscons to add python 3.12 support. + "enscons @ git+https://github.com/ShadowJonathan/enscons-soar@544f39f", + + # Required sub-dependencies of enscons. + "toml>=0.1", + "wheel", + "versioningit", + "scons==4.4.0" +] +build-backend = "enscons.api" + +[tool.versioningit] +# https://versioningit.readthedocs.io/en/stable/configuration.html#the-tool-versioningit-next-version-subtable +next-version = "smallest" + +[tool.versioningit.format] +# For "distance" (development releases) we cannot use the local identifier, as pypi does not accept those. +# Instead, we encode the distance in .dev, and the build time in a 4th version component. +distance = "{next_version}.{committer_date:%Y%m%d%H%M}.dev{distance}" + +dirty = "{base_version}+dirty{build_date:%Y%m%d.%H%M}" + +distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.{branch}.dirty{build_date:%Y%m%d.%H%M}" + +[tool.versioningit.tag2version] +rmprefix = "releases/" + +# Optional regexes to extract a full version from git tags +# regex = "\\d+\\.\\d+\\.\\d+" +# regex = "\\d[A-Za-z0-9_.!+\\-]+" + +[tool.enscons] +# scons can only properly build when chdir'd to the right directory. +# +# Here we specify which relative directory enscons needs to chdir into to get scons to properly build. +# +# In SConstruct we have a line referring back to this directory, for enscons to properly grab the right metadata. +build-from = "../../../" + +[tool.cibuildwheel] +# We increase verbosity in the CI, so that tracking down problems is easier. +build-verbosity = 3 +skip = [ + # Skip PyPy: Build works, but does not properly work at runtime. + "pp*", + # Skip 32-bit Windows: scons does not properly build 32-bit version, + # and linkage problems occur. + "*-win32", + # Skip MUSL (as opposed to GLIB) linux, due to unknown concurrency symbols. + "*-musllinux_*", + # Cibuildwheel cannot build for Python 3.8 on Apple Silicon, and just creates an x86 wheel there, + # which would be a duplicate with the x86 runner. + "cp38-macosx_arm64" +] +# This test command: +# - imports soar_sml +# - creates the kernel +# - creates an agent +# - executes a command on that agent +# - fails if the result of this command isn't as expected +# to test if Soar properly boots and runs. +test-command = """\ +python -c "import soar_sml;\ +k=soar_sml.Kernel.CreateKernelInNewThread();\ +a=k.CreateAgent('soar');\ +assert(a.ExecuteCommandLine('echo hello world').strip()=='hello world')" +""" + +# Skip testing python 3.8 on ARM64. This is due to a limitation with cibuildwheel: +# https://github.com/pypa/cibuildwheel/pull/1169#issuecomment-1178727618 +test-skip = "cp38-macosx_*:arm64" + +[tool.cibuildwheel.linux] +# Specific instructions for cibuildwheel when running on linux: +# - add the /project/out directory for the linker to look at. +# - This allows it to find the soar library file, and link it. +repair-wheel-command = """\ +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/project/out auditwheel repair -w {dest_dir} {wheel}\ +""" + +[tool.cibuildwheel.macos] +# Specific instructions for cibuildwheel when running on macos: +# - remove the intermediate build artifacts before every new build. +# - this is because cibuildwheel builds for both architectures (arm64 / x86_64), and build artifacts from +# either arch aren't properly removed, and will fail the linker (which refuses to link artifacts from +# the wrong arch). +before-build = "rm -rf build/" diff --git a/SConstruct b/SConstruct index 45d52686a1..f59e340eff 100644 --- a/SConstruct +++ b/SConstruct @@ -12,6 +12,13 @@ import fnmatch from SCons.Node.Alias import default_ans import time +try: + enscons_active = True + import toml + import enscons, enscons.cpyext +except ImportError as e: + enscons_active = False + # Add the current directory to the path so we can from build_support script_dir = Dir('.').srcnode().abspath sys.path.append(script_dir) @@ -141,8 +148,14 @@ AddOption('--opt', action='store_false', dest='dbg', default=False, help='Enable AddOption('--verbose', action='store_true', dest='verbose', default=False, help='Output full compiler commands') AddOption('--no-svs', action='store_true', dest='nosvs', default=False, help='Build Soar without SVS functionality') +if enscons_active: + tools = ['default', 'packaging', enscons.generate] +else: + tools = None env = Environment( + tools=tools, + ENSCONS_ACTIVE=enscons_active, ENV=os.environ.copy(), SCU=GetOption('scu'), DEBUG=GetOption('dbg'), @@ -367,6 +380,63 @@ for d in os.listdir('.'): if os.path.exists(script): SConscript(script, variant_dir=join(GetOption('build-dir'), d), duplicate=0) +# section Python-related packaging +Import('python_shlib') +Import('python_source') +Import('soarlib') +py_lib_namespace = "soar_sml" + +# Targets to be built and included in wheel files. +py_sources = [] +# Targets to be built but NOT included in wheel files. +py_extra = [] + +if sys.platform == 'darwin' or os.name == 'nt': + # Add soar's library to the wheel directory. + # + # With MacOS builds, these are shipped in the final wheel archive. Ditto for Windows. + # + # With linux builds, this step isn't neccecary, as its linker will pick up on the library from out/, + # and statically link it against the SWIG-generated shared library. + py_sources += [ + env.Install(py_lib_namespace, soarlib) + ] + +if sys.platform == 'darwin': + # For MacOS, also add to the out/ directory, + # so the linker can pick up on it properly. + py_extra += [ + env.Install(env['OUT_DIR'], soarlib) + ] + +py_sources += [ + env.Install(py_lib_namespace, python_shlib), + env.InstallAs(py_lib_namespace + "/__init__.py", python_source) +] + +env.Alias(SML_PYTHON_ALIAS + "_dev", py_sources) + +if enscons_active: + env['PACKAGE_METADATA'] = enscons.get_pyproject(env)['project'] + # Instead of giving an explicit tag, we tell enscons that we're not building a "pure" (python-only) library, + # and so we let it determine the wheel tag by itself. + env['ROOT_IS_PURELIB'] = False + + # Whl and WhlFile add multiple targets (sdist, dist_info, bdist_wheel, editable) to env + # for enscons (python build backend for scons; required for building with cibuildwheel). + whl = env.Whl("platlib", py_sources, root="") + + # Adding Depends makes scons build this file, but enscons will not include it in the final wheel file. + env.Depends(whl, py_extra) + + env.WhlFile(source=whl) + + # We make sure that an editable (`pip install -e`) installation always properly installs + # the files in the correct places. + env.Depends("editable", py_sources) + +# endsection Python-related packaging + if 'MSVSSolution' in env['BUILDERS']: msvs_solution = env.MSVSSolution(