diff --git a/.github/ISSUE_TEMPLATE/docs-change.md b/.github/ISSUE_TEMPLATE/docs-change.md index 12364389..9279df12 100644 --- a/.github/ISSUE_TEMPLATE/docs-change.md +++ b/.github/ISSUE_TEMPLATE/docs-change.md @@ -4,7 +4,7 @@ about: Report an omission, contradiction, or inaccuracy in the documentation, or request an addition to the documentation, title: "[DOCS]" -labels: enhancement,documentation,bug +labels: documentation assignees: '' --- diff --git a/.gitignore b/.gitignore index 1ac7368b..f46cc6be 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__/ .vscode/ .mypy_cache/ _prebuilt/ -.pytest_cache +.pytest_cache/ +.vagrant/ \ No newline at end of file diff --git a/Makefile b/Makefile index e60d0584..b7f61a53 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ .SILENT: -.PHONY: docs docs-server docs-watch docs-sync-server +.PHONY: \ + docs docs-server docs-watch docs-sync-server nix-ci linux-ci macos-ci \ + vagrant-freebsd-ci _invalid: echo "Specify a target name to execute" + exit 1 docs: sphinx-build -b html \ @@ -21,8 +24,30 @@ docs-server: docs docs-watch: docs +sh tools/docs-watch.sh -docs-sync-server: docs +docs-sync-server: + mkdir -p _build/docs cd _build/docs && \ browser-sync start --server \ --reload-delay 300 \ --watch **/*.html + +macos-ci: nix-ci +linux-ci: nix-ci + +nix-ci: + python3 -u tools/ci.py \ + -B build \ + -T tools/gcc-9.dds \ + -T2 tools/gcc-9.jsonc + +vagrant-freebsd-ci: + vagrant up freebsd11 + vagrant ssh freebsd11 -c '\ + cd /vagrant && \ + python3.7 tools/ci.py \ + -B build \ + -T tools/freebsd-gcc-9.dds \ + -T2 tools/freebsd-gcc-9.jsonc \ + ' + vagrant scp freebsd11:/vagrant/_build/dds _build/dds-freebsd-x64 + vagrant halt diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 00000000..92ff4251 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,28 @@ +Vagrant.configure("2") do |config| + # Refer: https://docs.vagrantup.com. + + config.vm.synced_folder ".", "/vagrant", + type: 'rsync', + rsync__exclude: ['_build/', '.mypy_cache/', '.pytest_cache/', '_prebuilt/'] + + config.vm.define 'freebsd11' do |freebsd11| + freebsd11.vm.box = 'generic/freebsd11' + freebsd11.vm.provision 'shell', inline: <<-SHELL + set -eu + for package in python37 py37-pip ccache gcc9 gcc8 git; do + echo "Installing $package" + pkg install -qy $package + done + ln -fs g++9 /usr/local/bin/g++-9 + ln -fs gcc9 /usr/local/bin/gcc-9 + ln -fs g++8 /usr/local/bin/g++-8 + ln -fs gcc8 /usr/local/bin/gcc-8 + sudo -u vagrant pip install -q --user pytest pytest-xdist + SHELL + end + + config.vm.provider 'virtualbox' do |vbox| + vbox.memory = 1024 * 4 + vbox.cpus = 8 + end +end diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cb60e937..246e9c12 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,12 +12,12 @@ jobs: echo Executing Build and Tests reg add HKLM\SYSTEM\CurrentControlSet\Control\FileSystem /v LongPathsEnabled /t REG_DWORD /d 1 /f || exit 1 python -m pip install pytest pytest-xdist || exit 1 - python -u tools/ci.py -B download -T tools\msvc.dds || exit 1 + python -u tools/ci.py -B build -T tools\msvc.dds -T2 tools\msvc.jsonc || exit 1 displayName: Full CI - publish: _build/dds.exe artifact: DDS Executable - Windows VS2019 - - job: Linux_GCC8 + - job: Linux_GCC9 pool: vmImage: ubuntu-18.04 steps: @@ -27,12 +27,12 @@ jobs: sudo apt install -y python3-minimal g++-9 ccache python3 -m pip install pytest pytest-xdist displayName: Prepare System - - script: python3 -u tools/ci.py -B download -T tools/gcc-9.dds + - script: make linux-ci displayName: Full CI - publish: _build/dds artifact: DDS Executable - Linux - - job: macOS_GCC8 + - job: macOS_GCC9 pool: vmImage: macOS-10.14 steps: @@ -41,7 +41,7 @@ jobs: - script: | set -eu python3 -m pip install pytest pytest-xdist - python3 -u tools/ci.py -B download -T tools/gcc-9.dds + make macos-ci displayName: Build and Run Unit Tests - publish: _build/dds artifact: DDS Executable - macOS diff --git a/catalog.json b/catalog.json index 7e9e5127..10952008 100644 --- a/catalog.json +++ b/catalog.json @@ -1,5 +1,16 @@ { "packages": { + "ctre": { + "2.7.0": { + "depends": {}, + "description": "A compile-time PCRE (almost) compatible regular expression matcher", + "git": { + "auto-lib": "hanickadot/ctre", + "ref": "v2.7", + "url": "https://github.com/hanickadot/compile-time-regular-expressions.git" + } + } + }, "fmt": { "0.10.0": { "depends": {}, @@ -685,6 +696,31 @@ "url": "https://github.com/gabime/spdlog.git" } } + }, + "vob-json5": { + "0.1.5": { + "depends": {}, + "description": "A C++ implementation of a JSON5 parser", + "git": { + "auto-lib": null, + "ref": "0.1.5", + "url": "https://github.com/vector-of-bool/json5.git" + } + } + }, + "vob-semester": { + "0.1.0": { + "depends": { + "neo-concepts": "^0.2.1", + "neo-fun": "^0.1.0" + }, + "description": "A C++ library to process recursive dynamic data", + "git": { + "auto-lib": null, + "ref": "0.1.0", + "url": "https://github.com/vector-of-bool/semester.git" + } + } } }, "version": 1 diff --git a/docs/_static/tweaks.css b/docs/_static/tweaks.css deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/conf.py b/docs/conf.py index 78f3cc2b..89d8056e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.1.0' +release = '0.1.0-alpha.2' # -- General configuration --------------------------------------------------- extensions = [] @@ -23,9 +23,5 @@ # -- Options for HTML output ------------------------------------------------- html_theme = 'pyramid' html_theme_options = {} -html_static_path = ['_static'] +html_static_path = [] html_sidebars = {} - - -def setup(app): - app.add_stylesheet('tweaks.css') diff --git a/docs/dev/building.rst b/docs/dev/building.rst new file mode 100644 index 00000000..c238cccb --- /dev/null +++ b/docs/dev/building.rst @@ -0,0 +1,209 @@ +Building ``dds`` from Source +############################ + +While prebuilt ``dds`` executables are `available on the GitHub page +`_, one may wish to build ``dds`` from source. + +.. _releases: https://github.com/vector-of-bool/dds/releases + +The ``dds`` build process is designed to be as turn-key simple as possible. + + +Platform Support +**************** + +``dds`` aims to be as cross-platform as possible. It currently build and +executes on Windows, macOS, Linux, and FreeBSD. Support for additional +platforms is possible but will require modifications to ``bootstrap.py`` that +will allow it to be built on such platforms. + + +Build Requirements +****************** + +Building ``dds`` has a simple set of requirements: + +- **Python 3.6** or newer to run the bootstrap/CI scripts. +- A C++ compiler that has rudimentary support for C++20 concepts. Newer + releases of Visual C++ that ship with **VS 2019** will be sufficient on + Windows, as will **GCC 9** with ``-fconcepts`` on other platforms. + +.. note:: + On Windows, you will need to execute the build from within a Visual C++ + enabled environment. This will involve launching the build from a Visual + Studio Command Prompt. + +.. note:: + At the time of writing, C++20 Concepts has not yet been released in Clang, + but should be available in LLVM/Clang 11 and newer. + + +Build Scripts and the CI Process +******************************** + +The main CI process is driven by Python. The root CI script is ``tools/ci.py``, +and it accepts several command-line parameters. Only a few of are immediate +interest: + +``--bootstrap-with=`` or ``-B `` + Tell ``ci.py`` how to obtain the previous ``dds`` executable that can build + the *current* ``dds`` source tree. This accepts one of three values: + ``skip``, ``download``, or ``build``. Refer to :ref:`bootstrapping`. + +``--build-only`` + A flag that tells ``ci.py`` to exit after it has successfully built the + current source tree, and to not execute the phase-2 build nor the automated + tests. + +``--toolchain=`` or ``-T `` + Tell ``ci.py`` what toolchain to give to the prior ``dds`` to build the + current ``dds``. + +The ``ci.py`` script performs the following actions, in order: + +#. Prepare the build output directory +#. Bootstrap the prior version of ``dds`` that will build the current version. +#. Import the embedded ``catalog.json`` into a catalog database stored within + ``_build/``. This will be used to resolve the third-party packages that + ``dds`` itself uses. +#. Invoke the build of ``dds`` using the prebuilt ``dds`` from the prior + bootstrap phase. If ``--build-only`` was specified, the CI script stops + here. +#. Use the new ``dds`` executable to rebuild itself *again* (phase-2 self-build + test). A bit of a "sanity test." +#. Execute the test suite using ``pytest``. + + +.. _bootstrapping: + +Bootstrapping ``dds`` +********************* + +In the beginning, ``dds`` was built by a Python script that globbed the sources +and invoked the compiler+linker on those sources. Once ``dds`` was able to +build and link itself, this Python script was replaced instead with ``dds`` +building itself. ``dds`` has never used another build system. + +The ``ci.py`` script accepts one of three methods for the ``--bootstrap-with`` +flag: ``skip``, ``download``, or ``build``. + +Once bootstrapping is complete, a ``dds`` executable will be written to +``_prebuilt/dds``. This executable refers to a **previous** version of ``dds`` +that is able to build the newer ``dds`` source tree. + +.. note:: + For all development work on ``dds``, the ``_prebuilt/dds`` executable should + always be used. This means that newer ``dds`` features are not available + for use within the ``dds`` repository. + + +Bootstrap: ``skip`` +=================== + +If given ``skip``, ``ci.py`` will not perform any bootstrapping steps. It will +assume that there is an existing ``_prebuilt/dds`` executable. This option +should be used once bootstrapping has been performed at least once with another +method, as this is much faster than rebuilding/redownloading every time. + + +Bootstrap: ``download`` +======================= + +The ``ci.py`` script has a reference to a download URL of the prior version of +``dds`` that has been designated for the bootstrap. These executables originate +from `the GitHub releases `_ page. + +If given ``download``, then ``ci.py`` will download a predetermined ``dds`` +executable and use it to perform the remainder of the build. + + +Bootstrap: ``build`` +==================== + +Another script, ``tools/bootstrap.py`` is able to build ``dds`` from the ground +up. It works by progressively cloning previous versions of the ``dds`` +repository and using them to build the next commit in the chain. + +While this is a neat trick, it isn't necessary for most development, as the +resulting executable will be derived from the same commit as the executable +that would be obtained using the ``download`` method. This is also more fragile +as the past commits may make certain assumptions about the system that might +not be true outside of the CI environment. The build process may be tweaked in +the future to correct these assumptions. + + +Selecting a Build Toolchain +*************************** + +``dds`` includes three toolchains that it uses to build itself in its CI +environment: ``tools/gcc-9.jsonc`` for Linux and macOS, +``tools/freebsd-gcc-9.jsonc`` for FreeBSD, and ``tools/msvc.jsonc`` for +Windows. + +While these toolchains will work perfectly well in CI, you may need to tweak +these for your build setup. For example: ``gcc-9.jsonc`` assumes that the GCC 9 +executables are named ``gcc-9`` and ``g++-9``, which is incorrect on some +Linux distributions. + +It is recommended to tweak these files as necessary to get the build working on +your system. However, do not include those tweaks in a commit unless they are +necessary to get the build running in CI. + + +Giving a Toolchain to ``ci.py`` +=============================== + +Just like passing a toolchain to ``dds``, ``ci.py`` also requires a toolchain. +Simply pass the path to your desired toolchain using the ``--toolchain``/ +``-T`` argument: + +.. code-block:: bash + + $ python3 tools/ci.py [...] -T tools/gcc-9.jsonc + + +Building for Development +************************ + +While ``ci.py`` is rigorous in maintaining a clean and reproducible environment, +we often don't need such rigor for a rapid development iteration cycle. Instead +we can invoke the build command directly in the same way that ``ci.py`` does +it: + +.. code-block:: bash + + $ _prebuilt/dds build -t [toolchain] \ + --catalog _build/catalog.db \ + --repo-dir _build/ci-repo + +The ``--catalog`` and ``--repo-dir`` arguments are not strictly necessary, but +help to isolate the ``dds`` dev environment from the user-local ``dds`` +environment. This is important if modifications are made to the catalog +database schema that would conflict with the one of an external ``dds`` +version. + +.. note:: + You'll likely want to run ``ci.py`` *at least once* for it to prepare the + necessary ``catalog.db``. + +.. note:: + As mentioned previously, if using MSVC, the above command must execute with + the appropriate VS development environment enabled. + + +Running the Test Suite +********************** + +The ``--build-only`` flag for ``ci.py`` will disable test execution. When this +flag is omitted, ``ci.py`` will execute a self-build sanity test and then +execute the main test suite, which is itself written as a set of ``pytest`` +tests in the ``tests/`` subdirectory. + + +Unit Tests +========== + +Various pieces of ``dds`` contain unit tests. These are stored within the +``src/`` directory itself in ``*.test.cpp`` files. They are built and executed +by the bootstrapped ``dds`` executable unconditionally. These tests execute +in milliseconds and do not burden the development iteration cycle. diff --git a/docs/dev/index.rst b/docs/dev/index.rst new file mode 100644 index 00000000..204875e4 --- /dev/null +++ b/docs/dev/index.rst @@ -0,0 +1,9 @@ +``dds`` Development +################### + +This section will discuss how to modify and build ``dds`` itself. + +.. toctree:: + :maxdepth: 2 + + building \ No newline at end of file diff --git a/docs/err/dup-lib-name.rst b/docs/err/dup-lib-name.rst index fd5f2c0e..88200d28 100644 --- a/docs/err/dup-lib-name.rst +++ b/docs/err/dup-lib-name.rst @@ -10,17 +10,17 @@ identifier would be ``ACME/Gadgets``. The "namespace" of a library in this case is arbitrary and not necessarily associated with any C++ ``namespace``. -If more than one library declares itself to have the same ``Name`` and lives in -the same ``Namespace``, ``dds`` will issue an error. +If more than one library declares itself to have the same ``name`` and lives in +the same ``namespace``, ``dds`` will issue an error. To avoid this error in your own project and to avoid causing this error in your -downstream consumers, the ``Namespace`` of your package should be considered -carefully and be unique. Do not use a ``Namespace`` that is likely to be used +downstream consumers, the ``namespace`` of your package should be considered +carefully and be unique. Do not use a ``namespace`` that is likely to be used by another developer or organization, especially generic names. If you are seeing this issue and it names a library that you do not own, it means that two or more of your dependencies are attempting to declare a library -of the same ``Name`` in the same ``Namespace``. This issue should be raised +of the same ``name`` in the same ``namespace``. This issue should be raised with the maintainers of the packages in question. .. seealso:: diff --git a/docs/err/invalid-lib-manifest.rst b/docs/err/invalid-lib-manifest.rst new file mode 100644 index 00000000..46d82f35 --- /dev/null +++ b/docs/err/invalid-lib-manifest.rst @@ -0,0 +1,11 @@ +Error: Invalid library manifest +############################### + +Every ``dds`` package must contain a valid library manifest for each library +that it exports. These manifests are stored in the library root to which they +correspond, and are stored in JSON5 format in ``library.json5`` (or similarly +named file). + +The contents of this file must follow a prescribed content, or ``dds`` will +reject the manifest. Refer to the :ref:`pkgs.libs` documentation page for more +information about how to declare libraries. diff --git a/docs/err/invalid-pkg-filesystem.rst b/docs/err/invalid-pkg-filesystem.rst index 76c27aa8..e953fdae 100644 --- a/docs/err/invalid-pkg-filesystem.rst +++ b/docs/err/invalid-pkg-filesystem.rst @@ -6,8 +6,8 @@ build and process packages and libraries. This structure isn't overly strict, and is thoroughly explained on the :doc:`/guide/packages` page. For exporting/generating a source distribution from a package, the *package -root* requires a ``package.dds`` file and each *library root* requires a -``library.dds`` file. +root* requires a ``package.json5`` file and each *library root* requires a +``library.json5`` file. .. . TODO: Create are more detailed reference page for package and library layout, diff --git a/docs/err/invalid-pkg-manifest.rst b/docs/err/invalid-pkg-manifest.rst new file mode 100644 index 00000000..a31c8178 --- /dev/null +++ b/docs/err/invalid-pkg-manifest.rst @@ -0,0 +1,9 @@ +Error: Invalid package manifest +############################### + +Every ``dds`` package must contain a valid package manifest, which is stored in +JSON5 format in ``package.json5`` (or similarly named file). + +The contents of this file must follow a prescribed content, or ``dds`` will +reject the manifest. Refer to the :ref:`pkgs.pkgs` documentation page for more +information about how to declare packages. diff --git a/docs/err/no-default-toolchain.rst b/docs/err/no-default-toolchain.rst new file mode 100644 index 00000000..96a93600 --- /dev/null +++ b/docs/err/no-default-toolchain.rst @@ -0,0 +1,29 @@ +Error: Unable to find a default toolchain to use for the build +############################################################## + +Unlike other build systems, ``dds`` will not attempt to make a "best guess" +about the local platform and the available build toolchains. Instead, a +toolchain must be provided for every build. This can be named on the command +line with ``--toolchain`` or by placing a toolchain file in one of a few +prescribed locations. + + +Solution: Request a built-in toolchain +************************************** + +A built-in toolchain can be specified with the ``--toolchain`` (or ``-t``) +argument. Refer to the :ref:`toolchains.builtin` section for more information. + + +Solution: Set up a default toolchain file for the project or for your local user +******************************************************************************** + +If no toolchain argument is given when a build is invoked, ``dds`` will search +a predefined set of filepaths for the existence of a toolchain file to use as +the "default." Writing a simple toolchain to one of these paths will cause +``dds`` to use this toolchain when none was provided. + + +.. seealso:: + For more information, refer to the full documentation page: + :doc:`/guide/toolchains` diff --git a/docs/err/test-failure.rst b/docs/err/test-failure.rst index 1da9cbb1..06bdfb33 100644 --- a/docs/err/test-failure.rst +++ b/docs/err/test-failure.rst @@ -3,7 +3,7 @@ Error: One or more tests failed This error message is printed when a project's tests encounter a failure condition. The exact behavior of tests is determined by a project's -``Test-Driver``. +``test_driver``. If you see this error, it is most likely that you have an issue in the tests of your project. diff --git a/docs/err/unknown-test-driver.rst b/docs/err/unknown-test-driver.rst index eb831d9d..efc767df 100644 --- a/docs/err/unknown-test-driver.rst +++ b/docs/err/unknown-test-driver.rst @@ -1,8 +1,8 @@ -Error: Unknown ``Test-Driver`` +Error: Unknown ``test_driver`` ############################## -``dds`` has a set of known ``Test-Driver``s built-in, and they may be specified -with the ``Test-Driver`` key. Receiving this error indicates that the specified -``Test-Driver`` was not recognized by ``dds``. Check your spelling, and check +``dds`` has a set of known ``test_driver``s built-in, and they may be specified +with the ``test_driver`` key. Receiving this error indicates that the specified +``test_driver`` was not recognized by ``dds``. Check your spelling, and check that the driver you want to use is supported by ``dds``. Refer to the :doc:`/guide/packages` page. \ No newline at end of file diff --git a/docs/err/unknown-usage.rst b/docs/err/unknown-usage.rst index 35865368..b84946c6 100644 --- a/docs/err/unknown-usage.rst +++ b/docs/err/unknown-usage.rst @@ -2,11 +2,11 @@ Error: Unknown Usage/Linking Requirements ######################################### A library can declare that it *uses* or *links* to another library by using the -``Uses`` and ``Links`` keys in ``library.dds``, respectively. +``uses`` and ``links`` keys in ``library.json5``, respectively. -These requirements are specified by using the ``Namespace/Name`` pair that +These requirements are specified by using the ``namespace/name`` pair that identifies a library. These are defined by both the project's dependencies and -the project itself. If a ``Uses`` or ``Links`` key does not correspond to a +the project itself. If a ``uses`` or ``links`` key does not correspond to a known library, ``dds`` will not be able to resolve the usage requirements, and will generate an error. diff --git a/docs/guide/catalog.rst b/docs/guide/catalog.rst index e563657c..f12c7ca6 100644 --- a/docs/guide/catalog.rst +++ b/docs/guide/catalog.rst @@ -33,7 +33,7 @@ command: dds catalog add [--depends [--depends [...]]] [--git-url --git-ref ] - [--auto-lib /] + [--auto-lib /] The ```` positional arguments is the ``name@version`` package ID that will be added to the catalog. The following options are supported: @@ -41,14 +41,14 @@ that will be added to the catalog. The following options are supported: ``--depends `` This argument, which can be specified multiple times to represent multiple dependencies, sets the dependencies of the package within the catalog. If - the obtained package root contains a ``package.dds``, then the dependencies - listed here must be identical to those listed in ``package.dds``, or - dependency resolution may yield unexpected results. + the obtained package root contains a ``package.json5``, then the + dependencies listed here must be identical to those listed in + ``package.json5``, or dependency resolution may yield unexpected results. ``--git-url `` Specify a Git URL to clone from to obtain the package. The root of the cloned repository must be a package root, but does not necessarily need to - have the ``package.dds`` and ``library.dds`` files if relying on the + have the ``package.json5`` and ``library.json5`` files if relying on the ``--auto-lib`` parameter. ``--git-ref`` **must** be passed with ``--git-url``. @@ -66,8 +66,8 @@ that will be added to the catalog. The following options are supported: can only be specified for packages that contain a single library root at the package root. - The form of the argument is that of ``/``, where - ``Namespace`` and ``Name`` are the usage requirement keys that should be + The form of the argument is that of ``/``, where + ``namespace`` and ``name`` are the usage requirement keys that should be generated for the library. diff --git a/docs/guide/cmake.rst b/docs/guide/cmake.rst new file mode 100644 index 00000000..b4e6014d --- /dev/null +++ b/docs/guide/cmake.rst @@ -0,0 +1,239 @@ +.. highlight:: cmake + +Using ``dds`` Packages in a CMake Project +######################################### + +One of ``dds``'s primary goals is to inter-operate with other build systems +cleanly. One of ``dds``'s primary outputs is *libman* package indices. These +package indices can be imported into other build systems that support the +*libman* format. + +.. note:: + ``dds`` doesn't (yet) have a ready-made central repository of packages that + can be downloaded. You'll need to populate the local package catalog + appropriately. + + .. seealso:: Refer to :doc:`catalog` for information about remote packages. + +.. _PMM: https://github.com/vector-of-bool/PMM + +.. _CMakeCM: https://github.com/vector-of-bool/CMakeCM + +.. _lm-cmake: https://raw.githubusercontent.com/vector-of-bool/libman/develop/cmake/libman.cmake + + +Generating a libman Index +************************* + +Importing libman packages into a build system requires that we have a libman +index generated on the filesystem. **This index is not generated globally**: It +is generated on a per-build basis as part of the build setup. The index will +describe in build-system-agnostic terms how to include a set of packages and +libraries as part of a build. + +``dds`` has first-class support for generating this index. The ``build-deps`` +subcommand of ``dds`` will download and build a set of dependencies, and places +an ``INDEX.lmi`` file that can be used to import the built results. + + +Declaring Dependencies +====================== + +``dds build-deps`` accepts a list of dependencies as commnad line arguments, +but it may be useful to specify those requirements in a file. + +``dds build-deps`` accepts a JSON5 file describing the dependencies of a +project as well. This file is similar to a very stripped-down version of a +``dds`` :ref:`package manifest `, and only includes the ``depends`` +key. (The presence of any other key is an error.) + +Here is a simple dependencies file that declares a single requirement: + +.. code-block:: js + :caption: ``dependencies.json5`` + + { + depends: { + 'neo-sqlite3': '^0.2.0', + } + } + + +Building Dependencies and the Index +=================================== + +We can invoke ``dds build-deps`` and give it the path to this file: + +.. code-block:: bash + + $ dds build-deps --deps dependencies.json5 + +When finished, ``dds`` will write the build results into a subdirectory called +``_deps`` and generate a file named ``INDEX.lmi``. This file is ready to be +imported into any build system that can understand libman files (in our case, +CMake). + +.. note:: + The output directory and index filepath can be controlled with the + ``--out`` and ``--lmi-path`` flags, respectively. + + +Importing into CMake +******************** + +We've generated a libman index and set of packages, and we want to import +them into CMake. CMake doesn't know how to do this natively, but there exists a +single-file module for CMake that allows CMake to import libraries from libman +indices without any additional work. + +The module is not shipped with CMake, but is available online as a single +stand-alone file. The `libman.cmake `_ file can be downloaded and +added to a project directly, or it can be obtained automatically through a +CMake tool like `PMM`_ (recommended). + + +Enabling *libman* Support in CMake via PMM +========================================== + +Refer to the ``README.md`` file in `the PMM repo `_ for information on how +to get PMM into your CMake project. In short, download and place the +``pmm.cmake`` file in your repository, and ``include()`` the file near the top +of your ``CMakeLists.txt``:: + + include(pmm.cmake) + +Once it has been included, you can call the ``pmm()`` function. To obtain +*libman*, we need to start by enabling `CMakeCM`_:: + + pmm(CMakeCM ROLLING) + +.. warning:: + It is not recommended to use the ``ROLLING`` mode, but it is the easiest to + use when getting started. For reproducible and reliable builds, you should + pin your CMakeCM version using the ``FROM `` argument. + +Enabling CMakeCM will make available all of the CMake modules available in `the +CMakeCM repository `_, which includes `libman.cmake `_. + +After the call to ``pmm()``, simply ``include()`` the ``libman`` module:: + + include(libman) + +That's it! The only function from the module that we will care about for now +is the ``import_packages()`` function. + + +Importing Our Dependencies' Packages +==================================== + +To import a package from a libman tree, we need only know the *name* of the +package we wish to import. In our example case above, we depend on +``neo-sqlite3``, so we simply call the libman-CMake function +``import_packages()`` with that package name:: + + import_packages("neo-sqlite3") + +You'll note that we don't request any particular version of the package: All +versioning resolution is handled by ``dds``. You'll also note that we don't +need to specify our transitive dependencies: This is handled by the libman +index that was generated by ``dds``: It will automatically ``import_packages()`` +any of the transitive dependencies required. + + +Using Out Dependencies' Libraries +================================= + +Like with ``dds``, CMake wants us to explicitly declare how our build targets +*use* other libraries. When we import a package from a libman index, the +import will generate CMake ``IMPORTED`` targets that can be linked against. + +In ``dds`` and in libman, a library is identified by a combination of +*namespace* and *name*, joined together with a slash ``/`` character. This +*qualified name* of a library is decided by the original package author, and +should be documented. In the case of ``neo-sqlite3``, the only target is +``neo/sqlite3``. + +When the libman CMake module imports a library, it creates a qualified name +using a double-colon "``::``" instead of a slash. As such, our ``neo/sqlite3`` +is imported in CMake as ``neo::sqlite3``. We can link against it as we would +with any other target:: + + add_executable(my-application app.cpp) + target_link_libraries(my-application PRIVATE neo::sqlite3) + +Altogether, here is the final CMake file: + +.. code-block:: + :caption: ``CMakeLists.txt`` + :linenos: + + cmake_minimum_required(VERSION 3.15) + project(MyApplication VERSION 1.0.0) + + include(pmm.cmake) + pmm(CMakeCM ROLLING) + + include(libman) + import_packages("neo-sqlite3") + + add_executable(my-application app.cpp) + target_link_libraries(my-application PRIVATE neo::sqlite3) + + +Additional PMM Support +********************** + +The ``pmm()`` function also supports ``dds`` directly, similar to ``CMakeCM`` +mode. This will automatically download a prebuilt ``dds`` for the host platform +and invoke ``dds build-deps`` in a single pass as part of CMake's configure +process. This is especially useful for a CI environment where you want to have +a stable ``dds`` version and always have your dependencies obtained +just-in-time. + +To start, pass the ``DDS`` argument to ``pmm()`` to use it:: + + pmm(DDS) + +..note:: + The ``_deps`` directory and ``INDEX.lmi`` file will be placed in the CMake + build directory, out of the way of the rest of the project. + +.. note:: + The version of ``dds`` that PMM downloads depends on the version of PMM + that is in use. + +This alone won't do anything useful, because you'll need to tell it what +dependencies we want to install:: + + pmm(DDS DEP_FILES dependencies.json5) + +You can also list your dependencies as an inline string in your CMakeLists.txt +instead of a separate file:: + + pmm(DDS DEPENDS "neo-sqlite3 ^0.2.2") + +Since you'll probably want to be using ``libman.cmake`` at the same time, the +calls for ``CMakeCM`` and ``DDS`` can simply be combined. This is how our new +CMake project might look: + +.. code-block:: + :caption: ``CMakeLists.txt`` + :linenos: + + cmake_minimum_required(VERSION 3.15) + project(MyApplication VERSION 1.0.0) + + include(pmm.cmake) + pmm(CMakeCM ROLLING + DDS DEPENDS "neo-sqlite3 ^0.2.2" + ) + + include(libman) + import_packages("neo-sqlite3") + + add_executable(my-application app.cpp) + target_link_libraries(my-application PRIVATE neo::sqlite3) + +This removes the requirement that we write a separate dependencies file, and we +no longer need to invoke ``dds build-deps`` externally, as it is all handled +by ``pmm``. diff --git a/docs/guide/index.rst b/docs/guide/index.rst index 26b9981d..3ed75692 100644 --- a/docs/guide/index.rst +++ b/docs/guide/index.rst @@ -12,3 +12,4 @@ User Guide repo catalog interdeps + cmake diff --git a/docs/guide/interdeps.rst b/docs/guide/interdeps.rst index f2d9388f..405ad8cc 100644 --- a/docs/guide/interdeps.rst +++ b/docs/guide/interdeps.rst @@ -15,16 +15,18 @@ Package Dependencies ******************** Consider that we are creating a package ``acme-gadgets@4.3.6``. We declare the -name and version in the ``package.dds`` in the package root: +name and version in the ``package.json5`` in the package root: -.. code-block:: +.. code-block:: js - Name: acme-gadgets - Version: 4.3.6 - Namespace: acme + { + name: 'acme-widgets', + version: '4.3.6', + namespace: 'acme', + } .. note:: - The ``Namespace`` field is required, but will be addressed in the + The ``namespace`` field is required, but will be addressed in the :ref:`deps.lib-deps` section. Suppose that our package's libraries build upon the libraries in the @@ -32,14 +34,17 @@ Suppose that our package's libraries build upon the libraries in the not as new as ``2.0.0``. Such a dependency can be declared with the ``Depends`` key: -.. code-block:: - :emphasize-lines: 5 - - Name: acme-gadgets - Version: 4.3.6 - Namespace: acme +.. code-block:: js + :emphasize-lines: 5-7 - Depends: acme-widgets ^1.4.3 + { + name: 'acme-gadgets', + version: '4.3.6', + namespace: 'acme', + depends: { + 'acme-widgets': '^1.4.3', + }, + } .. seealso:: :ref:`deps.ranges`. @@ -47,15 +52,18 @@ If we wish to declare additional dependencies, we simply declare them with additional ``Depends`` keys .. code-block:: - :emphasize-lines: 5-7 - - Name: acme-gadgets - Version: 4.3.6 - Namespace: acme - - Depends: acme-widgets ^1.4.3 - Depends: acme-gizmos ~5.6.5 - Depends: acme-utils ^3.3.0 + :emphasize-lines: 7-8 + + { + name: 'acme-gadgets', + version: '4.3.6', + namespace: 'acme', + depends: { + 'acme-widgets': '^1.4.3', + 'acme-gizmos': '~5.6.5', + 'acme-utils': '^3.3.0', + }, + } When ``dds`` attempts to build a project, it will first build the dependency solution by iteratively scanning the dependencies of the containing project and @@ -190,45 +198,50 @@ Library Dependencies In ``dds``, library interdependencies are tracked separately from the packages that contain them. A library must declare its intent to use another library -in the ``library.dds`` at its library root. The minimal content of a -``library.dds`` is the ``Name`` key: +in the ``library.json5`` at its library root. The minimal content of a +``library.json5`` is the ``name`` key: -.. code-block:: +.. code-block:: js - Name: gadgets + { + name: 'gadgets' + } To announce that a library wishes to *use* another library, use the aptly-named -``Uses`` key: - -.. code-block:: - :emphasize-lines: 3-5 - - Name: gadgets - - Uses: acme/widgets - Uses: acme/gizmos - Uses: acme/utils - -Here is where the package's ``Namespace`` key comes into play: A library's -qualified name is specified by joining the ``Namespace`` of the containing -package with the ``Name`` of the library within that package with a ``/`` +``uses`` key: + +.. code-block:: js + :emphasize-lines: 3-7 + + { + name: 'gadgets', + uses: [ + 'acme/widgets', + 'acme/gizmos', + 'acme/utils', + ], + } + +Here is where the package's ``namespace`` key comes into play: A library's +qualified name is specified by joining the ``namespace`` of the containing +package with the ``name`` of the library within that package with a ``/`` between them. -It is the responsibility of package authors to document the ``Namespace`` and -``Name`` of the packages and libraries that they distribute. +It is the responsibility of package authors to document the ``namespace`` and +``name`` of the packages and libraries that they distribute. .. note:: - The ``Namespace`` of a package is completely arbitrary, and need not relate + The ``namespace`` of a package is completely arbitrary, and need not relate to a C++ ``namespace``. .. note:: - The ``Namespace`` need not be unique to a single package. For example, a - single organization (Like Acme Inc.) can share a single ``Namespace`` for + The ``namespace`` need not be unique to a single package. For example, a + single organization (Like Acme Inc.) can share a single ``namespace`` for many of their packages and libraries. - However, it is essential that the ``/`` pair be + However, it is essential that the ``/`` pair be universally unique, so choose wisely! -Once the ``Uses`` key appears in the ``library.dds`` file of a library, ``dds`` +Once the ``uses`` key appears in the ``library.dds`` file of a library, ``dds`` will make available the headers for the library being used, and will transitively propagate that usage requirement to users of the library. diff --git a/docs/guide/packages.rst b/docs/guide/packages.rst index b4729cfb..ef72b2c9 100644 --- a/docs/guide/packages.rst +++ b/docs/guide/packages.rst @@ -88,7 +88,7 @@ executable. A *test* source file is a source file whose file stem ends with ``.test``. Like application sources, a *test* source file is omitted from the main library, and it will be used to generate tests. The exact behavior of tests is determined by -the ``Test-Driver`` setting for the package, but the default is that each test +the ``test_driver`` setting for the package, but the default is that each test source file will generate a single test executable that is executed by ``dds`` when running unit tests. @@ -242,13 +242,14 @@ included in downstream binaries, but it will still generate link rules for the dependencies of a header-only library. In order for ``dds`` to be able to distribute and interlink libraries, a -``library.dds`` file must be present at the corresponding library root. The -only required key in a ``library.dds`` file is ``Name``: +``library.json5`` file must be present at the corresponding library root. The +only required key in a ``library.json5`` file is ``name``: -.. code-block:: yaml - - Name: my-library +.. code-block:: js + { + name: 'my-library' + } .. seealso:: More information is discussed on the :ref:`deps.lib-deps` page @@ -287,23 +288,25 @@ the ``name@version`` string forms the *package ID*, and it must be unique within a repository or package catalog. In order for a package to be exported by ``dds`` it must have a -``package.dds`` file at its package root. Three keys are required to be -present in the ``package.dds`` file: ``Name``, ``Version``, and ``Namespace``: +``package.json5`` file at its package root. Three keys are required to be +present in the ``package.json5`` file: ``name``, ``version``, and ``namespace``: -.. code-block:: yaml +.. code-block:: js - Name: acme-widgets - Version: 6.7.3 - Namespace: acme + { + name: 'acme-widgets', + version: '6.7.3', + namespace: 'acme', + } -``Version`` must be a valid semantic version string. +``version`` must be a valid semantic version string. .. note:: - The ``Namespace`` key is arbitrary, and not necessarily associated with - and C++ ``namespace``. + The ``namespace`` key is arbitrary, and not necessarily associated with + any C++ ``namespace``. .. seealso:: - The purpose of ``Namespace``, as well as additional options in this file, + The purpose of ``namespace``, as well as additional options in this file, are described in the :ref:`deps.pkg-deps` page @@ -315,8 +318,9 @@ Naming Requirements Package names aren't a complete free-for-all. Package names must follow a set of specific rules: -- Package names may consist of ASCII, lowercase characters, digits, - underscores (``_``), hyphens (``-``), and periods (``.``). +- Package names may consist of a subset of ASCII including lowercase + characters, digits, underscores (``_``), hyphens (``-``), and periods + (``.``). .. note:: Different filesystems differ in their handling of filenames. Some platforms diff --git a/docs/guide/repo.rst b/docs/guide/repo.rst index 430b18bf..78e95136 100644 --- a/docs/guide/repo.rst +++ b/docs/guide/repo.rst @@ -62,8 +62,8 @@ package ID as the name of the source distribution directory:: $ ls ./spdlog@1.4.2/ include/ src/ - library.dds - package.dds + library.json5 + package.json5 .. _repo.export-local: diff --git a/docs/guide/toolchains.rst b/docs/guide/toolchains.rst index 9061c85f..c61f8ae1 100644 --- a/docs/guide/toolchains.rst +++ b/docs/guide/toolchains.rst @@ -1,4 +1,4 @@ -.. highlight:: yaml +.. highlight:: js Toolchains ########## @@ -30,14 +30,15 @@ effectively in your project. Passing a Toolchain ******************* -In ``dds``, the default format of a toolchain is that of a single file that -describes the entire toolchain, and uses the extension ``.tc.dds`` by -convention. When running a build for a project, the ``dds`` executable will -look for a file named ``toolchain.tc.dds`` by default, and will error out if -this file does not exist. A different toolchain can be provided by passing the -toolchain file for the ``--toolchain`` (or ``-t``) option on the command line:: +In ``dds``, the default format of a toolchain is that of a single JSON5 file +that describes the entire toolchain. When running a build for a project, the +``dds`` executable will look in a few locations for a default toolchain, and +generate an error if no default toolchain file is found (Refer to +:ref:`toolchains.default`). A different toolchain can be provided by passing +the toolchain file for the ``--toolchain`` (or ``-t``) option on the command +line:: - $ dds build -t my-toolchain.tc.dds + $ dds build -t my-toolchain.json5 Alternatively, you can pass the name of a built-in toolchain. See below. @@ -51,7 +52,7 @@ For convenience, ``dds`` includes several built-in toolchains that can be accessed in the ``--toolchain`` command-line option using a colon ``:`` prefix:: - $ dds build -T :gcc + $ dds build -t :gcc ``dds`` will treat the leading colon (``:``) as a name for a built-in toolchain (this means that a toolchain's filepath may not begin with a colon). @@ -88,6 +89,35 @@ The following pseudo-toolchains are also available: Sets the C++ version to ``C++UV`` and uses the ``:XYZ`` toolchain. +.. _toolchains.default: + +Providing a Default Toolchain File +********************************** + +If you do not which to provide a new toolchain for every individual project, +and the built-in toolchains do not suit your needs, you can write a toolchain +file to one of a few predefined paths, and ``dds`` will find and use it for the +build. The following directories are searched, in order: + +#. ``$pwd/`` - If the working directory contains a toolchain file, it will be + used as the default. +#. ``$dds_config_dir/`` - Searches for a toolchain file in ``dds``'s user-local + configuration directory (see below). +#. ``$user_home/`` - Searches for a toolchain file at the root of the current + user's home directory. (``$HOME`` on Unix-like systems, and ``$PROFILE`` on + Windows.) + +In each directory, it will search for ``toolchain.json5``, ``toolchain.jsonc``, +or ``toolchain.json``. + +The ``$dds_user_config`` directory is the ``dds`` subdirectory of the +user-local configuration directory. + +The user-local config directory is ``$XDG_CONFIG_DIR`` or ``~/.config`` on +Linux, ``~/Library/Preferences`` on macOS, and ``~/AppData/Roaming`` on +Windows. + + Toolchain Definitions ********************* @@ -98,13 +128,15 @@ simply one line: .. code-block:: - Compiler-ID: + { + compiler_id: "" + } -where ```` is one of the known ``Compiler-ID`` options (See the +where ```` is one of the known ``compiler_id`` options (See the toolchain option reference). ``dds`` will infer common suitable defaults for -the remaining options based on the value of ``Compiler-ID``. +the remaining options based on the value of ``compiler_id``. -For example, if you specify ``GNU``, then ``dds`` will assume ``gcc`` to be the +For example, if you specify ``gnu``, then ``dds`` will assume ``gcc`` to be the C compiler, ``g++`` to be the C++ compiler, and ``ar`` to be the library archiving tool. @@ -113,28 +145,36 @@ specify them with additional options: .. code-block:: - Compiler-ID: GNU - C-Compiler: gcc-9 - C++-Compiler: g++-9 + { + compiler_id: 'gnu', + c_compiler: 'gcc-9', + cxx_compiler: 'g++-9', + } -``dds`` will continue to infer other options based on the ``Compiler-ID``, but +``dds`` will continue to infer other options based on the ``compiler_id``, but will use the provided executable names when compiling files for the respective languages. -To specify compilation flags, the ``Flags`` option can be used: +To specify compilation flags, the ``flags`` option can be used: .. code-block:: - Flags: -fsanitize=address -fno-inline + { + // [...] + flags: '-fsanitize=address -fno-inline', + } .. note:: - Use ``Warning-Flags`` to specify options regarding compiler warnings. + Use ``warning_flags`` to specify options regarding compiler warnings. -Flags for linking executables can be specified with ``Link-Flags``: +Flags for linking executables can be specified with ``link_flags``: .. code-block:: - Link-Flags: -fsanitize=address -fPIE + { + // [...] + link_flags: '-fsanitize=address -fPIE' + } .. _toolchains.opt-ref: @@ -142,10 +182,36 @@ Flags for linking executables can be specified with ``Link-Flags``: Toolchain Option Reference ************************** -The following options are available to be specified within a toolchain file: +Understanding Flags and Shell Parsing +------------------------------------- + +Many of the ``dds`` toolchain parameters accept argument lists or shell-string +lists. If such an option is given a single string, then that string is split +using the syntax of a POSIX shell command parser. It accepts both single ``'`` +and double ``"`` quote characters as argument delimiters. + +If an option is given a list of strings instead, then each string in that +array is treated as a full command line argument and is passed as such. + +For example, this sample with ``flags``:: + + { + flags: "-fsanitize=address -fPIC" + } -``Compiler-ID`` +is equivalent to this one:: + + { + flags: ["-fsanitize=address", "-fPIC"] + } + +Despite splitting strings as-if they were shell commands, ``dds`` does nothing +else shell-like. It does not expand environment variables, nor does it expand +globs. + + +``compiler_id`` --------------- Specify the identity of the compiler. This option is used to infer many other @@ -154,153 +220,257 @@ templates, this option is not required. Valid values are: -``GNU`` +``gnu`` For GCC -``Clang`` +``clang`` For LLVM/Clang -``MSVC`` +``msvc`` For Microsoft Visual C++ -``C-Compiler`` and ``C++-Compiler`` +``c_compiler`` and ``cxx_compiler`` ----------------------------------- Names/paths of the C and C++ compilers, respectively. Defaults will be inferred -from ``Compiler-ID``. +from ``compiler_id``. -``C-Version`` and ``C++-Version`` +``c_version`` and ``cxx_version`` --------------------------------- Specify the language versions for C and C++, respectively. By default, ``dds`` will not set any language version. Using this option requires that the -``Compiler-ID`` be specified +``compiler_id`` be specified. Setting this value will cause the corresponding +language-version flag to be passed to the compiler. -Valid ``C-Version`` values are: +Valid ``c_version`` values are: -- ``C89`` -- ``C99`` -- ``C11`` -- ``C18`` +- ``c89`` +- ``c99`` +- ``c11`` +- ``c18`` -Valid ``C++-Version`` values are: +Valid ``cxx_version`` values are: -- ``C++98`` -- ``C++03`` -- ``C++11`` -- ``C++14`` -- ``C++17`` -- ``C++20`` +- ``c++98`` +- ``c++03`` +- ``c++11`` +- ``c++14`` +- ``c++17`` +- ``c++20`` .. warning:: ``dds`` will not do any "smarts" to infer the exact option to pass to have - the required effect. If you ask for ``C++20`` from ``gcc 5.3``, ``dds`` + the required effect. If you ask for ``c++20`` from ``gcc 4.8``, ``dds`` will simply pass ``-std=c++20`` with no questions asked. If you need - finer-grained control, use the ``Flags`` option. + finer-grained control, use the ``c_flags`` and ``cxx_flags`` options. -``Warning-Flags`` +``warning_flags`` ----------------- Override the compiler flags that should be used to enable warnings. This option -is stored separately from ``Flags``, as these options may be enabled/disabled +is stored separately from ``flags``, as these options may be enabled/disabled separately depending on how ``dds`` is invoked. .. note:: - If ``Compiler-ID`` is provided, a default value will be used that enables + If ``compiler_id`` is provided, a default value will be used that enables common warning levels. If you need to tweak warnings further, use this option. +On GNU-like compilers, the default flags are ``-Wall -Wextra -Wpedantic +-Wconversion``. On MSVC the default flag is ``/W4``. + -``Flags``, ``C-Flags``, and ``C++-Flags`` +``flags``, ``c_flags``, and ``cxx_flags`` ----------------------------------------- Specify *additional* compiler options, possibly per-language. -``Link-Flags`` +``link_flags`` -------------- Specify *additional* link options to use when linking executables. -``Optimize`` +``optimize`` ------------ -Boolean option (``True`` or ``False``) to enable/disable optimizations. Default -is ``False``. +Boolean option (``true`` or ``false``) to enable/disable optimizations. Default +is ``false``. -``Debug`` +``debug`` --------- -Boolean option (``True`` or ``False``) to enable/disable the generation of -debugging information. Default is ``False``. +Boolean option (``true`` or ``false``) to enable/disable the generation of +debugging information. Default is ``false``. -``Compiler-Launcher`` +``compiler_launcher`` --------------------- Provide a command prefix that should be used on all compiler executions. e.g. ``ccache``. +``advanced`` +------------ + +A nested object that contains advanced toolchain options. Refer to section on +advanced toolchain options. + + Advanced Options Reference ************************** The options below are probably not good to tweak unless you *really* know what -you are doing. Their values will be inferred from ``Compiler-ID``. +you are doing. Their values will be inferred from ``compiler_id``. + + +Command Templates +----------------- +Many of the below options take the form of command-line templates. These are +templates from which ``dds`` will create a command-line for a subprocess, +possibly by combining them together. -``Deps-Mode`` +Each command template allows some set of placeholders. Each instance of the +placeholder string will be replaced in the final command line. Refer to each +respective option for more information. + + +``deps_mode`` ------------- Specify the way in which ``dds`` should track compilation dependencies. One -of ``GNU``, ``MSVC``, or ``None``. +of ``gnu``, ``msvc``, or ``none``. + +.. note:: + If ``none``, then dependency tracking will be disabled entirely. This will + prevent ``dds`` from tracking interdependencies of source files, and + inhibits incremental compilation. -``C-Compile-File`` ------------------- +``c_compile_file`` and ``cxx_compile_file`` +------------------------------------------- + +Override the *command template* that is used to compile source files. -Override the *command template* that is used to compile C source files. +This template expects three placeholders: +- ``[in]`` is the path to the file that will be compiled. +- ``[out]`` is the path to the object file that will be generated. +- ``[flags]`` is the placeholder of the compilation flags. This placeholder + must not be attached to any other arguments. The compilation flag argument + list will be inserted in place of ``[flags]``. -``C++-Compile-File`` --------------------- +Defaults:: -Override the *command template* that is used to compile C++ source files. + { + // On GNU-like compilers (GCC, Clang): + c_compile_file: " -fPIC -pthread [flags] -c [in] -o[out]", + cxx_compile_file: " -fPIC -pthread [flags] -c [in] -o[out]", + // When `optimize` is enabled, `-O2` is added as a flag + // When `debug` is enabled, `-g` is added as a flag + // On MSVC: + c_compile_file: "cl.exe /MT /nologo /permissive- [flags] /c [in] /Fo[out]", + cxx_compile_file: "cl.exe /MT /EHsc /nologo /permissive- [flags] /c [in] /Fo[out]", + // When `optimize` is enabled, `/O2` is added as a flag + // When `debug` is enabled, `/Z7` and `/DEBUG` are added, and `/MT` becomes `/MTd` + } -``Create-Archive`` + +``create_archive`` ------------------ Override the *command template* that is used to generate static library archive files. +This template expects three placeholders: + +- ``[in]`` is the a placeholder for the list of inputs. It must not be attached + to any other arguments. The list of input paths will be inserted in place of + ``[in]``. +- ``[out]`` is the placeholder for the output path for the static library + archive. + +Defaults:: -``Link-Executable`` + { + // On GNU-like: + create_archive: "ar rcs [out] [in]", + // On MSVC: + create_archive: "lib /nologo /OUT:[out] [in]", + } + + +``link_executable`` ------------------- Override the *command template* that is used to link executables. +This template expects the same placeholders as ``create_archive``, but +``[out]`` is a placeholder for the executable file rather than a static +library. + +Defaults:: -``Include-Template`` --------------------- + { + // For GNU-like: + link_executable: " -fPIC [in] -pthread -o[out] [flags]", + // For MSVC: + link_executable: "cl.exe /nologo /EHsc [in] /Fe[out]", + } + + +``include_template`` and ``external_include_template`` +------------------------------------------------------ Override the *command template* for the flags to specify a header search path. +``external_include_template`` will be used to specify the include search path +for a directory that is "external" (i.e. does not live within the main project). +For each directory added to the ``#include`` search path, this argument +template is instantiated in the ``[flags]`` for the compilation. -``External-Include-Template`` ------------------------------ +This template expects only a single placeholder: ``[path]``, which will be +replaced with the path to the directory to be added to the search path. -Override the *command template* for the flags to specify a header search path -of an external library. +On MSVC, this defaults to ``/I [path]``. On GNU-like, ``-isystem [path]`` is +used for ``external_include_template`` and ``-I [path]`` for +``include_template``. -``Define-Template`` +``define_template`` ------------------- Override the *command template* for the flags to set a preprocessor definition. + +This template expects only a single placeholder: ``[def]``, which is the +preprocessor macro definition argument. + +On MSVC, this defaults to ``/D [def]``. On GNU-like compilers, this is +``-D [def]``. + + +``tty_flags`` +------------- + +Supply additional flags when compiling/linking that will only be applied if +standard output is an ANSI-capable terminal. + +On GNU and Clang this will be ``-fdiagnostics-color`` by default. + + +``obj_prefix``, ``obj_suffix``, ``archive_prefix``, ``archive_suffix``, +``exe_prefix``, and ``exe_suffix`` +---------------------------------- + +Set the filename prefixes and suffixes for object files, library archive files, +and executable files, respectively. diff --git a/docs/index.rst b/docs/index.rst index 80f7ebae..d5018221 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ the :doc:`tut/index` page. tut/index guide/index design + dev/index err/index .. Hide the link to the error reference since we don't want it cluttering our diff --git a/docs/tut/hello-test.rst b/docs/tut/hello-test.rst index 99e18718..6210e33e 100644 --- a/docs/tut/hello-test.rst +++ b/docs/tut/hello-test.rst @@ -48,7 +48,7 @@ will already know the problem, but wouldn't it be better if we had better test diagnostics? -A ``Test-Driver``: Using *Catch2* +A ``test_driver``: Using *Catch2* ********************************* ``dds`` ships with built-in support for the `Catch2`_ C and C++ testing @@ -57,20 +57,21 @@ framework. .. _catch2: https://github.com/catchorg/Catch2 To make use of Catch as our test driver, we simply declare this intent in the -``package.dds`` file at the package root: +``package.json5`` file at the package root: -.. code-block:: yaml - :caption: ``/package.dds`` +.. code-block:: js + :caption: ``/package.json5`` :emphasize-lines: 5 - Name: hello-dds - Version: 0.1.0 - Namespace: tutorial - - Test-Driver: Catch-Main + { + name: 'hello-dds', + version: '0.1.0', + namespace: 'tutorial', + test_driver: 'Catch-Main', + } If you now run ``dds build``, we will get a linker error for a multiply-defined -``main`` function. When setting the ``Test-Driver`` to ``Catch-Main``, ``dds`` +``main`` function. When setting the ``test_driver`` to ``Catch-Main``, ``dds`` will compile an entrypoint separately from any particular test, and the tests will link against that entrypoint. This means we cannot provide our own ``main`` function, and should instead use Catch's ``TEST_CASE`` macro to diff --git a/docs/tut/hello-world.rst b/docs/tut/hello-world.rst index 8f12132c..ac1e98ae 100644 --- a/docs/tut/hello-world.rst +++ b/docs/tut/hello-world.rst @@ -221,20 +221,21 @@ Creating a package manifest file. ``dds`` will work happily with packages that do not declare themselves, as long as the filesystem structure is sufficient. However: To use features covered in -later tutorials, we'll need a simple ``package.dds`` file to declare +later tutorials, we'll need a simple ``package.json5`` file to declare information about are package. This file should be placed directly in the package root: -.. code-block:: yaml - :caption: ``/package.dds`` - - Name: hello-dds - Version: 0.1.0 - Namespace: tutorial +.. code-block:: js + :caption: ``/package.json5`` + { + name: 'hello-dds', + version: '0.1.0', + namespace: 'tutorial', + } .. note:: - The ``Namespace`` option will be discussed later. + The ``namespace`` option will be discussed later. Rebuilding the project will show no difference at the moment. diff --git a/library.dds b/library.dds index e9bc7cbc..30da1932 100644 --- a/library.dds +++ b/library.dds @@ -7,4 +7,7 @@ Uses: nlohmann/json Uses: neo/sqlite3 Uses: neo/fun Uses: semver/semver +Uses: vob/semester Uses: pubgrub/pubgrub +Uses: vob/json5 +Uses: hanickadot/ctre diff --git a/library.jsonc b/library.jsonc new file mode 100644 index 00000000..16223ef5 --- /dev/null +++ b/library.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "./res/library-schema.json", + "name": "dds", + "uses": [ + "spdlog/spdlog", + "Microsoft/wil", + "range-v3/range-v3", + "nlohmann/json", + "neo/sqlite3", + "neo/fun", + "semver/semver", + "pubgrub/pubgrub", + "vob/json5", + "vob/semester", + "hanickadot/ctre", + ] +} \ No newline at end of file diff --git a/package.dds b/package.dds index 43bb0dd4..15d389c9 100644 --- a/package.dds +++ b/package.dds @@ -1,5 +1,5 @@ Name: dds -Version: 0.1.0 +Version: 0.1.0-alpha.3 # SPDX-License-Identifier: MPL-2.0 Depends: spdlog 1.4.2 @@ -10,5 +10,8 @@ Depends: neo-sqlite3 0.2.2 Depends: neo-fun 0.1.0 Depends: semver 0.2.1 Depends: pubgrub 0.2.0 +Depends: vob-json5 0.1.5 +Depends: vob-semester 0.1.0 +Depends: ctre 2.7.0 -Test-Driver: Catch-Main \ No newline at end of file +Test-Driver: Catch-Main diff --git a/package.jsonc b/package.jsonc new file mode 100644 index 00000000..6675144b --- /dev/null +++ b/package.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "./res/package-schema.json", + "name": "dds", + "version": "0.1.0-alpha.3", + "namespace": "dds", + "depends": { + "spdlog": "1.4.2", + "ms-wil": "2019.11.10", + "range-v3": "0.10.0", + "nlohmann-json": "3.7.1", + "neo-sqlite3": "0.2.2", + "neo-fun": "0.1.0", + "semver": "0.2.1", + "pubgrub": "0.2.0", + "vob-json5": "0.1.5", + "vob-semester": "0.1.0", + "ctre": "2.7.0", + }, + "test_driver": "Catch-Main" +} \ No newline at end of file diff --git a/res/library-schema.json b/res/library-schema.json new file mode 100644 index 00000000..d9a7a97e --- /dev/null +++ b/res/library-schema.json @@ -0,0 +1,34 @@ +{ + "type": "object", + "description": "DDS Library Manifest", + "additionalProperties": false, + "patternProperties": { + "^\\$": {} + }, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the library within the package.", + "pattern": "^[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*$" + }, + "uses": { + "type": "array", + "items": { + "type": "string", + "description": "A library that is used by this library. Should be of the form `namespace/name`.", + "pattern": "^[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*/[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*$" + } + }, + "links": { + "type": "array", + "items": { + "type": "string", + "description": "A library that is linked to this library. Should be of the form `namespace/name`.", + "pattern": "^[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*/[A-z][A-z0-9_]*((\\.|-)[A-z0-9_]+)*$" + } + } + } +} \ No newline at end of file diff --git a/res/package-schema.json b/res/package-schema.json new file mode 100644 index 00000000..00402b2e --- /dev/null +++ b/res/package-schema.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "description": "DDS Package Manifest", + "additionalProperties": false, + "patternProperties": { + "^\\$": {} + }, + "required": [ + "name", + "version", + "namespace" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the package", + "pattern": "^[a-z][a-z0-9_]*((\\.|-)[a-z0-9_]+)*$" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "description": "The version of the package. Must be a valid Semantic Version string.", + "default": "0.1.0" + }, + "namespace": { + "type": "string", + "description": "The package's namespace. Must be a valid string.", + "pattern": "^[a-z][a-z0-9_]*((\\.|-)[a-z0-9_]+)*$" + }, + "$schema": { + "type": "string", + "description": "JSON schema tag. Ignored by dds." + }, + "depends": { + "type": "object", + "patternProperties": { + "^[a-z][a-z0-9_]*((\\.|-)[a-z0-9_]+)*$": { + "type": "string", + "description": "The version of the dependency. Must be a valid Semantic Version string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } + }, + "test_driver": { + "type": "string", + "default": "Catch-Main", + "enum": [ + "Catch-Main", + "Catch" + ] + } + } +} \ No newline at end of file diff --git a/res/toolchain-schema.json b/res/toolchain-schema.json new file mode 100644 index 00000000..ced6469b --- /dev/null +++ b/res/toolchain-schema.json @@ -0,0 +1,187 @@ +{ + "type": "object", + "description": "DDS Toolchain Description File", + "additionalProperties": false, + "patternProperties": { + "^\\$": {} + }, + "definitions": { + "command_line_flags": { + "anyOf": [ + { + "type": "string", + "description": "Shell-style string of command-line arguments" + }, + { + "type": "array", + "description": "An array of command-line arguments. Will be passed verbatim.", + "items": { + "type": "string", + "description": "A single command-line argument. Will be passed verbatim." + } + } + ] + } + }, + "properties": { + "compiler_id": { + "type": "string", + "description": "The general compiler identification. This is one of a fixed set of values that DDS will use to infer most toolchain attributes.", + "enum": [ + "msvc", + "gnu", + "clang" + ] + }, + "c_compiler": { + "type": "string", + "description": "Executable name or filepath for a C compiler", + "examples": [ + "gcc", + "clang-9", + "cl.exe" + ] + }, + "cxx_compiler": { + "type": "string", + "description": "Executable name or filepath for a C++ compiler", + "examples": [ + "g++", + "clang++-9", + "cl.exe" + ] + }, + "flags": { + "description": "Pass additional compile flags, regardless of the source language", + "$ref": "#/definitions/command_line_flags" + }, + "c_flags": { + "description": "Pass additional flags to the C compiler.", + "$ref": "#/definitions/command_line_flags" + }, + "cxx_flags": { + "description": "Pass additional flags to the C++ compiler.", + "$ref": "#/definitions/command_line_flags" + }, + "c_version": { + "description": "The C language version", + "type": "string", + "enum": [ + "c89", + "c99", + "c11", + "c18" + ] + }, + "cxx_version": { + "description": "The C++ language version", + "type": "string", + "enum": [ + "c++98", + "c++03", + "c++11", + "c++14", + "c++17", + "c++20" + ] + }, + "warning_flags": { + "description": "Set the flags that will be passed to the compiler to enable/disable warnings", + "$ref": "#/definitions/command_line_flags" + }, + "link_flags": { + "description": "Pass additional flags to the compiler when it is linking runtime binaries (executables)", + "$ref": "#/definitions/command_line_flags" + }, + "compiler_launcher": { + "description": "Set a command-line prefix that will be prepended to all compiler invocations", + "$ref": "#/definitions/command_line_flags" + }, + "debug": { + "description": "Enable the generation of debug information", + "type": "boolean", + "default": true + }, + "optimize": { + "description": "Optimize generated code", + "type": "boolean", + "default": true + }, + "advanced": { + "type": "object", + "additionalProperties": false, + "description": "Advanced toolchain options. All of these options will be inferred from `compiler_id` by default. Handle with care.", + "properties": { + "deps_mode": { + "type": "string", + "description": "Dependency tracking mode.", + "enum": [ + "msvc", + "gnu", + "none" + ] + }, + "include_template": { + "description": "Set the include-directory flags template", + "$ref": "#/definitions/command_line_flags" + }, + "external_include_template": { + "description": "Set the external include-directory flags template", + "$ref": "#/definitions/command_line_flags" + }, + "define_template": { + "description": "Set the preprocessor-definition flags template", + "$ref": "#/definitions/command_line_flags" + }, + "base_warning_flags": { + "description": "Set the base warning flags for the toolchain. These are always prepended to `warning_flags`.", + "$ref": "#/definitions/command_line_flags" + }, + "c_compile_file": { + "description": "Set the command template for compiling C source files", + "$ref": "#/definitions/command_line_flags" + }, + "cxx_compile_file": { + "description": "Set the command template for compiling C++ source files", + "$ref": "#/definitions/command_line_flags" + }, + "create_archive": { + "description": "Set the command template for generating static library archives", + "$ref": "#/definitions/command_line_flags" + }, + "link_executable": { + "description": "Set the command template for linking executable binaries", + "$ref": "#/definitions/command_line_flags" + }, + "tty_flags": { + "description": "Set command line flags that will be applied only if stdout is an ANSI-capable terminal", + "$ref": "#/definitions/command_line_flags" + }, + "obj_prefix": { + "description": "Set the filename prefix for object files", + "type": "string" + }, + "obj_suffix": { + "description": "Set the filename suffix for object files", + "type": "string" + }, + "archive_prefix": { + "description": "Set the filename prefix for library archive files", + "type": "string" + }, + "archive_suffix": { + "description": "Set the filename suffix for library archive files", + "type": "string" + }, + "exe_prefix": { + "description": "Set the filename prefix for executable files", + "type": "string" + }, + "exe_suffix": { + "description": "Set the filename suffix for executable files", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/src/dds.main.cpp b/src/dds.main.cpp index d56e90ce..43afb9d1 100644 --- a/src/dds.main.cpp +++ b/src/dds.main.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -31,11 +31,22 @@ struct toolchain_flag : string_flag { toolchain_flag(args::Group& grp) : string_flag{grp, "toolchain_file", - "Path/ident of the toolchain to use", - {"toolchain", 't'}, - (dds::fs::current_path() / "toolchain.dds").string()} {} + "Path/identifier of the toolchain to use", + {"toolchain", 't'}} {} dds::toolchain get_toolchain() { + if (*this) { + return get_arg(); + } else { + auto found = dds::toolchain::get_default(); + if (!found) { + dds::throw_user_error(); + } + return *found; + } + } + + dds::toolchain get_arg() { const auto tc_path = this->Get(); if (tc_path.find(":") == 0) { auto default_tc = tc_path.substr(1); @@ -47,7 +58,8 @@ struct toolchain_flag : string_flag { } return std::move(*tc); } else { - return dds::parse_toolchain_dds(dds::slurp_file(tc_path)); + return dds::parse_toolchain_json5(dds::slurp_file(tc_path)); + // return dds::parse_toolchain_dds(dds::slurp_file(tc_path)); } } }; @@ -580,11 +592,9 @@ struct cli_build { params.out_root = out.Get(); params.toolchain = tc_filepath.get_toolchain(); params.parallel_jobs = n_jobs.Get(); - dds::package_manifest man; - const auto man_filepath = project.root.Get() / "package.dds"; - if (exists(man_filepath)) { - man = dds::package_manifest::load_from_file(man_filepath); - } + + auto man = dds::package_manifest::load_from_directory(project.root.Get()) + .value_or(dds::package_manifest{}); dds::builder bd; dds::sdist_build_params main_params; diff --git a/src/dds/build/builder.cpp b/src/dds/build/builder.cpp index 6f56a35d..f30872d6 100644 --- a/src/dds/build/builder.cpp +++ b/src/dds/build/builder.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -103,7 +104,7 @@ prepare_test_driver(const build_params& params, test_lib test_driver, build_env_ library_plan prepare_library(state& st, const sdist_target& sdt, - const library& lib, + const library_root& lib, const package_manifest& pkg_man) { library_build_params lp; lp.out_subdir = sdt.params.subdir; @@ -121,7 +122,7 @@ library_plan prepare_library(state& st, } } } - return library_plan::create(lib, std::move(lp)); + return library_plan::create(lib, std::move(lp), pkg_man.namespace_ + "/" + lib.manifest().name); } package_plan prepare_one(state& st, const sdist_target& sd) { @@ -150,9 +151,13 @@ prepare_ureqs(const build_plan& plan, const toolchain& toolchain, path_ref out_r lib_reqs.include_paths.push_back(lib.library_().public_include_dir()); lib_reqs.uses = lib.library_().manifest().uses; lib_reqs.links = lib.library_().manifest().links; - if (const auto& arc = lib.create_archive()) { + if (const auto& arc = lib.archive_plan()) { lib_reqs.linkable_path = out_root / arc->calc_archive_file_path(toolchain); } + auto gen_incdir_opt = lib.generated_include_dir(); + if (gen_incdir_opt) { + lib_reqs.include_paths.push_back(out_root / *gen_incdir_opt); + } } } return ureqs; @@ -170,7 +175,7 @@ void write_lml(build_env_ref env, const library_plan& lib, path_ref lml_path) { for (auto&& link : lib.links()) { out << "Links: " << link.namespace_ << "/" << link.name << '\n'; } - if (auto&& arc = lib.create_archive()) { + if (auto&& arc = lib.archive_plan()) { out << "Path: " << (env.output_root / arc->calc_archive_file_path(env.toolchain)).generic_string() << '\n'; @@ -210,7 +215,15 @@ void builder::build(const build_params& params) const { state st; auto plan = prepare_build_plan(st, _sdists); auto ureqs = prepare_ureqs(plan, params.toolchain, params.out_root); - build_env env{params.toolchain, params.out_root, db, ureqs}; + build_env env{ + params.toolchain, + params.out_root, + db, + toolchain_knobs{ + .is_tty = stdout_is_a_tty(), + }, + ureqs, + }; if (st.generate_catch2_main) { auto catch_lib = prepare_test_driver(params, test_lib::catch_main, env); @@ -225,6 +238,8 @@ void builder::build(const build_params& params) const { generate_compdb(plan, env); } + plan.render_all(env); + dds::stopwatch sw; plan.compile_all(env, params.parallel_jobs); spdlog::info("Compilation completed in {:n}ms", sw.elapsed_ms().count()); diff --git a/src/dds/build/file_deps.hpp b/src/dds/build/file_deps.hpp index 4d3027cf..69d4f5c3 100644 --- a/src/dds/build/file_deps.hpp +++ b/src/dds/build/file_deps.hpp @@ -11,7 +11,7 @@ * output may make use of a single input, and each output will need to keep track of the * outdated-ness of its inputs separately. * - * A toolchain has an associated `file_deps_mode`, which can be deduced from the Compiler-ID. The + * A toolchain has an associated `file_deps_mode`, which can be deduced from the compiler_id. The * three dependency modes are: * * 1. None - No dependency tracking takes place. diff --git a/src/dds/build/iter_compilations.hpp b/src/dds/build/iter_compilations.hpp index 1a4a5880..224f1fab 100644 --- a/src/dds/build/iter_compilations.hpp +++ b/src/dds/build/iter_compilations.hpp @@ -24,12 +24,14 @@ inline auto iter_libraries(const build_plan& plan) { * Return a range iterating over ever file compilation defined in the given build plan */ inline auto iter_compilations(const build_plan& plan) { - auto lib_compiles = // - iter_libraries(plan) // - | ranges::views::transform(&library_plan::create_archive) // - | ranges::views::filter([&](auto&& opt) { return bool(opt); }) // - | ranges::views::transform([&](auto&& opt) -> auto& { return opt->compile_files(); }) // - | ranges::views::join // + auto lib_compiles = // + iter_libraries(plan) // + | ranges::views::transform(&library_plan::archive_plan) // + | ranges::views::filter([&](auto&& opt) { return bool(opt); }) // + | ranges::views::transform([&](auto&& opt) -> auto& { + return opt->file_compilations(); + }) // + | ranges::views::join // ; auto exe_compiles = // diff --git a/src/dds/build/plan/archive.cpp b/src/dds/build/plan/archive.cpp index 79060946..ab18437a 100644 --- a/src/dds/build/plan/archive.cpp +++ b/src/dds/build/plan/archive.cpp @@ -24,7 +24,7 @@ void create_archive_plan::archive(const build_env& env) const { archive_spec ar; ar.input_files = std::move(objects); ar.out_path = env.output_root / calc_archive_file_path(env.toolchain); - auto ar_cmd = env.toolchain.create_archive_command(ar); + auto ar_cmd = env.toolchain.create_archive_command(ar, env.knobs); // `out_relpath` is purely for the benefit of the user to have a short name // in the logs @@ -40,17 +40,19 @@ void create_archive_plan::archive(const build_env& env) const { fs::create_directories(ar.out_path.parent_path()); // Do it! - spdlog::info("[{}] Archive: {}", _name, out_relpath); + spdlog::info("[{}] Archive: {}", _qual_name, out_relpath); auto&& [dur_ms, ar_res] = timed([&] { return run_proc(ar_cmd); }); - spdlog::info("[{}] Archive: {} - {:n}ms", _name, out_relpath, dur_ms.count()); + spdlog::info("[{}] Archive: {} - {:n}ms", _qual_name, out_relpath, dur_ms.count()); // Check, log, and throw if (!ar_res.okay()) { - spdlog::error("Creating static library archive [{}] failed for '{}'", out_relpath, _name); + spdlog::error("Creating static library archive [{}] failed for '{}'", + out_relpath, + _qual_name); spdlog::error("Subcommand FAILED: {}\n{}", quote_command(ar_cmd), ar_res.output); throw_external_error< errc::archive_failure>("Creating static library archive [{}] failed for '{}'", out_relpath, - _name); + _qual_name); } } diff --git a/src/dds/build/plan/archive.hpp b/src/dds/build/plan/archive.hpp index 37f6a54e..fbdaf8e3 100644 --- a/src/dds/build/plan/archive.hpp +++ b/src/dds/build/plan/archive.hpp @@ -18,6 +18,8 @@ namespace dds { class create_archive_plan { /// The name of the archive. Not the filename, but the base name thereof std::string _name; + /// The qualified name of the library, as it would appear in a libman-usage + std::string _qual_name; /// The subdirectory in which the archive should be generated. fs::path _subdir; /// The plans for compiling the constituent source files of this library @@ -32,8 +34,12 @@ class create_archive_plan { * @param cfs The file compilation plans that will be collected together to * form the static library. */ - create_archive_plan(std::string_view name, path_ref subdir, std::vector cfs) + create_archive_plan(std::string_view name, + std::string_view qual_name, + path_ref subdir, + std::vector cfs) : _name(name) + , _qual_name(qual_name) , _subdir(subdir) , _compile_files(std::move(cfs)) {} @@ -52,7 +58,7 @@ class create_archive_plan { /** * Get the compilation plans for this library. */ - auto& compile_files() const noexcept { return _compile_files; } + auto& file_compilations() const noexcept { return _compile_files; } /** * Perform the actual archive generation. Expects all compilations to have diff --git a/src/dds/build/plan/base.hpp b/src/dds/build/plan/base.hpp index 6e68c29e..89071c65 100644 --- a/src/dds/build/plan/base.hpp +++ b/src/dds/build/plan/base.hpp @@ -12,6 +12,8 @@ struct build_env { fs::path output_root; database& db; + toolchain_knobs knobs; + const usage_requirement_map& ureqs; }; diff --git a/src/dds/build/plan/compile_file.cpp b/src/dds/build/plan/compile_file.cpp index bf433ae9..21597cfd 100644 --- a/src/dds/build/plan/compile_file.cpp +++ b/src/dds/build/plan/compile_file.cpp @@ -15,17 +15,22 @@ using namespace dds; compile_command_info compile_file_plan::generate_compile_command(build_env_ref env) const { compile_file_spec spec{_source.path, calc_object_file_path(env)}; spec.enable_warnings = _rules.enable_warnings(); - extend(spec.include_dirs, _rules.include_dirs()); + for (auto dirpath : _rules.include_dirs()) { + if (!dirpath.is_absolute()) { + dirpath = env.output_root / dirpath; + } + dirpath = fs::weakly_canonical(dirpath); + spec.include_dirs.push_back(std::move(dirpath)); + } for (const auto& use : _rules.uses()) { extend(spec.external_include_dirs, env.ureqs.include_paths(use)); } extend(spec.definitions, _rules.defs()); - return env.toolchain.create_compile_command(spec); + return env.toolchain.create_compile_command(spec, env.knobs); } fs::path compile_file_plan::calc_object_file_path(const build_env& env) const noexcept { - // `relpath` is just the path from the root of the source directory to the source file. - auto relpath = fs::relative(_source.path, _source.basis_path); + auto relpath = _source.relative_path(); // The full output directory is prefixed by `_subdir` auto ret = env.output_root / _subdir / relpath; ret.replace_filename(relpath.filename().string() + env.toolchain.object_suffix()); diff --git a/src/dds/build/plan/exe.cpp b/src/dds/build/plan/exe.cpp index 287dd463..5cec793b 100644 --- a/src/dds/build/plan/exe.cpp +++ b/src/dds/build/plan/exe.cpp @@ -25,11 +25,11 @@ void link_executable_plan::link(build_env_ref env, const library_plan& lib) cons for (const lm::usage& links : _links) { extend(spec.inputs, env.ureqs.link_paths(links)); } - if (lib.create_archive()) { + if (lib.archive_plan()) { // The associated library has compiled components. Add the static library a as a linker // input spec.inputs.push_back(env.output_root - / lib.create_archive()->calc_archive_file_path(env.toolchain)); + / lib.archive_plan()->calc_archive_file_path(env.toolchain)); } // The main object should be a linker input, of course. @@ -43,10 +43,10 @@ void link_executable_plan::link(build_env_ref env, const library_plan& lib) cons std::reverse(spec.inputs.begin(), spec.inputs.end()); // Do it! - const auto link_command = env.toolchain.create_link_executable_command(spec); + const auto link_command = env.toolchain.create_link_executable_command(spec, env.knobs); fs::create_directories(spec.output.parent_path()); auto msg = fmt::format("[{}] Link: {:30}", - lib.name(), + lib.qualified_name(), fs::relative(spec.output, env.output_root).string()); spdlog::info(msg); auto [dur_ms, proc_res] diff --git a/src/dds/build/plan/full.cpp b/src/dds/build/plan/full.cpp index 482e3d2e..dce3270e 100644 --- a/src/dds/build/plan/full.cpp +++ b/src/dds/build/plan/full.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include @@ -72,8 +74,25 @@ bool parallel_run(Range&& rng, int n_jobs, Fn&& fn) { return exceptions.empty(); } +template +decltype(auto) pair_up(T& left, Range& right) { + auto rep = ranges::view::repeat(left); + return ranges::view::zip(rep, right); +} + } // namespace +void build_plan::render_all(build_env_ref env) const { + auto templates = _packages // + | ranges::view::transform(&package_plan::libraries) // + | ranges::view::join // + | ranges::view::transform([](const auto& lib) { return pair_up(lib, lib.templates()); }) // + | ranges::view::join; + for (const auto& [lib, tmpl] : templates) { + tmpl.render(env, lib.library_()); + } +} + void build_plan::compile_all(const build_env& env, int njobs) const { auto okay = dds::compile_all(iter_compilations(*this), env, njobs); if (!okay) { @@ -83,8 +102,8 @@ void build_plan::compile_all(const build_env& env, int njobs) const { void build_plan::archive_all(const build_env& env, int njobs) const { auto okay = parallel_run(iter_libraries(*this), njobs, [&](const library_plan& lib) { - if (lib.create_archive()) { - lib.create_archive()->archive(env); + if (lib.archive_plan()) { + lib.archive_plan()->archive(env); } }); if (!okay) { diff --git a/src/dds/build/plan/full.hpp b/src/dds/build/plan/full.hpp index eb2700fd..14ef4412 100644 --- a/src/dds/build/plan/full.hpp +++ b/src/dds/build/plan/full.hpp @@ -28,6 +28,10 @@ class build_plan { * All of the packages in this plan */ auto& packages() const noexcept { return _packages; } + /** + * Render all config templates in the plan. + */ + void render_all(const build_env& env) const; /** * Compile all files in the plan. */ diff --git a/src/dds/build/plan/library.cpp b/src/dds/build/plan/library.cpp index 00c1ab16..3d7d5e5c 100644 --- a/src/dds/build/plan/library.cpp +++ b/src/dds/build/plan/library.cpp @@ -7,18 +7,38 @@ #include #include +#include using namespace dds; -library_plan library_plan::create(const library& lib, const library_build_params& params) { +namespace { + +const std::string gen_dir_qual = "__dds/gen"; + +fs::path rebase_gen_incdir(path_ref subdir) { return gen_dir_qual / subdir; } +} // namespace + +std::optional library_plan::generated_include_dir() const noexcept { + if (_templates.empty()) { + return std::nullopt; + } + return rebase_gen_incdir(output_subdirectory()); +} + +library_plan library_plan::create(const library_root& lib, + const library_build_params& params, + std::optional qual_name_) { // Source files are kept in three groups: std::vector app_sources; std::vector test_sources; std::vector lib_sources; + std::vector template_sources; - // Collect the source for this library. This will look for any compilable sources in the `src/` - // subdirectory of the library. - auto src_dir = lib.src_dir(); + auto qual_name = std::string(qual_name_.value_or(lib.manifest().name)); + + // Collect the source for this library. This will look for any compilable sources in the + // `src/` subdirectory of the library. + auto src_dir = lib.src_source_root(); if (src_dir.exists()) { // Sort each source file between the three source arrays, depending on // the kind of source that we are looking at. @@ -30,6 +50,8 @@ library_plan library_plan::create(const library& lib, const library_build_params app_sources.push_back(sfile); } else if (sfile.kind == source_kind::source) { lib_sources.push_back(sfile); + } else if (sfile.kind == source_kind::header_template) { + template_sources.push_back(sfile); } else { assert(sfile.kind == source_kind::header); } @@ -41,24 +63,28 @@ library_plan library_plan::create(const library& lib, const library_build_params compile_rules.enable_warnings() = params.enable_warnings; compile_rules.uses() = lib.manifest().uses; + const auto codegen_subdir = rebase_gen_incdir(params.out_subdir); + + if (!template_sources.empty()) { + compile_rules.include_dirs().push_back(codegen_subdir); + } + // Convert the library sources into their respective file compilation plans. auto lib_compile_files = // lib_sources // | ranges::views::transform([&](const source_file& sf) { - return compile_file_plan(compile_rules, - sf, - lib.manifest().name, - params.out_subdir / "obj"); + return compile_file_plan(compile_rules, sf, qual_name, params.out_subdir / "obj"); }) | ranges::to_vector; // If we have any compiled library files, generate a static library archive // for this library - std::optional create_archive; + std::optional archive_plan; if (!lib_compile_files.empty()) { - create_archive.emplace(lib.manifest().name, - params.out_subdir, - std::move(lib_compile_files)); + archive_plan.emplace(lib.manifest().name, + qual_name, + params.out_subdir, + std::move(lib_compile_files)); } // Collect the paths to linker inputs that should be used when generating executables for this @@ -84,8 +110,7 @@ library_plan library_plan::create(const library& lib, const library_build_params // Pick a subdir based on app/test const auto subdir_base = is_test ? params.out_subdir / "test" : params.out_subdir; // Put test/app executables in a further subdirectory based on the source file path - const auto subdir - = subdir_base / fs::relative(source.path.parent_path(), lib.src_dir().path); + const auto subdir = subdir_base / source.relative_path().parent_path(); // Pick compile rules based on app/test auto rules = is_test ? test_rules : compile_rules; // Pick input libs based on app/test @@ -96,13 +121,23 @@ library_plan library_plan::create(const library& lib, const library_build_params exe_links, compile_file_plan(rules, source, - lib.manifest().name, + qual_name, params.out_subdir / "obj"), subdir, source.path.stem().stem().string()}; link_executables.emplace_back(std::move(exe)); } + std::vector render_templates; + for (const auto& sf : template_sources) { + render_templates.emplace_back(sf, codegen_subdir); + } + // Done! - return library_plan{lib, std::move(create_archive), std::move(link_executables)}; + return library_plan{lib, + qual_name, + params.out_subdir, + std::move(archive_plan), + std::move(link_executables), + std::move(render_templates)}; } diff --git a/src/dds/build/plan/library.hpp b/src/dds/build/plan/library.hpp index b70dc564..2074b09a 100644 --- a/src/dds/build/plan/library.hpp +++ b/src/dds/build/plan/library.hpp @@ -2,7 +2,8 @@ #include #include -#include +#include +#include #include #include @@ -52,26 +53,38 @@ struct library_build_params { * initialize all of the constructor parameters correctly. */ class library_plan { - /// The underlying library object - library _lib; + /// The underlying library root + library_root _lib; + /// The qualified name of the library + std::string _qual_name; + /// The library's subdirectory within the output directory + fs::path _subdir; /// The `create_archive_plan` for this library, if applicable std::optional _create_archive; /// The executables that should be linked as part of this library's build std::vector _link_exes; + /// The templates that must be rendered for this library + std::vector _templates; public: /** * Construct a new `library_plan` - * @param lib The `library` object underlying this plan. + * @param lib The `library_root` object underlying this plan. * @param ar The `create_archive_plan`, or `nullopt` for this library. * @param exes The `link_executable_plan` objects for this library. */ - library_plan(library lib, + library_plan(library_root lib, + std::string_view qual_name, + fs::path subdir, std::optional ar, - std::vector exes) + std::vector exes, + std::vector tmpls) : _lib(std::move(lib)) + , _qual_name(qual_name) + , _subdir(std::move(subdir)) , _create_archive(std::move(ar)) - , _link_exes(std::move(exes)) {} + , _link_exes(std::move(exes)) + , _templates(std::move(tmpls)) {} /** * Get the underlying library object @@ -81,6 +94,14 @@ class library_plan { * Get the name of the library */ auto& name() const noexcept { return _lib.manifest().name; } + /** + * Get the qualified name of the library, as if for a libman usage requirement + */ + auto& qualified_name() const noexcept { return _qual_name; } + /** + * The output subdirectory of this library plan + */ + path_ref output_subdirectory() const noexcept { return _subdir; } /** * The directory that defines the source root of the library. */ @@ -89,7 +110,11 @@ class library_plan { * A `create_archive_plan` object, or `nullopt`, depending on if this library has compiled * components */ - auto& create_archive() const noexcept { return _create_archive; } + auto& archive_plan() const noexcept { return _create_archive; } + /** + * The template rendering plans for this library. + */ + auto& templates() const noexcept { return _templates; } /** * The executables that should be created by this library */ @@ -102,18 +127,29 @@ class library_plan { * The library identifiers that are linked by this library */ auto& links() const noexcept { return _lib.manifest().links; } + /** + * The path to the directory that should be added for the #include search + * path for this library, relative to the build root. Returns `nullopt` if + * this library has no generated headers. + */ + std::optional generated_include_dir() const noexcept; /** * Named constructor: Create a new `library_plan` automatically from some build-time parameters. * - * @param lib The `library` object from which we will inherit several properties. + * @param lib The `library_root` from which we will inherit several properties and the build + * will be inferred * @param params Parameters controlling the build of the library. i.e. if we create tests, * enable warnings, etc. + * @param qual_name Optionally, provide the fully-qualified name of the library that is being + * built * * The `lib` parameter defines the usage requirements of this library, and they are looked up in * the `ureqs` map. If there are any missing requirements, an exception will be thrown. */ - static library_plan create(const library& lib, const library_build_params& params); + static library_plan create(const library_root& lib, + const library_build_params& params, + std::optional qual_name); }; -} // namespace dds \ No newline at end of file +} // namespace dds diff --git a/src/dds/build/plan/template.cpp b/src/dds/build/plan/template.cpp new file mode 100644 index 00000000..5805cf8e --- /dev/null +++ b/src/dds/build/plan/template.cpp @@ -0,0 +1,193 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +using namespace dds; + +using json_data = semester::basic_data>>; + +namespace { + +static constexpr ctll::fixed_string IDENT_RE{"([_a-zA-Z]\\w*)(.*)"}; + +std::string_view skip(std::string_view in) { + auto nspace_pos = in.find_first_not_of(" \t\n\r\f"); + in = in.substr(nspace_pos); + if (starts_with(in, "/*")) { + // It's a block comment. Find the block-end marker. + auto block_end = in.find("*/"); + if (block_end == in.npos) { + throw_user_error("Unterminated block comment"); + } + in = in.substr(block_end + 2); + // Recursively skip some more + return skip(in); + } + if (starts_with(in, "//")) { + more: + // It's a line comment. Find the next not-continued newline + auto cn_nl = in.find("\\\n"); + auto nl = in.find("\n"); + if (cn_nl < nl) { + // The next newline is a continuation of the comment. Keep looking + in = in.substr(nl + 1); + goto more; + } + if (nl == in.npos) { + // We've reached the end. Okay. + return in.substr(nl); + } + } + // Not a comment, and not whitespace. Okay. + return in; +} + +std::string stringify(const json_data& dat) { + if (dat.is_bool()) { + return dat.as_bool() ? "true" : "false"; + } else if (dat.is_double()) { + return std::to_string(dat.as_double()); + } else if (dat.is_null()) { + return "nullptr"; + } else if (dat.is_string()) { + /// XXX: This probably isn't quite enough sanitization for edge cases. + auto str = dat.as_string(); + str = replace(str, "\n", "\\n"); + str = replace(str, "\"", "\\\""); + return "\"" + str + "\""; + } else { + throw_user_error("Cannot render un-stringable data type"); + } +} + +std::pair eval_expr_tail(std::string_view in, const json_data& dat) { + in = skip(in); + if (starts_with(in, ".")) { + // Accessing a subproperty of the data + in.remove_prefix(1); + in = skip(in); + // We _must_ see an identifier + auto [is_ident, ident, tail] = ctre::match(in); + if (!is_ident) { + throw_user_error("Expected identifier following dot `.`"); + } + if (!dat.is_mapping()) { + throw_user_error("Cannot use dot `.` on non-mapping object"); + } + auto& map = dat.as_mapping(); + auto found = map.find(ident.to_view()); + if (found == map.end()) { + throw_user_error("No subproperty '{}'", ident.to_view()); + } + return eval_expr_tail(tail, found->second); + } + return {stringify(dat), in}; +} + +std::pair eval_primary_expr(std::string_view in, + const json_data& dat) { + in = skip(in); + + if (in.empty()) { + throw_user_error("Expected primary expression"); + } + + if (in.front() == '(') { + in = in.substr(1); + auto [ret, tail] = eval_primary_expr(in, dat); + if (!starts_with(tail, ")")) { + throw_user_error( + "Expected closing parenthesis `)` following expression"); + } + return {ret, tail.substr(1)}; + } + + auto [is_ident, ident, tail_1] = ctre::match(in); + + if (is_ident) { + auto& map = dat.as_mapping(); + auto found = map.find(ident.to_view()); + if (found == map.end()) { + throw_user_error("Unknown top-level identifier '{}'", + ident.to_view()); + } + + return eval_expr_tail(tail_1, found->second); + } + + return {"nope", in}; +} + +std::string render_template(std::string_view tmpl, const library_root& lib) { + std::string acc; + std::string_view MARKER_STRING = "__dds"; + + // Fill out a data structure that will be exposed to the template + json_data dat = json_data::mapping_type({ + { + "lib", + json_data::mapping_type{ + {"name", lib.manifest().name}, + {"root", lib.path().string()}, + }, + }, + }); + + while (!tmpl.empty()) { + // Find the next marker in the template string + auto next_marker = tmpl.find(MARKER_STRING); + if (next_marker == tmpl.npos) { + // We've reached the end of the template. Stop + acc.append(tmpl); + break; + } + // Append the string up to the next marker + acc.append(tmpl.substr(0, next_marker)); + // Consume up to the next marker + tmpl = tmpl.substr(next_marker + MARKER_STRING.size()); + auto next_not_space = tmpl.find_first_not_of(" \t"); + if (next_not_space == tmpl.npos || tmpl[next_not_space] != '(') { + throw_user_error( + "Expected `(` following `__dds` identifier in template file"); + } + + auto [inner, tail] = eval_primary_expr(tmpl, dat); + acc.append(inner); + tmpl = tail; + } + + return acc; +} + +} // namespace + +void render_template_plan::render(build_env_ref env, const library_root& lib) const { + auto content = slurp_file(_source.path); + + // Calculate the destination of the template rendering + auto dest = env.output_root / _subdir / _source.relative_path(); + dest.replace_filename(dest.stem().stem().filename().string() + dest.extension().string()); + fs::create_directories(dest.parent_path()); + + auto result = render_template(content, lib); + if (fs::is_regular_file(dest)) { + auto existing_content = slurp_file(dest); + if (result == existing_content) { + /// The content of the file has not changed. Do not write a file. + return; + } + } + + auto ofile = open(dest, std::ios::binary | std::ios::out); + ofile << result; + ofile.close(); // Throw any exceptions while closing the file +} diff --git a/src/dds/build/plan/template.hpp b/src/dds/build/plan/template.hpp new file mode 100644 index 00000000..7d890ccc --- /dev/null +++ b/src/dds/build/plan/template.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include + +namespace dds { + +class library_root; + +class render_template_plan { + /** + * The source file that defines the config template + */ + source_file _source; + /** + * The subdirectory in which the template should be rendered. + */ + fs::path _subdir; + +public: + /** + * Create a new instance + * @param sf The source file of the template + * @param subdir The subdirectort into which the template should render + */ + render_template_plan(source_file sf, path_ref subdir) + : _source(std::move(sf)) + , _subdir(subdir) {} + + /** + * Render the template into its output directory + */ + void render(build_env_ref, const library_root& owning_library) const; +}; + +} // namespace dds \ No newline at end of file diff --git a/src/dds/catalog/get.cpp b/src/dds/catalog/get.cpp index b48c454f..9fb313bf 100644 --- a/src/dds/catalog/get.cpp +++ b/src/dds/catalog/get.cpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace dds; @@ -32,12 +33,20 @@ temporary_sdist do_pull_sdist(const package_info& listing, const git_remote_list spdlog::info("Create sdist from clone ..."); if (git.auto_lib.has_value()) { spdlog::info("Generating library data automatically"); - auto pkg_strm = dds::open(tmpdir.path() / "package.dds", std::ios::binary | std::ios::out); - pkg_strm << "Name: " << listing.ident.name << '\n' // - << "Version: " << listing.ident.version.to_string() << '\n' // - << "Namespace: " << git.auto_lib->namespace_; - auto lib_strm = dds::open(tmpdir.path() / "library.dds", std::ios::binary | std::ios::out); - lib_strm << "Name: " << git.auto_lib->name; + + auto pkg_strm + = dds::open(tmpdir.path() / "package.json5", std::ios::binary | std::ios::out); + auto man_json = nlohmann::json::object(); + man_json["name"] = listing.ident.name; + man_json["version"] = listing.ident.version.to_string(); + man_json["namespace"] = git.auto_lib->namespace_; + pkg_strm << nlohmann::to_string(man_json); + + auto lib_strm + = dds::open(tmpdir.path() / "library.json5", std::ios::binary | std::ios::out); + auto lib_json = nlohmann::json::object(); + lib_json["name"] = git.auto_lib->name; + lib_strm << nlohmann::to_string(lib_json); } sdist_params params; diff --git a/src/dds/deps.cpp b/src/dds/deps.cpp index 1191f978..6c290ffd 100644 --- a/src/dds/deps.cpp +++ b/src/dds/deps.cpp @@ -1,21 +1,14 @@ #include "./deps.hpp" -#include #include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include +#include + +#include #include -#include -#include #include using namespace dds; @@ -42,20 +35,52 @@ dependency dependency::parse_depends_string(std::string_view str) { } dependency_manifest dependency_manifest::from_file(path_ref fpath) { - auto kvs = lm::parse_file(fpath); - dependency_manifest ret; - lm::read( - fmt::format("Reading dependencies from '{}'", fpath.string()), - kvs, - [&](auto, auto key, auto val) { - if (key == "Depends") { - ret.dependencies.push_back(dependency::parse_depends_string(val)); - return true; - } - return false; - }, - lm_reject_dym{{"Depends"}}); - return ret; + auto content = slurp_file(fpath); + auto data = json5::parse_data(content); + + dependency_manifest depman; + using namespace semester::decompose_ops; + auto res = semester::decompose( + data, + try_seq{ + require_type{ + "The root of a dependency manifest must be an object (mapping)"}, + mapping{ + if_key{"$schema", just_accept}, + if_key{"depends", + require_type{ + "`depends` must be a mapping between package names and version ranges"}, + mapping{[&](auto pkg_name, const json5::data& range_str_) { + if (!range_str_.is_string()) { + throw_user_error( + "Issue in dependency manifest: Dependency for '{}' must be a " + "range string", + pkg_name); + } + try { + auto rng = semver::range::parse_restricted(range_str_.as_string()); + dependency dep{std::string(pkg_name), {rng.low(), rng.high()}}; + depman.dependencies.push_back(std::move(dep)); + } catch (const semver::invalid_range&) { + throw_user_error( + "Invalid version range string '{}' in dependency declaration " + "for '{}'", + range_str_.as_string(), + pkg_name); + } + return semester::dc_accept; + }}}, + [&](auto key, auto&&) { + return semester::dc_reject_t{ + fmt::format("Unknown key `{}` in dependency manifest", key)}; + }}, + }); + auto rej = std::get_if(&res); + if (rej) { + throw_user_error(rej->message); + } + + return depman; } namespace { diff --git a/src/dds/error/errors.cpp b/src/dds/error/errors.cpp index d63abf24..b5d3f81c 100644 --- a/src/dds/error/errors.cpp +++ b/src/dds/error/errors.cpp @@ -13,6 +13,8 @@ std::string error_url_suffix(dds::errc ec) noexcept { switch (ec) { case errc::invalid_builtin_toolchain: return "invalid-builtin-toolchain.html"; + case errc::no_default_toolchain: + return "no-default-toolchain.html"; case errc::no_such_catalog_package: return "no-such-catalog-package.html"; case errc::git_url_ref_mutual_req: @@ -39,6 +41,10 @@ std::string error_url_suffix(dds::errc ec) noexcept { return "sdist-ident-mismatch.html"; case errc::corrupted_build_db: return "corrupted-build-db.html"; + case errc::invalid_lib_manifest: + return "invalid-lib-manifest.html"; + case errc::invalid_pkg_manifest: + return "invalid-pkg-manifest.html"; case errc::invalid_version_range_string: return "invalid-version-string.html#range"; case errc::invalid_version_string: @@ -84,6 +90,13 @@ toolchain. (Toolchain file paths cannot begin with a leading colon). These toolchain names are encoded into the dds executable and cannot be modified. +)"; + case errc::no_default_toolchain: + return R"( +`dds` requires a toolchain to be specified in order to execute the build. `dds` +will not perform a "best-guess" at a default toolchain. You may either pass the +name of a built-in toolchain, or write a "default toolchain" file to one of the +supported filepaths. Refer to the documentation for more information. )"; case errc::no_such_catalog_package: return R"( @@ -130,6 +143,16 @@ modified by a newer version of dds? The catalog database schema doesn't match what dds expects. This indicates that the database file has been modified in a way that dds cannot automatically fix and handle. +)"; + case errc::invalid_lib_manifest: + return R"( +A library manifest is malformed Refer to the documentation and above error +message for more details. +)"; + case errc::invalid_pkg_manifest: + return R"( +The package manifest is malformed. Refer to the documentation and above error +message for more details. )"; case errc::invalid_catalog_json: return R"( @@ -211,9 +234,9 @@ which packages are claiming the library name. )"; case errc::unknown_usage_name: return R"( -A `Uses` or `Links` field for a library specifies a library of an unknown name. +A `uses` or `links` field for a library specifies a library of an unknown name. Check your spelling, and check that the package containing the library is -available, either from the `package.dds` or from the `INDEX.lmi` that was used +available, either from the `package.json5` or from the `INDEX.lmi` that was used for the build. )"; case errc::none: @@ -227,6 +250,8 @@ std::string_view dds::default_error_string(dds::errc ec) noexcept { switch (ec) { case errc::invalid_builtin_toolchain: return "The built-in toolchain name is invalid"; + case errc::no_default_toolchain: + return "Unable to find a default toolchain to use for the build"; case errc::no_such_catalog_package: return "The catalog has no entry for the given package ID"; case errc::git_url_ref_mutual_req: @@ -255,35 +280,40 @@ std::string_view dds::default_error_string(dds::errc ec) noexcept { "that was expected of it"; case errc::corrupted_build_db: return "The build database file is corrupted"; + case errc::invalid_lib_manifest: + return "The library manifest is invalid"; + case errc::invalid_pkg_manifest: + return "The package manifest is invalid"; case errc::invalid_version_range_string: return "Attempted to parse an invalid version range string. <- (Seeing this text is a " "`dds` bug. Please report it.)"; case errc::invalid_version_string: - return "Attempted to parse an invalid version string. <- (Seeing this text is a `dds` bug. " - "Please report it.)"; + return "Attempted to parse an invalid version string. <- (Seeing this text is a `dds` " + "bug. Please report it.)"; case errc::invalid_config_key: - return "Found an invalid configuration key. <- (Seeing this text is a `dds` bug. Please " - "report it.)"; + return "Found an invalid configuration key. <- (Seeing this text is a `dds` bug. " + "Please report it.)"; case errc::invalid_lib_filesystem: case errc::invalid_pkg_filesystem: - return "The filesystem structure of the package/library is invalid. <- (Seeing this text " - "is a `dds` bug. Please report it.)"; + return "The filesystem structure of the package/library is invalid. <- (Seeing this " + "text is a `dds` bug. Please report it.)"; case errc::invalid_pkg_id: return "A package identifier is invalid <- (Seeing this text is a `dds` bug. Please " "report it.)"; case errc::invalid_pkg_name: - return "A package name is invalid <- (Seeing this text is a `dds` bug. Please report it.)"; + return "A package name is invalid <- (Seeing this text is a `dds` bug. Please report " + "it.)"; case errc::sdist_exists: - return "The source ditsribution already exists at the destination <- (Seeing this text is " - "a `dds` bug. Please report it.)"; + return "The source ditsribution already exists at the destination <- (Seeing this " + "text is a `dds` bug. Please report it.)"; case errc::unknown_test_driver: - return "The specified Test-Driver is not known to `dds`"; + return "The specified test_driver is not known to `dds`"; case errc::dependency_resolve_failure: return "`dds` was unable to find a solution for the package dependencies given."; case errc::dup_lib_name: return "More than one library has claimed the same name."; case errc::unknown_usage_name: - return "A `Uses` or `Links` field names a library that isn't recognized."; + return "A `uses` or `links` field names a library that isn't recognized."; case errc::none: break; } diff --git a/src/dds/error/errors.hpp b/src/dds/error/errors.hpp index 3a49d3f5..656b760a 100644 --- a/src/dds/error/errors.hpp +++ b/src/dds/error/errors.hpp @@ -10,6 +10,7 @@ namespace dds { enum class errc { none = 0, invalid_builtin_toolchain, + no_default_toolchain, no_such_catalog_package, git_url_ref_mutual_req, test_failure, @@ -28,6 +29,8 @@ enum class errc { corrupted_build_db, + invalid_lib_manifest, + invalid_pkg_manifest, invalid_version_range_string, invalid_version_string, invalid_pkg_id, @@ -40,6 +43,8 @@ enum class errc { invalid_lib_filesystem, invalid_pkg_filesystem, + + template_error, }; std::string error_reference_of(errc) noexcept; diff --git a/src/dds/library/manifest.cpp b/src/dds/library/manifest.cpp index b630ab09..84e4c6a3 100644 --- a/src/dds/library/manifest.cpp +++ b/src/dds/library/manifest.cpp @@ -1,16 +1,21 @@ #include "./manifest.hpp" #include +#include #include -#include - #include -#include +#include +#include +#include +#include using namespace dds; -library_manifest library_manifest::load_from_file(const fs::path& fpath) { +library_manifest library_manifest::load_from_dds_file(path_ref fpath) { + spdlog::warn( + "Using deprecated library.dds parsing (on file {}). This will be removed soon. Migrate!", + fpath.string()); auto kvs = lm::parse_file(fpath); library_manifest ret; ret.name = fpath.parent_path().filename().string(); @@ -27,3 +32,107 @@ library_manifest library_manifest::load_from_file(const fs::path& fpath) { extend(ret.links, ranges::views::transform(links_strings, lm::split_usage_string)); return ret; } + +library_manifest library_manifest::load_from_file(path_ref fpath) { + auto content = slurp_file(fpath); + auto data = json5::parse_data(content); + + if (!data.is_object()) { + throw_user_error("Root value must be an object"); + } + + library_manifest lib; + using namespace semester::decompose_ops; + auto res = semester::decompose( // + data, + try_seq{require_type{ + "The root of the library manifest must be an object (mapping)"}, + mapping{ + if_key{"name", + require_type{"`name` must be a string"}, + put_into{lib.name}}, + if_key{"uses", + require_type{ + "`uses` must be an array of usage requirements"}, + for_each{ + require_type{"`uses` elements must be strings"}, + [&](auto&& uses) { + lib.uses.push_back(lm::split_usage_string(uses.as_string())); + return semester::dc_accept; + }, + }}, + if_key{"links", + require_type{ + "`links` must be an array of usage requirements"}, + for_each{ + require_type{"`links` elements must be strings"}, + [&](auto&& links) { + lib.links.push_back(lm::split_usage_string(links.as_string())); + return semester::dc_accept; + }, + }}, + }}); + auto rej = std::get_if(&res); + if (rej) { + throw_user_error(rej->message); + } + // using namespace json_read::ops; + // json_read::decompose( // + // data.as_object(), + // object(key("name", require_string(put_into{lib.name}, "`name` must be a string")), + // key("uses", + // array_each{require_string( + // [&](auto&& uses) { + // lib.uses.push_back(lm::split_usage_string(uses.as_string())); + // return json_read::accept_t{}; + // }, + // "All `uses` items must be strings")}), + // key("links", + // array_each{require_string( + // [&](auto&& links) { + // lib.links.push_back(lm::split_usage_string(links.as_string())); + // return json_read::accept_t{}; + // }, + // "All `links` items must be strings")}))); + + if (lib.name.empty()) { + throw_user_error( + "The 'name' field is required (Reading library manifest [{}])", fpath.string()); + } + + return lib; +} + +std::optional library_manifest::find_in_directory(path_ref dirpath) { + auto fnames = { + "library.json5", + "library.jsonc", + "library.json", + }; + for (auto c : fnames) { + auto cand = dirpath / c; + if (fs::is_regular_file(cand)) { + return cand; + } + } + + auto dds_file = dirpath / "library.dds"; + if (fs::is_regular_file(dds_file)) { + return dds_file; + } + + return std::nullopt; +} + +std::optional library_manifest::load_from_directory(path_ref dirpath) { + auto found = find_in_directory(dirpath); + if (!found.has_value()) { + return std::nullopt; + } + + if (found->extension() == ".dds") { + return load_from_dds_file(*found); + } else { + return load_from_file(*found); + } +} \ No newline at end of file diff --git a/src/dds/library/manifest.hpp b/src/dds/library/manifest.hpp index 95536d9e..27cbedb5 100644 --- a/src/dds/library/manifest.hpp +++ b/src/dds/library/manifest.hpp @@ -9,9 +9,9 @@ namespace dds { /** - * Represents the contents of a `library.dds`. This is somewhat a stripped-down + * Represents the contents of a `library.json5`. This is somewhat a stripped-down * version of lm::library, to only represent exactly the parts that we want to - * offer via `library.dds`. + * offer via `library.json5`. */ struct library_manifest { /// The name of the library @@ -24,7 +24,16 @@ struct library_manifest { /** * Load the library manifest from an existing file */ - static library_manifest load_from_file(const fs::path&); + static library_manifest load_from_file(path_ref); + static library_manifest load_from_dds_file(path_ref); + + /** + * Find a library manifest within a directory. This will search for a few + * file candidates and return the result from the first matching. If none + * match, it will return nullopt. + */ + static std::optional find_in_directory(path_ref); + static std::optional load_from_directory(path_ref); }; } // namespace dds diff --git a/src/dds/library/library.cpp b/src/dds/library/root.cpp similarity index 62% rename from src/dds/library/library.cpp rename to src/dds/library/root.cpp index 9a8bcb3d..8dc1ef13 100644 --- a/src/dds/library/library.cpp +++ b/src/dds/library/root.cpp @@ -1,8 +1,8 @@ -#include +#include #include #include -#include +#include #include #include @@ -14,8 +14,8 @@ using namespace dds; namespace { auto collect_pf_sources(path_ref path) { - auto include_dir = source_directory{path / "include"}; - auto src_dir = source_directory{path / "src"}; + auto include_dir = source_root{path / "include"}; + auto src_dir = source_root{path / "src"}; source_list sources; @@ -51,34 +51,38 @@ auto collect_pf_sources(path_ref path) { } // namespace -library library::from_directory(path_ref lib_dir) { +library_root library_root::from_directory(path_ref lib_dir) { auto sources = collect_pf_sources(lib_dir); library_manifest man; - man.name = lib_dir.filename().string(); - auto man_path = lib_dir / "library.dds"; - if (fs::is_regular_file(man_path)) { - man = library_manifest::load_from_file(man_path); + man.name = lib_dir.filename().string(); + auto found = library_manifest::find_in_directory(lib_dir); + if (found) { + if (found->extension() == ".dds") { + man = library_manifest::load_from_dds_file(*found); + } else { + man = library_manifest::load_from_file(*found); + } } - auto lib = library(lib_dir, std::move(sources), std::move(man)); + auto lib = library_root(lib_dir, std::move(sources), std::move(man)); return lib; } -fs::path library::public_include_dir() const noexcept { - auto inc_dir = include_dir(); +fs::path library_root::public_include_dir() const noexcept { + auto inc_dir = include_source_root(); if (inc_dir.exists()) { return inc_dir.path; } - return src_dir().path; + return src_source_root().path; } -fs::path library::private_include_dir() const noexcept { return src_dir().path; } +fs::path library_root::private_include_dir() const noexcept { return src_source_root().path; } -shared_compile_file_rules library::base_compile_rules() const noexcept { - auto inc_dir = include_dir(); - auto src_dir = this->src_dir(); +shared_compile_file_rules library_root::base_compile_rules() const noexcept { + auto inc_dir = include_source_root(); + auto src_dir = this->src_source_root(); shared_compile_file_rules ret; if (inc_dir.exists()) { ret.include_dirs().push_back(inc_dir.path); @@ -92,10 +96,10 @@ shared_compile_file_rules library::base_compile_rules() const noexcept { auto has_library_dirs = [](path_ref dir) { return fs::exists(dir / "src") || fs::exists(dir / "include"); }; -std::vector dds::collect_libraries(path_ref root) { - std::vector ret; +std::vector dds::collect_libraries(path_ref root) { + std::vector ret; if (has_library_dirs(root)) { - ret.emplace_back(library::from_directory(root)); + ret.emplace_back(library_root::from_directory(root)); } auto pf_libs_dir = root / "libs"; @@ -104,7 +108,8 @@ std::vector dds::collect_libraries(path_ref root) { extend(ret, fs::directory_iterator(pf_libs_dir) // | ranges::views::filter(has_library_dirs) // - | ranges::views::transform([&](auto p) { return library::from_directory(p); })); + | ranges::views::transform( + [&](auto p) { return library_root::from_directory(p); })); } return ret; } \ No newline at end of file diff --git a/src/dds/library/library.hpp b/src/dds/library/root.hpp similarity index 85% rename from src/dds/library/library.hpp rename to src/dds/library/root.hpp index 0d721912..6592addc 100644 --- a/src/dds/library/library.hpp +++ b/src/dds/library/root.hpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include @@ -12,7 +12,7 @@ namespace dds { /** * Represents a library that exists on the filesystem */ -class library { +class library_root { // The path containing the source directories for this library fs::path _path; // The sources that are part of this library @@ -22,7 +22,7 @@ class library { // Private constructor. Use named constructor `from_directory`, which will build // the construct arguments approperiately - library(path_ref dir, source_list&& src, library_manifest&& man) + library_root(path_ref dir, source_list&& src, library_manifest&& man) : _path(dir) , _sources(std::move(src)) , _man(std::move(man)) {} @@ -33,7 +33,7 @@ class library { * directory path. This will load the sources and manifest properly and * return the resulting library object. */ - static library from_directory(path_ref); + static library_root from_directory(path_ref); /** * Obtain the manifest for this library @@ -43,12 +43,12 @@ class library { /** * The `src/` directory for this library. */ - source_directory src_dir() const noexcept { return source_directory{path() / "src"}; } + source_root src_source_root() const noexcept { return source_root{path() / "src"}; } /** * The `include/` directory for this library */ - source_directory include_dir() const noexcept { return source_directory{path() / "include"}; } + source_root include_source_root() const noexcept { return source_root{path() / "include"}; } /** * The root path for this library (parent of `src/` and `include/`, if present) @@ -87,6 +87,6 @@ class library { * but there might also be libraries in `where/libs`. This function will find * them all. */ -std::vector collect_libraries(path_ref where); +std::vector collect_libraries(path_ref where); } // namespace dds \ No newline at end of file diff --git a/src/dds/package/manifest.cpp b/src/dds/package/manifest.cpp index d5d5ee3d..43f62f85 100644 --- a/src/dds/package/manifest.cpp +++ b/src/dds/package/manifest.cpp @@ -8,11 +8,17 @@ #include #include #include -#include +#include +#include + +#include using namespace dds; -package_manifest package_manifest::load_from_file(const fs::path& fpath) { +package_manifest package_manifest::load_from_dds_file(const fs::path& fpath) { + spdlog::warn( + "Using deprecated package.dds parsing (on file {}). This will be removed soon. Migrate!", + fpath.string()); auto kvs = lm::parse_file(fpath); package_manifest ret; std::string version_str; @@ -45,7 +51,7 @@ package_manifest package_manifest::load_from_file(const fs::path& fpath) { } else { auto dym = *did_you_mean(test_driver_str, {"Catch-Main", "Catch"}); throw_user_error< - errc::unknown_test_driver>("Unknown 'Test-Driver' '{}' (Did you mean '{}'?)", + errc::unknown_test_driver>("Unknown 'test_driver' '{}' (Did you mean '{}'?)", test_driver_str, dym); } @@ -63,3 +69,131 @@ package_manifest package_manifest::load_from_file(const fs::path& fpath) { return ret; } + +package_manifest package_manifest::load_from_file(const fs::path& fpath) { + auto content = slurp_file(fpath); + auto data = json5::parse_data(content); + + if (!data.is_object()) { + throw_user_error("Root value must be an object"); + } + + package_manifest ret; + using namespace semester::decompose_ops; + auto res = semester::decompose( // + data, + try_seq{ + require_type{ + "The root of a package manifest must be an object (mapping)"}, + mapping{ + if_key{"$schema", just_accept}, + if_key{ + "name", + require_type{"`name` must be a string"}, + put_into{ret.pkg_id.name}, + }, + if_key{ + "namespace", + require_type{"`namespace` must be a string"}, + put_into{ret.namespace_}, + }, + if_key{ + "version", + require_type{"`version` must be a string"}, + [&](auto&& version_str_) { + auto& version = version_str_.as_string(); + ret.pkg_id.version = semver::version::parse(version); + return semester::dc_accept; + }, + }, + if_key{ + "depends", + require_type{ + "`depends` must be a mapping between package names and version ranges"}, + mapping{[&](auto pkg_name, auto&& range_str_) { + if (!range_str_.is_string()) { + throw_user_error( + "Dependency for '{}' must be a range string", pkg_name); + } + try { + auto rng = semver::range::parse_restricted(range_str_.as_string()); + dependency dep{std::string(pkg_name), {rng.low(), rng.high()}}; + ret.dependencies.push_back(std::move(dep)); + } catch (const semver::invalid_range&) { + throw_user_error( + "Invalid version range string '{}' in dependency declaration for " + "'{}'", + range_str_.as_string(), + pkg_name); + } + return semester::dc_accept; + }}, + }, + if_key{"test_driver", + require_type{"`test_driver` must be a string"}, + [&](auto&& test_driver_str_) { + auto& test_driver = test_driver_str_.as_string(); + if (test_driver == "Catch-Main") { + ret.test_driver = test_lib::catch_main; + } else if (test_driver == "Catch") { + ret.test_driver = test_lib::catch_; + } else { + auto dym = *did_you_mean(test_driver, {"Catch-Main", "Catch"}); + throw_user_error( + "Unknown 'test_driver' '{}' (Did you mean '{}'?)", + test_driver, + dym); + } + return semester::dc_accept; + }}, + [&](auto key, auto&&) { + return semester::dc_reject_t{ + fmt::format("Unknown key `{}` in package manifest", key)}; + }}}); + auto rej = std::get_if(&res); + if (rej) { + throw_user_error(rej->message); + } + + if (ret.pkg_id.name.empty()) { + throw_user_error("The 'name' field is required."); + } + + if (ret.namespace_.empty()) { + throw_user_error("The 'namespace'` field is required."); + } + + return ret; +} + +std::optional package_manifest::find_in_directory(path_ref dirpath) { + auto cands = { + "package.json5", + "package.jsonc", + "package.json", + }; + for (auto c : cands) { + auto cand = dirpath / c; + if (fs::is_regular_file(cand)) { + return cand; + } + } + + auto dds_fname = dirpath / "package.dds"; + if (fs::is_regular_file(dds_fname)) { + return dds_fname; + } + return std::nullopt; +} + +std::optional package_manifest::load_from_directory(path_ref dirpath) { + auto found = find_in_directory(dirpath); + if (!found.has_value()) { + return std::nullopt; + } + if (found->extension() == ".dds") { + return load_from_dds_file(*found); + } else { + return load_from_file(*found); + } +} \ No newline at end of file diff --git a/src/dds/package/manifest.hpp b/src/dds/package/manifest.hpp index 686188d1..389c74b8 100644 --- a/src/dds/package/manifest.hpp +++ b/src/dds/package/manifest.hpp @@ -11,7 +11,7 @@ namespace dds { /** - * Possible values for Test-Driver in a package.dds + * Possible values for test_driver in a package.json5 */ enum class test_lib { catch_, @@ -26,7 +26,7 @@ struct package_manifest { package_id pkg_id; /// The declared `Namespace` of the package. This directly corresponds with the libman Namespace std::string namespace_; - /// The `Test-Driver` that this package declares, or `nullopt` if absent. + /// The `test_driver` that this package declares, or `nullopt` if absent. std::optional test_driver; /// The dependencies declared with the `Depends` fields, if any. std::vector dependencies; @@ -35,6 +35,15 @@ struct package_manifest { * Load a package manifest from a file on disk. */ static package_manifest load_from_file(path_ref); + static package_manifest load_from_dds_file(path_ref); + + /** + * Find a package manifest contained within a directory. This will search + * for a few file candidates and return the result from the first matching. + * If none match, it will return nullopt. + */ + static std::optional find_in_directory(path_ref); + static std::optional load_from_directory(path_ref); }; } // namespace dds \ No newline at end of file diff --git a/src/dds/source/dist.cpp b/src/dds/source/dist.cpp index 8d84bb46..9eb5f0a5 100644 --- a/src/dds/source/dist.cpp +++ b/src/dds/source/dist.cpp @@ -1,7 +1,7 @@ #include "./dist.hpp" #include -#include +#include #include #include @@ -24,7 +24,7 @@ void sdist_export_file(path_ref out_root, path_ref in_root, path_ref filepath) { fs::copy(filepath, dest); } -void sdist_copy_library(path_ref out_root, const library& lib, const sdist_params& params) { +void sdist_copy_library(path_ref out_root, const library_root& lib, const sdist_params& params) { auto sources_to_keep = // lib.all_sources() // | ranges::views::filter([&](const source_file& sf) { @@ -43,14 +43,14 @@ void sdist_copy_library(path_ref out_root, const library& lib, const sdist_param ranges::sort(sources_to_keep, std::less<>(), [](auto&& s) { return s.path; }); - auto lib_dds_path = lib.path() / "library.dds"; - if (!fs::is_regular_file(lib_dds_path)) { + auto lib_man_path = library_manifest::find_in_directory(lib.path()); + if (!lib_man_path) { throw_user_error( - "Each library root in a source distribution requires a library manifest (Expected " - "[{}])", - lib_dds_path.string()); + "Each library root in a source distribution requires a library manifest (Expected a " + "library manifest in [{}])", + lib.path().string()); } - sdist_export_file(out_root, params.project_dir, lib_dds_path); + sdist_export_file(out_root, params.project_dir, *lib_man_path); spdlog::info("sdist: Export library from {}", lib.path().string()); fs::create_directories(out_root); @@ -84,26 +84,28 @@ sdist dds::create_sdist(const sdist_params& params) { sdist dds::create_sdist_in_dir(path_ref out, const sdist_params& params) { auto libs = collect_libraries(params.project_dir); - for (const library& lib : libs) { + for (const library_root& lib : libs) { sdist_copy_library(out, lib, params); } - auto man_path = params.project_dir / "package.dds"; - if (!fs::is_regular_file(man_path)) { + auto man_path = package_manifest::find_in_directory(params.project_dir); + if (!man_path) { throw_user_error( - "Creating a source distribution requires a package.dds file for the project (Expected " - "[{}])", - man_path.string()); + "Creating a source distribution requires a package.json5 file for the project " + "(Expected manifest in [{}])", + params.project_dir.string()); } - sdist_export_file(out, params.project_dir, man_path); - auto pkg_man = package_manifest::load_from_file(man_path); + auto pkg_man = man_path->extension() == ".dds" ? package_manifest::load_from_dds_file(*man_path) + : package_manifest::load_from_file(*man_path); + sdist_export_file(out, params.project_dir, *man_path); spdlog::info("Generated export as {}", pkg_man.pkg_id.to_string()); - return sdist::from_directory(out); } sdist sdist::from_directory(path_ref where) { - auto pkg_man = package_manifest::load_from_file(where / "package.dds"); - return sdist{std::move(pkg_man), where}; + auto pkg_man = package_manifest::load_from_directory(where); + // Code paths should only call here if they *know* that the sdist is valid + assert(pkg_man.has_value()); + return sdist{pkg_man.value(), where}; } \ No newline at end of file diff --git a/src/dds/source/file.cpp b/src/dds/source/file.cpp index eb8064a2..88983a7a 100644 --- a/src/dds/source/file.cpp +++ b/src/dds/source/file.cpp @@ -35,6 +35,10 @@ std::optional dds::infer_source_kind(path_ref p) noexcept { auto ext_found = std::lower_bound(header_exts.begin(), header_exts.end(), p.extension(), std::less<>()); if (ext_found != header_exts.end() && *ext_found == p.extension()) { + auto stem = p.stem(); + if (stem.extension() == ".config") { + return source_kind::header_template; + } return source_kind::header; } @@ -44,11 +48,11 @@ std::optional dds::infer_source_kind(path_ref p) noexcept { return std::nullopt; } - if (ends_with(p.stem().string(), ".test")) { + if (p.stem().extension() == ".test") { return source_kind::test; } - if (ends_with(p.stem().string(), ".main")) { + if (p.stem().extension() == ".main") { return source_kind::app; } diff --git a/src/dds/source/file.hpp b/src/dds/source/file.hpp index 9c613b13..b5e396e2 100644 --- a/src/dds/source/file.hpp +++ b/src/dds/source/file.hpp @@ -9,6 +9,7 @@ namespace dds { enum class source_kind { header, + header_template, source, test, app, @@ -17,11 +18,22 @@ enum class source_kind { std::optional infer_source_kind(path_ref) noexcept; struct source_file { - fs::path path; - fs::path basis_path; + /** + * The actual path to the file + */ + fs::path path; + /** + * The path to source root that contains the file in question + */ + fs::path basis_path; + /** + * The kind of the source file + */ source_kind kind; static std::optional from_path(path_ref path, path_ref base_path) noexcept; + + fs::path relative_path() const noexcept { return fs::relative(path, basis_path); } }; using source_list = std::vector; diff --git a/src/dds/source/file.test.cpp b/src/dds/source/file.test.cpp new file mode 100644 index 00000000..a5661ccf --- /dev/null +++ b/src/dds/source/file.test.cpp @@ -0,0 +1,16 @@ +#include + +#include + +using dds::source_kind; + +TEST_CASE("Infer source kind") { + using dds::infer_source_kind; + auto k = infer_source_kind("foo.h"); + CHECK(k == source_kind::header); + CHECK(infer_source_kind("foo.hpp") == source_kind::header); + CHECK_FALSE(infer_source_kind("foo.txt")); // Not a source file extension + + CHECK(infer_source_kind("foo.hh") == source_kind::header); + CHECK(infer_source_kind("foo.config.hpp") == source_kind::header_template); +} \ No newline at end of file diff --git a/src/dds/source/dir.cpp b/src/dds/source/root.cpp similarity index 90% rename from src/dds/source/dir.cpp rename to src/dds/source/root.cpp index ac6f2222..d6d60e5b 100644 --- a/src/dds/source/dir.cpp +++ b/src/dds/source/root.cpp @@ -1,4 +1,4 @@ -#include "./dir.hpp" +#include "./root.hpp" #include #include @@ -6,7 +6,7 @@ using namespace dds; -std::vector source_directory::collect_sources() const { +std::vector source_root::collect_sources() const { using namespace ranges::views; // Collect all source files from the directory return // diff --git a/src/dds/source/dir.hpp b/src/dds/source/root.hpp similarity index 76% rename from src/dds/source/dir.hpp rename to src/dds/source/root.hpp index baae50f9..7e162f80 100644 --- a/src/dds/source/dir.hpp +++ b/src/dds/source/root.hpp @@ -8,10 +8,10 @@ namespace dds { /** - * A `source_directory` is a simple wrapper type that provides type safety and utilities to - * represent a source directory. + * A `source_root` is a simple wrapper type that provides type safety and utilities to + * represent a source root. */ -struct source_directory { +struct source_root { /// The actual path to the directory fs::path path; diff --git a/src/dds/toolchain/from_dds.cpp b/src/dds/toolchain/from_dds.cpp deleted file mode 100644 index 9ad2fee1..00000000 --- a/src/dds/toolchain/from_dds.cpp +++ /dev/null @@ -1,590 +0,0 @@ -#include "./from_dds.hpp" - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include - -using namespace dds; - -using fmt::format; -using std::optional; -using std::string; -using std::vector; -using string_seq = vector; -using opt_string = optional; -using opt_str_seq = optional; -using strv = std::string_view; - -toolchain dds::parse_toolchain_dds(strv str, strv context) { - auto kvs = lm::parse_string(str); - return parse_toolchain_dds(kvs, context); -} - -namespace { -struct read_argv_acc { - strv my_key; - opt_str_seq& out; - - bool operator()(strv, strv key, strv value) const { - if (key != my_key) { - return false; - } - if (!out) { - out.emplace(); - } - auto cmd = split_shell_string(value); - extend(*out, cmd); - return true; - } -}; - -struct read_argv { - strv my_key; - opt_str_seq& out; - - bool operator()(strv ctx, strv key, strv value) const { - if (key != my_key) { - return false; - } - if (out.has_value()) { - throw std::runtime_error( - format("{}: More than one value provided for key '{}'", ctx, key)); - } - out.emplace(split_shell_string(value)); - return true; - } -}; - -template -T read_opt(const std::optional& what, Func&& fn) { - if (!what.has_value()) { - return fn(); - } - return *what; -} - -template -[[noreturn]] void fail(strv context, strv message, Args&&... args) { - auto fmtd = format(message, args...); - throw std::runtime_error(format("{} - Failed to read toolchain file: {}", context, fmtd)); -} -} // namespace - -toolchain dds::parse_toolchain_dds(const lm::pair_list& pairs, strv context) { - opt_string compiler_id; - opt_string c_compiler_fpath; - opt_string cxx_compiler_fpath; - opt_string c_version; - opt_string cxx_version; - opt_string archive_prefix; - opt_string archive_suffix; - opt_string obj_prefix; - opt_string obj_suffix; - opt_string exe_prefix; - opt_string exe_suffix; - opt_string deps_mode_str; - optional do_debug; - optional do_optimize; - opt_str_seq include_template; - opt_str_seq external_include_template; - opt_str_seq define_template; - opt_str_seq warning_flags; - opt_str_seq flags; - opt_str_seq c_flags; - opt_str_seq cxx_flags; - opt_str_seq link_flags; - opt_str_seq c_compile_file; - opt_str_seq cxx_compile_file; - opt_str_seq create_archive; - opt_str_seq link_executable; - opt_str_seq compile_launcher; - - lm::read(context, - pairs, - // Base compile info: - lm::read_opt("Compiler-ID", compiler_id), - lm::read_opt("C-Compiler", c_compiler_fpath), - lm::read_opt("C++-Compiler", cxx_compiler_fpath), - // Language options - lm::read_opt("C-Version", c_version), - lm::read_opt("C++-Version", cxx_version), - // Flag templates - read_argv{"Include-Template", include_template}, - read_argv{"External-Include-Template", include_template}, - read_argv{"Define-Template", define_template}, - // Flags - read_argv_acc{"Warning-Flags", warning_flags}, - read_argv_acc{"Flags", flags}, - read_argv_acc{"C-Flags", c_flags}, - read_argv_acc{"C++-Flags", cxx_flags}, - read_argv_acc{"Link-Flags", link_flags}, - // Options for flags - lm::read_bool("Optimize", do_optimize), - lm::read_bool("Debug", do_debug), - // Miscellaneous - read_argv{"Compiler-Launcher", compile_launcher}, - lm::read_opt("Deps-Mode", deps_mode_str), - // Command templates - read_argv{"C-Compile-File", c_compile_file}, - read_argv{"C++-Compile-File", cxx_compile_file}, - read_argv{"Create-Archive", create_archive}, - read_argv{"Link-Executable", link_executable}, - // Filename affixes - lm::read_opt("Archive-Prefix", archive_prefix), - lm::read_opt("Archive-Suffix", archive_suffix), - lm::read_opt("Object-Prefix", obj_prefix), - lm::read_opt("Object-Suffix", obj_suffix), - lm::read_opt("Executable-Prefix", exe_prefix), - lm::read_opt("Executable-Suffix", exe_suffix), - // Die: - lm_reject_dym{{ - "Compiler-ID", - "C-Compiler", - "C++-Compiler", - "C-Version", - "C++-Version", - "Include-Template", - "External-Include-Template", - "Define-Template", - "Warning-Flags", - "Flags", - "C-Flags", - "C++-Flags", - "Link-Flags", - "Optimize", - "Debug", - "Compiler-Launcher", - "Deps-Mode", - "C-Compile-File", - "C++-Compile-File", - "Create-Archive", - "Link-Executable", - "Archive-Prefix", - "Archive-Suffix", - "Object-Prefix", - "Object-Suffix", - "Executable-Prefix", - "Executable-Suffix", - }}); - - toolchain_prep tc; - - enum compiler_id_e_t { - no_comp_id, - msvc, - clang, - gnu, - } compiler_id_e - = [&] { - if (!compiler_id) { - return no_comp_id; - } else if (compiler_id == "MSVC") { - return msvc; - } else if (compiler_id == "GNU") { - return gnu; - } else if (compiler_id == "Clang") { - return clang; - } else { - fail(context, "Unknown Compiler-ID '{}'", *compiler_id); - } - }(); - - bool is_gnu = compiler_id_e == gnu; - bool is_clang = compiler_id_e == clang; - bool is_msvc = compiler_id_e == msvc; - bool is_gnu_like = is_gnu || is_clang; - - const enum file_deps_mode deps_mode = [&] { - if (!deps_mode_str.has_value()) { - if (is_gnu_like) { - return file_deps_mode::gnu; - } else if (is_msvc) { - return file_deps_mode::msvc; - } else { - return file_deps_mode::none; - } - } else if (deps_mode_str == "GNU") { - return file_deps_mode::gnu; - } else if (deps_mode_str == "MSVC") { - return file_deps_mode::msvc; - } else if (deps_mode_str == "None") { - return file_deps_mode::none; - } else { - fail(context, "Unknown Deps-Mode '{}'", *deps_mode_str); - } - }(); - - // Now convert the flags we've been given into a real toolchain - auto get_compiler = [&](language lang) -> string { - if (lang == language::cxx && cxx_compiler_fpath) { - return *cxx_compiler_fpath; - } - if (lang == language::c && c_compiler_fpath) { - return *c_compiler_fpath; - } - if (!compiler_id.has_value()) { - fail(context, "Unable to determine what compiler to use."); - } - if (is_gnu) { - return (lang == language::cxx) ? "g++" : "gcc"; - } - if (is_clang) { - return (lang == language::cxx) ? "clang++" : "clang"; - } - if (is_msvc) { - return "cl.exe"; - } - assert(false && "Compiler name deduction failed"); - std::terminate(); - }; - - enum c_version_e_t { - c_none, - c89, - c99, - c11, - c18, - } c_version_e - = [&] { - if (!c_version) { - return c_none; - } else if (c_version == "C89") { - return c89; - } else if (c_version == "C99") { - return c99; - } else if (c_version == "C11") { - return c11; - } else if (c_version == "C18") { - return c18; - } else { - fail(context, "Unknown C-Version '{}'", *c_version); - } - }(); - - enum cxx_version_e_t { - cxx_none, - cxx98, - cxx03, - cxx11, - cxx14, - cxx17, - cxx20, - } cxx_version_e - = [&] { - if (!cxx_version) { - return cxx_none; - } else if (cxx_version == "C++98") { - return cxx98; - } else if (cxx_version == "C++03") { - return cxx03; - } else if (cxx_version == "C++11") { - return cxx11; - } else if (cxx_version == "C++14") { - return cxx14; - } else if (cxx_version == "C++17") { - return cxx17; - } else if (cxx_version == "C++20") { - return cxx20; - } else { - fail(context, "Unknown C++-Version '{}'", *cxx_version); - } - }(); - - std::map, string_seq> c_version_flag_table = { - {{msvc, c_none}, {}}, - {{msvc, c89}, {}}, - {{msvc, c99}, {}}, - {{msvc, c11}, {}}, - {{msvc, c18}, {}}, - {{gnu, c_none}, {}}, - {{gnu, c89}, {"-std=c89"}}, - {{gnu, c99}, {"-std=c99"}}, - {{gnu, c11}, {"-std=c11"}}, - {{gnu, c18}, {"-std=c18"}}, - {{clang, c_none}, {}}, - {{clang, c89}, {"-std=c89"}}, - {{clang, c99}, {"-std=c99"}}, - {{clang, c11}, {"-std=c11"}}, - {{clang, c18}, {"-std=c18"}}, - }; - - auto get_c_version_flags = [&]() -> string_seq { - if (!compiler_id.has_value()) { - fail(context, "Unable to deduce flags for 'C-Version' without setting 'Compiler-ID'"); - } - auto c_ver_iter = c_version_flag_table.find({compiler_id_e, c_version_e}); - assert(c_ver_iter != c_version_flag_table.end()); - return c_ver_iter->second; - }; - - std::map, string_seq> cxx_version_flag_table = { - {{msvc, cxx_none}, {}}, - {{msvc, cxx98}, {}}, - {{msvc, cxx03}, {}}, - {{msvc, cxx11}, {}}, - {{msvc, cxx14}, {"/std:c++14"}}, - {{msvc, cxx17}, {"/std:c++17"}}, - {{msvc, cxx20}, {"/std:c++latest"}}, - {{gnu, cxx_none}, {}}, - {{gnu, cxx98}, {"-std=c++98"}}, - {{gnu, cxx03}, {"-std=c++03"}}, - {{gnu, cxx11}, {"-std=c++11"}}, - {{gnu, cxx14}, {"-std=c++14"}}, - {{gnu, cxx17}, {"-std=c++17"}}, - {{gnu, cxx20}, {"-std=c++20"}}, - {{clang, cxx_none}, {}}, - {{clang, cxx98}, {"-std=c++98"}}, - {{clang, cxx03}, {"-std=c++03"}}, - {{clang, cxx11}, {"-std=c++11"}}, - {{clang, cxx14}, {"-std=c++14"}}, - {{clang, cxx17}, {"-std=c++17"}}, - {{clang, cxx20}, {"-std=c++20"}}, - }; - - auto get_cxx_version_flags = [&]() -> string_seq { - if (!compiler_id.has_value()) { - fail(context, "Unable to deduce flags for 'C++-Version' without setting 'Compiler-ID'"); - } - auto cxx_ver_iter = cxx_version_flag_table.find({compiler_id_e, cxx_version_e}); - assert(cxx_ver_iter != cxx_version_flag_table.end()); - return cxx_ver_iter->second; - }; - - auto get_link_flags = [&]() -> string_seq { - string_seq ret; - if (is_msvc) { - strv rt_lib = "/MT"; - if (do_optimize.value_or(false)) { - extend(ret, {"/O2"}); - } - if (do_debug.value_or(false)) { - extend(ret, {"/Z7", "/DEBUG"}); - rt_lib = "/MTd"; - } - ret.emplace_back(rt_lib); - } else if (is_gnu_like) { - if (do_optimize.value_or(false)) { - extend(ret, {"-O2"}); - } - if (do_debug.value_or(false)) { - extend(ret, {"-g"}); - } - } - if (link_flags) { - extend(ret, *link_flags); - } - return ret; - }; - - auto get_flags = [&](language lang) -> string_seq { - string_seq ret; - if (lang == language::cxx && cxx_flags) { - extend(ret, *cxx_flags); - } - if (lang == language::cxx && cxx_version) { - extend(ret, get_cxx_version_flags()); - } - if (lang == language::c && c_flags) { - extend(ret, *c_flags); - } - if (lang == language::c && c_version) { - extend(ret, get_c_version_flags()); - } - if (is_msvc) { - strv rt_lib = "/MT"; - if (do_optimize.has_value() && *do_optimize) { - extend(ret, {"/O2"}); - } - if (do_debug.has_value() && *do_debug) { - extend(ret, {"/Z7", "/DEBUG"}); - rt_lib = "/MTd"; - } - ret.emplace_back(rt_lib); - if (lang == language::cxx) { - extend(ret, {"/EHsc"}); - } - extend(ret, {"/nologo", "/permissive-", "", "/c", "", "/Fo"}); - } else if (is_gnu_like) { - if (do_optimize.has_value() && *do_optimize) { - extend(ret, {"-O2"}); - } - if (do_debug.has_value() && *do_debug) { - extend(ret, {"-g"}); - } - extend(ret, - {"-fPIC", - "-fdiagnostics-color", - "-pthread", - "", - "-c", - "", - "-o"}); - } - if (flags) { - extend(ret, *flags); - } - return ret; - }; - - tc.deps_mode = deps_mode; - - tc.c_compile = read_opt(c_compile_file, [&] { - string_seq c; - if (compile_launcher) { - extend(c, *compile_launcher); - } - c.push_back(get_compiler(language::c)); - extend(c, get_flags(language::c)); - return c; - }); - - tc.cxx_compile = read_opt(cxx_compile_file, [&] { - string_seq cxx; - if (compile_launcher) { - extend(cxx, *compile_launcher); - } - cxx.push_back(get_compiler(language::cxx)); - extend(cxx, get_flags(language::cxx)); - return cxx; - }); - - tc.include_template = read_opt(include_template, [&]() -> string_seq { - if (!compiler_id) { - fail(context, "Cannot deduce 'Include-Template' without 'Compiler-ID'"); - } - if (is_gnu_like) { - return {"-I", ""}; - } else if (is_msvc) { - return {"/I", ""}; - } - assert(false && "Include-Template deduction failed"); - std::terminate(); - }); - - tc.external_include_template = read_opt(external_include_template, [&]() -> string_seq { - if (!compiler_id) { - // Just reuse the include template for regular files - return tc.include_template; - } - if (is_gnu_like) { - return {"-isystem", ""}; - } else if (is_msvc) { - // MSVC has external-header support inbound, but it is not fully ready yet - return {"/I", ""}; - } - assert(false && "External-Include-Template deduction failed"); - std::terminate(); - }); - - tc.define_template = read_opt(define_template, [&]() -> string_seq { - if (!compiler_id) { - fail(context, "Cannot deduce 'Define-Template' without 'Compiler-ID'"); - } - if (is_gnu_like) { - return {"-D", ""}; - } else if (is_msvc) { - return {"/D", ""}; - } - assert(false && "Define-Template deduction failed"); - std::terminate(); - }); - - tc.archive_prefix = archive_prefix.value_or("lib"); - tc.archive_suffix = read_opt(archive_suffix, [&] { - if (!compiler_id) { - fail(context, "Cannot deduce library file extension without Compiler-ID"); - } - if (is_gnu) { - return ".a"; - } else if (is_msvc) { - return ".lib"; - } - assert(false && "No archive suffix"); - std::terminate(); - }); - - tc.object_prefix = obj_prefix.value_or(""); - tc.object_suffix = read_opt(obj_suffix, [&] { - if (!compiler_id) { - fail(context, "Cannot deduce object file extension without Compiler-ID"); - } - if (is_gnu) { - return ".o"; - } else if (is_msvc) { - return ".obj"; - } - assert(false && "No object file suffix"); - std::terminate(); - }); - - tc.exe_prefix = exe_prefix.value_or(""); - tc.exe_suffix = read_opt(exe_suffix, [&] { -#ifdef _WIN32 - return ".exe"; -#else - return ""; -#endif - }); - - tc.warning_flags = read_opt(warning_flags, [&]() -> string_seq { - if (!compiler_id) { - // No error. Just no warning flags - return {}; - } - if (is_msvc) { - return {"/W4"}; - } else if (is_gnu_like) { - return {"-Wall", "-Wextra", "-Wpedantic", "-Wconversion"}; - } - assert(false && "No warning flags"); - std::terminate(); - }); - - tc.link_archive = read_opt(create_archive, [&]() -> string_seq { - if (!compiler_id) { - fail(context, "Unable to deduce archive creation rules without a Compiler-ID"); - } - if (is_msvc) { - return {"lib", "/nologo", "/OUT:", ""}; - } else if (is_gnu_like) { - return {"ar", "rcs", "", ""}; - } - assert(false && "No archive command"); - std::terminate(); - }); - - tc.link_exe = read_opt(link_executable, [&]() -> string_seq { - if (!compiler_id) { - fail(context, "Unable to deduce how to link executables without a Compiler-ID"); - } - string_seq ret; - if (is_msvc) { - ret = {get_compiler(language::cxx), "/nologo", "/EHsc", "", "/Fe"}; - } else if (is_gnu_like) { - ret = {get_compiler(language::cxx), - "-fPIC", - "-fdiagnostics-color", - "", - "-pthread", - "-lstdc++fs", - "-o"}; - } else { - assert(false && "No link-exe command"); - std::terminate(); - } - extend(ret, get_link_flags()); - return ret; - }); - - return tc.realize(); -} diff --git a/src/dds/toolchain/from_dds.hpp b/src/dds/toolchain/from_dds.hpp deleted file mode 100644 index 0c65b5ec..00000000 --- a/src/dds/toolchain/from_dds.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace dds { - -class toolchain; - -toolchain parse_toolchain_dds(std::string_view str, - std::string_view context = "Loading toolchain file"); -toolchain parse_toolchain_dds(const lm::pair_list&, - std::string_view context = "Loading toolchain file"); - -} // namespace dds diff --git a/src/dds/toolchain/from_json.cpp b/src/dds/toolchain/from_json.cpp new file mode 100644 index 00000000..0f65b801 --- /dev/null +++ b/src/dds/toolchain/from_json.cpp @@ -0,0 +1,647 @@ +#include "./from_json.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace dds; + +using std::optional; +using std::string; +using std::vector; +using string_seq = vector; +using opt_string = optional; +using opt_string_seq = optional; +using strv = std::string_view; + +namespace { + +template +T read_opt(const std::optional& what, Func&& fn) { + if (!what.has_value()) { + return fn(); + } + return *what; +} + +template +[[noreturn]] void fail(strv context, strv message, Args&&... args) { + auto fmtd = fmt::format(message, args...); + throw std::runtime_error(fmt::format("{} - Failed to read toolchain file: {}", context, fmtd)); +} + +} // namespace + +toolchain dds::parse_toolchain_json5(std::string_view j5_str, std::string_view context) { + auto dat = json5::parse_data(j5_str); + return parse_toolchain_json_data(dat, context); +} + +toolchain dds::parse_toolchain_json_data(const json5::data& dat, std::string_view context) { + using namespace semester; + + opt_string compiler_id; + opt_string c_compiler; + opt_string cxx_compiler; + opt_string c_version; + opt_string cxx_version; + opt_string_seq compiler_launcher; + + opt_string_seq common_flags; + opt_string_seq c_flags; + opt_string_seq cxx_flags; + opt_string_seq link_flags; + opt_string_seq warning_flags; + + optional do_debug; + optional do_optimize; + + // Advanced-mode: + opt_string deps_mode_str; + opt_string archive_prefix; + opt_string archive_suffix; + opt_string obj_prefix; + opt_string obj_suffix; + opt_string exe_prefix; + opt_string exe_suffix; + opt_string_seq base_warning_flags; + opt_string_seq include_template; + opt_string_seq external_include_template; + opt_string_seq define_template; + opt_string_seq c_compile_file; + opt_string_seq cxx_compile_file; + opt_string_seq create_archive; + opt_string_seq link_executable; + opt_string_seq tty_flags; + + // For copy-pasting convenience: ‘{}’ + + auto extend_flags = [&](string key, auto& opt_flags) { + return [&opt_flags, key](const json5::data& dat) { + if (!opt_flags) { + opt_flags.emplace(); + } + return decompose( // + dat, + try_seq{ + if_type([&](auto& str_) { + auto more_flags = split_shell_string(str_.as_string()); + extend(*opt_flags, more_flags); + return dc_accept; + }), + if_array{for_each{ + require_type{ + fmt::format("Elements of `{}` array must be strings", key)}, + write_to{std::back_inserter(*opt_flags)}, + }}, + reject_with{fmt::format("`{}` must be an array or a shell-like string", key)}, + }); + }; + }; + +#define KEY_EXTEND_FLAGS(Name) \ + if_key { #Name, extend_flags(#Name, Name) } + +#define KEY_STRING(Name) \ + if_key { #Name, require_type < string>("`" #Name "` must be a string"), put_into{Name }, } + + auto result = semester::decompose( // + dat, + try_seq{ + require_type("Root of toolchain data must be a mapping"), + mapping{ + if_key{"$schema", just_accept}, + KEY_STRING(compiler_id), + KEY_STRING(c_compiler), + KEY_STRING(cxx_compiler), + KEY_STRING(c_version), + KEY_STRING(cxx_version), + KEY_EXTEND_FLAGS(c_flags), + KEY_EXTEND_FLAGS(cxx_flags), + KEY_EXTEND_FLAGS(warning_flags), + KEY_EXTEND_FLAGS(link_flags), + KEY_EXTEND_FLAGS(compiler_launcher), + if_key{"debug", + require_type("`debug` must be a boolean value"), + put_into{do_debug}}, + if_key{"optimize", + require_type("`optimize` must be a boolean value"), + put_into{do_optimize}}, + if_key{"flags", extend_flags("flags", common_flags)}, + if_key{ + "advanced", + require_type("`advanced` must be a mapping"), + mapping{ + if_key{"deps_mode", + require_type("`deps_mode` must be a string"), + put_into{deps_mode_str}}, + KEY_EXTEND_FLAGS(include_template), + KEY_EXTEND_FLAGS(external_include_template), + KEY_EXTEND_FLAGS(define_template), + KEY_EXTEND_FLAGS(base_warning_flags), + KEY_EXTEND_FLAGS(c_compile_file), + KEY_EXTEND_FLAGS(cxx_compile_file), + KEY_EXTEND_FLAGS(create_archive), + KEY_EXTEND_FLAGS(link_executable), + KEY_EXTEND_FLAGS(tty_flags), + KEY_STRING(obj_prefix), + KEY_STRING(obj_suffix), + KEY_STRING(archive_prefix), + KEY_STRING(archive_suffix), + KEY_STRING(exe_prefix), + KEY_STRING(exe_suffix), + [&](auto key, auto) -> dc_reject_t { + auto dym = did_you_mean(key, + { + "deps_mode", + "include_template", + "external_include_template", + "define_template", + "base_warning_flags", + "c_compile_file", + "cxx_compile_file", + "create_archive", + "link_executable", + "obj_prefix", + "obj_suffix", + "archive_prefix", + "archive_suffix", + "exe_prefix", + "exe_suffix", + "tty_flags", + }); + fail(context, + "Unknown toolchain advanced-config key ‘{}’ (Did you mean ‘{}’?)", + key, + *dym); + std::terminate(); + }, + }, + }, + [&](auto key, auto &&) -> dc_reject_t { + // They've given an unknown key. Ouch. + auto dym = did_you_mean(key, + { + "compiler_id", + "c_compiler", + "cxx_compiler", + "c_version", + "cxx_version", + "c_flags", + "cxx_flags", + "warning_flags", + "link_flags", + "flags", + "debug", + "optimize", + }); + fail(context, + "Unknown toolchain config key ‘{}’ (Did you mean ‘{}’?)", + key, + *dym); + std::terminate(); + }, + }, + }); + + auto rej_opt = std::get_if(&result); + if (rej_opt) { + fail(context, rej_opt->message); + } + + enum compiler_id_e_t { + no_comp_id, + msvc, + clang, + gnu, + } compiler_id_e + = [&] { + if (!compiler_id) { + return no_comp_id; + } else if (compiler_id == "msvc") { + return msvc; + } else if (compiler_id == "gnu") { + return gnu; + } else if (compiler_id == "clang") { + return clang; + } else { + fail(context, "Invalid `compiler_id` value ‘{}’", *compiler_id); + } + }(); + + bool is_gnu = compiler_id_e == gnu; + bool is_clang = compiler_id_e == clang; + bool is_msvc = compiler_id_e == msvc; + bool is_gnu_like = is_gnu || is_clang; + + const enum file_deps_mode deps_mode = [&] { + if (!deps_mode_str.has_value()) { + if (is_gnu_like) { + return file_deps_mode::gnu; + } else if (is_msvc) { + return file_deps_mode::msvc; + } else { + return file_deps_mode::none; + } + } else if (deps_mode_str == "gnu") { + return file_deps_mode::gnu; + } else if (deps_mode_str == "msvc") { + return file_deps_mode::msvc; + } else if (deps_mode_str == "none") { + return file_deps_mode::none; + } else { + fail(context, "Invalid `deps_mode` value ‘{}’", *deps_mode_str); + } + }(); + + // Now convert the flags we've been given into a real toolchain + auto get_compiler_executable_path = [&](language lang) -> string { + if (lang == language::cxx && cxx_compiler) { + return *cxx_compiler; + } + if (lang == language::c && c_compiler) { + return *c_compiler; + } + if (!compiler_id.has_value()) { + if (lang == language::c) { + fail(context, "Unable to determine the executable for a C compiler"); + } + if (lang == language::cxx) { + fail(context, "Unable to determine the executable for a C++ compiler"); + } + } + if (is_gnu) { + return (lang == language::cxx) ? "g++" : "gcc"; + } + if (is_clang) { + return (lang == language::cxx) ? "clang++" : "clang"; + } + if (is_msvc) { + return "cl.exe"; + } + assert(false && "Compiler name deduction failed"); + std::terminate(); + }; + + // Determine the C language version + enum c_version_e_t { + c_none, + c89, + c99, + c11, + c18, + } c_version_e + = [&] { + if (!c_version) { + return c_none; + } else if (c_version == "c89") { + return c89; + } else if (c_version == "c99") { + return c99; + } else if (c_version == "c11") { + return c11; + } else if (c_version == "c18") { + return c18; + } else { + fail(context, "Unknown `c_version` ‘{}’", *c_version); + } + }(); + + enum cxx_version_e_t { + cxx_none, + cxx98, + cxx03, + cxx11, + cxx14, + cxx17, + cxx20, + } cxx_version_e + = [&] { + if (!cxx_version) { + return cxx_none; + } else if (cxx_version == "c++98") { + return cxx98; + } else if (cxx_version == "c++03") { + return cxx03; + } else if (cxx_version == "c++11") { + return cxx11; + } else if (cxx_version == "c++14") { + return cxx14; + } else if (cxx_version == "c++17") { + return cxx17; + } else if (cxx_version == "c++20") { + return cxx20; + } else { + fail(context, "Unknown `cxx_version` ‘{}’", *cxx_version); + } + }(); + + std::map, string_seq> c_version_flag_table = { + {{msvc, c_none}, {}}, + {{msvc, c89}, {}}, + {{msvc, c99}, {}}, + {{msvc, c11}, {}}, + {{msvc, c18}, {}}, + {{gnu, c_none}, {}}, + {{gnu, c89}, {"-std=c89"}}, + {{gnu, c99}, {"-std=c99"}}, + {{gnu, c11}, {"-std=c11"}}, + {{gnu, c18}, {"-std=c18"}}, + {{clang, c_none}, {}}, + {{clang, c89}, {"-std=c89"}}, + {{clang, c99}, {"-std=c99"}}, + {{clang, c11}, {"-std=c11"}}, + {{clang, c18}, {"-std=c18"}}, + }; + + auto get_c_version_flags = [&]() -> string_seq { + if (!compiler_id.has_value()) { + fail(context, "Unable to deduce flags for 'c_version' without setting 'compiler_id'"); + } + auto c_ver_iter = c_version_flag_table.find({compiler_id_e, c_version_e}); + assert(c_ver_iter != c_version_flag_table.end()); + return c_ver_iter->second; + }; + + std::map, string_seq> cxx_version_flag_table = { + {{msvc, cxx_none}, {}}, + {{msvc, cxx98}, {}}, + {{msvc, cxx03}, {}}, + {{msvc, cxx11}, {}}, + {{msvc, cxx14}, {"/std:c++14"}}, + {{msvc, cxx17}, {"/std:c++17"}}, + {{msvc, cxx20}, {"/std:c++latest"}}, + {{gnu, cxx_none}, {}}, + {{gnu, cxx98}, {"-std=c++98"}}, + {{gnu, cxx03}, {"-std=c++03"}}, + {{gnu, cxx11}, {"-std=c++11"}}, + {{gnu, cxx14}, {"-std=c++14"}}, + {{gnu, cxx17}, {"-std=c++17"}}, + {{gnu, cxx20}, {"-std=c++20"}}, + {{clang, cxx_none}, {}}, + {{clang, cxx98}, {"-std=c++98"}}, + {{clang, cxx03}, {"-std=c++03"}}, + {{clang, cxx11}, {"-std=c++11"}}, + {{clang, cxx14}, {"-std=c++14"}}, + {{clang, cxx17}, {"-std=c++17"}}, + {{clang, cxx20}, {"-std=c++20"}}, + }; + + auto get_cxx_version_flags = [&]() -> string_seq { + if (!compiler_id.has_value()) { + fail(context, "Unable to deduce flags for 'cxx_version' without setting 'compiler_id'"); + } + auto cxx_ver_iter = cxx_version_flag_table.find({compiler_id_e, cxx_version_e}); + assert(cxx_ver_iter != cxx_version_flag_table.end()); + return cxx_ver_iter->second; + }; + + auto get_link_flags = [&]() -> string_seq { + string_seq ret; + if (is_msvc) { + strv rt_lib = "/MT"; + if (do_optimize.value_or(false)) { + extend(ret, {"/O2"}); + } + if (do_debug.value_or(false)) { + extend(ret, {"/Z7", "/DEBUG"}); + rt_lib = "/MTd"; + } + ret.emplace_back(rt_lib); + } else if (is_gnu_like) { + if (do_optimize.value_or(false)) { + extend(ret, {"-O2"}); + } + if (do_debug.value_or(false)) { + extend(ret, {"-g"}); + } + } + if (link_flags) { + extend(ret, *link_flags); + } + return ret; + }; + + auto get_flags = [&](language lang) -> string_seq { + string_seq ret; + if (is_msvc) { + strv rt_lib = "/MT"; + if (do_optimize.has_value() && *do_optimize) { + extend(ret, {"/O2"}); + } + if (do_debug.has_value() && *do_debug) { + extend(ret, {"/Z7", "/DEBUG"}); + rt_lib = "/MTd"; + } + ret.emplace_back(rt_lib); + if (lang == language::cxx) { + extend(ret, {"/EHsc"}); + } + extend(ret, {"/nologo", "/permissive-", "[flags]", "/c", "[in]", "/Fo[out]"}); + } else if (is_gnu_like) { + if (do_optimize.has_value() && *do_optimize) { + extend(ret, {"-O2"}); + } + if (do_debug.has_value() && *do_debug) { + extend(ret, {"-g"}); + } + extend(ret, {"-fPIC", "-pthread", "[flags]", "-c", "[in]", "-o[out]"}); + } + if (common_flags) { + extend(ret, *common_flags); + } + if (lang == language::cxx && cxx_flags) { + extend(ret, *cxx_flags); + } + if (lang == language::cxx && cxx_version) { + extend(ret, get_cxx_version_flags()); + } + if (lang == language::c && c_flags) { + extend(ret, *c_flags); + } + if (lang == language::c && c_version) { + extend(ret, get_c_version_flags()); + } + return ret; + }; + + toolchain_prep tc; + tc.deps_mode = deps_mode; + tc.c_compile = read_opt(c_compile_file, [&] { + string_seq c; + if (compiler_launcher) { + extend(c, *compiler_launcher); + } + c.push_back(get_compiler_executable_path(language::c)); + extend(c, get_flags(language::c)); + return c; + }); + + tc.cxx_compile = read_opt(cxx_compile_file, [&] { + string_seq cxx; + if (compiler_launcher) { + extend(cxx, *compiler_launcher); + } + cxx.push_back(get_compiler_executable_path(language::cxx)); + extend(cxx, get_flags(language::cxx)); + return cxx; + }); + + tc.include_template = read_opt(include_template, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Cannot deduce 'include_template' without 'compiler_id'"); + } + if (is_gnu_like) { + return {"-I", "[path]"}; + } else if (is_msvc) { + return {"/I", "[path]"}; + } + assert(false && "'include_template' deduction failed"); + std::terminate(); + }); + + tc.external_include_template = read_opt(external_include_template, [&]() -> string_seq { + if (!compiler_id) { + // Just reuse the include template for regular files + return tc.include_template; + } + if (is_gnu_like) { + return {"-isystem", "[path]"}; + } else if (is_msvc) { + // MSVC has external-header support inbound, but it is not fully ready yet + return {"/I", "[path]"}; + } + assert(false && "external_include_template deduction failed"); + std::terminate(); + }); + + tc.define_template = read_opt(define_template, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Cannot deduce 'define_template' without 'compiler_id'"); + } + if (is_gnu_like) { + return {"-D", "[def]"}; + } else if (is_msvc) { + return {"/D", "[def]"}; + } + assert(false && "define_template deduction failed"); + std::terminate(); + }); + + tc.archive_prefix = archive_prefix.value_or("lib"); + tc.archive_suffix = read_opt(archive_suffix, [&] { + if (!compiler_id) { + fail(context, "Cannot deduce library file extension without 'compiler_id'"); + } + if (is_gnu_like) { + return ".a"; + } else if (is_msvc) { + return ".lib"; + } + assert(false && "No archive suffix"); + std::terminate(); + }); + + tc.object_prefix = obj_prefix.value_or(""); + tc.object_suffix = read_opt(obj_suffix, [&] { + if (!compiler_id) { + fail(context, "Cannot deduce object file extension without 'compiler_id'"); + } + if (is_gnu_like) { + return ".o"; + } else if (is_msvc) { + return ".obj"; + } + assert(false && "No object file suffix"); + std::terminate(); + }); + + tc.exe_prefix = exe_prefix.value_or(""); + tc.exe_suffix = read_opt(exe_suffix, [&] { +#ifdef _WIN32 + return ".exe"; +#else + return ""; +#endif + }); + + /// TODO: Handle base_warning_flags: + tc.warning_flags = read_opt(warning_flags, [&]() -> string_seq { + if (!compiler_id) { + // No error. Just no warning flags + return {}; + } + if (is_msvc) { + return {"/W4"}; + } else if (is_gnu_like) { + return {"-Wall", "-Wextra", "-Wpedantic", "-Wconversion"}; + } + assert(false && "No warning flags"); + std::terminate(); + }); + + tc.link_archive = read_opt(create_archive, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Unable to deduce archive creation rules without a 'compiler_id'"); + } + if (is_msvc) { + return {"lib", "/nologo", "/OUT:[out]", "[in]"}; + } else if (is_gnu_like) { + return {"ar", "rcs", "[out]", "[in]"}; + } + assert(false && "No archive command"); + std::terminate(); + }); + + tc.link_exe = read_opt(link_executable, [&]() -> string_seq { + if (!compiler_id) { + fail(context, "Unable to deduce how to link executables without a 'compiler_id'"); + } + string_seq ret; + if (is_msvc) { + ret = {get_compiler_executable_path(language::cxx), + "/nologo", + "/EHsc", + "[in]", + "/Fe[out]"}; + } else if (is_gnu_like) { + ret = {get_compiler_executable_path(language::cxx), + "-fPIC", + "[in]", + "-pthread", + "-o[out]"}; + } else { + assert(false && "No link-exe command"); + std::terminate(); + } + extend(ret, get_link_flags()); + return ret; + }); + + tc.tty_flags = read_opt(tty_flags, [&]() -> string_seq { + if (!compiler_id) { + // Don't deduce any flags. This is a non-error, as these flags should be purely + // aesthetic + return {}; + } + if (is_msvc) { + // MSVC doesn't have any special TTY flags (yet...) + return {}; + } else if (is_gnu_like) { + return {"-fdiagnostics-color"}; + } else { + assert(false && "Impossible compiler_id while deducing `tty_flags`"); + std::terminate(); + } + }); + + return tc.realize(); +} \ No newline at end of file diff --git a/src/dds/toolchain/from_json.hpp b/src/dds/toolchain/from_json.hpp new file mode 100644 index 00000000..ab2169b6 --- /dev/null +++ b/src/dds/toolchain/from_json.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include + +#include + +namespace dds { + +toolchain parse_toolchain_json5(std::string_view json5, + std::string_view context = "Loading toolchain JSON"); + +toolchain parse_toolchain_json_data(const json5::data& data, + std::string_view context = "Loading toolchain JSON"); + +} // namespace dds \ No newline at end of file diff --git a/src/dds/toolchain/from_dds.test.cpp b/src/dds/toolchain/from_json.test.cpp similarity index 63% rename from src/dds/toolchain/from_dds.test.cpp rename to src/dds/toolchain/from_json.test.cpp index 694c4911..a9ee0f3d 100644 --- a/src/dds/toolchain/from_dds.test.cpp +++ b/src/dds/toolchain/from_json.test.cpp @@ -1,28 +1,26 @@ -#include +#include #include -// #include #include namespace { - void check_tc_compile(std::string_view tc_content, std::string_view expected_compile, std::string_view expected_compile_warnings, std::string_view expected_ar, std::string_view expected_exe) { - auto tc = dds::parse_toolchain_dds(tc_content); + auto tc = dds::parse_toolchain_json5(tc_content); dds::compile_file_spec cf; cf.source_path = "foo.cpp"; cf.out_path = "foo.o"; - auto cf_cmd = tc.create_compile_command(cf); + auto cf_cmd = tc.create_compile_command(cf, dds::toolchain_knobs{}); auto cf_cmd_str = dds::quote_command(cf_cmd.command); CHECK(cf_cmd_str == expected_compile); cf.enable_warnings = true; - cf_cmd = tc.create_compile_command(cf); + cf_cmd = tc.create_compile_command(cf, dds::toolchain_knobs{}); cf_cmd_str = dds::quote_command(cf_cmd.command); CHECK(cf_cmd_str == expected_compile_warnings); @@ -30,7 +28,7 @@ void check_tc_compile(std::string_view tc_content, ar_spec.input_files.push_back("foo.o"); ar_spec.input_files.push_back("bar.o"); ar_spec.out_path = "stuff.a"; - auto ar_cmd = tc.create_archive_command(ar_spec); + auto ar_cmd = tc.create_archive_command(ar_spec, dds::toolchain_knobs{}); auto ar_cmd_str = dds::quote_command(ar_cmd); CHECK(ar_cmd_str == expected_ar); @@ -38,62 +36,67 @@ void check_tc_compile(std::string_view tc_content, exe_spec.inputs.push_back("foo.o"); exe_spec.inputs.push_back("bar.a"); exe_spec.output = "meow.exe"; - auto exe_cmd = tc.create_link_executable_command(exe_spec); + auto exe_cmd = tc.create_link_executable_command(exe_spec, dds::toolchain_knobs{}); auto exe_cmd_str = dds::quote_command(exe_cmd); CHECK(exe_cmd_str == expected_exe); } -TEST_CASE("Generating toolchain commands") { - check_tc_compile( - "Compiler-ID: GNU", - "g++ -fPIC -fdiagnostics-color -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", - "g++ -fPIC -fdiagnostics-color -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC -fdiagnostics-color foo.o bar.a -pthread -lstdc++fs -omeow.exe"); - - check_tc_compile( - "Compiler-ID: GNU\nDebug: True", - "g++ -g -fPIC -fdiagnostics-color -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", - "g++ -g -fPIC -fdiagnostics-color -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC -fdiagnostics-color foo.o bar.a -pthread -lstdc++fs -omeow.exe -g"); +} // namespace - check_tc_compile( - "Compiler-ID: GNU\nDebug: True\nOptimize: True", - "g++ -O2 -g -fPIC -fdiagnostics-color -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp " - "-ofoo.o", - "g++ -O2 -g -fPIC -fdiagnostics-color -pthread -Wall -Wextra -Wpedantic -Wconversion " - "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", - "ar rcs stuff.a foo.o bar.o", - "g++ -fPIC -fdiagnostics-color foo.o bar.a -pthread -lstdc++fs -omeow.exe -O2 -g"); - - check_tc_compile("Compiler-ID: MSVC", +TEST_CASE("Generating toolchain commands") { + check_tc_compile("{compiler_id: 'gnu'}", + "g++ -fPIC -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", + "g++ -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " + "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", + "ar rcs stuff.a foo.o bar.o", + "g++ -fPIC foo.o bar.a -pthread -omeow.exe"); + + check_tc_compile("{compiler_id: 'gnu', debug: true}", + "g++ -g -fPIC -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", + "g++ -g -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " + "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", + "ar rcs stuff.a foo.o bar.o", + "g++ -fPIC foo.o bar.a -pthread -omeow.exe -g"); + + check_tc_compile("{compiler_id: 'gnu', debug: true, optimize: true}", + "g++ -O2 -g -fPIC -pthread -MD -MF foo.o.d -MT foo.o -c foo.cpp " + "-ofoo.o", + "g++ -O2 -g -fPIC -pthread -Wall -Wextra -Wpedantic -Wconversion " + "-MD -MF foo.o.d -MT foo.o -c foo.cpp -ofoo.o", + "ar rcs stuff.a foo.o bar.o", + "g++ -fPIC foo.o bar.a -pthread -omeow.exe -O2 -g"); + + check_tc_compile("{compiler_id: 'msvc'}", "cl.exe /MT /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", "cl.exe /MT /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", "lib /nologo /OUT:stuff.a foo.o bar.o", "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT"); check_tc_compile( - "Compiler-ID: MSVC\nDebug: True", + "{compiler_id: 'msvc', debug: true}", "cl.exe /Z7 /DEBUG /MTd /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o", "cl.exe /Z7 /DEBUG /MTd /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o", "lib /nologo /OUT:stuff.a foo.o bar.o", "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /Z7 /DEBUG /MTd"); - auto tc = dds::parse_toolchain_dds(R"( - Compiler-ID: GNU -)"); + check_tc_compile( + "{compiler_id: 'msvc', flags: '-DFOO'}", + "cl.exe /MT /EHsc /nologo /permissive- /showIncludes /c foo.cpp /Fofoo.o -DFOO", + "cl.exe /MT /EHsc /nologo /permissive- /W4 /showIncludes /c foo.cpp /Fofoo.o -DFOO", + "lib /nologo /OUT:stuff.a foo.o bar.o", + "cl.exe /nologo /EHsc foo.o bar.a /Femeow.exe /MT"); +} + +TEST_CASE("Manipulate a toolchain and file compilation") { + auto tc = dds::parse_toolchain_json5("{compiler_id: 'gnu'}"); dds::compile_file_spec cfs; cfs.source_path = "foo.cpp"; cfs.out_path = "foo.o"; - auto cmd = tc.create_compile_command(cfs); + auto cmd = tc.create_compile_command(cfs, dds::toolchain_knobs{}); CHECK(cmd.command == std::vector{"g++", "-fPIC", - "-fdiagnostics-color", "-pthread", "-MD", "-MF", @@ -105,12 +108,12 @@ TEST_CASE("Generating toolchain commands") { "-ofoo.o"}); cfs.definitions.push_back("FOO=BAR"); - cmd = tc.create_compile_command(cfs); + cmd = tc.create_compile_command(cfs, dds::toolchain_knobs{.is_tty = true}); CHECK(cmd.command == std::vector{"g++", "-fPIC", - "-fdiagnostics-color", "-pthread", + "-fdiagnostics-color", "-D", "FOO=BAR", "-MD", @@ -123,11 +126,10 @@ TEST_CASE("Generating toolchain commands") { "-ofoo.o"}); cfs.include_dirs.push_back("fake-dir"); - cmd = tc.create_compile_command(cfs); + cmd = tc.create_compile_command(cfs, dds::toolchain_knobs{}); CHECK(cmd.command == std::vector{"g++", "-fPIC", - "-fdiagnostics-color", "-pthread", "-I", "fake-dir", @@ -142,5 +144,3 @@ TEST_CASE("Generating toolchain commands") { "foo.cpp", "-ofoo.o"}); } - -} // namespace diff --git a/src/dds/toolchain/prep.hpp b/src/dds/toolchain/prep.hpp index fd2bea61..30019d6c 100644 --- a/src/dds/toolchain/prep.hpp +++ b/src/dds/toolchain/prep.hpp @@ -19,6 +19,7 @@ struct toolchain_prep { string_seq link_archive; string_seq link_exe; string_seq warning_flags; + string_seq tty_flags; std::string archive_prefix; std::string archive_suffix; diff --git a/src/dds/toolchain/toolchain.cpp b/src/dds/toolchain/toolchain.cpp index cef35c0c..c5025dde 100644 --- a/src/dds/toolchain/toolchain.cpp +++ b/src/dds/toolchain/toolchain.cpp @@ -1,8 +1,9 @@ #include "./toolchain.hpp" -#include +#include #include #include +#include #include #include @@ -35,25 +36,24 @@ toolchain toolchain::realize(const toolchain_prep& prep) { ret._exe_prefix = prep.exe_prefix; ret._exe_suffix = prep.exe_suffix; ret._deps_mode = prep.deps_mode; + ret._tty_flags = prep.tty_flags; return ret; } vector toolchain::include_args(const fs::path& p) const noexcept { - return replace(_inc_template, "", p.string()); + return replace(_inc_template, "[path]", p.string()); } vector toolchain::external_include_args(const fs::path& p) const noexcept { - return replace(_extern_inc_template, "", p.string()); + return replace(_extern_inc_template, "[path]", p.string()); } vector toolchain::definition_args(std::string_view s) const noexcept { - return replace(_def_template, "", s); + return replace(_def_template, "[def]", s); } -compile_command_info -toolchain::create_compile_command(const compile_file_spec& spec) const noexcept { - vector flags; - +compile_command_info toolchain::create_compile_command(const compile_file_spec& spec, + toolchain_knobs knobs) const noexcept { using namespace std::literals; language lang = spec.lang; @@ -65,7 +65,10 @@ toolchain::create_compile_command(const compile_file_spec& spec) const noexcept } } - auto& cmd_template = lang == language::c ? _c_compile : _cxx_compile; + vector flags; + if (knobs.is_tty) { + extend(flags, _tty_flags); + } for (auto&& inc_dir : spec.include_dirs) { auto inc_args = include_args(inc_dir); @@ -102,43 +105,46 @@ toolchain::create_compile_command(const compile_file_spec& spec) const noexcept } vector command; + auto& cmd_template = lang == language::c ? _c_compile : _cxx_compile; for (auto arg : cmd_template) { - if (arg == "") { + if (arg == "[flags]") { extend(command, flags); } else { - arg = replace(arg, "", spec.source_path.string()); - arg = replace(arg, "", spec.out_path.string()); + arg = replace(arg, "[in]", spec.source_path.string()); + arg = replace(arg, "[out]", spec.out_path.string()); command.push_back(arg); } } return {command, gnu_depfile_path}; } -vector toolchain::create_archive_command(const archive_spec& spec) const noexcept { +vector toolchain::create_archive_command(const archive_spec& spec, + toolchain_knobs) const noexcept { vector cmd; for (auto& arg : _link_archive) { - if (arg == "") { + if (arg == "[in]") { std::transform(spec.input_files.begin(), spec.input_files.end(), std::back_inserter(cmd), [](auto&& p) { return p.string(); }); } else { - cmd.push_back(replace(arg, "", spec.out_path.string())); + cmd.push_back(replace(arg, "[out]", spec.out_path.string())); } } return cmd; } -vector toolchain::create_link_executable_command(const link_exe_spec& spec) const noexcept { +vector toolchain::create_link_executable_command(const link_exe_spec& spec, + toolchain_knobs) const noexcept { vector cmd; for (auto& arg : _link_exe) { - if (arg == "") { + if (arg == "[in]") { std::transform(spec.inputs.begin(), spec.inputs.end(), std::back_inserter(cmd), [](auto&& p) { return p.string(); }); } else { - cmd.push_back(replace(arg, "", spec.output.string())); + cmd.push_back(replace(arg, "[out]", spec.output.string())); } } return cmd; @@ -147,31 +153,32 @@ vector toolchain::create_link_executable_command(const link_exe_spec& sp std::optional toolchain::get_builtin(std::string_view tc_id) noexcept { using namespace std::literals; - std::string tc_content; + json5::data tc_data = json5::data::mapping_type(); + auto& root_map = tc_data.as_object(); if (starts_with(tc_id, "debug:")) { tc_id = tc_id.substr("debug:"sv.length()); - tc_content += "Debug: True\n"; + root_map.emplace("debug", true); } if (starts_with(tc_id, "ccache:")) { tc_id = tc_id.substr("ccache:"sv.length()); - tc_content += "Compiler-Launcher: ccache\n"; + root_map.emplace("compiler_launcher", "ccache"); } #define CXX_VER_TAG(str, version) \ if (starts_with(tc_id, str)) { \ tc_id = tc_id.substr(std::string_view(str).length()); \ - tc_content += "C++-Version: "s + version + "\n"; \ + root_map.emplace("cxx_version", version); \ } \ static_assert(true) - CXX_VER_TAG("c++98:", "C++98"); - CXX_VER_TAG("c++03:", "C++03"); - CXX_VER_TAG("c++11:", "C++11"); - CXX_VER_TAG("c++14:", "C++14"); - CXX_VER_TAG("c++17:", "C++17"); - CXX_VER_TAG("c++20:", "C++20"); + CXX_VER_TAG("c++98:", "c++98"); + CXX_VER_TAG("c++03:", "c++03"); + CXX_VER_TAG("c++11:", "c++11"); + CXX_VER_TAG("c++14:", "c++14"); + CXX_VER_TAG("c++17:", "c++17"); + CXX_VER_TAG("c++20:", "c++20"); struct compiler_info { string c; @@ -186,9 +193,9 @@ std::optional toolchain::get_builtin(std::string_view tc_id) noexcept const auto [c_compiler_base, cxx_compiler_base, compiler_id] = [&]() -> compiler_info { if (is_gcc) { - return {"gcc", "g++", "GNU"}; + return {"gcc", "g++", "gnu"}; } else if (is_clang) { - return {"clang", "clang++", "Clang"}; + return {"clang", "clang++", "clang"}; } assert(false && "Unreachable"); std::terminate(); @@ -220,7 +227,7 @@ std::optional toolchain::get_builtin(std::string_view tc_id) noexcept auto cxx_compiler_name = cxx_compiler_base + compiler_suffix; return compiler_info{c_compiler_name, cxx_compiler_name, compiler_id}; } else if (tc_id == "msvc") { - return compiler_info{"cl.exe", "cl.exe", "MSVC"}; + return compiler_info{"cl.exe", "cl.exe", "msvc"}; } else { return std::nullopt; } @@ -230,8 +237,34 @@ std::optional toolchain::get_builtin(std::string_view tc_id) noexcept return std::nullopt; } - tc_content += "C-Compiler: "s + opt_triple->c + "\n"; - tc_content += "C++-Compiler: "s + opt_triple->cxx + "\n"; - tc_content += "Compiler-ID: " + opt_triple->id + "\n"; - return parse_toolchain_dds(tc_content); + if (starts_with(tc_id, "gcc") || starts_with(tc_id, "clang")) { + json5::data& arr = root_map.emplace("link_flags", json5::data::array_type()).first->second; + arr.as_array().emplace_back("-static-libgcc"); + arr.as_array().emplace_back("-static-libstdc++"); + } + + root_map.emplace("c_compiler", opt_triple->c); + root_map.emplace("cxx_compiler", opt_triple->cxx); + root_map.emplace("compiler_id", opt_triple->id); + return parse_toolchain_json_data(tc_data); +} + +std::optional dds::toolchain::get_default() { + auto candidates = { + fs::current_path() / "toolchain.json5", + fs::current_path() / "toolchain.jsonc", + fs::current_path() / "toolchain.json", + dds_config_dir() / "toolchain.json5", + dds_config_dir() / "toolchain.jsonc", + dds_config_dir() / "toolchain.json", + user_home_dir() / "toolchain.json5", + user_home_dir() / "toolchain.jsonc", + user_home_dir() / "toolchain.json", + }; + for (auto&& cand : candidates) { + if (fs::exists(cand)) { + return parse_toolchain_json5(slurp_file(cand)); + } + } + return std::nullopt; } diff --git a/src/dds/toolchain/toolchain.hpp b/src/dds/toolchain/toolchain.hpp index 5dc1f5e7..47e4bb07 100644 --- a/src/dds/toolchain/toolchain.hpp +++ b/src/dds/toolchain/toolchain.hpp @@ -16,6 +16,10 @@ enum class language { cxx, }; +struct toolchain_knobs { + bool is_tty = false; +}; + struct compile_file_spec { fs::path source_path; fs::path out_path; @@ -54,6 +58,7 @@ class toolchain { string_seq _link_archive; string_seq _link_exe; string_seq _warning_flags; + string_seq _tty_flags; std::string _archive_prefix; std::string _archive_suffix; @@ -77,11 +82,15 @@ class toolchain { std::vector definition_args(std::string_view s) const noexcept; std::vector include_args(const fs::path& p) const noexcept; std::vector external_include_args(const fs::path& p) const noexcept; - compile_command_info create_compile_command(const compile_file_spec&) const noexcept; - std::vector create_archive_command(const archive_spec&) const noexcept; - std::vector create_link_executable_command(const link_exe_spec&) const noexcept; + compile_command_info create_compile_command(const compile_file_spec&, + toolchain_knobs) const noexcept; + std::vector create_archive_command(const archive_spec&, + toolchain_knobs) const noexcept; + std::vector create_link_executable_command(const link_exe_spec&, + toolchain_knobs) const noexcept; static std::optional get_builtin(std::string_view key) noexcept; + static std::optional get_default(); }; } // namespace dds diff --git a/src/dds/util/output.hpp b/src/dds/util/output.hpp new file mode 100644 index 00000000..df5d078b --- /dev/null +++ b/src/dds/util/output.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace dds { + +bool stdout_is_a_tty() noexcept; + +} // namespace dds diff --git a/src/dds/util/output.nix.cpp b/src/dds/util/output.nix.cpp new file mode 100644 index 00000000..e94babf2 --- /dev/null +++ b/src/dds/util/output.nix.cpp @@ -0,0 +1,11 @@ +#if !_WIN32 + +#include + +#include + +using namespace dds; + +bool dds::stdout_is_a_tty() noexcept { return ::isatty(STDOUT_FILENO) != 0; } + +#endif diff --git a/src/dds/util/output.win.cpp b/src/dds/util/output.win.cpp new file mode 100644 index 00000000..eb46d02a --- /dev/null +++ b/src/dds/util/output.win.cpp @@ -0,0 +1,10 @@ +#if _WIN32 + +#include + +bool dds::stdout_is_a_tty() noexcept { + // XXX: Newer Windows consoles support ANSI color, so this should be made smarter + return false; +} + +#endif \ No newline at end of file diff --git a/src/dds/util/paths.linux.cpp b/src/dds/util/paths.linux_fbsd.cpp similarity index 97% rename from src/dds/util/paths.linux.cpp rename to src/dds/util/paths.linux_fbsd.cpp index 6db2fe2e..0be907c7 100644 --- a/src/dds/util/paths.linux.cpp +++ b/src/dds/util/paths.linux_fbsd.cpp @@ -1,4 +1,4 @@ -#ifdef __linux__ +#if __linux__ || __FreeBSD__ #include "./paths.hpp" diff --git a/tests/basics/config_template/copy_only/src/info.config.hpp b/tests/basics/config_template/copy_only/src/info.config.hpp new file mode 100644 index 00000000..1190151d --- /dev/null +++ b/tests/basics/config_template/copy_only/src/info.config.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +int config_file_value = 42; diff --git a/tests/basics/config_template/copy_only/src/info.test.cpp b/tests/basics/config_template/copy_only/src/info.test.cpp new file mode 100644 index 00000000..caf4ae8b --- /dev/null +++ b/tests/basics/config_template/copy_only/src/info.test.cpp @@ -0,0 +1,5 @@ +#include + +#include + +int main() { assert(config_file_value == 42); } diff --git a/tests/basics/config_template/simple/library.jsonc b/tests/basics/config_template/simple/library.jsonc new file mode 100644 index 00000000..6c300d07 --- /dev/null +++ b/tests/basics/config_template/simple/library.jsonc @@ -0,0 +1,3 @@ +{ + "name": "test-library" +} \ No newline at end of file diff --git a/tests/basics/config_template/simple/package.jsonc b/tests/basics/config_template/simple/package.jsonc new file mode 100644 index 00000000..6b63f8d2 --- /dev/null +++ b/tests/basics/config_template/simple/package.jsonc @@ -0,0 +1,5 @@ +{ + "name": "test-simple", + "version": "1.2.3-gamma", + "namespace": "test" +} \ No newline at end of file diff --git a/tests/basics/config_template/simple/src/simple/config.config.hpp b/tests/basics/config_template/simple/src/simple/config.config.hpp new file mode 100644 index 00000000..6efa5bde --- /dev/null +++ b/tests/basics/config_template/simple/src/simple/config.config.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::string_view lib_name = __dds(lib.name); diff --git a/tests/basics/config_template/simple/src/simple/simple.test.cpp b/tests/basics/config_template/simple/src/simple/simple.test.cpp new file mode 100644 index 00000000..3c099cab --- /dev/null +++ b/tests/basics/config_template/simple/src/simple/simple.test.cpp @@ -0,0 +1,5 @@ +#include + +#include + +int main() { assert(lib_name == "test-library"); } \ No newline at end of file diff --git a/tests/basics/config_template/test_config_template.py b/tests/basics/config_template/test_config_template.py new file mode 100644 index 00000000..6f9d72ce --- /dev/null +++ b/tests/basics/config_template/test_config_template.py @@ -0,0 +1,26 @@ +import pytest +from time import sleep + +from tests import DDS, dds_fixture_conf_1 + + +@dds_fixture_conf_1('copy_only') +def test_config_template(dds: DDS): + generated_fpath = dds.build_dir / '__dds/gen/info.hpp' + assert not generated_fpath.is_file() + dds.build() + assert generated_fpath.is_file() + + # Check that re-running the build will not update the generated file (the + # file's content has not changed. Re-generating it would invalidate the + # cache and force a false-rebuild.) + start_time = generated_fpath.stat().st_mtime + sleep(0.1) # Wait just long enough to register a new stamp time + dds.build() + new_time = generated_fpath.stat().st_mtime + assert new_time == start_time + + +@dds_fixture_conf_1('simple') +def test_simple_substitution(dds: DDS): + dds.build() diff --git a/tests/basics/test_basics.py b/tests/basics/test_basics.py index 72c20f4c..767b14b4 100644 --- a/tests/basics/test_basics.py +++ b/tests/basics/test_basics.py @@ -17,9 +17,12 @@ def test_build_simple(dds: DDS): def basic_pkg_dds(dds: DDS): return set_contents( - dds.source_root / 'package.dds', b''' - Name: test-pkg - Version: 0.2.2 + dds.source_root / 'package.json5', b''' + { + name: 'test-pkg', + version: '0.2.2', + namespace: 'test', + } ''') diff --git a/tests/catalog/get_test.py b/tests/catalog/get_test.py index 69c0db5f..fac2cb7c 100644 --- a/tests/catalog/get_test.py +++ b/tests/catalog/get_test.py @@ -3,6 +3,8 @@ from tests import dds, DDS from tests.fileutil import ensure_dir +import pytest + def test_get(dds: DDS): dds.scope.enter_context(ensure_dir(dds.build_dir)) @@ -16,8 +18,10 @@ def test_get(dds: DDS): '0.2.2': { 'depends': {}, 'git': { - 'url': 'https://github.com/vector-of-bool/neo-sqlite3.git', - 'ref': '0.2.2', + 'url': + 'https://github.com/vector-of-bool/neo-sqlite3.git', + 'ref': + '0.2.2', }, }, }, diff --git a/tests/dds.py b/tests/dds.py index db71cc6f..c6c328b8 100644 --- a/tests/dds.py +++ b/tests/dds.py @@ -52,11 +52,11 @@ def run_unchecked(self, cmd: proc.CommandLine, *, full_cmd = itertools.chain([self.dds_exe], cmd) return proc.run(full_cmd, cwd=cwd or self.source_root) - def run(self, cmd: proc.CommandLine, *, - cwd: Path = None) -> subprocess.CompletedProcess: + def run(self, cmd: proc.CommandLine, *, cwd: Path = None, + check=True) -> subprocess.CompletedProcess: cmdline = list(proc.flatten_cmd(cmd)) res = self.run_unchecked(cmd, cwd=cwd) - if res.returncode != 0: + if res.returncode != 0 and check: raise subprocess.CalledProcessError( res.returncode, [self.dds_exe] + cmdline, res.stdout) return res @@ -86,18 +86,22 @@ def build(self, toolchain: str = None, apps: bool = True, warnings: bool = True, - tests: bool = True) -> subprocess.CompletedProcess: - return self.run([ - 'build', - f'--out={self.build_dir}', - f'--toolchain={toolchain or self.default_builtin_toolchain}', - f'--catalog={self.catalog_path}', - f'--repo-dir={self.repo_dir}', - ['--no-tests'] if not tests else [], - ['--no-apps'] if not apps else [], - ['--no-warnings'] if not warnings else [], - self.project_dir_arg, - ]) + tests: bool = True, + check: bool = True) -> subprocess.CompletedProcess: + return self.run( + [ + 'build', + f'--out={self.build_dir}', + f'--toolchain={toolchain or self.default_builtin_toolchain}', + f'--catalog={self.catalog_path}', + f'--repo-dir={self.repo_dir}', + ['--no-tests'] if not tests else [], + ['--no-apps'] if not apps else [], + ['--no-warnings'] if not warnings else [], + self.project_dir_arg, + ], + check=check, + ) def sdist_create(self) -> subprocess.CompletedProcess: return self.run([ diff --git a/tests/deps/build-deps/project/deps.dds b/tests/deps/build-deps/project/deps.dds deleted file mode 100644 index 748d3c86..00000000 --- a/tests/deps/build-deps/project/deps.dds +++ /dev/null @@ -1,2 +0,0 @@ - -Depends: neo-sqlite3 +0.2.2 diff --git a/tests/deps/build-deps/project/deps.json5 b/tests/deps/build-deps/project/deps.json5 new file mode 100644 index 00000000..f078d36f --- /dev/null +++ b/tests/deps/build-deps/project/deps.json5 @@ -0,0 +1,5 @@ +{ + depends: { + 'neo-sqlite3': '+0.2.2', + }, +} diff --git a/tests/deps/build-deps/test_build_deps.py b/tests/deps/build-deps/test_build_deps.py index 6bbff525..eaca053e 100644 --- a/tests/deps/build-deps/test_build_deps.py +++ b/tests/deps/build-deps/test_build_deps.py @@ -4,7 +4,7 @@ def test_build_deps_from_file(dds: DDS): assert not dds.deps_build_dir.is_dir() dds.catalog_import(dds.source_root / 'catalog.json') - dds.build_deps(['-d', 'deps.dds']) + dds.build_deps(['-d', 'deps.json5']) assert (dds.deps_build_dir / 'neo-sqlite3@0.2.2').is_dir() assert (dds.scratch_dir / 'INDEX.lmi').is_file() assert (dds.deps_build_dir / '_libman/neo-sqlite3.lmp').is_file() diff --git a/tests/deps/git-remote/catalog.json b/tests/deps/git-remote/catalog.json index 2b6a4017..f3a14c27 100644 --- a/tests/deps/git-remote/catalog.json +++ b/tests/deps/git-remote/catalog.json @@ -5,7 +5,7 @@ "0.1.0": { "git": { "url": "https://github.com/vector-of-bool/neo-buffer.git", - "ref": "develop" + "ref": "0.1.0" }, "depends": {} } diff --git a/tests/deps/git-remote/package.dds b/tests/deps/git-remote/package.dds deleted file mode 100644 index 5b5e0814..00000000 --- a/tests/deps/git-remote/package.dds +++ /dev/null @@ -1,5 +0,0 @@ -Name: deps-test -Version: 0.0.0 - -Depends: neo-buffer 0.1.0 -Depends: range-v3 0.9.1 \ No newline at end of file diff --git a/tests/deps/git-remote/package.json5 b/tests/deps/git-remote/package.json5 new file mode 100644 index 00000000..dfcf2b2a --- /dev/null +++ b/tests/deps/git-remote/package.json5 @@ -0,0 +1,9 @@ +{ + name: 'deps-test', + "namespace": "test", + version: '0.0.0', + depends: { + 'neo-buffer': '0.1.0', + 'range-v3': '0.9.1', + } +} \ No newline at end of file diff --git a/tests/deps/no-deps/package.dds b/tests/deps/no-deps/package.dds deleted file mode 100644 index f50c0b7f..00000000 --- a/tests/deps/no-deps/package.dds +++ /dev/null @@ -1,2 +0,0 @@ -Name: deps-test -Version: 0.0.0 diff --git a/tests/deps/no-deps/package.json5 b/tests/deps/no-deps/package.json5 new file mode 100644 index 00000000..1edea844 --- /dev/null +++ b/tests/deps/no-deps/package.json5 @@ -0,0 +1,5 @@ +{ + name: 'deps-test', + version: '0.0.0', + "namespace": "test", +} \ No newline at end of file diff --git a/tests/deps/use-remote/library.dds b/tests/deps/use-remote/library.dds deleted file mode 100644 index 371a780b..00000000 --- a/tests/deps/use-remote/library.dds +++ /dev/null @@ -1,3 +0,0 @@ -Name: dummy - -Uses: nlohmann/json \ No newline at end of file diff --git a/tests/deps/use-remote/library.json5 b/tests/deps/use-remote/library.json5 new file mode 100644 index 00000000..1a2d7ba1 --- /dev/null +++ b/tests/deps/use-remote/library.json5 @@ -0,0 +1,6 @@ +{ + name: "dummy", + uses: [ + 'nlohmann/json', + ] +} \ No newline at end of file diff --git a/tests/deps/use-remote/package.dds b/tests/deps/use-remote/package.dds deleted file mode 100644 index dee6092e..00000000 --- a/tests/deps/use-remote/package.dds +++ /dev/null @@ -1,4 +0,0 @@ -Name: json-test -Version: 0.0.0 - -Depends: nlohmann-json 3.7.1 \ No newline at end of file diff --git a/tests/deps/use-remote/package.json5 b/tests/deps/use-remote/package.json5 new file mode 100644 index 00000000..0654df13 --- /dev/null +++ b/tests/deps/use-remote/package.json5 @@ -0,0 +1,8 @@ +{ + "name": "json-test", + "version": "0.0.0", + "namespace": "test", + "depends": { + "nlohmann-json": "3.7.1" + } +} \ No newline at end of file diff --git a/tests/deps/use-spdlog/gcc.tc.jsonc b/tests/deps/use-spdlog/gcc.tc.jsonc new file mode 100644 index 00000000..d70a9d4e --- /dev/null +++ b/tests/deps/use-spdlog/gcc.tc.jsonc @@ -0,0 +1,7 @@ +{ + "compiler_id": 'gnu', + "cxx_version": 'c++17', + "cxx_compiler": 'g++-9', + "flags": '-DSPDLOG_COMPILED_LIB', + "link_flags": '-static-libgcc -static-libstdc++' +} \ No newline at end of file diff --git a/tests/deps/use-spdlog/msvc.tc.jsonc b/tests/deps/use-spdlog/msvc.tc.jsonc new file mode 100644 index 00000000..497c53d3 --- /dev/null +++ b/tests/deps/use-spdlog/msvc.tc.jsonc @@ -0,0 +1,4 @@ +{ + "compiler_id": 'msvc', + "flags": '-DSPDLOG_COMPILED_LIB', +} \ No newline at end of file diff --git a/tests/deps/use-spdlog/project/library.dds b/tests/deps/use-spdlog/project/library.dds deleted file mode 100644 index d28da1b5..00000000 --- a/tests/deps/use-spdlog/project/library.dds +++ /dev/null @@ -1,3 +0,0 @@ -Name: spdlog-user - -Uses: spdlog/spdlog \ No newline at end of file diff --git a/tests/deps/use-spdlog/project/library.json5 b/tests/deps/use-spdlog/project/library.json5 new file mode 100644 index 00000000..b8699e2c --- /dev/null +++ b/tests/deps/use-spdlog/project/library.json5 @@ -0,0 +1,6 @@ +{ + name: 'spdlog-user', + uses: [ + 'spdlog/spdlog', + ] +} \ No newline at end of file diff --git a/tests/deps/use-spdlog/project/package.dds b/tests/deps/use-spdlog/project/package.dds deleted file mode 100644 index 63e122c8..00000000 --- a/tests/deps/use-spdlog/project/package.dds +++ /dev/null @@ -1,4 +0,0 @@ -Name: test -Version: 0.0.0 - -Depends: spdlog 1.4.2 \ No newline at end of file diff --git a/tests/deps/use-spdlog/project/package.json5 b/tests/deps/use-spdlog/project/package.json5 new file mode 100644 index 00000000..8d79494e --- /dev/null +++ b/tests/deps/use-spdlog/project/package.json5 @@ -0,0 +1,8 @@ +{ + name: 'test', + version: '0.0.0', + "namespace": "test", + depends: { + 'spdlog': '1.4.2', + }, +} \ No newline at end of file diff --git a/tests/deps/use-spdlog/use_spdlog_test.py b/tests/deps/use-spdlog/use_spdlog_test.py index 75232ab0..a6f9e9aa 100644 --- a/tests/deps/use-spdlog/use_spdlog_test.py +++ b/tests/deps/use-spdlog/use_spdlog_test.py @@ -5,7 +5,7 @@ def test_get_build_use_spdlog(dds: DDS): dds.catalog_import(dds.source_root / 'catalog.json') - tc_fname = 'gcc.tc.dds' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.dds' + tc_fname = 'gcc.tc.jsonc' if 'gcc' in dds.default_builtin_toolchain else 'msvc.tc.jsonc' tc = str(dds.test_dir / tc_fname) dds.build(toolchain=tc, apps=True) proc.check_run((dds.build_dir / 'use-spdlog').with_suffix(dds.exe_suffix)) diff --git a/tests/sdist/create/library.dds b/tests/sdist/create/library.dds deleted file mode 100644 index d4da056d..00000000 --- a/tests/sdist/create/library.dds +++ /dev/null @@ -1 +0,0 @@ -Name: foo \ No newline at end of file diff --git a/tests/sdist/create/library.jsonc b/tests/sdist/create/library.jsonc new file mode 100644 index 00000000..07a21daa --- /dev/null +++ b/tests/sdist/create/library.jsonc @@ -0,0 +1,3 @@ +{ + "name": "foo" +} \ No newline at end of file diff --git a/tests/sdist/create/package.dds b/tests/sdist/create/package.dds deleted file mode 100644 index 2fec2df7..00000000 --- a/tests/sdist/create/package.dds +++ /dev/null @@ -1,2 +0,0 @@ -Name: foo -Version: 1.2.3 \ No newline at end of file diff --git a/tests/sdist/create/package.json5 b/tests/sdist/create/package.json5 new file mode 100644 index 00000000..5ccccdc0 --- /dev/null +++ b/tests/sdist/create/package.json5 @@ -0,0 +1,5 @@ +{ + name: 'foo', + version: '1.2.3', + "namespace": "test", +} \ No newline at end of file diff --git a/tests/test_drivers/catch/custom-runner/package.dds b/tests/test_drivers/catch/custom-runner/package.dds deleted file mode 100644 index 0c160506..00000000 --- a/tests/test_drivers/catch/custom-runner/package.dds +++ /dev/null @@ -1,4 +0,0 @@ -Name: Test -Version: 0.0.0 - -Test-Driver: Catch \ No newline at end of file diff --git a/tests/test_drivers/catch/custom-runner/package.json5 b/tests/test_drivers/catch/custom-runner/package.json5 new file mode 100644 index 00000000..4102183e --- /dev/null +++ b/tests/test_drivers/catch/custom-runner/package.json5 @@ -0,0 +1,6 @@ +{ + "name": "test", + "version": "0.0.0", + "namespace": "test", + "test_driver": "Catch", +} \ No newline at end of file diff --git a/tests/test_drivers/catch/main/package.dds b/tests/test_drivers/catch/main/package.dds deleted file mode 100644 index 7fda0e8f..00000000 --- a/tests/test_drivers/catch/main/package.dds +++ /dev/null @@ -1,4 +0,0 @@ -Name: Test -Version: 0.0.0 - -Test-Driver: Catch-Main \ No newline at end of file diff --git a/tests/test_drivers/catch/main/package.json5 b/tests/test_drivers/catch/main/package.json5 new file mode 100644 index 00000000..23063cd0 --- /dev/null +++ b/tests/test_drivers/catch/main/package.json5 @@ -0,0 +1,6 @@ +{ + "name": "test", + "version": "0.0.0", + "namespace": "test", + "test_driver": "Catch-Main", +} \ No newline at end of file diff --git a/tools/bootstrap.py b/tools/bootstrap.py index 2ebfd837..09ccf39c 100644 --- a/tools/bootstrap.py +++ b/tools/bootstrap.py @@ -21,9 +21,9 @@ def platform_compiler(self): BOOTSTRAP_PHASES = [ - BootstrapPhase('bootstrap-p1', 'g++-8', 'cl.exe'), - BootstrapPhase('bootstrap-p4', 'g++-8', 'cl.exe'), - BootstrapPhase('bootstrap-p5.1', 'g++-9', 'cl.exe'), + BootstrapPhase('bootstrap-p1.2', 'g++-8', 'cl.exe'), + BootstrapPhase('bootstrap-p4.2', 'g++-8', 'cl.exe'), + BootstrapPhase('bootstrap-p5.2', 'g++-9', 'cl.exe'), ] HERE = Path(__file__).parent.absolute() @@ -100,7 +100,7 @@ def _run_boot_phase(phase: BootstrapPhase) -> Path: return _pull_executable(bts_dir) -def main(argv: Sequence[str]) -> int: +def main() -> int: for idx, phase in enumerate(BOOTSTRAP_PHASES): print(f'Bootstrap phase [{idx+1}/{len(BOOTSTRAP_PHASES)}]') exe = _run_boot_phase(phase) @@ -110,4 +110,4 @@ def main(argv: Sequence[str]) -> int: if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) + sys.exit(main()) diff --git a/tools/build.py b/tools/build.py index e3f1ff44..fa514578 100755 --- a/tools/build.py +++ b/tools/build.py @@ -33,6 +33,8 @@ def main(argv: Sequence[str]) -> int: print(f'Using previously built DDS executable: {dds_exe}') if os.name == 'nt': tc_fpath = ROOT / 'tools/msvc.dds' + elif sys.platform.startswith('freebsd'): + tc_fpath = ROOT / 'tools/freebsd-gcc-9.dds' else: tc_fpath = ROOT / 'tools/gcc-9.dds' diff --git a/tools/ci.py b/tools/ci.py index cd6b7511..959f7387 100644 --- a/tools/ci.py +++ b/tools/ci.py @@ -14,6 +14,7 @@ class CIOptions(NamedTuple): toolchain: str + toolchain_json5: str def _do_bootstrap_build(opts: CIOptions) -> None: @@ -67,9 +68,24 @@ def main(argv: Sequence[str]) -> int: '-T', help='The toolchain to use for the CI process', required=True) + parser.add_argument( + '--toolchain-json5', + '-T2', + help='The toolchain JSON to use with the bootstrapped executable', + ) + parser.add_argument( + '--build-only', + action='store_true', + help='Only build the `dds` executable. Skip second-phase and tests.') args = parser.parse_args(argv) - opts = CIOptions(toolchain=args.toolchain) + if not args.build_only and not args.toolchain_json5: + raise RuntimeError( + 'The --toolchain-json5/-T2 argument is required (unless using --build-only)' + ) + + opts = CIOptions( + toolchain=args.toolchain, toolchain_json5=args.toolchain_json5) if args.bootstrap_with == 'build': _do_bootstrap_build(opts) @@ -103,6 +119,13 @@ def main(argv: Sequence[str]) -> int: ('--repo-dir', ci_repo_dir), ]) print('Main build PASSED!') + print(f'A `dds` executable has been generated: {paths.CUR_BUILT_DDS}') + + if args.build_only: + print( + f'`--build-only` was given, so second phase and tests will not execute' + ) + return 0 # Delete the catalog database, since there may be schema changes since the # bootstrap executable was built @@ -117,7 +140,7 @@ def main(argv: Sequence[str]) -> int: ]) self_build( paths.CUR_BUILT_DDS, - toolchain=opts.toolchain, + toolchain=opts.toolchain_json5, dds_flags=[f'--repo-dir={ci_repo_dir}', f'--catalog={cat_path}']) print('Bootstrap test PASSED!') diff --git a/tools/freebsd-gcc-9.dds b/tools/freebsd-gcc-9.dds new file mode 100644 index 00000000..d16f7d02 --- /dev/null +++ b/tools/freebsd-gcc-9.dds @@ -0,0 +1,9 @@ +Compiler-ID: GNU +C-Compiler: gcc9 +C++-Compiler: g++9 +# Range-v3 0.10.0 contains an accidental conversion warning +Flags: -D SPDLOG_COMPILED_LIB -Werror=return-type -Wno-conversion +C++-Flags: -std=c++2a -fconcepts +Link-Flags: -static-libgcc -static-libstdc++ +Optimize: True +Compiler-Launcher: ccache diff --git a/tools/freebsd-gcc-9.jsonc b/tools/freebsd-gcc-9.jsonc new file mode 100644 index 00000000..986d80fc --- /dev/null +++ b/tools/freebsd-gcc-9.jsonc @@ -0,0 +1,22 @@ +{ + "$schema": "../res/toolchain-schema.json", + "compiler_id": "gnu", + "c_compiler": "gcc9", + "cxx_compiler": "g++9", + // "cxx_version": "c++17", + "flags": [ + "-DSPDLOG_COMPILED_LIB", // Required to use a compiled spdlog + "-Werror=return-type", + ], + "cxx_flags": [ + "-fconcepts", + "-std=c++2a", + ], + "link_flags": [ + "-static-libgcc", + "-static-libstdc++", + ], + // "debug": true, + "optimize": true, + "compiler_launcher": "ccache" +} \ No newline at end of file diff --git a/tools/gcc-9.dds b/tools/gcc-9.dds index c6317e34..26125188 100644 --- a/tools/gcc-9.dds +++ b/tools/gcc-9.dds @@ -1,11 +1,9 @@ Compiler-ID: GNU -C++-Version: C++17 C-Compiler: gcc-9 C++-Compiler: g++-9 # Range-v3 0.10.0 contains an accidental conversion warning -Flags: -D SPDLOG_COMPILED_LIB -Werror=return-type -Wno-conversion -C++-Flags: -fconcepts +Flags: -D SPDLOG_COMPILED_LIB -Werror=return-type -Wno-sign-compare -Wno-conversion +C++-Flags: -fconcepts -std=c++2a Link-Flags: -static-libgcc -static-libstdc++ -# Debug: True Optimize: True Compiler-Launcher: ccache \ No newline at end of file diff --git a/tools/gcc-9.jsonc b/tools/gcc-9.jsonc new file mode 100644 index 00000000..8915a97f --- /dev/null +++ b/tools/gcc-9.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "../res/toolchain-schema.json", + "compiler_id": "gnu", + "c_compiler": "gcc-9", + "cxx_compiler": "g++-9", + // "cxx_version": "c++17", + "flags": [ + "-DSPDLOG_COMPILED_LIB", // Required to use a compiled spdlog + "-Werror=return-type", + ], + "cxx_flags": [ + "-fconcepts", + "-std=c++2a", + ], + // "debug": true, + "optimize": true, + "compiler_launcher": "ccache" +} \ No newline at end of file diff --git a/tools/gen-catalog-json.py b/tools/gen-catalog-json.py index 76fbeffa..fdec1ec0 100644 --- a/tools/gen-catalog-json.py +++ b/tools/gen-catalog-json.py @@ -30,7 +30,7 @@ def to_dict(self) -> dict: ret: dict = { 'description': self.description, } - ret['depends'] = {} + ret['depends'] = self.depends if isinstance(self.remote, Git): ret['git'] = self.remote.to_dict() return ret @@ -150,6 +150,34 @@ def many_versions(name: str, 'A C++ implementation of the Pubgrub version solving algorithm', git_url='https://github.com/vector-of-bool/pubgrub.git', ), + many_versions( + 'vob-json5', + ('0.1.5', ), + description='A C++ implementation of a JSON5 parser', + git_url='https://github.com/vector-of-bool/json5.git', + ), + Package('vob-semester', [ + Version( + '0.1.0', + description='A C++ library to process recursive dynamic data', + remote=Git('https://github.com/vector-of-bool/semester.git', + '0.1.0'), + depends={ + 'neo-fun': '^0.1.0', + 'neo-concepts': '^0.2.1', + }), + ]), + Package('ctre', [ + Version( + '2.7.0', + description= + 'A compile-time PCRE (almost) compatible regular expression matcher', + remote=Git( + 'https://github.com/hanickadot/compile-time-regular-expressions.git', + 'v2.7', + auto_lib='hanickadot/ctre', + )) + ]), many_versions( 'spdlog', ( diff --git a/tools/msvc.jsonc b/tools/msvc.jsonc new file mode 100644 index 00000000..41eae41d --- /dev/null +++ b/tools/msvc.jsonc @@ -0,0 +1,14 @@ +{ + "$schema": "../res/toolchain-schema.json", + "compiler_id": "msvc", + "flags": [ + "/experimental:preprocessor", // Required for range-v3 + "/DSPDLOG_COMPILED_LIB", // Required to use spdlog as a compiled lib + "/std:c++latest", + ], + "link_flags": [ + "rpcrt4.lib", + ], + // "debug": true, + "optimize": true +} \ No newline at end of file diff --git a/tools/self_build.py b/tools/self_build.py index 5431041a..cbb5c28c 100755 --- a/tools/self_build.py +++ b/tools/self_build.py @@ -15,7 +15,7 @@ def self_build(exe: Path, *, toolchain: str, lmi_path: Path = None, - dds_flags: Iterable[str] = ()): + dds_flags: proc.CommandLine = ()): # Copy the exe to another location, as windows refuses to let a binary be # replaced while it is executing new_exe = ROOT / '_dds.bootstrap-test.exe'