From: TizenOpenSource Date: Fri, 22 Dec 2023 09:14:46 +0000 (+0900) Subject: Imported Upstream version 1.3.0 X-Git-Tag: upstream/1.3.0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=refs%2Ftags%2Fupstream%2F1.3.0;p=platform%2Fupstream%2Fmeson.git Imported Upstream version 1.3.0 --- diff --git a/.github/codecov.yml b/.github/codecov.yml index bfdc987..fa7b82a 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -6,3 +6,6 @@ coverage: patch: default: informational: true +comment: false +github_checks: + annotations: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c152647..455a9d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,6 +22,8 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: + # bypass cache: https://github.com/github/codeql-action/issues/1445 + tools: latest config-file: .github/codeql/codeql-config.yml languages: python # we have none diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 5b1cd95..e62c745 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -35,9 +35,8 @@ jobs: MESON_CI_JOBNAME: cygwin-${{ matrix.NAME }} steps: - # cache should be saved on failure, but the action doesn't support that - # https://github.com/actions/cache/issues/92 - - uses: actions/cache@v1 + - uses: actions/cache/restore@v3 + id: restore-cache with: # should use 'pip3 cache dir' to discover this path path: C:\cygwin\home\runneradmin\.cache\pip @@ -74,18 +73,19 @@ jobs: vala zlib-devel - - name: work around gcovr bug https://github.com/gcovr/gcovr/pull/576 - run: | - export PATH=/usr/bin:/usr/local/bin:$(cygpath ${SYSTEMROOT})/system32 - python3 -m pip --disable-pip-version-check install 'jinja2<3.1.0' - shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}' - - name: Run pip run: | export PATH=/usr/bin:/usr/local/bin:$(cygpath ${SYSTEMROOT})/system32 - python3 -m pip --disable-pip-version-check install gcovr jsonschema pefile pytest pytest-subtests pytest-xdist coverage codecov + # jsonschema is max capped because the new version depends on rust dependencies which are... hard to get on cygwin + python3 -m pip --disable-pip-version-check install gcovr 'jsonschema<4.18' pefile pytest pytest-subtests pytest-xdist coverage shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}' + - uses: actions/cache/save@v3 + with: + # should use 'pip3 cache dir' to discover this path + path: C:\cygwin\home\runneradmin\.cache\pip + key: cygwin-pip-${{ github.run_number }} + - name: Run tests run: | export PATH=/usr/bin:/usr/local/bin:$(cygpath ${SYSTEMROOT})/system32 @@ -103,5 +103,16 @@ jobs: # test log should be saved on failure if: ${{ !cancelled() }} + - name: Aggregate coverage reports + run: | + export PATH=/usr/bin:/usr/local/bin:$(cygpath ${SYSTEMROOT})/system32 + ./ci/combine_cov.sh + shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}' + - name: Upload coverage report - run: ./ci/upload_cov.sh "${{ matrix.NAME }}" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "${{ matrix.NAME }}" + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dea20d4..547a520 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -27,7 +27,7 @@ jobs: with: python-version: '3.x' - run: python -m pip install pylint - - run: pylint mesonbuild + - run: pylint --output-format colorized mesonbuild flake8: runs-on: ubuntu-latest @@ -37,7 +37,7 @@ jobs: with: python-version: '3.x' - run: python -m pip install flake8 - - run: flake8 mesonbuild/ + - run: flake8 --color always mesonbuild/ mypy: runs-on: ubuntu-latest @@ -47,4 +47,8 @@ jobs: with: python-version: '3.x' - run: python -m pip install mypy types-PyYAML - - run: python run_mypy.py + - run: python run_mypy.py --allver + env: + PYTHONUNBUFFERED: 1 + TERM: xterm-color + MYPY_FORCE_COLOR: 1 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 66e84be..6d6b43a 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -33,7 +33,7 @@ jobs: python-version: '3.x' - run: | python -m pip install --upgrade pip - python -m pip install pytest pytest-xdist pytest-subtests jsonschema coverage codecov + python -m pip install pytest pytest-xdist pytest-subtests jsonschema coverage - run: brew install pkg-config ninja llvm qt@5 - env: CPPFLAGS: "-I/usr/local/include" @@ -46,8 +46,17 @@ jobs: export PATH="$HOME/tools:/usr/local/opt/qt@5/bin:$PATH:$(brew --prefix llvm)/bin" export PKG_CONFIG_PATH="/usr/local/opt/qt@5/lib/pkgconfig:$PKG_CONFIG_PATH" ./tools/run_with_cov.py ./run_unittests.py + + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "appleclang [unit tests]" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "appleclang [unit tests]" + fail_ci_if_error: false + verbose: true project-tests-appleclang: @@ -81,7 +90,7 @@ jobs: - run: | python3 -m pip install --upgrade setuptools python3 -m pip install --upgrade pip - python3 -m pip install cython coverage codecov + python3 -m pip install cython coverage - env: CPPFLAGS: "-I/usr/local/include" LDFLAGS: "-L/usr/local/lib" @@ -94,8 +103,17 @@ jobs: export PKG_CONFIG_PATH="/usr/local/opt/qt@5/lib/pkgconfig:$PKG_CONFIG_PATH" export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" ./tools/run_with_cov.py ./run_project_tests.py --backend=ninja + + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "appleclang [project tests; unity=${{ matrix.unity }}]" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "appleclang [project tests; unity=${{ matrix.unity }}]" + fail_ci_if_error: false + verbose: true Qt4macos: runs-on: macos-latest diff --git a/.github/workflows/msys2.yml b/.github/workflows/msys2.yml index 4f05983..2bdcdd3 100644 --- a/.github/workflows/msys2.yml +++ b/.github/workflows/msys2.yml @@ -51,9 +51,6 @@ jobs: TOOLCHAIN: clang env: MESON_CI_JOBNAME: msys2-${{ matrix.NAME }} - # XXX: For some reason enabling jit debugging "fixes" random python crashes - # see https://github.com/msys2/MINGW-packages/issues/11864 - MSYS: "winjitdebug" defaults: run: @@ -83,11 +80,12 @@ jobs: mingw-w64-${{ matrix.MSYS2_ARCH }}-python-lxml mingw-w64-${{ matrix.MSYS2_ARCH }}-python-setuptools mingw-w64-${{ matrix.MSYS2_ARCH }}-python-pip + mingw-w64-${{ matrix.MSYS2_ARCH }}-python-jsonschema mingw-w64-${{ matrix.MSYS2_ARCH }}-${{ matrix.TOOLCHAIN }} - name: Install dependencies run: | - python3 -m pip --disable-pip-version-check install gcovr jsonschema pefile pytest pytest-subtests pytest-xdist coverage codecov + python3 -m pip --disable-pip-version-check install gcovr pefile pytest pytest-subtests pytest-xdist coverage - name: Install pypy3 on x86_64 run: | @@ -129,5 +127,13 @@ jobs: name: ${{ matrix.NAME }} path: meson-test-run.* + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "${{ matrix.NAME }}" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "${{ matrix.NAME }}" + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/nonative.yml b/.github/workflows/nonnative.yml similarity index 67% rename from .github/workflows/nonative.yml rename to .github/workflows/nonnative.yml index 32c2329..b606682 100644 --- a/.github/workflows/nonative.yml +++ b/.github/workflows/nonnative.yml @@ -1,7 +1,7 @@ name: Cross-only compilation environment concurrency: - group: nonative-${{ github.head_ref || github.ref }} + group: nonnative-${{ github.head_ref || github.ref }} cancel-in-progress: true on: @@ -13,13 +13,13 @@ on: paths: - "mesonbuild/**" - "test cases/**" - - ".github/workflows/nonative.yml" + - ".github/workflows/nonnative.yml" - "run*tests.py" pull_request: paths: - "mesonbuild/**" - "test cases/**" - - ".github/workflows/nonative.yml" + - ".github/workflows/nonnative.yml" - "run*tests.py" permissions: @@ -36,9 +36,18 @@ jobs: - run: | apt-get -y purge clang gcc gdc apt-get -y autoremove - python3 -m pip install coverage codecov + python3 -m pip install coverage - uses: actions/checkout@v3 - name: Run tests run: bash -c 'source /ci/env_vars.sh; cd $GITHUB_WORKSPACE; ./tools/run_with_cov.py ./run_tests.py $CI_ARGS --cross ubuntu-armhf.json --cross-only' + + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "Ubuntu nonnative" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "Ubuntu nonnative" + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/os_comp.yml b/.github/workflows/os_comp.yml index 21059c2..d12e54c 100644 --- a/.github/workflows/os_comp.yml +++ b/.github/workflows/os_comp.yml @@ -53,8 +53,32 @@ jobs: # They are defined in the `env` section in each image.json. CI_ARGS should be set # via the `args` array ub the image.json run: bash -c 'source /ci/env_vars.sh; cd $GITHUB_WORKSPACE; ./tools/run_with_cov.py ./run_tests.py $CI_ARGS' + + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "OS Comp [${{ matrix.cfg.name }}]" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "OS Comp [${{ matrix.cfg.name }}]" + fail_ci_if_error: false + verbose: true + + pypy: + name: 'Arch / PyPy' + runs-on: ubuntu-latest + container: mesonbuild/arch:latest + env: + MESON_CI_JOBNAME_UPDATE: linux-arch-gcc-pypy + + steps: + - uses: actions/checkout@v3 + - name: Run tests + run: | + source /ci/env_vars.sh + export MESON_CI_JOBNAME=$MESON_CI_JOBNAME_UPDATE + pypy3 run_tests.py ubuntu-rolling: name: 'Ubuntu Rolling' @@ -130,5 +154,13 @@ jobs: ./tools/run_with_cov.py ./run_tests.py $RUN_TESTS_ARGS -- $MESON_ARGS + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "Ubuntu [${{ matrix.cfg.CC }} ${{ matrix.cfg.RUN_TESTS_ARGS }} ${{ matrix.cfg.MESON_ARGS }}]" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "Ubuntu [${{ matrix.cfg.CC }} ${{ matrix.cfg.RUN_TESTS_ARGS }} ${{ matrix.cfg.MESON_ARGS }}]" + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/unusedargs_missingreturn.yml b/.github/workflows/unusedargs_missingreturn.yml index e7aef7a..d823c31 100644 --- a/.github/workflows/unusedargs_missingreturn.yml +++ b/.github/workflows/unusedargs_missingreturn.yml @@ -23,7 +23,7 @@ on: - "test cases/linuxlike/**" - "test cases/objc/**" - "test cases/objcpp/**" - - "test caes/windows/**" + - "test cases/windows/**" pull_request: paths: @@ -34,7 +34,7 @@ on: - "test cases/linuxlike/**" - "test cases/objc/**" - "test cases/objcpp/**" - - "test caes/windows/**" + - "test cases/windows/**" permissions: contents: read @@ -52,12 +52,21 @@ jobs: run: | sudo apt update -yq sudo apt install -yq --no-install-recommends g++ gfortran ninja-build gobjc gobjc++ - python -m pip install coverage codecov + python -m pip install coverage - run: ./tools/run_with_cov.py run_project_tests.py --only cmake common fortran platform-linux "objective c" "objective c++" env: MESON_CI_JOBNAME: linux-ubuntu-gcc-werror + + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + - name: Upload coverage report - run: ./ci/upload_cov.sh "UnusedMissingReturn" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "UnusedMissingReturn" + fail_ci_if_error: false + verbose: true windows: runs-on: windows-latest @@ -67,7 +76,7 @@ jobs: with: python-version: '3.x' - - run: pip install ninja pefile coverage codecov + - run: pip install ninja pefile coverage - run: python ./tools/run_with_cov.py run_project_tests.py --only platform-windows env: @@ -76,5 +85,14 @@ jobs: FC: gfortran MESON_CI_JOBNAME: msys2-gcc-werror + - name: Aggregate coverage reports + run: ./ci/combine_cov.sh + shell: C:\msys64\usr\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}' + - name: Upload coverage report - run: ./ci/upload_cov.sh "UnusedMissingReturn Windows" + uses: codecov/codecov-action@v3 + with: + files: .coverage/coverage.xml + name: "UnusedMissingReturn Windows" + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 3b1d517..2c76d87 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -10,9 +10,11 @@ on: branches: - master paths: + - .github/workflows/website.yml - docs/** pull_request: paths: + - .github/workflows/website.yml - docs/** workflow_dispatch: release: @@ -31,17 +33,33 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + + - uses: actions/cache/restore@v3 + id: restore-cache + with: + # should use 'pip3 cache dir' to discover this path + path: ~/.cache/pip + key: website-pip-${{ github.run_number }} + restore-keys: website-pip- + - name: Install package run: | sudo apt-get -y install python3-pip ninja-build libjson-glib-dev pip install hotdoc chevron strictyaml + + - uses: actions/cache/save@v3 + with: + # should use 'pip3 cache dir' to discover this path + path: ~/.cache/pip + key: website-pip-${{ github.run_number }} + - name: Setup SSH Keys and known_hosts env: SSH_AUTH_SOCK: /tmp/ssh_agent.sock run: | ssh-agent -a $SSH_AUTH_SOCK > /dev/null ssh-add - <<< "${{ secrets.WEBSITE_PRIV_KEY }}" - if: env.HAS_SSH_KEY == 'true' + if: github.ref == 'refs/heads/master' && env.HAS_SSH_KEY == 'true' - name: Build website run: | git config --global user.name "github-actions" @@ -56,7 +74,7 @@ jobs: run: | cd docs ninja -C _build upload - if: env.HAS_SSH_KEY == 'true' + if: github.ref == 'refs/heads/master' && env.HAS_SSH_KEY == 'true' - name: Release the current JSON docs uses: svenstaro/upload-release-action@v2 with: @@ -71,4 +89,3 @@ jobs: file: docs/_build/meson-reference.3 tag: ${{ github.ref }} if: ${{ github.event_name == 'release' }} - diff --git a/.mypy.ini b/.mypy.ini index 2ee1e59..70fdcd9 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -3,6 +3,7 @@ strict_optional = False show_error_context = False show_column_numbers = True ignore_missing_imports = True +implicit_reexport = False follow_imports = silent warn_redundant_casts = True diff --git a/README.md b/README.md index 868728d..f3a2657 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ build system. - [Python](https://python.org) (version 3.7 or newer) - [Ninja](https://ninja-build.org) (version 1.8.2 or newer) +Latest Meson version supporting previous Python versions: +- Python 3.6: **0.61.5** +- Python 3.5: **0.56.2** +- Python 3.4: **0.45.1** + #### Installing from source Meson is available on [PyPi](https://pypi.python.org/pypi/meson), so diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ba5b28e..76440de 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,7 @@ trigger: include: - 'master' # Release branches - - '0.*' + - '1.*' paths: include: - 'mesonbuild' @@ -94,6 +94,8 @@ jobs: - bash: ci/intel-scripts/cache_exclude_windows.sh displayName: exclude unused files from cache condition: and(ne(variables.CACHE_RESTORED, 'true'), eq(variables.ifort, 'true')) + - script: choco install -y nasm + displayName: install NASM - task: UsePythonVersion@0 inputs: versionSpec: '3.7' diff --git a/ci/ciimage/arch/install.sh b/ci/ciimage/arch/install.sh index 9628635..76eb8cd 100755 --- a/ci/ciimage/arch/install.sh +++ b/ci/ciimage/arch/install.sh @@ -7,18 +7,18 @@ source /ci/common.sh # Inspired by https://github.com/greyltc/docker-archlinux-aur/blob/master/add-aur.sh pkgs=( - python python-pip + python python-pip pypy3 ninja make git sudo fakeroot autoconf automake patch libelf gcc gcc-fortran gcc-objc vala rust bison flex cython go dlang-dmd - mono boost qt5-base gtkmm3 gtest gmock protobuf wxgtk2 gobject-introspection + mono boost qt5-base gtkmm3 gtest gmock protobuf gobject-introspection itstool gtk3 java-environment=8 gtk-doc llvm clang sdl2 graphviz doxygen vulkan-validation-layers openssh mercurial gtk-sharp-2 qt5-tools - libwmf valgrind cmake netcdf-fortran openmpi nasm gnustep-base gettext + libwmf cmake netcdf-fortran openmpi nasm gnustep-base gettext python-lxml hotdoc rust-bindgen qt6-base qt6-tools wayland wayland-protocols # cuda ) -aur_pkgs=(scalapack) +aur_pkgs=(scalapack wxwidgets-gtk2) cleanup_pkgs=(go) AUR_USER=docker @@ -33,6 +33,9 @@ sed -i "s,PKGEXT='.pkg.tar.zst',PKGEXT='.pkg.tar',g" /etc/makepkg.conf pacman -Syu $PACMAN_OPTS "${pkgs[@]}" install_python_packages +pypy3 -m ensurepip +pypy3 -m pip install "${base_python_pkgs[@]}" + # Setup the user useradd -m $AUR_USER echo "${AUR_USER}:" | chpasswd -e diff --git a/ci/ciimage/bionic/install.sh b/ci/ciimage/bionic/install.sh index 63f6512..23e0774 100755 --- a/ci/ciimage/bionic/install.sh +++ b/ci/ciimage/bionic/install.sh @@ -58,22 +58,6 @@ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 2 \ python3 -m pip install -U "${base_python_pkgs[@]}" "${python_pkgs[@]}" -pushd /opt -# Download and install PyPy3.8, and link it to /usr/bin/pypy3 -# At some point it would be more robust to download and parse -# https://downloads.python.org/pypy/versions.json -wget https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux64.tar.bz2 -pypy_sha256="08be25ec82fc5d23b78563eda144923517daba481a90af0ace7a047c9c9a3c34" -if [ $pypy_sha256 != $(sha256sum pypy3.8-v7.3.9-linux64.tar.bz2 | cut -f1 -d" ") ]; then - echo bad sha256 for PyPy - exit -1 -fi -tar -xf pypy3.8-v7.3.9-linux64.tar.bz2 -pypy3.8-v7.3.9-linux64/bin/pypy3 -m ensurepip -popd -ln -s /opt/pypy3.8-v7.3.9-linux64/bin/pypy3 /usr/bin/pypy3 - - # Install the ninja 0.10 wget https://github.com/ninja-build/ninja/releases/download/v1.10.0/ninja-linux.zip unzip ninja-linux.zip -d /ci diff --git a/ci/ciimage/build.py b/ci/ciimage/build.py index 1e1f238..b9d3181 100755 --- a/ci/ciimage/build.py +++ b/ci/ciimage/build.py @@ -74,6 +74,12 @@ class Builder(BuilderBase): # Also add /ci to PATH out_data += 'export PATH="/ci:$PATH"\n' + out_data += ''' + if [ -f "$HOME/.cargo/env" ]; then + source "$HOME/.cargo/env" + fi + ''' + out_file.write_text(out_data, encoding='utf-8') # make it executable @@ -137,13 +143,14 @@ class ImageTester(BuilderBase): shutil.copytree( self.meson_root, self.temp_dir / 'meson', + symlinks=True, ignore=shutil.ignore_patterns( '.git', '*_cache', '__pycache__', # 'work area', self.temp_dir.name, - ) + ), ) def do_test(self, tty: bool = False) -> None: @@ -175,7 +182,7 @@ class ImageTester(BuilderBase): else: test_cmd = [ self.docker, 'run', '--rm', '-t', 'meson_test_image', - '/bin/bash', '-c', 'source /ci/env_vars.sh; cd meson; ./run_tests.py $CI_ARGS' + '/bin/bash', '-xc', 'source /ci/env_vars.sh; cd meson; ./run_tests.py $CI_ARGS' ] if subprocess.run(test_cmd).returncode != 0 and not tty: diff --git a/ci/ciimage/common.sh b/ci/ciimage/common.sh index 68a4dae..67d5997 100644 --- a/ci/ciimage/common.sh +++ b/ci/ciimage/common.sh @@ -15,7 +15,6 @@ base_python_pkgs=( pytest-xdist pytest-subtests coverage - codecov jsonschema ) @@ -40,9 +39,11 @@ dub_fetch() { } install_minimal_python_packages() { + rm -f /usr/lib*/python3.*/EXTERNALLY-MANAGED python3 -m pip install "${base_python_pkgs[@]}" $* } install_python_packages() { + rm -f /usr/lib*/python3.*/EXTERNALLY-MANAGED python3 -m pip install "${base_python_pkgs[@]}" "${python_pkgs[@]}" $* } diff --git a/ci/ciimage/cuda/install.sh b/ci/ciimage/cuda/install.sh index 0d412e0..7c79d28 100755 --- a/ci/ciimage/cuda/install.sh +++ b/ci/ciimage/cuda/install.sh @@ -5,7 +5,7 @@ set -e source /ci/common.sh pkgs=( - python python-pip + python python-pip python-setuptools ninja gcc gcc-objc git cmake cuda zlib pkgconf ) diff --git a/ci/ciimage/fedora/install.sh b/ci/ciimage/fedora/install.sh index 2f1ae7d..dc9c20a 100755 --- a/ci/ciimage/fedora/install.sh +++ b/ci/ciimage/fedora/install.sh @@ -8,7 +8,7 @@ pkgs=( python python-pip python3-devel ninja-build make git autoconf automake patch elfutils gcc gcc-c++ gcc-fortran gcc-objc gcc-objc++ vala rust bison flex ldc libasan libasan-static - mono-core boost-devel gtkmm30 gtest-devel gmock-devel protobuf-devel wxGTK3-devel gobject-introspection + mono-core boost-devel gtkmm30 gtest-devel gmock-devel protobuf-devel wxGTK-devel gobject-introspection boost-python3-devel itstool gtk3-devel java-latest-openjdk-devel gtk-doc llvm-devel clang-devel SDL2-devel graphviz-devel zlib zlib-devel zlib-static #hdf5-openmpi-devel hdf5-devel netcdf-openmpi-devel netcdf-devel netcdf-fortran-openmpi-devel netcdf-fortran-devel scalapack-openmpi-devel diff --git a/ci/ciimage/opensuse/install.sh b/ci/ciimage/opensuse/install.sh index 41cb961..b009717 100755 --- a/ci/ciimage/opensuse/install.sh +++ b/ci/ciimage/opensuse/install.sh @@ -9,7 +9,7 @@ pkgs=( ninja make git autoconf automake patch libjpeg-devel elfutils gcc gcc-c++ gcc-fortran gcc-objc gcc-obj-c++ vala rust bison flex curl lcov mono-core gtkmm3-devel gtest gmock protobuf-devel wxGTK3-3_2-devel gobject-introspection-devel - itstool gtk3-devel java-15-openjdk-devel gtk-doc llvm-devel clang-devel libSDL2-devel graphviz-devel zlib-devel zlib-devel-static + itstool gtk3-devel java-17-openjdk-devel gtk-doc llvm-devel clang-devel libSDL2-devel graphviz-devel zlib-devel zlib-devel-static #hdf5-devel netcdf-devel libscalapack2-openmpi3-devel libscalapack2-gnu-openmpi3-hpc-devel openmpi3-devel doxygen vulkan-devel vulkan-validationlayers openssh mercurial gtk-sharp3-complete gtk-sharp2-complete libpcap-devel libgpgme-devel libqt5-qtbase-devel libqt5-qttools-devel libqt5-linguist libqt5-qtbase-private-headers-devel diff --git a/ci/ciimage/ubuntu-rolling/install.sh b/ci/ciimage/ubuntu-rolling/install.sh index 697ef06..35a0b0e 100755 --- a/ci/ciimage/ubuntu-rolling/install.sh +++ b/ci/ciimage/ubuntu-rolling/install.sh @@ -50,6 +50,14 @@ dub_fetch dubtestproject dub build dubtestproject:test1 --compiler=ldc2 dub build dubtestproject:test2 --compiler=ldc2 +# Remove debian version of Rust and install latest with rustup. +# This is needed to get the cross toolchain as well. +apt-get -y remove rustc || true +wget -O - https://sh.rustup.rs | sh -s -- -y --profile minimal --component clippy +source "$HOME/.cargo/env" +rustup target add x86_64-pc-windows-gnu +rustup target add arm-unknown-linux-gnueabihf + # cleanup apt-get -y clean apt-get -y autoclean diff --git a/ci/upload_cov.sh b/ci/combine_cov.sh similarity index 67% rename from ci/upload_cov.sh rename to ci/combine_cov.sh index 089641b..99a503b 100755 --- a/ci/upload_cov.sh +++ b/ci/combine_cov.sh @@ -8,6 +8,3 @@ coverage xml echo "Printing report" coverage report - -echo "Uploading to codecov..." -codecov -f .coverage/coverage.xml -n "$1" diff --git a/ci/run.ps1 b/ci/run.ps1 index 43e8008..badb4a7 100644 --- a/ci/run.ps1 +++ b/ci/run.ps1 @@ -3,7 +3,7 @@ if ($LastExitCode -ne 0) { exit 0 } -# remove Chocolately, MinGW, Strawberry Perl from path, so we don't find gcc/gfortran and try to use it +# remove Chocolatey, MinGW, Strawberry Perl from path, so we don't find gcc/gfortran and try to use it # remove PostgreSQL from path so we don't pickup a broken zlib from it $env:Path = ($env:Path.Split(';') | Where-Object { $_ -notmatch 'mingw|Strawberry|Chocolatey|PostgreSQL' }) -join ';' @@ -59,6 +59,9 @@ if ($env:arch -eq 'x64') { Expand-Archive $env:AGENT_WORKFOLDER\pypy38.zip -DestinationPath $env:AGENT_WORKFOLDER\pypy38 $ENV:Path = $ENV:Path + ";$ENV:AGENT_WORKFOLDER\pypy38\pypy3.8-v7.3.9-win64;$ENV:AGENT_WORKFOLDER\pypy38\pypy3.8-v7.3.9-win64\Scripts" pypy3 -m ensurepip + + DownloadFile -Source https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi -Destination $env:AGENT_WORKFOLDER\python27.msi + Start-Process msiexec.exe -Wait -ArgumentList "/I $env:AGENT_WORKFOLDER\python27.msi /quiet" } @@ -76,7 +79,7 @@ foreach ($prog in $progs) { echo "" -echo "Ninja / MSBuld version:" +echo "Ninja / MSBuild version:" if ($env:backend -eq 'ninja') { ninja --version } else { @@ -91,6 +94,9 @@ python --version echo "" python -m pip --disable-pip-version-check install --upgrade pefile pytest-xdist pytest-subtests jsonschema coverage +# Needed for running the Cython tests +python -m pip --disable-pip-version-check install cython + echo "" echo "=== Start running tests ===" # Starting from VS2019 Powershell(?) will fail the test run diff --git a/cross/armclang-linux.txt b/cross/armclang-linux.txt index 10f6fa4..36927b8 100644 --- a/cross/armclang-linux.txt +++ b/cross/armclang-linux.txt @@ -17,15 +17,12 @@ [binaries] # we could set exe_wrapper = qemu-arm-static but to test the case # when cross compiled binaries can't be run we don't do that -c = '/opt/arm/developmentstudio-2019.0/sw/ARMCompiler6.12/bin/armclang' +c = ['/opt/arm/developmentstudio-2019.0/sw/ARMCompiler6.12/bin/armclang', '--target=aarch64-arm-none-eabi'] #c = '/opt/arm/developmentstudio-2019.0/sw/ARMCompiler5.06u6/bin/armcc' #cpp = '/usr/bin/arm-linux-gnueabihf-g++' ar = '/opt/arm/developmentstudio-2019.0/sw/ARMCompiler6.12/bin/armar' #strip = '/usr/arm-linux-gnueabihf/bin/strip' -#pkgconfig = '/usr/bin/arm-linux-gnueabihf-pkg-config' - -[built-in options] -c_args = ['--target=aarch64-arm-none-eabi'] +#pkg-config = '/usr/bin/arm-linux-gnueabihf-pkg-config' [host_machine] system = 'baremetal' diff --git a/cross/armclang.txt b/cross/armclang.txt index 6146e0d..aad7efc 100644 --- a/cross/armclang.txt +++ b/cross/armclang.txt @@ -2,16 +2,16 @@ # to the environment(PATH) variable, so that Meson can find # the armclang, armlink and armar while building. [binaries] -c = 'armclang' -cpp = 'armclang' +c = ['armclang', '--target=arm-arm-none-eabi'] +cpp = ['armclang', '--target=arm-arm-none-eabi'] ar = 'armar' strip = 'armar' [built-in options] # The '--target', '-mcpu' options with the appropriate values should be mentioned # to cross compile c/c++ code with armclang. -c_args = ['--target=arm-arm-none-eabi', '-mcpu=cortex-m0plus'] -cpp_args = ['--target=arm-arm-none-eabi', '-mcpu=cortex-m0plus'] +c_args = ['-mcpu=cortex-m0plus'] +cpp_args = ['-mcpu=cortex-m0plus'] [host_machine] system = 'bare metal' # Update with your system name - bare metal/OS. diff --git a/cross/ccomp-armv7a.txt b/cross/ccomp-armv7a.txt index af66ed2..b90b96f 100644 --- a/cross/ccomp-armv7a.txt +++ b/cross/ccomp-armv7a.txt @@ -1,10 +1,10 @@ [binaries] -c = 'ccomp' +c = ['ccomp', '-target', 'armv7a-eabi'] ar = 'ccomp' strip = 'strip' [built-in options] -c_args = ['-target', 'armv7a-eabi', '-fall'] +c_args = ['-fall'] [host_machine] system = 'bare metal' # Update with your system name - bare metal/OS. diff --git a/cross/iphone.txt b/cross/iphone.txt index 6346248..b7149c1 100644 --- a/cross/iphone.txt +++ b/cross/iphone.txt @@ -1,22 +1,22 @@ # This is a cross compilation file from OSX Yosemite to iPhone # Apple keeps changing the location and names of files so -# these might not work for you. Use the googels and xcrun. +# these might not work for you. Use the googles and xcrun. [binaries] -c = 'clang' -cpp = 'clang++' -objc = 'clang' -objcpp = 'clang++' +c = ['clang', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] +cpp = ['clang++', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] +objc = ['clang', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] +objcpp = ['clang++', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] ar = 'ar' strip = 'strip' [built-in options] -c_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] -cpp_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] -c_link_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] -cpp_link_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] -objc_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] -objcpp_args = ['-arch', 'arm64', '-miphoneos-version-min=11.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'] +c_args = ['-miphoneos-version-min=11.0'] +cpp_args = ['-miphoneos-version-min=11.0'] +c_link_args = ['-miphoneos-version-min=11.0'] +cpp_link_args = ['-miphoneos-version-min=11.0'] +objc_args = ['-miphoneos-version-min=11.0'] +objcpp_args = ['-miphoneos-version-min=11.0'] [properties] root = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer' @@ -25,6 +25,8 @@ has_function_hfkerhisadf = false [host_machine] system = 'darwin' +subsystem = 'ios' +kernel = 'xnu' cpu_family = 'aarch64' cpu = 'aarch64' endian = 'little' diff --git a/cross/linux-mingw-w64-32bit.txt b/cross/linux-mingw-w64-32bit.txt index caf1da1..91ad9c5 100644 --- a/cross/linux-mingw-w64-32bit.txt +++ b/cross/linux-mingw-w64-32bit.txt @@ -4,7 +4,7 @@ cpp = '/usr/bin/i686-w64-mingw32-g++' objc = '/usr/bin/i686-w64-mingw32-gcc' ar = '/usr/bin/i686-w64-mingw32-ar' strip = '/usr/bin/i686-w64-mingw32-strip' -pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' +pkg-config = '/usr/bin/i686-w64-mingw32-pkg-config' windres = '/usr/bin/i686-w64-mingw32-windres' exe_wrapper = 'wine' ld = '/usr/bin/i686-w64-mingw32-ld' diff --git a/cross/linux-mingw-w64-64bit.txt b/cross/linux-mingw-w64-64bit.txt index f49fb35..08fa704 100644 --- a/cross/linux-mingw-w64-64bit.txt +++ b/cross/linux-mingw-w64-64bit.txt @@ -4,9 +4,9 @@ cpp = '/usr/bin/x86_64-w64-mingw32-g++' objc = '/usr/bin/x86_64-w64-mingw32-gcc' ar = '/usr/bin/x86_64-w64-mingw32-ar' strip = '/usr/bin/x86_64-w64-mingw32-strip' -pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' +pkg-config = '/usr/bin/x86_64-w64-mingw32-pkg-config' windres = '/usr/bin/x86_64-w64-mingw32-windres' -exe_wrapper = 'wine64' +exe_wrapper = 'wine' cmake = '/usr/bin/cmake' [properties] diff --git a/cross/metrowerks-arm.txt b/cross/metrowerks-arm.txt new file mode 100644 index 0000000..a98643e --- /dev/null +++ b/cross/metrowerks-arm.txt @@ -0,0 +1,23 @@ +# This file assumes that the path to your Metrowerks Embedded ARM +# toolchain is added to the environment(PATH) variable, so that +# Meson can find the binaries while building. + +[binaries] +c = 'mwccarm' +c_ld = 'mwldarm' +cpp = 'mwccarm' +cpp_ld = 'mwldarm' +ar = 'mwldarm' +as = 'mwasmarm' + +[built-in options] +c_args = ['-lang', 'c99', '-D_NITRO', '-nosyspath'] +c_link_args = '@DIRNAME@' / 'metrowerks.lcf' +cpp_args = ['-lang', 'c++', '-D_NITRO', '-nosyspath'] +cpp_link_args = '@DIRNAME@' / 'metrowerks.lcf' + +[host_machine] +system = 'bare metal' +cpu = 'arm' +cpu_family = 'arm' +endian = 'little' diff --git a/cross/metrowerks-eppc.txt b/cross/metrowerks-eppc.txt new file mode 100644 index 0000000..63a4543 --- /dev/null +++ b/cross/metrowerks-eppc.txt @@ -0,0 +1,23 @@ +# This file assumes that the path to your Metrowerks toolchain +# of choice is added to the environment(PATH) variable, so that +# Meson can find the binaries while building. + +[binaries] +c = 'mwcceppc' +c_ld = 'mwldeppc' +cpp = 'mwcceppc' +cpp_ld = 'mwldeppc' +ar = 'mwldeppc' +as = 'mwasmeppc' + +[built-in options] +c_args = ['-lang', 'c99', '-nosyspath'] +c_link_args = '@DIRNAME@' / 'metrowerks.lcf' +cpp_args = ['-lang', 'c++', '-nosyspath'] +cpp_link_args = '@DIRNAME@' / 'metrowerks.lcf' + +[host_machine] +system = 'bare metal' +cpu = 'ppc' +cpu_family = 'ppc' +endian = 'little' diff --git a/cross/metrowerks.lcf b/cross/metrowerks.lcf new file mode 100644 index 0000000..96d13af --- /dev/null +++ b/cross/metrowerks.lcf @@ -0,0 +1,18 @@ +# General-purpose linker script for Metrowerks toolchains. +# This script will link a blank application. Its only purpose +# is to allow the toolchains to run Meson tests. To link an +# actual application, you need to write your own fine-tuned lcf. + +MEMORY { + TEST (RWX) : ORIGIN=0, LENGTH=0 +} + +SECTIONS { + .TEST:{ + * (.text) + * (.data) + * (.rodata) + * (.bss) + __startup=.; + } > TEST +} \ No newline at end of file diff --git a/cross/none.txt b/cross/none.txt index 1fbe471..9eadf97 100644 --- a/cross/none.txt +++ b/cross/none.txt @@ -15,5 +15,5 @@ fc = ['false'] objc = ['false'] objcpp = ['false'] ar = ['false'] -pkgconfig = ['false'] +pkg-config = ['false'] cmake = ['false'] diff --git a/cross/tvos.txt b/cross/tvos.txt index a7d5dd1..6f6bfb5 100644 --- a/cross/tvos.txt +++ b/cross/tvos.txt @@ -1,18 +1,18 @@ # This is a cross compilation file from OSX Yosemite to Apple tvOS # Apple keeps changing the location and names of files so -# these might not work for you. Use the googels and xcrun. +# these might not work for you. Use the googles and xcrun. [binaries] -c = 'clang' -cpp = 'clang++' +c = ['clang', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] +cpp = ['clang++', '-arch', 'arm64', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] ar = 'ar' strip = 'strip' [built-in options] -c_args = ['-arch', 'arm64', '-mtvos-version-min=12.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] -cpp_args = ['-arch', 'arm64', '-mtvos-version-min=12.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] -c_link_args = ['-arch', 'arm64', '-mtvos-version-min=12.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] -cpp_link_args = ['-arch', 'arm64', '-mtvos-version-min=12.0', '-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk'] +c_args = ['-mtvos-version-min=12.0'] +cpp_args = ['-mtvos-version-min=12.0'] +c_link_args = ['-mtvos-version-min=12.0'] +cpp_link_args = ['-mtvos-version-min=12.0'] [properties] root = '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer' @@ -22,6 +22,8 @@ has_function_hfkerhisadf = false [host_machine] system = 'darwin' +subsystem = 'tvos' +kernel = 'xnu' cpu_family = 'arm' cpu = 'arm64' endian = 'little' diff --git a/cross/ubuntu-armhf.txt b/cross/ubuntu-armhf.txt index 69e0c86..6409e39 100644 --- a/cross/ubuntu-armhf.txt +++ b/cross/ubuntu-armhf.txt @@ -1,12 +1,12 @@ [binaries] # we could set exe_wrapper = qemu-arm-static but to test the case # when cross compiled binaries can't be run we don't do that -c = '/usr/bin/arm-linux-gnueabihf-gcc' -cpp = '/usr/bin/arm-linux-gnueabihf-g++' +c = ['/usr/bin/arm-linux-gnueabihf-gcc'] +cpp = ['/usr/bin/arm-linux-gnueabihf-g++'] rust = ['rustc', '--target', 'arm-unknown-linux-gnueabihf', '-C', 'linker=/usr/bin/arm-linux-gnueabihf-gcc-7'] ar = '/usr/arm-linux-gnueabihf/bin/ar' strip = '/usr/arm-linux-gnueabihf/bin/strip' -pkgconfig = '/usr/bin/arm-linux-gnueabihf-pkg-config' +pkg-config = '/usr/bin/arm-linux-gnueabihf-pkg-config' ld = '/usr/bin/arm-linux/gnueabihf-ld' [built-in options] diff --git a/cross/wasm.txt b/cross/wasm.txt index e03a10b..2a64319 100644 --- a/cross/wasm.txt +++ b/cross/wasm.txt @@ -1,13 +1,13 @@ [binaries] -c = '/home/jpakkane/src/emsdk/upstream/emscripten/emcc' -cpp = '/home/jpakkane/src/emsdk/upstream/emscripten/em++' -ar = '/home/jpakkane/src/emsdk/upstream/emscripten/emar' +c = 'emcc' +cpp = 'em++' +ar = 'emar' [built-in options] c_args = [] -c_link_args = ['-s','EXPORT_ALL=1'] +c_link_args = ['-sEXPORT_ALL=1'] cpp_args = [] -cpp_link_args = ['-s', 'EXPORT_ALL=1'] +cpp_link_args = ['-sEXPORT_ALL=1'] [host_machine] diff --git a/data/shell-completions/bash/meson b/data/shell-completions/bash/meson index 4357a3f..595b5b5 100644 --- a/data/shell-completions/bash/meson +++ b/data/shell-completions/bash/meson @@ -103,20 +103,28 @@ _meson_complete_filedir() { cur=$2 case $option in prefix |\ - libdir |\ - libexecdir |\ bindir |\ - sbindir |\ - includedir |\ datadir |\ - mandir |\ + includedir |\ infodir |\ + libdir |\ + licensedir |\ + libexecdir |\ localedir |\ - sysconfdir |\ localstatedir |\ - sharedstatedir) + mandir |\ + sbindir |\ + sharedstatedir |\ + sysconfdir |\ + python.platlibdir |\ + python.purelibdir |\ + pkg-config-path |\ + build.pkg-config-path |\ + cmake-prefix-path |\ + build.cmake-prefix-path) _filedir -d ;; + cross-file) _filedir COMPREPLY+=($(_filedir_in "$XDG_DATA_DIRS"/meson/cross)) @@ -125,14 +133,91 @@ _meson_complete_filedir() { COMPREPLY+=($(_filedir_in "$XDG_DATA_HOME"/meson/cross)) COMPREPLY+=($(_filedir_in ~/.local/share/meson/cross)) ;; + + native-file) + _filedir + COMPREPLY+=($(_filedir_in "$XDG_DATA_DIRS"/meson/native)) + COMPREPLY+=($(_filedir_in /usr/local/share/meson/native)) + COMPREPLY+=($(_filedir_in /usr/share/meson/native)) + COMPREPLY+=($(_filedir_in "$XDG_DATA_HOME"/meson/native)) + COMPREPLY+=($(_filedir_in ~/.local/share/meson/native)) + ;; + *) return 1;; esac return 0 } -_meson-setup() { +_meson_compgen_options() { + local -r cur=$1 + if [[ ${cur:0:2} == -- ]]; then + COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) + elif [[ ${cur:0:1} == - ]]; then + if [[ ${#cur} == 1 ]]; then + # Only add longopts if cur not "-something" + COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "")) + fi + + COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) + else + return 1 + fi + + return 0 +} + +_meson_common_setup_configure_longopts=( + help + prefix + bindir + datadir + includedir + infodir + libdir + licensedir + libexecdir + localedir + localstatedir + mandir + sbindir + sharedstatedir + sysconfdir + auto-features + backend + genvslite + buildtype + debug + default-library + errorlogs + install-umask + layout + optimization + prefer-static + stdsplit + strip + unity + unity-size + warnlevel + werror + wrap-mode + force-fallback-for + vsenv + pkgconfig.relocatable + python.bytecompile + python.install-env + python.platlibdir + python.purelibdir + python.allow-limited-api + pkg-config-path + build.pkg-config-path + cmake-prefix-path + build.cmake-prefix-path + clearcache +) + +_meson-setup() { shortopts=( h D @@ -140,44 +225,26 @@ _meson-setup() { ) longopts=( - help - prefix - libdir - libexecdir - bindir - sbindir - includedir - datadir - mandir - infodir - localedir - sysconfdir - localstatedir - sharedstatedir - backend - buildtype - strip - unity - werror - layout - default-library - warnlevel - stdsplit - errorlogs + ${_meson_common_setup_configure_longopts[@]} + native-file cross-file version - wrap-mode + fatal-meson-warnings + reconfigure + wipe ) local cur prev - if _get_comp_words_by_ref cur prev &>/dev/null && - [ "${prev:0:2}" = '--' ] && _meson_complete_option "${prev:2}" "$cur"; then - return - elif _get_comp_words_by_ref cur prev &>/dev/null && - [ "${prev:0:1}" = '-' ] && [ "${prev:1:2}" != '-' ] && _meson_complete_option "${prev:1}"; then - return - elif _get_comp_words_by_ref -n '=' cur prev &>/dev/null; then - if [ $prev == -D ]; then + if _get_comp_words_by_ref cur prev &>/dev/null; then + if [[ ${prev:0:2} == -- ]] && _meson_complete_option "${prev:2}" "$cur"; then + return + elif [[ ${prev:0:1} == - ]] && [[ ${prev:1:2} != - ]] && _meson_complete_option "${prev:1}"; then + return + fi + fi + + if _get_comp_words_by_ref -n '=' cur prev &>/dev/null; then + if [[ $prev == -D ]]; then _meson_complete_option "$cur" return fi @@ -185,39 +252,33 @@ _meson-setup() { cur="${COMP_WORDS[COMP_CWORD]}" fi - if [[ "$cur" == "--"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - elif [[ "$cur" == "-"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) - else + if ! _meson_compgen_options "$cur"; then _filedir -d - if [ -z "$cur" ]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) + if [[ -z $cur ]]; then + COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) + COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) fi - if [ $COMP_CWORD -eq 1 ]; then + if [[ $COMP_CWORD == 1 ]]; then COMPREPLY+=($(compgen -W "${meson_subcommands[*]}" -- "$cur")) fi fi } _meson-configure() { - shortopts=( h D ) longopts=( - help - clearcache + ${_meson_common_setup_configure_longopts[@]} + no-pager ) local cur prev if _get_comp_words_by_ref -n '=' cur prev &>/dev/null; then - if [ $prev == -D ]; then + if [[ $prev == -D ]]; then _meson_complete_option "$cur" return fi @@ -225,25 +286,20 @@ _meson-configure() { cur="${COMP_WORDS[COMP_CWORD]}" fi - if [[ "$cur" == "--"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - elif [[ "$cur" == "-"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) - else + if ! _meson_compgen_options "$cur"; then for dir in "${COMP_WORDS[@]}"; do - if [ -d "$dir" ]; then + if [[ -d "$dir" ]]; then break fi dir=. done - if [ ! -d "$dir/meson-private" ]; then + if [[ ! -d "$dir/meson-private" ]]; then _filedir -d fi - if [ -z "$cur" ]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) + if [[ -z $cur ]]; then + COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) + COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) fi fi } @@ -259,17 +315,27 @@ _meson-install() { _meson-introspect() { shortopts=( h + a + i + f ) longopts=( - targets - installed - buildsystem-files - buildoptions - tests + ast benchmarks + buildoptions + buildsystem-files dependencies + scan-dependencies + installed + install-plan projectinfo + targets + tests + backend + all + indent + force-object-output ) local cur prev @@ -277,12 +343,7 @@ _meson-introspect() { cur="${COMP_WORDS[COMP_CWORD]}" fi - if [[ "$cur" == "--"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - elif [[ "$cur" == "-"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) - else + if ! _meson_compgen_options "$cur"; then for dir in "${COMP_WORDS[@]}"; do if [ -d "$dir" ]; then break @@ -306,7 +367,6 @@ _meson-init() { C n e - e d l b @@ -315,31 +375,28 @@ _meson-init() { longopts=( help - name - executable - deps - language - builddir - force - type - version + name + executable + deps + language + build + builddir + force + type + version ) - if [[ "$cur" == "--"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - elif [[ "$cur" == "-"* && ${#cur} -gt 1 ]]; then - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) - else - if [ -z "$cur" ]; then + if ! _meson_compgen_options "$cur"; then + if [[ -z $cur ]]; then COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) fi fi - } _meson-test() { shortopts=( + h q v t @@ -347,21 +404,24 @@ _meson-test() { ) longopts=( - quiet - verbose - timeout-multiplier + help + maxfail repeat no-rebuild gdb + gdb-path list - wrapper --wrap - no-suite + wrapper suite + no-suite no-stdsplit print-errorlogs benchmark logbase num-processes + verbose + quiet + timeout-multiplier setup test-args ) @@ -369,18 +429,21 @@ _meson-test() { local cur prev if _get_comp_words_by_ref -n ':' cur prev &>/dev/null; then case $prev in - --repeat) + --maxfail | --repeat) # number, can't be completed return ;; + --wrapper) _command_offset $COMP_CWORD return ;; - -C) + + --gdb-path | -C) _filedir -d return ;; + --suite | --no-suite) for i in "${!COMP_WORDS[@]}"; do opt="${COMP_WORDS[i]}" @@ -392,31 +455,40 @@ _meson-test() { esac dir=. done - suites=($(python3 -c 'import sys, json; + + suites=$(meson introspect "$dir" --tests | python3 -c 'import sys, json; for test in json.load(sys.stdin): for suite in test["suite"]: print(suite) - ' <<< "$(meson introspect "$dir" --tests)")) -# TODO - COMPREPLY+=($(compgen -W "${suites[*]}" -- "$cur")) +' 2> /dev/null) +# TODO - what? + + if [[ $? == 0 ]]; then + COMPREPLY+=($(compgen -W "${suites[*]}" -- "$cur")) + fi return ;; + --logbase) # free string, can't be completed return ;; + --num-processes) # number, can't be completed return ;; + -t | --timeout-multiplier) # number, can't be completed return ;; + --setup) # TODO return ;; + --test-args) return ;; @@ -425,17 +497,14 @@ for test in json.load(sys.stdin): cur="${COMP_WORDS[COMP_CWORD]}" fi - if [[ "$cur" == "--"* ]]; then - COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) - elif [[ "$cur" == "-"* && ${#cur} -gt 1 ]]; then - COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}' -- "${cur:1}")) - else + if ! _meson_compgen_options "$cur"; then for dir in "${COMP_WORDS[@]}"; do if [ -d "$dir" ]; then break fi dir=. done + if [ ! -d "$dir/meson-private" ]; then _filedir -d fi @@ -450,11 +519,15 @@ for test in json.load(sys.stdin): esac dir=. done - tests=($(python3 -c 'import sys, json; + + tests=$(meson introspect "$dir" --tests | python3 -c 'import sys, json; for test in json.load(sys.stdin): print(test["name"]) -' <<< "$(meson introspect "$dir" --tests)")) - COMPREPLY+=($(compgen -W "${tests[*]}" -- "$cur")) +' 2> /dev/null) + + if [[ $? == 0 ]]; then + COMPREPLY+=($(compgen -W "${tests[*]}" -- "$cur")) + fi if [ -z "$cur" ]; then COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}' -- "${cur:2}")) @@ -472,7 +545,27 @@ _meson-subprojects() { } _meson-help() { - : # Nothing to do + longopts=( + setup + configure + dist + install + introspect + init + test + wrap + subprojects + rewrite + compile + devenv + env2mfile + ) + + local cur prev + + if _get_comp_words_by_ref cur prev &>/dev/null; then + COMPREPLY+=($(compgen -W '${longopts[*]}' -- "${cur}")) + fi } _meson-rewrite() { @@ -480,7 +573,48 @@ _meson-rewrite() { } _meson-compile() { - : TODO + shortopts=( + h + C + j + l + v + ) + + longopts=( + help + clean + jobs + load-average + verbose + ninja-args + vs-args + xcode-args + ) + + local cur prev + if _get_comp_words_by_ref cur prev &>/dev/null; then + if [[ ${prev:0:2} == -- ]] && _meson_complete_option "${prev:2}" "$cur"; then + return + elif [[ ${prev:0:1} == - ]] && [[ ${prev:1:2} != - ]] && _meson_complete_option "${prev:1}"; then + return + fi + else + cur="${COMP_WORDS[COMP_CWORD]}" + fi + + if ! _meson_compgen_options "$cur"; then + _filedir -d + + if [[ -z $cur ]]; then + COMPREPLY+=($(compgen -P '--' -W '${longopts[*]}')) + COMPREPLY+=($(compgen -P '-' -W '${shortopts[*]}')) + fi + + if [[ $COMP_CWORD == 1 ]]; then + COMPREPLY+=($(compgen -W "${meson_subcommands[*]}" -- "$cur")) + fi + fi } _meson-devenv() { diff --git a/data/shell-completions/zsh/_meson b/data/shell-completions/zsh/_meson index 18b7b62..bd71a31 100644 --- a/data/shell-completions/zsh/_meson +++ b/data/shell-completions/zsh/_meson @@ -74,7 +74,7 @@ local -a meson_commands=( 'configure:configure a project' 'dist:generate release archive' 'init:create a new project' -'install:install one more more targets' +'install:install one or more targets' 'introspect:query project properties' 'setup:set up a build directory' 'test:run tests' @@ -180,7 +180,7 @@ local -a meson_commands=( '--repeat[number of times to run the tests]:number of times to repeat: ' '--no-rebuild[do not rebuild before running tests]' '--gdb[run tests under gdb]' - '--gdb-path=[program to run for gdb (can be wrapper or compaitble program)]:program:_path_commands' + '--gdb-path=[program to run for gdb (can be wrapper or compatible program)]:program:_path_commands' '--list[list available tests]' '(--wrapper --wrap)'{'--wrapper=','--wrap='}'[wrapper to run tests with]:wrapper program:_path_commands' "$__meson_cd" @@ -391,7 +391,7 @@ _arguments \ "$__meson_cd" '--clean[Clean the build directory]' '(-j --jobs)'{'-j','--jobs'}'=[the number fo work jobs to run (if supported)]:_guard "[0-9]#" "number of jobs"' - '(-l --load-averate)'{'-l','--load-average'}'=[the system load average to try to maintain (if supported)]:_guard "[0-9]#" "load average"' + '(-l --load-average)'{'-l','--load-average'}'=[the system load average to try to maintain (if supported)]:_guard "[0-9]#" "load average"' '(-v --verbose)'{'-v','--verbose'}'[Show more output]' '--ninja-args=[Arguments to pass to ninja (only when using ninja)]' '--vs-args=[Arguments to pass to vs (only when using msbuild)]' diff --git a/data/syntax-highlighting/vim/ftdetect/meson.vim b/data/syntax-highlighting/vim/ftdetect/meson.vim index 3233c58..28e3a29 100644 --- a/data/syntax-highlighting/vim/ftdetect/meson.vim +++ b/data/syntax-highlighting/vim/ftdetect/meson.vim @@ -1,3 +1,4 @@ au BufNewFile,BufRead meson.build set filetype=meson +au BufNewFile,BufRead meson.options set filetype=meson au BufNewFile,BufRead meson_options.txt set filetype=meson au BufNewFile,BufRead *.wrap set filetype=dosini diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 3b858bd..a1679e0 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -3,7 +3,7 @@ " License: VIM License " Maintainer: Nirbheek Chauhan " Liam Beguin -" Last Change: 2021 Aug 16 +" Last Change: 2023 Aug 27 " Credits: Zvezdan Petkovic " Neil Schemenauer " Dmitry Vasiliev @@ -64,6 +64,11 @@ syn keyword mesonBoolean false true " Built-in functions syn keyword mesonBuiltin + \ build_machine + \ host_machine + \ meson + \ option + \ target_machine \ add_global_arguments \ add_global_link_arguments \ add_languages @@ -75,11 +80,11 @@ syn keyword mesonBuiltin \ assert \ benchmark \ both_libraries - \ build_machine \ build_target \ configuration_data \ configure_file \ custom_target + \ debug \ declare_dependency \ dependency \ disabler @@ -87,30 +92,26 @@ syn keyword mesonBuiltin \ error \ executable \ files - \ find_library \ find_program \ generator \ get_option \ get_variable - \ gettext - \ host_machine \ import \ include_directories \ install_data + \ install_emptydir \ install_headers \ install_man \ install_subdir \ install_symlink - \ install_emptydir \ is_disabler \ is_variable \ jar \ join_paths \ library - \ meson \ message - \ option \ project + \ range \ run_command \ run_target \ set_variable @@ -122,13 +123,10 @@ syn keyword mesonBuiltin \ subdir_done \ subproject \ summary - \ target_machine \ test \ unset_variable \ vcs_tag \ warning - \ range - \ debug if exists("meson_space_error_highlight") " trailing whitespace @@ -150,7 +148,7 @@ hi def link mesonEscape Special hi def link mesonNumber Number hi def link mesonBuiltin Function hi def link mesonBoolean Boolean -if exists("meson_space_error_higlight") +if exists("meson_space_error_highlight") hi def link mesonSpaceError Error endif diff --git a/data/test.schema.json b/data/test.schema.json index a809388..6f6a193 100644 --- a/data/test.schema.json +++ b/data/test.schema.json @@ -26,11 +26,15 @@ "exe", "shared_lib", "python_lib", + "python_limited_lib", + "python_bytecode", "pdb", "implib", "py_implib", + "py_limited_implib", "implibempty", - "expr" + "expr", + "link" ] }, "platform": { diff --git a/docs/README.md b/docs/README.md index f6645d3..169e9cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,10 @@ Minimum required version of hotdoc is *0.8.9*. Instructions on how to install hotdoc are [here](https://hotdoc.github.io/installing.html). +Our custom hotdoc extensions require: +- [chevron](https://pypi.org/project/chevron) +- [strictyaml](https://pypi.org/project/strictyaml) + ## Building the documentation From the Meson repository root dir: diff --git a/docs/markdown/Adding-new-projects-to-wrapdb.md b/docs/markdown/Adding-new-projects-to-wrapdb.md index 03998cd..7c28b49 100644 --- a/docs/markdown/Adding-new-projects-to-wrapdb.md +++ b/docs/markdown/Adding-new-projects-to-wrapdb.md @@ -106,7 +106,7 @@ Remember that all files go in the directory `subprojects/packagefiles/`. ``` -${EDITOR} meson.build meson_options.txt +${EDITOR} meson.build meson.options ``` In order to apply the locally added build files to the upstream @@ -170,6 +170,6 @@ The first command is to ensure the wrap is correctly fetched from the latest packagefiles. The second command configures meson and selects a set of subprojects to enable. -The Github project contains automatic CI on pushing to run the project +The GitHub project contains automatic CI on pushing to run the project and check the metadata for obvious mistakes. This can be checked from your fork before submitting a PR. diff --git a/docs/markdown/Build-options.md b/docs/markdown/Build-options.md index e8785f9..93e3326 100644 --- a/docs/markdown/Build-options.md +++ b/docs/markdown/Build-options.md @@ -7,8 +7,9 @@ short-description: Build options to configure project properties Most non-trivial builds require user-settable options. As an example a program may have two different data backends that are selectable at build time. Meson provides for this by having a option definition -file. Its name is `meson_options.txt` and it is placed at the root of -your source tree. +file. Its name is `meson.options` and it is placed at the root of +your source tree. For versions of meson before 1.1, this file was called +`meson_options.txt`. Here is a simple option file. @@ -182,7 +183,7 @@ issue the following command: prefix = get_option('prefix') ``` -It should be noted that you can not set option values in your Meson +It should be noted that you cannot set option values in your Meson scripts. They have to be set externally with the `meson configure` command line tool. Running `meson configure` without arguments in a build dir shows you all options you can set. diff --git a/docs/markdown/Build-targets.md b/docs/markdown/Build-targets.md index 83f959f..780595c 100644 --- a/docs/markdown/Build-targets.md +++ b/docs/markdown/Build-targets.md @@ -9,7 +9,7 @@ Meson provides four kinds of build targets: executables, libraries the build configuration time), static libraries, and shared libraries. They are created with the commands `executable`, `library`, `static_library` and `shared_library`, respectively. All objects created -in this way are **immutable**. That is, you can not change any aspect of +in this way are **immutable**. That is, you cannot change any aspect of them after they have been constructed. This ensures that all information pertaining to a given build target is specified in one well defined place. diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index 4c43f09..f3cbcea 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -41,6 +41,7 @@ not be relied on, since they can be absolute paths in the following cases: | includedir | include | Header file directory | | infodir | share/info | Info page directory | | libdir | see below | Library directory | +| licensedir | see below | Licenses directory (since 1.1.0)| | libexecdir | libexec | Library executable directory | | localedir | share/locale | Locale data directory | | localstatedir | var | Localstate data directory | @@ -61,6 +62,10 @@ different distributions have different defaults. Using a [cross file](Cross-compilation.md#defining-the-environment), particularly the paths section may be necessary. +`licensedir` is empty by default. If set, it defines the default location +to install a dependency manifest and project licenses. For more details, +see [[meson.install_dependency_manifest]]. + ### Core options Options that are labeled "per machine" in the table are set per @@ -69,11 +74,12 @@ machine](#specifying-options-per-machine) section for details. | Option | Default value | Description | Is per machine | Is per subproject | | -------------------------------------- | ------------- | ----------- | -------------- | ----------------- | -| auto_features {enabled, disabled, auto} | auto | Override value of all 'auto' features | no | no | -| backend {ninja, vs,
vs2010, vs2012, vs2013, vs2015, vs2017, vs2019, vs2022, xcode} | ninja | Backend to use | no | no | -| buildtype {plain, debug,
debugoptimized, release, minsize, custom} | debug | Build type to use | no | no | +| auto_features {enabled, disabled, auto} | auto | Override value of all 'auto' features | no | no | +| backend {ninja, vs,
vs2010, vs2012, vs2013, vs2015, vs2017, vs2019, vs2022, xcode, none} | ninja | Backend to use | no | no | +| genvslite {vs2022} | vs2022 | Setup multi-builtype ninja build directories and Visual Studio solution | no | no | +| buildtype {plain, debug,
debugoptimized, release, minsize, custom} | debug | Build type to use | no | no | | debug | true | Enable debug symbols and other information | no | no | -| default_library {shared, static, both} | shared | Default library type | no | yes | +| default_library {shared, static, both} | shared | Default library type | no | yes | | errorlogs | true | Whether to print the logs from failing tests. | no | no | | install_umask {preserve, 0000-0777} | 022 | Default umask to apply on permissions of installed files | no | no | | layout {mirror,flat} | mirror | Build directory layout | no | no | @@ -87,8 +93,41 @@ machine](#specifying-options-per-machine) section for details. | unity_size {>=2} | 4 | Unity file block size | no | no | | warning_level {0, 1, 2, 3, everything} | 1 | Set the warning level. From 0 = none to everything = highest | no | yes | | werror | false | Treat warnings as errors | no | yes | -| wrap_mode {default, nofallback,
nodownload, forcefallback, nopromote} | default | Wrap mode to use | no | no | +| wrap_mode {default, nofallback,
nodownload, forcefallback, nopromote} | default | Wrap mode to use | no | no | | force_fallback_for | [] | Force fallback for those dependencies | no | no | +| vsenv | false | Activate Visual Studio environment | no | no | + +#### Details for `backend` + +Several build file formats are supported as command runners to build the +configured project. Meson prefers ninja by default, but platform-specific +backends are also available for better IDE integration with native tooling: +Visual Studio for Windows, and xcode for macOS. It is also possible to +configure with no backend at all, which is an error if you have targets to +build, but for projects that need configuration + testing + installation allows +for a lighter automated build pipeline. + +#### Details for `genvslite` + +Setup multiple buildtype-suffixed, ninja-backend build directories (e.g. +[builddir]_[debug/release/etc.]) and generate [builddir]_vs containing a Visual +Studio solution with multiple configurations that invoke a meson compile of the +setup build directories, as appropriate for the current configuration (builtype). + +This has the effect of a simple setup macro of multiple 'meson setup ...' +invocations with a set of different buildtype values. E.g. +`meson setup ... --genvslite vs2022 somebuilddir` does the following - +``` +meson setup ... --backend ninja --buildtype debug somebuilddir_debug +meson setup ... --backend ninja --buildtype debugoptimized somebuilddir_debugoptimized +meson setup ... --backend ninja --buildtype release somebuilddir_release +``` +and additionally creates another 'somebuilddir_vs' directory that contains +a generated multi-configuration visual studio solution and project(s) that are +set to build/compile with the somebuilddir_[...] that's appropriate for the +solution's selected buildtype configuration. + +#### Details for `buildtype` For setting optimization levels and toggling debug, you can either set the `buildtype` option, or you can @@ -108,6 +147,36 @@ the two-way mapping: All other combinations of `debug` and `optimization` set `buildtype` to `'custom'`. +#### Details for `warning_level` + +Exact flags per warning level is compiler specific, but there is an approximative +table for most common compilers. + +| Warning level | GCC/Clang | MSVC | +| ------------- | --- | ---- | +| 0 | | | +| 1 | -Wall | /W2 | +| 2 | -Wall -Wextra | /W3 | +| 3 | -Wall -Wextra -Wpedantic | /W4 | +| everything | -Weverything | /Wall | + +Clang's `-Weverything` is emulated on GCC by passing all known warning flags. + +#### Details for `vsenv` + +The `--vsenv` argument is supported since `0.60.0`, `-Dvsenv=true` syntax is supported +since `1.1.0`. + +Since `0.59.0`, meson automatically activates a Visual Studio environment on Windows +for all its subcommands, but only if no other compilers (e.g. `gcc` or `clang`) +are found, and silently continues if Visual Studio activation fails. + +Setting the `vsenv` option to `true` forces Visual Studio activation even when other +compilers are found. It also make Meson abort with an error message when activation +fails. + +`vsenv` is `true` by default when using the `vs` backend. + ## Base options These are set in the same way as universal options, either by @@ -193,7 +262,7 @@ or compiler being used: | c_thread_count | 4 | integer value ≥ 0 | Number of threads to use with emcc when using threads | | cpp_args | | free-form comma-separated list | C++ compile arguments to use | | cpp_link_args | | free-form comma-separated list | C++ link arguments to use | -| cpp_std | none | none, c++98, c++03, c++11, c++14, c++17, c++20
c++2a, c++1z, gnu++03, gnu++11, gnu++14, gnu++17, gnu++1z,
gnu++2a, gnu++20, vc++14, vc++17, vc++latest | C++ language standard to use | +| cpp_std | none | none, c++98, c++03, c++11, c++14, c++17, c++20
c++2a, c++1z, gnu++03, gnu++11, gnu++14, gnu++17, gnu++1z,
gnu++2a, gnu++20, vc++14, vc++17, vc++20, vc++latest | C++ language standard to use | | cpp_debugstl | false | true, false | C++ STL debug mode | | cpp_eh | default | none, default, a, s, sc | C++ exception handling type | | cpp_rtti | true | true, false | Whether to enable RTTI (runtime type identification) | @@ -227,6 +296,21 @@ is inherited from the main project. This is useful, for example, when the main project requires C++11, but a subproject requires C++14. The `cpp_std` value from the subproject's `default_options` is now respected. +Since *1.3.0* `c_std` and `cpp_std` options now accept a list of values. +Projects that prefer GNU C, but can fallback to ISO C, can now set, for +example, `default_options: 'c_std=gnu11,c11'`, and it will use `gnu11` when +available, but fallback to c11 otherwise. It is an error only if none of the +values are supported by the current compiler. +Likewise, a project that can take benefit of `c++17` but can still build with +`c++11` can set `default_options: 'cpp_std=c++17,c++11'`. +This allows us to deprecate `gnuXX` values from the MSVC compiler. That means +that `default_options: 'c_std=gnu11'` will now print a warning with MSVC +but fallback to `c11`. No warning is printed if at least one +of the values is valid, i.e. `default_options: 'c_std=gnu11,c11'`. +In the future that deprecation warning will become an hard error because +`c_std=gnu11` should mean GNU is required, for projects that cannot be +built with MSVC for example. + ## Specifying options per machine Since *0.51.0*, some options are specified per machine rather than @@ -301,11 +385,13 @@ install prefix. For example: if the install prefix is `/usr` and the ### Python module -| Option | Default value | Possible values | Description | -| ------ | ------------- | ----------------- | ----------- | -| install_env | prefix | {auto,prefix,system,venv} | Which python environment to install to (Since 0.62.0) | -| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | -| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | +| Option | Default value | Possible values | Description | +| ------ | ------------- | ----------------- | ----------- | +| bytecompile | 0 | integer from -1 to 2 | What bytecode optimization level to use (Since 1.2.0) | +| install_env | prefix | {auto,prefix,system,venv} | Which python environment to install to (Since 0.62.0) | +| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | +| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | +| allow_limited_api | true | true, false | Disables project-wide use of the Python Limited API (Since 1.3.0) | *Since 0.60.0* The `python.platlibdir` and `python.purelibdir` options are used by the python module methods `python.install_sources()` and @@ -325,3 +411,17 @@ installation is a virtualenv, and use `venv` or `system` as appropriate (but never `prefix`). This option is mutually exclusive with the `platlibdir`/`purelibdir`. For backwards compatibility purposes, the default `install_env` is `prefix`. + +*Since 1.2.0* The `python.bytecompile` option can be used to enable compiling +python bytecode. Bytecode has 3 optimization levels: + +- 0, bytecode without optimizations +- 1, bytecode with some optimizations +- 2, bytecode with some more optimizations + +To this, Meson adds level `-1`, which is to not attempt to compile bytecode at +all. + +*Since 1.3.0* The `python.allow_limited_api` option affects whether the +`limited_api` keyword argument of the `extension_module` method is respected. +If set to `false`, the effect of the `limited_api` argument is disabled. diff --git a/docs/markdown/CMake-module.md b/docs/markdown/CMake-module.md index ceaee0b..f8275c9 100644 --- a/docs/markdown/CMake-module.md +++ b/docs/markdown/CMake-module.md @@ -41,7 +41,7 @@ sub_proj = cmake.subproject('libsimple_cmake') # Fetch the dependency object cm_lib = sub_proj.dependency('cm_lib') -executable(exe1, ['sources'], dependencies: [cm_lib]) +executable('exe1', ['sources'], dependencies: [cm_lib]) ``` The `subproject` method is almost identical to the normal Meson diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md index 4a00c4f..3fcdedd 100644 --- a/docs/markdown/Commands.md +++ b/docs/markdown/Commands.md @@ -58,14 +58,17 @@ Builds a default or a specified target of a configured Meson project. *(since 0.55.0)* -`TARGET` has the following syntax `[PATH/]NAME[:TYPE]`, where: +`TARGET` has the following syntax `[PATH/]NAME.SUFFIX[:TYPE]`, where: - `NAME`: name of the target from `meson.build` (e.g. `foo` from `executable('foo', ...)`). +- `SUFFIX`: name of the suffix of the target from `meson.build` (e.g. `exe` from `executable('foo', suffix: 'exe', ...)`). - `PATH`: path to the target relative to the root `meson.build` file. Note: relative path for a target specified in the root `meson.build` is `./`. -- `TYPE`: type of the target. Can be one of the following: 'executable', 'static_library', 'shared_library', 'shared_module', 'custom', 'run', 'jar'. +- `TYPE`: type of the target. Can be one of the following: 'executable', 'static_library', 'shared_library', 'shared_module', 'custom', 'alias', 'run', 'jar'. -`PATH` and/or `TYPE` can be omitted if the resulting `TARGET` can be +`PATH`, `SUFFIX`, and `TYPE` can all be omitted if the resulting `TARGET` can be used to uniquely identify the target in `meson.build`. +Note that `SUFFIX` did not exist prior to 1.3.0. + #### Backend specific arguments *(since 0.55.0)* @@ -244,6 +247,17 @@ Configures a build directory for the Meson project. was no COMMAND supplied). However, supplying the command is necessary to avoid clashes with future added commands, so "setup" should be used explicitly. +*Since 1.1.0* `--reconfigure` is allowed even if the build directory does not +already exist, that argument is ignored in that case. + +*Since 1.3.0* If the build directory already exists, options are updated with +their new value given on the command line (`-Dopt=value`). Unless `--reconfigure` +is also specified, this won't reconfigure immediately. This has the same behaviour +as `meson configure -Dopt=value`. + +*Since 1.3.0* It is possible to clear the cache and reconfigure in a single command +with `meson setup --clearcache --reconfigure `. + {{ setup_arguments.inc }} See [Meson introduction @@ -281,6 +295,12 @@ Run tests for the configure Meson project. See [the unit test documentation](Unit-tests.md) for more info. +Since *1.2.0* you can use wildcards in *args* for test names. +For example, "bas*" will match all test with names beginning with "bas". + +Since *1.2.0* it is an error to provide a test name or wildcard that +does not match any test. + #### Examples: Run tests for the project: @@ -348,20 +368,31 @@ These variables are set in environment in addition to those set using [[meson.ad - `QEMU_LD_PREFIX` *Since 1.0.0* is set to the `sys_root` value from cross file when cross compiling and that property is defined. -Since *Since 0.62.0* if bash-completion scripts are being installed and the +*Since 0.62.0* if bash-completion scripts are being installed and the shell is bash, they will be automatically sourced. -Since *Since 0.62.0* when GDB helper scripts (*-gdb.py, *-gdb.gdb, and *-gdb.csm) +*Since 0.62.0* when GDB helper scripts (*-gdb.py, *-gdb.gdb, and *-gdb.csm) are installed with a library name that matches one being built, Meson adds the needed auto-load commands into `/.gdbinit` file. When running gdb from top build directory, that file is loaded by gdb automatically. In the case of python scripts that needs to load other python modules, `PYTHONPATH` may need to be modified using `meson.add_devenv()`. -Since *Since 0.63.0* when cross compiling for Windows `WINEPATH` is used instead +*Since 0.63.0* when cross compiling for Windows `WINEPATH` is used instead of `PATH` which allows running Windows executables using wine. Note that since `WINEPATH` size is currently limited to 1024 characters, paths relative to the root of build directory are used. That means current workdir must be the root of build directory when running wine. +*Since 1.1.0* `meson devenv --dump []` command takes an optional +filename argument to write the environment into a file instead of printing to +stdout. + +*Since 1.1.0* `--dump-format` argument has been added to select which shell +format should be used. There are currently 3 formats supported: +- `sh`: Lines are in the format `VAR=/prepend:$VAR:/append`. +- `export`: Same as `sh` but with extra `export VAR` lines. +- `vscode`: Same as `sh` but without `$VAR` substitution because they do not + seems to be properly supported by vscode. + {{ devenv_arguments.inc }} diff --git a/docs/markdown/Compiler-properties.md b/docs/markdown/Compiler-properties.md index adbaa1d..6d04c8b 100644 --- a/docs/markdown/Compiler-properties.md +++ b/docs/markdown/Compiler-properties.md @@ -3,7 +3,7 @@ Not all compilers and platforms are alike. Therefore Meson provides the tools to detect properties of the system during configure time. To get most of this information, you first need to extract the *[compiler -object](Reference-manual_returned_compiler.html)* from the main +object](Reference-manual_returned_compiler.md)* from the main *meson* variable. ```meson @@ -228,4 +228,4 @@ has_special_flags = [[#compiler.has_argument]]('-Wspecialthing') ``` *Note*: some compilers silently swallow command line arguments they do -not understand. Thus this test can not be made 100% reliable. +not understand. Thus this test cannot be made 100% reliable. diff --git a/docs/markdown/Configuring-a-build-directory.md b/docs/markdown/Configuring-a-build-directory.md index 6b9bb40..1dcef46 100644 --- a/docs/markdown/Configuring-a-build-directory.md +++ b/docs/markdown/Configuring-a-build-directory.md @@ -7,7 +7,7 @@ short-description: Configuring a pre-generated build directory Often you want to change the settings of your build after it has been generated. For example you might want to change from a debug build into a release build, set custom compiler flags, change the build -options provided in your `meson_options.txt` file and so on. +options provided in your `meson.options` file and so on. The main tool for this is the `meson configure` command. diff --git a/docs/markdown/Contributing.md b/docs/markdown/Contributing.md index b3deefc..68e943b 100644 --- a/docs/markdown/Contributing.md +++ b/docs/markdown/Contributing.md @@ -14,7 +14,7 @@ Thank you for your interest in participating to the development. ## Submitting patches All changes must be submitted as [pull requests to -Github](https://github.com/mesonbuild/meson/pulls). This causes them +GitHub](https://github.com/mesonbuild/meson/pulls). This causes them to be run through the CI system. All submissions must pass a full CI test run before they are even considered for submission. @@ -110,7 +110,7 @@ Meson's merge strategy should fulfill the following guidelines: These goals are slightly contradictory so the correct thing to do often requires some judgement on part of the person doing the -merge. Github provides three different merge options, The rules of +merge. GitHub provides three different merge options, The rules of thumb for choosing between them goes like this: - single commit pull requests should always be rebased diff --git a/docs/markdown/Cross-compilation.md b/docs/markdown/Cross-compilation.md index fb22222..e1ad837 100644 --- a/docs/markdown/Cross-compilation.md +++ b/docs/markdown/Cross-compilation.md @@ -212,6 +212,8 @@ target machines look the same. Here is a sample for host machine. ```ini [host_machine] system = 'windows' +subsystem = 'windows' +kernel = 'nt' cpu_family = 'x86' cpu = 'i686' endian = 'little' @@ -221,9 +223,13 @@ These values define the machines sufficiently for cross compilation purposes. The corresponding target definition would look the same but have `target_machine` in the header. These values are available in your Meson scripts. There are three predefined variables called, -surprisingly, [[@build_machine]], [[@host_machine]] and [[@target_machine]]. -Determining the operating system of your host machine is simply a -matter of calling `host_machine.system()`. +surprisingly, [[@build_machine]], [[@host_machine]] and +[[@target_machine]]. Determining the operating system of your host +machine is simply a matter of calling `host_machine.system()`. +Starting from version 1.2.0 you can get more fine grained information +using the `.subsystem()` and `.kernel()` methods. The return values of +these functions are documented in [the reference table +page](Reference-tables.md). There are two different values for the CPU. The first one is `cpu_family`. It is a general type of the CPU. This should have a @@ -332,7 +338,7 @@ scratch. ## Custom data You can store arbitrary data in `properties` and access them from your -Meson files. As an example if you cross file has this: +Meson files. As an example if your cross file has this: ```ini [properties] diff --git a/docs/markdown/Dependencies.md b/docs/markdown/Dependencies.md index 88a1dbd..9ef4367 100644 --- a/docs/markdown/Dependencies.md +++ b/docs/markdown/Dependencies.md @@ -43,7 +43,7 @@ You can pass the `opt_dep` variable to target construction functions whether the actual dependency was found or not. Meson will ignore non-found dependencies. -Meson also allows to get variables that are defined in the +Meson also allows one to get variables that are defined in a `pkg-config` file. This can be done by using the [[dep.get_pkgconfig_variable]] function. @@ -69,8 +69,8 @@ page](#dependencies-with-custom-lookup-functionality). *Note* new in 0.51.0 *new in 0.54.0, the `internal` keyword* -When you need to get an arbitrary variables from a dependency that can -be found multiple ways and you don't want to constrain the type you +When you need to get an arbitrary variable from a dependency that can +be found multiple ways and you don't want to constrain the type, you can use the generic `get_variable` method. This currently supports cmake, pkg-config, and config-tool based variables. @@ -350,7 +350,7 @@ use those to link against your targets. If your boost headers or libraries are in non-standard locations you can set the `BOOST_ROOT`, or the `BOOST_INCLUDEDIR` and `BOOST_LIBRARYDIR` environment variables. *(added in 0.56.0)* You can -also set these parameters as `boost_root`, `boost_include`, and +also set these parameters as `boost_root`, `boost_includedir`, and `boost_librarydir` in your native or cross machine file. Note that machine file variables are preferred to environment variables, and that specifying any of these disables system-wide search for boost. @@ -659,6 +659,12 @@ The `language` keyword may used. `method` may be `auto`, `config-tool` or `pkg-config`. +## Pybind11 + +*(added 1.1.0)* + +`method` may be `auto`, `pkg-config`, `config-tool`, or `cmake`. + ## Python3 Python3 is handled specially by Meson: @@ -722,10 +728,10 @@ your own risk. ## SDL2 SDL2 can be located using `pkg-confg`, the `sdl2-config` config tool, -or as an OSX framework. +as an OSX framework, or `cmake`. -`method` may be `auto`, `config-tool`, `extraframework` or -`pkg-config`. +`method` may be `auto`, `config-tool`, `extraframework`, +`pkg-config` or `cmake`. ## Shaderc diff --git a/docs/markdown/Design-rationale.md b/docs/markdown/Design-rationale.md index a3bd5bb..67fec0a 100644 --- a/docs/markdown/Design-rationale.md +++ b/docs/markdown/Design-rationale.md @@ -240,7 +240,7 @@ project('pch demo', 'cxx') executable('myapp', 'myapp.cpp', pch : 'pch/myapp.hh') ``` -The main reason other build systems can not provide pch support this +The main reason other build systems cannot provide pch support this easily is because they don't enforce certain best practices. Due to the way include paths work, it is impossible to provide pch support that always works with both in-source and out-of-source diff --git a/docs/markdown/Disabler.md b/docs/markdown/Disabler.md index 4aed7ad..1531e1d 100644 --- a/docs/markdown/Disabler.md +++ b/docs/markdown/Disabler.md @@ -16,7 +16,7 @@ dep = dependency('foo') lib = shared_library('mylib', 'mylib.c', dependencies : dep) -# And ín a third directory +# And in a third directory exe = executable('mytest', 'mytest.c', link_with : lib) diff --git a/docs/markdown/External-commands.md b/docs/markdown/External-commands.md index 026ed54..07445ac 100644 --- a/docs/markdown/External-commands.md +++ b/docs/markdown/External-commands.md @@ -42,7 +42,7 @@ Meson will autodetect scripts with a shebang line and run them with the executable/interpreter specified in it both on Windows and on Unixes. -Note that you can not pass your command line as a single string. That +Note that you cannot pass your command line as a single string. That is, calling `run_command('do_something foo bar')` will not work. You must either split up the string into separate arguments or pass the split command as an array. It should also be noted that Meson will not diff --git a/docs/markdown/FAQ.md b/docs/markdown/FAQ.md index d3e3d0a..ffc9e17 100644 --- a/docs/markdown/FAQ.md +++ b/docs/markdown/FAQ.md @@ -62,7 +62,7 @@ executable('myprog', sources : '*.cpp') # This does NOT work! ``` Meson does not support this syntax and the reason for this is simple. -This can not be made both reliable and fast. By reliable we mean that +This cannot be made both reliable and fast. By reliable we mean that if the user adds a new source file to the subdirectory, Meson should detect that and make it part of the build automatically. @@ -149,7 +149,7 @@ subdir('tests') # test binaries would link against the library here ## Why is there not a Make backend? Because Make is slow. This is not an implementation issue, Make simply -can not be made fast. For further info we recommend you read [this +cannot be made fast. For further info we recommend you read [this post](http://neugierig.org/software/chromium/notes/2011/02/ninja.html) by Evan Martin, the author of Ninja. Makefiles also have a syntax that is very unpleasant to write which makes them a big maintenance burden. @@ -348,7 +348,7 @@ projects attempting to do just this: Meson needs to know several details about each compiler in order to compile code with it. These include things such as which compiler flags to use for each option and how to detect the compiler from its -output. This information can not be input via a configuration file, +output. This information cannot be input via a configuration file, instead it requires changes to Meson's source code that need to be submitted to Meson master repository. In theory you can run your own forked version with custom patches, but that's not good use of your @@ -405,6 +405,9 @@ advantages: not care what the extension is](https://docs.microsoft.com/en-us/cpp/build/reference/link-input-files?view=vs-2019), so specifying `libfoo.a` instead of `foo.lib` does not change the workflow, and is an improvement since it's less ambiguous. +1. Projects built with the MinGW compiler are fully compatible with + MSVC as long as they use the same CRT (e.g. UCRT with MSYS2). + These projects also name their static libraries `libfoo.a`. If, for some reason, you really need your project to output static libraries of the form `foo.lib` when building with MSVC, you can set diff --git a/docs/markdown/Feature-autodetection.md b/docs/markdown/Feature-autodetection.md index 2371226..bd6be75 100644 --- a/docs/markdown/Feature-autodetection.md +++ b/docs/markdown/Feature-autodetection.md @@ -17,7 +17,7 @@ automatically. If you do not wish to use Ccache for some reason, just specify your compiler with environment variables `CC` and/or `CXX` when first -running Meson (remember that once specified the compiler can not be +running Meson (remember that once specified the compiler cannot be changed). Meson will then use the specified compiler without Ccache. Coverage diff --git a/docs/markdown/Fs-module.md b/docs/markdown/Fs-module.md index 1393551..7ba4832 100644 --- a/docs/markdown/Fs-module.md +++ b/docs/markdown/Fs-module.md @@ -7,6 +7,14 @@ Since 0.59.0, all functions accept `files()` objects if they can do something useful with them (this excludes `exists`, `is_dir`, `is_file`, `is_absolute` since a `files()` object is always the absolute path to an existing file). +## Usage + +The module may be imported as follows: + +``` meson +fs = [[#import]]('fs') +``` + ## File lookup rules Non-absolute paths are looked up relative to the directory where the @@ -16,7 +24,7 @@ If specified, a leading `~` is expanded to the user home directory. Environment variables are not available as is the rule throughout Meson. That is, $HOME, %USERPROFILE%, $MKLROOT, etc. have no meaning to the Meson filesystem module. If needed, pass such variables into Meson via command -line options in `meson_options.txt`, native-file or cross-file. +line options in `meson.options`, native-file or cross-file. Where possible, symlinks and parent directory notation are resolved to an absolute path. @@ -90,7 +98,7 @@ Examples: x = 'foo.txt' y = 'sub/../foo.txt' z = 'bar.txt' # a symlink pointing to foo.txt -j = 'notafile.txt' # non-existent file +j = 'notafile.txt' # nonexistent file fs.is_samepath(x, y) # true fs.is_samepath(x, z) # true @@ -99,7 +107,7 @@ fs.is_samepath(x, j) # false p = 'foo/bar' q = 'foo/bar/baz/..' r = 'buz' # a symlink pointing to foo/bar -s = 'notapath' # non-existent directory +s = 'notapath' # nonexistent directory fs.is_samepath(p, q) # true fs.is_samepath(p, r) # true @@ -216,6 +224,20 @@ fs.stem('foo/bar/baz.dll.a') # baz.dll project. If the file specified by `path` is a `files()` object it cannot refer to a built file. +### relative_to + +*Since 1.3.0* + +Return a relative filepath. In the event a relative path could not be found, the +absolute path of `to` is returned. Relative path arguments will be assumed to be +relative to `meson.current_source_dir()`. + +Has the following positional arguments: + - to `str | file | custom_tgt | custom_idx | build_tgt`: end path + - from `str | file | custom_tgt | custom_idx | build_tgt`: start path + +returns: + - a string ### copyfile diff --git a/docs/markdown/Generating-sources.md b/docs/markdown/Generating-sources.md index b954a41..8b1b44e 100644 --- a/docs/markdown/Generating-sources.md +++ b/docs/markdown/Generating-sources.md @@ -53,7 +53,7 @@ is generated and that the proper include paths are created for the target: ```meson -prog_python = import('python').find_installation('python3') +prog_python = [[#find_program]]('python3') foo_c = custom_target( 'foo.c', @@ -89,7 +89,7 @@ get each output file separately. The order is the same as the order of the output argument to `custom_target` ```meson -prog_python = import('python').find_installation('python3') +prog_python = [[#find_program]]('python3') foo_ch = custom_target( 'foo.[ch]', @@ -179,7 +179,7 @@ gen2 = generator(someprog, arguments : ['--out_dir=@BUILD_DIR@', '@INPUT@']) ``` -In this case you can not use the plain `@OUTPUT@` variable, as it +In this case you cannot use the plain `@OUTPUT@` variable, as it would be ambiguous. This program only needs to know the output directory, it will generate the file names by itself. diff --git a/docs/markdown/Gnome-module.md b/docs/markdown/Gnome-module.md index 85b101a..013e8c8 100644 --- a/docs/markdown/Gnome-module.md +++ b/docs/markdown/Gnome-module.md @@ -40,6 +40,8 @@ takes two positional arguments. The first one is the name of the resource and the second is the XML file containing the resource definitions. If the name is `foobar`, Meson will generate a header file called `foobar.h`, which you can then include in your sources. +The resources specified are automatically added as dependencies of the +generated target. * `c_name`: passed to the resource compiler as an argument after `--c-name` @@ -92,6 +94,9 @@ There are several keyword arguments. Many of these map directly to the * `dependencies`: deps to use during introspection scanning * `extra_args`: command line arguments to pass to gir compiler +* `env`: (*Added 1.2.0*) environment variables to set, such as + `{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`, + or an [[@env]] object which allows more sophisticated environment juggling. * `export_packages`: extra packages the gir file exports * `sources`: the list of sources to be scanned for gir data * `nsversion`: namespace version @@ -199,6 +204,13 @@ for a build target, you must add the generated header to the build target's list of sources to codify the dependency. This is true for all generated sources, not just `mkenums_simple`. +The generated source file includes all headers passed to the sources keyword +argument, using paths relative to current build or source directory. That means +that targets that compile the generated source file must have the current +directory in its `include_directories`. *Since 1.3.0* `sources` outside of +current directory do not require adding those directories into +`include_directories` anymore. + * `body_prefix`: additional prefix at the top of the body file, e.g. for extra includes * `decorator`: optional decorator for the function declarations, diff --git a/docs/markdown/IDE-integration.md b/docs/markdown/IDE-integration.md index c60aeef..ad7185a 100644 --- a/docs/markdown/IDE-integration.md +++ b/docs/markdown/IDE-integration.md @@ -260,7 +260,7 @@ The list of all _found_ dependencies can be acquired from `intro-dependencies.json`. Here, the name, version, compiler and linker arguments for a dependency are listed. -### Scanning for dependecie with `--scan-dependencies` +### Scanning for dependencies with `--scan-dependencies` It is also possible to get most dependencies used without a build directory. This can be done by running `meson introspect @@ -338,7 +338,7 @@ The output format is as follows: ```json [ "/Path/to/the/targets/meson.build", - "/Path/to/the/targets/meson_options.txt", + "/Path/to/the/targets/meson.options", "/Path/to/the/targets/subdir/meson.build" ] ``` @@ -420,9 +420,9 @@ schema is defined by the class structure given in - [Gnome Builder](https://wiki.gnome.org/Apps/Builder) - [KDevelop](https://www.kdevelop.org) -- [Eclipse CDT](https://www.eclipse.org/cdt/) (experimental) -- [Meson Cmake Wrapper](https://github.com/prozum/meson-cmake-wrapper) (for cmake IDEs) (currently unmaintained !!) -- [Meson-UI](https://github.com/michaelbadcrumble/meson-ui) (Meson build GUI) +- [Eclipse CDT](https://www.eclipse.org/cdt/) - [Meson Syntax Highlighter](https://plugins.jetbrains.com/plugin/13269-meson-syntax-highlighter) plugin for JetBrains IDEs. -- [asabil.meson](https://open-vsx.org/extension/asabil/meson) extension for VS Code/Codium +- [vscode-meson](https://github.com/mesonbuild/vscode-meson) extension for VS Code/Codium - [Qt Creator](https://doc.qt.io/qtcreator/creator-project-meson.html) +- [Meson-UI](https://github.com/dreamer-coding-555/meson-ui) (build GUI for Meson) +- [mmeson](https://github.com/stephanlachnit/mmeson) (ccmake clone for Meson) diff --git a/docs/markdown/Installing.md b/docs/markdown/Installing.md index a692afe..2d18c17 100644 --- a/docs/markdown/Installing.md +++ b/docs/markdown/Installing.md @@ -102,6 +102,22 @@ Telling Meson to run this script at install time is a one-liner. The argument is the name of the script file relative to the current subdirectory. +## Installing as the superuser + +When building as a non-root user, but installing to root-owned locations via +e.g. `sudo ninja install`, ninja will attempt to rebuild any out of date +targets as root. This results in various bad behaviors due to build outputs and +ninja internal files being owned by root. + +Running `meson install` is preferred for several reasons. It can rebuild out of +date targets and then re-invoke itself as root. *(since 1.1.0)* Additionally, +running `sudo meson install` will drop permissions and rebuild out of date +targets as the original user, not as root. + +*(since 1.1.0)* Re-invoking as root will try to guess the user's preferred method for +re-running commands as root. The order of precedence is: sudo, doas, pkexec +(polkit). An elevation tool can be forced by setting `$MESON_ROOT_CMD`. + ## DESTDIR support Sometimes you need to install to a different directory than the diff --git a/docs/markdown/Machine-files.md b/docs/markdown/Machine-files.md index ecdb8b4..a3e876d 100644 --- a/docs/markdown/Machine-files.md +++ b/docs/markdown/Machine-files.md @@ -90,7 +90,7 @@ arch = 'aarch64-linux-gnu' c = arch + '-gcc' cpp = arch + '-g++' strip = arch + '-strip' -pkgconfig = arch + '-pkg-config' +pkg-config = arch + '-pkg-config' ... ``` @@ -128,6 +128,18 @@ b = a + 'World' a = 'Hello' ``` +*Since 1.3.0* Some tokens are replaced in the machine file before parsing it: +- `@GLOBAL_SOURCE_ROOT@`: the absolute path to the project's source tree +- `@DIRNAME@`: the absolute path to the machine file's parent directory. + +It can be used, for example, to have paths relative to the source directory, or +relative to toolchain's installation directory. +```ini +[binaries] +c = '@DIRNAME@/toolchain/gcc' +exe_wrapper = '@GLOBAL_SOURCE_ROOT@' / 'build-aux' / 'my-exe-wrapper.sh' +``` + ### Binaries The binaries section contains a list of binaries. These can be used @@ -165,7 +177,7 @@ c_ld = 'gold' cpp_ld = 'gold' ar = '/usr/i586-mingw32msvc/bin/ar' strip = '/usr/i586-mingw32msvc/bin/strip' -pkgconfig = '/usr/bin/i586-mingw32msvc-pkg-config' +pkg-config = '/usr/bin/i586-mingw32msvc-pkg-config' ``` An incomplete list of internally used programs that can be overridden @@ -179,7 +191,7 @@ here is: - libwmf-config - llvm-config - pcap-config -- pkgconfig +- pkg-config - sdl2-config - wx-config (or wx-3.0-config or wx-config-gtk) @@ -237,6 +249,8 @@ section. subprojects. This setting has no effect if the `exe_wrapper` was not specified. The default value is `true`. (*new in 0.56.0*) - `java_home` is an absolute path pointing to the root of a Java installation. +- `bindgen_clang_arguments` an array of extra arguments to pass to clang when + calling bindgen ### CMake variables diff --git a/docs/markdown/MesonCI.md b/docs/markdown/MesonCI.md index 5136c28..24f3063 100644 --- a/docs/markdown/MesonCI.md +++ b/docs/markdown/MesonCI.md @@ -32,7 +32,7 @@ The Dockerfile is generated from the `image.json` file and basically only adds a few common files and runs the `install.sh` script which should contain all distribution specific setup steps. The `common.sh` can be sourced via `source /ci/common.sh` to access some shared -functionalety. +functionality. To generate the image run `build.py -t build `. A generated image can be tested with `build.py -t test `. diff --git a/docs/markdown/Mixing-build-systems.md b/docs/markdown/Mixing-build-systems.md index 6830064..73f91b7 100644 --- a/docs/markdown/Mixing-build-systems.md +++ b/docs/markdown/Mixing-build-systems.md @@ -12,7 +12,7 @@ is the common case. This page lists the Meson project's stance on mixing build systems. The tl/dr version is that while we do provide some functionality for this use case, it only works for simple cases. Anything more complex -can not be made reliable and trying to do that would burden Meson +cannot be made reliable and trying to do that would burden Meson developers with an effectively infinite maintenance burden. Thus these use cases are not guaranteed to work, and even if a project using them works today there are no guarantees that it will work in any future diff --git a/docs/markdown/Porting-from-autotools.md b/docs/markdown/Porting-from-autotools.md index 34e1bbd..dc489af 100644 --- a/docs/markdown/Porting-from-autotools.md +++ b/docs/markdown/Porting-from-autotools.md @@ -150,7 +150,7 @@ else endif ``` -`meson_options.txt`: +`meson.options`: ```meson option('enable-dep11', type : 'boolean', value : true, description : 'enable DEP-11') diff --git a/docs/markdown/Python-module.md b/docs/markdown/Python-module.md index bc4311f..05ae57d 100644 --- a/docs/markdown/Python-module.md +++ b/docs/markdown/Python-module.md @@ -37,7 +37,10 @@ If provided, it can be: - One of `python2` or `python3`: in either case, the module will try some alternative names: `py -2` or `py -3` on Windows, and `python` everywhere. In the latter case, it will check whether the version - provided by the sysconfig module matches the required major version + provided by the sysconfig module matches the required major version. + + *Since 1.2.0*, searching for minor version (e.g. `python3.11`) also + works on Windows. Keyword arguments are the following: @@ -98,6 +101,11 @@ the addition of the following: `/usr/lib/site-packages`. When subdir is passed to this method, it will be appended to that location. This keyword argument is mutually exclusive with `install_dir` +- `limited_api`: *since 1.3.0* A string containing the Python version + of the [Py_LIMITED_API](https://docs.python.org/3/c-api/stable.html) that + the extension targets. For example, '3.7' to target Python 3.7's version of + the limited API. This behavior can be disabled by setting the value of + `python.allow_limited_api`. See [Python module options](Builtin-options.md#python-module). Additionally, the following diverge from [[shared_module]]'s default behavior: diff --git a/docs/markdown/Qt6-module.md b/docs/markdown/Qt6-module.md index e7b8901..4dbf649 100644 --- a/docs/markdown/Qt6-module.md +++ b/docs/markdown/Qt6-module.md @@ -80,7 +80,7 @@ This method takes the following keyword arguments: - `ui_files`: (string | File | CustomTarget)[]: Passed the `uic` compiler - `moc_sources`: (string | File | CustomTarget)[]: Passed the `moc` compiler. These are converted into .moc files meant to be `#include`ed - - `moc_headers`: (string | File | CustomTarget)[]: Passied the `moc` compiler. + - `moc_headers`: (string | File | CustomTarget)[]: Passed the `moc` compiler. These will be converted into .cpp files - `include_directories` (IncludeDirectories | string)[], the directories to add to header search path for `moc` diff --git a/docs/markdown/Reference-tables.md b/docs/markdown/Reference-tables.md index d2df3c8..86f3e9e 100644 --- a/docs/markdown/Reference-tables.md +++ b/docs/markdown/Reference-tables.md @@ -25,9 +25,12 @@ These are return values of the `get_id` (Compiler family) and | lcc | Elbrus C/C++/Fortran Compiler | | | llvm | LLVM-based compiler (Swift, D) | | | mono | Xamarin C# compiler | | +| mwccarm | Metrowerks C/C++ compiler for Embedded ARM | | +| mwcceppc | Metrowerks C/C++ compiler for Embedded PowerPC | | | msvc | Microsoft Visual Studio | msvc | | nagfor | The NAG Fortran compiler | | | nvidia_hpc| NVidia HPC SDK compilers | | +| nvcc | NVidia CUDA compiler | | | open64 | The Open64 Fortran Compiler | | | pathscale | The Pathscale Fortran compiler | | | pgi | Portland PGI C/C++/Fortran compilers | | @@ -42,6 +45,8 @@ These are return values of the `get_id` (Compiler family) and | yasm | The YASM compiler (Since 0.64.0) | | | ml | Microsoft Macro Assembler for x86 and x86_64 (Since 0.64.0) | msvc | | armasm | Microsoft Macro Assembler for ARM and AARCH64 (Since 0.64.0) | | +| mwasmarm | Metrowerks Assembler for Embedded ARM | | +| mwasmeppc | Metrowerks Assembler for Embedded PowerPC | | ## Linker ids @@ -56,6 +61,7 @@ These are return values of the `get_linker_id` method in a compiler object. | ld.solaris | Solaris and illumos | | ld.wasm | emscripten's wasm-ld linker | | ld64 | Apple ld64 | +| ld64.lld | The LLVM linker, with the ld64 interface | | link | MSVC linker | | lld-link | The LLVM linker, with the MSVC interface | | xilink | Used with Intel-cl only, MSVC like | @@ -68,6 +74,8 @@ These are return values of the `get_linker_id` method in a compiler object. | pgi | Portland/Nvidia PGI | | nvlink | Nvidia Linker used with cuda | | ccomp | CompCert used as the linker driver | +| mwldarm | The Metrowerks Linker with the ARM interface, used with mwccarm only | +| mwldeppc | The Metrowerks Linker with the PowerPC interface, used with mwcceppc only | For languages that don't have separate dynamic linkers such as C# and Java, the `get_linker_id` will return the compiler name. @@ -120,11 +128,13 @@ set in the cross file. | sh4 | SuperH SH-4 | | sparc | 32 bit SPARC | | sparc64 | SPARC v9 processor | +| sw_64 | 64 bit sunway processor | | wasm32 | 32 bit Webassembly | | wasm64 | 64 bit Webassembly | | x86 | 32 bit x86 processor | | x86_64 | 64 bit x86 processor | + Any cpu family not listed in the above list is not guaranteed to remain stable in future releases. @@ -143,7 +153,7 @@ These are provided by the `.system()` method call. | cygwin | The Cygwin environment for Windows | | darwin | Either OSX or iOS | | dragonfly | DragonFly BSD | -| emscripten | Emscripten's Javascript environment | +| emscripten | Emscripten's JavaScript environment | | freebsd | FreeBSD and its derivatives | | gnu | GNU Hurd | | haiku | | @@ -156,15 +166,52 @@ These are provided by the `.system()` method call. Any string not listed above is not guaranteed to remain stable in future releases. +## Kernel names (since 1.2.0) + +Native names as returned by the `.kernel()` method. + +| Value | Comment | +| ----- | ------- | +| linux | | +| freebsd | | +| openbsd | | +| netbsd | | +| nt | | +| xnu | Kernel of various Apple OSes | +| illumos | Kernel derived from OpenSolaris by community efforts | +| solaris | Kernel derived from OpenSolaris by Oracle | +| dragonfly | | +| haiku| | +| none | For e.g. bare metal embedded | + + +## Subsystem names (since 1.2.0) + +A more specific description of the system in question. Most values are +meant to be used in cross files only, as those platforms can not run +Meson natively. + +| Value | Comment | +| ----- | ------- | +| macos | Apple macOS (formerly OSX) | +| ios | Apple iOS | +| ios-simulator | | +| tvos | Apple tvOS | +| tvos-simulator | | +| watchos | Apple watchOS | +| watchos-simulator | | + ## Language arguments parameter names -These are the parameter names for passing language specific arguments to your build target. +These are the parameter names for passing language specific arguments +to your build target. | Language | compiler name | linker name | | ------------- | ------------- | ----------------- | | C | c_args | c_link_args | | C++ | cpp_args | cpp_link_args | | C# | cs_args | cs_link_args | +| CUDA | cuda_args | cuda_link_args | | D | d_args | d_link_args | | Fortran | fortran_args | fortran_link_args | | Java | java_args | java_link_args | @@ -196,6 +243,7 @@ arguments](#language-arguments-parameter-names) instead. | ----- | ------- | | CFLAGS | Flags for the C compiler | | CXXFLAGS | Flags for the C++ compiler | +| CUFLAGS | Flags for the CUDA compiler | | OBJCFLAGS | Flags for the Objective C compiler | | FFLAGS | Flags for the Fortran compiler | | DFLAGS | Flags for the D compiler | @@ -258,6 +306,7 @@ which are supported by GCC, Clang, and other compilers. | sentinel⁵ | | unused | | used | +| vector_size⁶ | | visibility* | | visibility:default† | | visibility:hidden† | @@ -280,6 +329,8 @@ which are supported by GCC, Clang, and other compilers. ⁵ *New in 0.63.0* +⁶ *New in 1.1.0* + ### MSVC __declspec These values are supported using the MSVC style `__declspec` annotation, @@ -325,9 +376,29 @@ machine](#Environment-variables-per-machine) section for details. | C# | CSC | CSC | The linker is the compiler | | nasm | NASM | | Uses the C linker | -*The old environment variales are still supported, but are deprecated +*The old environment variables are still supported, but are deprecated and will be removed in a future version of Meson.* +*changed in 1.3.0* Paths with spaces were split unconditionally to extract +components such as the [path to Ccache](Feature-autodetection.md#ccache), +intrinsic compiler flags like `-m32` or `--target`, etc. This broke passing +a hardcoded compiler path to CMake subprojects. To work around this, paths +must be wrapped with double quotes: + +```bash +export CC='"C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.34.31933/bin/Hostx64/x64/cl.exe"' +``` + +You can also set the values through [machine files](Machine-files.md#binaries). + +*New in 1.3.0* Paths that point to an existing executable no longer need +wrapping: + +```bash +export CC='C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.34.31933/bin/Hostx64/x64/cl.exe' +``` + + ## Environment variables per machine Since *0.54.0*, Following Autotool and other legacy build systems, diff --git a/docs/markdown/Release-notes-for-0.40.0.md b/docs/markdown/Release-notes-for-0.40.0.md index 53bc9ba..5b3410b 100644 --- a/docs/markdown/Release-notes-for-0.40.0.md +++ b/docs/markdown/Release-notes-for-0.40.0.md @@ -119,7 +119,7 @@ qt5_dep = dependency('qt5', modules : 'core', method : 'qmake') ## Link whole contents of static libraries The default behavior of static libraries is to discard all symbols -that are not not directly referenced. This may lead to exported +that are not directly referenced. This may lead to exported symbols being lost. Most compilers support "whole archive" linking that includes all symbols and code of a given static library. This is exposed with the `link_whole` keyword. diff --git a/docs/markdown/Release-notes-for-0.45.0.md b/docs/markdown/Release-notes-for-0.45.0.md index 9dd56e3..1d736bb 100644 --- a/docs/markdown/Release-notes-for-0.45.0.md +++ b/docs/markdown/Release-notes-for-0.45.0.md @@ -97,7 +97,7 @@ int_255 = 0xFF The value `if-release` can be given for the `b_ndebug` project option. This will make the `NDEBUG` pre-compiler macro to be defined for -release type builds as if the `b_ndebug` project option had had the +release type builds as if the `b_ndebug` project option had the value `true` defined for it. ## `install_data()` defaults to `{datadir}/{projectname}` diff --git a/docs/markdown/Release-notes-for-0.46.0.md b/docs/markdown/Release-notes-for-0.46.0.md index 3de6bcd..f17a1b7 100644 --- a/docs/markdown/Release-notes-for-0.46.0.md +++ b/docs/markdown/Release-notes-for-0.46.0.md @@ -269,7 +269,7 @@ helper = static_library( final = shared_library( 'final', ['final.c'], - dependencyes : dep, + dependencies : dep, ) ``` diff --git a/docs/markdown/Release-notes-for-0.47.0.md b/docs/markdown/Release-notes-for-0.47.0.md index f85a41b..c0e0fd7 100644 --- a/docs/markdown/Release-notes-for-0.47.0.md +++ b/docs/markdown/Release-notes-for-0.47.0.md @@ -24,7 +24,7 @@ preprocessor symbol's value `"ab" "cd"` is returned as `"abcd"`. Cross-compilation is now supported for ARM targets using ARM compiler version 6 - ARMCLANG. The required ARMCLANG compiler options for building a shareable library are not included in the current Meson -implementation for ARMCLANG support, so it can not build shareable +implementation for ARMCLANG support, so it cannot build shareable libraries. This current Meson implementation for ARMCLANG support can not build assembly files with arm syntax (we need to use armasm instead of ARMCLANG for the `.s` files with this syntax) and only diff --git a/docs/markdown/Release-notes-for-0.48.0.md b/docs/markdown/Release-notes-for-0.48.0.md index 144355d..d8dbeac 100644 --- a/docs/markdown/Release-notes-for-0.48.0.md +++ b/docs/markdown/Release-notes-for-0.48.0.md @@ -12,7 +12,7 @@ use, such as *debug* and *minsize*. There is also a *plain* type that adds nothing by default but instead makes it the user's responsibility to add everything by hand. This works but is a bit tedious. -In this release we have added new new options to manually toggle e.g. +In this release we have added new options to manually toggle e.g. optimization levels and debug info so those can be changed independently of other options. For example by default the debug buildtype has no optimization enabled at all. If you wish to use GCC's @@ -79,7 +79,7 @@ which has special properties such as not buffering stdout and serializing all targets in this pool. The primary use-case for this is to be able to run external commands -that take a long time to exeute. Without setting this, the user does +that take a long time to execute. Without setting this, the user does not receive any feedback about what the program is doing. ## `dependency(version:)` now applies to all dependency types diff --git a/docs/markdown/Release-notes-for-0.49.0.md b/docs/markdown/Release-notes-for-0.49.0.md index 6b84af1..4c09183 100644 --- a/docs/markdown/Release-notes-for-0.49.0.md +++ b/docs/markdown/Release-notes-for-0.49.0.md @@ -279,7 +279,7 @@ associated with the generated pkg-config file should be passed as first positional argument instead of in the `libraries` keyword argument. The previous behaviour is maintained but prints a deprecation warning and support for this will be removed in a future -Meson release. If you can not create the needed pkg-config file +Meson release. If you cannot create the needed pkg-config file without this warning, please file an issue with as much details as possible about the situation. diff --git a/docs/markdown/Release-notes-for-0.50.0.md b/docs/markdown/Release-notes-for-0.50.0.md index 9579c33..d0f2d76 100644 --- a/docs/markdown/Release-notes-for-0.50.0.md +++ b/docs/markdown/Release-notes-for-0.50.0.md @@ -227,7 +227,7 @@ Furthermore, the `filename` and `install_filename` keys in the targets introspection are now lists of strings with identical length. The `--target-files` option is now deprecated, since the same information -can be acquired from the `--tragets` introspection API. +can be acquired from the `--targets` introspection API. ## Meson file rewriter @@ -317,7 +317,7 @@ A complete introspection dump is also stored in the `meson-info` directory. This dump will be (re)generated each time meson updates the configuration of the build directory. -Additionlly the format of `meson introspect target` was changed: +Additionally the format of `meson introspect target` was changed: - New: the `sources` key. It stores the source files of a target and their compiler parameters. - New: the `defined_in` key. It stores the Meson file where a target is defined diff --git a/docs/markdown/Release-notes-for-0.51.0.md b/docs/markdown/Release-notes-for-0.51.0.md index e015a95..635fbbd 100644 --- a/docs/markdown/Release-notes-for-0.51.0.md +++ b/docs/markdown/Release-notes-for-0.51.0.md @@ -213,7 +213,7 @@ The output of `custom_target` and `custom_target[i]` can be used in integrating custom code generator steps, but note that there are many limitations: - - Meson can not know about link dependencies of the custom target. If + - Meson cannot know about link dependencies of the custom target. If the target requires further link libraries, you need to add them manually - The user is responsible for ensuring that the code produced by @@ -325,7 +325,7 @@ sub_proj = cmake.subproject('libsimple_cmake') # Fetch the dependency object cm_lib = sub_proj.dependency('cm_lib') -executable(exe1, ['sources'], dependencies: [cm_lib]) +executable('exe1', ['sources'], dependencies: [cm_lib]) ``` It should be noted that not all projects are guaranteed to work. The diff --git a/docs/markdown/Release-notes-for-0.52.0.md b/docs/markdown/Release-notes-for-0.52.0.md index 5e574ba..5095d30 100644 --- a/docs/markdown/Release-notes-for-0.52.0.md +++ b/docs/markdown/Release-notes-for-0.52.0.md @@ -94,7 +94,7 @@ linker internal re-architecture this has become possible ## Compiler and dynamic linker representation split -0.52.0 includes a massive refactor of the representaitons of compilers to +0.52.0 includes a massive refactor of the representations of compilers to tease apart the representations of compilers and dynamic linkers (ld). This fixes a number of compiler/linker combinations. In particular this fixes use GCC and vanilla clang on macOS. @@ -160,7 +160,7 @@ lib2 = static_library(other_sources, link_whole : lib1, install : true) ``` - `link_with:` of a static library with an uninstalled static library. In the example below, lib2 now implicitly promote `link_with:` to `link_whole:` because -the installed lib2 would oterhwise be unusable. +the installed lib2 would otherwise be unusable. ```meson lib1 = static_library(sources, install : false) lib2 = static_library(sources, link_with : lib1, install : true) diff --git a/docs/markdown/Release-notes-for-0.54.0.md b/docs/markdown/Release-notes-for-0.54.0.md index f9bfcf6..8338180 100644 --- a/docs/markdown/Release-notes-for-0.54.0.md +++ b/docs/markdown/Release-notes-for-0.54.0.md @@ -286,7 +286,7 @@ This old behavior is inconsistent with the way Autotools works, which undermines the purpose of distro-integration that is the only reason environment variables are supported at all in Meson. The new behavior is not quite the same, but doesn't conflict: Meson doesn't always -repond to an environment when Autoconf would, but when it does it +respond to an environment when Autoconf would, but when it does it interprets it as Autotools would. ## Added 'pkg_config_libdir' property diff --git a/docs/markdown/Release-notes-for-0.55.0.md b/docs/markdown/Release-notes-for-0.55.0.md index 8d3b4ba..1b086b2 100644 --- a/docs/markdown/Release-notes-for-0.55.0.md +++ b/docs/markdown/Release-notes-for-0.55.0.md @@ -93,7 +93,7 @@ Meson now supports passing configuration options to CMake and overriding certain build details extracted from the CMake subproject. The new CMake configuration options object is very similar to the -[[@cfg_data]] object object returned +[[@cfg_data]] object returned by [[configuration_data]]. It is generated by the `subproject_options` function @@ -175,7 +175,7 @@ changed), but is now deprecated. ## String concatenation in meson_options.txt It is now possible to use string concatenation (with the `+` -opperator) in the `meson_options.txt` file. This allows splitting long +operator) in the `meson_options.txt` file. This allows splitting long option descriptions. ```meson diff --git a/docs/markdown/Release-notes-for-0.59.0.md b/docs/markdown/Release-notes-for-0.59.0.md index 8a04d34..5cdffe8 100644 --- a/docs/markdown/Release-notes-for-0.59.0.md +++ b/docs/markdown/Release-notes-for-0.59.0.md @@ -196,7 +196,7 @@ executable( ## New `build target` methods The [[@build_tgt]] object now supports -the following two functions, to ensure feature compatebility with +the following two functions, to ensure feature compatibility with [[@external_program]] objects: - `found()`: Always returns `true`. This function is meant @@ -205,7 +205,7 @@ the following two functions, to ensure feature compatebility with use-cases where an executable is used instead of an external program. - `path()`: **(deprecated)** does the exact same as `full_path()`. - **NOTE:** This function is solely kept for compatebility + **NOTE:** This function is solely kept for compatibility with `external program` objects. It will be removed once the, also deprecated, corresponding `path()` function in the `external program` object is removed. diff --git a/docs/markdown/Release-notes-for-0.60.0.md b/docs/markdown/Release-notes-for-0.60.0.md index 4bf342c..2debfca 100644 --- a/docs/markdown/Release-notes-for-0.60.0.md +++ b/docs/markdown/Release-notes-for-0.60.0.md @@ -286,7 +286,7 @@ be flattened. ## The qt modules now accept generated outputs as inputs for qt.compile_* -This means you can uset `custom_target`, custom_target indices +This means you can use `custom_target`, custom_target indices (`custom_target[0]`, for example), or the output of `generator.process` as inputs to the various `qt.compile_*` methods. @@ -357,7 +357,7 @@ are found, and silently continue if Visual Studio activation fails. `meson setup --vsenv` command line argument can now be used to force Visual Studio activation even when other compilers are found. It also make Meson abort with an -error message when activation fails. This is especially useful for Github Action +error message when activation fails. This is especially useful for GitHub Actions because their Windows images have gcc in their PATH by default. `--vsenv` is set by default when using `vs` backend. diff --git a/docs/markdown/Release-notes-for-0.62.0.md b/docs/markdown/Release-notes-for-0.62.0.md index cc32ac6..9a7468f 100644 --- a/docs/markdown/Release-notes-for-0.62.0.md +++ b/docs/markdown/Release-notes-for-0.62.0.md @@ -19,9 +19,9 @@ directory, that file is loaded by gdb automatically. ## Print modified environment variables with `meson devenv --dump` -With `--dump` option, all envorinment variables that have been modified are +With `--dump` option, all environment variables that have been modified are printed instead of starting an interactive shell. It can be used by shell -scripts that wish to setup their environment themself. +scripts that wish to setup their environment themselves. ## New `method` and `separator` kwargs on `environment()` and `meson.add_devenv()` diff --git a/docs/markdown/Release-notes-for-0.63.0.md b/docs/markdown/Release-notes-for-0.63.0.md index 9f8da3e..3b47ff4 100644 --- a/docs/markdown/Release-notes-for-0.63.0.md +++ b/docs/markdown/Release-notes-for-0.63.0.md @@ -112,7 +112,7 @@ and the resulting directory tree will look like ## JAR Resources The ability to add resources to a JAR has been added. Use the `java_resources` -keyword argument. It takes a `sturctured_src` object. +keyword argument. It takes a `structured_src` object. ```meson jar( diff --git a/docs/markdown/Release-notes-for-1.0.0.md b/docs/markdown/Release-notes-for-1.0.0.md index cc29d16..42c05dd 100644 --- a/docs/markdown/Release-notes-for-1.0.0.md +++ b/docs/markdown/Release-notes-for-1.0.0.md @@ -54,7 +54,7 @@ Meson function name styling. The `bindgen` method of the `rust` module now accepts a dependencies argument. Any include paths in these dependencies will be passed to the underlying call to -`clang`, and the call to `bindgen` will correctly depend on any generatd sources. +`clang`, and the call to `bindgen` will correctly depend on any generated sources. ## String arguments to the rust.bindgen include_directories argument diff --git a/docs/markdown/Release-notes-for-1.1.0.md b/docs/markdown/Release-notes-for-1.1.0.md new file mode 100644 index 0000000..d053948 --- /dev/null +++ b/docs/markdown/Release-notes-for-1.1.0.md @@ -0,0 +1,224 @@ +--- +title: Release 1.1.0 +short-description: Release notes for 1.1.0 +... + +# New features + +Meson 1.1.0 was released on 10 April 2023 +## `clang-cl` now accepts `cpp_std=c++20` + +Requires `clang-cl` 13 or later. + +## coercing values in the option() function is deprecated + +Currently code such as: +```meson +option('foo', type : 'boolean', value : 'false') +``` +works, because Meson coerces `'false'` to `false`. + +This should be avoided, and will now result in a deprecation warning. + +## New `declare_dependency(objects: )` argument + +A new argument to `declare_dependency` makes it possible to add objects +directly to executables that use an internal dependency, without going +for example through `link_whole`. + +## Dump devenv into file and select format + +`meson devenv --dump []` command now takes an optional filename argument +to write the environment into a file instead of printing to stdout. + +A new `--dump-format` argument has been added to select which shell format +should be used. There are currently 3 formats supported: +- `sh`: Lines are in the format `VAR=/prepend:$VAR:/append`. +- `export`: Same as `sh` but with extra `export VAR` lines. +- `vscode`: Same as `sh` but without `$VAR` substitution because they do not + seems to be properly supported by vscode. + +## Feature objects now have an enable_auto_if method + +This performs the opposite task of the disable_auto_if method, enabling the +feature if the condition is true. + +## Add a FeatureOption.enable_if and .disable_if + +These are useful when features need to be constrained to pass to [[dependency]], +as the behavior of an `auto` and `disabled` or `enabled` feature is markedly +different. consider the following case: + +```meson +opt = get_option('feature').disable_auto_if(not foo) +if opt.enabled() and not foo + error('Cannot enable feat when foo is not also enabled') +endif +dep = dependency('foo', required : opt) +``` + +This could be simplified to +```meson +opt = get_option('feature').disable_if(not foo, error_message : 'Cannot enable feature when foo is not also enabled') +dep = dependency('foo', required : opt) +``` + +For a real life example, here is some code in mesa: +```meson +_llvm = get_option('llvm') +dep_llvm = null_dep +with_llvm = false +if _llvm.allowed() + dep_llvm = dependency( + 'llvm', + version : _llvm_version, + modules : llvm_modules, + optional_modules : llvm_optional_modules, + required : ( + with_amd_vk or with_gallium_radeonsi or with_gallium_opencl or with_clc + or _llvm.enabled() + ), + static : not _shared_llvm, + fallback : ['llvm', 'dep_llvm'], + include_type : 'system', + ) + with_llvm = dep_llvm.found() +endif +if with_llvm + ... +elif with_amd_vk and with_aco_tests + error('ACO tests require LLVM, but LLVM is disabled.') +elif with_gallium_radeonsi or with_swrast_vk + error('The following drivers require LLVM: RadeonSI, SWR, Lavapipe. One of these is enabled, but LLVM is disabled.') +elif with_gallium_opencl + error('The OpenCL "Clover" state tracker requires LLVM, but LLVM is disabled.') +elif with_clc + error('The CLC compiler requires LLVM, but LLVM is disabled.') +else + draw_with_llvm = false +endif +``` + +simplified to: +```meson +_llvm = get_option('llvm') \ + .enable_if(with_amd_vk and with_aco_tests, error_message : 'ACO tests requires LLVM') \ + .enable_if(with_gallium_radeonsi, error_message : 'RadeonSI requires LLVM') \ + .enable_if(with_swrast_vk, error_message : 'Vulkan SWRAST requires LLVM') \ + .enable_if(with_gallium_opencl, error_message : 'The OpenCL Clover state trackers requires LLVM') \ + .enable_if(with_clc, error_message : 'CLC library requires LLVM') + +dep_llvm = dependency( + 'llvm', + version : _llvm_version, + modules : llvm_modules, + optional_modules : llvm_optional_modules, + required : _llvm, + static : not _shared_llvm, + fallback : ['llvm', 'dep_llvm'], + include_type : 'system', +) +with_llvm = dep_llvm.found() +``` + +## Generated objects can be passed in the `objects:` keyword argument + +In previous versions of Meson, generated objects could only be +passed as sources of a build target. This was confusing, therefore +generated objects can now be passed in the `objects:` keyword +argument as well. + +## The project function now supports setting the project license files + +This goes together with the license name. The license files can be +automatically installed via [[meson.install_dependency_manifest]], +or queried via [[meson.project_license_files]]. + +## A new core directory option "licensedir" is available + +This will install a dependency manifest to the specified directory, if none +is is explicitly set. + +## `sudo meson install` now drops privileges when rebuilding targets + +It is common to install projects using sudo, which should not affect build +outputs but simply install the results. Unfortunately, since the ninja backend +updates a state file when run, it's not safe to run ninja as root at all. + +It has always been possible to carefully build with: + +``` +ninja && sudo meson install --no-rebuild +``` + +Meson now tries to be extra safe as a general solution. `sudo meson install` +will attempt to rebuild, but has learned to run `ninja` as the original +(pre-sudo or pre-doas) user, ensuring that build outputs are generated/compiled +as non-root. + +## `meson install` now supports user-preferred root elevation tools + +Previously, when installing a project, if any files could not be installed due +to insufficient permissions the install process was automatically re-run using +polkit. Now it prompts to ask whether that is desirable, and checks for +CLI-based tools such as sudo or opendoas or `$MESON_ROOT_CMD`, first. + +Meson will no longer attempt privilege elevation at all, when not running +interactively. + +## Support for reading options from meson.options + +Support has been added for reading options from `meson.options` instead of +`meson_options.txt`. These are equivalent, but not using the `.txt` extension +for a build file has a few advantages, chief among them many tools and text +editors expect a file with the `.txt` extension to be plain text files, not +build scripts. + +## Redirect introspection outputs to stderr + +`meson introspect` used to disable logging to `stdout` to not interfere with generated json. +It now redirect outputs to `stderr` to allow printing warnings to the console +while keeping `stdout` clean for json outputs. + +## New "none" backend + +The `--backend=none` option has been added, to configure a project that has no +build rules, only install rules. This avoids depending on ninja. + +## compiler.preprocess() + +Dependencies keyword argument can now be passed to `compiler.preprocess()` to +add include directories or compiler arguments. + +Generated sources such as custom targets are now allowed too. + +## New pybind11 custom dependency + +`dependency('pybind11')` works with pkg-config and cmake without any special +support, but did not handle the `pybind11-config` script. + +This is useful because the config-tool will work out of the box when pybind11 +is installed, but the pkg-config and cmake files are shoved into python's +site-packages, which makes it impossible to use in an out of the box manner. + + +## Allow --reconfigure and --wipe of empty builddir + +`meson setup --reconfigure builddir` and `meson setup --wipe builddir` are now +accepting `builddir/` to be empty or containing a previously failed setup attempt. +Note that in that case previously passed command line options must be repeated +as only a successful build saves configured options. + +This is useful for example with scripts that always repeat all options, +`meson setup builddir --wipe -Dfoo=bar` will always work regardless whether +it is a first invocation or not. + +## Allow custom install scripts to run with `--dry-run` option + +An new `dry_run` keyword is added to `meson.add_install_script()` +to allow a custom install script to run when meson is invoked +with `meson install --dry-run`. + +In dry run mode, the `MESON_INSTALL_DRY_RUN` environment variable +is set. + diff --git a/docs/markdown/Release-notes-for-1.2.0.md b/docs/markdown/Release-notes-for-1.2.0.md new file mode 100644 index 0000000..2331216 --- /dev/null +++ b/docs/markdown/Release-notes-for-1.2.0.md @@ -0,0 +1,187 @@ +--- +title: Release 1.2.0 +short-description: Release notes for 1.2.0 +... + +# New features + +Meson 1.2.0 was released on 17 July 2023 +## Added Metrowerks C/C++ toolchains + +Added support for the Metrowerks Embedded ARM and Metrowerks Embedded PowerPC toolchains (https://www.nxp.com/docs/en/reference-manual/CWMCUKINCMPREF.pdf). + +The implementation is somewhat experimental. It has been tested on a few projects and works fairly well, but may have issues. + +## Added str.splitlines method + +[[str.splitlines]] can now be used to split a string into an array of lines. + +## `generator.process(generator.process(...))` + +Added support for code like this: +```meson +gen1 = generator(...) +gen2 = generator(...) +gen2.process(gen1.process('input.txt')) +``` + +## Extra files keyword in `declare_dependency` + +`declare_dependency` have a new `extra_files` keyword, +to add extra files to a target. It is used mostly for IDE integration. + +## Added a new '--genvslite' option for use with 'meson setup ...' + +To facilitate a more usual visual studio work-flow of supporting and switching between +multiple build configurations (buildtypes) within the same solution, among other +[reasons](https://github.com/mesonbuild/meson/pull/11049), use of this new option +has the effect of setting up multiple ninja back-end-configured build directories, +named with their respective buildtype suffix. E.g. 'somebuilddir_debug', +'somebuilddir_release', etc. as well as a '_vs'-suffixed directory that contains the +generated multi-buildtype solution. Building/cleaning/rebuilding in the solution +now launches the meson build (compile) of the corresponding buildtype-suffixed build +directory, instead of using Visual Studio's native engine. + +## `gnome.generate_gir()` now supports `env` kwarg + +`gnome.generate_gir()` now accepts the `env` kwarg which lets you set environment variables. + +## More data in introspection files + +- Used compilers are listed in `intro-compilers.json` +- Informations about `host`, `build` and `target` machines + are lister in `intro-machines.json` +- `intro-dependencies.json` now includes internal dependencies, + and relations between dependencies. +- `intro-targets.json` now includes dependencies, `vs_module_defs`, + `win_subsystem`, and linker parameters. + +## Machine objects get `kernel` and `subsystem` properties + +Meson has traditionally provided a `system` property to detect the +system being run on. However this is not enough to reliably +differentiate between e.g. an iOS platform from a watchOS one. Two new +properties, namely `kernel` and `subsystem` have been added so these +setups can be reliably detected. + +These new properties are not necessary in cross files for now, but if +they are not defined and a build file tries to access them, Meson will +exit with a hard error. It is expected that at some point in the +future defining the new properties will become mandatory. + +## default_options and override_options may now be dictionaries + +Instead of passing them as `default_options : ['key=value']`, they can now be +passed as `default_options : {'key': 'value'}`, and the same for +`override_options`. + +## New override of `find_program('meson')` + +In some cases, it has been useful for build scripts to access the Meson command +used to invoke the build script. This has led to various ad-hoc solutions that +can be very brittle and project-specific. + +```meson +meson_prog = find_program('meson') +``` + +This call will supply the build script with an external program pointing at the +invoked Meson. + +Because Meson also uses `find_program` for program lookups internally, this +override will also be handled in cases similar to the following: + +```meson +custom_target( + # ... + command: [ + 'meson', + ], + # ... +) + +run_command( + 'meson', + # ... +) + +run_target( + 'tgt', + command: [ + 'meson', + # ... + ] +) +``` + +## Find more specific python version on Windows + +You can now use `python3.x`, where `x` is the minor version, +to find a more specific version of python on Windows, when +using the python module. On other platforms, it was already +working as `python3.x` is the executable name. + +## Python module can now compile bytecode + +A new builtin option is available: `-Dpython.bytecompile=2`. It can be used to +compile bytecode for all pure python files installed via the python module. + +## rust.bindgen allows passing extra arguments to rustc + +This may be necessary to pass extra `cfg`s or to change warning levels. + +## Support for defining crate names of Rust dependencies in Rust targets + +Rust supports defining a different crate name for a dependency than what the +actual crate name during compilation of that dependency was. + +This allows using multiple versions of the same crate at once, or simply using +a shorter name of the crate for convenience. + +```meson +a_dep = dependency('some-very-long-name') + +my_executable = executable('my-executable', 'src/main.rs', + rust_dependency_map : { + 'some_very_long_name' : 'a', + }, + dependencies : [a_dep], +) +``` + +## A machine file may be used to pass extra arguments to clang in a bindgen call + +Because of the way that bindgen proxies arguments to clang the only choice to +add extra arguments currently is to wrap bindgen in a script, since the +arguments must come after a `--`. This is inelegant, and not very portable. Now +a `bindgen_clang_arguments` field may be placed in the machine file for the host +machine, and these arguments will be added to every bindgen call for clang. This +is intended to be useful for things like injecting `--target` arguments. + +## Add a `link_with` keyword to `rust.test()` + +This can already be be worked around by creating `declare_dependency()` objects +to pass to the `dependencies` keyword, but this cuts out the middle man. + +## Rust now supports the b_ndebug option + +Which controls the `debug_assertions` cfg, which in turn controls +`debug_assert!()` macro. This macro is roughly equivalent to C's `assert()`, as +it can be toggled with command line options, unlike Rust's `assert!()`, which +cannot be turned off, and is not designed to be. + +## Wildcards in list of tests to run + +The `meson test` command now accepts wildcards in the list of test names. +For example `meson test basic*` will run all tests whose name begins +with "basic". + +meson will report an error if the given test name does not match any +existing test. meson will log a warning if two redundant test names +are given (for example if you give both "proj:basic" and "proj:"). + +## New for the generation of Visual Studio vcxproj projects + +When vcxproj is generated, another file vcxproj.filters is generated in parallel. +It enables to set a hierarchy of the files inside the solution following their place on filesystem. + diff --git a/docs/markdown/Release-notes-for-1.3.0.md b/docs/markdown/Release-notes-for-1.3.0.md new file mode 100644 index 0000000..cf6ad46 --- /dev/null +++ b/docs/markdown/Release-notes-for-1.3.0.md @@ -0,0 +1,338 @@ +--- +title: Release 1.3.0 +short-description: Release notes for 1.3.0 +... + +# New features + +Meson 1.3.0 was released on 19 November 2023 +## Clarify of implicitly-included headers in C-like compiler checks + +Compiler check methods `compiler.compute_int()`, `compiler.alignment()` +and `compiler.sizeof()` now have their implicitly-included headers +corrected and documented. + +`` was included unintentionally when cross-compiling, which +is less than ideal because there is no guarantee that a standard library +is available for the target platform. Only `` is included instead. + +For projects that depend on the old behavior, the compiler check methods +have an optional argument `prefix`, which can be used to specify additional +`#include` directives. + +## Treat warnings as error in compiler checks + +Compiler check methods `compiler.compiles()`, `compiler.links()` and `compiler.run()` +now have a new `werror: true` keyword argument to treat compiler warnings as error. +This can be used to check if code compiles without warnings. + +## Compilers now have a `has_define` method + +This method returns true if the given preprocessor symbol is +defined, else false is returned. This is useful is cases where +an empty define has to be distinguished from a non-set one, which +is not possible using `get_define`. + +Additionally it makes intent clearer for code that only needs +to check if a specific define is set at all and does not care +about its value. + +## [[configure_file]] now has a `macro_name` parameter. + +This new paramater, `macro_name` allows C macro-style include guards to be added +to [[configure_file]]'s output when a template file is not given. This change +simplifies the creation of configure files that define macros with dynamic names +and want the C-style include guards. + +## `c_std` and `cpp_std` options now accepts a list of values + +Projects that prefer GNU C, but can fallback to ISO C, can now set, for +example, `default_options: 'c_std=gnu11,c11'`, and it will use `gnu11` when +available, but fallback to `c11` otherwise. It is an error only if none of the +values are supported by the current compiler. + +Likewise, a project that can take benefit of `c++17` but can still build with +`c++11` can set `default_options: 'cpp_std=c++17,c++11'`. + +This allows us to deprecate `gnuXX` values from the MSVC compiler. That means +that `default_options: 'c_std=gnu11'` will now print a warning with MSVC +but fallback to `c11`. No warning is printed if at least one +of the values is valid, i.e. `default_options: 'c_std=gnu11,c11'`. + +In the future that deprecation warning will become an hard error because +`c_std=gnu11` should mean GNU is required, for projects that cannot be +built with MSVC for example. + +## More meaningful description of many generative tasks + +When a module uses a `CustomTarget` to process files, it now has the possibility +to customize the message displayed by ninja. + +Many modules were updated to take advantage of this new feature. + +## Deprecate 'jar' as a build_target type + +The point of `build_target()` is that what is produced can be conditionally +changed. However, `jar()` has a significant number of non-overlapping arguments +from other build_targets, including the kinds of sources it can include. Because +of this crafting a `build_target` that can be used as a Jar and as something +else is incredibly hard to do. As such, it has been deprecated, and using +`jar()` directly is recommended. + +## generator.process() gains 'env' keyword argument + +Like the kwarg of the same name in `custom_target()`, `env` allows +you to set the environment in which the generator will process inputs. + +## Target names for executables now take into account suffixes. + +In previous versions of meson, a `meson.build` file like this: + +``` +exectuable('foo', 'main.c') +exectuable('foo', 'main.c', name_suffix: 'bar') +``` + +would result in a configure error because meson internally used +the same id for both executables. This build file is now allowed +since meson takes into account the `bar` suffix when generating the +second executable. This allows for executables with the same basename +but different suffixes to be built in the same subdirectory. + +## Executable gains vs_module_defs keyword + +This allows using a .def file to control which functions an [[executable]] will +expose to a [[shared_module]]. + +## find_program() now supports the 'default_options' argument + +In a similar fashion as dependency(), find_program() now also allows you to set default +options for the subproject that gets built in case of a fallback. + +## `fs.relative_to()` + +The `fs` module now has a `relative_to` method. The method will return the +relative path from argument one to argument two, if one exists. Otherwise, the +absolute path to argument one is returned. + +```meson +assert(fs.relative_to('c:\\prefix\\lib', 'c:\\prefix\\bin') == '..\\lib') +assert(fs.relative_to('c:\\proj1\\foo', 'd:\\proj1\\bar') == 'c:\\proj1\\foo') +assert(fs.relative_to('prefix\\lib\\foo', 'prefix') == 'lib\\foo') + +assert(fs.relative_to('/prefix/lib', '/prefix/bin') == '../lib') +assert(fs.relative_to('prefix/lib/foo', 'prefix') == 'lib/foo') +``` + +In addition to strings, it can handle files, custom targets, custom target +indices, and build targets. + +## Added follow_symlinks arg to install_data, install_header, and install_subdir + +The [[install_data]], [[install_headers]], [[install_subdir]] functions now +have an optional argument `follow_symlinks` that, if set to `true`, makes it so +symbolic links in the source are followed, rather than copied into the +destination tree, to match the old behavior. The default, which is currently +to follow links, is subject to change in the future. + +## Added 'fill' kwarg to int.to_string() + +int.to_string() now accepts a `fill` argument. This allows you to pad the +string representation of the integer with leading zeroes: + +```meson +n = 4 +message(n.to_string()) +message(n.to_string(fill: 3)) + +n = -4 +message(n.to_string(fill: 3)) +``` + +OUTPUT: +```meson +4 +004 +-04 +``` + +## Added 'json' output_format to configure_file() + +When no input file is specified, [[configure_file]] can now +generate a `json` file from given [[@cfg_data]]. +Field descriptions are not preserved in the json file. + +## `@GLOBAL_SOURCE_ROOT@` and `@DIRNAME@` in machine files + +Some tokens are now replaced in the machine file before parsing it: +- `@GLOBAL_SOURCE_ROOT@`: the absolute path to the project's source tree +- `@DIRNAME@`: the absolute path to the machine file's parent directory. + +It can be used, for example, to have paths relative to the source directory, or +relative to toolchain's installation directory. +```ini +[binaries] +c = '@DIRNAME@/toolchain/gcc' +exe_wrapper = '@GLOBAL_SOURCE_ROOT@' / 'build-aux' / 'my-exe-wrapper.sh' +``` + +## clang-tidy-fix target + +If `clang-tidy` is installed and the project's source root contains a +`.clang-tidy` (or `_clang-tidy`) file, Meson will automatically define +a `clang-tidy-fix` target that runs `run-clang-tidy` tool with `-fix` +option to apply the changes found by clang-tidy to the source code. + +If you have defined your own `clang-tidy-fix` target, Meson will not +generate its own target. + +## Meson compile command now accepts suffixes for TARGET + +The syntax for specifying a target for meson compile is now +`[PATH_TO_TARGET/]TARGET_NAME.TARGET_SUFFIX[:TARGET_TYPE]` where +`TARGET_SUFFIX` is the suffix argument given in the build target +within meson.build. It is optional and `TARGET_NAME` remains +sufficient if it uniquely resolves to one single target. + +## New environment variable `MESON_PACKAGE_CACHE_DIR` + +If the `MESON_PACKAGE_CACHE_DIR` environment variable is set, it is used instead of the +project's `subprojects/packagecache`. This allows sharing the cache across multiple +projects. In addition it can contain an already extracted source tree as long as it +has the same directory name as the `directory` field in the wrap file. In that +case, the directory will be copied into `subprojects/` before applying patches. + +## Update options with `meson setup -Dopt=value` + +If the build directory already exists, options are updated with their new value +given on the command line (`-Dopt=value`). Unless `--reconfigure` is also specified, +this won't reconfigure immediately. This has the same behaviour as +`meson configure -Dopt=value`. + +Previous Meson versions were simply a no-op. + +## Clear persistent cache with `meson setup --clearcache` + +Just like `meson configure --clearcache`, it is now possible to clear the cache +and reconfigure in a single command with `meson setup --clearcache --reconfigure `. + +## pkg-config dependencies can now get a variable with multiple replacements + +When using [[dep.get_variable]] and defining a `pkgconfig_define`, it is +sometimes useful to remap multiple dependency variables. For example, if the +upstream project changed the variable name that is interpolated and it is +desirable to support both versions. + +It is now possible to pass multiple pairs of variable/value. + +The same applies to the compatibility [[dep.get_pkgconfig_variable]] method. + +## Machine files: `pkgconfig` field deprecated and replaced by `pkg-config` + +Meson used to allow both `pkgconfig` and `pkg-config` entries in machine files, +the former was used for `dependency()` lookup and the latter was used as return +value for `find_program('pkg-config')`. + +This inconsistency is now fixed by deprecating `pkgconfig` in favor of +`pkg-config` which matches the name of the binary. For backward compatibility +it is still allowed to define both with the same value, in that case no +deprecation warning is printed. + +## Support targeting Python's limited C API + +The Python module's `extension_module` function has gained the ability +to build extensions which target Python's limited C API via a new keyword +argument: `limited_api`. + +## All compiler `has_*` methods support the `required` keyword + +Now instead of + +```meson +assert(cc.has_function('some_function')) +assert(cc.has_type('some_type')) +assert(cc.has_member('struct some_type', 'x')) +assert(cc.has_members('struct some_type', ['x', 'y'])) +``` + +we can use + +```meson +cc.has_function('some_function', required: true) +cc.has_type('some_type', required: true) +cc.has_member('struct some_type', 'x', required: true) +cc.has_members('struct some_type', ['x', 'y'], required: true) +``` + +## Deprecated `rust_crate_type` and replaced by `rust_abi` + +The new `rust_abi` keyword argument is accepted by [[shared_library]], +[[static_library]], [[library]] and [[shared_module]] functions. It can be either +`'rust'` (the default) or `'c'` strings. + +`rust_crate_type` is now deprecated because Meson already knows if it's a shared +or static library, user only need to specify the ABI (Rust or C). + +`proc_macro` crates are now handled by the [`rust.proc_macro()`](Rust-module.md#proc_macro) +method. + +## Tests now abort on errors by default under sanitizers + +Sanitizers like AddressSanitizer and UndefinedBehaviorSanitizer do not abort +by default on detected violations. Meson now exports `ASAN_OPTIONS` and `UBSAN_OPTIONS` +when unset in the environment to provide sensible abort-by-default behavior. + +## `_(shared|static)_args` for both_library, library, and build_target + +We now allow passing arguments like `c_static_args` and `c_shared_args`. This +allows a [[both_libraries]] to have arguments specific to either the shared or +static library, as well as common arguments to both. + +There is a drawback to this, since Meson now cannot re-use object files between +the static and shared targets. This could lead to much higher compilation time +when using a [[both_libraries]] if there are many sources. + +## `-j` shorthand for `--num-processes` + +`-j` now means the same thing as `--num-processes`. It was inconsistently +supported only in some subcommands. Now you may use it everywhere + +## Unified message(), str.format() and f-string formatting + +They now all support the same set of values: strings, integers, bools, options, +dictionaries and lists thereof. + +- Feature options (i.e. enabled, disabled, auto) were not previously supported + by any of those functions. +- Lists and dictionaries were not previously supported by f-string. +- str.format() allowed any type and often resulted in printing the internal + representation which is now deprecated. + +## Subprojects excluded from scan-build reports + +The `scan-build` target, created when using the `ninja` backend with `scan-build` +present, now excludes bugs found in subprojects from its final report. + +## vs_module_defs keyword now supports indexes of custom_target + +This means you can do something like: +```meson +defs = custom_target('generate_module_defs', ...) +shared_library('lib1', vs_module_defs : defs[0]) +shared_library('lib2', vs_module_defs : defs[2]) +``` + +## Automatic fallback to `cmake` and `cargo` subproject + +CMake subprojects have been supported for a while using the `cmake.subproject()` +module method. However until now it was not possible to use a CMake subproject +as fallback in a `dependency()` call. + +A wrap file can now specify the method used to build it by setting the `method` +key in the wrap file's first section. The method defaults to `meson`. + +Supported methods: +- `meson` requires `meson.build` file. +- `cmake` requires `CMakeLists.txt` file. [See details](Wrap-dependency-system-manual.md#cmake-wraps). +- `cargo` requires `Cargo.toml` file. [See details](Wrap-dependency-system-manual.md#cargo-wraps). + diff --git a/docs/markdown/Release-procedure.md b/docs/markdown/Release-procedure.md index a7ef689..4a6e7f8 100644 --- a/docs/markdown/Release-procedure.md +++ b/docs/markdown/Release-procedure.md @@ -24,7 +24,7 @@ Before a major release is made a stable branch will be made, and will be made, and all bugs effecting the RC will be assigned to this milestone. Patches fixing bugs in the milestone will be picked to the stable branch, and normal development will continue on the master -branch. Every week after after this a new release candidate will be +branch. Every week after this a new release candidate will be made until all bugs are resolved in that milestone. When all of the bugs are fixed the 0.X.0 release will be made. diff --git a/docs/markdown/Rewriter.md b/docs/markdown/Rewriter.md index 535093b..82f8635 100644 --- a/docs/markdown/Rewriter.md +++ b/docs/markdown/Rewriter.md @@ -26,7 +26,7 @@ mode", on the other hand, is meant to be used by external programs The rewriter itself is considered stable, however the user interface and the "script mode" API might change in the future. These changes -may also break backwards comaptibility to older releases. +may also break backwards compatibility to older releases. We are also open to suggestions for API improvements. diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index 031b62a..0fb9ede 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -18,7 +18,11 @@ like Meson, rather than Meson work more like rust. ## Functions -### test(name: string, target: library | executable, dependencies: []Dependency) +### test() + +```meson +rustmod.test(name, target, ...) +``` This function creates a new rust unittest target from an existing rust based target, which may be a library or executable. It does this by @@ -26,33 +30,38 @@ copying the sources and arguments passed to the original target and adding the `--test` argument to the compilation, then creates a new test target which calls that executable, using the rust test protocol. -This accepts all of the keyword arguments as the -[[test]] function except `protocol`, it will set -that automatically. +This function takes two positional arguments, the first is the name of the +test and the second is the library or executable that is the rust based target. +It also takes the following keyword arguments: -Additional, test only dependencies may be passed via the dependencies -argument. +- `dependencies`: a list of test-only Dependencies +- `link_with`: a list of additional build Targets to link with (*since 1.2.0*) +- `rust_args`: a list of extra arguments passed to the Rust compiler (*since 1.2.0*) -### bindgen(*, input: string | BuildTarget | [](string | BuildTarget), output: string, include_directories: [](include_directories | string), c_args: []string, args: []string, dependencies: []Dependency) +This function also accepts all of the keyword arguments accepted by the +[[test]] function except `protocol`, it will set that automatically. + +### bindgen() This function wraps bindgen to simplify creating rust bindings around C -libraries. This has two advantages over hand-rolling ones own with a +libraries. This has two advantages over invoking bindgen with a `generator` or `custom_target`: - It handles `include_directories`, so one doesn't have to manually convert them to `-I...` - It automatically sets up a depfile, making the results more reliable +- It automatically handles assertions, synchronizing Rust and C/C++ to have the same behavior It takes the following keyword arguments -- input — A list of Files, Strings, or CustomTargets. The first element is +- `input`: a list of Files, Strings, or CustomTargets. The first element is the header bindgen will parse, additional elements are dependencies. -- output — the name of the output rust file -- include_directories — A list of `include_directories` or `string` objects, +- `output`: the name of the output rust file +- `include_directories`: A list of `include_directories` or `string` objects, these are passed to clang as `-I` arguments *(string since 1.0.0)* -- c_args — A list of string arguments to pass to clang untouched -- args — A list of string arguments to pass to `bindgen` untouched. -- dependencies — A list of `Dependency` objects to pass to the underlying clang call (*since 1.0.0*) +- `c_args`: a list of string arguments to pass to clang untouched +- `args`: a list of string arguments to pass to `bindgen` untouched. +- `dependencies`: a list of `Dependency` objects to pass to the underlying clang call (*since 1.0.0*) ```meson rust = import('unstable-rust') @@ -68,7 +77,7 @@ generated = rust.bindgen( ) ``` -If the header depeneds on generated headers, those headers must be passed to +If the header depends on generated headers, those headers must be passed to `bindgen` as well to ensure proper dependency ordering, static headers do not need to be passed, as a proper depfile is generated: @@ -81,3 +90,47 @@ r1 = rust.bindgen( output : 'out.rs', ) ``` + +*Since 1.1.0* Meson will synchronize assertions for Rust and C/C++ when the +`b_ndebug` option is set (via `-DNDEBUG` for C/C++, and `-C +debug-assertions=on` for Rust), and will pass `-DNDEBUG` as an extra argument +to clang. This allows for reliable wrapping of `-DNDEBUG` controlled behavior +with `#[cfg(debug_asserions)]` and or `cfg!()`. Before 1.1.0, assertions for Rust +were never turned on by Meson. + +*Since 1.2.0* Additional arguments to pass to clang may be specified in a +*machine file in the properties section: + +```ini +[properties] +bindgen_clang_arguments = ['--target', 'x86_64-linux-gnu'] +``` + +### proc_macro() + +```meson +rustmod.proc_macro(name, sources, ...) +``` + +*Since 1.3.0* + +This function creates a Rust `proc-macro` crate, similar to: +```meson +[[shared_library]](name, sources, + rust_crate_type: 'proc-macro', + native: true) +``` + +`proc-macro` targets can be passed to `link_with` keyword argument of other Rust +targets. + +Only a subset of [[shared_library]] keyword arguments are allowed: +- rust_args +- rust_dependency_map +- sources +- dependencies +- extra_files +- link_args +- link_depends +- link_with +- override_options diff --git a/docs/markdown/Rust.md b/docs/markdown/Rust.md index b7e36e3..151aac0 100644 --- a/docs/markdown/Rust.md +++ b/docs/markdown/Rust.md @@ -65,3 +65,11 @@ be configured to use the file as it's not in the source root (Meson does not write files into the source directory). [See the upstream docs](https://rust-analyzer.github.io/manual.html#non-cargo-based-projects) for more information on how to configure that. + +## Linking with standard libraries + +Meson will link the Rust standard libraries (e.g. libstd) statically, unless the +target is a proc macro or dylib, or it depends on a dylib, in which case [`-C +prefer-dynamic`](https://doc.rust-lang.org/rustc/codegen-options/index.html#prefer-dynamic) +will be passed to the Rust compiler, and the standard libraries will be +dynamically linked. diff --git a/docs/markdown/Shipping-prebuilt-binaries-as-wraps.md b/docs/markdown/Shipping-prebuilt-binaries-as-wraps.md index ebbe34e..a49f45e 100644 --- a/docs/markdown/Shipping-prebuilt-binaries-as-wraps.md +++ b/docs/markdown/Shipping-prebuilt-binaries-as-wraps.md @@ -14,19 +14,26 @@ library at the top level and headers in a subdirectory called `include`. The Meson build definition would look like the following. ```meson -project('binary dep', 'c') +project('bob', 'c') + +# Do some sanity checking so that meson can fail early instead of at final link time +if not (host_machine.system() == 'windows' and host_machine.cpu_family() == 'x86_64') + error('This wrap of libbob is a binary wrap for x64_64 Windows, and will not work on your system') +endif cc = meson.get_compiler('c') -bin_dep = declare_dependency( +bob_dep = declare_dependency( dependencies : cc.find_library('bob', dirs : meson.current_source_dir()), include_directories : include_directories('include')) + +meson.override_dependency('bob', bob_dep) ``` Now you can use this subproject as if it was a Meson project: ```meson project('using dep', 'c') -bob_dep = subproject('bob').get_variable('bin_dep') +bob_dep = dependency('bob') executable('prog', 'prog.c', dependencies : bob_dep) ``` @@ -35,6 +42,33 @@ compiler flags) might not be compatible. If you do this, then you are responsible for verifying that your libraries are compatible, Meson will not check things for you. +## Using a wrap file + +To make this all work automatically, a project will need a +[wrap file](Wrap-dependency-system-manual.md#wrap-format), as well as the +meson.build definition from above. For this example our dependency is called +`bob`. + +The wrap ini (subprojects/bob.wrap): +```ini +[wrap-file] +directory = libbob-1.0 +source_url = https://libbob.example.com/libbob-1.0.zip +source_filename = libbob-1.0.zip +source_hash = 5ebeea0dfb75d090ea0e7ff84799b2a7a1550db3fe61eb5f6f61c2e971e57663 +patch_directory = libbob + +[provide] +dependency_names = bob +``` + +Then create `subprojects/packagefiles/libbob/`, and place the `meson.build` from +above in that directory. With these in place a call to `dependency('bob')` will +first try standard discovery methods for your system (such as pkg-config, cmake, +and any built-in meson find methods), and then fall back to using the binary +wrap if it cannot find the dependency on the system. Meson provides the +`--force-fallback-for=bob` command line option to force the use of the fallback. + ## Note for Linux libraries A precompiled linux shared library (.so) requires a soname field to be properly installed. If the soname field is missing, binaries referencing the library will require a hard link to the location of the library at install time (`/path/to/your/project/subprojects/precompiledlibrary/lib.so` instead of `$INSTALL_PREFIX/lib/lib.so`) after installation. diff --git a/docs/markdown/Simple-comparison.md b/docs/markdown/Simple-comparison.md index a8ce17b..47faa79 100644 --- a/docs/markdown/Simple-comparison.md +++ b/docs/markdown/Simple-comparison.md @@ -29,7 +29,7 @@ how much time the build system takes to check the states of all source files because if any of them could potentially cause a rebuild. Since CMake has two different backends, Make and Ninja, we ran the -tests on both of them. All tests were run on a 2011 era Macbook Pro +tests on both of them. All tests were run on a 2011 era MacBook Pro running Ubuntu 13/04. The tests were run multiple times and we always took the fastest time. diff --git a/docs/markdown/SourceSet-module.md b/docs/markdown/SourceSet-module.md index 26c1995..70dca20 100644 --- a/docs/markdown/SourceSet-module.md +++ b/docs/markdown/SourceSet-module.md @@ -47,7 +47,7 @@ if zlib.found() then dependencies += [zlib] endif # many more "if"s here... -executable('exe', sources: sources, dependencies: dependencies()) +executable('exe', sources: sources, dependencies: dependencies) ``` Sourcesets can be used with a single invocation of the `apply` method, diff --git a/docs/markdown/Subprojects.md b/docs/markdown/Subprojects.md index 09ff8bf..78239b9 100644 --- a/docs/markdown/Subprojects.md +++ b/docs/markdown/Subprojects.md @@ -219,7 +219,7 @@ the following command-line options: * **--wrap-mode=nodownload** Meson will not use the network to download any subprojects or - fetch any wrap information. Only pre-existing sources will be used. + fetch any wrap information. Only preexisting sources will be used. This is useful (mostly for distros) when you want to only use the sources provided by a software release, and want to manually handle or provide missing dependencies. diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md index 708fc25..59ec5f7 100644 --- a/docs/markdown/Syntax.md +++ b/docs/markdown/Syntax.md @@ -109,7 +109,7 @@ Strings in Meson are declared with single quotes. To enter a literal single quote do it like this: ```meson -single quote = 'contains a \' character' +single_quote = 'contains a \' character' ``` The full list of escape sequences is: @@ -252,13 +252,13 @@ s = s.replace('as', 'are') #### .strip() ```meson -# Similar to the Python str.strip(). Removes leading/ending spaces and newlines +# Similar to the Python str.strip(). Removes leading/ending spaces and newlines. define = ' -Dsomedefine ' stripped_define = define.strip() # 'stripped_define' now has the value '-Dsomedefine' # You may also pass a string to strip, which specifies the set of characters to -# be removed. +# be removed instead of the default whitespace. string = 'xyxHelloxyx'.strip('xy') # 'string' now has the value 'Hello' ``` @@ -454,6 +454,15 @@ Keys must be unique: my_dict = {'foo': 42, 'foo': 43} ``` +Accessing elements of a dictionary works similarly to array indexing: + +```meson +my_dict = {'foo': 42, 'bar': 'baz'} +forty_two = my_dict['foo'] +# This will fail +my_dict['does_not_exist'] +``` + Dictionaries are immutable and do not have a guaranteed order. Dictionaries are available since 0.47.0. @@ -766,8 +775,8 @@ additive_expression: multiplicative_expression | (additive_expression additive_o additive_operator: "+" | "-" argument_list: positional_arguments ["," keyword_arguments] | keyword_arguments array_literal: "[" [expression_list] "]" -assignment_statement: expression asssignment_operator expression -assignment_operator: "=" | "*=" | "/=" | "%=" | "+=" | "-=" +assignment_statement: expression assignment_operator expression +assignment_operator: "=" | "+=" binary_literal: "0b" BINARY_NUMBER BINARY_NUMBER: /[01]+/ boolean_literal: "true" | "false" @@ -815,5 +824,5 @@ STRING_MULTILINE_VALUE: \.*?(''')\ STRING_SIMPLE_VALUE: \.*?(?_dep` syntax should be used, where +`` is the name of a CMake library with all non alphanumeric +characters replaced by underscores `_`. + +For example, a CMake project that contains `add_library(foo-bar ...)` in its +`CMakeList.txt` and that applications would usually find using the dependency +name `foo-bar-1.0` (e.g. via pkg-config) would have a wrap file like this: + +```ini +[wrap-file] +... +method = cmake +[provide] +foo-bar-1.0 = foo_bar_dep +``` +### Cargo wraps + +Cargo subprojects automatically override the `-rs` dependency name. +`package_name` is defined in `[package] name = ...` section of the `Cargo.toml` +and `-rs` suffix is added. That means the `.wrap` file should have +`dependency_names = foo-rs` in their `[provide]` section when `Cargo.toml` has +package name `foo`. + +Cargo subprojects require a toml parser. Python >= 3.11 have one built-in, older +Python versions require either the external `tomli` module or `toml2json` program. + +For example, a Cargo project with the package name `foo-bar` would have a wrap +file like that: +```ini +[wrap-file] +... +method = cargo +[provide] +dependency_names = foo-bar-rs +``` + ## Using wrapped projects Wraps provide a convenient way of obtaining a project into your diff --git a/docs/markdown/Yaml-RefMan.md b/docs/markdown/Yaml-RefMan.md index 1bb800f..2872791 100644 --- a/docs/markdown/Yaml-RefMan.md +++ b/docs/markdown/Yaml-RefMan.md @@ -34,7 +34,7 @@ To link to functions, the function name should be put into the tag: `[[]]`. Methods (for all kinds of objects, including modules) can be linked to like this: `[[.]]`. -To link to objects themself, the `[[@]]` syntax can be used. +To link to objects themselves, the `[[@]]` syntax can be used. These tags do **not** need to be put in inline code! A hotdoc extension handles the formatting here. If tags need to be placed (for instance, to include reference @@ -69,7 +69,7 @@ module has its own directory. The module itself **must** be in a file called `module.yaml`. All objects returned by the module are then located next to this file. -The name of the YAML files themself are ignored (with the exception of +The name of the YAML files themselves are ignored (with the exception of `module.yaml`) and carry no specific meaning. However, it is recommended to name the YAML files after the `name` entry of the object. @@ -81,7 +81,7 @@ is to make inheriting functions and arguments easier. # YAML schema -The YAML files themself are structured as follows: +The YAML files themselves are structured as follows: ## Functions diff --git a/docs/markdown/_include_qt_base.md b/docs/markdown/_include_qt_base.md index e0d4869..0a11924 100644 --- a/docs/markdown/_include_qt_base.md +++ b/docs/markdown/_include_qt_base.md @@ -71,7 +71,7 @@ This method takes the following keyword arguments: - `qresources` (string | File)[]: Passed to the RCC compiler - `ui_files`: (string | File | CustomTarget)[]: Passed the `uic` compiler - `moc_sources`: (string | File | CustomTarget)[]: Passed the `moc` compiler. These are converted into .moc files meant to be `#include`ed - - `moc_headers`: (string | File | CustomTarget)[]: Passied the `moc` compiler. These will be converted into .cpp files + - `moc_headers`: (string | File | CustomTarget)[]: Passed the `moc` compiler. These will be converted into .cpp files - `include_directories` (IncludeDirectories | string)[], the directories to add to header search path for `moc` - `moc_extra_arguments` string[]: any additional arguments to `moc`. Since v0.44.0. - `uic_extra_arguments` string[]: any additional arguments to `uic`. Since v0.49.0. @@ -145,7 +145,7 @@ qt5 = import('qt5') qt5_dep = dependency('qt5', modules: ['Core', 'Gui']) inc = include_directories('includes') moc_files = qt5.compile_moc(headers : 'myclass.h', - extra_arguments: ['-DMAKES_MY_MOC_HEADER_COMPILE'], + extra_args: ['-DMAKES_MY_MOC_HEADER_COMPILE'], include_directories: inc, dependencies: qt5_dep) translations = qt5.compile_translations(ts_files : 'myTranslation_fr.ts', build_by_default : true) diff --git a/docs/markdown/howtox.md b/docs/markdown/howtox.md index 651f089..79a87b7 100644 --- a/docs/markdown/howtox.md +++ b/docs/markdown/howtox.md @@ -197,7 +197,7 @@ This causes all subsequent builds to use this command line argument. ## Use address sanitizer -Clang comes with a selection of analysis tools such as the [address +Clang and gcc come with a selection of analysis tools such as the [address sanitizer](https://clang.llvm.org/docs/AddressSanitizer.html). Meson has native support for these with the `b_sanitize` option. diff --git a/docs/markdown/i18n-module.md b/docs/markdown/i18n-module.md index a1efa5a..a939a34 100644 --- a/docs/markdown/i18n-module.md +++ b/docs/markdown/i18n-module.md @@ -27,7 +27,7 @@ argument which is the name of the gettext module. * `preset`: (*Added 0.37.0*) name of a preset list of arguments, current option is `'glib'`, see [source](https://github.com/mesonbuild/meson/blob/master/mesonbuild/modules/i18n.py) - for for their value + for their value * `install`: (*Added 0.43.0*) if false, do not install the built translations. * `install_dir`: (*Added 0.50.0*) override default install location, default is `localedir` diff --git a/docs/meson.build b/docs/meson.build index a14055c..3ad12b7 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -1,22 +1,16 @@ project('Meson documentation', version: '1.0') -cur_bdir = meson.current_build_dir() +yaml_modname = get_option('unsafe_yaml') ? 'yaml' : 'strictyaml' +py = import('python').find_installation('python3', modules: [yaml_modname], required: false) +if not py.found() + error(f'Cannot build documentation without yaml support') +endif -# Only the script knows which files are being generated -docs_gen = custom_target( - 'gen_docs', - input: files('markdown/index.md'), - output: 'gen_docs.stamp', - command: [ - files('../tools/regenerate_docs.py'), - '--output-dir', cur_bdir, - '--dummy-output-file', '@OUTPUT@', - ], - build_by_default: true, - install: false) +cur_bdir = meson.current_build_dir() sitemap = files('sitemap.txt') +yaml_loader = get_option('unsafe_yaml') ? 'fastyaml' : 'yaml' genrefman = find_program('./genrefman.py') refman_binary = custom_target( 'gen_refman_bin', @@ -25,7 +19,7 @@ refman_binary = custom_target( depfile: 'reman_dep.d', command: [ genrefman, - '-l', 'yaml', + '-l', yaml_loader, '-g', 'pickle', '-o', '@OUTPUT@', '--depfile', '@DEPFILE@', @@ -33,24 +27,6 @@ refman_binary = custom_target( ] ) -refman_md = custom_target( - 'gen_refman_md', - input: refman_binary, - output: ['configured_sitemap.txt', 'refman_links.json'], - command: [ - genrefman, - '-l', 'pickle', - '-g', 'md', - '-s', sitemap, - '-i', '@INPUT@', - '-o', '@OUTPUT0@', - '--link-defs', '@OUTPUT1@', - '--force-color', - '--no-modules', - ], -) -sitemap = refman_md[0] - refman_json = custom_target( 'gen_refman_json', build_by_default: true, @@ -65,6 +41,7 @@ refman_json = custom_target( '--force-color', ], ) +test('validate_docs', find_program('./jsonvalidator.py'), args: [refman_json]) refman_man = custom_target( 'gen_refman_man', @@ -80,8 +57,47 @@ refman_man = custom_target( '--force-color', '--no-modules', ], + install: true, + install_dir: get_option('mandir') / 'man3', ) +# Everything past here is HTML resources. +if not get_option('html') + subdir_done() +endif + +# Only the script knows which files are being generated +docs_gen = custom_target( + 'gen_docs', + input: files('markdown/index.md'), + output: 'gen_docs.stamp', + command: [ + files('../tools/regenerate_docs.py'), + '--output-dir', cur_bdir, + '--dummy-output-file', '@OUTPUT@', + ], + build_by_default: true, + install: false, +) + +refman_md = custom_target( + 'gen_refman_md', + input: refman_binary, + output: ['configured_sitemap.txt', 'refman_links.json'], + command: [ + genrefman, + '-l', 'pickle', + '-g', 'md', + '-s', sitemap, + '-i', '@INPUT@', + '-o', '@OUTPUT0@', + '--link-defs', '@OUTPUT1@', + '--force-color', + '--no-modules', + ], +) +sitemap = refman_md[0] + genrelnotes = custom_target( output: ['sitemap-genrelnotes.txt'], build_always_stale: true, @@ -94,9 +110,11 @@ genrelnotes = custom_target( ) sitemap = genrelnotes[0] -test('validate_docs', find_program('./jsonvalidator.py'), args: [refman_json]) - hotdoc_prog = find_program('hotdoc', version: '>=0.13.7') +py = import('python').find_installation('python3', modules: ['chevron'], required: false) +if not py.found() + error('Building the HTML docs requires the chevron module to render generated markdown pages') +endif hotdoc = import('hotdoc') documentation = hotdoc.generate_doc(meson.project_name(), @@ -116,6 +134,7 @@ documentation = hotdoc.generate_doc(meson.project_name(), keep_markup_in_code_blocks: true, extra_extension: meson.current_source_dir() / 'extensions' / 'refman_links.py', refman_data_file: refman_md[1], + fatal_warnings: true, ) run_target('upload', diff --git a/docs/meson_options.txt b/docs/meson_options.txt new file mode 100644 index 0000000..bc0ec55 --- /dev/null +++ b/docs/meson_options.txt @@ -0,0 +1,4 @@ +option('unsafe_yaml', type: 'boolean', value: false, + description: 'disable safety checks and use a faster, but less correct YAML loader') +option('html', type: 'boolean', value: true, + description: 'build the hotdoc-based HTML documentation') diff --git a/docs/refman/generatorjson.py b/docs/refman/generatorjson.py index d41cb71..a2edc18 100644 --- a/docs/refman/generatorjson.py +++ b/docs/refman/generatorjson.py @@ -1,5 +1,6 @@ # SPDX-License-Identifer: Apache-2.0 # Copyright 2021 The Meson development team +from __future__ import annotations from pathlib import Path import json diff --git a/docs/refman/generatormd.py b/docs/refman/generatormd.py index 9723034..79029c1 100644 --- a/docs/refman/generatormd.py +++ b/docs/refman/generatormd.py @@ -105,7 +105,7 @@ class GeneratorMD(GeneratorBase): def _link_to_object(self, obj: T.Union[Function, Object], in_code_block: bool = False) -> str: ''' - Generate a palaceholder tag for the the function/method/object documentation. + Generate a palaceholder tag for the function/method/object documentation. This tag is then replaced in the custom hotdoc plugin. ''' prefix = '#' if in_code_block else '' @@ -161,6 +161,9 @@ class GeneratorMD(GeneratorBase): # I know, this regex is ugly but it works. return len(re.sub(r'\[\[(#|@)*([^\[])', r'\2', s)) + def arg_anchor(arg: ArgBase) -> str: + return f'{func.name}_{arg.name.replace("<", "_").replace(">", "_")}' + def render_signature() -> str: # Skip a lot of computations if the function does not take any arguments if not any([func.posargs, func.optargs, func.kwargs, func.varargs]): @@ -184,12 +187,15 @@ class GeneratorMD(GeneratorBase): max_name_len = max([len(x.name) for x in all_args]) # Generate some common strings - def prepare(arg: ArgBase) -> T.Tuple[str, str, str, str]: + def prepare(arg: ArgBase, link: bool = True) -> T.Tuple[str, str, str, str]: type_str = render_type(arg.type, True) type_len = len_stripped(type_str) type_space = ' ' * (max_type_len - type_len) name_space = ' ' * (max_name_len - len(arg.name)) name_str = f'{arg.name.replace("<", "<").replace(">", ">")}' + if link: + name_str = f'{name_str}' + return type_str, type_space, name_str, name_space for i in func.posargs: @@ -201,7 +207,7 @@ class GeneratorMD(GeneratorBase): signature += f' {type_str}{type_space} [{name_str}],{name_space} # {self.brief(i)}\n' if func.varargs: - type_str, type_space, name_str, name_space = prepare(func.varargs) + type_str, type_space, name_str, name_space = prepare(func.varargs, link=False) signature += f' {type_str}{type_space} {name_str}...,{name_space} # {self.brief(func.varargs)}\n' # Abort if there are no kwargs @@ -227,6 +233,7 @@ class GeneratorMD(GeneratorBase): def gen_arg_data(arg: T.Union[PosArg, Kwarg, VarArgs], *, optional: bool = False) -> T.Dict[str, PlaceholderTypes]: data: T.Dict[str, PlaceholderTypes] = { + 'row-id': arg_anchor(arg), 'name': arg.name, 'type': render_type(arg.type), 'description': arg.description, @@ -281,6 +288,7 @@ class GeneratorMD(GeneratorBase): def _write_object(self, obj: Object) -> None: data = { 'name': obj.name, + 'title': obj.long_name if obj.obj_type == ObjectType.RETURNED else obj.name, 'description': obj.description, 'notes': obj.notes, 'warnings': obj.warnings, diff --git a/docs/refman/generatorprint.py b/docs/refman/generatorprint.py index d836091..d805296 100644 --- a/docs/refman/generatorprint.py +++ b/docs/refman/generatorprint.py @@ -19,7 +19,7 @@ from mesonbuild import mlog import typing as T def my_nested() -> T.ContextManager[None]: - prefix = '|' * len(mlog.log_depth) + prefix = '|' * mlog.get_log_depth() return mlog.nested(prefix) class GeneratorPrint(GeneratorBase): diff --git a/docs/refman/generatorvim.py b/docs/refman/generatorvim.py new file mode 100644 index 0000000..ea72574 --- /dev/null +++ b/docs/refman/generatorvim.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifer: Apache-2.0 +# Copyright 2023 The Meson development team +from __future__ import annotations + +from pathlib import Path + +from .generatorbase import GeneratorBase +from .model import ReferenceManual + + +class GeneratorVim(GeneratorBase): + def __init__(self, manual: ReferenceManual, out_dir: Path) -> None: + super().__init__(manual) + self.out_dir = out_dir + + def generate(self) -> None: + template_dir = Path(__file__).resolve().parent / 'templates' + outname = 'meson.vim' + template_name = f'{outname}.mustache' + template_file = template_dir / template_name + + builtin_funcs = [f.name for f in self.sorted_and_filtered(self.functions)] + data = { + 'builtin_funcs': '\n \\ '.join(builtin_funcs) + } + + # Import here, so that other generators don't also depend on it + import chevron + result = chevron.render( + template=template_file.read_text(encoding='utf-8'), + data=data, + warn=True, + ) + + self.out_dir.mkdir(parents=True, exist_ok=True) + out_file = self.out_dir / outname + out_file.write_text(result, encoding='utf-8') diff --git a/docs/refman/jsonschema.py b/docs/refman/jsonschema.py index e64bf06..283d3a2 100644 --- a/docs/refman/jsonschema.py +++ b/docs/refman/jsonschema.py @@ -10,79 +10,82 @@ import typing as T VERSION_MAJOR = 1 # Changes here indicate breaking format changes (changes to existing keys) VERSION_MINOR = 1 # Changes here indicate non-breaking changes (only new keys are added to the existing structure) -class BaseObject(T.TypedDict): - ''' - Base object for most dicts in the JSON doc. +if T.TYPE_CHECKING: + from typing_extensions import TypedDict - All objects inheriting from BaseObject will support - the keys specified here: - ''' - name: str - description: str - since: T.Optional[str] - deprecated: T.Optional[str] - notes: T.List[str] - warnings: T.List[str] + class BaseObject(TypedDict): + ''' + Base object for most dicts in the JSON doc. -class Type(T.TypedDict): - obj: str # References an object from `root.objects` - holds: T.Sequence[object] # Mypy does not support recursive dicts, but this should be T.List[Type]... + All objects inheriting from BaseObject will support + the keys specified here: + ''' + name: str + description: str + since: T.Optional[str] + deprecated: T.Optional[str] + notes: T.List[str] + warnings: T.List[str] -class Argument(BaseObject): - ''' - Object that represents any type of a single function or method argument. - ''' - type: T.List[Type] # A non-empty list of types that are supported. - type_str: str # Formatted version of `type`. Is guaranteed to not contain any whitespaces. - required: bool - default: T.Optional[str] - min_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments - max_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments + class Type(TypedDict): + obj: str # References an object from `root.objects` + holds: T.Sequence[object] # Mypy does not support recursive dicts, but this should be T.List[Type]... -class Function(BaseObject): - ''' - Represents a function or method. - ''' - returns: T.List[Type] # A non-empty list of types that are supported. - returns_str: str # Formatted version of `returns`. Is guaranteed to not contain any whitespaces. - example: T.Optional[str] - posargs: T.Dict[str, Argument] - optargs: T.Dict[str, Argument] - kwargs: T.Dict[str, Argument] - varargs: T.Optional[Argument] - arg_flattening: bool + class Argument(BaseObject): + ''' + Object that represents any type of a single function or method argument. + ''' + type: T.List[Type] # A non-empty list of types that are supported. + type_str: str # Formatted version of `type`. Is guaranteed to not contain any whitespaces. + required: bool + default: T.Optional[str] + min_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments + max_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments -class Object(BaseObject): - ''' - Represents all types of Meson objects. The specific object type is stored in the `object_type` field. - ''' - example: T.Optional[str] - object_type: str # Defines the object type: Must be one of: ELEMENTARY, BUILTIN, MODULE, RETURNED - methods: T.Dict[str, Function] - is_container: bool - extends: T.Optional[str] - returned_by: T.List[str] - extended_by: T.List[str] - defined_by_module: T.Optional[str] + class Function(BaseObject): + ''' + Represents a function or method. + ''' + returns: T.List[Type] # A non-empty list of types that are supported. + returns_str: str # Formatted version of `returns`. Is guaranteed to not contain any whitespaces. + example: T.Optional[str] + posargs: T.Dict[str, Argument] + optargs: T.Dict[str, Argument] + kwargs: T.Dict[str, Argument] + varargs: T.Optional[Argument] + arg_flattening: bool -class ObjectsByType(T.TypedDict): - ''' - References to other objects are stored here for ease of navigation / filtering - ''' - elementary: T.List[str] - builtins: T.List[str] - returned: T.List[str] - modules: T.Dict[str, T.List[str]] + class Object(BaseObject): + ''' + Represents all types of Meson objects. The specific object type is stored in the `object_type` field. + ''' + example: T.Optional[str] + object_type: str # Defines the object type: Must be one of: ELEMENTARY, BUILTIN, MODULE, RETURNED + methods: T.Dict[str, Function] + is_container: bool + extends: T.Optional[str] + returned_by: T.List[str] + extended_by: T.List[str] + defined_by_module: T.Optional[str] + class ObjectsByType(TypedDict): + ''' + References to other objects are stored here for ease of navigation / filtering + ''' + elementary: T.List[str] + builtins: T.List[str] + returned: T.List[str] + modules: T.Dict[str, T.List[str]] -class Root(T.TypedDict): - ''' - The root object of the JSON reference manual - ''' - version_major: int # See the description above for - version_minor: int # VERSION_MAJOR and VERSION_MINOR - meson_version: str - functions: T.Dict[str, Function] # A mapping of to a `Function` object for *all* Meson functions - objects: T.Dict[str, Object] # A mapping of to a `Object` object for *all* Meson objects (including modules, elementary, etc.) - objects_by_type: ObjectsByType + + class Root(TypedDict): + ''' + The root object of the JSON reference manual + ''' + version_major: int # See the description above for + version_minor: int # VERSION_MAJOR and VERSION_MINOR + meson_version: str + functions: T.Dict[str, Function] # A mapping of to a `Function` object for *all* Meson functions + objects: T.Dict[str, Object] # A mapping of to a `Object` object for *all* Meson objects (including modules, elementary, etc.) + objects_by_type: ObjectsByType diff --git a/docs/refman/loaderbase.py b/docs/refman/loaderbase.py index 3011126..e64134f 100644 --- a/docs/refman/loaderbase.py +++ b/docs/refman/loaderbase.py @@ -108,7 +108,7 @@ class _Resolver: for obj in func.returns.resolved: obj.data_type.returned_by += [func] - # Handle kwargs inehritance + # Handle kwargs inheritance for base_name in func.kwargs_inherit: base_name = base_name.strip() assert base_name in self.func_map, f'Unknown base function `{base_name}` for {func.name}' @@ -123,7 +123,7 @@ class _Resolver: missing = {k: v for k, v in base.kwargs.items() if k in base_keys - curr_keys} func.kwargs.update(missing) - # Handloe other args inheritance + # Handle other args inheritance _T = T.TypeVar('_T', bound=T.Union[ArgBase, T.List[PosArg]]) def resolve_inherit(name: str, curr: _T, resolver: T.Callable[[Function], _T]) -> _T: if name and not curr: diff --git a/docs/refman/main.py b/docs/refman/main.py index 5727c20..9a3d16a 100644 --- a/docs/refman/main.py +++ b/docs/refman/main.py @@ -28,13 +28,14 @@ from .generatorprint import GeneratorPrint from .generatorpickle import GeneratorPickle from .generatormd import GeneratorMD from .generatorman import GeneratorMan +from .generatorvim import GeneratorVim meson_root = Path(__file__).absolute().parents[2] def main() -> int: parser = argparse.ArgumentParser(description='Meson reference manual generator') parser.add_argument('-l', '--loader', type=str, default='yaml', choices=['yaml', 'fastyaml', 'pickle'], help='Information loader backend') - parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md', 'json', 'man'], required=True, help='Generator backend') + parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md', 'json', 'man', 'vim'], required=True, help='Generator backend') parser.add_argument('-s', '--sitemap', type=Path, default=meson_root / 'docs' / 'sitemap.txt', help='Path to the input sitemap.txt') parser.add_argument('-o', '--out', type=Path, required=True, help='Output directory for generated files') parser.add_argument('-i', '--input', type=Path, default=meson_root / 'docs' / 'yaml', help='Input path for the selected loader') @@ -66,6 +67,7 @@ def main() -> int: 'md': lambda: GeneratorMD(refMan, args.out, args.sitemap, args.link_defs, not args.no_modules), 'json': lambda: GeneratorJSON(refMan, args.out, not args.no_modules), 'man': lambda: GeneratorMan(refMan, args.out, not args.no_modules), + 'vim': lambda: GeneratorVim(refMan, args.out), } generator = generators[args.generator]() diff --git a/docs/refman/templates/args.mustache b/docs/refman/templates/args.mustache index f3ee84b..67cca2a 100644 --- a/docs/refman/templates/args.mustache +++ b/docs/refman/templates/args.mustache @@ -13,7 +13,7 @@ {{#args}} - + {{name}} {{&type}} diff --git a/docs/refman/templates/meson.vim.mustache b/docs/refman/templates/meson.vim.mustache new file mode 100644 index 0000000..d8f009e --- /dev/null +++ b/docs/refman/templates/meson.vim.mustache @@ -0,0 +1,103 @@ +" Vim syntax file +" Language: Meson +" License: VIM License +" Maintainer: Nirbheek Chauhan +" Liam Beguin +" Last Change: 2023 Aug 27 +" Credits: Zvezdan Petkovic +" Neil Schemenauer +" Dmitry Vasiliev +" +" This version is copied and edited from python.vim +" It's very basic, and doesn't do many things I'd like it to +" For instance, it should show errors for syntax that is valid in +" Python but not in Meson. +" +" Optional highlighting can be controlled using these variables. +" +" let meson_space_error_highlight = 1 +" + +if exists("b:current_syntax") + finish +endif + +" We need nocompatible mode in order to continue lines with backslashes. +" Original setting will be restored. +let s:cpo_save = &cpo +set cpo&vim + +" http://mesonbuild.com/Syntax.html +syn keyword mesonConditional elif else if endif +syn keyword mesonRepeat foreach endforeach +syn keyword mesonOperator and not or in +syn keyword mesonStatement continue break + +syn match mesonComment "#.*$" contains=mesonTodo,@Spell +syn keyword mesonTodo FIXME NOTE NOTES TODO XXX contained + +" Strings can either be single quoted or triple counted across multiple lines, +" but always with a ' +syn region mesonString + \ start="\z('\)" end="\z1" skip="\\\\\|\\\z1" + \ contains=mesonEscape,@Spell +syn region mesonString + \ start="\z('''\)" end="\z1" keepend + \ contains=mesonEscape,mesonSpaceError,@Spell + +syn match mesonEscape "\\[abfnrtv'\\]" contained +syn match mesonEscape "\\\o\{1,3}" contained +syn match mesonEscape "\\x\x\{2}" contained +syn match mesonEscape "\%(\\u\x\{4}\|\\U\x\{8}\)" contained +" Meson allows case-insensitive Unicode IDs: http://www.unicode.org/charts/ +syn match mesonEscape "\\N{\a\+\%(\s\a\+\)*}" contained +syn match mesonEscape "\\$" + +" Meson only supports integer numbers +" http://mesonbuild.com/Syntax.html#numbers +syn match mesonNumber "\<\d\+\>" +syn match mesonNumber "\<0x\x\+\>" +syn match mesonNumber "\<0o\o\+\>" + +" booleans +syn keyword mesonBoolean false true + +" Built-in functions +syn keyword mesonBuiltin + \ build_machine + \ host_machine + \ meson + \ option + \ target_machine + \ {{builtin_funcs}} + +if exists("meson_space_error_highlight") + " trailing whitespace + syn match mesonSpaceError display excludenl "\s\+$" + " mixed tabs and spaces + syn match mesonSpaceError display " \+\t" + syn match mesonSpaceError display "\t\+ " +endif + +" The default highlight links. Can be overridden later. +hi def link mesonStatement Statement +hi def link mesonConditional Conditional +hi def link mesonRepeat Repeat +hi def link mesonOperator Operator +hi def link mesonComment Comment +hi def link mesonTodo Todo +hi def link mesonString String +hi def link mesonEscape Special +hi def link mesonNumber Number +hi def link mesonBuiltin Function +hi def link mesonBoolean Boolean +if exists("meson_space_error_highlight") + hi def link mesonSpaceError Error +endif + +let b:current_syntax = "meson" + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim:set sw=2 sts=2 ts=8 noet: diff --git a/docs/refman/templates/object.mustache b/docs/refman/templates/object.mustache index ec86034..3b6c679 100644 --- a/docs/refman/templates/object.mustache +++ b/docs/refman/templates/object.mustache @@ -1,6 +1,6 @@ --- short-description: "{{obj_type_name}} object: {{long_name}}" -title: {{name}}{{#extends}} (extends {{.}}){{/extends}} +title: {{title}} render-subpages: false ... # {{long_name}} (`{{name}}`{{#extends}} extends [[@{{.}}]]{{/extends}}) diff --git a/docs/sitemap.txt b/docs/sitemap.txt index 4494afe..5eb2284 100644 --- a/docs/sitemap.txt +++ b/docs/sitemap.txt @@ -88,6 +88,9 @@ index.md Wrap-best-practices-and-tips.md Shipping-prebuilt-binaries-as-wraps.md Release-notes.md + Release-notes-for-1.3.0.md + Release-notes-for-1.2.0.md + Release-notes-for-1.1.0.md Release-notes-for-1.0.0.md Release-notes-for-0.64.0.md Release-notes-for-0.63.0.md diff --git a/docs/yaml/builtins/meson.yaml b/docs/yaml/builtins/meson.yaml index 8eb3aca..0d4eacf 100644 --- a/docs/yaml/builtins/meson.yaml +++ b/docs/yaml/builtins/meson.yaml @@ -14,7 +14,7 @@ methods: distribution source has been generated but before it is archived. Note that this runs the script file that is in the _staging_ directory, not the one in the source directory. If the - script file can not be found in the staging directory, it is a hard + script file cannot be found in the staging directory, it is a hard error. The `MESON_DIST_ROOT` environment variables is set when dist scripts is run. @@ -64,6 +64,9 @@ methods: *(since 0.54.0)* If `meson install` is called with the `--quiet` option, the environment variable `MESON_INSTALL_QUIET` will be set. + *(since 1.1.0)* If `meson install` is called with the `--dry-run` option, the + environment variable `MESON_INSTALL_DRY_RUN` will be set. + Meson uses the `DESTDIR` environment variable as set by the inherited environment to determine the (temporary) installation location for files. Your install script must be aware of this while @@ -125,6 +128,15 @@ methods: to install only a subset of the files. By default the script has no install tag which means it is not being run when `meson install --tags` argument is specified. + + dry_run: + type: bool + since: 1.1.0 + default: false + description: | + If `true` the script will be run even if `--dry-run` option is provided to + the `meson install` command. The script can use the `MESON_INSTALL_DRY_RUN` + variable to determine if it is in dry run mode or not. - name: add_postconf_script returns: void @@ -152,6 +164,21 @@ methods: - `vs2022` - `xcode` + - name: build_options + returns: str + since: 1.1.0 + description: | + Returns a string with the configuration line used to set the current project up. + notes: + - | + **Do not try to parse this string!** + + You should use [[cfg_data.set_quoted]] to safely escape any embedded + quotes prior to storing it into e.g. a C header macro. + + The contents returned by this function are the same as the + "Build Options:" line reported in `/meson-logs/meson-log.txt`. + - name: build_root returns: str deprecated: 0.56.0 @@ -206,7 +233,7 @@ methods: returns: str since: 0.58.0 description: | - Returns a string with the absolute path to the source root directory + Returns a string with the absolute path to the source root directory. This function will return the source root of the main project if called from a subproject, which is usually not what you want. It is usually preferable to use [[meson.current_source_dir]] or [[meson.project_source_root]]. @@ -317,8 +344,15 @@ methods: returns: void description: | Installs a manifest file - containing a list of all subprojects, their versions and license - files to the file name given as the argument. + containing a list of all subprojects, their versions and license names + to the file name given as the argument. + + If license files are defined as well, they will be copied next to the + manifest and referenced in it. + + If this function is not used, the builtin option `licensedir` can + be used to install the manifest to a given directory with the name + `depmf.json`. posargs: output_name: @@ -406,6 +440,11 @@ methods: since: 0.45.0 description: Returns the array of licenses specified in [[project]] function call. + - name: project_license_files + returns: list[file] + since: 1.1.0 + description: Returns the array of license files specified in the [[project]] function call. + - name: project_name returns: str description: Returns the project name specified in the [[project]] function call. diff --git a/docs/yaml/elementary/dict.yml b/docs/yaml/elementary/dict.yml index f3ca837..19263df 100644 --- a/docs/yaml/elementary/dict.yml +++ b/docs/yaml/elementary/dict.yml @@ -8,8 +8,9 @@ description: | You can also iterate over dictionaries with the [`foreach` statement](Syntax.md#foreach-statements). - *(since 0.48.0)* Dictionaries can be added (e.g. `d1 = d2 + d3` and `d1 += d2`). + *(since 0.48.0)*: Dictionaries can be added (e.g. `d1 = d2 + d3` and `d1 += d2`). Values from the second dictionary overrides values from the first. + *(since 0.62.0)*: Dictionary order is guaranteed to be insertion order. methods: diff --git a/docs/yaml/elementary/int.yml b/docs/yaml/elementary/int.yml index 65ab959..f8d8e25 100644 --- a/docs/yaml/elementary/int.yml +++ b/docs/yaml/elementary/int.yml @@ -14,3 +14,15 @@ methods: - name: to_string returns: str description: Returns the value of the number as a string. + + optargs: + fill: + type: int + description: | + Left fill the string with zeros until it reaches the length + specified by this argument. A leading negative sign counts towards + the length, and is handled by inserting the padding after the `-` + character rather than before. The original string is returned if the + value provided is less than or equal to the former's length. + + diff --git a/docs/yaml/elementary/str.yml b/docs/yaml/elementary/str.yml index 7b60e1e..83ab3dd 100644 --- a/docs/yaml/elementary/str.yml +++ b/docs/yaml/elementary/str.yml @@ -14,6 +14,10 @@ methods: See [the Meson syntax entry](Syntax.md#string-formatting) for more information. + + *Since 1.3.0* values other than strings, integers, bools, options, + dictionaries and lists thereof are deprecated. They were previously printing + the internal representation of the raw Python object. example: | ```meson template = 'string: @0@, number: @1@, bool: @2@' @@ -39,7 +43,7 @@ methods: # str.replace(old, new) - name: replace - description: Search all occurrences of `old` and and replace it with `new` + description: Search all occurrences of `old` and replace it with `new` returns: str since: 0.58.0 example: | @@ -61,7 +65,10 @@ methods: # str.strip() - name: strip - description: Removes leading/ending spaces and newlines from the string. + description: | + Removes leading/ending characters from the string. + + By default the characters to remove are spaces and newlines. returns: str example: | ```meson @@ -75,7 +82,7 @@ methods: strip_chars: type: str since: 0.43.0 - description: All characters in this string will be stripped. + description: Instead of whitespace, strip all the characters in this string. # str.to_lower() - name: to_lower @@ -204,6 +211,28 @@ methods: type: str description: Specifies the character / substring where to split the string. +- name: splitlines + returns: list[str] + since: 1.2.0 + description: | + Splits the string into an array of lines. + Unlike .split('\n'), the empty string produced an empty array, + and if the string ends in a newline, splitlines() doesn't split + on that last newline. + '\n', '\r' and '\r\n' are all considered newlines. + + example: | + ```meson + output = 'hello\nworld\n'.splitlines() + # Output value is ['hello', 'world'] + output = ''.splitlines() + # Output value is [] + fs = import('fs') + paths = fs.read('my_paths.list').splitlines() + # paths is now the paths listed in 'my_paths.list', or an empty list + # if 'my_paths.list' is empty + ``` + # str.join() - name: join returns: str diff --git a/docs/yaml/functions/_build_target_base.yaml b/docs/yaml/functions/_build_target_base.yaml index 46eedc1..1db49a5 100644 --- a/docs/yaml/functions/_build_target_base.yaml +++ b/docs/yaml/functions/_build_target_base.yaml @@ -48,6 +48,11 @@ kwargs: compiler flags to use for the given language; eg: `cpp_args` for C++ + vala_args: + type: list[str | file] + description: | + Compiler flags for Vala. Unlike other languages this may contain Files + sources: type: str | file | custom_tgt | custom_idx | generated_list | structured_src description: Additional source files. Same as the source varargs. @@ -89,7 +94,7 @@ kwargs: default: false description: | When set to true flags this target as a GUI application - on platforms where this makes a differerence, **deprecated** since + on platforms where this makes a difference, **deprecated** since 0.56.0, use `win_subsystem` instead. link_args: @@ -123,8 +128,10 @@ kwargs: type: list[lib | custom_tgt | custom_idx] since: 0.40.0 description: | - Links all contents of the given static libraries - whether they are used by not, equivalent to the `-Wl,--whole-archive` argument flag of GCC. + Links all contents of the given static libraries whether they are used or + not, equivalent to the `-Wl,--whole-archive` argument flag of GCC, or the + '/WHOLEARCHIVE' MSVC linker option. This allows the linked target to + re-export symbols from all objects in the static libraries. *(since 0.41.0)* If passed a list that list will be flattened. @@ -194,8 +201,11 @@ kwargs: type: list[extracted_obj | file | str] description: | List of object files that should be linked in this target. - These can include third party products you don't have source to, - or object files produced by other build targets. + + **Since 1.1.0** this can include generated files in addition to + object files that you don't have source to or that object files + produced by other build targets. In earlier release, generated + object files had to be placed in `sources`. name_prefix: type: str | list[void] @@ -225,12 +235,13 @@ kwargs: Set this to `[]`, or omit the keyword argument for the default behaviour. override_options: - type: list[str] + type: list[str] | dict[str | bool | int | list[str]] since: 0.40.0 description: | takes an array of strings in the same format as `project`'s `default_options` overriding the values of these options for this target only. + *(since 1.2.0)*: A dictionary may now be passed. gnu_symbol_visibility: type: str @@ -283,6 +294,7 @@ kwargs: rust_crate_type: type: str since: 0.42.0 + deprecated: 1.3.0 description: | Set the specific type of rust crate to compile (when compiling rust). @@ -296,6 +308,20 @@ kwargs: If it is a [[shared_library]] it defaults to "lib", and may be "lib", "dylib", "cdylib", or "proc-macro". If "lib" then Rustc will pick a default, "cdylib" means a C ABI library, "dylib" means a Rust ABI, and - "proc-macro" is a special rust proceedural macro crate. + "proc-macro" is a special rust procedural macro crate. "proc-macro" is new in 0.62.0. + + *Since 1.3.0* this is deprecated and replaced by "rust_abi" keyword argument. + `proc_macro` crates are now handled by the [`rust.proc_macro()`](Rust-module.md#proc_macro) + method. + + rust_dependency_map: + type: dict[str] + since: 1.2.0 + description: | + On rust targets this provides a map of library names to the crate name + with which it would be available inside the rust code. + + This allows renaming similar to the dependency renaming feature of cargo + or `extern crate foo as bar` inside rust code. diff --git a/docs/yaml/functions/add_languages.yaml b/docs/yaml/functions/add_languages.yaml index 6851c4e..3a77225 100644 --- a/docs/yaml/functions/add_languages.yaml +++ b/docs/yaml/functions/add_languages.yaml @@ -36,7 +36,7 @@ varargs: kwargs: required: - type: bool + type: bool | feature default: true description: | If set to `true`, Meson will halt if any of the languages diff --git a/docs/yaml/functions/add_project_link_arguments.yaml b/docs/yaml/functions/add_project_link_arguments.yaml index 8ae4763..e259b6c 100644 --- a/docs/yaml/functions/add_project_link_arguments.yaml +++ b/docs/yaml/functions/add_project_link_arguments.yaml @@ -1,9 +1,9 @@ name: add_project_link_arguments returns: void description: | - Adds global arguments to the linker command line. + Adds project specific arguments to the linker command line. - Like [[add_global_arguments]] but the arguments are passed to the linker. + Like [[add_project_arguments]] but the arguments are passed to the linker. notes: - You must pass always arguments individually `arg1, arg2, ...` @@ -14,4 +14,4 @@ varargs: name: Linker argument description: The linker arguments to add -kwargs_inherit: add_global_arguments +kwargs_inherit: add_project_arguments diff --git a/docs/yaml/functions/build_target.yaml b/docs/yaml/functions/build_target.yaml index 48385f2..74d45f0 100644 --- a/docs/yaml/functions/build_target.yaml +++ b/docs/yaml/functions/build_target.yaml @@ -12,7 +12,7 @@ description: | - `static_library` (see [[static_library]]) - `both_libraries` (see [[both_libraries]]) - `library` (see [[library]]) - - `jar` (see [[jar]]) + - `jar` (see [[jar]])* This declaration: @@ -32,6 +32,9 @@ description: | The returned object also has methods that are documented in [[@build_tgt]]. + *"jar" is deprecated because it is fundementally a different thing than the + other build_target types. + posargs_inherit: _build_target_base varargs_inherit: _build_target_base kwargs_inherit: @@ -42,4 +45,4 @@ kwargs_inherit: kwargs: target_type: type: str - description: The actual target to build + description: The actual target type to build diff --git a/docs/yaml/functions/configure_file.yaml b/docs/yaml/functions/configure_file.yaml index 8f4d9e0..34cb3c1 100644 --- a/docs/yaml/functions/configure_file.yaml +++ b/docs/yaml/functions/configure_file.yaml @@ -20,6 +20,9 @@ description: | this function will copy the file provided in `input:` to a file in the build directory with the name `output:` in the current directory. +warnings: + - the `install_mode` kwarg ignored integer values between 0.62 -- 1.1.0. + kwargs: capture: type: bool @@ -131,7 +134,7 @@ kwargs: The format of the output to generate when no input was specified. It defaults to `c`, in which case preprocessor directives will be prefixed with `#`, you can also use `nasm`, in which case the - prefix will be `%`. + prefix will be `%`. *(since 1.3.0)* `json` format can also be used. encoding: type: str @@ -141,3 +144,10 @@ kwargs: Set the file encoding for the input and output file. The supported encodings are those of python3, see [standard-encodings](https://docs.python.org/3/library/codecs.html#standard-encodings). + + macro_name: + type: str + since: 1.3.0 + description: | + When specified, macro guards will be used instead of '#pragma once'. The + macro guard name will be the specified name. diff --git a/docs/yaml/functions/custom_target.yaml b/docs/yaml/functions/custom_target.yaml index 7d05282..caccd48 100644 --- a/docs/yaml/functions/custom_target.yaml +++ b/docs/yaml/functions/custom_target.yaml @@ -52,6 +52,9 @@ notes: is not portable, notably to Windows. Instead, consider using a `native: true` [[executable]], or a python script. +warnings: + - the `install_mode` kwarg ignored integer values between 0.60.0 -- 1.1.0. + optargs: name: type: str diff --git a/docs/yaml/functions/declare_dependency.yaml b/docs/yaml/functions/declare_dependency.yaml index 7524447..9d085fd 100644 --- a/docs/yaml/functions/declare_dependency.yaml +++ b/docs/yaml/functions/declare_dependency.yaml @@ -43,6 +43,13 @@ kwargs: (or generated header files that should be built before sources including them are built) + extra_files: + type: list[str | file] + since: 1.2.0 + description: | + extra files to add to targets. + mostly used for IDE integration. + version: type: str description: | @@ -71,3 +78,10 @@ kwargs: description: | the directories to add to the string search path (i.e. `-J` switch for DMD). Must be [[@inc]] objects or plain strings. + + objects: + type: list[extracted_obj] + since: 1.1.0 + description: | + a list of object files, to be linked directly into the targets that use the + dependency. diff --git a/docs/yaml/functions/dependency.yaml b/docs/yaml/functions/dependency.yaml index 2d9e366..5ae1634 100644 --- a/docs/yaml/functions/dependency.yaml +++ b/docs/yaml/functions/dependency.yaml @@ -78,14 +78,15 @@ varargs: kwargs: default_options: - type: list[str] + type: list[str] | dict[str | bool | int | list[str]] since: 0.38.0 description: | An array of default option values - that override those set in the subproject's `meson_options.txt` + that override those set in the subproject's `meson.options` (like `default_options` in [[project]], they only have effect when Meson is run for the first time, and command line arguments override any default options in build files) + *(since 1.2.0)*: A dictionary may now be passed. allow_fallback: type: bool diff --git a/docs/yaml/functions/executable.yaml b/docs/yaml/functions/executable.yaml index cdf764a..abbc5fe 100644 --- a/docs/yaml/functions/executable.yaml +++ b/docs/yaml/functions/executable.yaml @@ -10,6 +10,9 @@ description: | The returned object also has methods that are documented in [[@exe]]. + *Since 1.3.0* executable names can be the same across multiple targets as + long as they each have a different `name_suffix`. + warnings: - The `link_language` kwarg was broken until 0.55.0 @@ -44,3 +47,13 @@ kwargs: type: bool since: 0.49.0 description: Build a position-independent executable. + + vs_module_defs: + type: str | file | custom_tgt | custom_idx + since: 1.3.0 + description: | + Specify a Microsoft module definition file for controlling symbol exports, + etc., on platforms where that is possible (e.g. Windows). + + This can be used to expose which functions a shared_module loaded by an + executable will be allowed to use. diff --git a/docs/yaml/functions/find_program.yaml b/docs/yaml/functions/find_program.yaml index c0c13d2..3153bd3 100644 --- a/docs/yaml/functions/find_program.yaml +++ b/docs/yaml/functions/find_program.yaml @@ -42,6 +42,9 @@ description: | setcap = find_program(['setcap', '/usr/sbin/setcap', '/sbin/setcap'], required : false) ``` + *Since 1.2.0* `find_program('meson')` is automatically overridden to the Meson + command used to execute the build script. + The returned [[@external_program]] object also has documented methods. posargs: @@ -110,3 +113,13 @@ kwargs: type: list[str] since: 0.53.0 description: extra list of absolute paths where to look for program names. + + default_options: + type: list[str] | dict[str | bool | int | list[str]] + since: 1.3.0 + description: | + An array of default option values + that override those set in the subproject's `meson.options` + (like `default_options` in [[project]], they only have + effect when Meson is run for the first time, and command line + arguments override any default options in build files) diff --git a/docs/yaml/functions/install_data.yaml b/docs/yaml/functions/install_data.yaml index 191c612..ff4f336 100644 --- a/docs/yaml/functions/install_data.yaml +++ b/docs/yaml/functions/install_data.yaml @@ -10,6 +10,11 @@ varargs: type: file | str description: Files to install. +warnings: + - the `install_mode` kwarg ignored integer values between 0.59.0 -- 1.1.0. + - an omitted `install_dir` kwarg did not work correctly inside of a subproject until 1.3.0. + - an omitted `install_dir` kwarg did not work correctly when combined with the `preserve_path` kwarg untill 1.3.0. + kwargs: install_dir: type: str @@ -64,3 +69,11 @@ kwargs: sources: type: list[file | str] description: Additional files to install. + + follow_symlinks: + type: bool + since: 1.3.0 + default: true + description: | + If true, dereferences links and copies their target instead. The default + value will become false in the future. diff --git a/docs/yaml/functions/install_emptydir.yaml b/docs/yaml/functions/install_emptydir.yaml index e598b5e..df84f60 100644 --- a/docs/yaml/functions/install_emptydir.yaml +++ b/docs/yaml/functions/install_emptydir.yaml @@ -6,6 +6,9 @@ description: | argument. If the directory exists and is not empty, the contents are left in place. +warnings: + - the `install_mode` kwarg ignored integer values before 1.1.0. + varargs: name: dirpath type: str diff --git a/docs/yaml/functions/install_headers.yaml b/docs/yaml/functions/install_headers.yaml index 50e1c55..0ac4fc5 100644 --- a/docs/yaml/functions/install_headers.yaml +++ b/docs/yaml/functions/install_headers.yaml @@ -41,6 +41,9 @@ varargs: type: file | str description: Header files to install. +warnings: + - the `install_mode` kwarg ignored integer values between 0.59.0 -- 1.1.0. + kwargs: install_dir: type: str @@ -67,6 +70,14 @@ kwargs: since: 0.63.0 default: false description: | - Disable stripping child-direcories from header files when installing. + Disable stripping child-directories from header files when installing. This is equivalent to GNU Automake's `nobase` option. + + follow_symlinks: + type: bool + since: 1.3.0 + default: true + description: | + If true, dereferences links and copies their target instead. The default + value will become false in the future. diff --git a/docs/yaml/functions/install_man.yaml b/docs/yaml/functions/install_man.yaml index b695dc1..8d9ba60 100644 --- a/docs/yaml/functions/install_man.yaml +++ b/docs/yaml/functions/install_man.yaml @@ -15,6 +15,9 @@ varargs: type: file | str description: Man pages to install. +warnings: + - the `install_mode` kwarg ignored integer values between 0.59.0 -- 1.1.0. + kwargs: install_mode: type: list[str | int] diff --git a/docs/yaml/functions/install_subdir.yaml b/docs/yaml/functions/install_subdir.yaml index 90baed2..19abee3 100644 --- a/docs/yaml/functions/install_subdir.yaml +++ b/docs/yaml/functions/install_subdir.yaml @@ -56,6 +56,9 @@ example: | new_directory/ ``` +warnings: + - the `install_mode` kwarg ignored integer values between 0.59.0 -- 1.1.0. + posargs: subdir_name: type: str @@ -103,3 +106,11 @@ kwargs: description: | Install directory contents. If `strip_directory=true` only the last component of the source path is used. + + follow_symlinks: + type: bool + since: 1.3.0 + default: true + description: | + If true, dereferences links and copies their target instead. The default + value will become false in the future. diff --git a/docs/yaml/functions/library.yaml b/docs/yaml/functions/library.yaml index f10ef8e..1d406f1 100644 --- a/docs/yaml/functions/library.yaml +++ b/docs/yaml/functions/library.yaml @@ -16,8 +16,53 @@ description: | The keyword arguments for this are the same as for [[build_target]] +warnings: + - using _shared_args and/or _static_args may lead to much higher + compilation times with both_library, as object files cannot be shared between + the static and shared targets. It is guaranteed to not duplicate the build if + these arguments are empty arrays + posargs_inherit: _build_target_base varargs_inherit: _build_target_base kwargs_inherit: - shared_library - static_library + +kwargs: + rust_abi: + type: str + since: 1.3.0 + description: | + Set the specific ABI to compile (when compiling rust). + - 'rust' (default): Create a "rlib" or "dylib" crate depending on the library + type being build. + - 'c': Create a "cdylib" or "staticlib" crate depending on the library + type being build. + + _static_args: + type: list[str] + since: 1.3.0 + description: + Arguments that are only passed to a static library + + vala_static_args: + type: list[str | file] + since: 1.3.0 + description: + Arguments that are only passed to a static library + + Like `vala_args`, [[files]] is allowed in addition to string + + _shared_args: + type: list[str] + since: 1.3.0 + description: + Arguments that are only passed to a shared library + + vala_shared_args: + type: list[str | file] + since: 1.3.0 + description: + Arguments that are only passed to a shared library + + Like `vala_args`, [[files]] is allowed in addition to string diff --git a/docs/yaml/functions/project.yaml b/docs/yaml/functions/project.yaml index ba33efc..5be8cac 100644 --- a/docs/yaml/functions/project.yaml +++ b/docs/yaml/functions/project.yaml @@ -22,8 +22,9 @@ description: | *(since 0.56.0)* The build machine compilers for the specified languages are not required. - Supported values for languages are `c`, `cpp` (for `C++`), `cuda`, `d`, - `objc`, `objcpp`, `fortran`, `java`, `cs` (for `C#`), `vala` and `rust`. + Supported values for languages are `c`, `cpp` (for `C++`), `cuda`, + `cython`, `d`, `objc`, `objcpp`, `fortran`, `java`, `cs` (for `C#`), + `vala` and `rust`. posargs: project_name: @@ -37,9 +38,9 @@ varargs: kwargs: default_options: - type: list[str] + type: list[str] | dict[str | bool | int | list[str]] description: | - Accecpts strings in the form `key=value` + Accepts strings in the form `key=value` which have the same format as options to `meson configure`. For example to set the default project type you would set this: `default_options : ['buildtype=debugoptimized']`. Note @@ -48,6 +49,13 @@ kwargs: the master project, settings in subprojects are ignored. Project specific options are used normally even in subprojects. + Note that some options can override the default behavior; + for example, using `c_args` here means that the `CFLAGS` + environment variable is not used. Consider using + [[add_project_arguments()]] instead. + + *(since 1.2.0)*: A dictionary may now be passed. + version: type: str | file description: | @@ -76,7 +84,7 @@ kwargs: For backwards compatibility reasons you can also pass an array of licenses here. This is not recommended, as it is ambiguous: `license : - ['Apache-2.0', 'GPL-2.0-only']` instead use an SPDX espression: `license + ['Apache-2.0', 'GPL-2.0-only']` instead use an SPDX expression: `license : 'Apache-2.0 OR GPL-2.0-only'`, which makes it clear that the license mean OR, not AND. @@ -85,6 +93,22 @@ kwargs: for verifying that you abide by all licensing terms. You can access the value in your Meson build files with `meson.project_license()`. + license_files: + type: str | list[str] + since: 1.1.0 + description: | + Takes a string or array of strings with the paths to the license file(s) + the code is under. + + This enhances the value of the `license` kwarg by allowing to specify both + the short license name and the full license text. Usually this would be + something like `license_files: ['COPYING']`. + + Note that the files are informal and are only installed with the dependency + manifest. Meson does not do any license validation, you are responsible + for verifying that you abide by all licensing terms. You can access the + value in your Meson build files with [[meson.project_license_files]]. + subproject_dir: type: str default: "'subprojects'" diff --git a/docs/yaml/functions/shared_library.yaml b/docs/yaml/functions/shared_library.yaml index 956fb2c..f633aca 100644 --- a/docs/yaml/functions/shared_library.yaml +++ b/docs/yaml/functions/shared_library.yaml @@ -44,3 +44,13 @@ kwargs: description: | Specify a Microsoft module definition file for controlling symbol exports, etc., on platforms where that is possible (e.g. Windows). + + *(Since 1.3.0)* [[@custom_idx]] are supported + + rust_abi: + type: str + since: 1.3.0 + description: | + Set the specific ABI to compile (when compiling rust). + - 'rust' (default): Create a "dylib" crate. + - 'c': Create a "cdylib" crate. diff --git a/docs/yaml/functions/shared_module.yaml b/docs/yaml/functions/shared_module.yaml index 20bd5c4..6b94e56 100644 --- a/docs/yaml/functions/shared_module.yaml +++ b/docs/yaml/functions/shared_module.yaml @@ -39,3 +39,13 @@ kwargs: description: | Specify a Microsoft module definition file for controlling symbol exports, etc., on platforms where that is possible (e.g. Windows). + + *(Since 1.3.0)* [[@custom_idx]] are supported + + rust_abi: + type: str + since: 1.3.0 + description: | + Set the specific ABI to compile (when compiling rust). + - 'rust' (default): Create a "dylib" crate. + - 'c': Create a "cdylib" crate. diff --git a/docs/yaml/functions/static_library.yaml b/docs/yaml/functions/static_library.yaml index 1d42d60..615baa2 100644 --- a/docs/yaml/functions/static_library.yaml +++ b/docs/yaml/functions/static_library.yaml @@ -23,3 +23,11 @@ kwargs: If `true` the object files in the target will be prelinked, meaning that it will contain only one prelinked object file rather than the individual object files. + + rust_abi: + type: str + since: 1.3.0 + description: | + Set the specific ABI to compile (when compiling rust). + - 'rust' (default): Create a "rlib" crate. + - 'c': Create a "staticlib" crate. diff --git a/docs/yaml/functions/subproject.yaml b/docs/yaml/functions/subproject.yaml index 4d19a31..bccac79 100644 --- a/docs/yaml/functions/subproject.yaml +++ b/docs/yaml/functions/subproject.yaml @@ -9,11 +9,12 @@ description: | `${MESON_SOURCE_ROOT}/subprojects/foo`. - `default_options` *(since 0.37.0)*: an array of default option values - that override those set in the subproject's `meson_options.txt` + that override those set in the subproject's `meson.options` (like `default_options` in `project`, they only have effect when Meson is run for the first time, and command line arguments override - any default options in build files). *(since 0.54.0)*: `default_library` - built-in option can also be overridden. + any default options in build files). + *(since 0.54.0)*: `default_library` built-in option can also be overridden. + *(since 1.2.0)*: A dictionary may be passed instead of array. - `version`: works just like the same as in `dependency`. It specifies what version the subproject should be, as an example `>=1.0.1` - `required` *(since 0.48.0)*: By default, `required` is `true` and @@ -41,15 +42,16 @@ posargs: kwargs: default_options: - type: list[str] + type: list[str] | dict[str | bool | int | list[str]] since: 0.37.0 description: | An array of default option values - that override those set in the subproject's `meson_options.txt` + that override those set in the subproject's `meson.options` (like `default_options` in [[project]], they only have effect when Meson is run for the first time, and command line arguments override - any default options in build files). *(since 0.54.0)*: `default_library` - built-in option can also be overridden. + any default options in build files). + *(since 0.54.0)*: `default_library` built-in option can also be overridden. + *(since 1.2.0)*: A dictionary may now be passed. version: type: str diff --git a/docs/yaml/functions/test.yaml b/docs/yaml/functions/test.yaml index 4e79167..622b7c3 100644 --- a/docs/yaml/functions/test.yaml +++ b/docs/yaml/functions/test.yaml @@ -33,6 +33,10 @@ description: | test(..., env: nomalloc, ...) ``` + By default, the environment variables `ASAN_OPTIONS` and `UBSAN_OPTIONS` are + set to enable aborting on detected violations and to give a backtrace. To suppress + this, `ASAN_OPTIONS` and `UBSAN_OPTIONS` can be set in the environment. + In addition to running individual executables as test cases, `test()` can also be used to invoke an external test harness. In this case, it is best to use `verbose: true` *(since 0.62.0)* and, if supported diff --git a/docs/yaml/objects/build_tgt.yaml b/docs/yaml/objects/build_tgt.yaml index 2dec753..73b9b5d 100644 --- a/docs/yaml/objects/build_tgt.yaml +++ b/docs/yaml/objects/build_tgt.yaml @@ -13,8 +13,8 @@ methods: source files. This is typically used to take single object files and link them to unit tests or to compile some source files with custom flags. To use the object file(s) in another build target, use the - `objects:` keyword argument to a [[build_target]] or include them in the command - line of a [[custom_target]]. + `objects:` keyword argument to a [[build_target]] or [[declare_dependency]], + or include them in the command line of a [[custom_target]]. varargs: name: source type: str | file @@ -53,7 +53,7 @@ methods: deprecated: 0.59.0 description: | Does the exact same as [[build_tgt.full_path]]. **NOTE**: This - function is solely kept for compatebility with [[@external_program]] objects. + function is solely kept for compatibility with [[@external_program]] objects. It will be removed once the, also deprecated, corresponding `path()` function in the [[@external_program]] object is removed. diff --git a/docs/yaml/objects/compiler.yaml b/docs/yaml/objects/compiler.yaml index 7224011..239a9bc 100644 --- a/docs/yaml/objects/compiler.yaml +++ b/docs/yaml/objects/compiler.yaml @@ -127,6 +127,7 @@ methods: - compiler._dependencies - compiler._no_builtin_args - compiler._name + - compiler._werror - name: _header returns: void @@ -143,6 +144,29 @@ methods: When set to a [`feature`](Build-options.md#features) option, the feature will control if it is searched and whether to fail if not found. +- name: _required + returns: void + description: You have found a bug if you can see this! + kwargs: + required: + type: bool | feature + default: false + since: 1.3.0 + description: + When set to `true`, Meson will halt if the check fails. + + When set to a [`feature`](Build-options.md#features) option, the feature + will control if it is searched and whether to fail if not found. + +- name: _werror + returns: void + description: You have found a bug if you can see this! + kwargs: + werror: + type: bool + default: false + description: When set to `true`, compiler warnings are treated as error. + since: 1.3.0 # Star of the actual functions - name: version @@ -156,7 +180,11 @@ methods: - name: alignment returns: int - description: Returns the alignment of the specified type. + description: | + Returns the alignment of the specified type. For C-like languages, + For C-like languages, the header `stddef.h` and `stdio.h` are included + implicitly for native compilation, only `stddef.h` is included when + cross-compiling. posargs: typename: @@ -196,7 +224,9 @@ methods: - name: has_member returns: bool description: Returns true if the type has the specified member. - kwargs_inherit: compiler._common + kwargs_inherit: + - compiler._common + - compiler._required posargs: typename: type: str @@ -208,7 +238,9 @@ methods: - name: has_members returns: bool description: Returns `true` if the type has *all* the specified members. - kwargs_inherit: compiler._common + kwargs_inherit: + - compiler._common + - compiler._required posargs: typename: type: str @@ -225,7 +257,9 @@ methods: Returns true if the given function is provided by the standard library or a library passed in with the `args` keyword. - kwargs_inherit: compiler._common + kwargs_inherit: + - compiler._common + - compiler._required posargs: funcname: type: str @@ -234,7 +268,9 @@ methods: - name: has_type returns: bool description: Returns `true` if the specified token is a type. - kwargs_inherit: compiler._common + kwargs_inherit: + - compiler._common + - compiler._required posargs: typename: type: str @@ -251,6 +287,9 @@ methods: (defaults to -1024), `high` (defaults to 1024) and `guess` to specify max and min values for the search and the value to try first. + For C-like languages, the header `stddef.h` and `stdio.h` are included + implicitly for native compilation, only `stddef.h` is included when + cross-compiling. posargs: expr: @@ -272,7 +311,11 @@ methods: - name: sizeof returns: int - description: returns the size of the given type (e.g. `'int'`) or -1 if the type is unknown. + description: | + returns the size of the given type (e.g. `'int'`) or -1 if the type is unknown. + For C-like languages, the header `stddef.h` and `stdio.h` are included + implicitly for native compilation, only `stddef.h` is included when + cross-compiling. kwargs_inherit: compiler._common posargs: typename: @@ -295,6 +338,17 @@ methods: type: str description: The define to check. +- name: has_define + returns: bool + since: 1.3.0 + description: | + Returns true if the given preprocessor symbol is *defined*. + kwargs_inherit: compiler._common + posargs: + definename: + type: str + description: The define to check. + - name: compiles returns: bool description: Returns true if the code compiles. @@ -457,6 +511,8 @@ methods: argument: type: str description: The argument to check. + kwargs_inherit: + - compiler._required - name: has_multi_arguments since: 0.37.0 @@ -469,6 +525,8 @@ methods: name: arg type: str description: The arguments to check. + kwargs_inherit: + - compiler._required - name: get_supported_arguments returns: list[str] @@ -485,7 +543,7 @@ methods: default: "'off'" description: | Supported values: - - `'off'`: Quietely ignore unsupported arguments + - `'off'`: Quietly ignore unsupported arguments - `'warn'`: Print a warning for unsupported arguments - `'require'`: Abort if at least one argument is not supported @@ -515,6 +573,8 @@ methods: argument: type: str description: The argument to check. + kwargs_inherit: + - compiler._required - name: has_multi_link_arguments since: 0.46.0 @@ -527,6 +587,8 @@ methods: name: arg type: str description: The link arguments to check. + kwargs_inherit: + - compiler._required - name: get_supported_link_arguments returns: list[str] @@ -544,7 +606,7 @@ methods: # default: "'off'" # description: | # Supported values: - # - `'off'`: Quietely ignore unsupported arguments + # - `'off'`: Quietly ignore unsupported arguments # - `'warn'`: Print a warning for unsupported arguments # - `'require'`: Abort if at least one argument is not supported @@ -556,10 +618,6 @@ methods: Given a list of strings, returns the first argument that passes the [[compiler.has_link_argument]] test or an empty array if none pass. - - - - - name: has_function_attribute returns: bool since: 0.48.0 @@ -573,6 +631,8 @@ methods: name: type: str description: The attribute name to check. + kwargs_inherit: + - compiler._required - name: get_supported_function_attributes returns: list[str] @@ -617,3 +677,7 @@ methods: type: list[str] description: | Extra flags to pass to the preprocessor + dependencies: + type: dep | list[dep] + description: Additionally dependencies required. + since: 1.1.0 diff --git a/docs/yaml/objects/dep.yaml b/docs/yaml/objects/dep.yaml index d847690..52e28fa 100644 --- a/docs/yaml/objects/dep.yaml +++ b/docs/yaml/objects/dep.yaml @@ -38,6 +38,8 @@ methods: variable by passing a list to this kwarg that can affect the retrieved variable: `['prefix', '/'])`. + *(Since 1.3.0)* Multiple variables can be specified in pairs. + default: type: str since: 0.45.0 @@ -174,6 +176,12 @@ methods: from the object then `default_value` is returned, if it is not set then an error is raised. + warnings: + - Before 1.3.0, specifying multiple pkgconfig_define pairs would silently + malform the results. Only the first variable would be redefined, but + its value would contain both the second variable name, as well as its + value. + optargs: varname: type: str diff --git a/docs/yaml/objects/env.yaml b/docs/yaml/objects/env.yaml index 36d3aba..d784c68 100644 --- a/docs/yaml/objects/env.yaml +++ b/docs/yaml/objects/env.yaml @@ -2,9 +2,8 @@ name: env long_name: Environment description: | This object is returned by [[environment]] and stores - detailed information about how environment variables should be set - during tests. It should be passed as the `env` keyword argument to - tests and other functions. + detailed information about how environment variables should be set. + It should be passed as the `env` keyword argument to tests and other functions. *Since 0.58.0* [[env.append]] and [[env.prepend]] can be called multiple times on the same `varname`. Earlier Meson versions would warn and only the last diff --git a/docs/yaml/objects/external_program.yaml b/docs/yaml/objects/external_program.yaml index f0a5ac0..d175a01 100644 --- a/docs/yaml/objects/external_program.yaml +++ b/docs/yaml/objects/external_program.yaml @@ -18,7 +18,7 @@ methods: **NOTE:** You should not usually need to use this method. Passing the object itself should work in most contexts where a program can appear, and allows Meson to setup inter-target dependencies correctly (for - example in cases where a program might be overridden by a [[build_tgt]]). + example in cases where a program might be overridden by a [[@build_tgt]]). Only use this if you specifically need a string, such as when embedding a program path into a header file. @@ -45,7 +45,7 @@ methods: **NOTE:** You should not usually need to use this method. Passing the object itself should work in most contexts where a program can appear, and allows Meson to setup inter-target dependencies correctly (for - example in cases where a program might be overridden by a [[build_tgt]]). + example in cases where a program might be overridden by a [[@build_tgt]]). Only use this if you specifically need a string, such as when embedding a program path into a header file. diff --git a/docs/yaml/objects/feature.yaml b/docs/yaml/objects/feature.yaml index b6a754b..3e0ae69 100644 --- a/docs/yaml/objects/feature.yaml +++ b/docs/yaml/objects/feature.yaml @@ -27,11 +27,42 @@ methods: description: | Returns the feature, with `'auto'` converted to `'disabled'` if value is true. - | Feature / Condition | `value = true` | `value = false` | - | ------------------- | -------------- | --------------- | - | Enabled | Enabled | Enabled | - | Disabled | Disabled | Disabled | - | Auto | Disabled | Auto | + | Feature | `value = true` | `value = false` | + | -------- | -------------- | --------------- | + | Auto | Disabled | Auto | + | Enabled | Enabled | Enabled | + | Disabled | Disabled | Disabled | + + example: | + `disable_auto_if` is useful to give precedence to mutually exclusive dependencies + (that provide the same API) if either or both are available: + + ``` + # '-Dfoo=auto -Dbar=enabled' will not pick foo even if installed. + use_bar = get_option('bar') + use_foo = get_option('foo').disable_auto_if(use_bar.enabled()) + dep_foo = dependency('foo', required: use_foo) + if not dep_foo.found() + dep_foo = dependency('bar', required: use_bar) + endif + ``` + + posargs: + value: + type: bool + description: See the table above + +- name: enable_auto_if + since: 1.1.0 + returns: feature + description: | + Returns the feature, with `'auto'` converted to `'enabled'` if value is true. + + | Feature | `value = true` | `value = false` | + | -------- | -------------- | --------------- | + | Auto | Enabled | Auto | + | Enabled | Enabled | Enabled | + | Disabled | Disabled | Disabled | posargs: value: @@ -46,6 +77,12 @@ methods: `'enabled'` and the value is false; a disabled feature if the object is `'auto'` or `'disabled'` and the value is false. + | Feature | `value = true` | `value = false` | + | -------- | -------------- | --------------- | + | Auto | Auto | Disabled | + | Enabled | Enabled | Error | + | Disabled | Disabled | Disabled | + example: | `require` is useful to restrict the applicability of `'auto'` features, for example based on other features or on properties of the host machine: @@ -67,4 +104,76 @@ methods: error_message: type: str default: "''" - description: The error Message to print if the check fails + description: The error message to print if the check fails + +- name: enable_if + returns: feature + since: 1.1.0 + description: | + Returns the object itself if the value is false; an error if the object is + `'disabled'` and the value is true; an enabled feature if the object + is `'auto'` or `'enabled'` and the value is true. + + | Feature | `value = true` | `value = false` | + | -------- | -------------- | --------------- | + | Auto | Enabled | Auto | + | Enabled | Enabled | Enabled | + | Disabled | Error | Disabled | + + example: | + `enable_if` is useful to restrict the applicability of `'auto'` features, + particularly when passing them to [[dependency]]: + + ``` + use_llvm = get_option('llvm').enable_if(with_clang).enable_if(with_llvm_libs) + dep_llvm = dependency('llvm', required: use_llvm) + ``` + + posargs: + value: + type: bool + description: The value to check + + kwargs: + error_message: + type: str + default: "''" + description: The error message to print if the check fails + +- name: disable_if + returns: feature + since: 1.1.0 + description: | + Returns the object itself if the value is false; an error if the object is + `'enabled'` and the value is true; a disabled feature if the object + is `'auto'` or `'disabled'` and the value is true. + + | Feature | `value = true` | `value = false` | + | -------- | -------------- | --------------- | + | Auto | Disabled | Auto | + | Enabled | Error | Enabled | + | Disabled | Disabled | Disabled | + + This is equivalent to `feature_opt.require(not condition)`, but may make + code easier to reason about, especially when mixed with `enable_if` + + example: | + `disable_if` is useful to restrict the applicability of `'auto'` features, + particularly when passing them to [[dependency]]: + + ``` + use_os_feature = get_option('foo') \ + .disable_if(host_machine.system() == 'darwin', error_message : 'os feature not supported on MacOS') + dep_os_feature = dependency('os_feature', required: use_os_feature) + ``` + + posargs: + value: + type: bool + description: The value to check + + kwargs: + error_message: + type: str + default: "''" + description: The error message to print if the check fails diff --git a/docs/yaml/objects/generator.yaml b/docs/yaml/objects/generator.yaml index e7b866a..fbef95f 100644 --- a/docs/yaml/objects/generator.yaml +++ b/docs/yaml/objects/generator.yaml @@ -34,3 +34,12 @@ methods: `subdir/one.input` is processed it generates a file `{target private directory}/subdir/one.out` as opposed to `{target private directory}/one.out`. + + env: + type: env | list[str] | dict[str] + since: 1.3.0 + description: | + environment variables to set, such as + `{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`, + or an [[@env]] object which allows more + sophisticated environment juggling. diff --git a/docs/yaml/objects/module.yaml b/docs/yaml/objects/module.yaml index cd98faa..518c1aa 100644 --- a/docs/yaml/objects/module.yaml +++ b/docs/yaml/objects/module.yaml @@ -4,7 +4,7 @@ description: | Base type for all modules. Modules provide their own specific implementation methods, but all modules - proivide the following methods: + provide the following methods: methods: - name: found diff --git a/graphics/meson_logo.svg b/graphics/meson_logo.svg index d5b47bc..2b0f024 100644 --- a/graphics/meson_logo.svg +++ b/graphics/meson_logo.svg @@ -2,20 +2,20 @@ + inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" + sodipodi:docname="meson_logo.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:window-width="2504" + inkscape:window-height="1650" + inkscape:window-x="546" + inkscape:window-y="110" + inkscape:window-maximized="0" + inkscape:showpageshadow="2" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" /> @@ -87,7 +90,7 @@ image/svg+xml - + @@ -336,5 +339,21 @@ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26416996;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 96.975039,102.57831 c -3.04835,1.72126 -4.94049,4.94629 -4.95784,8.44909 3.18944,1.73892 6.93427,1.67939 9.836611,0.0181 2.6e-4,-0.008 7.9e-4,-0.0165 0.001,-0.0248 -0.004,-3.484 -1.863431,-6.70217 -4.879301,-8.44238 z m 1.64176,3.72948 c 0.45198,0.30715 0.38492,1.04655 0.31212,1.55288 -0.0926,0.55776 -0.62713,1.47487 -1.32188,2.07326 0.0899,0.12885 0.14181,0.28753 0.14108,0.45682 -0.43052,0.21761 -0.85742,-0.0278 -1.37408,-0.47026 -0.51665,-0.44244 -1.07391,-1.32905 -1.25884,-2.24017 -0.16403,0.0174 -0.33188,-0.0192 -0.47594,-0.11007 0.0376,-0.51089 0.57985,-0.8146 1.0604,-0.99374 0.48056,-0.17913 1.71868,-0.2395 2.59158,0.0708 0.0681,-0.1429 0.18071,-0.26288 0.32556,-0.339 z" id="path817-6-2-9-7-9-93-9-8-1-2-4-6" /> + The official color of the logo is PANTONE 2105 C.The sRGB substitute is (57, 32, 124). diff --git a/man/meson.1 b/man/meson.1 index 7e7f486..b984013 100644 --- a/man/meson.1 +++ b/man/meson.1 @@ -1,4 +1,4 @@ -.TH MESON "1" "December 2022" "meson 1.0.0" "User Commands" +.TH MESON "1" "November 2023" "meson 1.3.0" "User Commands" .SH NAME meson - a high productivity build system .SH DESCRIPTION @@ -105,7 +105,7 @@ print all top level targets (executables, libraries, etc) print the source files of the given target .TP \fB\-\-buildsystem\-files\fR -print all files that make up the build system (meson.build, meson_options.txt etc) +print all files that make up the build system (meson.build, meson.options, meson_options.txt etc) .TP \fB\-\-tests\fR print all unit tests diff --git a/manual tests/12 wrap mirror/meson.build b/manual tests/12 wrap mirror/meson.build index 6645bdf..d299577 100644 --- a/manual tests/12 wrap mirror/meson.build +++ b/manual tests/12 wrap mirror/meson.build @@ -1,4 +1,4 @@ project('downloader') -# this test will timeout, showing that a subdomain isn't caught as masquarading url +# this test will timeout, showing that a subdomain isn't caught as masquerading url subproject('zlib') diff --git a/manual tests/3 git wrap/meson.build b/manual tests/3 git wrap/meson.build index 7fd5083..14b0671 100644 --- a/manual tests/3 git wrap/meson.build +++ b/manual tests/3 git wrap/meson.build @@ -1,4 +1,4 @@ -project('git outcheckker', 'c') +project('git outchecker', 'c') sp = subproject('samplesubproject') diff --git a/manual tests/4 standalone binaries/readme.txt b/manual tests/4 standalone binaries/readme.txt index b689779..39d21f3 100644 --- a/manual tests/4 standalone binaries/readme.txt +++ b/manual tests/4 standalone binaries/readme.txt @@ -1,5 +1,5 @@ This directory shows how you can build redistributable binaries. On -OSX this menans building an app bundle and a .dmg installer. On Linux +OSX this means building an app bundle and a .dmg installer. On Linux it means building an archive that bundles its dependencies. On Windows it means building an .exe installer. diff --git a/manual tests/6 hg wrap/meson.build b/manual tests/6 hg wrap/meson.build index c7ac004..d0e7550 100644 --- a/manual tests/6 hg wrap/meson.build +++ b/manual tests/6 hg wrap/meson.build @@ -1,4 +1,4 @@ -project('Mercurial outcheckker', 'c') +project('Mercurial outchecker', 'c') sp = subproject('samplesubproject') diff --git a/mesonbuild/_pathlib.py b/mesonbuild/_pathlib.py index 640b5ed..561a135 100644 --- a/mesonbuild/_pathlib.py +++ b/mesonbuild/_pathlib.py @@ -29,6 +29,7 @@ python bugs are fixed and it is OK to tell our users to "just upgrade python". ''' +from __future__ import annotations import pathlib import os diff --git a/mesonbuild/arglist.py b/mesonbuild/arglist.py index 189b77b..f50d54e 100644 --- a/mesonbuild/arglist.py +++ b/mesonbuild/arglist.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from functools import lru_cache import collections @@ -21,11 +22,11 @@ import re import typing as T if T.TYPE_CHECKING: - from .linkers import StaticLinker + from .linkers.linkers import StaticLinker from .compilers import Compiler # execinfo is a compiler lib on BSD -UNIXY_COMPILER_INTERNAL_LIBS = ['m', 'c', 'pthread', 'dl', 'rt', 'execinfo'] # type: T.List[str] +UNIXY_COMPILER_INTERNAL_LIBS = ['m', 'c', 'pthread', 'dl', 'rt', 'execinfo'] class Dedup(enum.Enum): @@ -81,44 +82,44 @@ class CompilerArgs(T.MutableSequence[str]): ''' # Arg prefixes that override by prepending instead of appending - prepend_prefixes = () # type: T.Tuple[str, ...] + prepend_prefixes: T.Tuple[str, ...] = () # Arg prefixes and args that must be de-duped by returning 2 - dedup2_prefixes = () # type: T.Tuple[str, ...] - dedup2_suffixes = () # type: T.Tuple[str, ...] - dedup2_args = () # type: T.Tuple[str, ...] + dedup2_prefixes: T.Tuple[str, ...] = () + dedup2_suffixes: T.Tuple[str, ...] = () + dedup2_args: T.Tuple[str, ...] = () # Arg prefixes and args that must be de-duped by returning 1 # # NOTE: not thorough. A list of potential corner cases can be found in # https://github.com/mesonbuild/meson/pull/4593#pullrequestreview-182016038 - dedup1_prefixes = () # type: T.Tuple[str, ...] - dedup1_suffixes = ('.lib', '.dll', '.so', '.dylib', '.a') # type: T.Tuple[str, ...] + dedup1_prefixes: T.Tuple[str, ...] = () + dedup1_suffixes = ('.lib', '.dll', '.so', '.dylib', '.a') # Match a .so of the form path/to/libfoo.so.0.1.0 # Only UNIX shared libraries require this. Others have a fixed extension. dedup1_regex = re.compile(r'([\/\\]|\A)lib.*\.so(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]+)?$') - dedup1_args = () # type: T.Tuple[str, ...] + dedup1_args: T.Tuple[str, ...] = () # In generate_link() we add external libs without de-dup, but we must # *always* de-dup these because they're special arguments to the linker # TODO: these should probably move too - always_dedup_args = tuple('-l' + lib for lib in UNIXY_COMPILER_INTERNAL_LIBS) # type : T.Tuple[str, ...] + always_dedup_args = tuple('-l' + lib for lib in UNIXY_COMPILER_INTERNAL_LIBS) def __init__(self, compiler: T.Union['Compiler', 'StaticLinker'], iterable: T.Optional[T.Iterable[str]] = None): self.compiler = compiler - self._container = list(iterable) if iterable is not None else [] # type: T.List[str] - self.pre = collections.deque() # type: T.Deque[str] - self.post = collections.deque() # type: T.Deque[str] + self._container: T.List[str] = list(iterable) if iterable is not None else [] + self.pre: T.Deque[str] = collections.deque() + self.post: T.Deque[str] = collections.deque() # Flush the saved pre and post list into the _container list # # This correctly deduplicates the entries after _can_dedup definition # Note: This function is designed to work without delete operations, as deletions are worsening the performance a lot. def flush_pre_post(self) -> None: - new = [] # type: T.List[str] - pre_flush_set = set() # type: T.Set[str] - post_flush = collections.deque() # type: T.Deque[str] - post_flush_set = set() # type: T.Set[str] + new: T.List[str] = [] + pre_flush_set: T.Set[str] = set() + post_flush: T.Deque[str] = collections.deque() + post_flush_set: T.Set[str] = set() #The two lists are here walked from the front to the back, in order to not need removals for deduplication for a in self.pre: @@ -197,13 +198,13 @@ class CompilerArgs(T.MutableSequence[str]): """Returns whether the argument can be safely de-duped. In addition to these, we handle library arguments specially. - With GNU ld, we surround library arguments with -Wl,--start/end-gr -> Dedupoup + With GNU ld, we surround library arguments with -Wl,--start/end-group to recursively search for symbols in the libraries. This is not needed with other linkers. """ # A standalone argument must never be deduplicated because it is - # defined by what comes _after_ it. Thus dedupping this: + # defined by what comes _after_ it. Thus deduping this: # -D FOO -D BAR # would yield either # -D FOO BAR @@ -284,7 +285,7 @@ class CompilerArgs(T.MutableSequence[str]): Add two CompilerArgs while taking into account overriding of arguments and while preserving the order of arguments as much as possible ''' - tmp_pre = collections.deque() # type: T.Deque[str] + tmp_pre: T.Deque[str] = collections.deque() if not isinstance(args, collections.abc.Iterable): raise TypeError(f'can only concatenate Iterable[str] (not "{args}") to CompilerArgs') for arg in args: diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 7484e04..382fa41 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -29,6 +29,7 @@ from ..interpreterbase import ( InvalidArguments, BreakRequest, ContinueRequest, + Disabler, default_resolve_key, ) @@ -53,6 +54,7 @@ from ..mparser import ( NotNode, PlusAssignmentNode, TernaryNode, + TestCaseClauseNode, ) if T.TYPE_CHECKING: @@ -97,10 +99,10 @@ class AstInterpreter(InterpreterBase): def __init__(self, source_root: str, subdir: str, subproject: str, visitors: T.Optional[T.List[AstVisitor]] = None): super().__init__(source_root, subdir, subproject) self.visitors = visitors if visitors is not None else [] - self.processed_buildfiles = set() # type: T.Set[str] - self.assignments = {} # type: T.Dict[str, BaseNode] - self.assign_vals = {} # type: T.Dict[str, T.Any] - self.reverse_assignment = {} # type: T.Dict[str, BaseNode] + self.processed_buildfiles: T.Set[str] = set() + self.assignments: T.Dict[str, BaseNode] = {} + self.assign_vals: T.Dict[str, T.Any] = {} + self.reverse_assignment: T.Dict[str, BaseNode] = {} self.funcs.update({'project': self.func_do_nothing, 'test': self.func_do_nothing, 'benchmark': self.func_do_nothing, @@ -148,14 +150,12 @@ class AstInterpreter(InterpreterBase): 'is_disabler': self.func_do_nothing, 'is_variable': self.func_do_nothing, 'disabler': self.func_do_nothing, - 'gettext': self.func_do_nothing, 'jar': self.func_do_nothing, 'warning': self.func_do_nothing, 'shared_module': self.func_do_nothing, 'option': self.func_do_nothing, 'both_libraries': self.func_do_nothing, 'add_test_setup': self.func_do_nothing, - 'find_library': self.func_do_nothing, 'subdir_done': self.func_do_nothing, 'alias_target': self.func_do_nothing, 'summary': self.func_do_nothing, @@ -241,7 +241,7 @@ class AstInterpreter(InterpreterBase): def evaluate_dictstatement(self, node: mparser.DictNode) -> TYPE_nkwargs: def resolve_key(node: mparser.BaseNode) -> str: - if isinstance(node, mparser.StringNode): + if isinstance(node, mparser.BaseStringNode): return node.value return '__AST_UNKNOWN__' arguments, kwargs = self.reduce_arguments(node.args, key_resolver=resolve_key) @@ -256,10 +256,10 @@ class AstInterpreter(InterpreterBase): def evaluate_plusassign(self, node: PlusAssignmentNode) -> None: assert isinstance(node, PlusAssignmentNode) # Cheat by doing a reassignment - self.assignments[node.var_name] = node.value # Save a reference to the value node + self.assignments[node.var_name.value] = node.value # Save a reference to the value node if node.value.ast_id: self.reverse_assignment[node.value.ast_id] = node - self.assign_vals[node.var_name] = self.evaluate_statement(node.value) + self.assign_vals[node.var_name.value] = self.evaluate_statement(node.value) def evaluate_indexing(self, node: IndexNode) -> int: return 0 @@ -274,7 +274,7 @@ class AstInterpreter(InterpreterBase): duplicate_key_error: T.Optional[str] = None, ) -> T.Tuple[T.List[TYPE_nvar], TYPE_nkwargs]: if isinstance(args, ArgumentNode): - kwargs = {} # type: T.Dict[str, TYPE_nvar] + kwargs: T.Dict[str, TYPE_nvar] = {} for key, val in args.kwargs.items(): kwargs[key_resolver(key)] = val if args.incorrect_order(): @@ -314,17 +314,17 @@ class AstInterpreter(InterpreterBase): for i in node.ifs: self.evaluate_codeblock(i.block) if not isinstance(node.elseblock, EmptyNode): - self.evaluate_codeblock(node.elseblock) + self.evaluate_codeblock(node.elseblock.block) def get_variable(self, varname: str) -> int: return 0 def assignment(self, node: AssignmentNode) -> None: assert isinstance(node, AssignmentNode) - self.assignments[node.var_name] = node.value # Save a reference to the value node + self.assignments[node.var_name.value] = node.value # Save a reference to the value node if node.value.ast_id: self.reverse_assignment[node.value.ast_id] = node - self.assign_vals[node.var_name] = self.evaluate_statement(node.value) # Evaluate the value just in case + self.assign_vals[node.var_name.value] = self.evaluate_statement(node.value) # Evaluate the value just in case def resolve_node(self, node: BaseNode, include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.Optional[T.Any]: def quick_resolve(n: BaseNode, loop_detect: T.Optional[T.List[str]] = None) -> T.Any: @@ -352,7 +352,7 @@ class AstInterpreter(InterpreterBase): return None # Loop detected id_loop_detect += [node.ast_id] - # Try to evealuate the value of the node + # Try to evaluate the value of the node if isinstance(node, IdNode): result = quick_resolve(node) @@ -373,8 +373,8 @@ class AstInterpreter(InterpreterBase): elif isinstance(node, ArithmeticNode): if node.operation != 'add': return None # Only handle string and array concats - l = quick_resolve(node.left) - r = quick_resolve(node.right) + l = self.resolve_node(node.left, include_unknown_args, id_loop_detect) + r = self.resolve_node(node.right, include_unknown_args, id_loop_detect) if isinstance(l, str) and isinstance(r, str): result = l + r # String concatenation detected else: @@ -383,18 +383,19 @@ class AstInterpreter(InterpreterBase): elif isinstance(node, MethodNode): src = quick_resolve(node.source_object) margs = self.flatten_args(node.args.arguments, include_unknown_args, id_loop_detect) - mkwargs = {} # type: T.Dict[str, TYPE_nvar] + mkwargs: T.Dict[str, TYPE_nvar] = {} + method_name = node.name.value try: if isinstance(src, str): - result = StringHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs) + result = StringHolder(src, T.cast('Interpreter', self)).method_call(method_name, margs, mkwargs) elif isinstance(src, bool): - result = BooleanHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs) + result = BooleanHolder(src, T.cast('Interpreter', self)).method_call(method_name, margs, mkwargs) elif isinstance(src, int): - result = IntegerHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs) + result = IntegerHolder(src, T.cast('Interpreter', self)).method_call(method_name, margs, mkwargs) elif isinstance(src, list): - result = ArrayHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs) + result = ArrayHolder(src, T.cast('Interpreter', self)).method_call(method_name, margs, mkwargs) elif isinstance(src, dict): - result = DictHolder(src, T.cast('Interpreter', self)).method_call(node.name, margs, mkwargs) + result = DictHolder(src, T.cast('Interpreter', self)).method_call(method_name, margs, mkwargs) except mesonlib.MesonException: return None @@ -402,7 +403,7 @@ class AstInterpreter(InterpreterBase): if isinstance(result, BaseNode): result = self.resolve_node(result, include_unknown_args, id_loop_detect) elif isinstance(result, list): - new_res = [] # type: T.List[TYPE_nvar] + new_res: T.List[TYPE_nvar] = [] for i in result: if isinstance(i, BaseNode): resolved = self.resolve_node(i, include_unknown_args, id_loop_detect) @@ -421,7 +422,7 @@ class AstInterpreter(InterpreterBase): else: args = [args_raw] - flattend_args = [] # type: T.List[TYPE_nvar] + flattened_args: T.List[TYPE_nvar] = [] # Resolve the contents of args for i in args: @@ -430,18 +431,21 @@ class AstInterpreter(InterpreterBase): if resolved is not None: if not isinstance(resolved, list): resolved = [resolved] - flattend_args += resolved + flattened_args += resolved elif isinstance(i, (str, bool, int, float)) or include_unknown_args: - flattend_args += [i] - return flattend_args + flattened_args += [i] + return flattened_args def flatten_kwargs(self, kwargs: T.Dict[str, TYPE_nvar], include_unknown_args: bool = False) -> T.Dict[str, TYPE_nvar]: - flattend_kwargs = {} + flattened_kwargs = {} for key, val in kwargs.items(): if isinstance(val, BaseNode): resolved = self.resolve_node(val, include_unknown_args) if resolved is not None: - flattend_kwargs[key] = resolved + flattened_kwargs[key] = resolved elif isinstance(val, (str, bool, int, float)) or include_unknown_args: - flattend_kwargs[key] = val - return flattend_kwargs + flattened_kwargs[key] = val + return flattened_kwargs + + def evaluate_testcase(self, node: TestCaseClauseNode) -> Disabler | None: + return Disabler(subproject=self.subproject) diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py index 722c908..7d91a85 100644 --- a/mesonbuild/ast/introspection.py +++ b/mesonbuild/ast/introspection.py @@ -27,7 +27,7 @@ from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary from ..compilers import detect_compiler_for from ..interpreterbase import InvalidArguments from ..mesonlib import MachineChoice, OptionKey -from ..mparser import BaseNode, ArithmeticNode, ArrayNode, ElementaryNode, IdNode, FunctionNode, StringNode +from ..mparser import BaseNode, ArithmeticNode, ArrayNode, ElementaryNode, IdNode, FunctionNode, BaseStringNode from .interpreter import AstInterpreter if T.TYPE_CHECKING: @@ -46,9 +46,9 @@ class IntrospectionHelper(argparse.Namespace): # mimic an argparse namespace def __init__(self, cross_file: str): super().__init__() - self.cross_file = cross_file # type: str - self.native_file = None # type: str - self.cmd_line_options = {} # type: T.Dict[str, str] + self.cross_file = cross_file + self.native_file: str = None + self.cmd_line_options: T.Dict[str, str] = {} def __eq__(self, other: object) -> bool: return NotImplemented @@ -76,13 +76,12 @@ class IntrospectionInterpreter(AstInterpreter): self.environment = env self.subproject_dir = subproject_dir self.coredata = self.environment.get_coredata() - self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') self.backend = backend self.default_options = {OptionKey('backend'): self.backend} - self.project_data = {} # type: T.Dict[str, T.Any] - self.targets = [] # type: T.List[T.Dict[str, T.Any]] - self.dependencies = [] # type: T.List[T.Dict[str, T.Any]] - self.project_node = None # type: BaseNode + self.project_data: T.Dict[str, T.Any] = {} + self.targets: T.List[T.Dict[str, T.Any]] = [] + self.dependencies: T.List[T.Dict[str, T.Any]] = [] + self.project_node: BaseNode = None self.funcs.update({ 'add_languages': self.func_add_languages, @@ -113,9 +112,12 @@ class IntrospectionInterpreter(AstInterpreter): proj_vers = 'undefined' self.project_data = {'descriptive_name': proj_name, 'version': proj_vers} - if os.path.exists(self.option_file): + optfile = os.path.join(self.source_root, self.subdir, 'meson.options') + if not os.path.exists(optfile): + optfile = os.path.join(self.source_root, self.subdir, 'meson_options.txt') + if os.path.exists(optfile): oi = optinterpreter.OptionInterpreter(self.subproject) - oi.process(self.option_file) + oi.process(optfile) self.coredata.update_project_options(oi.options) def_opts = self.flatten_args(kwargs.get('default_options', [])) @@ -126,7 +128,7 @@ class IntrospectionInterpreter(AstInterpreter): if not self.is_subproject() and 'subproject_dir' in kwargs: spdirname = kwargs['subproject_dir'] - if isinstance(spdirname, StringNode): + if isinstance(spdirname, BaseStringNode): assert isinstance(spdirname.value, str) self.subproject_dir = spdirname.value if not self.is_subproject(): @@ -141,8 +143,8 @@ class IntrospectionInterpreter(AstInterpreter): options = {k: v for k, v in self.environment.options.items() if k.is_backend()} self.coredata.set_options(options) - self._add_languages(proj_langs, MachineChoice.HOST) - self._add_languages(proj_langs, MachineChoice.BUILD) + self._add_languages(proj_langs, True, MachineChoice.HOST) + self._add_languages(proj_langs, True, MachineChoice.BUILD) def do_subproject(self, dirname: str) -> None: subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir) @@ -157,25 +159,35 @@ class IntrospectionInterpreter(AstInterpreter): def func_add_languages(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None: kwargs = self.flatten_kwargs(kwargs) + required = kwargs.get('required', True) + if isinstance(required, cdata.UserFeatureOption): + required = required.is_enabled() if 'native' in kwargs: native = kwargs.get('native', False) - self._add_languages(args, MachineChoice.BUILD if native else MachineChoice.HOST) + self._add_languages(args, required, MachineChoice.BUILD if native else MachineChoice.HOST) else: for for_machine in [MachineChoice.BUILD, MachineChoice.HOST]: - self._add_languages(args, for_machine) + self._add_languages(args, required, for_machine) - def _add_languages(self, raw_langs: T.List[TYPE_nvar], for_machine: MachineChoice) -> None: - langs = [] # type: T.List[str] + def _add_languages(self, raw_langs: T.List[TYPE_nvar], required: bool, for_machine: MachineChoice) -> None: + langs: T.List[str] = [] for l in self.flatten_args(raw_langs): if isinstance(l, str): langs.append(l) - elif isinstance(l, StringNode): + elif isinstance(l, BaseStringNode): langs.append(l.value) for lang in sorted(langs, key=compilers.sort_clink): lang = lang.lower() if lang not in self.coredata.compilers[for_machine]: - comp = detect_compiler_for(self.environment, lang, for_machine) + try: + comp = detect_compiler_for(self.environment, lang, for_machine, True) + except mesonlib.MesonException: + # do we even care about introspecting this language? + if required: + raise + else: + continue if self.subproject: options = {} for k in comp.get_options(): @@ -226,7 +238,7 @@ class IntrospectionInterpreter(AstInterpreter): kwargs = self.flatten_kwargs(kwargs_raw, True) def traverse_nodes(inqueue: T.List[BaseNode]) -> T.List[BaseNode]: - res = [] # type: T.List[BaseNode] + res: T.List[BaseNode] = [] while inqueue: curr = inqueue.pop(0) arg_node = None @@ -249,11 +261,11 @@ class IntrospectionInterpreter(AstInterpreter): continue arg_nodes = arg_node.arguments.copy() # Pop the first element if the function is a build target function - if isinstance(curr, FunctionNode) and curr.func_name in BUILD_TARGET_FUNCTIONS: + if isinstance(curr, FunctionNode) and curr.func_name.value in BUILD_TARGET_FUNCTIONS: arg_nodes.pop(0) - elemetary_nodes = [x for x in arg_nodes if isinstance(x, (str, StringNode))] + elementary_nodes = [x for x in arg_nodes if isinstance(x, (str, BaseStringNode))] inqueue += [x for x in arg_nodes if isinstance(x, (FunctionNode, ArrayNode, IdNode, ArithmeticNode))] - if elemetary_nodes: + if elementary_nodes: res += [curr] return res @@ -265,13 +277,13 @@ class IntrospectionInterpreter(AstInterpreter): kwargs_reduced = {k: v.value if isinstance(v, ElementaryNode) else v for k, v in kwargs_reduced.items()} kwargs_reduced = {k: v for k, v in kwargs_reduced.items() if not isinstance(v, BaseNode)} for_machine = MachineChoice.HOST - objects = [] # type: T.List[T.Any] - empty_sources = [] # type: T.List[T.Any] + objects: T.List[T.Any] = [] + empty_sources: T.List[T.Any] = [] # Passing the unresolved sources list causes errors + kwargs_reduced['_allow_no_sources'] = True target = targetclass(name, self.subdir, self.subproject, for_machine, empty_sources, [], objects, self.environment, self.coredata.compilers[for_machine], kwargs_reduced) - target.process_compilers() - target.process_compilers_late([]) + target.process_compilers_late() new_target = { 'name': target.get_basename(), @@ -350,3 +362,22 @@ class IntrospectionInterpreter(AstInterpreter): self.sanity_check_ast() self.parse_project() self.run() + + def extract_subproject_dir(self) -> T.Optional[str]: + '''Fast path to extract subproject_dir kwarg. + This is faster than self.parse_project() which also initialize options + and also calls parse_project() on every subproject. + ''' + if not self.ast.lines: + return + project = self.ast.lines[0] + # first line is always project() + if not isinstance(project, FunctionNode): + return + for kw, val in project.args.kwargs.items(): + assert isinstance(kw, IdNode), 'for mypy' + if kw.value == 'subproject_dir': + # mypy does not understand "and isinstance" + if isinstance(val, BaseStringNode): + return val.value + return None diff --git a/mesonbuild/ast/postprocess.py b/mesonbuild/ast/postprocess.py index 0c28af0..7d2036e 100644 --- a/mesonbuild/ast/postprocess.py +++ b/mesonbuild/ast/postprocess.py @@ -16,7 +16,7 @@ # or an interpreter-based tool from __future__ import annotations -from . import AstVisitor +from .visitor import AstVisitor import typing as T if T.TYPE_CHECKING: @@ -80,7 +80,7 @@ class AstIndentationGenerator(AstVisitor): class AstIDGenerator(AstVisitor): def __init__(self) -> None: - self.counter = {} # type: T.Dict[str, int] + self.counter: T.Dict[str, int] = {} def visit_default_func(self, node: mparser.BaseNode) -> None: name = type(node).__name__ diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py index 1e33cf0..155b5fc 100644 --- a/mesonbuild/ast/printer.py +++ b/mesonbuild/ast/printer.py @@ -14,9 +14,12 @@ # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool +from __future__ import annotations from .. import mparser -from . import AstVisitor +from .visitor import AstVisitor + +from itertools import zip_longest import re import typing as T @@ -83,7 +86,17 @@ class AstPrinter(AstVisitor): def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None: assert isinstance(node.value, str) - self.append("f'" + node.value + "'", node) + self.append("f'" + self.escape(node.value) + "'", node) + node.lineno = self.curr_line or node.lineno + + def visit_MultilineStringNode(self, node: mparser.StringNode) -> None: + assert isinstance(node.value, str) + self.append("'''" + node.value + "'''", node) + node.lineno = self.curr_line or node.lineno + + def visit_FormatMultilineStringNode(self, node: mparser.FormatStringNode) -> None: + assert isinstance(node.value, str) + self.append("f'''" + node.value + "'''", node) node.lineno = self.curr_line or node.lineno def visit_ContinueNode(self, node: mparser.ContinueNode) -> None: @@ -151,30 +164,30 @@ class AstPrinter(AstVisitor): def visit_MethodNode(self, node: mparser.MethodNode) -> None: node.lineno = self.curr_line or node.lineno node.source_object.accept(self) - self.append('.' + node.name + '(', node) + self.append('.' + node.name.value + '(', node) node.args.accept(self) self.append(')', node) def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: node.lineno = self.curr_line or node.lineno - self.append(node.func_name + '(', node) + self.append(node.func_name.value + '(', node) node.args.accept(self) self.append(')', node) def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: node.lineno = self.curr_line or node.lineno - self.append(node.var_name + ' = ', node) + self.append(node.var_name.value + ' = ', node) node.value.accept(self) def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: node.lineno = self.curr_line or node.lineno - self.append(node.var_name + ' += ', node) + self.append(node.var_name.value + ' += ', node) node.value.accept(self) def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: node.lineno = self.curr_line or node.lineno self.append_padded('foreach', node) - self.append_padded(', '.join(node.varnames), node) + self.append_padded(', '.join(varname.value for varname in node.varnames), node) self.append_padded(':', node) node.items.accept(self) self.newline() @@ -237,14 +250,231 @@ class AstPrinter(AstVisitor): else: self.result = re.sub(r', $', '', self.result) +class RawPrinter(AstVisitor): + + def __init__(self): + self.result = '' + + def visit_default_func(self, node: mparser.BaseNode): + self.result += node.value + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_unary_operator(self, node: mparser.UnaryOperatorNode): + node.operator.accept(self) + node.value.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_binary_operator(self, node: mparser.BinaryOperatorNode): + node.left.accept(self) + node.operator.accept(self) + node.right.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_BooleanNode(self, node: mparser.BooleanNode) -> None: + self.result += 'true' if node.value else 'false' + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_NumberNode(self, node: mparser.NumberNode) -> None: + self.result += node.raw_value + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_StringNode(self, node: mparser.StringNode) -> None: + self.result += f"'{node.raw_value}'" + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_MultilineStringNode(self, node: mparser.MultilineStringNode) -> None: + self.result += f"'''{node.value}'''" + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None: + self.result += 'f' + self.visit_StringNode(node) + + def visit_MultilineFormatStringNode(self, node: mparser.MultilineFormatStringNode) -> None: + self.result += 'f' + self.visit_MultilineStringNode(node) + + def visit_ContinueNode(self, node: mparser.ContinueNode) -> None: + self.result += 'continue' + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_BreakNode(self, node: mparser.BreakNode) -> None: + self.result += 'break' + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_ArrayNode(self, node: mparser.ArrayNode) -> None: + node.lbracket.accept(self) + node.args.accept(self) + node.rbracket.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_DictNode(self, node: mparser.DictNode) -> None: + node.lcurl.accept(self) + node.args.accept(self) + node.rcurl.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_ParenthesizedNode(self, node: mparser.ParenthesizedNode) -> None: + node.lpar.accept(self) + node.inner.accept(self) + node.rpar.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_OrNode(self, node: mparser.OrNode) -> None: + self.visit_binary_operator(node) + + def visit_AndNode(self, node: mparser.AndNode) -> None: + self.visit_binary_operator(node) + + def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None: + self.visit_binary_operator(node) + + def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None: + self.visit_binary_operator(node) + + def visit_NotNode(self, node: mparser.NotNode) -> None: + self.visit_unary_operator(node) + + def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None: + if node.pre_whitespaces: + node.pre_whitespaces.accept(self) + for i in node.lines: + i.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_IndexNode(self, node: mparser.IndexNode) -> None: + node.iobject.accept(self) + node.lbracket.accept(self) + node.index.accept(self) + node.rbracket.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_MethodNode(self, node: mparser.MethodNode) -> None: + node.source_object.accept(self) + node.dot.accept(self) + node.name.accept(self) + node.lpar.accept(self) + node.args.accept(self) + node.rpar.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: + node.func_name.accept(self) + node.lpar.accept(self) + node.args.accept(self) + node.rpar.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: + node.var_name.accept(self) + node.operator.accept(self) + node.value.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: + node.var_name.accept(self) + node.operator.accept(self) + node.value.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: + node.foreach_.accept(self) + for varname, comma in zip_longest(node.varnames, node.commas): + varname.accept(self) + if comma is not None: + comma.accept(self) + node.column.accept(self) + node.items.accept(self) + node.block.accept(self) + node.endforeach.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: + for i in node.ifs: + i.accept(self) + if not isinstance(node.elseblock, mparser.EmptyNode): + node.elseblock.accept(self) + node.endif.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_UMinusNode(self, node: mparser.UMinusNode) -> None: + self.visit_unary_operator(node) + + def visit_IfNode(self, node: mparser.IfNode) -> None: + node.if_.accept(self) + node.condition.accept(self) + node.block.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_ElseNode(self, node: mparser.ElseNode) -> None: + node.else_.accept(self) + node.block.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_TernaryNode(self, node: mparser.TernaryNode) -> None: + node.condition.accept(self) + node.questionmark.accept(self) + node.trueblock.accept(self) + node.column.accept(self) + node.falseblock.accept(self) + if node.whitespaces: + node.whitespaces.accept(self) + + def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None: + commas_iter = iter(node.commas) + + for arg in node.arguments: + arg.accept(self) + try: + comma = next(commas_iter) + comma.accept(self) + except StopIteration: + pass + + assert len(node.columns) == len(node.kwargs) + for (key, val), column in zip(node.kwargs.items(), node.columns): + key.accept(self) + column.accept(self) + val.accept(self) + try: + comma = next(commas_iter) + comma.accept(self) + except StopIteration: + pass + + if node.whitespaces: + node.whitespaces.accept(self) + class AstJSONPrinter(AstVisitor): def __init__(self) -> None: - self.result = {} # type: T.Dict[str, T.Any] + self.result: T.Dict[str, T.Any] = {} self.current = self.result def _accept(self, key: str, node: mparser.BaseNode) -> None: old = self.current - data = {} # type: T.Dict[str, T.Any] + data: T.Dict[str, T.Any] = {} self.current = data node.accept(self) self.current = old @@ -252,7 +482,7 @@ class AstJSONPrinter(AstVisitor): def _accept_list(self, key: str, nodes: T.Sequence[mparser.BaseNode]) -> None: old = self.current - datalist = [] # type: T.List[T.Dict[str, T.Any]] + datalist: T.List[T.Dict[str, T.Any]] = [] for i in nodes: self.current = {} i.accept(self) @@ -341,28 +571,28 @@ class AstJSONPrinter(AstVisitor): def visit_MethodNode(self, node: mparser.MethodNode) -> None: self._accept('object', node.source_object) self._accept('args', node.args) - self.current['name'] = node.name + self.current['name'] = node.name.value self.setbase(node) def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: self._accept('args', node.args) - self.current['name'] = node.func_name + self.current['name'] = node.func_name.value self.setbase(node) def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: self._accept('value', node.value) - self.current['var_name'] = node.var_name + self.current['var_name'] = node.var_name.value self.setbase(node) def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: self._accept('value', node.value) - self.current['var_name'] = node.var_name + self.current['var_name'] = node.var_name.value self.setbase(node) def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: self._accept('items', node.items) self._accept('block', node.block) - self.current['varnames'] = node.varnames + self.current['varnames'] = [varname.value for varname in node.varnames] self.setbase(node) def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: @@ -387,10 +617,10 @@ class AstJSONPrinter(AstVisitor): def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None: self._accept_list('positional', node.arguments) - kwargs_list = [] # type: T.List[T.Dict[str, T.Dict[str, T.Any]]] + kwargs_list: T.List[T.Dict[str, T.Dict[str, T.Any]]] = [] for key, val in node.kwargs.items(): - key_res = {} # type: T.Dict[str, T.Any] - val_res = {} # type: T.Dict[str, T.Any] + key_res: T.Dict[str, T.Any] = {} + val_res: T.Dict[str, T.Any] = {} self._raw_accept(key, key_res) self._raw_accept(val, val_res) kwargs_list += [{'key': key_res, 'val': val_res}] diff --git a/mesonbuild/ast/visitor.py b/mesonbuild/ast/visitor.py index 8a0e77b..d05d3ff 100644 --- a/mesonbuild/ast/visitor.py +++ b/mesonbuild/ast/visitor.py @@ -43,12 +43,24 @@ class AstVisitor: def visit_FormatStringNode(self, node: mparser.FormatStringNode) -> None: self.visit_default_func(node) + def visit_MultilineStringNode(self, node: mparser.StringNode) -> None: + self.visit_default_func(node) + + def visit_FormatMultilineStringNode(self, node: mparser.FormatStringNode) -> None: + self.visit_default_func(node) + def visit_ContinueNode(self, node: mparser.ContinueNode) -> None: self.visit_default_func(node) def visit_BreakNode(self, node: mparser.BreakNode) -> None: self.visit_default_func(node) + def visit_SymbolNode(self, node: mparser.SymbolNode) -> None: + self.visit_default_func(node) + + def visit_WhitespaceNode(self, node: mparser.WhitespaceNode) -> None: + self.visit_default_func(node) + def visit_ArrayNode(self, node: mparser.ArrayNode) -> None: self.visit_default_func(node) node.args.accept(self) @@ -97,22 +109,28 @@ class AstVisitor: def visit_MethodNode(self, node: mparser.MethodNode) -> None: self.visit_default_func(node) node.source_object.accept(self) + node.name.accept(self) node.args.accept(self) def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: self.visit_default_func(node) + node.func_name.accept(self) node.args.accept(self) def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: self.visit_default_func(node) + node.var_name.accept(self) node.value.accept(self) def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: self.visit_default_func(node) + node.var_name.accept(self) node.value.accept(self) def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: self.visit_default_func(node) + for varname in node.varnames: + varname.accept(self) node.items.accept(self) node.block.accept(self) @@ -131,6 +149,10 @@ class AstVisitor: node.condition.accept(self) node.block.accept(self) + def visit_ElseNode(self, node: mparser.IfNode) -> None: + self.visit_default_func(node) + node.block.accept(self) + def visit_TernaryNode(self, node: mparser.TernaryNode) -> None: self.visit_default_func(node) node.condition.accept(self) @@ -144,3 +166,7 @@ class AstVisitor: for key, val in node.kwargs.items(): key.accept(self) val.accept(self) + + def visit_ParenthesizedNode(self, node: mparser.ParenthesizedNode) -> None: + self.visit_default_func(node) + node.inner.accept(self) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 27004f8..2c24e4c 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -46,7 +46,7 @@ if T.TYPE_CHECKING: from ..compilers import Compiler from ..environment import Environment from ..interpreter import Interpreter, Test - from ..linkers import StaticLinker + from ..linkers.linkers import StaticLinker from ..mesonlib import FileMode, FileOrString from typing_extensions import TypedDict @@ -172,6 +172,7 @@ class InstallDataBase: subproject: str tag: T.Optional[str] = None data_type: T.Optional[str] = None + follow_symlinks: T.Optional[bool] = None @dataclass(eq=False) class InstallSymlinkData: @@ -186,8 +187,9 @@ class InstallSymlinkData: class SubdirInstallData(InstallDataBase): def __init__(self, path: str, install_path: str, install_path_name: str, install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]], - subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None): - super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type) + subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None, + follow_symlinks: T.Optional[bool] = None): + super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type, follow_symlinks) self.exclude = exclude @@ -202,7 +204,7 @@ class TestSerialisation: needs_exe_wrapper: bool is_parallel: bool cmd_args: T.List[str] - env: build.EnvironmentVariables + env: mesonlib.EnvironmentVariables should_fail: bool timeout: T.Optional[int] workdir: T.Optional[str] @@ -251,6 +253,16 @@ def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, i elif backend == 'xcode': from . import xcodebackend return xcodebackend.XCodeBackend(build, interpreter) + elif backend == 'none': + from . import nonebackend + return nonebackend.NoneBackend(build, interpreter) + return None + + +def get_genvslite_backend(genvsname: str, build: T.Optional[build.Build] = None, interpreter: T.Optional['Interpreter'] = None) -> T.Optional['Backend']: + if genvsname == 'vs2022': + from . import vs2022backend + return vs2022backend.Vs2022Backend(build, interpreter, gen_lite = True) return None # This class contains the basic functionality that is needed by all backends. @@ -258,6 +270,7 @@ def get_backend_from_name(backend: str, build: T.Optional[build.Build] = None, i class Backend: environment: T.Optional['Environment'] + name = '' def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional['Interpreter']): # Make it possible to construct a dummy backend @@ -269,7 +282,6 @@ class Backend: self.interpreter = interpreter self.environment = build.environment self.processed_targets: T.Set[str] = set() - self.name = '' self.build_dir = self.environment.get_build_dir() self.source_dir = self.environment.get_source_dir() self.build_to_src = mesonlib.relpath(self.environment.get_source_dir(), @@ -277,14 +289,22 @@ class Backend: self.src_to_build = mesonlib.relpath(self.environment.get_build_dir(), self.environment.get_source_dir()) - def generate(self) -> None: + # If requested via 'capture = True', returns captured compile args per + # target (e.g. captured_args[target]) that can be used later, for example, + # to populate things like intellisense fields in generated visual studio + # projects (as is the case when using '--genvslite'). + # + # 'vslite_ctx' is only provided when + # we expect this backend setup/generation to make use of previously captured + # compile args (as is the case when using '--genvslite'). + def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional[dict]: raise RuntimeError(f'generate is not implemented in {type(self).__name__}') def get_target_filename(self, t: T.Union[build.Target, build.CustomTargetIndex], *, warn_multi_output: bool = True) -> str: if isinstance(t, build.CustomTarget): if warn_multi_output and len(t.get_outputs()) != 1: mlog.warning(f'custom_target {t.name!r} has more than one output! ' - 'Using the first one.') + f'Using the first one. Consider using `{t.name}[0]`.') filename = t.get_outputs()[0] elif isinstance(t, build.CustomTargetIndex): filename = t.get_outputs()[0] @@ -296,6 +316,20 @@ class Backend: def get_target_filename_abs(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> str: return os.path.join(self.environment.get_build_dir(), self.get_target_filename(target)) + def get_target_debug_filename(self, target: build.BuildTarget) -> T.Optional[str]: + assert isinstance(target, build.BuildTarget), target + if target.get_debug_filename(): + debug_filename = target.get_debug_filename() + return os.path.join(self.get_target_dir(target), debug_filename) + else: + return None + + def get_target_debug_filename_abs(self, target: build.BuildTarget) -> T.Optional[str]: + assert isinstance(target, build.BuildTarget), target + if not target.get_debug_filename(): + return None + return os.path.join(self.environment.get_build_dir(), self.get_target_debug_filename(target)) + def get_source_dir_include_args(self, target: build.BuildTarget, compiler: 'Compiler', *, absolute_path: bool = False) -> T.List[str]: curdir = target.get_subdir() if absolute_path: @@ -321,6 +355,9 @@ class Backend: # On all other platforms, we link to the library directly. if isinstance(target, build.SharedLibrary): link_lib = target.get_import_filename() or target.get_filename() + # In AIX, if we archive .so, the blibpath must link to archived shared library otherwise to the .so file. + if mesonlib.is_aix() and target.aix_so_archive: + link_lib = re.sub('[.][a]([.]?([0-9]+))*([.]?([a-z]+))*', '.a', link_lib.replace('.so', '.a')) return os.path.join(self.get_target_dir(target), link_lib) elif isinstance(target, build.StaticLibrary): return os.path.join(self.get_target_dir(target), target.get_filename()) @@ -492,11 +529,12 @@ class Backend: self, cmd: T.Sequence[T.Union[programs.ExternalProgram, build.BuildTarget, build.CustomTarget, File, str]], workdir: T.Optional[str] = None, extra_bdeps: T.Optional[T.List[build.BuildTarget]] = None, - capture: T.Optional[bool] = None, - feed: T.Optional[bool] = None, - env: T.Optional[build.EnvironmentVariables] = None, + capture: T.Optional[str] = None, + feed: T.Optional[str] = None, + env: T.Optional[mesonlib.EnvironmentVariables] = None, tag: T.Optional[str] = None, - verbose: bool = False) -> 'ExecutableSerialisation': + verbose: bool = False, + installdir_map: T.Optional[T.Dict[str, str]] = None) -> 'ExecutableSerialisation': # XXX: cmd_args either need to be lowered to strings, or need to be checked for non-string arguments, right? exe, *raw_cmd_args = cmd @@ -557,16 +595,16 @@ class Backend: workdir = workdir or self.environment.get_build_dir() return ExecutableSerialisation(exe_cmd + cmd_args, env, exe_wrapper, workdir, - extra_paths, capture, feed, tag, verbose) + extra_paths, capture, feed, tag, verbose, installdir_map) def as_meson_exe_cmdline(self, exe: T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, programs.ExternalProgram], cmd_args: T.Sequence[T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, programs.ExternalProgram]], workdir: T.Optional[str] = None, extra_bdeps: T.Optional[T.List[build.BuildTarget]] = None, - capture: T.Optional[bool] = None, - feed: T.Optional[bool] = None, + capture: T.Optional[str] = None, + feed: T.Optional[str] = None, force_serialize: bool = False, - env: T.Optional[build.EnvironmentVariables] = None, + env: T.Optional[mesonlib.EnvironmentVariables] = None, verbose: bool = False) -> T.Tuple[T.Sequence[T.Union[str, File, build.Target, programs.ExternalProgram]], str]: ''' Serialize an executable for running with a generator or a custom target @@ -619,9 +657,9 @@ class Backend: return es.cmd_args, '' args: T.List[str] = [] if capture: - args += ['--capture', str(capture)] + args += ['--capture', capture] if feed: - args += ['--feed', str(feed)] + args += ['--feed', feed] return ( self.environment.get_build_command() + ['--internal', 'exe'] + args + ['--'] + es.cmd_args, @@ -630,7 +668,7 @@ class Backend: if isinstance(exe, (programs.ExternalProgram, build.BuildTarget, build.CustomTarget)): - basename = exe.name + basename = os.path.basename(exe.name) elif isinstance(exe, mesonlib.File): basename = os.path.basename(exe.fname) else: @@ -733,8 +771,10 @@ class Backend: @lru_cache(maxsize=None) def rpaths_for_non_system_absolute_shared_libraries(self, target: build.BuildTarget, exclude_system: bool = True) -> 'ImmutableListProtocol[str]': paths: OrderedSet[str] = OrderedSet() + srcdir = self.environment.get_source_dir() + for dep in target.external_deps: - if not isinstance(dep, (dependencies.ExternalLibrary, dependencies.PkgConfigDependency)): + if dep.type_name not in {'library', 'pkgconfig'}: continue for libpath in dep.link_args: # For all link args that are absolute paths to a library file, add RPATH args @@ -748,11 +788,18 @@ class Backend: if libdir in self.get_external_rpath_dirs(target): continue # Windows doesn't support rpaths, but we use this function to - # emulate rpaths by setting PATH, so also accept DLLs here - if os.path.splitext(libpath)[1] not in ['.dll', '.lib', '.so', '.dylib']: + # emulate rpaths by setting PATH + # .dll is there for mingw gcc + if os.path.splitext(libpath)[1] not in {'.dll', '.lib', '.so', '.dylib'}: continue - if libdir.startswith(self.environment.get_source_dir()): - rel_to_src = libdir[len(self.environment.get_source_dir()) + 1:] + + try: + commonpath = os.path.commonpath((libdir, srcdir)) + except ValueError: # when paths are on different drives on Windows + commonpath = '' + + if commonpath == srcdir: + rel_to_src = libdir[len(srcdir) + 1:] assert not os.path.isabs(rel_to_src), f'rel_to_src: {rel_to_src} is absolute' paths.add(os.path.join(self.build_to_src, rel_to_src)) else: @@ -830,6 +877,8 @@ class Backend: def _determine_ext_objs(self, extobj: 'build.ExtractedObjects', proj_dir_to_build_root: str) -> T.List[str]: result: T.List[str] = [] + targetdir = self.get_target_private_dir(extobj.target) + # Merge sources and generated sources raw_sources = list(extobj.srclist) for gensrc in extobj.genlist: @@ -846,12 +895,18 @@ class Backend: elif self.environment.is_object(s): result.append(s.relative_name()) + # MSVC generate an object file for PCH + if extobj.pch and self.target_uses_pch(extobj.target): + for lang, pch in extobj.target.pch.items(): + compiler = extobj.target.compilers[lang] + if compiler.get_argument_syntax() == 'msvc': + objname = self.get_msvc_pch_objname(lang, pch) + result.append(os.path.join(proj_dir_to_build_root, targetdir, objname)) + # extobj could contain only objects and no sources if not sources: return result - targetdir = self.get_target_private_dir(extobj.target) - # With unity builds, sources don't map directly to objects, # we only support extracting all the objects in this mode, # so just return all object files. @@ -865,7 +920,7 @@ class Backend: if comp.language in LANGS_CANT_UNITY: sources += srcs continue - for i in range(len(srcs) // unity_size + 1): + for i in range((len(srcs) + unity_size - 1) // unity_size): _src = self.get_unity_source_file(extobj.target, comp.get_default_suffix(), i) sources.append(_src) @@ -886,6 +941,12 @@ class Backend: args += compiler.get_pch_use_args(pchpath, p[0]) return includeargs + args + def get_msvc_pch_objname(self, lang: str, pch: T.List[str]) -> str: + if len(pch) == 1: + # Same name as in create_msvc_pch_implementation() below. + return f'meson_pch-{lang}.obj' + return os.path.splitext(pch[1])[0] + '.obj' + def create_msvc_pch_implementation(self, target: build.BuildTarget, lang: str, pch_header: str) -> str: # We have to include the language in the file name, otherwise # pch.c and pch.cpp will both end up as pch.obj in VS backends. @@ -903,6 +964,12 @@ class Backend: mesonlib.replace_if_different(pch_file, pch_file_tmp) return pch_rel_to_build + def target_uses_pch(self, target: build.BuildTarget) -> bool: + try: + return T.cast('bool', target.get_option(OptionKey('b_pch'))) + except KeyError: + return False + @staticmethod def escape_extra_args(args: T.List[str]) -> T.List[str]: # all backslashes in defines are doubly-escaped @@ -993,7 +1060,8 @@ class Backend: continue if compiler.language == 'vala': - if isinstance(dep, dependencies.PkgConfigDependency): + if dep.type_name == 'pkgconfig': + assert isinstance(dep, dependencies.ExternalDependency) if dep.name == 'glib-2.0' and dep.version_reqs is not None: for req in dep.version_reqs: if req.startswith(('>=', '==')): @@ -1049,6 +1117,62 @@ class Backend: paths.update(cc.get_library_dirs(self.environment)) return list(paths) + @staticmethod + @lru_cache(maxsize=None) + def search_dll_path(link_arg: str) -> T.Optional[str]: + if link_arg.startswith(('-l', '-L')): + link_arg = link_arg[2:] + + p = Path(link_arg) + if not p.is_absolute(): + return None + + try: + p = p.resolve(strict=True) + except FileNotFoundError: + return None + + for f in p.parent.glob('*.dll'): + # path contains dlls + return str(p.parent) + + if p.is_file(): + p = p.parent + # Heuristic: replace *last* occurence of '/lib' + binpath = Path('/bin'.join(p.as_posix().rsplit('/lib', maxsplit=1))) + for _ in binpath.glob('*.dll'): + return str(binpath) + + return None + + @classmethod + @lru_cache(maxsize=None) + def extract_dll_paths(cls, target: build.BuildTarget) -> T.Set[str]: + """Find paths to all DLLs needed for a given target, since + we link against import libs, and we don't know the actual + path of the DLLs. + + 1. If there are DLLs in the same directory than the .lib dir, use it + 2. If there is a sibbling directory named 'bin' with DLLs in it, use it + """ + results = set() + for dep in target.external_deps: + + if dep.type_name == 'pkgconfig': + # If by chance pkg-config knows the bin dir... + bindir = dep.get_variable(pkgconfig='bindir', default_value='') + if bindir: + results.add(bindir) + continue + + results.update(filter(None, map(cls.search_dll_path, dep.link_args))) # pylint: disable=bad-builtin + + for i in chain(target.link_targets, target.link_whole_targets): + if isinstance(i, build.BuildTarget): + results.update(cls.extract_dll_paths(i)) + + return results + def determine_windows_extra_paths( self, target: T.Union[build.BuildTarget, build.CustomTarget, programs.ExternalProgram, mesonlib.File, str], extra_bdeps: T.Sequence[T.Union[build.BuildTarget, build.CustomTarget]]) -> T.List[str]: @@ -1063,8 +1187,8 @@ class Backend: if isinstance(target, build.BuildTarget): prospectives.update(target.get_transitive_link_deps()) # External deps - for deppath in self.rpaths_for_non_system_absolute_shared_libraries(target, exclude_system=False): - result.add(os.path.normpath(os.path.join(self.environment.get_build_dir(), deppath))) + result.update(self.extract_dll_paths(target)) + for bdep in extra_bdeps: prospectives.add(bdep) if isinstance(bdep, build.BuildTarget): @@ -1115,6 +1239,11 @@ class Backend: if isinstance(exe, build.CustomTarget): extra_bdeps = list(exe.get_transitive_build_target_deps()) extra_paths = self.determine_windows_extra_paths(exe, extra_bdeps) + for a in t.cmd_args: + if isinstance(a, build.BuildTarget): + for p in self.determine_windows_extra_paths(a, []): + if p not in extra_paths: + extra_paths.append(p) else: extra_paths = [] @@ -1127,8 +1256,6 @@ class Backend: depends.add(a) elif isinstance(a, build.CustomTargetIndex): depends.add(a.target) - if isinstance(a, build.BuildTarget): - extra_paths += self.determine_windows_extra_paths(a, []) if isinstance(a, mesonlib.File): a = os.path.join(self.environment.get_build_dir(), a.rel_to_builddir(self.build_to_src)) @@ -1187,11 +1314,19 @@ class Backend: return outputs def generate_depmf_install(self, d: InstallData) -> None: - if self.build.dep_manifest_name is None: - return + depmf_path = self.build.dep_manifest_name + if depmf_path is None: + option_dir = self.environment.coredata.get_option(OptionKey('licensedir')) + assert isinstance(option_dir, str), 'for mypy' + if option_dir: + depmf_path = os.path.join(option_dir, 'depmf.json') + else: + return ifilename = os.path.join(self.environment.get_build_dir(), 'depmf.json') - ofilename = os.path.join(self.environment.get_prefix(), self.build.dep_manifest_name) - out_name = os.path.join('{prefix}', self.build.dep_manifest_name) + ofilename = os.path.join(self.environment.get_prefix(), depmf_path) + odirname = os.path.join(self.environment.get_prefix(), os.path.dirname(depmf_path)) + out_name = os.path.join('{prefix}', depmf_path) + out_dir = os.path.join('{prefix}', os.path.dirname(depmf_path)) mfobj = {'type': 'dependency manifest', 'version': '1.0', 'projects': {k: v.to_json() for k, v in self.build.dep_manifest.items()}} with open(ifilename, 'w', encoding='utf-8') as f: @@ -1199,6 +1334,12 @@ class Backend: # Copy file from, to, and with mode unchanged d.data.append(InstallDataBase(ifilename, ofilename, out_name, None, '', tag='devel', data_type='depmf')) + for m in self.build.dep_manifest.values(): + for ifilename, name in m.license_files: + ofilename = os.path.join(odirname, name.relative_name()) + out_name = os.path.join(out_dir, name.relative_name()) + d.data.append(InstallDataBase(ifilename, ofilename, out_name, None, + m.subproject, tag='devel', data_type='depmf')) def get_regen_filelist(self) -> T.List[str]: '''List of all files whose alteration means that the build @@ -1337,7 +1478,7 @@ class Backend: srcs += fname return srcs - def get_custom_target_depend_files(self, target: build.CustomTarget, absolute_paths: bool = False) -> T.List[str]: + def get_target_depend_files(self, target: T.Union[build.CustomTarget, build.BuildTarget], absolute_paths: bool = False) -> T.List[str]: deps: T.List[str] = [] for i in target.depend_files: if isinstance(i, mesonlib.File): @@ -1471,8 +1612,8 @@ class Backend: cmd = [i.replace('\\', '/') for i in cmd] return inputs, outputs, cmd - def get_run_target_env(self, target: build.RunTarget) -> build.EnvironmentVariables: - env = target.env if target.env else build.EnvironmentVariables() + def get_run_target_env(self, target: build.RunTarget) -> mesonlib.EnvironmentVariables: + env = target.env if target.env else mesonlib.EnvironmentVariables() if target.default_env: introspect_cmd = join_args(self.environment.get_build_command() + ['introspect']) env.set('MESON_SOURCE_ROOT', [self.environment.get_source_dir()]) @@ -1494,7 +1635,6 @@ class Backend: mlog.log(f'Running postconf script {name!r}') run_exe(s, env) - @lru_cache(maxsize=1) def create_install_data(self) -> InstallData: strip_bin = self.environment.lookup_binary_entry(MachineChoice.HOST, 'strip') if strip_bin is None: @@ -1708,10 +1848,8 @@ class Backend: outdir_name = os.path.join('{includedir}', subdir) for f in h.get_sources(): - if not isinstance(f, File): - raise MesonException(f'Invalid header type {f!r} can\'t be installed') abspath = f.absolute_path(srcdir, builddir) - i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel') + i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel', follow_symlinks=h.follow_symlinks) d.headers.append(i) def generate_man_install(self, d: InstallData) -> None: @@ -1750,16 +1888,14 @@ class Backend: assert isinstance(de, build.Data) subdir = de.install_dir subdir_name = de.install_dir_name - if not subdir: - subdir = os.path.join(self.environment.get_datadir(), self.interpreter.build.project_name) - subdir_name = os.path.join('{datadir}', self.interpreter.build.project_name) for src_file, dst_name in zip(de.sources, de.rename): assert isinstance(src_file, mesonlib.File) dst_abs = os.path.join(subdir, dst_name) dstdir_name = os.path.join(subdir_name, dst_name) tag = de.install_tag or self.guess_install_tag(dst_abs) i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, dstdir_name, - de.install_mode, de.subproject, tag=tag, data_type=de.data_type) + de.install_mode, de.subproject, tag=tag, data_type=de.data_type, + follow_symlinks=de.follow_symlinks) d.data.append(i) def generate_symlink_install(self, d: InstallData) -> None: @@ -1790,7 +1926,8 @@ class Backend: dst_dir = os.path.join(dst_dir, os.path.basename(src_dir)) dst_name = os.path.join(dst_name, os.path.basename(src_dir)) tag = sd.install_tag or self.guess_install_tag(os.path.join(sd.install_dir, 'dummy')) - i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject, tag) + i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject, tag, + follow_symlinks=sd.follow_symlinks) d.install_subdirs.append(i) def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']: @@ -1843,8 +1980,8 @@ class Backend: return [] - def get_devenv(self) -> build.EnvironmentVariables: - env = build.EnvironmentVariables() + def get_devenv(self) -> mesonlib.EnvironmentVariables: + env = mesonlib.EnvironmentVariables() extra_paths = set() library_paths = set() build_machine = self.environment.machines[MachineChoice.BUILD] diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index c583024..049ae25 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -30,16 +30,15 @@ import typing as T from . import backends from .. import modules -from ..modules import gnome from .. import environment, mesonlib from .. import build from .. import mlog from .. import compilers from ..arglist import CompilerArgs from ..compilers import Compiler -from ..linkers import ArLinker, RSPFileSyntax +from ..linkers import ArLikeLinker, RSPFileSyntax from ..mesonlib import ( - File, LibType, MachineChoice, MesonException, OrderedSet, PerMachine, + File, LibType, MachineChoice, MesonBugException, MesonException, OrderedSet, PerMachine, ProgressBar, quote_arg ) from ..mesonlib import get_compiler_for_source, has_path_sep, OptionKey @@ -50,12 +49,13 @@ if T.TYPE_CHECKING: from typing_extensions import Literal from .._typing import ImmutableListProtocol - from ..build import ExtractedObjects + from ..build import ExtractedObjects, LibTypes from ..interpreter import Interpreter - from ..linkers import DynamicLinker, StaticLinker + from ..linkers.linkers import DynamicLinker, StaticLinker from ..compilers.cs import CsCompiler from ..compilers.fortran import FortranCompiler + CommandArgOrStr = T.List[T.Union['NinjaCommandArg', str]] RUST_EDITIONS = Literal['2015', '2018', '2021'] @@ -64,20 +64,20 @@ FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$" FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)" FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)" -def cmd_quote(s): +def cmd_quote(arg: str) -> str: # see: https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw#remarks # backslash escape any existing double quotes # any existing backslashes preceding a quote are doubled - s = re.sub(r'(\\*)"', lambda m: '\\' * (len(m.group(1)) * 2 + 1) + '"', s) + arg = re.sub(r'(\\*)"', lambda m: '\\' * (len(m.group(1)) * 2 + 1) + '"', arg) # any terminal backslashes likewise need doubling - s = re.sub(r'(\\*)$', lambda m: '\\' * (len(m.group(1)) * 2), s) + arg = re.sub(r'(\\*)$', lambda m: '\\' * (len(m.group(1)) * 2), arg) # and double quote - s = f'"{s}"' + arg = f'"{arg}"' - return s + return arg -def gcc_rsp_quote(s): +def gcc_rsp_quote(s: str) -> str: # see: the function buildargv() in libiberty # # this differs from sh-quoting in that a backslash *always* escapes the @@ -99,7 +99,7 @@ else: rmfile_prefix = ['rm', '-f', '{}', '&&'] -def get_rsp_threshold(): +def get_rsp_threshold() -> int: '''Return a conservative estimate of the commandline size in bytes above which a response file should be used. May be overridden for debugging by setting environment variable MESON_RSP_THRESHOLD.''' @@ -114,7 +114,7 @@ def get_rsp_threshold(): # single argument; see MAX_ARG_STRLEN limit = 131072 # Be conservative - limit = limit / 2 + limit = limit // 2 return int(os.environ.get('MESON_RSP_THRESHOLD', limit)) # a conservative estimate of the command-line length limit @@ -129,7 +129,7 @@ raw_names = {'DEPFILE_UNQUOTED', 'DESC', 'pool', 'description', 'targetdep', 'dy NINJA_QUOTE_BUILD_PAT = re.compile(r"[$ :\n]") NINJA_QUOTE_VAR_PAT = re.compile(r"[$ \n]") -def ninja_quote(text: str, is_build_line=False) -> str: +def ninja_quote(text: str, is_build_line: bool = False) -> str: if is_build_line: quote_re = NINJA_QUOTE_BUILD_PAT else: @@ -159,22 +159,22 @@ class Quoting(Enum): none = 3 class NinjaCommandArg: - def __init__(self, s, quoting = Quoting.both): + def __init__(self, s: str, quoting: Quoting = Quoting.both) -> None: self.s = s self.quoting = quoting - def __str__(self): + def __str__(self) -> str: return self.s @staticmethod - def list(l, q): + def list(l: T.List[str], q: Quoting) -> T.List[NinjaCommandArg]: return [NinjaCommandArg(i, q) for i in l] +@dataclass class NinjaComment: - def __init__(self, comment): - self.comment = comment + comment: str - def write(self, outfile): + def write(self, outfile: T.TextIO) -> None: for l in self.comment.split('\n'): outfile.write('# ') outfile.write(l) @@ -182,11 +182,12 @@ class NinjaComment: outfile.write('\n') class NinjaRule: - def __init__(self, rule, command, args, description, - rspable = False, deps = None, depfile = None, extra = None, + def __init__(self, rule: str, command: CommandArgOrStr, args: CommandArgOrStr, + description: str, rspable: bool = False, deps: T.Optional[str] = None, + depfile: T.Optional[str] = None, extra: T.Optional[str] = None, rspfile_quote_style: RSPFileSyntax = RSPFileSyntax.GCC): - def strToCommandArg(c): + def strToCommandArg(c: T.Union[NinjaCommandArg, str]) -> NinjaCommandArg: if isinstance(c, NinjaCommandArg): return c @@ -209,8 +210,8 @@ class NinjaRule: return NinjaCommandArg(c) self.name = rule - self.command = [strToCommandArg(c) for c in command] # includes args which never go into a rspfile - self.args = [strToCommandArg(a) for a in args] # args which will go into a rspfile, if used + self.command: T.List[NinjaCommandArg] = [strToCommandArg(c) for c in command] # includes args which never go into a rspfile + self.args: T.List[NinjaCommandArg] = [strToCommandArg(a) for a in args] # args which will go into a rspfile, if used self.description = description self.deps = deps # depstyle 'gcc' or 'msvc' self.depfile = depfile @@ -235,9 +236,11 @@ class NinjaRule: # fallthrough return ninja_quote(qf(str(x))) - def write(self, outfile): + def write(self, outfile: T.TextIO) -> None: + rspfile_args = self.args if self.rspfile_quote_style is RSPFileSyntax.MSVC: rspfile_quote_func = cmd_quote + rspfile_args = [NinjaCommandArg('$in_newline', arg.quoting) if arg.s == '$in' else arg for arg in rspfile_args] else: rspfile_quote_func = gcc_rsp_quote @@ -252,7 +255,7 @@ class NinjaRule: if rsp == '_RSP': outfile.write(' command = {} @$out.rsp\n'.format(' '.join([self._quoter(x) for x in self.command]))) outfile.write(' rspfile = $out.rsp\n') - outfile.write(' rspfile_content = {}\n'.format(' '.join([self._quoter(x, rspfile_quote_func) for x in self.args]))) + outfile.write(' rspfile_content = {}\n'.format(' '.join([self._quoter(x, rspfile_quote_func) for x in rspfile_args]))) else: outfile.write(' command = {}\n'.format(' '.join([self._quoter(x) for x in self.command + self.args]))) if self.deps: @@ -298,7 +301,7 @@ class NinjaRule: return estimate class NinjaBuildElement: - def __init__(self, all_outputs, outfilenames, rulename, infilenames, implicit_outs=None): + def __init__(self, all_outputs: T.Set[str], outfilenames, rulename, infilenames, implicit_outs=None): self.implicit_outfilenames = implicit_outs or [] if isinstance(outfilenames, str): self.outfilenames = [outfilenames] @@ -316,7 +319,7 @@ class NinjaBuildElement: self.all_outputs = all_outputs self.output_errors = '' - def add_dep(self, dep): + def add_dep(self, dep: T.Union[str, T.List[str]]) -> None: if isinstance(dep, list): self.deps.update(dep) else: @@ -328,7 +331,7 @@ class NinjaBuildElement: else: self.orderdeps.add(dep) - def add_item(self, name, elems): + def add_item(self, name: str, elems: T.Union[str, T.List[str, CompilerArgs]]) -> None: # Always convert from GCC-style argument naming to the naming used by the # current compiler. Also filter system include paths, deduplicate, etc. if isinstance(elems, CompilerArgs): @@ -380,7 +383,8 @@ class NinjaBuildElement: if len(self.deps) > 0: line += ' | ' + ' '.join([ninja_quote(x, True) for x in sorted(self.deps)]) if len(self.orderdeps) > 0: - line += ' || ' + ' '.join([ninja_quote(x, True) for x in sorted(self.orderdeps)]) + orderdeps = [str(x) for x in self.orderdeps] + line += ' || ' + ' '.join([ninja_quote(x, True) for x in sorted(orderdeps)]) line += '\n' # This is the only way I could find to make this work on all # platforms including Windows command shell. Slash is a dir separator @@ -424,7 +428,7 @@ class NinjaBuildElement: for n in self.outfilenames: if n in self.all_outputs: self.output_errors = f'Multiple producers for Ninja target "{n}". Please rename your targets.' - self.all_outputs[n] = True + self.all_outputs.add(n) @dataclass class RustDep: @@ -483,12 +487,13 @@ class NinjaBackend(backends.Backend): self.name = 'ninja' self.ninja_filename = 'build.ninja' self.fortran_deps = {} - self.all_outputs = {} + self.all_outputs: T.Set[str] = set() self.introspection_data = {} self.created_llvm_ir_rule = PerMachine(False, False) self.rust_crates: T.Dict[str, RustCrate] = {} + self.implicit_meson_outs = [] - def create_phony_target(self, all_outputs, dummy_outfile, rulename, phony_infilename, implicit_outs=None): + def create_phony_target(self, dummy_outfile: str, rulename: str, phony_infilename: str) -> NinjaBuildElement: ''' We need to use aliases for targets that might be used as directory names to workaround a Ninja bug that breaks `ninja -t clean`. @@ -500,10 +505,10 @@ class NinjaBackend(backends.Backend): raise AssertionError(f'Invalid usage of create_phony_target with {dummy_outfile!r}') to_name = f'meson-internal__{dummy_outfile}' - elem = NinjaBuildElement(all_outputs, dummy_outfile, 'phony', to_name) + elem = NinjaBuildElement(self.all_outputs, dummy_outfile, 'phony', to_name) self.add_build(elem) - return NinjaBuildElement(all_outputs, to_name, rulename, phony_infilename, implicit_outs) + return NinjaBuildElement(self.all_outputs, to_name, rulename, phony_infilename) def detect_vs_dep_prefix(self, tempfilename): '''VS writes its dependency in a locale dependent format. @@ -512,8 +517,8 @@ class NinjaBackend(backends.Backend): for compiler in self.environment.coredata.compilers.host.values(): # Have to detect the dependency format - # IFort on windows is MSVC like, but doesn't have /showincludes - if compiler.language == 'fortran': + # IFort / masm on windows is MSVC like, but doesn't have /showincludes + if compiler.language in {'fortran', 'masm'}: continue if compiler.id == 'pgi' and mesonlib.is_windows(): # for the purpose of this function, PGI doesn't act enough like MSVC @@ -523,8 +528,9 @@ class NinjaBackend(backends.Backend): else: # None of our compilers are MSVC, we're done. return open(tempfilename, 'a', encoding='utf-8') + filebase = 'incdetect.' + compilers.lang_suffixes[compiler.language][0] filename = os.path.join(self.environment.get_scratch_dir(), - 'incdetect.c') + filebase) with open(filename, 'w', encoding='utf-8') as f: f.write(dedent('''\ #include @@ -536,7 +542,7 @@ class NinjaBackend(backends.Backend): # Python strings leads to failure. We _must_ do this detection # in raw byte mode and write the result in raw bytes. pc = subprocess.Popen(compiler.get_exelist() + - ['/showIncludes', '/c', 'incdetect.c'], + ['/showIncludes', '/c', filebase], cwd=self.environment.get_scratch_dir(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = pc.communicate() @@ -544,7 +550,7 @@ class NinjaBackend(backends.Backend): # We want to match 'Note: including file: ' in the line # 'Note: including file: d:\MyDir\include\stdio.h', however # different locales have different messages with a different - # number of colons. Match up to the the drive name 'd:\'. + # number of colons. Match up to the drive name 'd:\'. # When used in cross compilation, the path separator is a # forward slash rather than a backslash so handle both; i.e. # the path is /MyDir/include/stdio.h. @@ -571,9 +577,13 @@ class NinjaBackend(backends.Backend): raise MesonException(f'Could not determine vs dep dependency prefix string. output: {stderr} {stdout}') - def generate(self): + def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional[dict]: + if vslite_ctx: + # We don't yet have a use case where we'd expect to make use of this, + # so no harm in catching and reporting something unexpected. + raise MesonBugException('We do not expect the ninja backend to be given a valid \'vslite_ctx\'') ninja = environment.detect_ninja_command_and_version(log=True) - if self.build.need_vsenv: + if self.environment.coredata.get_option(OptionKey('vsenv')): builddir = Path(self.environment.get_build_dir()) try: # For prettier printing, reduce to a relative path. If @@ -610,26 +620,40 @@ class NinjaBackend(backends.Backend): self.build_elements = [] self.generate_phony() self.add_build_comment(NinjaComment('Build rules for targets')) + + # Optionally capture compile args per target, for later use (i.e. VisStudio project's NMake intellisense include dirs, defines, and compile options). + if capture: + captured_compile_args_per_target = {} + for target in self.build.get_targets().values(): + if isinstance(target, build.BuildTarget): + captured_compile_args_per_target[target.get_id()] = self.generate_common_compile_args_per_src_type(target) + for t in ProgressBar(self.build.get_targets().values(), desc='Generating targets'): self.generate_target(t) + mlog.log_timestamp("Targets generated") self.add_build_comment(NinjaComment('Test rules')) self.generate_tests() + mlog.log_timestamp("Tests generated") self.add_build_comment(NinjaComment('Install rules')) self.generate_install() + mlog.log_timestamp("Install generated") self.generate_dist() + mlog.log_timestamp("Dist generated") key = OptionKey('b_coverage') if (key in self.environment.coredata.options and self.environment.coredata.options[key].value): - gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, _ = environment.find_coverage_tools() + gcovr_exe, gcovr_version, lcov_exe, lcov_version, genhtml_exe, _ = environment.find_coverage_tools() if gcovr_exe or (lcov_exe and genhtml_exe): self.add_build_comment(NinjaComment('Coverage rules')) self.generate_coverage_rules(gcovr_exe, gcovr_version) + mlog.log_timestamp("Coverage rules generated") else: # FIXME: since we explicitly opted in, should this be an error? # The docs just say these targets will be created "if possible". mlog.warning('Need gcovr or lcov/genhtml to generate any coverage reports') self.add_build_comment(NinjaComment('Suffix')) self.generate_utils() + mlog.log_timestamp("Utils generated") self.generate_ending() self.write_rules(outfile) @@ -642,12 +666,15 @@ class NinjaBackend(backends.Backend): os.replace(tempfilename, outfilename) mlog.cmd_ci_include(outfilename) # For CI debugging # Refresh Ninja's caches. https://github.com/ninja-build/ninja/pull/1685 - if mesonlib.version_compare(self.ninja_version, '>=1.10.0') and os.path.exists('.ninja_deps'): - subprocess.call(self.ninja_command + ['-t', 'restat']) - subprocess.call(self.ninja_command + ['-t', 'cleandead']) + if mesonlib.version_compare(self.ninja_version, '>=1.10.0') and os.path.exists(os.path.join(self.environment.build_dir, '.ninja_log')): + subprocess.call(self.ninja_command + ['-t', 'restat'], cwd=self.environment.build_dir) + subprocess.call(self.ninja_command + ['-t', 'cleandead'], cwd=self.environment.build_dir) self.generate_compdb() self.generate_rust_project_json() + if capture: + return captured_compile_args_per_target + def generate_rust_project_json(self) -> None: """Generate a rust-analyzer compatible rust-project.json file.""" if not self.rust_crates: @@ -745,11 +772,12 @@ class NinjaBackend(backends.Backend): return False return True - def create_target_source_introspection(self, target: build.Target, comp: compilers.Compiler, parameters, sources, generated_sources): + def create_target_source_introspection(self, target: build.Target, comp: compilers.Compiler, parameters, sources, generated_sources, + unity_sources: T.Optional[T.List[mesonlib.FileOrString]] = None): ''' Adds the source file introspection information for a language of a target - Internal introspection storage formart: + Internal introspection storage format: self.introspection_data = { '': { : { @@ -780,16 +808,40 @@ class NinjaBackend(backends.Backend): 'parameters': parameters, 'sources': [], 'generated_sources': [], + 'unity_sources': [], } tgt[id_hash] = src_block - # Make source files absolute - sources = [x.absolute_path(self.source_dir, self.build_dir) if isinstance(x, File) else os.path.normpath(os.path.join(self.build_dir, x)) - for x in sources] - generated_sources = [x.absolute_path(self.source_dir, self.build_dir) if isinstance(x, File) else os.path.normpath(os.path.join(self.build_dir, x)) - for x in generated_sources] - # Add the source files - src_block['sources'] += sources - src_block['generated_sources'] += generated_sources + + def compute_path(file: mesonlib.FileOrString) -> str: + """ Make source files absolute """ + if isinstance(file, File): + return file.absolute_path(self.source_dir, self.build_dir) + return os.path.normpath(os.path.join(self.build_dir, file)) + + src_block['sources'].extend(compute_path(x) for x in sources) + src_block['generated_sources'].extend(compute_path(x) for x in generated_sources) + if unity_sources: + src_block['unity_sources'].extend(compute_path(x) for x in unity_sources) + + def create_target_linker_introspection(self, target: build.Target, linker: T.Union[Compiler, StaticLinker], parameters): + tid = target.get_id() + tgt = self.introspection_data[tid] + lnk_hash = tuple(parameters) + lnk_block = tgt.get(lnk_hash, None) + if lnk_block is None: + if isinstance(parameters, CompilerArgs): + parameters = parameters.to_native(copy=True) + + if isinstance(linker, Compiler): + linkers = linker.get_linker_exelist() + else: + linkers = linker.get_exelist() + + lnk_block = { + 'linker': linkers, + 'parameters': parameters, + } + tgt[lnk_hash] = lnk_block def generate_target(self, target): try: @@ -829,7 +881,11 @@ class NinjaBackend(backends.Backend): self.generate_swift_target(target) return - # Pre-existing target C/C++ sources to be built; dict of full path to + # CompileTarget compiles all its sources and does not do a final link. + # This is, for example, a preprocessor. + is_compile_target = isinstance(target, build.CompileTarget) + + # Preexisting target C/C++ sources to be built; dict of full path to # source relative to build root and the original File object. target_sources: T.MutableMapping[str, File] @@ -838,7 +894,7 @@ class NinjaBackend(backends.Backend): generated_sources: T.MutableMapping[str, File] # List of sources that have been transpiled from a DSL (like Vala) into - # a language that is haneled below, such as C or C++ + # a language that is handled below, such as C or C++ transpiled_sources: T.List[str] if 'vala' in target.compilers: @@ -878,7 +934,7 @@ class NinjaBackend(backends.Backend): mlog.log(mlog.red('FIXME'), msg) # Get a list of all generated headers that will be needed while building - # this target's sources (generated sources and pre-existing sources). + # this target's sources (generated sources and preexisting sources). # This will be set as dependencies of all the target's sources. At the # same time, also deal with generated sources that need to be compiled. generated_source_files = [] @@ -895,6 +951,8 @@ class NinjaBackend(backends.Backend): obj_list.append(rel_src) elif self.environment.is_library(rel_src) or modules.is_module_library(rel_src): pass + elif is_compile_target: + generated_source_files.append(raw_src) else: # Assume anything not specifically a source file is a header. This is because # people generate files with weird suffixes (.inc, .fh) that they then include @@ -920,7 +978,7 @@ class NinjaBackend(backends.Backend): if s.split('.')[-1] in compilers.lang_suffixes['d']: d_generated_deps.append(o) - use_pch = self.environment.coredata.options.get(OptionKey('b_pch')) + use_pch = self.target_uses_pch(target) if use_pch and target.has_pch(): pch_objects = self.generate_pch(target, header_deps=header_deps) else: @@ -930,7 +988,7 @@ class NinjaBackend(backends.Backend): obj_targets = [t for t in od if t.uses_fortran()] obj_list.extend(o) - fortran_order_deps = [self.get_target_filename(t) for t in obj_targets] + fortran_order_deps = [File(True, *os.path.split(self.get_target_filename(t))) for t in obj_targets] fortran_inc_args: T.List[str] = [] if target.uses_fortran(): fortran_inc_args = mesonlib.listify([target.compilers['fortran'].get_include_args( @@ -963,9 +1021,9 @@ class NinjaBackend(backends.Backend): o, s = self.generate_single_compile(target, src, 'vala', [], header_deps) obj_list.append(o) - # Generate compile targets for all the pre-existing sources for this target + # Generate compile targets for all the preexisting sources for this target for src in target_sources.values(): - if not self.environment.is_header(src): + if not self.environment.is_header(src) or is_compile_target: if self.environment.is_llvm_ir(src): o, s = self.generate_llvm_ir_compile(target, src) obj_list.append(o) @@ -984,11 +1042,11 @@ class NinjaBackend(backends.Backend): if is_unity: for src in self.generate_unity_files(target, unity_src): o, s = self.generate_single_compile(target, src, True, unity_deps + header_deps + d_generated_deps, - fortran_order_deps, fortran_inc_args) + fortran_order_deps, fortran_inc_args, unity_src) obj_list.append(o) compiled_sources.append(s) source2object[s] = o - if isinstance(target, build.CompileTarget): + if is_compile_target: # Skip the link stage for this special type of target return linker, stdlib_args = self.determine_linker_and_stdlib_args(target) @@ -999,6 +1057,12 @@ class NinjaBackend(backends.Backend): elem = self.generate_link(target, outname, final_obj_list, linker, pch_objects, stdlib_args=stdlib_args) self.generate_dependency_scan_target(target, compiled_sources, source2object, generated_source_files, fortran_order_deps) self.add_build(elem) + #In AIX, we archive shared libraries. If the instance is a shared library, we add a command to archive the shared library + #object and create the build element. + if isinstance(target, build.SharedLibrary) and self.environment.machines[target.for_machine].is_aix(): + if target.aix_so_archive: + elem = NinjaBuildElement(self.all_outputs, linker.get_archive_name(outname), 'AIX_LINKER', [outname]) + self.add_build(elem) def should_use_dyndeps_for_target(self, target: 'build.BuildTarget') -> bool: if mesonlib.version_compare(self.ninja_version, '<1.10.0'): @@ -1007,13 +1071,13 @@ class NinjaBackend(backends.Backend): return True if 'cpp' not in target.compilers: return False - if '-fmodules-ts' in target.extra_args.get('cpp', []): + if '-fmodules-ts' in target.extra_args['cpp']: return True # Currently only the preview version of Visual Studio is supported. cpp = target.compilers['cpp'] if cpp.get_id() != 'msvc': return False - cppversion = self.environment.coredata.options[OptionKey('std', machine=target.for_machine, lang='cpp')].value + cppversion = target.get_option(OptionKey('std', machine=target.for_machine, lang='cpp')) if cppversion not in ('latest', 'c++latest', 'vc++latest'): return False if not mesonlib.current_vs_supports_modules(): @@ -1022,8 +1086,8 @@ class NinjaBackend(backends.Backend): return False return True - def generate_dependency_scan_target(self, target, compiled_sources, source2object, generated_source_files: T.List[mesonlib.File], - object_deps: T.List[str]) -> None: + def generate_dependency_scan_target(self, target: build.BuildTarget, compiled_sources, source2object, generated_source_files: T.List[mesonlib.File], + object_deps: T.List['mesonlib.FileOrString']) -> None: if not self.should_use_dyndeps_for_target(target): return depscan_file = self.get_dep_scan_file_for(target) @@ -1034,8 +1098,8 @@ class NinjaBackend(backends.Backend): rule_name = 'depscan' scan_sources = self.select_sources_to_scan(compiled_sources) - # Dump the sources as a json list. This avoids potential probllems where - # the number of sources passed to depscan exceedes the limit imposed by + # Dump the sources as a json list. This avoids potential problems where + # the number of sources passed to depscan exceeds the limit imposed by # the OS. with open(json_abs, 'w', encoding='utf-8') as f: json.dump(scan_sources, f) @@ -1086,11 +1150,11 @@ class NinjaBackend(backends.Backend): deps.append(os.path.join(self.get_target_dir(i), output)) return deps - def generate_custom_target(self, target): + def generate_custom_target(self, target: build.CustomTarget): self.custom_target_generator_inputs(target) (srcs, ofilenames, cmd) = self.eval_custom_target_command(target) deps = self.unwrap_dep_list(target) - deps += self.get_custom_target_depend_files(target) + deps += self.get_target_depend_files(target) if target.build_always_stale: deps.append('PHONY') if target.depfile is None: @@ -1124,7 +1188,7 @@ class NinjaBackend(backends.Backend): elem.add_item('pool', 'console') full_name = Path(target.subdir, target.name).as_posix() elem.add_item('COMMAND', cmd) - elem.add_item('description', f'Generating {full_name} with a custom command{cmd_type}') + elem.add_item('description', target.description.format(full_name) + cmd_type) self.add_build(elem) self.processed_targets.add(target.get_id()) @@ -1148,12 +1212,12 @@ class NinjaBackend(backends.Backend): env=target_env, verbose=True) cmd_type = f' (wrapped by meson {reason})' if reason else '' - elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', []) + elem = self.create_phony_target(target_name, 'CUSTOM_COMMAND', []) elem.add_item('COMMAND', meson_exe_cmd) elem.add_item('description', f'Running external command {target.name}{cmd_type}') elem.add_item('pool', 'console') deps = self.unwrap_dep_list(target) - deps += self.get_custom_target_depend_files(target) + deps += self.get_target_depend_files(target) elem.add_dep(deps) self.add_build(elem) self.processed_targets.add(target.get_id()) @@ -1179,38 +1243,38 @@ class NinjaBackend(backends.Backend): (['--use_llvm_cov'] if use_llvm_cov else [])) def generate_coverage_rules(self, gcovr_exe: T.Optional[str], gcovr_version: T.Optional[str]): - e = self.create_phony_target(self.all_outputs, 'coverage', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('coverage', 'CUSTOM_COMMAND', 'PHONY') self.generate_coverage_command(e, []) e.add_item('description', 'Generates coverage reports') self.add_build(e) self.generate_coverage_legacy_rules(gcovr_exe, gcovr_version) def generate_coverage_legacy_rules(self, gcovr_exe: T.Optional[str], gcovr_version: T.Optional[str]): - e = self.create_phony_target(self.all_outputs, 'coverage-html', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('coverage-html', 'CUSTOM_COMMAND', 'PHONY') self.generate_coverage_command(e, ['--html']) e.add_item('description', 'Generates HTML coverage report') self.add_build(e) if gcovr_exe: - e = self.create_phony_target(self.all_outputs, 'coverage-xml', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('coverage-xml', 'CUSTOM_COMMAND', 'PHONY') self.generate_coverage_command(e, ['--xml']) e.add_item('description', 'Generates XML coverage report') self.add_build(e) - e = self.create_phony_target(self.all_outputs, 'coverage-text', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('coverage-text', 'CUSTOM_COMMAND', 'PHONY') self.generate_coverage_command(e, ['--text']) e.add_item('description', 'Generates text coverage report') self.add_build(e) if mesonlib.version_compare(gcovr_version, '>=4.2'): - e = self.create_phony_target(self.all_outputs, 'coverage-sonarqube', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('coverage-sonarqube', 'CUSTOM_COMMAND', 'PHONY') self.generate_coverage_command(e, ['--sonarqube']) e.add_item('description', 'Generates Sonarqube XML coverage report') self.add_build(e) def generate_install(self): self.create_install_data_files() - elem = self.create_phony_target(self.all_outputs, 'install', 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target('install', 'CUSTOM_COMMAND', 'PHONY') elem.add_dep('all') elem.add_item('DESC', 'Installing files.') elem.add_item('COMMAND', self.environment.get_build_command() + ['install', '--no-rebuild']) @@ -1224,7 +1288,7 @@ class NinjaBackend(backends.Backend): cmd += ['--no-stdsplit'] if self.environment.coredata.get_option(OptionKey('errorlogs')): cmd += ['--print-errorlogs'] - elem = self.create_phony_target(self.all_outputs, 'test', 'CUSTOM_COMMAND', ['all', 'PHONY']) + elem = self.create_phony_target('test', 'CUSTOM_COMMAND', ['all', 'PHONY']) elem.add_item('COMMAND', cmd) elem.add_item('DESC', 'Running all tests.') elem.add_item('pool', 'console') @@ -1234,7 +1298,7 @@ class NinjaBackend(backends.Backend): cmd = self.environment.get_build_command(True) + [ 'test', '--benchmark', '--logbase', 'benchmarklog', '--num-processes=1', '--no-rebuild'] - elem = self.create_phony_target(self.all_outputs, 'benchmark', 'CUSTOM_COMMAND', ['all', 'PHONY']) + elem = self.create_phony_target('benchmark', 'CUSTOM_COMMAND', ['all', 'PHONY']) elem.add_item('COMMAND', cmd) elem.add_item('DESC', 'Running benchmark suite.') elem.add_item('pool', 'console') @@ -1266,25 +1330,27 @@ class NinjaBackend(backends.Backend): ['--internal', 'regenerate', self.environment.get_source_dir(), - self.environment.get_build_dir()] + # Ninja always runs from the build_dir. This includes cases where the user moved the + # build directory and invalidated most references. Make sure it still regenerates. + '.'] self.add_rule(NinjaRule('REGENERATE_BUILD', c, [], 'Regenerating build files.', extra='generator = 1')) - def add_rule_comment(self, comment): + def add_rule_comment(self, comment: NinjaComment) -> None: self.rules.append(comment) - def add_build_comment(self, comment): + def add_build_comment(self, comment: NinjaComment) -> None: self.build_elements.append(comment) - def add_rule(self, rule): + def add_rule(self, rule: NinjaRule) -> None: if rule.name in self.ruledict: raise MesonException(f'Tried to add rule {rule.name} twice.') self.rules.append(rule) self.ruledict[rule.name] = rule - def add_build(self, build): + def add_build(self, build: NinjaBuildElement) -> None: build.check_outputs() self.build_elements.append(build) @@ -1293,9 +1359,9 @@ class NinjaBackend(backends.Backend): if build.rulename in self.ruledict: build.rule = self.ruledict[build.rulename] else: - mlog.warning(f"build statement for {build.outfilenames} references non-existent rule {build.rulename}") + mlog.warning(f"build statement for {build.outfilenames} references nonexistent rule {build.rulename}") - def write_rules(self, outfile): + def write_rules(self, outfile: T.TextIO) -> None: for b in self.build_elements: if isinstance(b, NinjaBuildElement): b.count_rule_references() @@ -1303,11 +1369,12 @@ class NinjaBackend(backends.Backend): for r in self.rules: r.write(outfile) - def write_builds(self, outfile): + def write_builds(self, outfile: T.TextIO) -> None: for b in ProgressBar(self.build_elements, desc='Writing build.ninja'): b.write(outfile) + mlog.log_timestamp("build.ninja generated") - def generate_phony(self): + def generate_phony(self) -> None: self.add_build_comment(NinjaComment('Phony build target, always out of date')) elem = NinjaBuildElement(self.all_outputs, 'PHONY', 'phony', '') self.add_build(elem) @@ -1398,7 +1465,7 @@ class NinjaBackend(backends.Backend): compiler = target.compilers['cs'] rel_srcs = [os.path.normpath(s.rel_to_builddir(self.build_to_src)) for s in src_list] deps = [] - commands = compiler.compiler_args(target.extra_args.get('cs', [])) + commands = compiler.compiler_args(target.extra_args['cs']) commands += compiler.get_buildtype_args(buildtype) commands += compiler.get_optimization_args(target.get_option(OptionKey('optimization'))) commands += compiler.get_debug_args(target.get_option(OptionKey('debug'))) @@ -1504,7 +1571,7 @@ class NinjaBackend(backends.Backend): T.Tuple[T.MutableMapping[str, File], T.MutableMapping]]: """ Splits the target's sources into .vala, .gs, .vapi, and other sources. - Handles both pre-existing and generated sources. + Handles both preexisting and generated sources. Returns a tuple (vala, vapi, others) each of which is a dictionary with the keys being the path to the file (relative to the build directory) @@ -1514,7 +1581,7 @@ class NinjaBackend(backends.Backend): vapi: T.MutableMapping[str, File] = OrderedDict() others: T.MutableMapping[str, File] = OrderedDict() othersgen: T.MutableMapping[str, File] = OrderedDict() - # Split pre-existing sources + # Split preexisting sources for s in t.get_sources(): # BuildTarget sources are always mesonlib.File files which are # either in the source root, or generated with configure_file and @@ -1605,7 +1672,7 @@ class NinjaBackend(backends.Backend): valac_outputs.append(vala_c_file) args = self.generate_basic_compiler_args(target, valac) - args += valac.get_colorout_args(self.environment.coredata.options.get(OptionKey('b_colorout')).value) + args += valac.get_colorout_args(target.get_option(OptionKey('b_colorout'))) # Tell Valac to output everything in our private directory. Sadly this # means it will also preserve the directory components of Vala sources # found inside the build tree (generated sources). @@ -1630,8 +1697,6 @@ class NinjaBackend(backends.Backend): # Without this, it will write it inside c_out_dir args += ['--vapi', os.path.join('..', target.vala_vapi)] valac_outputs.append(vapiname) - target.outputs += [target.vala_header, target.vala_vapi] - target.install_tag += ['devel', 'devel'] # Install header and vapi to default locations if user requests this if len(target.install_dir) > 1 and target.install_dir[1] is True: target.install_dir[1] = self.environment.get_includedir() @@ -1642,28 +1707,18 @@ class NinjaBackend(backends.Backend): girname = os.path.join(self.get_target_dir(target), target.vala_gir) args += ['--gir', os.path.join('..', target.vala_gir)] valac_outputs.append(girname) - target.outputs.append(target.vala_gir) - target.install_tag.append('devel') # Install GIR to default location if requested by user if len(target.install_dir) > 3 and target.install_dir[3] is True: target.install_dir[3] = os.path.join(self.environment.get_datadir(), 'gir-1.0') # Detect gresources and add --gresources arguments for each for gensrc in other_src[1].values(): - if isinstance(gensrc, gnome.GResourceTarget): + if isinstance(gensrc, modules.GResourceTarget): gres_xml, = self.get_custom_target_sources(gensrc) args += ['--gresources=' + gres_xml] - extra_args = [] - - for a in target.extra_args.get('vala', []): - if isinstance(a, File): - relname = a.rel_to_builddir(self.build_to_src) - extra_dep_files.append(relname) - extra_args.append(relname) - else: - extra_args.append(a) dependency_vapis = self.determine_dep_vapis(target) extra_dep_files += dependency_vapis - args += extra_args + extra_dep_files.extend(self.get_target_depend_files(target)) + args += target.get_extra_args('vala') element = NinjaBuildElement(self.all_outputs, valac_outputs, self.compiler_to_rule_name(valac), all_files + dependency_vapis) @@ -1697,11 +1752,11 @@ class NinjaBackend(backends.Backend): ext = target.get_option(OptionKey('language', machine=target.for_machine, lang='cython')) + pyx_sources = [] # Keep track of sources we're adding to build + for src in target.get_sources(): if src.endswith('.pyx'): output = os.path.join(self.get_target_private_dir(target), f'{src}.{ext}') - args = args.copy() - args += cython.get_output_args(output) element = NinjaBuildElement( self.all_outputs, [output], self.compiler_to_rule_name(cython), @@ -1710,9 +1765,11 @@ class NinjaBackend(backends.Backend): self.add_build(element) # TODO: introspection? cython_sources.append(output) + pyx_sources.append(element) else: static_sources[src.rel_to_builddir(self.build_to_src)] = src + header_deps = [] # Keep track of generated headers for those sources for gen in target.get_generated_sources(): for ssrc in gen.get_outputs(): if isinstance(gen, GeneratedList): @@ -1720,19 +1777,27 @@ class NinjaBackend(backends.Backend): else: ssrc = os.path.join(gen.get_subdir(), ssrc) if ssrc.endswith('.pyx'): - args = args.copy() output = os.path.join(self.get_target_private_dir(target), f'{ssrc}.{ext}') - args += cython.get_output_args(output) element = NinjaBuildElement( self.all_outputs, [output], self.compiler_to_rule_name(cython), [ssrc]) element.add_item('ARGS', args) self.add_build(element) + pyx_sources.append(element) # TODO: introspection? cython_sources.append(output) else: generated_sources[ssrc] = mesonlib.File.from_built_file(gen.get_subdir(), ssrc) + # Following logic in L883-900 where we determine whether to add generated source + # as a header(order-only) dep to the .so compilation rule + if not self.environment.is_source(ssrc) and \ + not self.environment.is_object(ssrc) and \ + not self.environment.is_library(ssrc) and \ + not modules.is_module_library(ssrc): + header_deps.append(ssrc) + for source in pyx_sources: + source.add_orderdep(header_deps) return static_sources, generated_sources, cython_sources @@ -1767,8 +1832,8 @@ class NinjaBackend(backends.Backend): return orderdeps, first_file def _add_rust_project_entry(self, name: str, main_rust_file: str, args: CompilerArgs, - from_subproject: bool, is_proc_macro: bool, - output: str, deps: T.List[RustDep]) -> None: + from_subproject: bool, proc_macro_dylib_path: T.Optional[str], + deps: T.List[RustDep]) -> None: raw_edition: T.Optional[str] = mesonlib.first(reversed(args), lambda x: x.startswith('--edition')) edition: RUST_EDITIONS = '2015' if not raw_edition else raw_edition.split('=')[-1] @@ -1776,7 +1841,7 @@ class NinjaBackend(backends.Backend): arg_itr: T.Iterator[str] = iter(args) for arg in arg_itr: if arg == '--cfg': - cfg.append(next(arg)) + cfg.append(next(arg_itr)) elif arg.startswith('--cfg'): cfg.append(arg[len('--cfg'):]) @@ -1788,12 +1853,18 @@ class NinjaBackend(backends.Backend): deps, cfg, is_workspace_member=not from_subproject, - is_proc_macro=is_proc_macro, - proc_macro_dylib_path=output if is_proc_macro else None, + is_proc_macro=proc_macro_dylib_path is not None, + proc_macro_dylib_path=proc_macro_dylib_path, ) self.rust_crates[name] = crate + def _get_rust_dependency_name(self, target: build.BuildTarget, dependency: LibTypes) -> str: + # Convert crate names with dashes to underscores by default like + # cargo does as dashes can't be used as parts of identifiers + # in Rust + return target.rust_dependency_map.get(dependency.name, dependency.name).replace('-', '_') + def generate_rust_target(self, target: build.BuildTarget) -> None: rustc = target.compilers['rust'] # Rust compiler takes only the main file as input and @@ -1806,10 +1877,7 @@ class NinjaBackend(backends.Backend): self.generate_generator_list_rules(target) # dependencies need to cause a relink, they're not just for ordering - deps = [ - os.path.join(t.subdir, t.get_filename()) - for t in itertools.chain(target.link_targets, target.link_whole_targets) - ] + deps: T.List[str] = [] # Dependencies for rust-project.json project_deps: T.List[RustDep] = [] @@ -1861,16 +1929,7 @@ class NinjaBackend(backends.Backend): if main_rust_file is None: raise RuntimeError('A Rust target has no Rust sources. This is weird. Also a bug. Please report') target_name = os.path.join(target.subdir, target.get_filename()) - if isinstance(target, build.Executable): - cratetype = 'bin' - elif hasattr(target, 'rust_crate_type'): - cratetype = target.rust_crate_type - elif isinstance(target, build.SharedLibrary): - cratetype = 'dylib' - elif isinstance(target, build.StaticLibrary): - cratetype = 'rlib' - else: - raise InvalidArguments('Unknown target type for rustc.') + cratetype = target.rust_crate_type args.extend(['--crate-type', cratetype]) # If we're dynamically linking, add those arguments @@ -1881,61 +1940,125 @@ class NinjaBackend(backends.Backend): args.extend(rustc.get_linker_always_args()) args += self.generate_basic_compiler_args(target, rustc, False) - # Rustc replaces - with _. spaces are not allowed, so we replace them with underscores - args += ['--crate-name', target.name.replace('-', '_').replace(' ', '_')] + # Rustc replaces - with _. spaces or dots are not allowed, so we replace them with underscores + args += ['--crate-name', target.name.replace('-', '_').replace(' ', '_').replace('.', '_')] depfile = os.path.join(target.subdir, target.name + '.d') - args += ['--emit', f'dep-info={depfile}', '--emit', 'link'] + args += ['--emit', f'dep-info={depfile}', '--emit', f'link={target_name}'] + args += ['--out-dir', self.get_target_private_dir(target)] + args += ['-C', 'metadata=' + target.get_id()] args += target.get_extra_args('rust') - output = rustc.get_output_args(os.path.join(target.subdir, target.get_filename())) - args += output + + # Rustc always use non-debug Windows runtime. Inject the one selected + # by Meson options instead. + # https://github.com/rust-lang/rust/issues/39016 + if not isinstance(target, build.StaticLibrary): + try: + buildtype = target.get_option(OptionKey('buildtype')) + crt = target.get_option(OptionKey('b_vscrt')) + args += rustc.get_crt_link_args(crt, buildtype) + except KeyError: + pass + + if mesonlib.version_compare(rustc.version, '>= 1.67.0'): + verbatim = '+verbatim' + else: + verbatim = '' + + def _link_library(libname: str, static: bool, bundle: bool = False): + type_ = 'static' if static else 'dylib' + modifiers = [] + if not bundle and static: + modifiers.append('-bundle') + if verbatim: + modifiers.append(verbatim) + if modifiers: + type_ += ':' + ','.join(modifiers) + args.append(f'-l{type_}={libname}') + linkdirs = mesonlib.OrderedSet() external_deps = target.external_deps.copy() - for d in itertools.chain(target.link_targets, target.link_whole_targets): + target_deps = target.get_dependencies() + for d in target_deps: linkdirs.add(d.subdir) - if d.uses_rust(): + deps.append(self.get_dependency_filename(d)) + if d.uses_rust_abi(): + if d not in itertools.chain(target.link_targets, target.link_whole_targets): + # Indirect Rust ABI dependency, we only need its path in linkdirs. + continue # specify `extern CRATE_NAME=OUTPUT_FILE` for each Rust # dependency, so that collisions with libraries in rustc's # sysroot don't cause ambiguity - args += ['--extern', '{}={}'.format(d.name, os.path.join(d.subdir, d.filename))] - project_deps.append(RustDep(d.name, self.rust_crates[d.name].order)) - elif d.typename == 'static library': - # Rustc doesn't follow Meson's convention that static libraries - # are called .a, and import libraries are .lib, so we have to - # manually handle that. - if rustc.linker.id in {'link', 'lld-link'}: - args += ['-C', f'link-arg={self.get_target_filename_for_linking(d)}'] - else: - args += ['-l', f'static={d.name}'] + d_name = self._get_rust_dependency_name(target, d) + args += ['--extern', '{}={}'.format(d_name, os.path.join(d.subdir, d.filename))] + project_deps.append(RustDep(d_name, self.rust_crates[d.name].order)) + continue + + # Link a C ABI library + + if isinstance(d, build.StaticLibrary): external_deps.extend(d.external_deps) + + # Pass native libraries directly to the linker with "-C link-arg" + # because rustc's "-l:+verbatim=" is not portable and we cannot rely + # on linker to find the right library without using verbatim filename. + # For example "-lfoo" won't find "foo.so" in the case name_prefix set + # to "", or would always pick the shared library when both "libfoo.so" + # and "libfoo.a" are available. + # See https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-verbatim. + # + # However, rustc static linker (rlib and staticlib) requires using + # "-l" argument and does not rely on platform specific dynamic linker. + lib = self.get_target_filename_for_linking(d) + link_whole = d in target.link_whole_targets + if isinstance(target, build.StaticLibrary): + static = isinstance(d, build.StaticLibrary) + libname = os.path.basename(lib) if verbatim else d.name + _link_library(libname, static, bundle=link_whole) + elif link_whole: + link_whole_args = rustc.linker.get_link_whole_for([lib]) + args += [f'-Clink-arg={a}' for a in link_whole_args] else: - # Rust uses -l for non rust dependencies, but we still need to - # add dylib=foo - args += ['-l', f'dylib={d.name}'] + args.append(f'-Clink-arg={lib}') + for e in external_deps: for a in e.get_link_args(): - if a.endswith(('.dll', '.so', '.dylib')): - dir_, lib = os.path.split(a) - linkdirs.add(dir_) - lib, ext = os.path.splitext(lib) - if lib.startswith('lib'): - lib = lib[3:] - args.extend(['-l', f'dylib={lib}']) + if a in rustc.native_static_libs: + # Exclude link args that rustc already add by default + pass elif a.startswith('-L'): args.append(a) - elif a.startswith('-l'): - _type = 'static' if e.static else 'dylib' - args.extend(['-l', f'{_type}={a[2:]}']) + elif a.endswith(('.dll', '.so', '.dylib', '.a', '.lib')) and isinstance(target, build.StaticLibrary): + dir_, lib = os.path.split(a) + linkdirs.add(dir_) + if not verbatim: + lib, ext = os.path.splitext(lib) + if lib.startswith('lib'): + lib = lib[3:] + static = a.endswith(('.a', '.lib')) + _link_library(lib, static) + else: + args.append(f'-Clink-arg={a}') + for d in linkdirs: - if d == '': - d = '.' - args += ['-L', d] - has_shared_deps = any(isinstance(dep, build.SharedLibrary) for dep in target.get_dependencies()) - if isinstance(target, build.SharedLibrary) or has_shared_deps: + d = d or '.' + args.append(f'-L{d}') + + # Because of the way rustc links, this must come after any potential + # library need to link with their stdlibs (C++ and Fortran, for example) + args.extend(f'-Clink-arg={a}' for a in target.get_used_stdlib_args('rust')) + + has_shared_deps = any(isinstance(dep, build.SharedLibrary) for dep in target_deps) + has_rust_shared_deps = any(dep.uses_rust() + and dep.rust_crate_type == 'dylib' + for dep in target_deps) + + if cratetype in {'dylib', 'proc-macro'} or has_rust_shared_deps: # add prefer-dynamic if any of the Rust libraries we link - # against are dynamic, otherwise we'll end up with - # multiple implementations of crates + # against are dynamic or this is a dynamic library itself, + # otherwise we'll end up with multiple implementations of libstd. args += ['-C', 'prefer-dynamic'] + if isinstance(target, build.SharedLibrary) or has_shared_deps: # build the usual rpath arguments as well... # Set runtime-paths so we can run executables without needing to set @@ -1960,10 +2083,16 @@ class NinjaBackend(backends.Backend): for rpath_arg in rpath_args: args += ['-C', 'link-arg=' + rpath_arg + ':' + os.path.join(rustc.get_sysroot(), 'lib')] - self._add_rust_project_entry(target.name, main_rust_file, args, bool(target.subproject), - #XXX: There is a fix for this pending - getattr(target, 'rust_crate_type', '') == 'procmacro', - output, project_deps) + proc_macro_dylib_path = None + if getattr(target, 'rust_crate_type', '') == 'proc-macro': + proc_macro_dylib_path = os.path.abspath(os.path.join(target.subdir, target.get_filename())) + + self._add_rust_project_entry(target.name, + os.path.abspath(os.path.join(self.environment.build_dir, main_rust_file)), + args, + bool(target.subproject), + proc_macro_dylib_path, + project_deps) compiler_name = self.compiler_to_rule_name(rustc) element = NinjaBuildElement(self.all_outputs, target_name, compiler_name, main_rust_file) @@ -2128,7 +2257,7 @@ class NinjaBackend(backends.Backend): rsp_file_syntax() is only guaranteed to be implemented if can_linker_accept_rsp() returns True. """ - options = dict(rspable=tool.can_linker_accept_rsp()) + options = {'rspable': tool.can_linker_accept_rsp()} if options['rspable']: options['rspfile_quote_style'] = tool.rsp_file_syntax() return options @@ -2142,13 +2271,13 @@ class NinjaBackend(backends.Backend): if static_linker is None: continue rule = 'STATIC_LINKER{}'.format(self.get_rule_suffix(for_machine)) - cmdlist = [] + cmdlist: T.List[T.Union[str, NinjaCommandArg]] = [] args = ['$in'] # FIXME: Must normalize file names with pathlib.Path before writing # them out to fix this properly on Windows. See: # https://github.com/mesonbuild/meson/issues/1517 # https://github.com/mesonbuild/meson/issues/1526 - if isinstance(static_linker, ArLinker) and not mesonlib.is_windows(): + if isinstance(static_linker, ArLikeLinker) and not mesonlib.is_windows(): # `ar` has no options to overwrite archives. It always appends, # which is never what we want. Delete an existing library first if # it exists. https://github.com/mesonbuild/meson/issues/1355 @@ -2156,6 +2285,22 @@ class NinjaBackend(backends.Backend): cmdlist += static_linker.get_exelist() cmdlist += ['$LINK_ARGS'] cmdlist += NinjaCommandArg.list(static_linker.get_output_args('$out'), Quoting.none) + # The default ar on MacOS (at least through version 12), does not + # add extern'd variables to the symbol table by default, and + # requires that apple's ranlib be called with a special flag + # instead after linking + if static_linker.id == 'applear': + # This is a bit of a hack, but we assume that that we won't need + # an rspfile on MacOS, otherwise the arguments are passed to + # ranlib, not to ar + cmdlist.extend(args) + args = [] + # Ensure that we use the user-specified ranlib if any, and + # fallback to just picking up some ranlib otherwise + ranlib = self.environment.lookup_binary_entry(for_machine, 'ranlib') + if ranlib is None: + ranlib = ['ranlib'] + cmdlist.extend(['&&'] + ranlib + ['-c', '$out']) description = 'Linking static target $out' if num_pools > 0: pool = 'pool = link_pool' @@ -2183,6 +2328,13 @@ class NinjaBackend(backends.Backend): options = self._rsp_options(compiler) self.add_rule(NinjaRule(rule, command, args, description, **options, extra=pool)) + if self.environment.machines[for_machine].is_aix(): + rule = 'AIX_LINKER{}'.format(self.get_rule_suffix(for_machine)) + description = 'Archiving AIX shared library' + cmdlist = compiler.get_command_to_archive_shlib() + args = [] + options = {} + self.add_rule(NinjaRule(rule, cmdlist, args, description, **options, extra=None)) args = self.environment.get_build_command() + \ ['--internal', @@ -2220,9 +2372,18 @@ class NinjaBackend(backends.Backend): def generate_cython_compile_rules(self, compiler: 'Compiler') -> None: rule = self.compiler_to_rule_name(compiler) - command = compiler.get_exelist() + ['$ARGS', '$in'] description = 'Compiling Cython source $in' - self.add_rule(NinjaRule(rule, command, [], description, extra='restat = 1')) + command = compiler.get_exelist() + + depargs = compiler.get_dependency_gen_args('$out', '$DEPFILE') + depfile = '$out.dep' if depargs else None + + args = depargs + ['$ARGS', '$in'] + args += NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) + self.add_rule(NinjaRule(rule, command + args, [], + description, + depfile=depfile, + extra='restat = 1')) def generate_rust_compile_rules(self, compiler): rule = self.compiler_to_rule_name(compiler) @@ -2299,8 +2460,12 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) self.generate_cython_compile_rules(compiler) return crstr = self.get_rule_suffix(compiler.for_machine) + options = self._rsp_options(compiler) if langname == 'fortran': self.generate_fortran_dep_hack(crstr) + # gfortran does not update the modification time of *.mod files, therefore restat is needed. + # See also: https://github.com/ninja-build/ninja/pull/2275 + options['extra'] = 'restat = 1' rule = self.compiler_to_rule_name(compiler) depargs = NinjaCommandArg.list(compiler.get_dependency_gen_args('$out', '$DEPFILE'), Quoting.none) command = compiler.get_exelist() @@ -2312,7 +2477,6 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) else: deps = 'gcc' depfile = '$DEPFILE' - options = self._rsp_options(compiler) self.add_rule(NinjaRule(rule, command, args, description, **options, deps=deps, depfile=depfile)) @@ -2326,7 +2490,12 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) output = [] else: output = NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) - command = compiler.get_exelist() + ['$ARGS'] + depargs + output + compiler.get_compile_only_args() + ['$in'] + + if 'mwcc' in compiler.id: + output[0].s = '-precompile' + command = compiler.get_exelist() + ['$ARGS'] + depargs + output + ['$in'] # '-c' must be removed + else: + command = compiler.get_exelist() + ['$ARGS'] + depargs + output + compiler.get_compile_only_args() + ['$in'] description = 'Precompiling header $in' if compiler.get_argument_syntax() == 'msvc': deps = 'msvc' @@ -2384,19 +2553,21 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) return args def generate_genlist_for_target(self, genlist: build.GeneratedList, target: build.BuildTarget) -> None: + for x in genlist.depends: + if isinstance(x, build.GeneratedList): + self.generate_genlist_for_target(x, target) generator = genlist.get_generator() subdir = genlist.subdir exe = generator.get_exe() - exe_arr = self.build_target_to_cmd_array(exe) infilelist = genlist.get_inputs() outfilelist = genlist.get_outputs() - extra_dependencies = self.get_custom_target_depend_files(genlist) + extra_dependencies = self.get_target_depend_files(genlist) for i, curfile in enumerate(infilelist): if len(generator.outputs) == 1: sole_output = os.path.join(self.get_target_private_dir(target), outfilelist[i]) else: sole_output = f'{curfile}' - infilename = curfile.rel_to_builddir(self.build_to_src) + infilename = curfile.rel_to_builddir(self.build_to_src, self.get_target_private_dir(target)) base_args = generator.get_arglist(infilename) outfiles = genlist.get_outputs_for(curfile) outfiles = [os.path.join(self.get_target_private_dir(target), of) for of in outfiles] @@ -2415,9 +2586,10 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) if len(generator.outputs) > 1: outfilelist = outfilelist[len(generator.outputs):] args = self.replace_paths(target, args, override_subdir=subdir) - cmdlist = exe_arr + self.replace_extra_args(args, genlist) - cmdlist, reason = self.as_meson_exe_cmdline(cmdlist[0], cmdlist[1:], - capture=outfiles[0] if generator.capture else None) + cmdlist, reason = self.as_meson_exe_cmdline(exe, + self.replace_extra_args(args, genlist), + capture=outfiles[0] if generator.capture else None, + env=genlist.env) abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) os.makedirs(abs_pdir, exist_ok=True) @@ -2432,10 +2604,10 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) what = f'{sole_output!r}' else: # since there are multiple outputs, we log the source that caused the rebuild - what = f'from {sole_output!r}.' + what = f'from {sole_output!r}' if reason: reason = f' (wrapped by meson {reason})' - elem.add_item('DESC', f'Generating {what}{reason}.') + elem.add_item('DESC', f'Generating {what}{reason}') if isinstance(exe, build.BuildTarget): elem.add_dep(self.get_target_filename(exe)) @@ -2524,7 +2696,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # inside it. This can be either the final PDB (for, say, # foo.exe) or an object pdb (for foo.obj). If the former, then # each compilation step locks the pdb file for writing, which - # is a bottleneck and object files from one target can not be + # is a bottleneck and object files from one target cannot be # used in a different target. The latter seems to be the # sensible one (and what Unix does) but there is a catch. If # you try to use precompiled headers MSVC will error out @@ -2546,7 +2718,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # has pdb file called foo.pdb. So will a static library # foo.lib, which clobbers both foo.pdb _and_ the dll file's # export library called foo.lib (by default, currently we name - # them libfoo.a to avoidt this issue). You can give the files + # them libfoo.a to avoid this issue). You can give the files # unique names such as foo_exe.pdb but VC also generates a # bunch of other files which take their names from the target # basename (i.e. "foo") and stomp on each other. @@ -2565,16 +2737,18 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # this is actually doable, please send patches. if target.has_pch(): - tfilename = self.get_target_filename_abs(target) + tfilename = self.get_target_debug_filename_abs(target) + if not tfilename: + tfilename = self.get_target_filename_abs(target) return compiler.get_compile_debugfile_args(tfilename, pch=True) else: return compiler.get_compile_debugfile_args(objfile, pch=False) - def get_link_debugfile_name(self, linker, target, outname): - return linker.get_link_debugfile_name(outname) + def get_link_debugfile_name(self, linker, target) -> T.Optional[str]: + return linker.get_link_debugfile_name(self.get_target_debug_filename(target)) - def get_link_debugfile_args(self, linker, target, outname): - return linker.get_link_debugfile_args(outname) + def get_link_debugfile_args(self, linker, target): + return linker.get_link_debugfile_args(self.get_target_debug_filename(target)) def generate_llvm_ir_compile(self, target, src): base_proxy = target.get_options() @@ -2655,7 +2829,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) is_generated: bool = False) -> 'ImmutableListProtocol[str]': # The code generated by valac is usually crap and has tons of unused # variables and such, so disable warnings for Vala C sources. - no_warn_args = (is_generated == 'vala') + no_warn_args = is_generated == 'vala' # Add compiler args and include paths from several sources; defaults, # build options, external dependencies, etc. commands = self.generate_basic_compiler_args(target, compiler, no_warn_args) @@ -2710,10 +2884,39 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) commands += compiler.get_include_args(self.get_target_private_dir(target), False) return commands + # Returns a dictionary, mapping from each compiler src type (e.g. 'c', 'cpp', etc.) to a list of compiler arg strings + # used for that respective src type. + # Currently used for the purpose of populating VisualStudio intellisense fields but possibly useful in other scenarios. + def generate_common_compile_args_per_src_type(self, target: build.BuildTarget) -> dict[str, list[str]]: + src_type_to_args = {} + + use_pch = self.target_uses_pch(target) + + for src_type_str in target.compilers.keys(): + compiler = target.compilers[src_type_str] + commands = self._generate_single_compile_base_args(target, compiler) + + # Include PCH header as first thing as it must be the first one or it will be + # ignored by gcc https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100462 + if use_pch and 'mw' not in compiler.id: + commands += self.get_pch_include_args(compiler, target) + + commands += self._generate_single_compile_target_args(target, compiler, is_generated=False) + + # Metrowerks compilers require PCH include args to come after intraprocedural analysis args + if use_pch and 'mw' in compiler.id: + commands += self.get_pch_include_args(compiler, target) + + commands = commands.compiler.compiler_args(commands) + + src_type_to_args[src_type_str] = commands.to_native() + return src_type_to_args + def generate_single_compile(self, target: build.BuildTarget, src, is_generated=False, header_deps=None, - order_deps: T.Optional[T.List[str]] = None, - extra_args: T.Optional[T.List[str]] = None) -> None: + order_deps: T.Optional[T.List['mesonlib.FileOrString']] = None, + extra_args: T.Optional[T.List[str]] = None, + unity_sources: T.Optional[T.List[mesonlib.FileOrString]] = None) -> None: """ Compiles C/C++, ObjC/ObjC++, Fortran, and D sources """ @@ -2728,17 +2931,23 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # Include PCH header as first thing as it must be the first one or it will be # ignored by gcc https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100462 - if self.environment.coredata.options.get(OptionKey('b_pch')) and is_generated != 'pch': + use_pch = self.target_uses_pch(target) and is_generated != 'pch' + if use_pch and 'mw' not in compiler.id: commands += self.get_pch_include_args(compiler, target) commands += self._generate_single_compile_target_args(target, compiler, is_generated) + + # Metrowerks compilers require PCH include args to come after intraprocedural analysis args + if use_pch and 'mw' in compiler.id: + commands += self.get_pch_include_args(compiler, target) + commands = commands.compiler.compiler_args(commands) # Create introspection information if is_generated is False: - self.create_target_source_introspection(target, compiler, commands, [src], []) + self.create_target_source_introspection(target, compiler, commands, [src], [], unity_sources) else: - self.create_target_source_introspection(target, compiler, commands, [], [src]) + self.create_target_source_introspection(target, compiler, commands, [], [src], unity_sources) build_dir = self.environment.get_build_dir() if isinstance(src, File): @@ -2761,7 +2970,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) commands += self.get_compile_debugfile_args(compiler, target, rel_obj) # PCH handling - if self.environment.coredata.options.get(OptionKey('b_pch')): + if self.target_uses_pch(target): pchlist = target.get_pch(compiler.language) else: pchlist = [] @@ -2826,9 +3035,11 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) assert isinstance(rel_src, str) return (rel_obj, rel_src.replace('\\', '/')) - def add_dependency_scanner_entries_to_element(self, target, compiler, element, src): + def add_dependency_scanner_entries_to_element(self, target: build.BuildTarget, compiler, element, src): if not self.should_use_dyndeps_for_target(target): return + if isinstance(target, build.CompileTarget): + return extension = os.path.splitext(src.fname)[1][1:] if extension != 'C': extension = extension.lower() @@ -2838,7 +3049,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) element.add_item('dyndep', dep_scan_file) element.add_orderdep(dep_scan_file) - def get_dep_scan_file_for(self, target): + def get_dep_scan_file_for(self, target: build.BuildTarget) -> str: return os.path.join(self.get_target_private_dir(target), 'depscan.dd') def add_header_deps(self, target, ninja_element, header_deps): @@ -2849,7 +3060,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) d = os.path.join(self.get_target_private_dir(target), d) ninja_element.add_dep(d) - def has_dir_part(self, fname): + def has_dir_part(self, fname: mesonlib.FileOrString) -> bool: # FIXME FIXME: The usage of this is a terrible and unreliable hack if isinstance(fname, File): return fname.subdir != '' @@ -2904,6 +3115,13 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) dep = dst + '.' + compiler.get_depfile_suffix() return commands, dep, dst, [] # Gcc does not create an object file during pch generation. + def generate_mwcc_pch_command(self, target, compiler, pch): + commands = self._generate_single_compile(target, compiler) + dst = os.path.join(self.get_target_private_dir(target), + os.path.basename(pch) + '.' + compiler.get_pch_suffix()) + dep = os.path.splitext(dst)[0] + '.' + compiler.get_depfile_suffix() + return commands, dep, dst, [] # mwcc compilers do not create an object file during pch generation. + def generate_pch(self, target, header_deps=None): header_deps = header_deps if header_deps is not None else [] pch_objects = [] @@ -2922,13 +3140,17 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) elif compiler.id == 'intel': # Intel generates on target generation continue + elif 'mwcc' in compiler.id: + src = os.path.join(self.build_to_src, target.get_source_subdir(), pch[0]) + (commands, dep, dst, objs) = self.generate_mwcc_pch_command(target, compiler, pch[0]) + extradep = None else: src = os.path.join(self.build_to_src, target.get_source_subdir(), pch[0]) (commands, dep, dst, objs) = self.generate_gcc_pch_command(target, compiler, pch[0]) extradep = None pch_objects += objs rulename = self.compiler_to_pch_rule_name(compiler) - elem = NinjaBuildElement(self.all_outputs, dst, rulename, src) + elem = NinjaBuildElement(self.all_outputs, objs + [dst], rulename, src) if extradep is not None: elem.add_dep(extradep) self.add_header_deps(target, elem, header_deps) @@ -2968,10 +3190,11 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) commands += linker.gen_import_library_args(self.get_import_filename(target)) if target.pie: commands += linker.get_pie_link_args() + if target.vs_module_defs and hasattr(linker, 'gen_vs_module_defs_args'): + commands += linker.gen_vs_module_defs_args(target.vs_module_defs.rel_to_builddir(self.build_to_src)) elif isinstance(target, build.SharedLibrary): if isinstance(target, build.SharedModule): - options = self.environment.coredata.options - commands += linker.get_std_shared_module_link_args(options) + commands += linker.get_std_shared_module_link_args(target.get_options()) else: commands += linker.get_std_shared_lib_link_args() # All shared libraries are PIC @@ -2996,16 +3219,13 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) def get_target_type_link_args_post_dependencies(self, target, linker): commands = [] if isinstance(target, build.Executable): - # If gui_app is significant on this platform, add the appropriate linker arguments. + # If win_subsystem is significant on this platform, add the appropriate linker arguments. # Unfortunately this can't be done in get_target_type_link_args, because some misguided # libraries (such as SDL2) add -mwindows to their link flags. m = self.environment.machines[target.for_machine] if m.is_windows() or m.is_cygwin(): - if target.gui_app is not None: - commands += linker.get_gui_app_args(target.gui_app) - else: - commands += linker.get_win_subsystem_args(target.win_subsystem) + commands += linker.get_win_subsystem_args(target.win_subsystem) return commands def get_link_whole_args(self, linker, target): @@ -3045,7 +3265,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) def guess_external_link_dependencies(self, linker, target, commands, internal): # Ideally the linker would generate dependency information that could be used. # But that has 2 problems: - # * currently ld can not create dependency information in a way that ninja can use: + # * currently ld cannot create dependency information in a way that ninja can use: # https://sourceware.org/bugzilla/show_bug.cgi?id=22843 # * Meson optimizes libraries from the same build using the symbol extractor. # Just letting ninja use ld generated dependencies would undo this optimization. @@ -3162,8 +3382,8 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) commands += linker.get_buildtype_linker_args(target.get_option(OptionKey('buildtype'))) # Add /DEBUG and the pdb filename when using MSVC if target.get_option(OptionKey('debug')): - commands += self.get_link_debugfile_args(linker, target, outname) - debugfile = self.get_link_debugfile_name(linker, target, outname) + commands += self.get_link_debugfile_args(linker, target) + debugfile = self.get_link_debugfile_name(linker, target) if debugfile is not None: implicit_outs += [debugfile] # Add link args specific to this BuildTarget type, such as soname args, @@ -3248,7 +3468,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # # We shouldn't check whether we are making a static library, because # in the LTO case we do use a real compiler here. - commands += linker.get_option_link_args(self.environment.coredata.options) + commands += linker.get_option_link_args(target.get_options()) dep_targets = [] dep_targets.extend(self.guess_external_link_dependencies(linker, target, commands, internal)) @@ -3264,6 +3484,7 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) elem = NinjaBuildElement(self.all_outputs, outname, linker_rule, obj_list, implicit_outs=implicit_outs) elem.add_dep(dep_targets + custom_target_libraries) elem.add_item('LINK_ARGS', commands) + self.create_target_linker_introspection(target, linker, commands) return elem def get_dependency_filename(self, t): @@ -3279,20 +3500,23 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) def generate_shlib_aliases(self, target, outdir): for alias, to, tag in target.get_aliases(): - aliasfile = os.path.join(self.environment.get_build_dir(), outdir, alias) + aliasfile = os.path.join(outdir, alias) + abs_aliasfile = os.path.join(self.environment.get_build_dir(), outdir, alias) try: - os.remove(aliasfile) + os.remove(abs_aliasfile) except Exception: pass try: - os.symlink(to, aliasfile) + os.symlink(to, abs_aliasfile) except NotImplementedError: mlog.debug("Library versioning disabled because symlinks are not supported.") except OSError: mlog.debug("Library versioning disabled because we do not have symlink creation privileges.") + else: + self.implicit_meson_outs.append(aliasfile) def generate_custom_target_clean(self, trees: T.List[str]) -> str: - e = self.create_phony_target(self.all_outputs, 'clean-ctlist', 'CUSTOM_COMMAND', 'PHONY') + e = self.create_phony_target('clean-ctlist', 'CUSTOM_COMMAND', 'PHONY') d = CleanTrees(self.environment.get_build_dir(), trees) d_file = os.path.join(self.environment.get_scratch_dir(), 'cleantrees.dat') e.add_item('COMMAND', self.environment.get_build_command() + ['--internal', 'cleantrees', d_file]) @@ -3303,13 +3527,13 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) pickle.dump(d, ofile) return 'clean-ctlist' - def generate_gcov_clean(self): - gcno_elem = self.create_phony_target(self.all_outputs, 'clean-gcno', 'CUSTOM_COMMAND', 'PHONY') + def generate_gcov_clean(self) -> None: + gcno_elem = self.create_phony_target('clean-gcno', 'CUSTOM_COMMAND', 'PHONY') gcno_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcno']) gcno_elem.add_item('description', 'Deleting gcno files') self.add_build(gcno_elem) - gcda_elem = self.create_phony_target(self.all_outputs, 'clean-gcda', 'CUSTOM_COMMAND', 'PHONY') + gcda_elem = self.create_phony_target('clean-gcda', 'CUSTOM_COMMAND', 'PHONY') gcda_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcda']) gcda_elem.add_item('description', 'Deleting gcda files') self.add_build(gcda_elem) @@ -3324,27 +3548,27 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) # affect behavior in any other way. return sorted(cmds) - def generate_dist(self): - elem = self.create_phony_target(self.all_outputs, 'dist', 'CUSTOM_COMMAND', 'PHONY') + def generate_dist(self) -> None: + elem = self.create_phony_target('dist', 'CUSTOM_COMMAND', 'PHONY') elem.add_item('DESC', 'Creating source packages') elem.add_item('COMMAND', self.environment.get_build_command() + ['dist']) elem.add_item('pool', 'console') self.add_build(elem) - def generate_scanbuild(self): + def generate_scanbuild(self) -> None: if not environment.detect_scanbuild(): return if 'scan-build' in self.all_outputs: return cmd = self.environment.get_build_command() + \ - ['--internal', 'scanbuild', self.environment.source_dir, self.environment.build_dir] + \ + ['--internal', 'scanbuild', self.environment.source_dir, self.environment.build_dir, self.build.get_subproject_dir()] + \ self.environment.get_build_command() + self.get_user_option_args() - elem = self.create_phony_target(self.all_outputs, 'scan-build', 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target('scan-build', 'CUSTOM_COMMAND', 'PHONY') elem.add_item('COMMAND', cmd) elem.add_item('pool', 'console') self.add_build(elem) - def generate_clangtool(self, name, extra_arg=None): + def generate_clangtool(self, name: str, extra_arg: T.Optional[str] = None) -> None: target_name = 'clang-' + name extra_args = [] if extra_arg: @@ -3358,24 +3582,25 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) cmd = self.environment.get_build_command() + \ ['--internal', 'clang' + name, self.environment.source_dir, self.environment.build_dir] + \ extra_args - elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target(target_name, 'CUSTOM_COMMAND', 'PHONY') elem.add_item('COMMAND', cmd) elem.add_item('pool', 'console') self.add_build(elem) - def generate_clangformat(self): + def generate_clangformat(self) -> None: if not environment.detect_clangformat(): return self.generate_clangtool('format') self.generate_clangtool('format', 'check') - def generate_clangtidy(self): + def generate_clangtidy(self) -> None: import shutil if not shutil.which('clang-tidy'): return self.generate_clangtool('tidy') + self.generate_clangtool('tidy', 'fix') - def generate_tags(self, tool, target_name): + def generate_tags(self, tool: str, target_name: str) -> None: import shutil if not shutil.which(tool): return @@ -3383,13 +3608,13 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) return cmd = self.environment.get_build_command() + \ ['--internal', 'tags', tool, self.environment.source_dir] - elem = self.create_phony_target(self.all_outputs, target_name, 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target(target_name, 'CUSTOM_COMMAND', 'PHONY') elem.add_item('COMMAND', cmd) elem.add_item('pool', 'console') self.add_build(elem) # For things like scan-build and other helper tools we might have. - def generate_utils(self): + def generate_utils(self) -> None: self.generate_scanbuild() self.generate_clangformat() self.generate_clangtidy() @@ -3397,12 +3622,12 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) self.generate_tags('ctags', 'ctags') self.generate_tags('cscope', 'cscope') cmd = self.environment.get_build_command() + ['--internal', 'uninstall'] - elem = self.create_phony_target(self.all_outputs, 'uninstall', 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target('uninstall', 'CUSTOM_COMMAND', 'PHONY') elem.add_item('COMMAND', cmd) elem.add_item('pool', 'console') self.add_build(elem) - def generate_ending(self): + def generate_ending(self) -> None: for targ, deps in [ ('all', self.get_build_by_default_targets()), ('meson-test-prereq', self.get_testlike_targets()), @@ -3415,12 +3640,17 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) for t in deps.values(): # Add the first output of each target to the 'all' target so that # they are all built + #Add archive file if shared library in AIX for build all. + if isinstance(t, build.SharedLibrary) and t.aix_so_archive: + if self.environment.machines[t.for_machine].is_aix(): + linker, stdlib_args = self.determine_linker_and_stdlib_args(t) + t.get_outputs()[0] = linker.get_archive_name(t.get_outputs()[0]) targetlist.append(os.path.join(self.get_target_dir(t), t.get_outputs()[0])) elem = NinjaBuildElement(self.all_outputs, targ, 'phony', targetlist) self.add_build(elem) - elem = self.create_phony_target(self.all_outputs, 'clean', 'CUSTOM_COMMAND', 'PHONY') + elem = self.create_phony_target('clean', 'CUSTOM_COMMAND', 'PHONY') elem.add_item('COMMAND', self.ninja_command + ['-t', 'clean']) elem.add_item('description', 'Cleaning') @@ -3451,6 +3681,12 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) elem.add_item('pool', 'console') self.add_build(elem) + # If these files used to be explicitly created, they need to appear on the build graph somehow, + # otherwise cleandead deletes them. See https://github.com/ninja-build/ninja/issues/2299 + if self.implicit_meson_outs: + elem = NinjaBuildElement(self.all_outputs, 'meson-implicit-outs', 'phony', self.implicit_meson_outs) + self.add_build(elem) + elem = NinjaBuildElement(self.all_outputs, 'reconfigure', 'REGENERATE_BUILD', 'PHONY') elem.add_item('pool', 'console') self.add_build(elem) @@ -3459,13 +3695,11 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485''')) self.add_build(elem) def get_introspection_data(self, target_id: str, target: build.Target) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]: - if target_id not in self.introspection_data or len(self.introspection_data[target_id]) == 0: + data = self.introspection_data.get(target_id) + if not data: return super().get_introspection_data(target_id, target) - result = [] - for i in self.introspection_data[target_id].values(): - result += [i] - return result + return list(data.values()) def _scan_fortran_file_deps(src: Path, srcdir: Path, dirname: Path, tdeps, compiler) -> T.List[str]: @@ -3519,7 +3753,7 @@ def _scan_fortran_file_deps(src: Path, srcdir: Path, dirname: Path, tdeps, compi # a common occurrence, which would lead to lots of # distracting noise. continue - srcfile = srcdir / tdeps[usename].fname # type: Path + srcfile = srcdir / tdeps[usename].fname if not srcfile.is_file(): if srcfile.name != src.name: # generated source file pass @@ -3541,7 +3775,7 @@ def _scan_fortran_file_deps(src: Path, srcdir: Path, dirname: Path, tdeps, compi ancestor_child = '_'.join(parents) if ancestor_child not in tdeps: raise MesonException("submodule {} relies on ancestor module {} that was not found.".format(submodmatch.group(2).lower(), ancestor_child.split('_', maxsplit=1)[0])) - submodsrcfile = srcdir / tdeps[ancestor_child].fname # type: Path + submodsrcfile = srcdir / tdeps[ancestor_child].fname if not submodsrcfile.is_file(): if submodsrcfile.name != src.name: # generated source file pass diff --git a/mesonbuild/backend/nonebackend.py b/mesonbuild/backend/nonebackend.py new file mode 100644 index 0000000..35ec958 --- /dev/null +++ b/mesonbuild/backend/nonebackend.py @@ -0,0 +1,39 @@ +# Copyright 2022 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import typing as T + +from .backends import Backend +from .. import mlog +from ..mesonlib import MesonBugException + + +class NoneBackend(Backend): + + name = 'none' + + def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect the none backend to generate with \'capture = True\'') + if vslite_ctx: + raise MesonBugException('We do not expect the none backend to be given a valid \'vslite_ctx\'') + + if self.build.get_targets(): + raise MesonBugException('None backend cannot generate target rules, but should have failed earlier.') + mlog.log('Generating simple install-only backend') + self.serialize_tests() + self.create_install_data_files() diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index 20cff56..cb1ea78 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -20,22 +20,26 @@ import xml.dom.minidom import xml.etree.ElementTree as ET import uuid import typing as T -from pathlib import Path, PurePath +from pathlib import Path, PurePath, PureWindowsPath import re +from collections import Counter from . import backends from .. import build -from .. import dependencies from .. import mlog from .. import compilers +from .. import mesonlib from ..mesonlib import ( - File, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice + File, MesonBugException, MesonException, replace_if_different, OptionKey, version_compare, MachineChoice ) from ..environment import Environment, build_filename +from .. import coredata if T.TYPE_CHECKING: + from ..arglist import CompilerArgs from ..interpreter import Interpreter + Project = T.Tuple[str, Path, str, MachineChoice] def autodetect_vs_version(build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]) -> backends.Backend: vs_version = os.getenv('VisualStudioVersion', None) @@ -72,7 +76,7 @@ def autodetect_vs_version(build: T.Optional[build.Build], interpreter: T.Optiona 'Please specify the exact backend to use.'.format(vs_version, vs_install_dir)) -def split_o_flags_args(args): +def split_o_flags_args(args: T.List[str]) -> T.List[str]: """ Splits any /O args and returns them. Does not take care of flags overriding previous ones. Skips non-O flag arguments. @@ -93,17 +97,55 @@ def split_o_flags_args(args): o_flags += ['/O' + f for f in flags] return o_flags - -def generate_guid_from_path(path, path_type): +def generate_guid_from_path(path, path_type) -> str: return str(uuid.uuid5(uuid.NAMESPACE_URL, 'meson-vs-' + path_type + ':' + str(path))).upper() def detect_microsoft_gdk(platform: str) -> bool: return re.match(r'Gaming\.(Desktop|Xbox.XboxOne|Xbox.Scarlett)\.x64', platform, re.IGNORECASE) +def filtered_src_langs_generator(sources: T.List[str]): + for src in sources: + ext = src.split('.')[-1] + if compilers.compilers.is_source_suffix(ext): + yield compilers.compilers.SUFFIX_TO_LANG[ext] + +# Returns the source language (i.e. a key from 'lang_suffixes') of the most frequent source language in the given +# list of sources. +# We choose the most frequent language as 'primary' because it means the most sources in a target/project can +# simply refer to the project's shared intellisense define and include fields, rather than have to fill out their +# own duplicate full set of defines/includes/opts intellisense fields. All of which helps keep the vcxproj file +# size down. +def get_primary_source_lang(target_sources: T.List[File], custom_sources: T.List[str]) -> T.Optional[str]: + lang_counts = Counter([compilers.compilers.SUFFIX_TO_LANG[src.suffix] for src in target_sources if compilers.compilers.is_source_suffix(src.suffix)]) + lang_counts += Counter(filtered_src_langs_generator(custom_sources)) + most_common_lang_list = lang_counts.most_common(1) + # It may be possible that we have a target with no actual src files of interest (e.g. a generator target), + # leaving us with an empty list, which we should handle - + return most_common_lang_list[0][0] if most_common_lang_list else None + +# Returns a dictionary (by [src type][build type]) that contains a tuple of - +# (pre-processor defines, include paths, additional compiler options) +# fields to use to fill in the respective intellisense fields of sources that can't simply +# reference and re-use the shared 'primary' language intellisense fields of the vcxproj. +def get_non_primary_lang_intellisense_fields(vslite_ctx: dict, + target_id: str, + primary_src_lang: str) -> T.Dict[str, T.Dict[str, T.Tuple[str, str, str]]]: + defs_paths_opts_per_lang_and_buildtype = {} + for buildtype in coredata.get_genvs_default_buildtype_list(): + captured_build_args = vslite_ctx[buildtype][target_id] # Results in a 'Src types to compile args' dict + non_primary_build_args_per_src_lang = [(lang, build_args) for lang, build_args in captured_build_args.items() if lang != primary_src_lang] # Only need to individually populate intellisense fields for sources of non-primary types. + for src_lang, args_list in non_primary_build_args_per_src_lang: + if src_lang not in defs_paths_opts_per_lang_and_buildtype: + defs_paths_opts_per_lang_and_buildtype[src_lang] = {} + defs_paths_opts_per_lang_and_buildtype[src_lang][buildtype] = Vs2010Backend._extract_nmake_fields(args_list) + return defs_paths_opts_per_lang_and_buildtype + class Vs2010Backend(backends.Backend): - def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): + + name = 'vs2010' + + def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter], gen_lite: bool = False): super().__init__(build, interpreter) - self.name = 'vs2010' self.project_file_version = '10.0.30319.1' self.sln_file_version = '11.00' self.sln_version_comment = '2010' @@ -112,76 +154,88 @@ class Vs2010Backend(backends.Backend): self.windows_target_platform_version = None self.subdirs = {} self.handled_target_deps = {} + self.gen_lite = gen_lite # Synonymous with generating the simpler makefile-style multi-config projects that invoke 'meson compile' builds, avoiding native MSBuild complications def get_target_private_dir(self, target): return os.path.join(self.get_target_dir(target), target.get_id()) + def generate_genlist_for_target(self, genlist: T.Union[build.GeneratedList, build.CustomTarget, build.CustomTargetIndex], target: build.BuildTarget, parent_node: ET.Element, generator_output_files: T.List[str], custom_target_include_dirs: T.List[str], custom_target_output_files: T.List[str]) -> None: + if isinstance(genlist, build.GeneratedList): + for x in genlist.depends: + self.generate_genlist_for_target(x, target, parent_node, [], [], []) + target_private_dir = self.relpath(self.get_target_private_dir(target), self.get_target_dir(target)) + down = self.target_to_build_root(target) + if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)): + for i in genlist.get_outputs(): + # Path to the generated source from the current vcxproj dir via the build root + ipath = os.path.join(down, self.get_target_dir(genlist), i) + custom_target_output_files.append(ipath) + idir = self.relpath(self.get_target_dir(genlist), self.get_target_dir(target)) + if idir not in custom_target_include_dirs: + custom_target_include_dirs.append(idir) + else: + generator = genlist.get_generator() + exe = generator.get_exe() + infilelist = genlist.get_inputs() + outfilelist = genlist.get_outputs() + source_dir = os.path.join(down, self.build_to_src, genlist.subdir) + idgroup = ET.SubElement(parent_node, 'ItemGroup') + samelen = len(infilelist) == len(outfilelist) + for i, curfile in enumerate(infilelist): + if samelen: + sole_output = os.path.join(target_private_dir, outfilelist[i]) + else: + sole_output = '' + infilename = os.path.join(down, curfile.rel_to_builddir(self.build_to_src, target_private_dir)) + deps = self.get_target_depend_files(genlist, True) + base_args = generator.get_arglist(infilename) + outfiles_rel = genlist.get_outputs_for(curfile) + outfiles = [os.path.join(target_private_dir, of) for of in outfiles_rel] + generator_output_files += outfiles + args = [x.replace("@INPUT@", infilename).replace('@OUTPUT@', sole_output) + for x in base_args] + args = self.replace_outputs(args, target_private_dir, outfiles_rel) + args = [x.replace("@SOURCE_DIR@", self.environment.get_source_dir()) + .replace("@BUILD_DIR@", target_private_dir) + for x in args] + args = [x.replace("@CURRENT_SOURCE_DIR@", source_dir) for x in args] + args = [x.replace("@SOURCE_ROOT@", self.environment.get_source_dir()) + .replace("@BUILD_ROOT@", self.environment.get_build_dir()) + for x in args] + args = [x.replace('\\', '/') for x in args] + # Always use a wrapper because MSBuild eats random characters when + # there are many arguments. + tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) + cmd, _ = self.as_meson_exe_cmdline( + exe, + self.replace_extra_args(args, genlist), + workdir=tdir_abs, + capture=outfiles[0] if generator.capture else None, + force_serialize=True, + env=genlist.env + ) + deps = cmd[-1:] + deps + abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) + os.makedirs(abs_pdir, exist_ok=True) + cbs = ET.SubElement(idgroup, 'CustomBuild', Include=infilename) + ET.SubElement(cbs, 'Command').text = ' '.join(self.quote_arguments(cmd)) + ET.SubElement(cbs, 'Outputs').text = ';'.join(outfiles) + ET.SubElement(cbs, 'AdditionalInputs').text = ';'.join(deps) + def generate_custom_generator_commands(self, target, parent_node): generator_output_files = [] custom_target_include_dirs = [] custom_target_output_files = [] - target_private_dir = self.relpath(self.get_target_private_dir(target), self.get_target_dir(target)) - down = self.target_to_build_root(target) for genlist in target.get_generated_sources(): - if isinstance(genlist, (build.CustomTarget, build.CustomTargetIndex)): - for i in genlist.get_outputs(): - # Path to the generated source from the current vcxproj dir via the build root - ipath = os.path.join(down, self.get_target_dir(genlist), i) - custom_target_output_files.append(ipath) - idir = self.relpath(self.get_target_dir(genlist), self.get_target_dir(target)) - if idir not in custom_target_include_dirs: - custom_target_include_dirs.append(idir) - else: - generator = genlist.get_generator() - exe = generator.get_exe() - infilelist = genlist.get_inputs() - outfilelist = genlist.get_outputs() - source_dir = os.path.join(down, self.build_to_src, genlist.subdir) - exe_arr = self.build_target_to_cmd_array(exe) - idgroup = ET.SubElement(parent_node, 'ItemGroup') - for i, curfile in enumerate(infilelist): - if len(infilelist) == len(outfilelist): - sole_output = os.path.join(target_private_dir, outfilelist[i]) - else: - sole_output = '' - infilename = os.path.join(down, curfile.rel_to_builddir(self.build_to_src)) - deps = self.get_custom_target_depend_files(genlist, True) - base_args = generator.get_arglist(infilename) - outfiles_rel = genlist.get_outputs_for(curfile) - outfiles = [os.path.join(target_private_dir, of) for of in outfiles_rel] - generator_output_files += outfiles - args = [x.replace("@INPUT@", infilename).replace('@OUTPUT@', sole_output) - for x in base_args] - args = self.replace_outputs(args, target_private_dir, outfiles_rel) - args = [x.replace("@SOURCE_DIR@", self.environment.get_source_dir()) - .replace("@BUILD_DIR@", target_private_dir) - for x in args] - args = [x.replace("@CURRENT_SOURCE_DIR@", source_dir) for x in args] - args = [x.replace("@SOURCE_ROOT@", self.environment.get_source_dir()) - .replace("@BUILD_ROOT@", self.environment.get_build_dir()) - for x in args] - args = [x.replace('\\', '/') for x in args] - cmd = exe_arr + self.replace_extra_args(args, genlist) - # Always use a wrapper because MSBuild eats random characters when - # there are many arguments. - tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) - cmd, _ = self.as_meson_exe_cmdline( - cmd[0], - cmd[1:], - workdir=tdir_abs, - capture=outfiles[0] if generator.capture else None, - force_serialize=True - ) - deps = cmd[-1:] + deps - abs_pdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) - os.makedirs(abs_pdir, exist_ok=True) - cbs = ET.SubElement(idgroup, 'CustomBuild', Include=infilename) - ET.SubElement(cbs, 'Command').text = ' '.join(self.quote_arguments(cmd)) - ET.SubElement(cbs, 'Outputs').text = ';'.join(outfiles) - ET.SubElement(cbs, 'AdditionalInputs').text = ';'.join(deps) + self.generate_genlist_for_target(genlist, target, parent_node, generator_output_files, custom_target_include_dirs, custom_target_output_files) return generator_output_files, custom_target_output_files, custom_target_include_dirs - def generate(self): + def generate(self, + capture: bool = False, + vslite_ctx: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect any vs backend to generate with \'capture = True\'') target_machine = self.interpreter.builtin['target_machine'].cpu_family_method(None, None) if target_machine in {'64', 'x86_64'}: # amd64 or x86_64 @@ -230,10 +284,10 @@ class Vs2010Backend(backends.Backend): except MesonException: self.sanitize = 'none' sln_filename = os.path.join(self.environment.get_build_dir(), self.build.project_name + '.sln') - projlist = self.generate_projects() - self.gen_testproj('RUN_TESTS', os.path.join(self.environment.get_build_dir(), 'RUN_TESTS.vcxproj')) - self.gen_installproj('RUN_INSTALL', os.path.join(self.environment.get_build_dir(), 'RUN_INSTALL.vcxproj')) - self.gen_regenproj('REGEN', os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj')) + projlist = self.generate_projects(vslite_ctx) + self.gen_testproj() + self.gen_installproj() + self.gen_regenproj() self.generate_solution(sln_filename, projlist) self.generate_regen_info() Vs2010Backend.touch_regen_timestamp(self.environment.get_build_dir()) @@ -343,7 +397,7 @@ class Vs2010Backend(backends.Backend): ret.update(all_deps) return ret - def generate_solution_dirs(self, ofile, parents): + def generate_solution_dirs(self, ofile: str, parents: T.Sequence[Path]) -> None: prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n' iterpaths = reversed(parents) # Skip first path @@ -363,7 +417,7 @@ class Vs2010Backend(backends.Backend): ofile.write(prj_line) ofile.write('EndProject\n') - def generate_solution(self, sln_filename, projlist): + def generate_solution(self, sln_filename: str, projlist: T.List[Project]) -> None: default_projlist = self.get_build_by_default_targets() default_projlist.update(self.get_testlike_targets()) sln_filename_tmp = sln_filename + '~' @@ -374,8 +428,7 @@ class Vs2010Backend(backends.Backend): ofile.write('# Visual Studio %s\n' % self.sln_version_comment) prj_templ = 'Project("{%s}") = "%s", "%s", "{%s}"\n' for prj in projlist: - coredata = self.environment.coredata - if coredata.get_option(OptionKey('layout')) == 'mirror': + if self.environment.coredata.get_option(OptionKey('layout')) == 'mirror': self.generate_solution_dirs(ofile, prj[1].parents) target = self.build.targets[prj[0]] lang = 'default' @@ -401,8 +454,14 @@ class Vs2010Backend(backends.Backend): self.environment.coredata.test_guid) ofile.write(test_line) ofile.write('EndProject\n') + if self.gen_lite: # REGEN is replaced by the lighter-weight RECONFIGURE utility, for now. See comment in 'gen_regenproj' + regen_proj_name = 'RECONFIGURE' + regen_proj_fname = 'RECONFIGURE.vcxproj' + else: + regen_proj_name = 'REGEN' + regen_proj_fname = 'REGEN.vcxproj' regen_line = prj_templ % (self.environment.coredata.lang_guids['default'], - 'REGEN', 'REGEN.vcxproj', + regen_proj_name, regen_proj_fname, self.environment.coredata.regen_guid) ofile.write(regen_line) ofile.write('EndProject\n') @@ -414,18 +473,23 @@ class Vs2010Backend(backends.Backend): ofile.write('Global\n') ofile.write('\tGlobalSection(SolutionConfigurationPlatforms) = ' 'preSolution\n') - ofile.write('\t\t%s|%s = %s|%s\n' % - (self.buildtype, self.platform, self.buildtype, - self.platform)) + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() if self.gen_lite else [self.buildtype] + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t%s|%s = %s|%s\n' % + (buildtype, self.platform, buildtype, + self.platform)) ofile.write('\tEndGlobalSection\n') ofile.write('\tGlobalSection(ProjectConfigurationPlatforms) = ' 'postSolution\n') - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.regen_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) - ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % - (self.environment.coredata.regen_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) + # REGEN project (multi-)configurations + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (self.environment.coredata.regen_guid, buildtype, + self.platform, buildtype, self.platform)) + if not self.gen_lite: # With a 'genvslite'-generated solution, the regen (i.e. reconfigure) utility is only intended to run when the user explicitly builds this proj. + ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % + (self.environment.coredata.regen_guid, buildtype, + self.platform, buildtype, self.platform)) # Create the solution configuration for p in projlist: if p[3] is MachineChoice.BUILD: @@ -433,21 +497,31 @@ class Vs2010Backend(backends.Backend): else: config_platform = self.platform # Add to the list of projects in this solution + for buildtype in multi_config_buildtype_list: + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (p[2], buildtype, self.platform, + buildtype, config_platform)) + # If we're building the solution with Visual Studio's build system, enable building of buildable + # projects. However, if we're building with meson (via --genvslite), then, since each project's + # 'build' action just ends up doing the same 'meson compile ...' we don't want the 'solution build' + # repeatedly going off and doing the same 'meson compile ...' multiple times over, so we just + # leave it up to the user to select or build just one project. + # FIXME: Would be slightly nicer if we could enable building of just one top level target/project, + # but not sure how to identify that. + if not self.gen_lite and \ + p[0] in default_projlist and \ + not isinstance(self.build.targets[p[0]], build.RunTarget): + ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % + (p[2], buildtype, self.platform, + buildtype, config_platform)) + # RUN_TESTS and RUN_INSTALL project (multi-)configurations + for buildtype in multi_config_buildtype_list: ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (p[2], self.buildtype, self.platform, - self.buildtype, config_platform)) - if p[0] in default_projlist and \ - not isinstance(self.build.targets[p[0]], build.RunTarget): - # Add to the list of projects to be built - ofile.write('\t\t{%s}.%s|%s.Build.0 = %s|%s\n' % - (p[2], self.buildtype, self.platform, - self.buildtype, config_platform)) - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.test_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) - ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % - (self.environment.coredata.install_guid, self.buildtype, - self.platform, self.buildtype, self.platform)) + (self.environment.coredata.test_guid, buildtype, + self.platform, buildtype, self.platform)) + ofile.write('\t\t{%s}.%s|%s.ActiveCfg = %s|%s\n' % + (self.environment.coredata.install_guid, buildtype, + self.platform, buildtype, self.platform)) ofile.write('\tEndGlobalSection\n') ofile.write('\tGlobalSection(SolutionProperties) = preSolution\n') ofile.write('\t\tHideSolutionNode = FALSE\n') @@ -465,9 +539,9 @@ class Vs2010Backend(backends.Backend): ofile.write('EndGlobal\n') replace_if_different(sln_filename, sln_filename_tmp) - def generate_projects(self): + def generate_projects(self, vslite_ctx: dict = None) -> T.List[Project]: startup_project = self.environment.coredata.options[OptionKey('backend_startup_project')].value - projlist = [] + projlist: T.List[Project] = [] startup_idx = 0 for (i, (name, target)) in enumerate(self.build.targets.items()): if startup_project and startup_project == target.get_basename(): @@ -482,8 +556,9 @@ class Vs2010Backend(backends.Backend): relname = target_dir / fname projfile_path = outdir / fname proj_uuid = self.environment.coredata.target_guids[name] - self.gen_vcxproj(target, str(projfile_path), proj_uuid) - projlist.append((name, relname, proj_uuid, target.for_machine)) + generated = self.gen_vcxproj(target, str(projfile_path), proj_uuid, vslite_ctx) + if generated: + projlist.append((name, relname, proj_uuid, target.for_machine)) # Put the startup project first in the project list if startup_idx: @@ -523,7 +598,7 @@ class Vs2010Backend(backends.Backend): def quote_arguments(self, arr): return ['"%s"' % i for i in arr] - def add_project_reference(self, root, include, projid, link_outputs=False): + def add_project_reference(self, root: ET.Element, include: str, projid: str, link_outputs: bool = False) -> None: ig = ET.SubElement(root, 'ItemGroup') pref = ET.SubElement(ig, 'ProjectReference', Include=include) ET.SubElement(pref, 'Project').text = '{%s}' % projid @@ -533,7 +608,7 @@ class Vs2010Backend(backends.Backend): # objects and .lib files manually. ET.SubElement(pref, 'LinkLibraryDependencies').text = 'false' - def add_target_deps(self, root, target): + def add_target_deps(self, root: ET.Element, target): target_dict = {target.get_id(): target} for dep in self.get_target_deps(target_dict).values(): if dep.get_id() in self.handled_target_deps[target.get_id()]: @@ -549,7 +624,7 @@ class Vs2010Backend(backends.Backend): guid, conftype='Utility', target_ext=None, - target_platform=None): + target_platform=None) -> T.Tuple[ET.Element, ET.Element]: root = ET.Element('Project', {'DefaultTargets': "Build", 'ToolsVersion': '4.0', 'xmlns': 'http://schemas.microsoft.com/developer/msbuild/2003'}) @@ -557,12 +632,13 @@ class Vs2010Backend(backends.Backend): confitems = ET.SubElement(root, 'ItemGroup', {'Label': 'ProjectConfigurations'}) if not target_platform: target_platform = self.platform - prjconf = ET.SubElement(confitems, 'ProjectConfiguration', - {'Include': self.buildtype + '|' + target_platform}) - p = ET.SubElement(prjconf, 'Configuration') - p.text = self.buildtype - pl = ET.SubElement(prjconf, 'Platform') - pl.text = target_platform + + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() if self.gen_lite else [self.buildtype] + for buildtype in multi_config_buildtype_list: + prjconf = ET.SubElement(confitems, 'ProjectConfiguration', + {'Include': buildtype + '|' + target_platform}) + ET.SubElement(prjconf, 'Configuration').text = buildtype + ET.SubElement(prjconf, 'Platform').text = target_platform # Globals globalgroup = ET.SubElement(root, 'PropertyGroup', Label='Globals') @@ -570,54 +646,60 @@ class Vs2010Backend(backends.Backend): guidelem.text = '{%s}' % guid kw = ET.SubElement(globalgroup, 'Keyword') kw.text = self.platform + 'Proj' - # XXX Wasn't here before for anything but gen_vcxproj , but seems fine? - ns = ET.SubElement(globalgroup, 'RootNamespace') - ns.text = target_name - - p = ET.SubElement(globalgroup, 'Platform') - p.text = target_platform - pname = ET.SubElement(globalgroup, 'ProjectName') - pname.text = target_name - if self.windows_target_platform_version: - ET.SubElement(globalgroup, 'WindowsTargetPlatformVersion').text = self.windows_target_platform_version - ET.SubElement(globalgroup, 'UseMultiToolTask').text = 'true' ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.Default.props') - # Start configuration + # Configuration type_config = ET.SubElement(root, 'PropertyGroup', Label='Configuration') ET.SubElement(type_config, 'ConfigurationType').text = conftype - ET.SubElement(type_config, 'CharacterSet').text = 'MultiByte' - # Fixme: wasn't here before for gen_vcxproj() - ET.SubElement(type_config, 'UseOfMfc').text = 'false' if self.platform_toolset: ET.SubElement(type_config, 'PlatformToolset').text = self.platform_toolset - # End configuration section (but it can be added to further via type_config) + # This must come AFTER the '' element; importing before the 'PlatformToolset' elt + # gets set leads to msbuild failures reporting - + # "The build tools for v142 (Platform Toolset = 'v142') cannot be found. ... please install v142 build tools." + # This is extremely unhelpful and misleading since the v14x build tools ARE installed. ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.props') - # Project information - direlem = ET.SubElement(root, 'PropertyGroup') - fver = ET.SubElement(direlem, '_ProjectFileVersion') - fver.text = self.project_file_version - outdir = ET.SubElement(direlem, 'OutDir') - outdir.text = '.\\' - intdir = ET.SubElement(direlem, 'IntDir') - intdir.text = temp_dir + '\\' - - tname = ET.SubElement(direlem, 'TargetName') - tname.text = target_name - - if target_ext: - ET.SubElement(direlem, 'TargetExt').text = target_ext + if not self.gen_lite: # Plenty of elements aren't necessary for 'makefile'-style project that just redirects to meson builds + # XXX Wasn't here before for anything but gen_vcxproj , but seems fine? + ns = ET.SubElement(globalgroup, 'RootNamespace') + ns.text = target_name + + p = ET.SubElement(globalgroup, 'Platform') + p.text = target_platform + pname = ET.SubElement(globalgroup, 'ProjectName') + pname.text = target_name + if self.windows_target_platform_version: + ET.SubElement(globalgroup, 'WindowsTargetPlatformVersion').text = self.windows_target_platform_version + ET.SubElement(globalgroup, 'UseMultiToolTask').text = 'true' + + ET.SubElement(type_config, 'CharacterSet').text = 'MultiByte' + # Fixme: wasn't here before for gen_vcxproj() + ET.SubElement(type_config, 'UseOfMfc').text = 'false' + + # Project information + direlem = ET.SubElement(root, 'PropertyGroup') + fver = ET.SubElement(direlem, '_ProjectFileVersion') + fver.text = self.project_file_version + outdir = ET.SubElement(direlem, 'OutDir') + outdir.text = '.\\' + intdir = ET.SubElement(direlem, 'IntDir') + intdir.text = temp_dir + '\\' + + tname = ET.SubElement(direlem, 'TargetName') + tname.text = target_name + + if target_ext: + ET.SubElement(direlem, 'TargetExt').text = target_ext return (root, type_config) - def gen_run_target_vcxproj(self, target, ofname, guid): + def gen_run_target_vcxproj(self, target: build.RunTarget, ofname: str, guid: str) -> None: (root, type_config) = self.create_basic_project(target.name, temp_dir=target.get_id(), guid=guid) - depend_files = self.get_custom_target_depend_files(target) + depend_files = self.get_target_depend_files(target) if not target.command: # This is an alias target and thus doesn't run any command. It's @@ -643,7 +725,7 @@ class Vs2010Backend(backends.Backend): self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_custom_target_vcxproj(self, target, ofname, guid): + def gen_custom_target_vcxproj(self, target: build.CustomTarget, ofname: str, guid: str) -> None: if target.for_machine is MachineChoice.BUILD: platform = self.build_platform else: @@ -656,7 +738,7 @@ class Vs2010Backend(backends.Backend): # from the target dir, not the build root. target.absolute_paths = True (srcs, ofilenames, cmd) = self.eval_custom_target_command(target, True) - depend_files = self.get_custom_target_depend_files(target, True) + depend_files = self.get_target_depend_files(target, True) # Always use a wrapper because MSBuild eats random characters when # there are many arguments. tdir_abs = os.path.join(self.environment.get_build_dir(), self.get_target_dir(target)) @@ -683,7 +765,7 @@ class Vs2010Backend(backends.Backend): self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_compile_target_vcxproj(self, target, ofname, guid): + def gen_compile_target_vcxproj(self, target: build.CompileTarget, ofname: str, guid: str) -> None: if target.for_machine is MachineChoice.BUILD: platform = self.build_platform else: @@ -767,6 +849,36 @@ class Vs2010Backend(backends.Backend): args.append(self.escape_additional_option(arg)) ET.SubElement(parent_node, "AdditionalOptions").text = ' '.join(args) + # Set up each project's source file ('CLCompile') element with appropriate preprocessor, include dir, and compile option values for correct intellisense. + def add_project_nmake_defs_incs_and_opts(self, parent_node, src: str, defs_paths_opts_per_lang_and_buildtype: dict, platform: str): + # For compactness, sources whose type matches the primary src type (i.e. most frequent in the set of source types used in the target/project, + # according to the 'captured_build_args' map), can simply reference the preprocessor definitions, include dirs, and compile option NMake fields of + # the project itself. + # However, if a src is of a non-primary type, it could have totally different defs/dirs/options so we're going to have to fill in the full, verbose + # set of values for these fields, which needs to be fully expanded per build type / configuration. + # + # FIXME: Suppose a project contains .cpp and .c src files with different compile defs/dirs/options, while also having .h files, some of which + # are included by .cpp sources and others included by .c sources: How do we know whether the .h source should be using the .cpp or .c src + # defs/dirs/options? Might it also be possible for a .h header to be shared between .cpp and .c sources? If so, I don't see how we can + # correctly configure these intellisense fields. + # For now, all sources/headers that fail to find their extension's language in the '...nmake_defs_paths_opts...' map will just adopt the project + # defs/dirs/opts that are set for the nominal 'primary' src type. + ext = src.split('.')[-1] + lang = compilers.compilers.SUFFIX_TO_LANG.get(ext, None) + if lang in defs_paths_opts_per_lang_and_buildtype.keys(): + # This is a non-primary src type for which can't simply reference the project's nmake fields; + # we must laboriously fill in the fields for all buildtypes. + for buildtype in coredata.get_genvs_default_buildtype_list(): + (defs, paths, opts) = defs_paths_opts_per_lang_and_buildtype[lang][buildtype] + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{platform}\'' + ET.SubElement(parent_node, 'PreprocessorDefinitions', Condition=condition).text = defs + ET.SubElement(parent_node, 'AdditionalIncludeDirectories', Condition=condition).text = paths + ET.SubElement(parent_node, 'AdditionalOptions', Condition=condition).text = opts + else: # Can't find bespoke nmake defs/dirs/opts fields for this extention, so just reference the project's fields + ET.SubElement(parent_node, 'PreprocessorDefinitions').text = '$(NMakePreprocessorDefinitions)' + ET.SubElement(parent_node, 'AdditionalIncludeDirectories').text = '$(NMakeIncludeSearchPath)' + ET.SubElement(parent_node, 'AdditionalOptions').text = '$(AdditionalOptions)' + def add_preprocessor_defines(self, lang, parent_node, file_defines): defines = [] for define in file_defines[lang]: @@ -793,7 +905,7 @@ class Vs2010Backend(backends.Backend): return @staticmethod - def escape_preprocessor_define(define): + def escape_preprocessor_define(define: str) -> str: # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx table = str.maketrans({'%': '%25', '$': '%24', '@': '%40', "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A', @@ -804,7 +916,7 @@ class Vs2010Backend(backends.Backend): return define.translate(table) @staticmethod - def escape_additional_option(option): + def escape_additional_option(option: str) -> str: # See: https://msdn.microsoft.com/en-us/library/bb383819.aspx table = str.maketrans({'%': '%25', '$': '%24', '@': '%40', "'": '%27', ';': '%3B', '?': '%3F', '*': '%2A', ' ': '%20'}) @@ -862,154 +974,18 @@ class Vs2010Backend(backends.Backend): return c raise MesonException('Could not find a C or C++ compiler. MSVC can only build C/C++ projects.') - def _prettyprint_vcxproj_xml(self, tree, ofname): + def _prettyprint_vcxproj_xml(self, tree: ET.ElementTree, ofname: str) -> None: ofname_tmp = ofname + '~' tree.write(ofname_tmp, encoding='utf-8', xml_declaration=True) - # ElementTree can not do prettyprinting so do it manually + # ElementTree cannot do pretty-printing, so do it manually doc = xml.dom.minidom.parse(ofname_tmp) with open(ofname_tmp, 'w', encoding='utf-8') as of: of.write(doc.toprettyxml()) replace_if_different(ofname, ofname_tmp) - def gen_vcxproj(self, target, ofname, guid): - mlog.debug(f'Generating vcxproj {target.name}.') - subsystem = 'Windows' - self.handled_target_deps[target.get_id()] = [] - if isinstance(target, build.Executable): - conftype = 'Application' - if target.gui_app is not None: - if not target.gui_app: - subsystem = 'Console' - else: - # If someone knows how to set the version properly, - # please send a patch. - subsystem = target.win_subsystem.split(',')[0] - elif isinstance(target, build.StaticLibrary): - conftype = 'StaticLibrary' - elif isinstance(target, build.SharedLibrary): - conftype = 'DynamicLibrary' - elif isinstance(target, build.CustomTarget): - return self.gen_custom_target_vcxproj(target, ofname, guid) - elif isinstance(target, build.RunTarget): - return self.gen_run_target_vcxproj(target, ofname, guid) - elif isinstance(target, build.CompileTarget): - return self.gen_compile_target_vcxproj(target, ofname, guid) - else: - raise MesonException(f'Unknown target type for {target.get_basename()}') - # Prefix to use to access the build root from the vcxproj dir - down = self.target_to_build_root(target) - # Prefix to use to access the source tree's root from the vcxproj dir - proj_to_src_root = os.path.join(down, self.build_to_src) - # Prefix to use to access the source tree's subdir from the vcxproj dir - proj_to_src_dir = os.path.join(proj_to_src_root, self.get_target_dir(target)) - (sources, headers, objects, languages) = self.split_sources(target.sources) - if target.is_unity: - sources = self.generate_unity_files(target, sources) - compiler = self._get_cl_compiler(target) - build_args = compiler.get_buildtype_args(self.buildtype) - build_args += compiler.get_optimization_args(self.optimization) - build_args += compiler.get_debug_args(self.debug) - build_args += compiler.sanitizer_compile_args(self.sanitize) - buildtype_link_args = compiler.get_buildtype_linker_args(self.buildtype) - vscrt_type = self.environment.coredata.options[OptionKey('b_vscrt')] - target_name = target.name - if target.for_machine is MachineChoice.BUILD: - platform = self.build_platform - else: - platform = self.platform - - tfilename = os.path.splitext(target.get_filename()) - - (root, type_config) = self.create_basic_project(tfilename[0], - temp_dir=target.get_id(), - guid=guid, - conftype=conftype, - target_ext=tfilename[1], - target_platform=platform) - - # FIXME: Should these just be set in create_basic_project(), even if - # irrelevant for current target? - - # FIXME: Meson's LTO support needs to be integrated here - ET.SubElement(type_config, 'WholeProgramOptimization').text = 'false' - # Let VS auto-set the RTC level - ET.SubElement(type_config, 'BasicRuntimeChecks').text = 'Default' - # Incremental linking increases code size - if '/INCREMENTAL:NO' in buildtype_link_args: - ET.SubElement(type_config, 'LinkIncremental').text = 'false' - - # Build information - compiles = ET.SubElement(root, 'ItemDefinitionGroup') - clconf = ET.SubElement(compiles, 'ClCompile') - # CRT type; debug or release - if vscrt_type.value == 'from_buildtype': - if self.buildtype == 'debug': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' - elif vscrt_type.value == 'static_from_buildtype': - if self.buildtype == 'debug': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' - elif vscrt_type.value == 'mdd': - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' - elif vscrt_type.value == 'mt': - # FIXME, wrong - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' - elif vscrt_type.value == 'mtd': - # FIXME, wrong - ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' - else: - ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' - ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' - # Sanitizers - if '/fsanitize=address' in build_args: - ET.SubElement(type_config, 'EnableASAN').text = 'true' - # Debug format - if '/ZI' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue' - elif '/Zi' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase' - elif '/Z7' in build_args: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle' - else: - ET.SubElement(clconf, 'DebugInformationFormat').text = 'None' - # Runtime checks - if '/RTC1' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks' - elif '/RTCu' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck' - elif '/RTCs' in build_args: - ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck' - # Exception handling has to be set in the xml in addition to the "AdditionalOptions" because otherwise - # cl will give warning D9025: overriding '/Ehs' with cpp_eh value - if 'cpp' in target.compilers: - eh = self.environment.coredata.options[OptionKey('eh', machine=target.for_machine, lang='cpp')] - if eh.value == 'a': - ET.SubElement(clconf, 'ExceptionHandling').text = 'Async' - elif eh.value == 's': - ET.SubElement(clconf, 'ExceptionHandling').text = 'SyncCThrow' - elif eh.value == 'none': - ET.SubElement(clconf, 'ExceptionHandling').text = 'false' - else: # 'sc' or 'default' - ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync' - generated_files, custom_target_output_files, generated_files_include_dirs = self.generate_custom_generator_commands( - target, root) - (gen_src, gen_hdrs, gen_objs, gen_langs) = self.split_sources(generated_files) - (custom_src, custom_hdrs, custom_objs, custom_langs) = self.split_sources(custom_target_output_files) - gen_src += custom_src - gen_hdrs += custom_hdrs - gen_langs += custom_langs - + # Returns: (target_args,file_args), (target_defines,file_defines), (target_inc_dirs,file_inc_dirs) + def get_args_defines_and_inc_dirs(self, target, compiler, generated_files_include_dirs, proj_to_src_root, proj_to_src_dir, build_args): # Arguments, include dirs, defines for all files in the current target target_args = [] target_defines = [] @@ -1019,7 +995,7 @@ class Vs2010Backend(backends.Backend): # # file_args is also later split out into defines and include_dirs in # case someone passed those in there - file_args = {l: c.compiler_args() for l, c in target.compilers.items()} + file_args: T.Dict[str, CompilerArgs] = {l: c.compiler_args() for l, c in target.compilers.items()} file_defines = {l: [] for l in target.compilers} file_inc_dirs = {l: [] for l in target.compilers} # The order in which these compile args are added must match @@ -1043,8 +1019,7 @@ class Vs2010Backend(backends.Backend): # Compile args added from the env or cross file: CFLAGS/CXXFLAGS, etc. We want these # to override all the defaults, but not the per-target compile args. for l in file_args.keys(): - opts = self.environment.coredata.options[OptionKey('args', machine=target.for_machine, lang=l)] - file_args[l] += opts.value + file_args[l] += target.get_option(OptionKey('args', machine=target.for_machine, lang=l)) for args in file_args.values(): # This is where Visual Studio will insert target_args, target_defines, # etc, which are added later from external deps (see below). @@ -1070,8 +1045,9 @@ class Vs2010Backend(backends.Backend): for i in reversed(d.get_incdirs()): curdir = os.path.join(d.get_curdir(), i) try: - args.append('-I' + self.relpath(curdir, target.subdir)) # build dir + # Add source subdir first so that the build subdir overrides it args.append('-I' + os.path.join(proj_to_src_root, curdir)) # src dir + args.append('-I' + self.relpath(curdir, target.subdir)) # build dir except ValueError: # Include is on different drive args.append('-I' + os.path.normpath(curdir)) @@ -1125,9 +1101,7 @@ class Vs2010Backend(backends.Backend): for d in reversed(target.get_external_deps()): # Cflags required by external deps might have UNIX-specific flags, # so filter them out if needed - if isinstance(d, dependencies.OpenMPDependency): - ET.SubElement(clconf, 'OpenMPSupport').text = 'true' - else: + if d.name != 'openmp': d_compile_args = compiler.unix_args_to_native(d.get_compile_args()) for arg in d_compile_args: if arg.startswith(('-D', '/D')): @@ -1144,9 +1118,246 @@ class Vs2010Backend(backends.Backend): else: target_args.append(arg) - languages += gen_langs if '/Gw' in build_args: target_args.append('/Gw') + + return (target_args, file_args), (target_defines, file_defines), (target_inc_dirs, file_inc_dirs) + + @staticmethod + def get_build_args(compiler, buildtype: str, optimization_level: str, debug: bool, sanitize: str) -> T.List[str]: + build_args = compiler.get_buildtype_args(buildtype) + build_args += compiler.get_optimization_args(optimization_level) + build_args += compiler.get_debug_args(debug) + build_args += compiler.sanitizer_compile_args(sanitize) + + return build_args + + # Used in populating a simple nmake-style project's intellisense fields. + # Given a list of compile args, for example - + # [ '-I..\\some\\dir\\include', '-I../../some/other/dir', '/MDd', '/W2', '/std:c++17', '/Od', '/Zi', '-DSOME_DEF=1', '-DANOTHER_DEF=someval', ...] + # returns a tuple of pre-processor defs (for this example) - + # 'SOME_DEF=1;ANOTHER_DEF=someval;' + # and include paths, e.g. - + # '..\\some\\dir\\include;../../some/other/dir;' + # and finally any remaining compiler options, e.g. - + # '/MDd;/W2;/std:c++17;/Od/Zi' + @staticmethod + def _extract_nmake_fields(captured_build_args: list[str]) -> T.Tuple[str, str, str]: + include_dir_options = [ + '-I', + '/I', + '-isystem', # regular gcc / clang option to denote system header include search paths + '/clang:-isystem', # clang-cl (msvc 'cl'-style clang wrapper) option to pass '-isystem' option to clang driver + '/imsvc', # clang-cl option to 'Add directory to system include search path' + '/external:I', # msvc cl option to add 'external' include search paths + ] + + defs = '' + paths = '' + additional_opts = '' + for arg in captured_build_args: + if arg.startswith(('-D', '/D')): + defs += arg[2:] + ';' + else: + opt_match = next((opt for opt in include_dir_options if arg.startswith(opt)), None) + if opt_match: + paths += arg[len(opt_match):] + ';' + elif arg.startswith(('-', '/')): + additional_opts += arg + ';' + return (defs, paths, additional_opts) + + @staticmethod + def get_nmake_base_meson_command_and_exe_search_paths() -> T.Tuple[str, str]: + meson_cmd_list = mesonlib.get_meson_command() + assert (len(meson_cmd_list) == 1) or (len(meson_cmd_list) == 2) + # We expect get_meson_command() to either be of the form - + # 1: ['path/to/meson.exe'] + # or - + # 2: ['path/to/python.exe', 'and/path/to/meson.py'] + # so we'd like to ensure our makefile-style project invokes the same meson executable or python src as this instance. + exe_search_paths = os.path.dirname(meson_cmd_list[0]) + nmake_base_meson_command = os.path.basename(meson_cmd_list[0]) + if len(meson_cmd_list) != 1: + # We expect to be dealing with case '2', shown above. + # With Windows, it's also possible that we get a path to the second element of meson_cmd_list that contains spaces + # (e.g. 'and/path to/meson.py'). So, because this will end up directly in the makefile/NMake command lines, we'd + # better always enclose it in quotes. Only strictly necessary for paths with spaces but no harm for paths without - + nmake_base_meson_command += ' \"' + meson_cmd_list[1] + '\"' + exe_search_paths += ';' + os.path.dirname(meson_cmd_list[1]) + + # Additionally, in some cases, we appear to have to add 'C:\Windows\system32;C:\Windows' to the 'Path' environment (via the + # ExecutablePath element), without which, the 'meson compile ...' (NMakeBuildCommandLine) command can fail (failure to find + # stdio.h and similar), so something is quietly switching some critical build behaviour based on the presence of these in + # the 'Path'. + # Not sure if this ultimately comes down to some 'find and guess' hidden behaviours within meson or within MSVC tools, but + # I guess some projects may implicitly rely on this behaviour. + # Things would be cleaner, more robust, repeatable, and portable if meson (and msvc tools) replaced all this kind of + # find/guess behaviour with the requirement that things just be explicitly specified by the user. + # An example of this can be seen with - + # 1: Download https://github.com/facebook/zstd source + # 2: cd to the 'zstd-dev\build\meson' dir + # 3: meson setup -Dbin_programs=true -Dbin_contrib=true --genvslite vs2022 builddir_vslite + # 4: Open the generated 'builddir_vslite_vs\zstd.sln' and build through a project, which should explicitly add the above to + # the project's 'Executable Directories' paths and build successfully. + # 5: Remove 'C:\Windows\system32;C:\Windows;' from the same project's 'Executable Directories' paths and rebuild. + # This should now fail. + # It feels uncomfortable to do this but what better alternative is there (and might this introduce new problems)? - + exe_search_paths += ';C:\\Windows\\system32;C:\\Windows' + # A meson project that explicitly specifies compiler/linker tools and sdk/include paths is not going to have any problems + # with this addition. + + return (nmake_base_meson_command, exe_search_paths) + + def add_gen_lite_makefile_vcxproj_elements(self, + root: ET.Element, + platform: str, + target_ext: str, + vslite_ctx: dict, + target, + proj_to_build_root: str, + primary_src_lang: T.Optional[str]) -> None: + ET.SubElement(root, 'ImportGroup', Label='ExtensionSettings') + ET.SubElement(root, 'ImportGroup', Label='Shared') + prop_sheets_grp = ET.SubElement(root, 'ImportGroup', Label='PropertySheets') + ET.SubElement(prop_sheets_grp, 'Import', {'Project': r'$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props', + 'Condition': r"exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')", + 'Label': 'LocalAppDataPlatform' + }) + ET.SubElement(root, 'PropertyGroup', Label='UserMacros') + + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + + # Relative path from this .vcxproj to the directory containing the set of '..._[debug/debugoptimized/release]' setup meson build dirs. + proj_to_multiconfigured_builds_parent_dir = os.path.join(proj_to_build_root, '..') + + # Conditional property groups per configuration (buildtype). E.g. - + # + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + for buildtype in multi_config_buildtype_list: + per_config_prop_group = ET.SubElement(root, 'PropertyGroup', Condition=f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{platform}\'') + (_, build_dir_tail) = os.path.split(self.src_to_build) + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + ET.SubElement(per_config_prop_group, 'OutDir').text = f'{proj_to_build_dir_for_buildtype}\\' + ET.SubElement(per_config_prop_group, 'IntDir').text = f'{proj_to_build_dir_for_buildtype}\\' + ET.SubElement(per_config_prop_group, 'NMakeBuildCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}"' + ET.SubElement(per_config_prop_group, 'NMakeOutput').text = f'$(OutDir){target.name}{target_ext}' + captured_build_args = vslite_ctx[buildtype][target.get_id()] + # 'captured_build_args' is a dictionary, mapping from each src file type to a list of compile args to use for that type. + # Usually, there's just one but we could have multiple src types. However, since there's only one field for the makefile + # project's NMake... preprocessor/include intellisense fields, we'll just use the first src type we have to fill in + # these fields. Then, any src files in this VS project that aren't of this first src type will then need to override + # its intellisense fields instead of simply referencing the values in the project. + ET.SubElement(per_config_prop_group, 'NMakeReBuildCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}" --clean && {nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}"' + ET.SubElement(per_config_prop_group, 'NMakeCleanCommandLine').text = f'{nmake_base_meson_command} compile -C "{proj_to_build_dir_for_buildtype}" --clean' + # Need to set the 'ExecutablePath' element for the above NMake... commands to be able to invoke the meson command. + ET.SubElement(per_config_prop_group, 'ExecutablePath').text = exe_search_paths + # We may not have any src files and so won't have a primary src language. In which case, we've nothing to fill in for this target's intellisense fields - + if primary_src_lang: + primary_src_type_build_args = captured_build_args[primary_src_lang] + preproc_defs, inc_paths, other_compile_opts = Vs2010Backend._extract_nmake_fields(primary_src_type_build_args) + ET.SubElement(per_config_prop_group, 'NMakePreprocessorDefinitions').text = preproc_defs + ET.SubElement(per_config_prop_group, 'NMakeIncludeSearchPath').text = inc_paths + ET.SubElement(per_config_prop_group, 'AdditionalOptions').text = other_compile_opts + + # Unless we explicitly specify the following empty path elements, the project is assigned a load of nasty defaults that fill these + # with values like - + # $(VC_IncludePath);$(WindowsSDK_IncludePath); + # which are all based on the current install environment (a recipe for non-reproducibility problems), not the paths that will be used by + # the actual meson compile jobs. Although these elements look like they're only for MSBuild operations, they're not needed with our simple, + # lite/makefile-style projects so let's just remove them in case they do get used/confused by intellisense. + ET.SubElement(per_config_prop_group, 'IncludePath') + ET.SubElement(per_config_prop_group, 'ExternalIncludePath') + ET.SubElement(per_config_prop_group, 'ReferencePath') + ET.SubElement(per_config_prop_group, 'LibraryPath') + ET.SubElement(per_config_prop_group, 'LibraryWPath') + ET.SubElement(per_config_prop_group, 'SourcePath') + ET.SubElement(per_config_prop_group, 'ExcludePath') + + def add_non_makefile_vcxproj_elements( + self, + root: ET.Element, + type_config: ET.Element, + target, + platform: str, + subsystem, + build_args, + target_args, + target_defines, + target_inc_dirs, + file_args + ) -> None: + compiler = self._get_cl_compiler(target) + buildtype_link_args = compiler.get_buildtype_linker_args(self.buildtype) + + # Prefix to use to access the build root from the vcxproj dir + down = self.target_to_build_root(target) + + # FIXME: Should the following just be set in create_basic_project(), even if + # irrelevant for current target? + + # FIXME: Meson's LTO support needs to be integrated here + ET.SubElement(type_config, 'WholeProgramOptimization').text = 'false' + # Let VS auto-set the RTC level + ET.SubElement(type_config, 'BasicRuntimeChecks').text = 'Default' + # Incremental linking increases code size + if '/INCREMENTAL:NO' in buildtype_link_args: + ET.SubElement(type_config, 'LinkIncremental').text = 'false' + + # Build information + compiles = ET.SubElement(root, 'ItemDefinitionGroup') + clconf = ET.SubElement(compiles, 'ClCompile') + if True in ((dep.name == 'openmp') for dep in target.get_external_deps()): + ET.SubElement(clconf, 'OpenMPSupport').text = 'true' + # CRT type; debug or release + vscrt_type = target.get_option(OptionKey('b_vscrt')) + vscrt_val = compiler.get_crt_val(vscrt_type, self.buildtype) + if vscrt_val == 'mdd': + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebugDLL' + elif vscrt_val == 'mt': + # FIXME, wrong + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreaded' + elif vscrt_val == 'mtd': + # FIXME, wrong + ET.SubElement(type_config, 'UseDebugLibraries').text = 'true' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDebug' + else: + ET.SubElement(type_config, 'UseDebugLibraries').text = 'false' + ET.SubElement(clconf, 'RuntimeLibrary').text = 'MultiThreadedDLL' + # Sanitizers + if '/fsanitize=address' in build_args: + ET.SubElement(type_config, 'EnableASAN').text = 'true' + # Debug format + if '/ZI' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'EditAndContinue' + elif '/Zi' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'ProgramDatabase' + elif '/Z7' in build_args: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'OldStyle' + else: + ET.SubElement(clconf, 'DebugInformationFormat').text = 'None' + # Runtime checks + if '/RTC1' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'EnableFastChecks' + elif '/RTCu' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'UninitializedLocalUsageCheck' + elif '/RTCs' in build_args: + ET.SubElement(clconf, 'BasicRuntimeChecks').text = 'StackFrameRuntimeCheck' + # Exception handling has to be set in the xml in addition to the "AdditionalOptions" because otherwise + # cl will give warning D9025: overriding '/Ehs' with cpp_eh value + if 'cpp' in target.compilers: + eh = target.get_option(OptionKey('eh', machine=target.for_machine, lang='cpp')) + if eh == 'a': + ET.SubElement(clconf, 'ExceptionHandling').text = 'Async' + elif eh == 's': + ET.SubElement(clconf, 'ExceptionHandling').text = 'SyncCThrow' + elif eh == 'none': + ET.SubElement(clconf, 'ExceptionHandling').text = 'false' + else: # 'sc' or 'default' + ET.SubElement(clconf, 'ExceptionHandling').text = 'Sync' + if len(target_args) > 0: target_args.append('%(AdditionalOptions)') ET.SubElement(clconf, "AdditionalOptions").text = ' '.join(target_args) @@ -1155,7 +1366,7 @@ class Vs2010Backend(backends.Backend): ET.SubElement(clconf, 'PreprocessorDefinitions').text = ';'.join(target_defines) ET.SubElement(clconf, 'FunctionLevelLinking').text = 'true' # Warning level - warning_level = target.get_option(OptionKey('warning_level')) + warning_level = T.cast('str', target.get_option(OptionKey('warning_level'))) ET.SubElement(clconf, 'WarningLevel').text = 'Level' + str(1 + int(warning_level)) if target.get_option(OptionKey('werror')): ET.SubElement(clconf, 'TreatWarningAsError').text = 'true' @@ -1183,25 +1394,6 @@ class Vs2010Backend(backends.Backend): ET.SubElement(clconf, 'FavorSizeOrSpeed').text = 'Speed' # Note: SuppressStartupBanner is /NOLOGO and is 'true' by default self.generate_lang_standard_info(file_args, clconf) - pch_sources = {} - if self.environment.coredata.options.get(OptionKey('b_pch')): - for lang in ['c', 'cpp']: - pch = target.get_pch(lang) - if not pch: - continue - if compiler.id == 'msvc': - if len(pch) == 1: - # Auto generate PCH. - src = os.path.join(down, self.create_msvc_pch_implementation(target, lang, pch[0])) - pch_header_dir = os.path.dirname(os.path.join(proj_to_src_dir, pch[0])) - else: - src = os.path.join(proj_to_src_dir, pch[1]) - pch_header_dir = None - pch_sources[lang] = [pch[0], src, lang, pch_header_dir] - else: - # I don't know whether its relevant but let's handle other compilers - # used with a vs backend - pch_sources[lang] = [pch[0], None, lang, None] resourcecompile = ET.SubElement(compiles, 'ResourceCompile') ET.SubElement(resourcecompile, 'PreprocessorDefinitions') @@ -1220,8 +1412,7 @@ class Vs2010Backend(backends.Backend): ET.SubElement(link, 'GenerateDebugInformation').text = 'false' if not isinstance(target, build.StaticLibrary): if isinstance(target, build.SharedModule): - options = self.environment.coredata.options - extra_link_args += compiler.get_std_shared_module_link_args(options) + extra_link_args += compiler.get_std_shared_module_link_args(target.get_options()) # Add link args added using add_project_link_arguments() extra_link_args += self.build.get_project_link_args(compiler, target.subproject, target.for_machine) # Add link args added using add_global_link_arguments() @@ -1238,15 +1429,15 @@ class Vs2010Backend(backends.Backend): for dep in target.get_external_deps(): # Extend without reordering or de-dup to preserve `-L -l` sets # https://github.com/mesonbuild/meson/issues/1718 - if isinstance(dep, dependencies.OpenMPDependency): - ET.SubElement(clconf, 'OpenMPSuppport').text = 'true' + if dep.name == 'openmp': + ET.SubElement(clconf, 'OpenMPSupport').text = 'true' else: extra_link_args.extend_direct(dep.get_link_args()) for d in target.get_dependencies(): if isinstance(d, build.StaticLibrary): for dep in d.get_external_deps(): - if isinstance(dep, dependencies.OpenMPDependency): - ET.SubElement(clconf, 'OpenMPSuppport').text = 'true' + if dep.name == 'openmp': + ET.SubElement(clconf, 'OpenMPSupport').text = 'true' else: extra_link_args.extend_direct(dep.get_link_args()) # Add link args for c_* or cpp_* build options. Currently this only @@ -1254,7 +1445,7 @@ class Vs2010Backend(backends.Backend): # to be after all internal and external libraries so that unresolved # symbols from those can be found here. This is needed when the # *_winlibs that we want to link to are static mingw64 libraries. - extra_link_args += compiler.get_option_link_args(self.environment.coredata.options) + extra_link_args += compiler.get_option_link_args(target.get_options()) (additional_libpaths, additional_links, extra_link_args) = self.split_link_args(extra_link_args.to_native()) # Add more libraries to be linked if needed @@ -1309,12 +1500,6 @@ class Vs2010Backend(backends.Backend): additional_links.append(linkname) for lib in self.get_custom_target_provided_libraries(target): additional_links.append(self.relpath(lib, self.get_target_dir(target))) - additional_objects = [] - for o in self.flatten_object_list(target, down)[0]: - assert isinstance(o, str) - additional_objects.append(o) - for o in custom_objs: - additional_objects.append(o) if len(extra_link_args) > 0: extra_link_args.append('%(AdditionalOptions)') @@ -1333,14 +1518,14 @@ class Vs2010Backend(backends.Backend): # DLLs built with MSVC always have an import library except when # they're data-only DLLs, but we don't support those yet. ET.SubElement(link, 'ImportLibrary').text = target.get_import_filename() - if isinstance(target, build.SharedLibrary): + if isinstance(target, (build.SharedLibrary, build.Executable)): # Add module definitions file, if provided if target.vs_module_defs: relpath = os.path.join(down, target.vs_module_defs.rel_to_builddir(self.build_to_src)) ET.SubElement(link, 'ModuleDefinitionFile').text = relpath if self.debug: pdb = ET.SubElement(link, 'ProgramDataBaseFileName') - pdb.text = f'$(OutDir){target_name}.pdb' + pdb.text = f'$(OutDir){target.name}.pdb' targetmachine = ET.SubElement(link, 'TargetMachine') if target.for_machine is MachineChoice.BUILD: targetplatform = platform.lower() @@ -1361,9 +1546,112 @@ class Vs2010Backend(backends.Backend): # /nologo ET.SubElement(link, 'SuppressStartupBanner').text = 'true' # /release - if not self.environment.coredata.get_option(OptionKey('debug')): + if not target.get_option(OptionKey('debug')): ET.SubElement(link, 'SetChecksum').text = 'true' + # Visual studio doesn't simply allow the src files of a project to be added with the 'Condition=...' attribute, + # to allow us to point to the different debug/debugoptimized/release sets of generated src files for each of + # the solution's configurations. Similarly, 'ItemGroup' also doesn't support 'Condition'. So, without knowing + # a better (simple) alternative, for now, we'll repoint these generated sources (which will be incorrectly + # pointing to non-existent files under our '[builddir]_vs' directory) to the appropriate location under one of + # our buildtype build directores (e.g. '[builddir]_debug'). + # This will at least allow the user to open the files of generated sources listed in the solution explorer, + # once a build/compile has generated these sources. + # + # This modifies the paths in 'gen_files' in place, as opposed to returning a new list of modified paths. + def relocate_generated_file_paths_to_concrete_build_dir(self, gen_files: T.List[str], target: T.Union[build.Target, build.CustomTargetIndex]) -> None: + (_, build_dir_tail) = os.path.split(self.src_to_build) + meson_build_dir_for_buildtype = build_dir_tail[:-2] + coredata.get_genvs_default_buildtype_list()[0] # Get the first buildtype suffixed dir (i.e. '[builddir]_debug') from '[builddir]_vs' + # Relative path from this .vcxproj to the directory containing the set of '..._[debug/debugoptimized/release]' setup meson build dirs. + proj_to_build_root = self.target_to_build_root(target) + proj_to_multiconfigured_builds_parent_dir = os.path.join(proj_to_build_root, '..') + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + relocate_to_concrete_builddir_target = os.path.normpath(os.path.join(proj_to_build_dir_for_buildtype, self.get_target_dir(target))) + for idx, file_path in enumerate(gen_files): + gen_files[idx] = os.path.normpath(os.path.join(relocate_to_concrete_builddir_target, file_path)) + + # Returns bool indicating whether the .vcxproj has been generated. + # Under some circumstances, it's unnecessary to create some .vcxprojs, so, when generating the .sln, + # we need to respect that not all targets will have generated a project. + def gen_vcxproj(self, target: build.BuildTarget, ofname: str, guid: str, vslite_ctx: dict = None) -> bool: + mlog.debug(f'Generating vcxproj {target.name}.') + subsystem = 'Windows' + self.handled_target_deps[target.get_id()] = [] + + if self.gen_lite: + if not isinstance(target, build.BuildTarget): + # Since we're going to delegate all building to the one true meson build command, we don't need + # to generate .vcxprojs for targets that don't add any source files or just perform custom build + # commands. These are targets of types CustomTarget or RunTarget. So let's just skip generating + # these otherwise insubstantial non-BuildTarget targets. + return False + conftype = 'Makefile' + elif isinstance(target, build.Executable): + conftype = 'Application' + # If someone knows how to set the version properly, + # please send a patch. + subsystem = target.win_subsystem.split(',')[0] + elif isinstance(target, build.StaticLibrary): + conftype = 'StaticLibrary' + elif isinstance(target, build.SharedLibrary): + conftype = 'DynamicLibrary' + elif isinstance(target, build.CustomTarget): + self.gen_custom_target_vcxproj(target, ofname, guid) + return True + elif isinstance(target, build.RunTarget): + self.gen_run_target_vcxproj(target, ofname, guid) + return True + elif isinstance(target, build.CompileTarget): + self.gen_compile_target_vcxproj(target, ofname, guid) + return True + else: + raise MesonException(f'Unknown target type for {target.get_basename()}') + + (sources, headers, objects, _languages) = self.split_sources(target.sources) + if target.is_unity: + sources = self.generate_unity_files(target, sources) + if target.for_machine is MachineChoice.BUILD: + platform = self.build_platform + else: + platform = self.platform + + tfilename = os.path.splitext(target.get_filename()) + + (root, type_config) = self.create_basic_project(tfilename[0], + temp_dir=target.get_id(), + guid=guid, + conftype=conftype, + target_ext=tfilename[1], + target_platform=platform) + + generated_files, custom_target_output_files, generated_files_include_dirs = self.generate_custom_generator_commands( + target, root) + (gen_src, gen_hdrs, gen_objs, _gen_langs) = self.split_sources(generated_files) + (custom_src, custom_hdrs, custom_objs, _custom_langs) = self.split_sources(custom_target_output_files) + gen_src += custom_src + gen_hdrs += custom_hdrs + + compiler = self._get_cl_compiler(target) + build_args = Vs2010Backend.get_build_args(compiler, self.buildtype, self.optimization, self.debug, self.sanitize) + + assert isinstance(target, (build.Executable, build.SharedLibrary, build.StaticLibrary, build.SharedModule)), 'for mypy' + # Prefix to use to access the build root from the vcxproj dir + proj_to_build_root = self.target_to_build_root(target) + # Prefix to use to access the source tree's root from the vcxproj dir + proj_to_src_root = os.path.join(proj_to_build_root, self.build_to_src) + # Prefix to use to access the source tree's subdir from the vcxproj dir + proj_to_src_dir = os.path.join(proj_to_src_root, self.get_target_dir(target)) + + (target_args, file_args), (target_defines, file_defines), (target_inc_dirs, file_inc_dirs) = self.get_args_defines_and_inc_dirs( + target, compiler, generated_files_include_dirs, proj_to_src_root, proj_to_src_dir, build_args) + + if self.gen_lite: + assert vslite_ctx is not None + primary_src_lang = get_primary_source_lang(target.sources, custom_src) + self.add_gen_lite_makefile_vcxproj_elements(root, platform, tfilename[1], vslite_ctx, target, proj_to_build_root, primary_src_lang) + else: + self.add_non_makefile_vcxproj_elements(root, type_config, target, platform, subsystem, build_args, target_args, target_defines, target_inc_dirs, file_args) + meson_file_group = ET.SubElement(root, 'ItemGroup') ET.SubElement(meson_file_group, 'None', Include=os.path.join(proj_to_src_dir, build_filename)) @@ -1377,18 +1665,43 @@ class Vs2010Backend(backends.Backend): else: return False + pch_sources = {} + if self.target_uses_pch(target): + for lang in ['c', 'cpp']: + pch = target.get_pch(lang) + if not pch: + continue + if compiler.id == 'msvc': + if len(pch) == 1: + # Auto generate PCH. + src = os.path.join(proj_to_build_root, self.create_msvc_pch_implementation(target, lang, pch[0])) + pch_header_dir = os.path.dirname(os.path.join(proj_to_src_dir, pch[0])) + else: + src = os.path.join(proj_to_src_dir, pch[1]) + pch_header_dir = None + pch_sources[lang] = [pch[0], src, lang, pch_header_dir] + else: + # I don't know whether its relevant but let's handle other compilers + # used with a vs backend + pch_sources[lang] = [pch[0], None, lang, None] + previous_includes = [] if len(headers) + len(gen_hdrs) + len(target.extra_files) + len(pch_sources) > 0: + if self.gen_lite and gen_hdrs: + # Although we're constructing our .vcxproj under our '..._vs' directory, we want to reference generated files + # in our concrete build directories (e.g. '..._debug'), where generated files will exist after building. + self.relocate_generated_file_paths_to_concrete_build_dir(gen_hdrs, target) + inc_hdrs = ET.SubElement(root, 'ItemGroup') for h in headers: - relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, h.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_includes): ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath) for h in gen_hdrs: if path_normalize_add(h, previous_includes): ET.SubElement(inc_hdrs, 'CLInclude', Include=h) for h in target.extra_files: - relpath = os.path.join(down, h.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, h.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_includes): ET.SubElement(inc_hdrs, 'CLInclude', Include=relpath) for headers in pch_sources.values(): @@ -1398,50 +1711,77 @@ class Vs2010Backend(backends.Backend): previous_sources = [] if len(sources) + len(gen_src) + len(pch_sources) > 0: + if self.gen_lite: + # Get data to fill in intellisense fields for sources that can't reference the project-wide values + defs_paths_opts_per_lang_and_buildtype = get_non_primary_lang_intellisense_fields( + vslite_ctx, + target.get_id(), + primary_src_lang) + if gen_src: + # Although we're constructing our .vcxproj under our '..._vs' directory, we want to reference generated files + # in our concrete build directories (e.g. '..._debug'), where generated files will exist after building. + self.relocate_generated_file_paths_to_concrete_build_dir(gen_src, target) + inc_src = ET.SubElement(root, 'ItemGroup') for s in sources: - relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, s.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_sources): inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=relpath) - lang = Vs2010Backend.lang_from_source_file(s) - self.add_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - self.add_include_dirs(lang, inc_cl, file_inc_dirs) - ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ - self.object_filename_from_source(target, s) + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, relpath, defs_paths_opts_per_lang_and_buildtype, platform) + else: + lang = Vs2010Backend.lang_from_source_file(s) + self.add_pch(pch_sources, lang, inc_cl) + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + self.add_include_dirs(lang, inc_cl, file_inc_dirs) + ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ + self.object_filename_from_source(target, s) for s in gen_src: if path_normalize_add(s, previous_sources): inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=s) - lang = Vs2010Backend.lang_from_source_file(s) - self.add_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - self.add_include_dirs(lang, inc_cl, file_inc_dirs) - s = File.from_built_file(target.get_subdir(), s) - ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ - self.object_filename_from_source(target, s) + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, s, defs_paths_opts_per_lang_and_buildtype, platform) + else: + lang = Vs2010Backend.lang_from_source_file(s) + self.add_pch(pch_sources, lang, inc_cl) + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + self.add_include_dirs(lang, inc_cl, file_inc_dirs) + s = File.from_built_file(target.get_subdir(), s) + ET.SubElement(inc_cl, 'ObjectFileName').text = "$(IntDir)" + \ + self.object_filename_from_source(target, s) for lang, headers in pch_sources.items(): impl = headers[1] if impl and path_normalize_add(impl, previous_sources): inc_cl = ET.SubElement(inc_src, 'CLCompile', Include=impl) self.create_pch(pch_sources, lang, inc_cl) - self.add_additional_options(lang, inc_cl, file_args) - self.add_preprocessor_defines(lang, inc_cl, file_defines) - pch_header_dir = pch_sources[lang][3] - if pch_header_dir: - inc_dirs = copy.deepcopy(file_inc_dirs) - inc_dirs[lang] = [pch_header_dir] + inc_dirs[lang] + if self.gen_lite: + self.add_project_nmake_defs_incs_and_opts(inc_cl, impl, defs_paths_opts_per_lang_and_buildtype, platform) else: - inc_dirs = file_inc_dirs - self.add_include_dirs(lang, inc_cl, inc_dirs) - # XXX: Do we need to set the object file name name here too? + self.add_additional_options(lang, inc_cl, file_args) + self.add_preprocessor_defines(lang, inc_cl, file_defines) + pch_header_dir = pch_sources[lang][3] + if pch_header_dir: + inc_dirs = copy.deepcopy(file_inc_dirs) + inc_dirs[lang] = [pch_header_dir] + inc_dirs[lang] + else: + inc_dirs = file_inc_dirs + self.add_include_dirs(lang, inc_cl, inc_dirs) + # XXX: Do we need to set the object file name here too? + + additional_objects = [] + for o in self.flatten_object_list(target, proj_to_build_root)[0]: + assert isinstance(o, str) + additional_objects.append(o) + for o in custom_objs: + additional_objects.append(o) previous_objects = [] if self.has_objects(objects, additional_objects, gen_objs): inc_objs = ET.SubElement(root, 'ItemGroup') for s in objects: - relpath = os.path.join(down, s.rel_to_builddir(self.build_to_src)) + relpath = os.path.join(proj_to_build_root, s.rel_to_builddir(self.build_to_src)) if path_normalize_add(relpath, previous_objects): ET.SubElement(inc_objs, 'Object', Include=relpath) for s in additional_objects: @@ -1451,84 +1791,264 @@ class Vs2010Backend(backends.Backend): ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) - self.add_target_deps(root, target) + if not self.gen_lite: + # Injecting further target dependencies into this vcxproj implies and forces a Visual Studio BUILD dependency, + # which we don't want when using 'genvslite'. A gen_lite build as little involvement with the visual studio's + # build system as possible. + self.add_target_deps(root, target) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) + if self.environment.coredata.get_option(OptionKey('layout')) == 'mirror': + self.gen_vcxproj_filters(target, ofname) + return True + + def gen_vcxproj_filters(self, target, ofname): + # Generate pitchfork of filters based on directory structure. + root = ET.Element('Project', {'ToolsVersion': '4.0', + 'xmlns': 'http://schemas.microsoft.com/developer/msbuild/2003'}) + filter_folders = ET.SubElement(root, 'ItemGroup') + filter_items = ET.SubElement(root, 'ItemGroup') + mlog.debug(f'Generating vcxproj filters {target.name}.') + + def relative_to_defined_in(file): + # Get the relative path to file's directory from the location of the meson.build that defines this target. + return os.path.dirname(self.relpath(PureWindowsPath(file.subdir, file.fname), self.get_target_dir(target))) + + found_folders_to_filter = {} + all_files = target.sources + target.extra_files + + # Build a dictionary of all used relative paths (i.e. from the meson.build defining this target) + # for all sources. + for i in all_files: + if not os.path.isabs(i.fname): + dirname = relative_to_defined_in(i) + if dirname: + found_folders_to_filter[dirname] = '' + + # Now walk up each of those relative paths checking for empty intermediate dirs to generate the filter. + for folder in found_folders_to_filter: + dirname = folder + filter = '' + + while dirname: + basename = os.path.basename(dirname) + + if filter == '': + filter = basename + else: + # Use '/' to squash empty dirs. To actually get a '\', use '%255c'. + filter = basename + ('\\' if dirname in found_folders_to_filter else '/') + filter + + dirname = os.path.dirname(dirname) + + # Don't add an empty filter, breaks all other (?) filters. + if filter != '': + found_folders_to_filter[folder] = filter + filter_element = ET.SubElement(filter_folders, 'Filter', {'Include': filter}) + uuid_element = ET.SubElement(filter_element, 'UniqueIdentifier') + uuid_element.text = '{' + str(uuid.uuid4()).upper() + '}' + + sources, headers, objects, _ = self.split_sources(all_files) + down = self.target_to_build_root(target) + + def add_element(type_name, elements): + for i in elements: + if not os.path.isabs(i.fname): + dirname = relative_to_defined_in(i) + + if dirname and dirname in found_folders_to_filter: + relpath = os.path.join(down, i.rel_to_builddir(self.build_to_src)) + target_element = ET.SubElement(filter_items, type_name, {'Include': relpath}) + filter_element = ET.SubElement(target_element, 'Filter') + filter_element.text = found_folders_to_filter[dirname] + + add_element('ClCompile', sources) + add_element('ClInclude', headers) + add_element('Object', objects) + + self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname + '.filters') + + def gen_regenproj(self): + # To fully adapt the REGEN work for a 'genvslite' solution, to check timestamps, settings, and regenerate the + # '[builddir]_vs' solution/vcxprojs, as well as regenerating the accompanying buildtype-suffixed ninja build + # directories (from which we need to first collect correct, updated preprocessor defs and compiler options in + # order to fill in the regenerated solution's intellisense settings) would require some non-trivial intrusion + # into the 'meson --internal regencheck ./meson-private' execution path (and perhaps also the '--internal + # regenerate' and even 'meson setup --reconfigure' code). So, for now, we'll instead give the user a simpler + # 'reconfigure' utility project that just runs 'meson setup --reconfigure [builddir]_[buildtype] [srcdir]' on + # each of the ninja build dirs. + # + # FIXME: That will keep the building and compiling correctly configured but obviously won't update the + # solution and vcxprojs, which may allow solution src files and intellisense options to go out-of-date; the + # user would still have to manually 'meson setup --genvslite [vsxxxx] [builddir] [srcdir]' to fully regenerate + # a complete and correct solution. + if self.gen_lite: + project_name = 'RECONFIGURE' + ofname = os.path.join(self.environment.get_build_dir(), 'RECONFIGURE.vcxproj') + conftype = 'Makefile' + # I find the REGEN project doesn't work; it fails to invoke the appropriate - + # python meson.py --internal regencheck builddir\meson-private + # command, despite the fact that manually running such a command in a shell runs just fine. + # Running/building the regen project produces the error - + # ...Microsoft.CppBuild.targets(460,5): error MSB8020: The build tools for ClangCL (Platform Toolset = 'ClangCL') cannot be found. To build using the ClangCL build tools, please install ... + # Not sure why but a simple makefile-style project that executes the full '...regencheck...' command actually works (and seems a little simpler). + # Although I've limited this change to only happen under '--genvslite', perhaps ... + # FIXME : Should all utility projects use the simpler and less problematic makefile-style project? + else: + project_name = 'REGEN' + ofname = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') + conftype = 'Utility' - def gen_regenproj(self, project_name, ofname): guid = self.environment.coredata.regen_guid (root, type_config) = self.create_basic_project(project_name, temp_dir='regen-temp', - guid=guid) - - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - regen_command = self.environment.get_build_command() + ['--internal', 'regencheck'] - cmd_templ = '''call %s > NUL + guid=guid, + conftype=conftype + ) + + if self.gen_lite: + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + all_configs_prop_group = ET.SubElement(root, 'PropertyGroup') + + # Multi-line command to reconfigure all buildtype-suffixed build dirs + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this RECONFIGURE.vcxproj will always be in the '[buildir]_vs' dir. + proj_to_src_dir = self.build_to_src + reconfigure_all_cmd = '' + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + reconfigure_all_cmd += f'{nmake_base_meson_command} setup --reconfigure "{proj_to_build_dir_for_buildtype}" "{proj_to_src_dir}"\n' + ET.SubElement(all_configs_prop_group, 'NMakeBuildCommandLine').text = reconfigure_all_cmd + ET.SubElement(all_configs_prop_group, 'NMakeReBuildCommandLine').text = reconfigure_all_cmd + ET.SubElement(all_configs_prop_group, 'NMakeCleanCommandLine').text = '' + + #Need to set the 'ExecutablePath' element for the above NMake... commands to be able to execute + ET.SubElement(all_configs_prop_group, 'ExecutablePath').text = exe_search_paths + else: + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + regen_command = self.environment.get_build_command() + ['--internal', 'regencheck'] + cmd_templ = '''call %s > NUL "%s" "%s"''' - regen_command = cmd_templ % \ - (self.get_vcvars_command(), '" "'.join(regen_command), self.environment.get_scratch_dir()) - self.add_custom_build(root, 'regen', regen_command, deps=self.get_regen_filelist(), - outputs=[Vs2010Backend.get_regen_stampfile(self.environment.get_build_dir())], - msg='Checking whether solution needs to be regenerated.') + regen_command = cmd_templ % \ + (self.get_vcvars_command(), '" "'.join(regen_command), self.environment.get_scratch_dir()) + self.add_custom_build(root, 'regen', regen_command, deps=self.get_regen_filelist(), + outputs=[Vs2010Backend.get_regen_stampfile(self.environment.get_build_dir())], + msg='Checking whether solution needs to be regenerated.') + ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') ET.SubElement(root, 'ImportGroup', Label='ExtensionTargets') self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_testproj(self, target_name, ofname): + def gen_testproj(self): + project_name = 'RUN_TESTS' + ofname = os.path.join(self.environment.get_build_dir(), f'{project_name}.vcxproj') guid = self.environment.coredata.test_guid - (root, type_config) = self.create_basic_project(target_name, - temp_dir='test-temp', - guid=guid) + if self.gen_lite: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid, + conftype='Makefile' + ) + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this .vcxproj will always be in the '[buildir]_vs' dir. + # Add appropriate 'test' commands for the 'build' action of this project, for all buildtypes + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + test_cmd = f'{nmake_base_meson_command} test -C "{proj_to_build_dir_for_buildtype}" --no-rebuild' + if not self.environment.coredata.get_option(OptionKey('stdsplit')): + test_cmd += ' --no-stdsplit' + if self.environment.coredata.get_option(OptionKey('errorlogs')): + test_cmd += ' --print-errorlogs' + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{self.platform}\'' + prop_group = ET.SubElement(root, 'PropertyGroup', Condition=condition) + ET.SubElement(prop_group, 'NMakeBuildCommandLine').text = test_cmd + #Need to set the 'ExecutablePath' element for the NMake... commands to be able to execute + ET.SubElement(prop_group, 'ExecutablePath').text = exe_search_paths + else: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='test-temp', + guid=guid) + + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + # FIXME: No benchmarks? + test_command = self.environment.get_build_command() + ['test', '--no-rebuild'] + if not self.environment.coredata.get_option(OptionKey('stdsplit')): + test_command += ['--no-stdsplit'] + if self.environment.coredata.get_option(OptionKey('errorlogs')): + test_command += ['--print-errorlogs'] + self.serialize_tests() + self.add_custom_build(root, 'run_tests', '"%s"' % ('" "'.join(test_command))) - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - # FIXME: No benchmarks? - test_command = self.environment.get_build_command() + ['test', '--no-rebuild'] - if not self.environment.coredata.get_option(OptionKey('stdsplit')): - test_command += ['--no-stdsplit'] - if self.environment.coredata.get_option(OptionKey('errorlogs')): - test_command += ['--print-errorlogs'] - self.serialize_tests() - self.add_custom_build(root, 'run_tests', '"%s"' % ('" "'.join(test_command))) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def gen_installproj(self, target_name, ofname): - self.create_install_data_files() - + def gen_installproj(self): + project_name = 'RUN_INSTALL' + ofname = os.path.join(self.environment.get_build_dir(), f'{project_name}.vcxproj') guid = self.environment.coredata.install_guid - (root, type_config) = self.create_basic_project(target_name, - temp_dir='install-temp', - guid=guid) + if self.gen_lite: + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid, + conftype='Makefile' + ) + (nmake_base_meson_command, exe_search_paths) = Vs2010Backend.get_nmake_base_meson_command_and_exe_search_paths() + multi_config_buildtype_list = coredata.get_genvs_default_buildtype_list() + (_, build_dir_tail) = os.path.split(self.src_to_build) + proj_to_multiconfigured_builds_parent_dir = '..' # We know this .vcxproj will always be in the '[buildir]_vs' dir. + # Add appropriate 'install' commands for the 'build' action of this project, for all buildtypes + for buildtype in multi_config_buildtype_list: + meson_build_dir_for_buildtype = build_dir_tail[:-2] + buildtype # Get the buildtype suffixed 'builddir_[debug/release/etc]' from 'builddir_vs', for example. + proj_to_build_dir_for_buildtype = str(os.path.join(proj_to_multiconfigured_builds_parent_dir, meson_build_dir_for_buildtype)) + install_cmd = f'{nmake_base_meson_command} install -C "{proj_to_build_dir_for_buildtype}" --no-rebuild' + condition = f'\'$(Configuration)|$(Platform)\'==\'{buildtype}|{self.platform}\'' + prop_group = ET.SubElement(root, 'PropertyGroup', Condition=condition) + ET.SubElement(prop_group, 'NMakeBuildCommandLine').text = install_cmd + #Need to set the 'ExecutablePath' element for the NMake... commands to be able to execute + ET.SubElement(prop_group, 'ExecutablePath').text = exe_search_paths + else: + self.create_install_data_files() + + (root, type_config) = self.create_basic_project(project_name, + temp_dir='install-temp', + guid=guid) + + action = ET.SubElement(root, 'ItemDefinitionGroup') + midl = ET.SubElement(action, 'Midl') + ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' + ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' + ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' + ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' + ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' + ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' + install_command = self.environment.get_build_command() + ['install', '--no-rebuild'] + self.add_custom_build(root, 'run_install', '"%s"' % ('" "'.join(install_command))) - action = ET.SubElement(root, 'ItemDefinitionGroup') - midl = ET.SubElement(action, 'Midl') - ET.SubElement(midl, "AdditionalIncludeDirectories").text = '%(AdditionalIncludeDirectories)' - ET.SubElement(midl, "OutputDirectory").text = '$(IntDir)' - ET.SubElement(midl, 'HeaderFileName').text = '%(Filename).h' - ET.SubElement(midl, 'TypeLibraryName').text = '%(Filename).tlb' - ET.SubElement(midl, 'InterfaceIdentifierFilename').text = '%(Filename)_i.c' - ET.SubElement(midl, 'ProxyFileName').text = '%(Filename)_p.c' - install_command = self.environment.get_build_command() + ['install', '--no-rebuild'] - self.add_custom_build(root, 'run_install', '"%s"' % ('" "'.join(install_command))) ET.SubElement(root, 'Import', Project=r'$(VCTargetsPath)\Microsoft.Cpp.targets') self.add_regen_dependency(root) self._prettyprint_vcxproj_xml(ET.ElementTree(root), ofname) - def add_custom_build(self, node, rulename, command, deps=None, outputs=None, msg=None, verify_files=True): + def add_custom_build(self, node: ET.Element, rulename: str, command: str, deps: T.Optional[T.List[str]] = None, + outputs: T.Optional[T.List[str]] = None, msg: T.Optional[str] = None, verify_files: bool = True) -> None: igroup = ET.SubElement(node, 'ItemGroup') rulefile = os.path.join(self.environment.get_scratch_dir(), rulename + '.rule') if not os.path.exists(rulefile): @@ -1558,20 +2078,23 @@ class Vs2010Backend(backends.Backend): ET.SubElement(custombuild, 'AdditionalInputs').text = ';'.join(deps) @staticmethod - def nonexistent_file(prefix): + def nonexistent_file(prefix: str) -> str: i = 0 file = prefix while os.path.exists(file): file = '%s%d' % (prefix, i) return file - def generate_debug_information(self, link): + def generate_debug_information(self, link: ET.Element) -> None: # valid values for vs2015 is 'false', 'true', 'DebugFastLink' ET.SubElement(link, 'GenerateDebugInformation').text = 'true' - def add_regen_dependency(self, root): - regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') - self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid) + def add_regen_dependency(self, root: ET.Element) -> None: + # For now, with 'genvslite' solutions, REGEN is replaced by the lighter-weight RECONFIGURE utility that is + # no longer a forced build dependency. See comment in 'gen_regenproj' + if not self.gen_lite: + regen_vcxproj = os.path.join(self.environment.get_build_dir(), 'REGEN.vcxproj') + self.add_project_reference(root, regen_vcxproj, self.environment.coredata.regen_guid) - def generate_lang_standard_info(self, file_args, clconf): + def generate_lang_standard_info(self, file_args: T.Dict[str, CompilerArgs], clconf: ET.Element) -> None: pass diff --git a/mesonbuild/backend/vs2012backend.py b/mesonbuild/backend/vs2012backend.py index af8d5df..76e5c40 100644 --- a/mesonbuild/backend/vs2012backend.py +++ b/mesonbuild/backend/vs2012backend.py @@ -23,9 +23,11 @@ if T.TYPE_CHECKING: from ..interpreter import Interpreter class Vs2012Backend(Vs2010Backend): + + name = 'vs2012' + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'vs2012' self.vs_version = '2012' self.sln_file_version = '12.00' self.sln_version_comment = '2012' diff --git a/mesonbuild/backend/vs2013backend.py b/mesonbuild/backend/vs2013backend.py index 44d45d6..1fbde46 100644 --- a/mesonbuild/backend/vs2013backend.py +++ b/mesonbuild/backend/vs2013backend.py @@ -22,9 +22,11 @@ if T.TYPE_CHECKING: from ..interpreter import Interpreter class Vs2013Backend(Vs2010Backend): + + name = 'vs2013' + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'vs2013' self.vs_version = '2013' self.sln_file_version = '12.00' self.sln_version_comment = '2013' diff --git a/mesonbuild/backend/vs2015backend.py b/mesonbuild/backend/vs2015backend.py index 25e0a5e..8e4da36 100644 --- a/mesonbuild/backend/vs2015backend.py +++ b/mesonbuild/backend/vs2015backend.py @@ -23,9 +23,11 @@ if T.TYPE_CHECKING: from ..interpreter import Interpreter class Vs2015Backend(Vs2010Backend): + + name = 'vs2015' + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'vs2015' self.vs_version = '2015' self.sln_file_version = '12.00' self.sln_version_comment = '14' diff --git a/mesonbuild/backend/vs2017backend.py b/mesonbuild/backend/vs2017backend.py index 4ed5e48..375d660 100644 --- a/mesonbuild/backend/vs2017backend.py +++ b/mesonbuild/backend/vs2017backend.py @@ -26,9 +26,11 @@ if T.TYPE_CHECKING: class Vs2017Backend(Vs2010Backend): + + name = 'vs2017' + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'vs2017' self.vs_version = '2017' self.sln_file_version = '12.00' self.sln_version_comment = '15' diff --git a/mesonbuild/backend/vs2019backend.py b/mesonbuild/backend/vs2019backend.py index 0734336..f01f7ec 100644 --- a/mesonbuild/backend/vs2019backend.py +++ b/mesonbuild/backend/vs2019backend.py @@ -25,9 +25,11 @@ if T.TYPE_CHECKING: class Vs2019Backend(Vs2010Backend): + + name = 'vs2019' + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'vs2019' self.sln_file_version = '12.00' self.sln_version_comment = 'Version 16' if self.environment is not None: diff --git a/mesonbuild/backend/vs2022backend.py b/mesonbuild/backend/vs2022backend.py index b1f93c3..ea715d8 100644 --- a/mesonbuild/backend/vs2022backend.py +++ b/mesonbuild/backend/vs2022backend.py @@ -25,9 +25,11 @@ if T.TYPE_CHECKING: class Vs2022Backend(Vs2010Backend): - def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter]): - super().__init__(build, interpreter) - self.name = 'vs2022' + + name = 'vs2022' + + def __init__(self, build: T.Optional[Build], interpreter: T.Optional[Interpreter], gen_lite: bool = False): + super().__init__(build, interpreter, gen_lite=gen_lite) self.sln_file_version = '12.00' self.sln_version_comment = 'Version 17' if self.environment is not None: diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py index c56036b..ec03627 100644 --- a/mesonbuild/backend/xcodebackend.py +++ b/mesonbuild/backend/xcodebackend.py @@ -18,10 +18,9 @@ import typing as T from . import backends from .. import build -from .. import dependencies from .. import mesonlib from .. import mlog -from ..mesonlib import MesonException, OptionKey +from ..mesonlib import MesonBugException, MesonException, OptionKey if T.TYPE_CHECKING: from ..interpreter import Interpreter @@ -65,26 +64,21 @@ LINKABLE_EXTENSIONS = {'.o', '.a', '.obj', '.so', '.dylib'} class FileTreeEntry: - def __init__(self): + def __init__(self) -> None: self.subdirs = {} self.targets = [] -class PbxItem: - def __init__(self, value, comment = ''): - self.value = value - self.comment = comment - class PbxArray: - def __init__(self): + def __init__(self) -> None: self.items = [] - def add_item(self, item, comment=''): + def add_item(self, item: T.Union[PbxArrayItem, str], comment: str = '') -> None: if isinstance(item, PbxArrayItem): self.items.append(item) else: self.items.append(PbxArrayItem(item, comment)) - def write(self, ofile, indent_level): + def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write('(\n') indent_level += 1 for i in self.items: @@ -96,7 +90,7 @@ class PbxArray: ofile.write(indent_level*INDENT + ');\n') class PbxArrayItem: - def __init__(self, value, comment = ''): + def __init__(self, value: str, comment: str = ''): self.value = value if comment: if '/*' in comment: @@ -107,16 +101,16 @@ class PbxArrayItem: self.comment = comment class PbxComment: - def __init__(self, text): + def __init__(self, text: str): assert isinstance(text, str) assert '/*' not in text self.text = f'/* {text} */' - def write(self, ofile, indent_level): + def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write(f'\n{self.text}\n') class PbxDictItem: - def __init__(self, key, value, comment = ''): + def __init__(self, key: str, value: T.Union[PbxArray, PbxDict, str, int], comment: str = ''): self.key = key self.value = value if comment: @@ -128,13 +122,13 @@ class PbxDictItem: self.comment = comment class PbxDict: - def __init__(self): + def __init__(self) -> None: # This class is a bit weird, because we want to write PBX dicts in # defined order _and_ we want to write intermediate comments also in order. self.keys = set() self.items = [] - def add_item(self, key, value, comment=''): + def add_item(self, key: str, value: T.Union[PbxArray, PbxDict, str, int], comment: str = '') -> None: assert key not in self.keys item = PbxDictItem(key, value, comment) self.keys.add(key) @@ -143,14 +137,11 @@ class PbxDict: def has_item(self, key): return key in self.keys - def add_comment(self, comment): - if isinstance(comment, str): - self.items.append(PbxComment(str)) - else: - assert isinstance(comment, PbxComment) - self.items.append(comment) + def add_comment(self, comment: PbxComment) -> None: + assert isinstance(comment, PbxComment) + self.items.append(comment) - def write(self, ofile, indent_level): + def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write('{\n') indent_level += 1 for i in self.items: @@ -191,11 +182,13 @@ class PbxDict: ofile.write(';\n') class XCodeBackend(backends.Backend): + + name = 'xcode' + def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) - self.name = 'xcode' self.project_uid = self.environment.coredata.lang_guids['default'].replace('-', '')[:24] - self.buildtype = self.environment.coredata.get_option(OptionKey('buildtype')) + self.buildtype = T.cast('str', self.environment.coredata.get_option(OptionKey('buildtype'))) self.project_conflist = self.gen_id() self.maingroup_id = self.gen_id() self.all_id = self.gen_id() @@ -228,11 +221,11 @@ class XCodeBackend(backends.Backend): top_level_dict.write(ofile, 0) os.replace(tmpname, ofilename) - def gen_id(self): + def gen_id(self) -> str: return str(uuid.uuid4()).upper().replace('-', '')[:24] def get_target_dir(self, target): - dirname = os.path.join(target.get_subdir(), self.environment.coredata.get_option(OptionKey('buildtype'))) + dirname = os.path.join(target.get_subdir(), T.cast('str', self.environment.coredata.get_option(OptionKey('buildtype')))) #os.makedirs(os.path.join(self.environment.get_build_dir(), dirname), exist_ok=True) return dirname @@ -260,7 +253,12 @@ class XCodeBackend(backends.Backend): obj_path = f'{project}.build/{buildtype}/{tname}.build/Objects-normal/{arch}/{stem}.o' return obj_path - def generate(self): + def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional[dict]: + # Check for (currently) unexpected capture arg use cases - + if capture: + raise MesonBugException('We do not expect the xcode backend to generate with \'capture = True\'') + if vslite_ctx: + raise MesonBugException('We do not expect the xcode backend to be given a valid \'vslite_ctx\'') self.serialize_tests() # Cache the result as the method rebuilds the array every time it is called. self.build_targets = self.build.get_build_targets() @@ -342,7 +340,7 @@ class XCodeBackend(backends.Backend): xcodetype = 'sourcecode.unknown' return xcodetype - def generate_filemap(self): + def generate_filemap(self) -> None: self.filemap = {} # Key is source file relative to src root. self.target_filemap = {} for name, t in self.build_targets.items(): @@ -356,10 +354,10 @@ class XCodeBackend(backends.Backend): self.filemap[o] = self.gen_id() self.target_filemap[name] = self.gen_id() - def generate_buildstylemap(self): + def generate_buildstylemap(self) -> None: self.buildstylemap = {self.buildtype: self.gen_id()} - def generate_build_phase_map(self): + def generate_build_phase_map(self) -> None: for tname, t in self.build_targets.items(): # generate id for our own target-name t.buildphasemap = {} @@ -369,7 +367,7 @@ class XCodeBackend(backends.Backend): t.buildphasemap['Resources'] = self.gen_id() t.buildphasemap['Sources'] = self.gen_id() - def generate_build_configuration_map(self): + def generate_build_configuration_map(self) -> None: self.buildconfmap = {} for t in self.build_targets: bconfs = {self.buildtype: self.gen_id()} @@ -378,28 +376,28 @@ class XCodeBackend(backends.Backend): bconfs = {self.buildtype: self.gen_id()} self.buildconfmap[t] = bconfs - def generate_project_configurations_map(self): + def generate_project_configurations_map(self) -> None: self.project_configurations = {self.buildtype: self.gen_id()} - def generate_buildall_configurations_map(self): + def generate_buildall_configurations_map(self) -> None: self.buildall_configurations = {self.buildtype: self.gen_id()} - def generate_test_configurations_map(self): + def generate_test_configurations_map(self) -> None: self.test_configurations = {self.buildtype: self.gen_id()} - def generate_build_configurationlist_map(self): + def generate_build_configurationlist_map(self) -> None: self.buildconflistmap = {} for t in self.build_targets: self.buildconflistmap[t] = self.gen_id() for t in self.custom_targets: self.buildconflistmap[t] = self.gen_id() - def generate_native_target_map(self): + def generate_native_target_map(self) -> None: self.native_targets = {} for t in self.build_targets: self.native_targets[t] = self.gen_id() - def generate_custom_target_map(self): + def generate_custom_target_map(self) -> None: self.shell_targets = {} self.custom_target_output_buildfile = {} self.custom_target_output_fileref = {} @@ -412,7 +410,7 @@ class XCodeBackend(backends.Backend): self.custom_target_output_buildfile[o] = self.gen_id() self.custom_target_output_fileref[o] = self.gen_id() - def generate_generator_target_map(self): + def generate_generator_target_map(self) -> None: # Generator objects do not have natural unique ids # so use a counter. self.generator_fileref_ids = {} @@ -452,17 +450,17 @@ class XCodeBackend(backends.Backend): self.generator_buildfile_ids[k] = buildfile_ids self.generator_fileref_ids[k] = fileref_ids - def generate_native_frameworks_map(self): + def generate_native_frameworks_map(self) -> None: self.native_frameworks = {} self.native_frameworks_fileref = {} for t in self.build_targets.values(): for dep in t.get_external_deps(): - if isinstance(dep, dependencies.AppleFrameworks): + if dep.name == 'appleframeworks': for f in dep.frameworks: self.native_frameworks[f] = self.gen_id() self.native_frameworks_fileref[f] = self.gen_id() - def generate_target_dependency_map(self): + def generate_target_dependency_map(self) -> None: self.target_dependency_map = {} for tname, t in self.build_targets.items(): for target in t.link_targets: @@ -479,7 +477,7 @@ class XCodeBackend(backends.Backend): assert k not in self.target_dependency_map self.target_dependency_map[k] = self.gen_id() - def generate_pbxdep_map(self): + def generate_pbxdep_map(self) -> None: self.pbx_dep_map = {} self.pbx_custom_dep_map = {} for t in self.build_targets: @@ -487,12 +485,12 @@ class XCodeBackend(backends.Backend): for t in self.custom_targets: self.pbx_custom_dep_map[t] = self.gen_id() - def generate_containerproxy_map(self): + def generate_containerproxy_map(self) -> None: self.containerproxy_map = {} for t in self.build_targets: self.containerproxy_map[t] = self.gen_id() - def generate_target_file_maps(self): + def generate_target_file_maps(self) -> None: self.generate_target_file_maps_impl(self.build_targets) self.generate_target_file_maps_impl(self.custom_targets) @@ -526,13 +524,13 @@ class XCodeBackend(backends.Backend): else: raise RuntimeError('Unknown input type ' + str(o)) - def generate_build_file_maps(self): + def generate_build_file_maps(self) -> None: for buildfile in self.interpreter.get_build_def_files(): assert isinstance(buildfile, str) self.buildfile_ids[buildfile] = self.gen_id() self.fileref_ids[buildfile] = self.gen_id() - def generate_source_phase_map(self): + def generate_source_phase_map(self) -> None: self.source_phase = {} for t in self.build_targets: self.source_phase[t] = self.gen_id() @@ -599,7 +597,7 @@ class XCodeBackend(backends.Backend): def generate_pbx_build_file(self, objects_dict): for tname, t in self.build_targets.items(): for dep in t.get_external_deps(): - if isinstance(dep, dependencies.AppleFrameworks): + if dep.name == 'appleframeworks': for f in dep.frameworks: fw_dict = PbxDict() fwkey = self.native_frameworks[f] @@ -709,7 +707,7 @@ class XCodeBackend(backends.Backend): def generate_pbx_file_reference(self, objects_dict): for tname, t in self.build_targets.items(): for dep in t.get_external_deps(): - if isinstance(dep, dependencies.AppleFrameworks): + if dep.name == 'appleframeworks': for f in dep.frameworks: fw_dict = PbxDict() framework_fileref = self.native_frameworks_fileref[f] @@ -869,7 +867,7 @@ class XCodeBackend(backends.Backend): file_list = PbxArray() bt_dict.add_item('files', file_list) for dep in t.get_external_deps(): - if isinstance(dep, dependencies.AppleFrameworks): + if dep.name == 'appleframeworks': for f in dep.frameworks: file_list.add_item(self.native_frameworks[f], f'{f}.framework in Frameworks') bt_dict.add_item('runOnlyForDeploymentPostprocessing', 0) @@ -917,7 +915,7 @@ class XCodeBackend(backends.Backend): for t in self.build_targets.values(): for dep in t.get_external_deps(): - if isinstance(dep, dependencies.AppleFrameworks): + if dep.name == 'appleframeworks': for f in dep.frameworks: frameworks_children.add_item(self.native_frameworks_fileref[f], f) @@ -1023,6 +1021,7 @@ class XCodeBackend(backends.Backend): group_id = self.write_group_target_entry(objects_dict, target) children_array.add_item(group_id) potentials = [os.path.join(current_subdir, 'meson.build'), + os.path.join(current_subdir, 'meson.options'), os.path.join(current_subdir, 'meson_options.txt')] for bf in potentials: i = self.fileref_ids.get(bf, None) @@ -1219,6 +1218,8 @@ class XCodeBackend(backends.Backend): generator_id += 1 def generate_single_generator_phase(self, tname, t, genlist, generator_id, objects_dict): + # TODO: this should be rewritten to use the meson wrapper, like the other generators do + # Currently it doesn't handle a host binary that requires an exe wrapper correctly. generator = genlist.get_generator() exe = generator.get_exe() exe_arr = self.build_target_to_cmd_array(exe) @@ -1493,8 +1494,7 @@ class XCodeBackend(backends.Backend): else: raise RuntimeError(o) if isinstance(target, build.SharedModule): - options = self.environment.coredata.options - ldargs += linker.get_std_shared_module_link_args(options) + ldargs += linker.get_std_shared_module_link_args(target.get_options()) elif isinstance(target, build.SharedLibrary): ldargs += linker.get_std_shared_lib_link_args() ldstr = ' '.join(ldargs) @@ -1530,7 +1530,7 @@ class XCodeBackend(backends.Backend): # add the root build dir to the search path. So add an absolute path instead. # This may break reproducible builds, in which case patches are welcome. lang_cargs += self.get_custom_target_dir_include_args(target, compiler, absolute_path=True) - # Xcode can not handle separate compilation flags for C and ObjectiveC. They are both + # Xcode cannot handle separate compilation flags for C and ObjectiveC. They are both # put in OTHER_CFLAGS. Same with C++ and ObjectiveC++. if lang == 'objc': lang = 'c' @@ -1635,7 +1635,7 @@ class XCodeBackend(backends.Backend): quoted_args.append(a) settings_dict.add_item(f'OTHER_{langname}FLAGS', '"' + ' '.join(quoted_args) + '"') - def generate_xc_configurationList(self, objects_dict): + def generate_xc_configurationList(self, objects_dict: PbxDict) -> None: # FIXME: sort items conf_dict = PbxDict() objects_dict.add_item(self.project_conflist, conf_dict, f'Build configuration list for PBXProject "{self.build.project_name}"') @@ -1704,7 +1704,7 @@ class XCodeBackend(backends.Backend): t_dict.add_item('defaultConfigurationIsVisible', 0) t_dict.add_item('defaultConfigurationName', self.buildtype) - def generate_prefix(self, pbxdict): + def generate_prefix(self, pbxdict: PbxDict) -> PbxDict: pbxdict.add_item('archiveVersion', '1') pbxdict.add_item('classes', PbxDict()) pbxdict.add_item('objectVersion', '46') @@ -1713,5 +1713,5 @@ class XCodeBackend(backends.Backend): return objects_dict - def generate_suffix(self, pbxdict): + def generate_suffix(self, pbxdict: PbxDict) -> None: pbxdict.add_item('rootObject', self.project_uid, 'Project object') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 60cd0cf..12e7cb5 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -14,9 +14,9 @@ from __future__ import annotations from collections import defaultdict, OrderedDict -from dataclasses import dataclass, field +from dataclasses import dataclass, field, InitVar from functools import lru_cache -import copy +import abc import hashlib import itertools, pathlib import os @@ -25,8 +25,7 @@ import re import textwrap import typing as T - -from . import environment +from . import coredata from . import dependencies from . import mlog from . import programs @@ -35,24 +34,27 @@ from .mesonlib import ( File, MesonException, MachineChoice, PerMachine, OrderedSet, listify, extract_as_list, typeslistify, stringlistify, classify_unity_sources, get_filenames_templates_dict, substitute_values, has_path_sep, - OptionKey, PerMachineDefaultable, OptionOverrideProxy, + OptionKey, PerMachineDefaultable, MesonBugException, EnvironmentVariables, pickle_load, ) from .compilers import ( - is_object, clink_langs, sort_clink, all_languages, + is_header, is_object, is_source, clink_langs, sort_clink, all_languages, is_known_suffix, detect_static_linker ) from .interpreterbase import FeatureNew, FeatureDeprecated if T.TYPE_CHECKING: - from typing_extensions import Literal + from typing_extensions import Literal, TypedDict + + from . import environment from ._typing import ImmutableListProtocol - from .backend.backends import Backend, ExecutableSerialisation + from .backend.backends import Backend from .compilers import Compiler - from .interpreter.interpreter import Test, SourceOutputs, Interpreter + from .interpreter.interpreter import SourceOutputs, Interpreter + from .interpreter.interpreterobjects import Test from .interpreterbase import SubProject - from .linkers import StaticLinker - from .mesonlib import FileMode, FileOrString + from .linkers.linkers import StaticLinker + from .mesonlib import ExecutableSerialisation, FileMode, FileOrString from .modules import ModuleState from .mparser import BaseNode from .wrap import WrapMode @@ -60,6 +62,14 @@ if T.TYPE_CHECKING: GeneratedTypes = T.Union['CustomTarget', 'CustomTargetIndex', 'GeneratedList'] LibTypes = T.Union['SharedLibrary', 'StaticLibrary', 'CustomTarget', 'CustomTargetIndex'] BuildTargetTypes = T.Union['BuildTarget', 'CustomTarget', 'CustomTargetIndex'] + ObjectTypes = T.Union[str, 'File', 'ExtractedObjects', 'GeneratedTypes'] + + class DFeatures(TypedDict): + + unittest: bool + debug: T.List[T.Union[str, int]] + import_dirs: T.List[IncludeDirs] + versions: T.List[T.Union[str, int]] pch_kwargs = {'c_pch', 'cpp_pch'} @@ -72,7 +82,7 @@ lang_arg_kwargs |= { } vala_kwargs = {'vala_header', 'vala_gir', 'vala_vapi'} -rust_kwargs = {'rust_crate_type'} +rust_kwargs = {'rust_crate_type', 'rust_dependency_map'} cs_kwargs = {'resources', 'cs_args'} buildtarget_kwargs = { @@ -111,10 +121,10 @@ known_build_target_kwargs = ( rust_kwargs | cs_kwargs) -known_exe_kwargs = known_build_target_kwargs | {'implib', 'export_dynamic', 'pie'} -known_shlib_kwargs = known_build_target_kwargs | {'version', 'soversion', 'vs_module_defs', 'darwin_versions'} -known_shmod_kwargs = known_build_target_kwargs | {'vs_module_defs'} -known_stlib_kwargs = known_build_target_kwargs | {'pic', 'prelink'} +known_exe_kwargs = known_build_target_kwargs | {'implib', 'export_dynamic', 'pie', 'vs_module_defs'} +known_shlib_kwargs = known_build_target_kwargs | {'version', 'soversion', 'vs_module_defs', 'darwin_versions', 'rust_abi'} +known_shmod_kwargs = known_build_target_kwargs | {'vs_module_defs', 'rust_abi'} +known_stlib_kwargs = known_build_target_kwargs | {'pic', 'prelink', 'rust_abi'} known_jar_kwargs = known_exe_kwargs | {'main_class', 'java_resources'} def _process_install_tag(install_tag: T.Optional[T.List[T.Optional[str]]], @@ -153,6 +163,7 @@ class Headers(HoldableObject): custom_install_dir: T.Optional[str] custom_install_mode: 'FileMode' subproject: str + follow_symlinks: T.Optional[bool] = None # TODO: we really don't need any of these methods, but they're preserved to # keep APIs relying on them working. @@ -211,16 +222,20 @@ class InstallDir(HoldableObject): subproject: str from_source_dir: bool = True install_tag: T.Optional[str] = None + follow_symlinks: T.Optional[bool] = None @dataclass(eq=False) class DepManifest: version: str license: T.List[str] + license_files: T.List[T.Tuple[str, File]] + subproject: str def to_json(self) -> T.Dict[str, T.Union[str, T.List[str]]]: return { 'version': self.version, 'license': self.license, + 'license_files': [l[1].relative_name() for l in self.license_files], } @@ -231,11 +246,13 @@ class Build: """ def __init__(self, environment: environment.Environment): + self.version = coredata.version self.project_name = 'name of master project' self.project_version = None self.environment = environment self.projects = {} self.targets: 'T.OrderedDict[str, T.Union[CustomTarget, BuildTarget]]' = OrderedDict() + self.targetnames: T.Set[T.Tuple[str, str]] = set() # Set of executable names and their subdir self.global_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {}) self.global_link_args: PerMachine[T.Dict[str, T.List[str]]] = PerMachine({}, {}) self.projects_args: PerMachine[T.Dict[str, T.Dict[str, T.List[str]]]] = PerMachine({}, {}) @@ -268,7 +285,6 @@ class Build: environment.is_cross_build(), {}, {}) self.devenv: T.List[EnvironmentVariables] = [] self.modules: T.List[str] = [] - self.need_vsenv = False def get_build_targets(self): build_targets = OrderedDict() @@ -383,7 +399,7 @@ class IncludeDirs(HoldableObject): def get_extra_build_dirs(self) -> T.List[str]: return self.extra_build_dirs - def to_string_list(self, sourcedir: str, builddir: T.Optional[str] = None) -> T.List[str]: + def to_string_list(self, sourcedir: str, builddir: str) -> T.List[str]: """Convert IncludeDirs object to a list of strings. :param sourcedir: The absolute source directory @@ -394,8 +410,7 @@ class IncludeDirs(HoldableObject): strlist: T.List[str] = [] for idir in self.incdirs: strlist.append(os.path.join(sourcedir, self.curdir, idir)) - if builddir: - strlist.append(os.path.join(builddir, self.curdir, idir)) + strlist.append(os.path.join(builddir, self.curdir, idir)) return strlist @dataclass(eq=False) @@ -408,6 +423,7 @@ class ExtractedObjects(HoldableObject): genlist: T.List['GeneratedTypes'] = field(default_factory=list) objlist: T.List[T.Union[str, 'File', 'ExtractedObjects']] = field(default_factory=list) recursive: bool = True + pch: bool = False def __post_init__(self) -> None: if self.target.is_unity: @@ -429,7 +445,7 @@ class ExtractedObjects(HoldableObject): sources.append(s) # Filter out headers and all non-source files - return [s for s in sources if environment.is_source(s)] + return [s for s in sources if is_source(s)] def classify_all_sources(self, sources: T.List[FileOrString], generated_sources: T.Sequence['GeneratedTypes']) -> T.Dict['Compiler', T.List['FileOrString']]: sources_ = self.get_sources(sources, generated_sources) @@ -447,7 +463,7 @@ class ExtractedObjects(HoldableObject): for comp, srcs in extracted_cmpsrcs.items(): if set(srcs) != set(cmpsrcs[comp]): - raise MesonException('Single object files can not be extracted ' + raise MesonException('Single object files cannot be extracted ' 'in Unity builds. You can only extract all ' 'the object files for each compiler at once.') @@ -502,9 +518,7 @@ class StructuredSources(HoldableObject): @dataclass(eq=False) -class Target(HoldableObject): - - # TODO: should Target be an abc.ABCMeta? +class Target(HoldableObject, metaclass=abc.ABCMeta): name: str subdir: str @@ -512,21 +526,33 @@ class Target(HoldableObject): build_by_default: bool for_machine: MachineChoice environment: environment.Environment + install: bool = False + build_always_stale: bool = False + extra_files: T.List[File] = field(default_factory=list) + override_options: InitVar[T.Optional[T.Dict[OptionKey, str]]] = None - def __post_init__(self) -> None: + @abc.abstractproperty + def typename(self) -> str: + pass + + @abc.abstractmethod + def type_suffix(self) -> str: + pass + + def __post_init__(self, overrides: T.Optional[T.Dict[OptionKey, str]]) -> None: + if overrides: + ovr = {k.evolve(machine=self.for_machine) if k.lang else k: v + for k, v in overrides.items()} + else: + ovr = {} + self.options = coredata.OptionsView(self.environment.coredata.options, self.subproject, ovr) + # XXX: this should happen in the interpreter if has_path_sep(self.name): # Fix failing test 53 when this becomes an error. mlog.warning(textwrap.dedent(f'''\ Target "{self.name}" has a path separator in its name. This is not supported, it can cause unexpected failures and will become - a hard error in the future. - ''')) - self.install = False - self.build_always_stale = False - self.options = OptionOverrideProxy({}, self.environment.coredata.options, self.subproject) - self.extra_files = [] # type: T.List[File] - if not hasattr(self, 'typename'): - raise RuntimeError(f'Target type is not set for target class "{type(self).__name__}". This is a bug') + a hard error in the future.''')) # dataclass comparators? def __lt__(self, other: object) -> bool: @@ -587,7 +613,7 @@ class Target(HoldableObject): return self.typename @staticmethod - def _get_id_hash(target_id): + def _get_id_hash(target_id: str) -> str: # We don't really need cryptographic security here. # Small-digest hash function with unlikely collision is good enough. h = hashlib.sha256() @@ -616,15 +642,19 @@ class Target(HoldableObject): return my_id def get_id(self) -> str: + name = self.name + if getattr(self, 'name_suffix_set', False): + name += '.' + self.suffix return self.construct_id_from_path( - self.subdir, self.name, self.type_suffix()) + self.subdir, name, self.type_suffix()) def process_kwargs_base(self, kwargs: T.Dict[str, T.Any]) -> None: if 'build_by_default' in kwargs: self.build_by_default = kwargs['build_by_default'] if not isinstance(self.build_by_default, bool): raise InvalidArguments('build_by_default must be a boolean value.') - elif kwargs.get('install', False): + + if not self.build_by_default and kwargs.get('install', False): # For backward compatibility, if build_by_default is not explicitly # set, use the value of 'install' if it's enabled. self.build_by_default = True @@ -639,7 +669,7 @@ class Target(HoldableObject): else: self.options.overrides[k] = v - def get_options(self) -> OptionOverrideProxy: + def get_options(self) -> coredata.OptionsView: return self.options def get_option(self, key: 'OptionKey') -> T.Union[str, int, bool, 'WrapMode']: @@ -683,48 +713,89 @@ class BuildTarget(Target): install_dir: T.List[T.Union[str, Literal[False]]] - def __init__(self, name: str, subdir: str, subproject: SubProject, for_machine: MachineChoice, - sources: T.List['SourceOutputs'], structured_sources: T.Optional[StructuredSources], - objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], kwargs): - super().__init__(name, subdir, subproject, True, for_machine, environment) + # This set contains all the languages a linker can link natively + # without extra flags. For instance, nvcc (cuda) can link C++ + # without injecting -lc++/-lstdc++, see + # https://github.com/mesonbuild/meson/issues/10570 + _MASK_LANGS: T.FrozenSet[T.Tuple[str, str]] = frozenset([ + # (language, linker) + ('cpp', 'cuda'), + ]) + + def __init__( + self, + name: str, + subdir: str, + subproject: SubProject, + for_machine: MachineChoice, + sources: T.List['SourceOutputs'], + structured_sources: T.Optional[StructuredSources], + objects: T.List[ObjectTypes], + environment: environment.Environment, + compilers: T.Dict[str, 'Compiler'], + kwargs: T.Dict[str, T.Any]): + super().__init__(name, subdir, subproject, True, for_machine, environment, install=kwargs.get('install', False)) self.all_compilers = compilers - self.compilers = OrderedDict() # type: OrderedDict[str, Compiler] - self.objects: T.List[T.Union[str, 'File', 'ExtractedObjects']] = [] + self.compilers: OrderedDict[str, Compiler] = OrderedDict() + self.objects: T.List[ObjectTypes] = [] self.structured_sources = structured_sources self.external_deps: T.List[dependencies.Dependency] = [] self.include_dirs: T.List['IncludeDirs'] = [] self.link_language = kwargs.get('link_language') self.link_targets: T.List[LibTypes] = [] self.link_whole_targets: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]] = [] + self.depend_files: T.List[File] = [] self.link_depends = [] self.added_deps = set() self.name_prefix_set = False self.name_suffix_set = False self.filename = 'no_name' + # The debugging information file this target will generate + self.debug_filename = None # The list of all files outputted by this target. Useful in cases such # as Vala which generates .vapi and .h besides the compiled output. self.outputs = [self.filename] - self.need_install = False self.pch: T.Dict[str, T.List[str]] = {} - self.extra_args: T.Dict[str, T.List['FileOrString']] = {} + self.extra_args: T.DefaultDict[str, T.List[str]] = kwargs.get('language_args', defaultdict(list)) self.sources: T.List[File] = [] self.generated: T.List['GeneratedTypes'] = [] - self.d_features = defaultdict(list) + self.extra_files: T.List[File] = [] + self.d_features: DFeatures = { + 'debug': kwargs.get('d_debug', []), + 'import_dirs': kwargs.get('d_import_dirs', []), + 'versions': kwargs.get('d_module_versions', []), + 'unittest': kwargs.get('d_unittest', False), + } self.pic = False self.pie = False # Track build_rpath entries so we can remove them at install time self.rpath_dirs_to_remove: T.Set[bytes] = set() self.process_sourcelist(sources) # Objects can be: - # 1. Pre-existing objects provided by the user with the `objects:` kwarg + # 1. Preexisting objects provided by the user with the `objects:` kwarg # 2. Compiled objects created by and extracted from another target self.process_objectlist(objects) self.process_kwargs(kwargs) - self.check_unknown_kwargs(kwargs) - if not any([self.sources, self.generated, self.objects, self.link_whole_targets, self.structured_sources]): + self.missing_languages = self.process_compilers() + + # self.link_targets and self.link_whole_targets contains libraries from + # dependencies (see add_deps()). They have not been processed yet because + # we have to call process_compilers() first and we need to process libraries + # from link_with and link_whole first. + # See https://github.com/mesonbuild/meson/pull/11957#issuecomment-1629243208. + link_targets = extract_as_list(kwargs, 'link_with') + self.link_targets + link_whole_targets = extract_as_list(kwargs, 'link_whole') + self.link_whole_targets + self.link_targets.clear() + self.link_whole_targets.clear() + self.link(link_targets) + self.link_whole(link_whole_targets) + + if not any([self.sources, self.generated, self.objects, self.link_whole_targets, self.structured_sources, + kwargs.pop('_allow_no_sources', False)]): mlog.warning(f'Build target {name} has no sources. ' 'This was never supposed to be allowed but did because of a bug, ' 'support will be removed in a future release of Meson') + self.check_unknown_kwargs(kwargs) self.validate_install() self.check_module_linking() @@ -736,6 +807,16 @@ class BuildTarget(Target): raise MesonException('cannot mix structured sources and unstructured sources') if self.structured_sources and 'rust' not in self.compilers: raise MesonException('structured sources are only supported in Rust targets') + if self.uses_rust(): + # relocation-model=pic is rustc's default and Meson does not + # currently have a way to disable PIC. + self.pic = True + if 'vala' in self.compilers and self.is_linkable_target(): + self.outputs += [self.vala_header, self.vala_vapi] + self.install_tag += ['devel', 'devel'] + if self.vala_gir: + self.outputs.append(self.vala_gir) + self.install_tag.append('devel') def __repr__(self): repr_str = "<{0} {1}: {2}>" @@ -750,7 +831,7 @@ class BuildTarget(Target): return unity_opt == 'on' or (unity_opt == 'subprojects' and self.subproject != '') def validate_install(self): - if self.for_machine is MachineChoice.BUILD and self.need_install: + if self.for_machine is MachineChoice.BUILD and self.install: if self.environment.is_cross_build(): raise InvalidArguments('Tried to install a target for the build machine in a cross build.') else: @@ -764,6 +845,8 @@ class BuildTarget(Target): def check_unknown_kwargs_int(self, kwargs, known_kwargs): unknowns = [] for k in kwargs: + if k == 'language_args': + continue if k not in known_kwargs: unknowns.append(k) if len(unknowns) > 0: @@ -771,24 +854,29 @@ class BuildTarget(Target): def process_objectlist(self, objects): assert isinstance(objects, list) + deprecated_non_objects = [] for s in objects: if isinstance(s, (str, File, ExtractedObjects)): self.objects.append(s) - elif isinstance(s, (GeneratedList, CustomTarget)): - msg = 'Generated files are not allowed in the \'objects\' kwarg ' + \ - f'for target {self.name!r}.\nIt is meant only for ' + \ - 'pre-built object files that are shipped with the\nsource ' + \ - 'tree. Try adding it in the list of sources.' - raise InvalidArguments(msg) + if not isinstance(s, ExtractedObjects) and not is_object(s): + deprecated_non_objects.append(s) + elif isinstance(s, (CustomTarget, CustomTargetIndex, GeneratedList)): + non_objects = [o for o in s.get_outputs() if not is_object(o)] + if non_objects: + raise InvalidArguments(f'Generated file {non_objects[0]} in the \'objects\' kwarg is not an object.') + self.generated.append(s) else: raise InvalidArguments(f'Bad object of type {type(s).__name__!r} in target {self.name!r}.') + if deprecated_non_objects: + FeatureDeprecated.single_use(f'Source file {deprecated_non_objects[0]} in the \'objects\' kwarg is not an object.', + '1.3.0', self.subproject) def process_sourcelist(self, sources: T.List['SourceOutputs']) -> None: """Split sources into generated and static sources. Sources can be: - 1. Pre-existing source files in the source tree (static) - 2. Pre-existing sources generated by configure_file in the build tree. + 1. Preexisting source files in the source tree (static) + 2. Preexisting sources generated by configure_file in the build tree. (static as they are only regenerated if meson itself is regenerated) 3. Sources files generated by another target or a Generator (generated) """ @@ -810,14 +898,14 @@ class BuildTarget(Target): removed = True return removed - def process_compilers_late(self, extra_languages: T.List[str]): + def process_compilers_late(self): """Processes additional compilers after kwargs have been evaluated. This can add extra compilers that might be required by keyword arguments, such as link_with or dependencies. It will also try to guess which compiler to use if one hasn't been selected already. """ - for lang in extra_languages: + for lang in self.missing_languages: self.compilers[lang] = self.all_compilers[lang] # did user override clink_langs for this target? @@ -860,7 +948,7 @@ class BuildTarget(Target): missing_languages: T.List[str] = [] if not any([self.sources, self.generated, self.objects, self.structured_sources]): return missing_languages - # Pre-existing sources + # Preexisting sources sources: T.List['FileOrString'] = list(self.sources) generated = self.generated.copy() @@ -963,18 +1051,6 @@ class BuildTarget(Target): 'Link_depends arguments must be strings, Files, ' 'or a Custom Target, or lists thereof.') - def get_original_kwargs(self): - return self.kwargs - - def copy_kwargs(self, kwargs): - self.kwargs = copy.copy(kwargs) - for k, v in self.kwargs.items(): - if isinstance(v, list): - self.kwargs[k] = listify(v, flatten=True) - for t in ['dependencies', 'link_with', 'include_directories', 'sources']: - if t in self.kwargs: - self.kwargs[t] = listify(self.kwargs[t], flatten=True) - def extract_objects(self, srclist: T.List[T.Union['FileOrString', 'GeneratedTypes']]) -> ExtractedObjects: sources_set = set(self.sources) generated_set = set(self.generated) @@ -1002,7 +1078,7 @@ class BuildTarget(Target): def extract_all_objects(self, recursive: bool = True) -> ExtractedObjects: return ExtractedObjects(self, self.sources, self.generated, self.objects, - recursive) + recursive, pch=True) def get_all_link_deps(self) -> ImmutableListProtocol[BuildTargetTypes]: return self.get_transitive_link_deps() @@ -1048,55 +1124,16 @@ class BuildTarget(Target): def process_kwargs(self, kwargs): self.process_kwargs_base(kwargs) - self.copy_kwargs(kwargs) - kwargs.get('modules', []) - self.need_install = kwargs.get('install', self.need_install) - llist = extract_as_list(kwargs, 'link_with') - for linktarget in llist: - if isinstance(linktarget, dependencies.ExternalLibrary): - raise MesonException(textwrap.dedent('''\ - An external library was used in link_with keyword argument, which - is reserved for libraries built as part of this project. External - libraries must be passed using the dependencies keyword argument - instead, because they are conceptually "external dependencies", - just like those detected with the dependency() function. - ''')) - self.link(linktarget) - lwhole = extract_as_list(kwargs, 'link_whole') - for linktarget in lwhole: - self.link_whole(linktarget) - - for lang in all_languages: - lang_args = extract_as_list(kwargs, f'{lang}_args') - self.add_compiler_args(lang, lang_args) + self.original_kwargs = kwargs self.add_pch('c', extract_as_list(kwargs, 'c_pch')) self.add_pch('cpp', extract_as_list(kwargs, 'cpp_pch')) - if not isinstance(self, Executable) or 'export_dynamic' in kwargs: + if not isinstance(self, Executable) or kwargs.get('export_dynamic', False): self.vala_header = kwargs.get('vala_header', self.name + '.h') self.vala_vapi = kwargs.get('vala_vapi', self.name + '.vapi') self.vala_gir = kwargs.get('vala_gir', None) - dfeatures = defaultdict(list) - dfeature_unittest = kwargs.get('d_unittest', False) - if dfeature_unittest: - dfeatures['unittest'] = dfeature_unittest - dfeature_versions = kwargs.get('d_module_versions', []) - if dfeature_versions: - dfeatures['versions'] = dfeature_versions - dfeature_debug = kwargs.get('d_debug', []) - if dfeature_debug: - dfeatures['debug'] = dfeature_debug - if 'd_import_dirs' in kwargs: - dfeature_import_dirs = extract_as_list(kwargs, 'd_import_dirs') - for d in dfeature_import_dirs: - if not isinstance(d, IncludeDirs): - raise InvalidArguments('Arguments to d_import_dirs must be include_directories.') - dfeatures['import_dirs'] = dfeature_import_dirs - if dfeatures: - self.d_features = dfeatures - self.link_args = extract_as_list(kwargs, 'link_args') for i in self.link_args: if not isinstance(i, str): @@ -1122,32 +1159,21 @@ class BuildTarget(Target): (str, bool)) self.install_mode = kwargs.get('install_mode', None) self.install_tag = stringlistify(kwargs.get('install_tag', [None])) - main_class = kwargs.get('main_class', '') - if not isinstance(main_class, str): - raise InvalidArguments('Main class must be a string') - self.main_class = main_class - if isinstance(self, Executable): - # This kwarg is deprecated. The value of "none" means that the kwarg - # was not specified and win_subsystem should be used instead. - self.gui_app = None - if 'gui_app' in kwargs: - if 'win_subsystem' in kwargs: - raise InvalidArguments('Can specify only gui_app or win_subsystem for a target, not both.') - self.gui_app = kwargs['gui_app'] - if not isinstance(self.gui_app, bool): - raise InvalidArguments('Argument gui_app must be boolean.') - self.win_subsystem = self.validate_win_subsystem(kwargs.get('win_subsystem', 'console')) - elif 'gui_app' in kwargs: - raise InvalidArguments('Argument gui_app can only be used on executables.') - elif 'win_subsystem' in kwargs: - raise InvalidArguments('Argument win_subsystem can only be used on executables.') + if not isinstance(self, Executable): + # build_target will always populate these as `None`, which is fine + if kwargs.get('gui_app') is not None: + raise InvalidArguments('Argument gui_app can only be used on executables.') + if kwargs.get('win_subsystem') is not None: + raise InvalidArguments('Argument win_subsystem can only be used on executables.') extra_files = extract_as_list(kwargs, 'extra_files') for i in extra_files: assert isinstance(i, File) + if i in self.extra_files: + continue trial = os.path.join(self.environment.get_source_dir(), i.subdir, i.fname) if not os.path.isfile(trial): raise InvalidArguments(f'Tried to add non-existing extra file {i}.') - self.extra_files = extra_files + self.extra_files.append(i) self.install_rpath: str = kwargs.get('install_rpath', '') if not isinstance(self.install_rpath, str): raise InvalidArguments('Install_rpath is not a string.') @@ -1162,7 +1188,7 @@ class BuildTarget(Target): if not os.path.isfile(trial): raise InvalidArguments(f'Tried to add non-existing resource {r}.') self.resources = resources - if 'name_prefix' in kwargs: + if kwargs.get('name_prefix') is not None: name_prefix = kwargs['name_prefix'] if isinstance(name_prefix, list): if name_prefix: @@ -1172,7 +1198,7 @@ class BuildTarget(Target): raise InvalidArguments('name_prefix must be a string.') self.prefix = name_prefix self.name_prefix_set = True - if 'name_suffix' in kwargs: + if kwargs.get('name_suffix') is not None: name_suffix = kwargs['name_suffix'] if isinstance(name_suffix, list): if name_suffix: @@ -1212,13 +1238,14 @@ class BuildTarget(Target): if self.gnu_symbol_visibility not in permitted: raise InvalidArguments('GNU symbol visibility arg {} not one of: {}'.format(self.gnu_symbol_visibility, ', '.join(permitted))) - def validate_win_subsystem(self, value: str) -> str: - value = value.lower() - if re.fullmatch(r'(boot_application|console|efi_application|efi_boot_service_driver|efi_rom|efi_runtime_driver|native|posix|windows)(,\d+(\.\d+)?)?', value) is None: - raise InvalidArguments(f'Invalid value for win_subsystem: {value}.') - return value + rust_dependency_map = kwargs.get('rust_dependency_map', {}) + if not isinstance(rust_dependency_map, dict): + raise InvalidArguments(f'Invalid rust_dependency_map "{rust_dependency_map}": must be a dictionary.') + if any(not isinstance(v, str) for v in rust_dependency_map.values()): + raise InvalidArguments(f'Invalid rust_dependency_map "{rust_dependency_map}": must be a dictionary with string values.') + self.rust_dependency_map = rust_dependency_map - def _extract_pic_pie(self, kwargs, arg: str, option: str): + def _extract_pic_pie(self, kwargs: T.Dict[str, T.Any], arg: str, option: str) -> bool: # Check if we have -fPIC, -fpic, -fPIE, or -fpie in cflags all_flags = self.extra_args['c'] + self.extra_args['cpp'] if '-f' + arg.lower() in all_flags or '-f' + arg.upper() in all_flags: @@ -1226,8 +1253,8 @@ class BuildTarget(Target): return True k = OptionKey(option) - if arg in kwargs: - val = kwargs[arg] + if kwargs.get(arg) is not None: + val = T.cast('bool', kwargs[arg]) elif k in self.environment.coredata.options: val = self.environment.coredata.options[k].value else: @@ -1240,23 +1267,52 @@ class BuildTarget(Target): def get_filename(self) -> str: return self.filename + def get_debug_filename(self) -> T.Optional[str]: + """ + The name of debuginfo file that will be created by the compiler + + Returns None if the build won't create any debuginfo file + """ + return self.debug_filename + def get_outputs(self) -> T.List[str]: return self.outputs - def get_extra_args(self, language): - return self.extra_args.get(language, []) + def get_extra_args(self, language: str) -> T.List[str]: + return self.extra_args[language] - def get_dependencies(self, exclude=None): - transitive_deps = [] - if exclude is None: - exclude = [] + @lru_cache(maxsize=None) + def get_dependencies(self) -> OrderedSet[BuildTargetTypes]: + # Get all targets needed for linking. This includes all link_with and + # link_whole targets, and also all dependencies of static libraries + # recursively. The algorithm here is closely related to what we do in + # get_internal_static_libraries(): Installed static libraries include + # objects from all their dependencies already. + result: OrderedSet[BuildTargetTypes] = OrderedSet() for t in itertools.chain(self.link_targets, self.link_whole_targets): - if t in transitive_deps or t in exclude: + if t not in result: + result.add(t) + if isinstance(t, StaticLibrary): + t.get_dependencies_recurse(result) + return result + + def get_dependencies_recurse(self, result: OrderedSet[BuildTargetTypes], include_internals: bool = True) -> None: + # self is always a static library because we don't need to pull dependencies + # of shared libraries. If self is installed (not internal) it already + # include objects extracted from all its internal dependencies so we can + # skip them. + include_internals = include_internals and self.is_internal() + for t in self.link_targets: + if t in result: continue - transitive_deps.append(t) + if t.rust_crate_type == 'proc-macro': + continue + if include_internals or not t.is_internal(): + result.add(t) if isinstance(t, StaticLibrary): - transitive_deps += t.get_dependencies(transitive_deps + exclude) - return transitive_deps + t.get_dependencies_recurse(result, include_internals) + for t in self.link_whole_targets: + t.get_dependencies_recurse(result, include_internals) def get_source_subdir(self): return self.subdir @@ -1271,7 +1327,7 @@ class BuildTarget(Target): return self.generated def should_install(self) -> bool: - return self.need_install + return self.install def has_pch(self) -> bool: return bool(self.pch) @@ -1291,18 +1347,18 @@ class BuildTarget(Target): if isinstance(dep, dependencies.InternalDependency): # Those parts that are internal. self.process_sourcelist(dep.sources) + self.extra_files.extend(f for f in dep.extra_files if f not in self.extra_files) self.add_include_dirs(dep.include_directories, dep.get_include_type()) - for l in dep.libraries: - self.link(l) - for l in dep.whole_libraries: - self.link_whole(l) + self.objects.extend(dep.objects) + self.link_targets.extend(dep.libraries) + self.link_whole_targets.extend(dep.whole_libraries) if dep.get_compile_args() or dep.get_link_args(): # Those parts that are external. extpart = dependencies.InternalDependency('undefined', [], dep.get_compile_args(), dep.get_link_args(), - [], [], [], [], {}, [], []) + [], [], [], [], [], {}, [], [], []) self.external_deps.append(extpart) # Deps of deps. self.add_deps(dep.ext_deps) @@ -1312,8 +1368,8 @@ class BuildTarget(Target): self.process_sourcelist(dep.get_sources()) self.add_deps(dep.ext_deps) elif isinstance(dep, BuildTarget): - raise InvalidArguments('''Tried to use a build target as a dependency. -You probably should put it in link_with instead.''') + raise InvalidArguments(f'Tried to use a build target {dep.name} as a dependency of target {self.name}.\n' + 'You probably should put it in link_with instead.') else: # This is a bit of a hack. We do not want Build to know anything # about the interpreter so we can't import it and use isinstance. @@ -1344,89 +1400,117 @@ You probably should put it in link_with instead.''') def is_internal(self) -> bool: return False - def link(self, target): - for t in listify(target): - if isinstance(self, StaticLibrary) and self.need_install: - if isinstance(t, (CustomTarget, CustomTargetIndex)): - if not t.should_install(): - mlog.warning(f'Try to link an installed static library target {self.name} with a' - 'custom target that is not installed, this might cause problems' - 'when you try to use this static library') - elif t.is_internal() and not t.uses_rust(): - # When we're a static library and we link_with to an - # internal/convenience library, promote to link_whole. - # - # There are cases we cannot do this, however. In Rust, for - # example, this can't be done with Rust ABI libraries, though - # it could be done with C ABI libraries, though there are - # several meson issues that need to be fixed: - # https://github.com/mesonbuild/meson/issues/10722 - # https://github.com/mesonbuild/meson/issues/10723 - # https://github.com/mesonbuild/meson/issues/10724 - return self.link_whole(t) + def link(self, targets: T.List[BuildTargetTypes]) -> None: + for t in targets: if not isinstance(t, (Target, CustomTargetIndex)): + if isinstance(t, dependencies.ExternalLibrary): + raise MesonException(textwrap.dedent('''\ + An external library was used in link_with keyword argument, which + is reserved for libraries built as part of this project. External + libraries must be passed using the dependencies keyword argument + instead, because they are conceptually "external dependencies", + just like those detected with the dependency() function. + ''')) raise InvalidArguments(f'{t!r} is not a target.') if not t.is_linkable_target(): raise InvalidArguments(f"Link target '{t!s}' is not linkable.") + if isinstance(self, StaticLibrary) and self.install and t.is_internal(): + # When we're a static library and we link_with to an + # internal/convenience library, promote to link_whole. + self.link_whole([t], promoted=True) + continue if isinstance(self, SharedLibrary) and isinstance(t, StaticLibrary) and not t.pic: msg = f"Can't link non-PIC static library {t.name!r} into shared library {self.name!r}. " msg += "Use the 'pic' option to static_library to build with PIC." raise InvalidArguments(msg) - if self.for_machine is not t.for_machine: - msg = f'Tried to mix libraries for machines {self.for_machine} and {t.for_machine} in target {self.name!r}' - if self.environment.is_cross_build(): - raise InvalidArguments(msg + ' This is not possible in a cross build.') - else: - mlog.warning(msg + ' This will fail in cross build.') + self.check_can_link_together(t) self.link_targets.append(t) - def link_whole(self, target): - for t in listify(target): + def link_whole(self, targets: T.List[BuildTargetTypes], promoted: bool = False) -> None: + for t in targets: if isinstance(t, (CustomTarget, CustomTargetIndex)): if not t.is_linkable_target(): raise InvalidArguments(f'Custom target {t!r} is not linkable.') if t.links_dynamically(): raise InvalidArguments('Can only link_whole custom targets that are static archives.') - if isinstance(self, StaticLibrary): - # FIXME: We could extract the .a archive to get object files - raise InvalidArguments('Cannot link_whole a custom target into a static library') elif not isinstance(t, StaticLibrary): raise InvalidArguments(f'{t!r} is not a static library.') elif isinstance(self, SharedLibrary) and not t.pic: msg = f"Can't link non-PIC static library {t.name!r} into shared library {self.name!r}. " msg += "Use the 'pic' option to static_library to build with PIC." raise InvalidArguments(msg) - if self.for_machine is not t.for_machine: - msg = f'Tried to mix libraries for machines {self.for_machine} and {t.for_machine} in target {self.name!r}' - if self.environment.is_cross_build(): - raise InvalidArguments(msg + ' This is not possible in a cross build.') - else: - mlog.warning(msg + ' This will fail in cross build.') + self.check_can_link_together(t) if isinstance(self, StaticLibrary): # When we're a static library and we link_whole: to another static # library, we need to add that target's objects to ourselves. - self.objects += t.extract_all_objects_recurse() + self._bundle_static_library(t, promoted) + # If we install this static library we also need to include objects + # from all uninstalled static libraries it depends on. + if self.install: + for lib in t.get_internal_static_libraries(): + self._bundle_static_library(lib, True) self.link_whole_targets.append(t) - def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]: - objs = [self.extract_all_objects()] + @lru_cache(maxsize=None) + def get_internal_static_libraries(self) -> OrderedSet[BuildTargetTypes]: + result: OrderedSet[BuildTargetTypes] = OrderedSet() + self.get_internal_static_libraries_recurse(result) + return result + + def get_internal_static_libraries_recurse(self, result: OrderedSet[BuildTargetTypes]) -> None: for t in self.link_targets: + if t.is_internal() and t not in result: + result.add(t) + t.get_internal_static_libraries_recurse(result) + for t in self.link_whole_targets: if t.is_internal(): - objs += t.extract_all_objects_recurse() - return objs + t.get_internal_static_libraries_recurse(result) + + def _bundle_static_library(self, t: T.Union[BuildTargetTypes], promoted: bool = False) -> None: + if self.uses_rust(): + # Rustc can bundle static libraries, no need to extract objects. + self.link_whole_targets.append(t) + elif isinstance(t, (CustomTarget, CustomTargetIndex)) or t.uses_rust(): + # To extract objects from a custom target we would have to extract + # the archive, WIP implementation can be found in + # https://github.com/mesonbuild/meson/pull/9218. + # For Rust C ABI we could in theory have access to objects, but there + # are several meson issues that need to be fixed: + # https://github.com/mesonbuild/meson/issues/10722 + # https://github.com/mesonbuild/meson/issues/10723 + # https://github.com/mesonbuild/meson/issues/10724 + m = (f'Cannot link_whole a custom or Rust target {t.name!r} into a static library {self.name!r}. ' + 'Instead, pass individual object files with the "objects:" keyword argument if possible.') + if promoted: + m += (f' Meson had to promote link to link_whole because {self.name!r} is installed but not {t.name!r},' + f' and thus has to include objects from {t.name!r} to be usable.') + raise InvalidArguments(m) + else: + self.objects.append(t.extract_all_objects()) + + def check_can_link_together(self, t: BuildTargetTypes) -> None: + links_with_rust_abi = isinstance(t, BuildTarget) and t.uses_rust_abi() + if not self.uses_rust() and links_with_rust_abi: + raise InvalidArguments(f'Try to link Rust ABI library {t.name!r} with a non-Rust target {self.name!r}') + if self.for_machine is not t.for_machine and (not links_with_rust_abi or t.rust_crate_type != 'proc-macro'): + msg = f'Tried to mix libraries for machines {self.for_machine} and {t.for_machine} in target {self.name!r}' + if self.environment.is_cross_build(): + raise InvalidArguments(msg + ' This is not possible in a cross build.') + else: + mlog.warning(msg + ' This will fail in cross build.') def add_pch(self, language: str, pchlist: T.List[str]) -> None: if not pchlist: return elif len(pchlist) == 1: - if not environment.is_header(pchlist[0]): + if not is_header(pchlist[0]): raise InvalidArguments(f'PCH argument {pchlist[0]} is not a header.') elif len(pchlist) == 2: - if environment.is_header(pchlist[0]): - if not environment.is_source(pchlist[1]): + if is_header(pchlist[0]): + if not is_source(pchlist[1]): raise InvalidArguments('PCH definition must contain one header and at most one source.') - elif environment.is_source(pchlist[0]): - if not environment.is_header(pchlist[1]): + elif is_source(pchlist[0]): + if not is_header(pchlist[1]): raise InvalidArguments('PCH definition must contain one header and at most one source.') pchlist = [pchlist[1], pchlist[0]] else: @@ -1459,16 +1543,6 @@ You probably should put it in link_with instead.''') ids = [IncludeDirs(x.get_curdir(), x.get_incdirs(), is_system, x.get_extra_build_dirs()) for x in ids] self.include_dirs += ids - def add_compiler_args(self, language: str, args: T.List['FileOrString']) -> None: - args = listify(args) - for a in args: - if not isinstance(a, (str, File)): - raise InvalidArguments('A non-string passed to compiler args.') - if language in self.extra_args: - self.extra_args[language] += args - else: - self.extra_args[language] = args - def get_aliases(self) -> T.List[T.Tuple[str, str, str]]: return [] @@ -1481,7 +1555,7 @@ You probably should put it in link_with instead.''') See: https://github.com/mesonbuild/meson/issues/1653 ''' - langs = [] # type: T.List[str] + langs: T.List[str] = [] # Check if any of the external libraries were written in this language for dep in self.external_deps: @@ -1539,14 +1613,6 @@ You probably should put it in link_with instead.''') # Languages used by dependencies dep_langs = self.get_langs_used_by_deps() - # This set contains all the languages a linker can link natively - # without extra flags. For instance, nvcc (cuda) can link C++ - # without injecting -lc++/-lstdc++, see - # https://github.com/mesonbuild/meson/issues/10570 - MASK_LANGS = frozenset([ - # (language, linker) - ('cpp', 'cuda'), - ]) # Pick a compiler based on the language priority-order for l in clink_langs: if l in self.compilers or l in dep_langs: @@ -1557,10 +1623,7 @@ You probably should put it in link_with instead.''') f'Could not get a dynamic linker for build target {self.name!r}. ' f'Requires a linker for language "{l}", but that is not ' 'a project language.') - stdlib_args: T.List[str] = [] - for dl in itertools.chain(self.compilers, dep_langs): - if dl != linker.language and (dl, linker.language) not in MASK_LANGS: - stdlib_args += all_compilers[dl].language_stdlib_only_link_flags(self.environment) + stdlib_args: T.List[str] = self.get_used_stdlib_args(linker.language) # Type of var 'linker' is Compiler. # Pretty hard to fix because the return value is passed everywhere return linker, stdlib_args @@ -1576,9 +1639,24 @@ You probably should put it in link_with instead.''') raise AssertionError(f'Could not get a dynamic linker for build target {self.name!r}') + def get_used_stdlib_args(self, link_language: str) -> T.List[str]: + all_compilers = self.environment.coredata.compilers[self.for_machine] + all_langs = set(self.compilers).union(self.get_langs_used_by_deps()) + stdlib_args: T.List[str] = [] + for dl in all_langs: + if dl != link_language and (dl, link_language) not in self._MASK_LANGS: + # We need to use all_compilers here because + # get_langs_used_by_deps could return a language from a + # subproject + stdlib_args.extend(all_compilers[dl].language_stdlib_only_link_flags(self.environment)) + return stdlib_args + def uses_rust(self) -> bool: return 'rust' in self.compilers + def uses_rust_abi(self) -> bool: + return self.uses_rust() and self.rust_crate_type in {'dylib', 'rlib', 'proc-macro'} + def uses_fortran(self) -> bool: return 'fortran' in self.compilers @@ -1629,9 +1707,65 @@ You probably should put it in link_with instead.''') '\n ' f'If shared_module() was used for {link_target.name} because it has references to undefined symbols,' '\n ' - 'use shared_libary() with `override_options: [\'b_lundef=false\']` instead.') + 'use shared_library() with `override_options: [\'b_lundef=false\']` instead.') link_target.force_soname = True + def process_vs_module_defs_kw(self, kwargs: T.Dict[str, T.Any]) -> None: + if kwargs.get('vs_module_defs') is None: + return + + path: T.Union[str, File, CustomTarget, CustomTargetIndex] = kwargs['vs_module_defs'] + if isinstance(path, str): + if os.path.isabs(path): + self.vs_module_defs = File.from_absolute_file(path) + else: + self.vs_module_defs = File.from_source_file(self.environment.source_dir, self.subdir, path) + elif isinstance(path, File): + # When passing a generated file. + self.vs_module_defs = path + elif isinstance(path, (CustomTarget, CustomTargetIndex)): + # When passing output of a Custom Target + self.vs_module_defs = File.from_built_file(path.get_subdir(), path.get_filename()) + else: + raise InvalidArguments( + 'vs_module_defs must be either a string, ' + 'a file object, a Custom Target, or a Custom Target Index') + self.process_link_depends(path) + +class FileInTargetPrivateDir: + """Represents a file with the path '/path/to/build/target_private_dir/fname'. + target_private_dir is the return value of get_target_private_dir which is e.g. 'subdir/target.p'. + """ + + def __init__(self, fname: str): + self.fname = fname + + def __str__(self) -> str: + return self.fname + +class FileMaybeInTargetPrivateDir: + """Union between 'File' and 'FileInTargetPrivateDir'""" + + def __init__(self, inner: T.Union[File, FileInTargetPrivateDir]): + self.inner = inner + + @property + def fname(self) -> str: + return self.inner.fname + + def rel_to_builddir(self, build_to_src: str, target_private_dir: str) -> str: + if isinstance(self.inner, FileInTargetPrivateDir): + return os.path.join(target_private_dir, self.inner.fname) + return self.inner.rel_to_builddir(build_to_src) + + def absolute_path(self, srcdir: str, builddir: str) -> str: + if isinstance(self.inner, FileInTargetPrivateDir): + raise RuntimeError('Unreachable code') + return self.inner.absolute_path(srcdir, builddir) + + def __str__(self) -> str: + return self.fname + class Generator(HoldableObject): def __init__(self, exe: T.Union['Executable', programs.ExternalProgram], arguments: T.List[str], @@ -1683,18 +1817,28 @@ class Generator(HoldableObject): def process_files(self, files: T.Iterable[T.Union[str, File, 'CustomTarget', 'CustomTargetIndex', 'GeneratedList']], state: T.Union['Interpreter', 'ModuleState'], preserve_path_from: T.Optional[str] = None, - extra_args: T.Optional[T.List[str]] = None) -> 'GeneratedList': - output = GeneratedList(self, state.subdir, preserve_path_from, extra_args=extra_args if extra_args is not None else []) + extra_args: T.Optional[T.List[str]] = None, + env: T.Optional[EnvironmentVariables] = None) -> 'GeneratedList': + output = GeneratedList( + self, + state.subdir, + preserve_path_from, + extra_args=extra_args if extra_args is not None else [], + env=env if env is not None else EnvironmentVariables()) for e in files: if isinstance(e, CustomTarget): output.depends.add(e) if isinstance(e, CustomTargetIndex): output.depends.add(e.target) - - if isinstance(e, (CustomTarget, CustomTargetIndex, GeneratedList)): + if isinstance(e, (CustomTarget, CustomTargetIndex)): output.depends.add(e) fs = [File.from_built_file(state.subdir, f) for f in e.get_outputs()] + elif isinstance(e, GeneratedList): + if preserve_path_from: + raise InvalidArguments("generator.process: 'preserve_path_from' is not allowed if one input is a 'generated_list'.") + output.depends.add(e) + fs = [FileInTargetPrivateDir(f) for f in e.get_outputs()] elif isinstance(e, str): fs = [File.from_source_file(state.environment.source_dir, state.subdir, e)] else: @@ -1705,6 +1849,7 @@ class Generator(HoldableObject): abs_f = f.absolute_path(state.environment.source_dir, state.environment.build_dir) if not self.is_parent_path(preserve_path_from, abs_f): raise InvalidArguments('generator.process: When using preserve_path_from, all input files must be in a subdirectory of the given dir.') + f = FileMaybeInTargetPrivateDir(f) output.add_file(f, state) return output @@ -1718,19 +1863,23 @@ class GeneratedList(HoldableObject): subdir: str preserve_path_from: T.Optional[str] extra_args: T.List[str] + env: T.Optional[EnvironmentVariables] def __post_init__(self) -> None: self.name = self.generator.exe self.depends: T.Set[GeneratedTypes] = set() - self.infilelist: T.List['File'] = [] + self.infilelist: T.List[FileMaybeInTargetPrivateDir] = [] self.outfilelist: T.List[str] = [] - self.outmap: T.Dict[File, T.List[str]] = {} + self.outmap: T.Dict[FileMaybeInTargetPrivateDir, T.List[str]] = {} self.extra_depends = [] # XXX: Doesn't seem to be used? self.depend_files: T.List[File] = [] if self.extra_args is None: self.extra_args: T.List[str] = [] + if self.env is None: + self.env: EnvironmentVariables = EnvironmentVariables() + if isinstance(self.generator.exe, programs.ExternalProgram): if not self.generator.exe.found(): raise InvalidArguments('Tried to use not-found external program as generator') @@ -1740,7 +1889,7 @@ class GeneratedList(HoldableObject): # know the absolute path of self.depend_files.append(File.from_absolute_file(path)) - def add_preserved_path_segment(self, infile: File, outfiles: T.List[str], state: T.Union['Interpreter', 'ModuleState']) -> T.List[str]: + def add_preserved_path_segment(self, infile: FileMaybeInTargetPrivateDir, outfiles: T.List[str], state: T.Union['Interpreter', 'ModuleState']) -> T.List[str]: result: T.List[str] = [] in_abs = infile.absolute_path(state.environment.source_dir, state.environment.build_dir) assert os.path.isabs(self.preserve_path_from) @@ -1750,7 +1899,7 @@ class GeneratedList(HoldableObject): result.append(os.path.join(path_segment, of)) return result - def add_file(self, newfile: File, state: T.Union['Interpreter', 'ModuleState']) -> None: + def add_file(self, newfile: FileMaybeInTargetPrivateDir, state: T.Union['Interpreter', 'ModuleState']) -> None: self.infilelist.append(newfile) outfiles = self.generator.get_base_outnames(newfile.fname) if self.preserve_path_from: @@ -1758,13 +1907,13 @@ class GeneratedList(HoldableObject): self.outfilelist += outfiles self.outmap[newfile] = outfiles - def get_inputs(self) -> T.List['File']: + def get_inputs(self) -> T.List[FileMaybeInTargetPrivateDir]: return self.infilelist def get_outputs(self) -> T.List[str]: return self.outfilelist - def get_outputs_for(self, filename: 'File') -> T.List[str]: + def get_outputs_for(self, filename: FileMaybeInTargetPrivateDir) -> T.List[str]: return self.outmap[filename] def get_generator(self) -> 'Generator': @@ -1782,15 +1931,24 @@ class Executable(BuildTarget): typename = 'executable' - def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, - sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'], - objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], - kwargs): + def __init__( + self, + name: str, + subdir: str, + subproject: SubProject, + for_machine: MachineChoice, + sources: T.List['SourceOutputs'], + structured_sources: T.Optional[StructuredSources], + objects: T.List[ObjectTypes], + environment: environment.Environment, + compilers: T.Dict[str, 'Compiler'], + kwargs): key = OptionKey('b_pie') if 'pie' not in kwargs and key in environment.coredata.options: kwargs['pie'] = environment.coredata.options[key].value super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects, environment, compilers, kwargs) + self.win_subsystem = kwargs.get('win_subsystem') or 'console' # Check for export_dynamic self.export_dynamic = kwargs.get('export_dynamic', False) if not isinstance(self.export_dynamic, bool): @@ -1798,15 +1956,14 @@ class Executable(BuildTarget): self.implib = kwargs.get('implib') if not isinstance(self.implib, (bool, str, type(None))): raise InvalidArguments('"export_dynamic" keyword argument must be a boolean or string') - if self.implib: - self.export_dynamic = True - if self.export_dynamic and self.implib is False: - raise InvalidArguments('"implib" keyword argument must not be false for if "export_dynamic" is true') # Only linkwithable if using export_dynamic self.is_linkwithable = self.export_dynamic # Remember that this exe was returned by `find_program()` through an override self.was_returned_by_find_program = False + self.vs_module_defs: T.Optional[File] = None + self.process_vs_module_defs_kw(kwargs) + def post_init(self) -> None: super().post_init() machine = self.environment.machines[self.for_machine] @@ -1831,19 +1988,18 @@ class Executable(BuildTarget): elif ('c' in self.compilers and self.compilers['c'].get_id() in {'ti', 'c2000'} or 'cpp' in self.compilers and self.compilers['cpp'].get_id() in {'ti', 'c2000'}): self.suffix = 'out' + elif ('c' in self.compilers and self.compilers['c'].get_id() in {'mwccarm', 'mwcceppc'} or + 'cpp' in self.compilers and self.compilers['cpp'].get_id() in {'mwccarm', 'mwcceppc'}): + self.suffix = 'nef' else: self.suffix = machine.get_exe_suffix() self.filename = self.name if self.suffix: self.filename += '.' + self.suffix - self.outputs = [self.filename] + self.outputs[0] = self.filename # The import library this target will generate self.import_filename = None - # The import library that Visual Studio would generate (and accept) - self.vs_import_filename = None - # The import library that GCC would generate (and prefer) - self.gcc_import_filename = None # The debugging information file this target will generate self.debug_filename = None @@ -1853,17 +2009,33 @@ class Executable(BuildTarget): if isinstance(self.implib, str): implib_basename = self.implib if machine.is_windows() or machine.is_cygwin(): - self.vs_import_filename = f'{implib_basename}.lib' - self.gcc_import_filename = f'lib{implib_basename}.a' if self.get_using_msvc(): - self.import_filename = self.vs_import_filename + self.import_filename = f'{implib_basename}.lib' else: - self.import_filename = self.gcc_import_filename + self.import_filename = f'lib{implib_basename}.a' + + create_debug_file = ( + machine.is_windows() + and ('cs' in self.compilers or self.uses_rust() or self.get_using_msvc()) + # .pdb file is created only when debug symbols are enabled + and self.environment.coredata.get_option(OptionKey("debug")) + ) + if create_debug_file: + # If the target is has a standard exe extension (i.e. 'foo.exe'), + # then the pdb name simply becomes 'foo.pdb'. If the extension is + # something exotic, then include that in the name for uniqueness + # reasons (e.g. 'foo_com.pdb'). + name = self.name + if getattr(self, 'suffix', 'exe') != 'exe': + name += '_' + self.suffix + self.debug_filename = name + '.pdb' + + def process_kwargs(self, kwargs): + super().process_kwargs(kwargs) - if machine.is_windows() and ('cs' in self.compilers or - self.uses_rust() or - self.get_using_msvc()): - self.debug_filename = self.name + '.pdb' + self.rust_crate_type = kwargs.get('rust_crate_type') or 'bin' + if self.rust_crate_type != 'bin': + raise InvalidArguments('Invalid rust_crate_type: must be "bin" for executables.') def get_default_install_dir(self) -> T.Tuple[str, str]: return self.environment.get_bindir(), '{bindir}' @@ -1883,11 +2055,6 @@ class Executable(BuildTarget): """ return self.import_filename - def get_import_filenameslist(self): - if self.import_filename: - return [self.vs_import_filename, self.gcc_import_filename] - return [] - def get_debug_filename(self) -> T.Optional[str]: """ The name of debuginfo file that will be created by the compiler @@ -1906,18 +2073,33 @@ class Executable(BuildTarget): """ return self.outputs + def get_path(self) -> str: + """Provides compatibility with ExternalProgram.""" + return os.path.join(self.subdir, self.filename) + + def found(self) -> bool: + """Provides compatibility with ExternalProgram.""" + return True + + class StaticLibrary(BuildTarget): known_kwargs = known_stlib_kwargs typename = 'static library' - def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, - sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'], - objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], - kwargs): - self.prelink = kwargs.get('prelink', False) - if not isinstance(self.prelink, bool): - raise InvalidArguments('Prelink keyword argument must be a boolean.') + def __init__( + self, + name: str, + subdir: str, + subproject: SubProject, + for_machine: MachineChoice, + sources: T.List['SourceOutputs'], + structured_sources: T.Optional[StructuredSources], + objects: T.List[ObjectTypes], + environment: environment.Environment, + compilers: T.Dict[str, 'Compiler'], + kwargs): + self.prelink = T.cast('bool', kwargs.get('prelink', False)) super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects, environment, compilers, kwargs) @@ -1925,14 +2107,20 @@ class StaticLibrary(BuildTarget): super().post_init() if 'cs' in self.compilers: raise InvalidArguments('Static libraries not supported for C#.') - if 'rust' in self.compilers: - # If no crate type is specified, or it's the generic lib type, use rlib - if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'lib': - mlog.debug('Defaulting Rust static library target crate type to rlib') - self.rust_crate_type = 'rlib' - # Don't let configuration proceed with a non-static crate type - elif self.rust_crate_type not in ['rlib', 'staticlib']: - raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for static libraries; must be "rlib" or "staticlib"') + if self.uses_rust(): + # See https://github.com/rust-lang/rust/issues/110460 + if self.rust_crate_type == 'rlib' and any(c in self.name for c in ['-', ' ', '.']): + raise InvalidArguments(f'Rust crate {self.name} type {self.rust_crate_type} does not allow spaces, ' + 'periods or dashes in the library name due to a limitation of rustc. ' + 'Replace them with underscores, for example') + if self.rust_crate_type == 'staticlib': + # FIXME: In the case of no-std we should not add those libraries, + # but we have no way to know currently. + rustc = self.compilers['rust'] + d = dependencies.InternalDependency('undefined', [], [], + rustc.native_static_libs, + [], [], [], [], [], {}, [], [], []) + self.external_deps.append(d) # By default a static library is named libfoo.a even on Windows because # MSVC does not have a consistent convention for what static libraries # are called. The MSVC CRT uses libfoo.lib syntax but nothing else uses @@ -1940,11 +2128,14 @@ class StaticLibrary(BuildTarget): # libfoo.a. However, we cannot use foo.lib because that's the same as # the import library. Using libfoo.a is ok because people using MSVC # always pass the library filename while linking anyway. + # + # See our FAQ for more detailed rationale: + # https://mesonbuild.com/FAQ.html#why-does-building-my-project-with-msvc-output-static-libraries-called-libfooa if not hasattr(self, 'prefix'): self.prefix = 'lib' if not hasattr(self, 'suffix'): - if 'rust' in self.compilers: - if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'rlib': + if self.uses_rust(): + if self.rust_crate_type == 'rlib': # default Rust static library suffix self.suffix = 'rlib' elif self.rust_crate_type == 'staticlib': @@ -1952,7 +2143,7 @@ class StaticLibrary(BuildTarget): else: self.suffix = 'a' self.filename = self.prefix + self.name + '.' + self.suffix - self.outputs = [self.filename] + self.outputs[0] = self.filename def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]: return {} @@ -1965,39 +2156,54 @@ class StaticLibrary(BuildTarget): def process_kwargs(self, kwargs): super().process_kwargs(kwargs) - if 'rust_crate_type' in kwargs: - rust_crate_type = kwargs['rust_crate_type'] - if isinstance(rust_crate_type, str): + + rust_abi = kwargs.get('rust_abi') + rust_crate_type = kwargs.get('rust_crate_type') + if rust_crate_type: + if rust_abi: + raise InvalidArguments('rust_abi and rust_crate_type are mutually exclusive.') + if rust_crate_type == 'lib': + self.rust_crate_type = 'rlib' + elif rust_crate_type in {'rlib', 'staticlib'}: self.rust_crate_type = rust_crate_type else: - raise InvalidArguments(f'Invalid rust_crate_type "{rust_crate_type}": must be a string.') + raise InvalidArguments(f'Crate type {rust_crate_type!r} invalid for static libraries; must be "rlib" or "staticlib"') + else: + self.rust_crate_type = 'staticlib' if rust_abi == 'c' else 'rlib' def is_linkable_target(self): return True def is_internal(self) -> bool: - return not self.need_install + return not self.install class SharedLibrary(BuildTarget): known_kwargs = known_shlib_kwargs typename = 'shared library' - def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, - sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'], - objects, environment: environment.Environment, compilers: T.Dict[str, 'Compiler'], - kwargs): - self.soversion = None - self.ltversion = None + # Used by AIX to decide whether to archive shared library or not. + aix_so_archive = True + + def __init__( + self, + name: str, + subdir: str, + subproject: SubProject, + for_machine: MachineChoice, + sources: T.List['SourceOutputs'], + structured_sources: T.Optional[StructuredSources], + objects: T.List[ObjectTypes], + environment: environment.Environment, + compilers: T.Dict[str, 'Compiler'], + kwargs): + self.soversion: T.Optional[str] = None + self.ltversion: T.Optional[str] = None # Max length 2, first element is compatibility_version, second is current_version - self.darwin_versions = [] + self.darwin_versions: T.Optional[T.Tuple[str, str]] = None self.vs_module_defs = None # The import library this target will generate self.import_filename = None - # The import library that Visual Studio would generate (and accept) - self.vs_import_filename = None - # The import library that GCC would generate (and prefer) - self.gcc_import_filename = None # The debugging information file this target will generate self.debug_filename = None # Use by the pkgconfig module @@ -2007,14 +2213,13 @@ class SharedLibrary(BuildTarget): def post_init(self) -> None: super().post_init() - if 'rust' in self.compilers: - # If no crate type is specified, or it's the generic lib type, use dylib - if not hasattr(self, 'rust_crate_type') or self.rust_crate_type == 'lib': - mlog.debug('Defaulting Rust dynamic library target crate type to "dylib"') - self.rust_crate_type = 'dylib' - # Don't let configuration proceed with a non-dynamic crate type - elif self.rust_crate_type not in ['dylib', 'cdylib', 'proc-macro']: - raise InvalidArguments(f'Crate type "{self.rust_crate_type}" invalid for dynamic libraries; must be "dylib", "cdylib", or "proc-macro"') + if self.uses_rust(): + # See https://github.com/rust-lang/rust/issues/110460 + if self.rust_crate_type != 'cdylib' and any(c in self.name for c in ['-', ' ', '.']): + raise InvalidArguments(f'Rust crate {self.name} type {self.rust_crate_type} does not allow spaces, ' + 'periods or dashes in the library name due to a limitation of rustc. ' + 'Replace them with underscores, for example') + if not hasattr(self, 'prefix'): self.prefix = None if not hasattr(self, 'suffix'): @@ -2047,21 +2252,16 @@ class SharedLibrary(BuildTarget): The template is needed while creating aliases (self.get_aliases), which are needed while generating .so shared libraries for Linux. - Besides this, there's also the import library name, which is only used - on Windows since on that platform the linker uses a separate library - called the "import library" during linking instead of the shared - library (DLL). The toolchain will output an import library in one of - two formats: GCC or Visual Studio. - - When we're building with Visual Studio, the import library that will be - generated by the toolchain is self.vs_import_filename, and with - MinGW/GCC, it's self.gcc_import_filename. self.import_filename will - always contain the import library name this target will generate. + Besides this, there's also the import library name (self.import_filename), + which is only used on Windows since on that platform the linker uses a + separate library called the "import library" during linking instead of + the shared library (DLL). """ prefix = '' suffix = '' create_debug_file = False self.filename_tpl = self.basic_filename_tpl + import_filename_tpl = None # NOTE: manual prefix/suffix override is currently only tested for C/C++ # C# and Mono if 'cs' in self.compilers: @@ -2074,28 +2274,26 @@ class SharedLibrary(BuildTarget): # For all other targets/platforms import_filename stays None elif self.environment.machines[self.for_machine].is_windows(): suffix = 'dll' - self.vs_import_filename = '{}{}.lib'.format(self.prefix if self.prefix is not None else '', self.name) - self.gcc_import_filename = '{}{}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name) if self.uses_rust(): # Shared library is of the form foo.dll prefix = '' # Import library is called foo.dll.lib - self.import_filename = f'{self.name}.dll.lib' - # Debug files(.pdb) is only created with debug buildtype + import_filename_tpl = '{0.prefix}{0.name}.dll.lib' + # .pdb file is only created when debug symbols are enabled create_debug_file = self.environment.coredata.get_option(OptionKey("debug")) elif self.get_using_msvc(): # Shared library is of the form foo.dll prefix = '' # Import library is called foo.lib - self.import_filename = self.vs_import_filename - # Debug files(.pdb) is only created with debug buildtype + import_filename_tpl = '{0.prefix}{0.name}.lib' + # .pdb file is only created when debug symbols are enabled create_debug_file = self.environment.coredata.get_option(OptionKey("debug")) # Assume GCC-compatible naming else: # Shared library is of the form libfoo.dll prefix = 'lib' # Import library is called libfoo.dll.a - self.import_filename = self.gcc_import_filename + import_filename_tpl = '{0.prefix}{0.name}.dll.a' # Shared library has the soversion if it is defined if self.soversion: self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}' @@ -2103,12 +2301,12 @@ class SharedLibrary(BuildTarget): self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' elif self.environment.machines[self.for_machine].is_cygwin(): suffix = 'dll' - self.gcc_import_filename = '{}{}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name) # Shared library is of the form cygfoo.dll # (ld --dll-search-prefix=cyg is the default) prefix = 'cyg' # Import library is called libfoo.dll.a - self.import_filename = self.gcc_import_filename + import_prefix = self.prefix if self.prefix is not None else 'lib' + import_filename_tpl = import_prefix + '{0.name}.dll.a' if self.soversion: self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}' else: @@ -2145,108 +2343,48 @@ class SharedLibrary(BuildTarget): if self.suffix is None: self.suffix = suffix self.filename = self.filename_tpl.format(self) + if import_filename_tpl: + self.import_filename = import_filename_tpl.format(self) # There may have been more outputs added by the time we get here, so # only replace the first entry self.outputs[0] = self.filename if create_debug_file: self.debug_filename = os.path.splitext(self.filename)[0] + '.pdb' - @staticmethod - def _validate_darwin_versions(darwin_versions): - try: - if isinstance(darwin_versions, int): - darwin_versions = str(darwin_versions) - if isinstance(darwin_versions, str): - darwin_versions = 2 * [darwin_versions] - if not isinstance(darwin_versions, list): - raise InvalidArguments('Shared library darwin_versions: must be a string, integer,' - f'or a list, not {darwin_versions!r}') - if len(darwin_versions) > 2: - raise InvalidArguments('Shared library darwin_versions: list must contain 2 or fewer elements') - if len(darwin_versions) == 1: - darwin_versions = 2 * darwin_versions - for i, v in enumerate(darwin_versions[:]): - if isinstance(v, int): - v = str(v) - if not isinstance(v, str): - raise InvalidArguments('Shared library darwin_versions: list elements ' - f'must be strings or integers, not {v!r}') - if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', v): - raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z where ' - 'X, Y, Z are numbers, and Y and Z are optional') - parts = v.split('.') - if len(parts) in {1, 2, 3} and int(parts[0]) > 65535: - raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z ' - 'where X is [0, 65535] and Y, Z are optional') - if len(parts) in {2, 3} and int(parts[1]) > 255: - raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z ' - 'where Y is [0, 255] and Y, Z are optional') - if len(parts) == 3 and int(parts[2]) > 255: - raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z ' - 'where Z is [0, 255] and Y, Z are optional') - darwin_versions[i] = v - except ValueError: - raise InvalidArguments('Shared library darwin_versions: value is invalid') - return darwin_versions - def process_kwargs(self, kwargs): super().process_kwargs(kwargs) if not self.environment.machines[self.for_machine].is_android(): # Shared library version - if 'version' in kwargs: - self.ltversion = kwargs['version'] - if not isinstance(self.ltversion, str): - raise InvalidArguments('Shared library version needs to be a string, not ' + type(self.ltversion).__name__) - if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', self.ltversion): - raise InvalidArguments(f'Invalid Shared library version "{self.ltversion}". Must be of the form X.Y.Z where all three are numbers. Y and Z are optional.') - # Try to extract/deduce the soversion - if 'soversion' in kwargs: - self.soversion = kwargs['soversion'] - if isinstance(self.soversion, int): - self.soversion = str(self.soversion) - if not isinstance(self.soversion, str): - raise InvalidArguments('Shared library soversion is not a string or integer.') - elif self.ltversion: + self.ltversion = T.cast('T.Optional[str]', kwargs.get('version')) + self.soversion = T.cast('T.Optional[str]', kwargs.get('soversion')) + if self.soversion is None and self.ltversion is not None: # library version is defined, get the soversion from that # We replicate what Autotools does here and take the first # number of the version by default. self.soversion = self.ltversion.split('.')[0] # macOS, iOS and tvOS dylib compatibility_version and current_version - if 'darwin_versions' in kwargs: - self.darwin_versions = self._validate_darwin_versions(kwargs['darwin_versions']) - elif self.soversion: + self.darwin_versions = T.cast('T.Optional[T.Tuple[str, str]]', kwargs.get('darwin_versions')) + if self.darwin_versions is None and self.soversion is not None: # If unspecified, pick the soversion - self.darwin_versions = 2 * [self.soversion] + self.darwin_versions = (self.soversion, self.soversion) # Visual Studio module-definitions file - if 'vs_module_defs' in kwargs: - path = kwargs['vs_module_defs'] - if isinstance(path, str): - if os.path.isabs(path): - self.vs_module_defs = File.from_absolute_file(path) - else: - self.vs_module_defs = File.from_source_file(self.environment.source_dir, self.subdir, path) - elif isinstance(path, File): - # When passing a generated file. - self.vs_module_defs = path - elif hasattr(path, 'get_filename'): - # When passing output of a Custom Target - self.vs_module_defs = File.from_built_file(path.subdir, path.get_filename()) - else: - raise InvalidArguments( - 'Shared library vs_module_defs must be either a string, ' - 'a file object or a Custom Target') - self.process_link_depends(path) - - if 'rust_crate_type' in kwargs: - rust_crate_type = kwargs['rust_crate_type'] - if isinstance(rust_crate_type, str): + self.process_vs_module_defs_kw(kwargs) + + rust_abi = kwargs.get('rust_abi') + rust_crate_type = kwargs.get('rust_crate_type') + if rust_crate_type: + if rust_abi: + raise InvalidArguments('rust_abi and rust_crate_type are mutually exclusive.') + if rust_crate_type == 'lib': + self.rust_crate_type = 'dylib' + elif rust_crate_type in {'dylib', 'cdylib', 'proc-macro'}: self.rust_crate_type = rust_crate_type else: - raise InvalidArguments(f'Invalid rust_crate_type "{rust_crate_type}": must be a string.') - if rust_crate_type == 'proc-macro': - FeatureNew.single_use('Rust crate type "proc-macro"', '0.62.0', self.subproject) + raise InvalidArguments(f'Crate type {rust_crate_type!r} invalid for shared libraries; must be "dylib", "cdylib" or "proc-macro"') + else: + self.rust_crate_type = 'cdylib' if rust_abi == 'c' else 'dylib' def get_import_filename(self) -> T.Optional[str]: """ @@ -2264,11 +2402,6 @@ class SharedLibrary(BuildTarget): """ return self.debug_filename - def get_import_filenameslist(self): - if self.import_filename: - return [self.vs_import_filename, self.gcc_import_filename] - return [] - def get_all_link_deps(self): return [self] + self.get_transitive_link_deps() @@ -2317,10 +2450,21 @@ class SharedModule(SharedLibrary): typename = 'shared module' - def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, - sources: T.List[SourceOutputs], structured_sources: T.Optional['StructuredSources'], - objects, environment: environment.Environment, - compilers: T.Dict[str, 'Compiler'], kwargs): + # Used by AIX to not archive shared library for dlopen mechanism + aix_so_archive = False + + def __init__( + self, + name: str, + subdir: str, + subproject: SubProject, + for_machine: MachineChoice, + sources: T.List['SourceOutputs'], + structured_sources: T.Optional[StructuredSources], + objects: T.List[ObjectTypes], + environment: environment.Environment, + compilers: T.Dict[str, 'Compiler'], + kwargs): if 'version' in kwargs: raise MesonException('Shared modules must not specify the version kwarg.') if 'soversion' in kwargs: @@ -2389,7 +2533,26 @@ class CommandBase: raise InvalidArguments(f'Argument {c!r} in "command" is invalid') return final_cmd -class CustomTarget(Target, CommandBase): +class CustomTargetBase: + ''' Base class for CustomTarget and CustomTargetIndex + + This base class can be used to provide a dummy implementation of some + private methods to avoid repeating `isinstance(t, BuildTarget)` when dealing + with custom targets. + ''' + + rust_crate_type = '' + + def get_dependencies_recurse(self, result: OrderedSet[BuildTargetTypes], include_internals: bool = True) -> None: + pass + + def get_internal_static_libraries(self) -> OrderedSet[BuildTargetTypes]: + return OrderedSet() + + def get_internal_static_libraries_recurse(self, result: OrderedSet[BuildTargetTypes]) -> None: + pass + +class CustomTarget(Target, CustomTargetBase, CommandBase): typename = 'custom' @@ -2421,16 +2584,17 @@ class CustomTarget(Target, CommandBase): install_tag: T.Optional[T.List[T.Optional[str]]] = None, absolute_paths: bool = False, backend: T.Optional['Backend'] = None, + description: str = 'Generating {} with a custom command', ): # TODO expose keyword arg to make MachineChoice.HOST configurable - super().__init__(name, subdir, subproject, False, MachineChoice.HOST, environment) + super().__init__(name, subdir, subproject, False, MachineChoice.HOST, environment, + install, build_always_stale) self.sources = list(sources) self.outputs = substitute_values( outputs, get_filenames_templates_dict( get_sources_string_names(sources, backend), [])) self.build_by_default = build_by_default if build_by_default is not None else install - self.build_always_stale = build_always_stale self.capture = capture self.console = console self.depend_files = list(depend_files or []) @@ -2441,11 +2605,11 @@ class CustomTarget(Target, CommandBase): self.env = env or EnvironmentVariables() self.extra_depends = list(extra_depends or []) self.feed = feed - self.install = install self.install_dir = list(install_dir or []) self.install_mode = install_mode self.install_tag = _process_install_tag(install_tag, len(self.outputs)) self.name = name if name else self.outputs[0] + self.description = description # Whether to use absolute paths for all files on the commandline self.absolute_paths = absolute_paths @@ -2529,11 +2693,18 @@ class CustomTarget(Target, CommandBase): raise InvalidArguments('Substitution in depfile for custom_target that does not have an input file.') return self.depfile + def is_linkable_output(self, output: str) -> bool: + if output.endswith(('.a', '.dll', '.lib', '.so', '.dylib')): + return True + # libfoo.so.X soname + if re.search(r'\.so(\.\d+)*$', output): + return True + return False + def is_linkable_target(self) -> bool: if len(self.outputs) != 1: return False - suf = os.path.splitext(self.outputs[0])[-1] - return suf in {'.a', '.dll', '.lib', '.so', '.dylib'} + return self.is_linkable_output(self.outputs[0]) def links_dynamically(self) -> bool: """Whether this target links dynamically or statically @@ -2562,7 +2733,7 @@ class CustomTarget(Target, CommandBase): return False return CustomTargetIndex(self, self.outputs[0]).is_internal() - def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]: + def extract_all_objects(self) -> T.List[T.Union[str, 'ExtractedObjects']]: return self.get_outputs() def type_suffix(self): @@ -2597,22 +2768,33 @@ class CompileTarget(BuildTarget): subdir: str, subproject: str, environment: environment.Environment, - sources: T.List[File], + sources: T.List['SourceOutputs'], output_templ: str, compiler: Compiler, - kwargs): + backend: Backend, + compile_args: T.List[str], + include_directories: T.List[IncludeDirs], + dependencies: T.List[dependencies.Dependency]): compilers = {compiler.get_language(): compiler} + kwargs = { + 'build_by_default': False, + f'{compiler.language}_args': compile_args, + 'include_directories': include_directories, + 'dependencies': dependencies, + } super().__init__(name, subdir, subproject, compiler.for_machine, sources, None, [], environment, compilers, kwargs) self.filename = name self.compiler = compiler self.output_templ = output_templ self.outputs = [] - for f in sources: - plainname = os.path.basename(f.fname) - basename = os.path.splitext(plainname)[0] - self.outputs.append(output_templ.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname)) - self.sources_map = dict(zip(sources, self.outputs)) + self.sources_map: T.Dict[File, str] = {} + for f in self.sources: + self._add_output(f) + for gensrc in self.generated: + for s in gensrc.get_outputs(): + rel_src = backend.get_target_generated_dir(self, gensrc, s) + self._add_output(File.from_built_relative(rel_src)) def type_suffix(self) -> str: return "@compile" @@ -2621,6 +2803,13 @@ class CompileTarget(BuildTarget): def is_unity(self) -> bool: return False + def _add_output(self, f: File) -> None: + plainname = os.path.basename(f.fname) + basename = os.path.splitext(plainname)[0] + o = self.output_templ.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) + self.outputs.append(o) + self.sources_map[f] = o + class RunTarget(Target, CommandBase): @@ -2674,6 +2863,9 @@ class RunTarget(Target, CommandBase): return "@run" class AliasTarget(RunTarget): + + typename = 'alias' + def __init__(self, name: str, dependencies: T.Sequence['Target'], subdir: str, subproject: str, environment: environment.Environment): super().__init__(name, [], dependencies, subdir, subproject, environment) @@ -2703,7 +2895,8 @@ class Jar(BuildTarget): raise InvalidArguments('structured sources are not supported in Java targets.') self.filename = self.name + '.jar' self.outputs = [self.filename] - self.java_args = kwargs.get('java_args', []) + self.java_args = self.extra_args['java'] + self.main_class = kwargs.get('main_class', '') self.java_resources: T.Optional[StructuredSources] = kwargs.get('java_resources', None) def get_main_class(self): @@ -2736,7 +2929,7 @@ class Jar(BuildTarget): return self.environment.get_jar_dir(), '{jardir}' @dataclass(eq=False) -class CustomTargetIndex(HoldableObject): +class CustomTargetIndex(CustomTargetBase, HoldableObject): """A special opaque object returned by indexing a CustomTarget. This object exists in Meson, but acts as a proxy in the backends, making targets depend @@ -2781,8 +2974,7 @@ class CustomTargetIndex(HoldableObject): return self.target.get_link_dep_subdirs() def is_linkable_target(self) -> bool: - suf = os.path.splitext(self.output)[-1] - return suf in {'.a', '.dll', '.lib', '.so', '.dylib'} + return self.target.is_linkable_output(self.output) def links_dynamically(self) -> bool: """Whether this target links dynamically or statically @@ -2804,8 +2996,8 @@ class CustomTargetIndex(HoldableObject): suf = os.path.splitext(self.output)[-1] return suf in {'.a', '.lib'} and not self.should_install() - def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]: - return self.target.extract_all_objects_recurse() + def extract_all_objects(self) -> T.List[T.Union[str, 'ExtractedObjects']]: + return self.target.extract_all_objects() def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]: return self.target.get_custom_install_dir() @@ -2847,6 +3039,7 @@ class Data(HoldableObject): rename: T.List[str] = None install_tag: T.Optional[str] = None data_type: str = None + follow_symlinks: T.Optional[bool] = None def __post_init__(self) -> None: if self.rename is None: @@ -2895,11 +3088,20 @@ def get_sources_string_names(sources, backend): def load(build_dir: str) -> Build: filename = os.path.join(build_dir, 'meson-private', 'build.dat') try: - return pickle_load(filename, 'Build data', Build) + b = pickle_load(filename, 'Build data', Build) + # We excluded coredata when saving Build object, load it separately + b.environment.coredata = coredata.load(build_dir) + return b except FileNotFoundError: raise MesonException(f'No such build data file as {filename!r}.') def save(obj: Build, filename: str) -> None: - with open(filename, 'wb') as f: - pickle.dump(obj, f) + # Exclude coredata because we pickle it separately already + cdata = obj.environment.coredata + obj.environment.coredata = None + try: + with open(filename, 'wb') as f: + pickle.dump(obj, f) + finally: + obj.environment.coredata = cdata diff --git a/mesonbuild/cargo/__init__.py b/mesonbuild/cargo/__init__.py new file mode 100644 index 0000000..0007b9d --- /dev/null +++ b/mesonbuild/cargo/__init__.py @@ -0,0 +1,5 @@ +__all__ = [ + 'interpret' +] + +from .interpreter import interpret diff --git a/mesonbuild/cargo/builder.py b/mesonbuild/cargo/builder.py new file mode 100644 index 0000000..17b4ca5 --- /dev/null +++ b/mesonbuild/cargo/builder.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +"""Provides helpers for building AST + +This is meant to make building Meson AST from foreign (largely declarative) +build descriptions easier. +""" + +from __future__ import annotations +import dataclasses +import typing as T + +from .. import mparser + +if T.TYPE_CHECKING: + import builtins + + +@dataclasses.dataclass +class Builder: + + filename: str + + def _token(self, tid: str, value: mparser.TV_TokenTypes) -> mparser.Token[mparser.TV_TokenTypes]: + """Create a Token object, but with the line numbers stubbed out. + + :param tid: the token id (such as string, number, etc) + :param filename: the filename that the token was generated from + :param value: the value of the token + :return: A Token object + """ + return mparser.Token(tid, self.filename, -1, -1, -1, (-1, -1), value) + + def _symbol(self, val: str) -> mparser.SymbolNode: + return mparser.SymbolNode(self._token('', val)) + + def assign(self, value: mparser.BaseNode, varname: str) -> mparser.AssignmentNode: + return mparser.AssignmentNode(self.identifier(varname), self._symbol('='), value) + + def string(self, value: str) -> mparser.StringNode: + """Build A StringNode + + :param value: the value of the string + :return: A StringNode + """ + return mparser.StringNode(self._token('string', value)) + + def number(self, value: int) -> mparser.NumberNode: + """Build A NumberNode + + :param value: the value of the number + :return: A NumberNode + """ + return mparser.NumberNode(self._token('number', str(value))) + + def bool(self, value: builtins.bool) -> mparser.BooleanNode: + """Build A BooleanNode + + :param value: the value of the boolean + :return: A BooleanNode + """ + return mparser.BooleanNode(self._token('bool', value)) + + def array(self, value: T.List[mparser.BaseNode]) -> mparser.ArrayNode: + """Build an Array Node + + :param value: A list of nodes to insert into the array + :return: An ArrayNode built from the arguments + """ + args = mparser.ArgumentNode(self._token('array', 'unused')) + args.arguments = value + return mparser.ArrayNode(self._symbol('['), args, self._symbol(']')) + + def dict(self, value: T.Dict[mparser.BaseNode, mparser.BaseNode]) -> mparser.DictNode: + """Build an Dictionary Node + + :param value: A dict of nodes to insert into the dictionary + :return: An DictNode built from the arguments + """ + args = mparser.ArgumentNode(self._token('dict', 'unused')) + for key, val in value.items(): + args.set_kwarg_no_check(key, val) + return mparser.DictNode(self._symbol('{'), args, self._symbol('}')) + + def identifier(self, value: str) -> mparser.IdNode: + """Build A IdNode + + :param value: the value of the boolean + :return: A BooleanNode + """ + return mparser.IdNode(self._token('id', value)) + + def method(self, name: str, id_: mparser.IdNode, + pos: T.Optional[T.List[mparser.BaseNode]] = None, + kw: T.Optional[T.Mapping[str, mparser.BaseNode]] = None, + ) -> mparser.MethodNode: + """Create a method call. + + :param name: the name of the method + :param id_: the object to call the method of + :param pos: a list of positional arguments, defaults to None + :param kw: a dictionary of keyword arguments, defaults to None + :return: a method call object + """ + args = mparser.ArgumentNode(self._token('array', 'unused')) + if pos is not None: + args.arguments = pos + if kw is not None: + args.kwargs = {self.identifier(k): v for k, v in kw.items()} + return mparser.MethodNode(id_, self._symbol('.'), self.identifier(name), self._symbol('('), args, self._symbol(')')) + + def function(self, name: str, + pos: T.Optional[T.List[mparser.BaseNode]] = None, + kw: T.Optional[T.Mapping[str, mparser.BaseNode]] = None, + ) -> mparser.FunctionNode: + """Create a function call. + + :param name: the name of the function + :param pos: a list of positional arguments, defaults to None + :param kw: a dictionary of keyword arguments, defaults to None + :return: a method call object + """ + args = mparser.ArgumentNode(self._token('array', 'unused')) + if pos is not None: + args.arguments = pos + if kw is not None: + args.kwargs = {self.identifier(k): v for k, v in kw.items()} + return mparser.FunctionNode(self.identifier(name), self._symbol('('), args, self._symbol(')')) + + def equal(self, lhs: mparser.BaseNode, rhs: mparser.BaseNode) -> mparser.ComparisonNode: + """Create an equality operation + + :param lhs: The left hand side of the equal + :param rhs: the right hand side of the equal + :return: A compraison node + """ + return mparser.ComparisonNode('==', lhs, self._symbol('=='), rhs) + + def or_(self, lhs: mparser.BaseNode, rhs: mparser.BaseNode) -> mparser.OrNode: + """Create and OrNode + + :param lhs: The Left of the Node + :param rhs: The Right of the Node + :return: The OrNode + """ + return mparser.OrNode(lhs, self._symbol('or'), rhs) + + def and_(self, lhs: mparser.BaseNode, rhs: mparser.BaseNode) -> mparser.AndNode: + """Create an AndNode + + :param lhs: The left of the And + :param rhs: The right of the And + :return: The AndNode + """ + return mparser.AndNode(lhs, self._symbol('and'), rhs) + + def not_(self, value: mparser.BaseNode) -> mparser.NotNode: + """Create a not node + + :param value: The value to negate + :return: The NotNode + """ + return mparser.NotNode(self._token('not', ''), self._symbol('not'), value) + + def block(self, lines: T.List[mparser.BaseNode]) -> mparser.CodeBlockNode: + block = mparser.CodeBlockNode(self._token('node', '')) + block.lines = lines + return block diff --git a/mesonbuild/cargo/cfg.py b/mesonbuild/cargo/cfg.py new file mode 100644 index 0000000..0d49527 --- /dev/null +++ b/mesonbuild/cargo/cfg.py @@ -0,0 +1,274 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +"""Rust CFG parser. + +Rust uses its `cfg()` format in cargo. + +This may have the following functions: + - all() + - any() + - not() + +And additionally is made up of `identifier [ = str]`. Where the str is optional, +so you could have examples like: +``` +[target.`cfg(unix)`.dependencies] +[target.'cfg(target_arch = "x86_64")'.dependencies] +[target.'cfg(all(target_arch = "x86_64", target_arch = "x86"))'.dependencies] +``` +""" + +from __future__ import annotations +import dataclasses +import enum +import functools +import typing as T + + +from . import builder +from .. import mparser +from ..mesonlib import MesonBugException + +if T.TYPE_CHECKING: + _T = T.TypeVar('_T') + _LEX_TOKEN = T.Tuple['TokenType', T.Optional[str]] + _LEX_STREAM = T.Iterable[_LEX_TOKEN] + _LEX_STREAM_AH = T.Iterator[T.Tuple[_LEX_TOKEN, T.Optional[_LEX_TOKEN]]] + + +class TokenType(enum.Enum): + + LPAREN = enum.auto() + RPAREN = enum.auto() + STRING = enum.auto() + IDENTIFIER = enum.auto() + ALL = enum.auto() + ANY = enum.auto() + NOT = enum.auto() + COMMA = enum.auto() + EQUAL = enum.auto() + + +def lexer(raw: str) -> _LEX_STREAM: + """Lex a cfg() expression. + + :param raw: The raw cfg() expression + :return: An iterable of tokens + """ + buffer: T.List[str] = [] + is_string: bool = False + for s in raw: + if s.isspace() or s in {')', '(', ',', '='} or (s == '"' and buffer): + val = ''.join(buffer) + buffer.clear() + if is_string: + yield (TokenType.STRING, val) + elif val == 'any': + yield (TokenType.ANY, None) + elif val == 'all': + yield (TokenType.ALL, None) + elif val == 'not': + yield (TokenType.NOT, None) + elif val: + yield (TokenType.IDENTIFIER, val) + + if s == '(': + yield (TokenType.LPAREN, None) + continue + elif s == ')': + yield (TokenType.RPAREN, None) + continue + elif s == ',': + yield (TokenType.COMMA, None) + continue + elif s == '=': + yield (TokenType.EQUAL, None) + continue + elif s.isspace(): + continue + + if s == '"': + is_string = not is_string + else: + buffer.append(s) + if buffer: + # This should always be an identifier + yield (TokenType.IDENTIFIER, ''.join(buffer)) + + +def lookahead(iter: T.Iterator[_T]) -> T.Iterator[T.Tuple[_T, T.Optional[_T]]]: + """Get the current value of the iterable, and the next if possible. + + :param iter: The iterable to look into + :yield: A tuple of the current value, and, if possible, the next + :return: nothing + """ + current: _T + next_: T.Optional[_T] + try: + next_ = next(iter) + except StopIteration: + # This is an empty iterator, there's nothing to look ahead to + return + + while True: + current = next_ + try: + next_ = next(iter) + except StopIteration: + next_ = None + + yield current, next_ + + if next_ is None: + break + + +@dataclasses.dataclass +class IR: + + """Base IR node for Cargo CFG.""" + + +@dataclasses.dataclass +class String(IR): + + value: str + + +@dataclasses.dataclass +class Identifier(IR): + + value: str + + +@dataclasses.dataclass +class Equal(IR): + + lhs: IR + rhs: IR + + +@dataclasses.dataclass +class Any(IR): + + args: T.List[IR] + + +@dataclasses.dataclass +class All(IR): + + args: T.List[IR] + + +@dataclasses.dataclass +class Not(IR): + + value: IR + + +def _parse(ast: _LEX_STREAM_AH) -> IR: + (token, value), n_stream = next(ast) + if n_stream is not None: + ntoken, _ = n_stream + else: + ntoken, _ = (None, None) + + stream: T.List[_LEX_TOKEN] + if token is TokenType.IDENTIFIER: + if ntoken is TokenType.EQUAL: + return Equal(Identifier(value), _parse(ast)) + if token is TokenType.STRING: + return String(value) + if token is TokenType.EQUAL: + # In this case the previous caller already has handled the equal + return _parse(ast) + if token in {TokenType.ANY, TokenType.ALL}: + type_ = All if token is TokenType.ALL else Any + assert ntoken is TokenType.LPAREN + next(ast) # advance the iterator to get rid of the LPAREN + stream = [] + args: T.List[IR] = [] + while token is not TokenType.RPAREN: + (token, value), _ = next(ast) + if token is TokenType.COMMA: + args.append(_parse(lookahead(iter(stream)))) + stream.clear() + else: + stream.append((token, value)) + if stream: + args.append(_parse(lookahead(iter(stream)))) + return type_(args) + if token is TokenType.NOT: + next(ast) # advance the iterator to get rid of the LPAREN + stream = [] + # Mypy can't figure out that token is overridden inside the while loop + while token is not TokenType.RPAREN: # type: ignore + (token, value), _ = next(ast) + stream.append((token, value)) + return Not(_parse(lookahead(iter(stream)))) + + raise MesonBugException(f'Unhandled Cargo token: {token}') + + +def parse(ast: _LEX_STREAM) -> IR: + """Parse the tokenized list into Meson AST. + + :param ast: An iterable of Tokens + :return: An mparser Node to be used as a conditional + """ + ast_i: _LEX_STREAM_AH = lookahead(iter(ast)) + return _parse(ast_i) + + +@functools.singledispatch +def ir_to_meson(ir: T.Any, build: builder.Builder) -> mparser.BaseNode: + raise NotImplementedError + + +@ir_to_meson.register +def _(ir: String, build: builder.Builder) -> mparser.BaseNode: + return build.string(ir.value) + + +@ir_to_meson.register +def _(ir: Identifier, build: builder.Builder) -> mparser.BaseNode: + host_machine = build.identifier('host_machine') + if ir.value == "target_arch": + return build.method('cpu_family', host_machine) + elif ir.value in {"target_os", "target_family"}: + return build.method('system', host_machine) + elif ir.value == "target_endian": + return build.method('endian', host_machine) + raise MesonBugException(f"Unhandled Cargo identifier: {ir.value}") + + +@ir_to_meson.register +def _(ir: Equal, build: builder.Builder) -> mparser.BaseNode: + return build.equal(ir_to_meson(ir.lhs, build), ir_to_meson(ir.rhs, build)) + + +@ir_to_meson.register +def _(ir: Not, build: builder.Builder) -> mparser.BaseNode: + return build.not_(ir_to_meson(ir.value, build)) + + +@ir_to_meson.register +def _(ir: Any, build: builder.Builder) -> mparser.BaseNode: + args = iter(reversed(ir.args)) + last = next(args) + cur = build.or_(ir_to_meson(next(args), build), ir_to_meson(last, build)) + for a in args: + cur = build.or_(ir_to_meson(a, build), cur) + return cur + + +@ir_to_meson.register +def _(ir: All, build: builder.Builder) -> mparser.BaseNode: + args = iter(reversed(ir.args)) + last = next(args) + cur = build.and_(ir_to_meson(next(args), build), ir_to_meson(last, build)) + for a in args: + cur = build.and_(ir_to_meson(a, build), cur) + return cur diff --git a/mesonbuild/cargo/interpreter.py b/mesonbuild/cargo/interpreter.py new file mode 100644 index 0000000..a6634ac --- /dev/null +++ b/mesonbuild/cargo/interpreter.py @@ -0,0 +1,476 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +"""Interpreter for converting Cargo Toml definitions to Meson AST + +There are some notable limits here. We don't even try to convert something with +a build.rs: there's so few limits on what Cargo allows a build.rs (basically +none), and no good way for us to convert them. In that case, an actual meson +port will be required. +""" + +from __future__ import annotations +import dataclasses +import glob +import importlib +import itertools +import json +import os +import shutil +import typing as T + +from . import builder +from . import version +from ..mesonlib import MesonException, Popen_safe + +if T.TYPE_CHECKING: + from types import ModuleType + + from . import manifest + from .. import mparser + from ..environment import Environment + +# tomllib is present in python 3.11, before that it is a pypi module called tomli, +# we try to import tomllib, then tomli, +# TODO: add a fallback to toml2json? +tomllib: T.Optional[ModuleType] = None +toml2json: T.Optional[str] = None +for t in ['tomllib', 'tomli']: + try: + tomllib = importlib.import_module(t) + break + except ImportError: + pass +else: + # TODO: it would be better to use an Executable here, which could be looked + # up in the cross file or provided by a wrap. However, that will have to be + # passed in externally, since we don't have (and I don't think we should), + # have access to the `Environment` for that in this module. + toml2json = shutil.which('toml2json') + + +def load_toml(filename: str) -> T.Dict[object, object]: + if tomllib: + with open(filename, 'rb') as f: + raw = tomllib.load(f) + else: + if toml2json is None: + raise MesonException('Could not find an implementation of tomllib, nor toml2json') + + p, out, err = Popen_safe([toml2json, filename]) + if p.returncode != 0: + raise MesonException('toml2json failed to decode output\n', err) + + raw = json.loads(out) + + if not isinstance(raw, dict): + raise MesonException("Cargo.toml isn't a dictionary? How did that happen?") + + return raw + + +def fixup_meson_varname(name: str) -> str: + """Fixup a meson variable name + + :param name: The name to fix + :return: the fixed name + """ + return name.replace('-', '_') + + +# Pylance can figure out that these do not, in fact, overlap, but mypy can't +@T.overload +def _fixup_raw_mappings(d: manifest.BuildTarget) -> manifest.FixedBuildTarget: ... # type: ignore + +@T.overload +def _fixup_raw_mappings(d: manifest.LibTarget) -> manifest.FixedLibTarget: ... # type: ignore + +@T.overload +def _fixup_raw_mappings(d: manifest.Dependency) -> manifest.FixedDependency: ... + +def _fixup_raw_mappings(d: T.Union[manifest.BuildTarget, manifest.LibTarget, manifest.Dependency] + ) -> T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget, + manifest.FixedDependency]: + """Fixup raw cargo mappings to ones more suitable for python to consume. + + This does the following: + * replaces any `-` with `_`, cargo likes the former, but python dicts make + keys with `-` in them awkward to work with + * Convert Dependndency versions from the cargo format to something meson + understands + + :param d: The mapping to fix + :return: the fixed string + """ + raw = {fixup_meson_varname(k): v for k, v in d.items()} + if 'version' in raw: + assert isinstance(raw['version'], str), 'for mypy' + raw['version'] = version.convert(raw['version']) + return T.cast('T.Union[manifest.FixedBuildTarget, manifest.FixedLibTarget, manifest.FixedDependency]', raw) + + +@dataclasses.dataclass +class Package: + + """Representation of a Cargo Package entry, with defaults filled in.""" + + name: str + version: str + description: T.Optional[str] = None + resolver: T.Optional[str] = None + authors: T.List[str] = dataclasses.field(default_factory=list) + edition: manifest.EDITION = '2015' + rust_version: T.Optional[str] = None + documentation: T.Optional[str] = None + readme: T.Optional[str] = None + homepage: T.Optional[str] = None + repository: T.Optional[str] = None + license: T.Optional[str] = None + license_file: T.Optional[str] = None + keywords: T.List[str] = dataclasses.field(default_factory=list) + categories: T.List[str] = dataclasses.field(default_factory=list) + workspace: T.Optional[str] = None + build: T.Optional[str] = None + links: T.Optional[str] = None + exclude: T.List[str] = dataclasses.field(default_factory=list) + include: T.List[str] = dataclasses.field(default_factory=list) + publish: bool = True + metadata: T.Dict[str, T.Dict[str, str]] = dataclasses.field(default_factory=dict) + default_run: T.Optional[str] = None + autobins: bool = True + autoexamples: bool = True + autotests: bool = True + autobenches: bool = True + + +@dataclasses.dataclass +class Dependency: + + """Representation of a Cargo Dependency Entry.""" + + version: T.List[str] + registry: T.Optional[str] = None + git: T.Optional[str] = None + branch: T.Optional[str] = None + rev: T.Optional[str] = None + path: T.Optional[str] = None + optional: bool = False + package: T.Optional[str] = None + default_features: bool = False + features: T.List[str] = dataclasses.field(default_factory=list) + + @classmethod + def from_raw(cls, raw: manifest.DependencyV) -> Dependency: + """Create a dependency from a raw cargo dictionary""" + if isinstance(raw, str): + return cls(version.convert(raw)) + return cls(**_fixup_raw_mappings(raw)) + + +@dataclasses.dataclass +class BuildTarget: + + name: str + crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib']) + path: dataclasses.InitVar[T.Optional[str]] = None + + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field + # True for lib, bin, test + test: bool = True + + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doctest-field + # True for lib + doctest: bool = False + + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field + # True for lib, bin, benchmark + bench: bool = True + + # https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doc-field + # True for libraries and binaries + doc: bool = False + + harness: bool = True + edition: manifest.EDITION = '2015' + required_features: T.List[str] = dataclasses.field(default_factory=list) + plugin: bool = False + + +@dataclasses.dataclass +class Library(BuildTarget): + + """Representation of a Cargo Library Entry.""" + + doctest: bool = True + doc: bool = True + proc_macro: bool = False + crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['lib']) + doc_scrape_examples: bool = True + + +@dataclasses.dataclass +class Binary(BuildTarget): + + """Representation of a Cargo Bin Entry.""" + + doc: bool = True + + +@dataclasses.dataclass +class Test(BuildTarget): + + """Representation of a Cargo Test Entry.""" + + bench: bool = True + + +@dataclasses.dataclass +class Benchmark(BuildTarget): + + """Representation of a Cargo Benchmark Entry.""" + + test: bool = True + + +@dataclasses.dataclass +class Example(BuildTarget): + + """Representation of a Cargo Example Entry.""" + + crate_type: T.List[manifest.CRATE_TYPE] = dataclasses.field(default_factory=lambda: ['bin']) + + +@dataclasses.dataclass +class Manifest: + + """Cargo Manifest definition. + + Most of these values map up to the Cargo Manifest, but with default values + if not provided. + + Cargo subprojects can contain what Meson wants to treat as multiple, + interdependent, subprojects. + + :param subdir: the subdirectory that this cargo project is in + :param path: the path within the cargo subproject. + """ + + package: Package + dependencies: T.Dict[str, Dependency] + dev_dependencies: T.Dict[str, Dependency] + build_dependencies: T.Dict[str, Dependency] + lib: Library + bin: T.List[Binary] + test: T.List[Test] + bench: T.List[Benchmark] + example: T.List[Example] + features: T.Dict[str, T.List[str]] + target: T.Dict[str, T.Dict[str, Dependency]] + subdir: str + path: str = '' + + +def _convert_manifest(raw_manifest: manifest.Manifest, subdir: str, path: str = '') -> Manifest: + # This cast is a bit of a hack to deal with proc-macro + lib = _fixup_raw_mappings(raw_manifest.get('lib', {})) + + # We need to set the name field if it's not set manually, + # including if other fields are set in the lib section + lib.setdefault('name', raw_manifest['package']['name']) + + pkg = T.cast('manifest.FixedPackage', + {fixup_meson_varname(k): v for k, v in raw_manifest['package'].items()}) + + return Manifest( + Package(**pkg), + {k: Dependency.from_raw(v) for k, v in raw_manifest.get('dependencies', {}).items()}, + {k: Dependency.from_raw(v) for k, v in raw_manifest.get('dev-dependencies', {}).items()}, + {k: Dependency.from_raw(v) for k, v in raw_manifest.get('build-dependencies', {}).items()}, + Library(**lib), + [Binary(**_fixup_raw_mappings(b)) for b in raw_manifest.get('bin', {})], + [Test(**_fixup_raw_mappings(b)) for b in raw_manifest.get('test', {})], + [Benchmark(**_fixup_raw_mappings(b)) for b in raw_manifest.get('bench', {})], + [Example(**_fixup_raw_mappings(b)) for b in raw_manifest.get('example', {})], + raw_manifest.get('features', {}), + {k: {k2: Dependency.from_raw(v2) for k2, v2 in v.get('dependencies', {}).items()} + for k, v in raw_manifest.get('target', {}).items()}, + subdir, + path, + ) + + +def _load_manifests(subdir: str) -> T.Dict[str, Manifest]: + filename = os.path.join(subdir, 'Cargo.toml') + raw = load_toml(filename) + + manifests: T.Dict[str, Manifest] = {} + + raw_manifest: T.Union[manifest.Manifest, manifest.VirtualManifest] + if 'package' in raw: + raw_manifest = T.cast('manifest.Manifest', raw) + manifest_ = _convert_manifest(raw_manifest, subdir) + manifests[manifest_.package.name] = manifest_ + else: + raw_manifest = T.cast('manifest.VirtualManifest', raw) + + if 'workspace' in raw_manifest: + # XXX: need to verify that python glob and cargo globbing are the + # same and probably write a glob implementation. Blarg + + # We need to chdir here to make the glob work correctly + pwd = os.getcwd() + os.chdir(subdir) + members: T.Iterable[str] + try: + members = itertools.chain.from_iterable( + glob.glob(m) for m in raw_manifest['workspace']['members']) + finally: + os.chdir(pwd) + if 'exclude' in raw_manifest['workspace']: + members = (x for x in members if x not in raw_manifest['workspace']['exclude']) + + for m in members: + filename = os.path.join(subdir, m, 'Cargo.toml') + raw = load_toml(filename) + + raw_manifest = T.cast('manifest.Manifest', raw) + man = _convert_manifest(raw_manifest, subdir, m) + manifests[man.package.name] = man + + return manifests + + +def _dependency_name(package_name: str) -> str: + return package_name if package_name.endswith('-rs') else f'{package_name}-rs' + + +def _dependency_varname(package_name: str) -> str: + return f'{fixup_meson_varname(package_name)}_dep' + + +def _create_project(cargo: Manifest, build: builder.Builder, env: Environment) -> T.List[mparser.BaseNode]: + """Create a function call + + :param cargo: The Manifest to generate from + :param build: The AST builder + :param env: Meson environment + :return: a list nodes + """ + args: T.List[mparser.BaseNode] = [] + args.extend([ + build.string(cargo.package.name), + build.string('rust'), + ]) + kwargs: T.Dict[str, mparser.BaseNode] = { + 'version': build.string(cargo.package.version), + # Always assume that the generated meson is using the latest features + # This will warn when when we generate deprecated code, which is helpful + # for the upkeep of the module + 'meson_version': build.string(f'>= {env.coredata.version}'), + 'default_options': build.array([build.string(f'rust_std={cargo.package.edition}')]), + } + if cargo.package.license: + kwargs['license'] = build.string(cargo.package.license) + elif cargo.package.license_file: + kwargs['license_files'] = build.string(cargo.package.license_file) + + return [build.function('project', args, kwargs)] + + +def _create_dependencies(cargo: Manifest, build: builder.Builder) -> T.List[mparser.BaseNode]: + ast: T.List[mparser.BaseNode] = [] + for name, dep in cargo.dependencies.items(): + package_name = dep.package or name + kw = { + 'version': build.array([build.string(s) for s in dep.version]), + } + ast.extend([ + build.assign( + build.function( + 'dependency', + [build.string(_dependency_name(package_name))], + kw, + ), + _dependency_varname(package_name), + ), + ]) + return ast + + +def _create_lib(cargo: Manifest, build: builder.Builder, crate_type: manifest.CRATE_TYPE) -> T.List[mparser.BaseNode]: + dependencies: T.List[mparser.BaseNode] = [] + dependency_map: T.Dict[mparser.BaseNode, mparser.BaseNode] = {} + for name, dep in cargo.dependencies.items(): + package_name = dep.package or name + dependencies.append(build.identifier(_dependency_varname(package_name))) + if name != package_name: + dependency_map[build.string(fixup_meson_varname(package_name))] = build.string(name) + + posargs: T.List[mparser.BaseNode] = [ + build.string(fixup_meson_varname(cargo.package.name)), + build.string(os.path.join('src', 'lib.rs')), + ] + + kwargs: T.Dict[str, mparser.BaseNode] = { + 'dependencies': build.array(dependencies), + 'rust_dependency_map': build.dict(dependency_map), + } + + lib: mparser.BaseNode + if cargo.lib.proc_macro or crate_type == 'proc-macro': + kwargs['rust_args'] = build.array([build.string('--extern'), build.string('proc_macro')]) + lib = build.method('proc_macro', build.identifier('rust'), posargs, kwargs) + else: + if crate_type in {'lib', 'rlib', 'staticlib'}: + target_type = 'static_library' + elif crate_type in {'dylib', 'cdylib'}: + target_type = 'shared_library' + else: + raise MesonException(f'Unsupported crate type {crate_type}') + if crate_type in {'staticlib', 'cdylib'}: + kwargs['rust_abi'] = build.string('c') + lib = build.function(target_type, posargs, kwargs) + + return [ + build.assign(lib, 'lib'), + build.assign( + build.function( + 'declare_dependency', + kw={ + 'link_with': build.identifier('lib'), + }, + ), + 'dep' + ), + build.method( + 'override_dependency', + build.identifier('meson'), + [ + build.string(_dependency_name(cargo.package.name)), + build.identifier('dep'), + ], + ), + ] + + +def interpret(subp_name: str, subdir: str, env: Environment) -> mparser.CodeBlockNode: + package_name = subp_name[:-3] if subp_name.endswith('-rs') else subp_name + manifests = _load_manifests(os.path.join(env.source_dir, subdir)) + cargo = manifests.get(package_name) + if not cargo: + raise MesonException(f'Cargo package {package_name!r} not found in {subdir}') + + filename = os.path.join(cargo.subdir, cargo.path, 'Cargo.toml') + build = builder.Builder(filename) + + ast = _create_project(cargo, build, env) + ast += [build.assign(build.function('import', [build.string('rust')]), 'rust')] + ast += _create_dependencies(cargo, build) + + # Libs are always auto-discovered and there's no other way to handle them, + # which is unfortunate for reproducability + if os.path.exists(os.path.join(env.source_dir, cargo.subdir, cargo.path, 'src', 'lib.rs')): + for crate_type in cargo.lib.crate_type: + ast.extend(_create_lib(cargo, build, crate_type)) + + return build.block(ast) diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py new file mode 100644 index 0000000..e6192d0 --- /dev/null +++ b/mesonbuild/cargo/manifest.py @@ -0,0 +1,227 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +"""Type definitions for cargo manifest files.""" + +from __future__ import annotations +import typing as T + +from typing_extensions import Literal, TypedDict, Required + +EDITION = Literal['2015', '2018', '2021'] +CRATE_TYPE = Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro'] + +Package = TypedDict( + 'Package', + { + 'name': Required[str], + 'version': Required[str], + 'authors': T.List[str], + 'edition': EDITION, + 'rust-version': str, + 'description': str, + 'readme': str, + 'license': str, + 'license-file': str, + 'keywords': T.List[str], + 'categories': T.List[str], + 'workspace': str, + 'build': str, + 'links': str, + 'include': T.List[str], + 'exclude': T.List[str], + 'publish': bool, + 'metadata': T.Dict[str, T.Dict[str, str]], + 'default-run': str, + 'autobins': bool, + 'autoexamples': bool, + 'autotests': bool, + 'autobenches': bool, + }, + total=False, +) +"""A description of the Package Dictionary.""" + +class FixedPackage(TypedDict, total=False): + + """A description of the Package Dictionary, fixed up.""" + + name: Required[str] + version: Required[str] + authors: T.List[str] + edition: EDITION + rust_version: str + description: str + readme: str + license: str + license_file: str + keywords: T.List[str] + categories: T.List[str] + workspace: str + build: str + links: str + include: T.List[str] + exclude: T.List[str] + publish: bool + metadata: T.Dict[str, T.Dict[str, str]] + default_run: str + autobins: bool + autoexamples: bool + autotests: bool + autobenches: bool + + +class Badge(TypedDict): + + """An entry in the badge section.""" + + status: Literal['actively-developed', 'passively-developed', 'as-is', 'experimental', 'deprecated', 'none'] + + +Dependency = TypedDict( + 'Dependency', + { + 'version': str, + 'registry': str, + 'git': str, + 'branch': str, + 'rev': str, + 'path': str, + 'optional': bool, + 'package': str, + 'default-features': bool, + 'features': T.List[str], + }, + total=False, +) +"""An entry in the *dependencies sections.""" + + +class FixedDependency(TypedDict, total=False): + + """An entry in the *dependencies sections, fixed up.""" + + version: T.List[str] + registry: str + git: str + branch: str + rev: str + path: str + optional: bool + package: str + default_features: bool + features: T.List[str] + + +DependencyV = T.Union[Dependency, str] +"""A Dependency entry, either a string or a Dependency Dict.""" + + +_BaseBuildTarget = TypedDict( + '_BaseBuildTarget', + { + 'path': str, + 'test': bool, + 'doctest': bool, + 'bench': bool, + 'doc': bool, + 'plugin': bool, + 'proc-macro': bool, + 'harness': bool, + 'edition': EDITION, + 'crate-type': T.List[CRATE_TYPE], + 'required-features': T.List[str], + }, + total=False, +) + + +class BuildTarget(_BaseBuildTarget, total=False): + + name: Required[str] + +class LibTarget(_BaseBuildTarget, total=False): + + name: str + + +class _BaseFixedBuildTarget(TypedDict, total=False): + path: str + test: bool + doctest: bool + bench: bool + doc: bool + plugin: bool + harness: bool + edition: EDITION + crate_type: T.List[CRATE_TYPE] + required_features: T.List[str] + + +class FixedBuildTarget(_BaseFixedBuildTarget, total=False): + + name: str + +class FixedLibTarget(_BaseFixedBuildTarget, total=False): + + name: Required[str] + proc_macro: bool + + +class Target(TypedDict): + + """Target entry in the Manifest File.""" + + dependencies: T.Dict[str, DependencyV] + + +class Workspace(TypedDict): + + """The representation of a workspace. + + In a vritual manifest the :attribute:`members` is always present, but in a + project manifest, an empty workspace may be provided, in which case the + workspace is implicitly filled in by values from the path based dependencies. + + the :attribute:`exclude` is always optional + """ + + members: T.List[str] + exclude: T.List[str] + + +Manifest = TypedDict( + 'Manifest', + { + 'package': Package, + 'badges': T.Dict[str, Badge], + 'dependencies': T.Dict[str, DependencyV], + 'dev-dependencies': T.Dict[str, DependencyV], + 'build-dependencies': T.Dict[str, DependencyV], + 'lib': LibTarget, + 'bin': T.List[BuildTarget], + 'test': T.List[BuildTarget], + 'bench': T.List[BuildTarget], + 'example': T.List[BuildTarget], + 'features': T.Dict[str, T.List[str]], + 'target': T.Dict[str, Target], + 'workspace': Workspace, + + # TODO: patch? + # TODO: replace? + }, + total=False, +) +"""The Cargo Manifest format.""" + + +class VirtualManifest(TypedDict): + + """The Representation of a virtual manifest. + + Cargo allows a root manifest that contains only a workspace, this is called + a virtual manifest. This doesn't really map 1:1 with any meson concept, + except perhaps the proposed "meta project". + """ + + workspace: Workspace diff --git a/mesonbuild/cargo/version.py b/mesonbuild/cargo/version.py new file mode 100644 index 0000000..cde7a83 --- /dev/null +++ b/mesonbuild/cargo/version.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +"""Convert Cargo versions into Meson compatible ones.""" + +from __future__ import annotations +import typing as T + + +def convert(cargo_ver: str) -> T.List[str]: + """Convert a Cargo compatible version into a Meson compatible one. + + :param cargo_ver: The version, as Cargo specifies + :return: A list of version constraints, as Meson understands them + """ + # Cleanup, just for safety + cargo_ver = cargo_ver.strip() + cargo_vers = [c.strip() for c in cargo_ver.split(',')] + + out: T.List[str] = [] + + for ver in cargo_vers: + # This covers >= and =< as well + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#comparison-requirements + if ver.startswith(('>', '<', '=')): + out.append(ver) + + elif ver.startswith('~'): + # Rust has these tilde requirements, which means that it is >= to + # the version, but less than the next version + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#tilde-requirements + # we convert those into a pair of constraints + v = ver[1:].split('.') + out.append(f'>= {".".join(v)}') + if len(v) == 3: + out.append(f'< {v[0]}.{int(v[1]) + 1}.0') + elif len(v) == 2: + out.append(f'< {v[0]}.{int(v[1]) + 1}') + else: + out.append(f'< {int(v[0]) + 1}') + + elif '*' in ver: + # Rust has astrisk requirements,, which are like 1.* == ~1 + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#wildcard-requirements + v = ver.split('.')[:-1] + if v: + out.append(f'>= {".".join(v)}') + if len(v) == 2: + out.append(f'< {v[0]}.{int(v[1]) + 1}') + elif len(v) == 1: + out.append(f'< {int(v[0]) + 1}') + + else: + # a Caret version is equivalent to the default strategy + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#caret-requirements + if ver.startswith('^'): + ver = ver[1:] + + # If there is no qualifier, then it means this or the next non-zero version + # That means that if this is `1.1.0``, then we need `>= 1.1.0` && `< 2.0.0` + # Or if we have `0.1.0`, then we need `>= 0.1.0` && `< 0.2.0` + # Or if we have `0.1`, then we need `>= 0.1.0` && `< 0.2.0` + # Or if we have `0.0.0`, then we need `< 1.0.0` + # Or if we have `0.0`, then we need `< 1.0.0` + # Or if we have `0`, then we need `< 1.0.0` + # Or if we have `0.0.3`, then we need `>= 0.0.3` && `< 0.0.4` + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-cratesio + # + # this works much like the ~ versions, but in reverse. Tilde starts + # at the patch version and works up, to the major version, while + # bare numbers start at the major version and work down to the patch + # version + vers = ver.split('.') + min_: T.List[str] = [] + max_: T.List[str] = [] + bumped = False + for v_ in vers: + if v_ != '0' and not bumped: + min_.append(v_) + max_.append(str(int(v_) + 1)) + bumped = True + else: + min_.append(v_) + if not bumped: + max_.append('0') + + # If there is no minimum, don't emit one + if set(min_) != {'0'}: + out.append('>= {}'.format('.'.join(min_))) + if set(max_) != {'0'}: + out.append('< {}'.format('.'.join(max_))) + else: + out.append('< 1') + + return out diff --git a/mesonbuild/cmake/__init__.py b/mesonbuild/cmake/__init__.py index 16c1322..e9d7f2a 100644 --- a/mesonbuild/cmake/__init__.py +++ b/mesonbuild/cmake/__init__.py @@ -19,30 +19,21 @@ __all__ = [ 'CMakeExecutor', 'CMakeExecScope', 'CMakeException', - 'CMakeFileAPI', 'CMakeInterpreter', 'CMakeTarget', 'CMakeToolchain', - 'CMakeTraceLine', 'CMakeTraceParser', - 'SingleTargetOptions', 'TargetOptions', - 'parse_generator_expressions', 'language_map', - 'backend_generator_map', - 'cmake_get_generator_args', 'cmake_defines_to_args', 'check_cmake_args', 'cmake_is_debug', 'resolve_cmake_trace_targets', - 'ResolvedTarget', ] -from .common import CMakeException, SingleTargetOptions, TargetOptions, cmake_defines_to_args, language_map, backend_generator_map, cmake_get_generator_args, check_cmake_args, cmake_is_debug +from .common import CMakeException, TargetOptions, cmake_defines_to_args, language_map, check_cmake_args, cmake_is_debug from .executor import CMakeExecutor -from .fileapi import CMakeFileAPI -from .generator import parse_generator_expressions from .interpreter import CMakeInterpreter from .toolchain import CMakeToolchain, CMakeExecScope -from .traceparser import CMakeTarget, CMakeTraceLine, CMakeTraceParser -from .tracetargets import resolve_cmake_trace_targets, ResolvedTarget +from .traceparser import CMakeTarget, CMakeTraceParser +from .tracetargets import resolve_cmake_trace_targets diff --git a/mesonbuild/cmake/common.py b/mesonbuild/cmake/common.py index dffdbdf..aa0bbbf 100644 --- a/mesonbuild/cmake/common.py +++ b/mesonbuild/cmake/common.py @@ -14,6 +14,7 @@ # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool. +from __future__ import annotations from ..mesonlib import MesonException, OptionKey from .. import mlog @@ -22,6 +23,7 @@ import typing as T if T.TYPE_CHECKING: from ..environment import Environment + from ..interpreterbase import TYPE_var language_map = { 'c': 'C', @@ -120,16 +122,11 @@ def cmake_get_generator_args(env: 'Environment') -> T.List[str]: assert backend_name in backend_generator_map return ['-G', backend_generator_map[backend_name]] -def cmake_defines_to_args(raw: T.Any, permissive: bool = False) -> T.List[str]: - res = [] # type: T.List[str] - if not isinstance(raw, list): - raw = [raw] +def cmake_defines_to_args(raw: T.List[T.Dict[str, TYPE_var]], permissive: bool = False) -> T.List[str]: + res: T.List[str] = [] for i in raw: - if not isinstance(i, dict): - raise MesonException('Invalid CMake defines. Expected a dict, but got a {}'.format(type(i).__name__)) for key, val in i.items(): - assert isinstance(key, str) if key in blacklist_cmake_defs: mlog.warning('Setting', mlog.bold(key), 'is not supported. See the meson docs for cross compilation support:') mlog.warning(' - URL: https://mesonbuild.com/CMake-module.html#cross-compilation') @@ -145,9 +142,9 @@ def cmake_defines_to_args(raw: T.Any, permissive: bool = False) -> T.List[str]: return res -# TODO: this functuin will become obsolete once the `cmake_args` kwarg is dropped +# TODO: this function will become obsolete once the `cmake_args` kwarg is dropped def check_cmake_args(args: T.List[str]) -> T.List[str]: - res = [] # type: T.List[str] + res: T.List[str] = [] dis = ['-D' + x for x in blacklist_cmake_defs] assert dis # Ensure that dis is not empty. for i in args: @@ -169,14 +166,14 @@ class CMakeInclude: class CMakeFileGroup: def __init__(self, data: T.Dict[str, T.Any]) -> None: - self.defines = data.get('defines', '') # type: str - self.flags = _flags_to_list(data.get('compileFlags', '')) # type: T.List[str] - self.is_generated = data.get('isGenerated', False) # type: bool - self.language = data.get('language', 'C') # type: str - self.sources = [Path(x) for x in data.get('sources', [])] # type: T.List[Path] + self.defines: str = data.get('defines', '') + self.flags = _flags_to_list(data.get('compileFlags', '')) + self.is_generated: bool = data.get('isGenerated', False) + self.language: str = data.get('language', 'C') + self.sources = [Path(x) for x in data.get('sources', [])] # Fix the include directories - self.includes = [] # type: T.List[CMakeInclude] + self.includes: T.List[CMakeInclude] = [] for i in data.get('includePath', []): if isinstance(i, dict) and 'path' in i: isSystem = i.get('isSystem', False) @@ -199,21 +196,21 @@ class CMakeFileGroup: class CMakeTarget: def __init__(self, data: T.Dict[str, T.Any]) -> None: - self.artifacts = [Path(x) for x in data.get('artifacts', [])] # type: T.List[Path] - self.src_dir = Path(data.get('sourceDirectory', '')) # type: Path - self.build_dir = Path(data.get('buildDirectory', '')) # type: Path - self.name = data.get('name', '') # type: str - self.full_name = data.get('fullName', '') # type: str - self.install = data.get('hasInstallRule', False) # type: bool - self.install_paths = [Path(x) for x in set(data.get('installPaths', []))] # type: T.List[Path] - self.link_lang = data.get('linkerLanguage', '') # type: str - self.link_libraries = _flags_to_list(data.get('linkLibraries', '')) # type: T.List[str] - self.link_flags = _flags_to_list(data.get('linkFlags', '')) # type: T.List[str] - self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', '')) # type: T.List[str] - # self.link_path = Path(data.get('linkPath', '')) # type: Path - self.type = data.get('type', 'EXECUTABLE') # type: str - # self.is_generator_provided = data.get('isGeneratorProvided', False) # type: bool - self.files = [] # type: T.List[CMakeFileGroup] + self.artifacts = [Path(x) for x in data.get('artifacts', [])] + self.src_dir = Path(data.get('sourceDirectory', '')) + self.build_dir = Path(data.get('buildDirectory', '')) + self.name: str = data.get('name', '') + self.full_name: str = data.get('fullName', '') + self.install: bool = data.get('hasInstallRule', False) + self.install_paths = [Path(x) for x in set(data.get('installPaths', []))] + self.link_lang: str = data.get('linkerLanguage', '') + self.link_libraries = _flags_to_list(data.get('linkLibraries', '')) + self.link_flags = _flags_to_list(data.get('linkFlags', '')) + self.link_lang_flags = _flags_to_list(data.get('linkLanguageFlags', '')) + # self.link_path = Path(data.get('linkPath', '')) + self.type: str = data.get('type', 'EXECUTABLE') + # self.is_generator_provided: bool = data.get('isGeneratorProvided', False) + self.files: T.List[CMakeFileGroup] = [] for i in data.get('fileGroups', []): self.files += [CMakeFileGroup(i)] @@ -240,10 +237,10 @@ class CMakeTarget: class CMakeProject: def __init__(self, data: T.Dict[str, T.Any]) -> None: - self.src_dir = Path(data.get('sourceDirectory', '')) # type: Path - self.build_dir = Path(data.get('buildDirectory', '')) # type: Path - self.name = data.get('name', '') # type: str - self.targets = [] # type: T.List[CMakeTarget] + self.src_dir = Path(data.get('sourceDirectory', '')) + self.build_dir = Path(data.get('buildDirectory', '')) + self.name: str = data.get('name', '') + self.targets: T.List[CMakeTarget] = [] for i in data.get('targets', []): self.targets += [CMakeTarget(i)] @@ -259,8 +256,8 @@ class CMakeProject: class CMakeConfiguration: def __init__(self, data: T.Dict[str, T.Any]) -> None: - self.name = data.get('name', '') # type: str - self.projects = [] # type: T.List[CMakeProject] + self.name: str = data.get('name', '') + self.projects: T.List[CMakeProject] = [] for i in data.get('projects', []): self.projects += [CMakeProject(i)] @@ -273,9 +270,9 @@ class CMakeConfiguration: class SingleTargetOptions: def __init__(self) -> None: - self.opts = {} # type: T.Dict[str, str] - self.lang_args = {} # type: T.Dict[str, T.List[str]] - self.link_args = [] # type: T.List[str] + self.opts: T.Dict[str, str] = {} + self.lang_args: T.Dict[str, T.List[str]] = {} + self.link_args: T.List[str] = [] self.install = 'preserve' def set_opt(self, opt: str, val: str) -> None: @@ -293,7 +290,7 @@ class SingleTargetOptions: self.install = 'true' if install else 'false' def get_override_options(self, initial: T.List[str]) -> T.List[str]: - res = [] # type: T.List[str] + res: T.List[str] = [] for i in initial: opt = i[:i.find('=')] if opt not in self.opts: @@ -315,7 +312,7 @@ class SingleTargetOptions: class TargetOptions: def __init__(self) -> None: self.global_options = SingleTargetOptions() - self.target_options = {} # type: T.Dict[str, SingleTargetOptions] + self.target_options: T.Dict[str, SingleTargetOptions] = {} def __getitem__(self, tgt: str) -> SingleTargetOptions: if tgt not in self.target_options: diff --git a/mesonbuild/cmake/executor.py b/mesonbuild/cmake/executor.py index afd21ef..7958baf 100644 --- a/mesonbuild/cmake/executor.py +++ b/mesonbuild/cmake/executor.py @@ -33,15 +33,15 @@ if T.TYPE_CHECKING: from ..mesonlib import MachineChoice from ..programs import ExternalProgram -TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]] -TYPE_cache_key = T.Tuple[str, T.Tuple[str, ...], str, T.FrozenSet[T.Tuple[str, str]]] + TYPE_result = T.Tuple[int, T.Optional[str], T.Optional[str]] + TYPE_cache_key = T.Tuple[str, T.Tuple[str, ...], str, T.FrozenSet[T.Tuple[str, str]]] class CMakeExecutor: # The class's copy of the CMake path. Avoids having to search for it # multiple times in the same Meson invocation. - class_cmakebin = PerMachine(None, None) # type: PerMachine[T.Optional[ExternalProgram]] - class_cmakevers = PerMachine(None, None) # type: PerMachine[T.Optional[str]] - class_cmake_cache = {} # type: T.Dict[T.Any, TYPE_result] + class_cmakebin: PerMachine[T.Optional[ExternalProgram]] = PerMachine(None, None) + class_cmakevers: PerMachine[T.Optional[str]] = PerMachine(None, None) + class_cmake_cache: T.Dict[T.Any, TYPE_result] = {} def __init__(self, environment: 'Environment', version: str, for_machine: MachineChoice, silent: bool = False): self.min_version = version @@ -50,8 +50,8 @@ class CMakeExecutor: self.cmakebin, self.cmakevers = self.find_cmake_binary(self.environment, silent=silent) self.always_capture_stderr = True self.print_cmout = False - self.prefix_paths = [] # type: T.List[str] - self.extra_cmake_args = [] # type: T.List[str] + self.prefix_paths: T.List[str] = [] + self.extra_cmake_args: T.List[str] = [] if self.cmakebin is None: return @@ -108,23 +108,29 @@ class CMakeExecutor: mlog.log(f'Did not find CMake {cmakebin.name!r}') return None try: - p, out = Popen_safe(cmakebin.get_command() + ['--version'])[0:2] + cmd = cmakebin.get_command() + p, out = Popen_safe(cmd + ['--version'])[0:2] if p.returncode != 0: mlog.warning('Found CMake {!r} but couldn\'t run it' - ''.format(' '.join(cmakebin.get_command()))) + ''.format(' '.join(cmd))) return None except FileNotFoundError: mlog.warning('We thought we found CMake {!r} but now it\'s not there. How odd!' - ''.format(' '.join(cmakebin.get_command()))) + ''.format(' '.join(cmd))) return None except PermissionError: - msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmakebin.get_command())) + msg = 'Found CMake {!r} but didn\'t have permissions to run it.'.format(' '.join(cmd)) if not is_windows(): msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' mlog.warning(msg) return None - cmvers = re.search(r'(cmake|cmake3)\s*version\s*([\d.]+)', out).group(2) - return cmvers + + cmvers = re.search(r'(cmake|cmake3)\s*version\s*([\d.]+)', out) + if cmvers is not None: + return cmvers.group(2) + mlog.warning(f'We thought we found CMake {cmd!r}, but it was missing the expected ' + 'version string in its output.') + return None def set_exec_mode(self, print_cmout: T.Optional[bool] = None, always_capture_stderr: T.Optional[bool] = None) -> None: if print_cmout is not None: diff --git a/mesonbuild/cmake/fileapi.py b/mesonbuild/cmake/fileapi.py index fce2891..baf499f 100644 --- a/mesonbuild/cmake/fileapi.py +++ b/mesonbuild/cmake/fileapi.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .common import CMakeException, CMakeBuildFile, CMakeConfiguration import typing as T @@ -27,8 +28,8 @@ class CMakeFileAPI: self.api_base_dir = self.build_dir / '.cmake' / 'api' / 'v1' self.request_dir = self.api_base_dir / 'query' / 'client-meson' self.reply_dir = self.api_base_dir / 'reply' - self.cmake_sources = [] # type: T.List[CMakeBuildFile] - self.cmake_configurations = [] # type: T.List[CMakeConfiguration] + self.cmake_sources: T.List[CMakeBuildFile] = [] + self.cmake_configurations: T.List[CMakeConfiguration] = [] self.kind_resolver_map = { 'codemodel': self._parse_codemodel, 'cmakeFiles': self._parse_cmakeFiles, diff --git a/mesonbuild/cmake/generator.py b/mesonbuild/cmake/generator.py index 9c9fa1c..750e4c2 100644 --- a/mesonbuild/cmake/generator.py +++ b/mesonbuild/cmake/generator.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .. import mesonlib from .. import mlog @@ -37,8 +38,8 @@ def parse_generator_expressions( if '$<' not in raw: return raw - out = '' # type: str - i = 0 # type: int + out = '' + i = 0 def equal(arg: str) -> str: col_pos = arg.find(',') @@ -97,9 +98,9 @@ def parse_generator_expressions( return ';'.join([x for x in tgt.properties['IMPORTED_LOCATION'] if x]) return '' - supported = { + supported: T.Dict[str, T.Callable[[str], str]] = { # Boolean functions - 'BOOL': lambda x: '0' if x.upper() in {'0', 'FALSE', 'OFF', 'N', 'NO', 'IGNORE', 'NOTFOUND'} or x.endswith('-NOTFOUND') else '1', + 'BOOL': lambda x: '0' if x.upper() in {'', '0', 'FALSE', 'OFF', 'N', 'NO', 'IGNORE', 'NOTFOUND'} or x.endswith('-NOTFOUND') else '1', 'AND': lambda x: '1' if all(y == '1' for y in x.split(',')) else '0', 'OR': lambda x: '1' if any(y == '1' for y in x.split(',')) else '0', 'NOT': lambda x: '0' if x == '1' else '1', @@ -139,17 +140,17 @@ def parse_generator_expressions( 'TARGET_NAME_IF_EXISTS': lambda x: x if x in trace.targets else '', 'TARGET_PROPERTY': target_property, 'TARGET_FILE': target_file, - } # type: T.Dict[str, T.Callable[[str], str]] + } # Recursively evaluate generator expressions def eval_generator_expressions() -> str: nonlocal i i += 2 - func = '' # type: str - args = '' # type: str - res = '' # type: str - exp = '' # type: str + func = '' + args = '' + res = '' + exp = '' # Determine the body of the expression while i < len(raw): diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index f453aa3..8e9ea18 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -48,6 +48,7 @@ from ..mparser import ( IndexNode, MethodNode, NumberNode, + SymbolNode, ) @@ -55,13 +56,12 @@ if T.TYPE_CHECKING: from .common import CMakeConfiguration, TargetOptions from .traceparser import CMakeGeneratorTarget from .._typing import ImmutableListProtocol - from ..build import Build from ..backend.backends import Backend from ..environment import Environment -TYPE_mixed = T.Union[str, int, bool, Path, BaseNode] -TYPE_mixed_list = T.Union[TYPE_mixed, T.Sequence[TYPE_mixed]] -TYPE_mixed_kwargs = T.Dict[str, TYPE_mixed_list] + TYPE_mixed = T.Union[str, int, bool, Path, BaseNode] + TYPE_mixed_list = T.Union[TYPE_mixed, T.Sequence[TYPE_mixed]] + TYPE_mixed_kwargs = T.Dict[str, TYPE_mixed_list] # Disable all warnings automatically enabled with --trace and friends # See https://cmake.org/cmake/help/latest/variable/CMAKE_POLICY_WARNING_CMPNNNN.html @@ -765,10 +765,9 @@ class ConverterCustomTarget: mlog.log(' -- depends: ', mlog.bold(str(self.depends))) class CMakeInterpreter: - def __init__(self, build: 'Build', subdir: Path, src_dir: Path, install_prefix: Path, env: 'Environment', backend: 'Backend'): - self.build = build + def __init__(self, subdir: Path, install_prefix: Path, env: 'Environment', backend: 'Backend'): self.subdir = subdir - self.src_dir = src_dir + self.src_dir = Path(env.get_source_dir(), subdir) self.build_dir_rel = subdir / '__CMake_build' self.build_dir = Path(env.get_build_dir()) / self.build_dir_rel self.install_prefix = install_prefix @@ -959,14 +958,17 @@ class CMakeInterpreter: def token(tid: str = 'string', val: TYPE_mixed = '') -> Token: return Token(tid, self.subdir.as_posix(), 0, 0, 0, None, val) + def symbol(val: str) -> SymbolNode: + return SymbolNode(token('', val)) + def string(value: str) -> StringNode: - return StringNode(token(val=value)) + return StringNode(token(val=value), escape=False) def id_node(value: str) -> IdNode: return IdNode(token(val=value)) def number(value: int) -> NumberNode: - return NumberNode(token(val=value)) + return NumberNode(token(val=str(value))) def nodeify(value: TYPE_mixed_list) -> BaseNode: if isinstance(value, str): @@ -984,14 +986,14 @@ class CMakeInterpreter: raise RuntimeError('invalid type of value: {} ({})'.format(type(value).__name__, str(value))) def indexed(node: BaseNode, index: int) -> IndexNode: - return IndexNode(node, nodeify(index)) + return IndexNode(node, symbol('['), nodeify(index), symbol(']')) def array(elements: TYPE_mixed_list) -> ArrayNode: args = ArgumentNode(token()) if not isinstance(elements, list): elements = [args] args.arguments += [nodeify(x) for x in elements if x is not None] - return ArrayNode(args, 0, 0, 0, 0) + return ArrayNode(symbol('['), args, symbol(']')) def function(name: str, args: T.Optional[TYPE_mixed_list] = None, kwargs: T.Optional[TYPE_mixed_kwargs] = None) -> FunctionNode: args = [] if args is None else args @@ -1002,7 +1004,7 @@ class CMakeInterpreter: args = [args] args_n.arguments = [nodeify(x) for x in args if x is not None] args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None} - func_n = FunctionNode(self.subdir.as_posix(), 0, 0, 0, 0, name, args_n) + func_n = FunctionNode(id_node(name), symbol('('), args_n, symbol(')')) return func_n def method(obj: BaseNode, name: str, args: T.Optional[TYPE_mixed_list] = None, kwargs: T.Optional[TYPE_mixed_kwargs] = None) -> MethodNode: @@ -1014,10 +1016,10 @@ class CMakeInterpreter: args = [args] args_n.arguments = [nodeify(x) for x in args if x is not None] args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None} - return MethodNode(self.subdir.as_posix(), 0, 0, obj, name, args_n) + return MethodNode(obj, symbol('.'), id_node(name), symbol('('), args_n, symbol(')')) def assign(var_name: str, value: BaseNode) -> AssignmentNode: - return AssignmentNode(self.subdir.as_posix(), 0, 0, var_name, value) + return AssignmentNode(id_node(var_name), symbol('='), value) # Generate the root code block and the project function call root_cb = CodeBlockNode(token()) diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py index 477629e..be5bd66 100644 --- a/mesonbuild/cmake/toolchain.py +++ b/mesonbuild/cmake/toolchain.py @@ -144,7 +144,7 @@ class CMakeToolchain: return res def get_defaults(self) -> T.Dict[str, T.List[str]]: - defaults = {} # type: T.Dict[str, T.List[str]] + defaults: T.Dict[str, T.List[str]] = {} # Do nothing if the user does not want automatic defaults if not self.properties.get_cmake_defaults(): @@ -153,13 +153,13 @@ class CMakeToolchain: # Best effort to map the meson system name to CMAKE_SYSTEM_NAME, which # is not trivial since CMake lacks a list of all supported # CMAKE_SYSTEM_NAME values. - SYSTEM_MAP = { + SYSTEM_MAP: T.Dict[str, str] = { 'android': 'Android', 'linux': 'Linux', 'windows': 'Windows', 'freebsd': 'FreeBSD', 'darwin': 'Darwin', - } # type: T.Dict[str, str] + } # Only set these in a cross build. Otherwise CMake will trip up in native # builds and thing they are cross (which causes TRY_RUN() to break) diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py index e5aea8e..dd0dfb5 100644 --- a/mesonbuild/cmake/traceparser.py +++ b/mesonbuild/cmake/traceparser.py @@ -14,6 +14,7 @@ # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool. +from __future__ import annotations from .common import CMakeException from .generator import parse_generator_expressions @@ -66,9 +67,9 @@ class CMakeTarget: self.properties = properties self.imported = imported self.tline = tline - self.depends = [] # type: T.List[str] - self.current_bin_dir = None # type: T.Optional[Path] - self.current_src_dir = None # type: T.Optional[Path] + self.depends: T.List[str] = [] + self.current_bin_dir: T.Optional[Path] = None + self.current_src_dir: T.Optional[Path] = None def __repr__(self) -> str: s = 'CMake TARGET:\n -- name: {}\n -- type: {}\n -- imported: {}\n -- properties: {{\n{} }}\n -- tline: {}' @@ -88,10 +89,10 @@ class CMakeTarget: class CMakeGeneratorTarget(CMakeTarget): def __init__(self, name: str) -> None: super().__init__(name, 'CUSTOM', {}) - self.outputs = [] # type: T.List[Path] - self._outputs_str = [] # type: T.List[str] - self.command = [] # type: T.List[T.List[str]] - self.working_dir = None # type: T.Optional[Path] + self.outputs: T.List[Path] = [] + self._outputs_str: T.List[str] = [] + self.command: T.List[T.List[str]] = [] + self.working_dir: T.Optional[Path] = None class CMakeTraceParser: def __init__(self, cmake_version: str, build_dir: Path, env: 'Environment', permissive: bool = True) -> None: @@ -100,14 +101,14 @@ class CMakeTraceParser: self.targets: T.Dict[str, CMakeTarget] = {} self.cache: T.Dict[str, CMakeCacheEntry] = {} - self.explicit_headers = set() # type: T.Set[Path] + self.explicit_headers: T.Set[Path] = set() # T.List of targes that were added with add_custom_command to generate files - self.custom_targets = [] # type: T.List[CMakeGeneratorTarget] + self.custom_targets: T.List[CMakeGeneratorTarget] = [] self.env = env - self.permissive = permissive # type: bool - self.cmake_version = cmake_version # type: str + self.permissive = permissive + self.cmake_version = cmake_version self.trace_file = 'cmake_trace.txt' self.trace_file_path = build_dir / self.trace_file self.trace_format = 'json-v1' if version_compare(cmake_version, '>=3.17') else 'human' @@ -117,11 +118,11 @@ class CMakeTraceParser: # State for delayed command execution. Delayed command execution is realised # with a custom CMake file that overrides some functions and adds some # introspection information to the trace. - self.delayed_commands = [] # type: T.List[str] - self.stored_commands = [] # type: T.List[CMakeTraceLine] + self.delayed_commands: T.List[str] = [] + self.stored_commands: T.List[CMakeTraceLine] = [] # All supported functions - self.functions = { + self.functions: T.Dict[str, T.Callable[[CMakeTraceLine], None]] = { 'set': self._cmake_set, 'unset': self._cmake_unset, 'add_executable': self._cmake_add_executable, @@ -144,7 +145,7 @@ class CMakeTraceParser: 'meson_ps_execute_delayed_calls': self._meson_ps_execute_delayed_calls, 'meson_ps_reload_vars': self._meson_ps_reload_vars, 'meson_ps_disabled_function': self._meson_ps_disabled_function, - } # type: T.Dict[str, T.Callable[[CMakeTraceLine], None]] + } if version_compare(self.cmake_version, '<3.17.0'): mlog.deprecation(textwrap.dedent(f'''\ @@ -211,7 +212,7 @@ class CMakeTraceParser: p: {k: strlist_gen(v) for k, v in d.items()} for p, d in self.vars_by_file.items() } - self.explicit_headers = set(Path(parse_generator_expressions(str(x), self)) for x in self.explicit_headers) + self.explicit_headers = {Path(parse_generator_expressions(str(x), self)) for x in self.explicit_headers} self.cache = { k: CMakeCacheEntry( strlist_gen(v.value), @@ -287,7 +288,7 @@ class CMakeTraceParser: raise CMakeException(f'CMake: {function}() {error}\n{tline}') def _cmake_set(self, tline: CMakeTraceLine) -> None: - """Handler for the CMake set() function in all variaties. + """Handler for the CMake set() function in all varieties. comes in three flavors: set( [PARENT_SCOPE]) @@ -508,7 +509,7 @@ class CMakeTraceParser: targets += curr.split(';') if not args: - return self._gen_exception('set_property', 'faild to parse argument list', tline) + return self._gen_exception('set_property', 'failed to parse argument list', tline) if len(args) == 1: # Tries to set property to nothing so nothing has to be done @@ -574,7 +575,7 @@ class CMakeTraceParser: targets.append(curr) - # Now we need to try to reconsitute the original quoted format of the + # Now we need to try to reconstitute the original quoted format of the # arguments, as a property value could have spaces in it. Unlike # set_property() this is not context free. There are two approaches I # can think of, both have drawbacks: @@ -585,15 +586,15 @@ class CMakeTraceParser: # # Neither of these is awesome for obvious reasons. I'm going to try # option 1 first and fall back to 2, as 1 requires less code and less - # synchroniztion for cmake changes. + # synchronization for cmake changes. # # With the JSON output format, introduced in CMake 3.17, spaces are # handled properly and we don't have to do either options - arglist = [] # type: T.List[T.Tuple[str, T.List[str]]] + arglist: T.List[T.Tuple[str, T.List[str]]] = [] if self.trace_format == 'human': name = args.pop(0) - values = [] # type: T.List[str] + values: T.List[str] = [] prop_regex = re.compile(r'^[A-Z_]+$') for a in args: if prop_regex.match(a): @@ -767,7 +768,7 @@ class CMakeTraceParser: def _flatten_args(self, args: T.List[str]) -> T.List[str]: # Split lists in arguments - res = [] # type: T.List[str] + res: T.List[str] = [] for i in args: res += i.split(';') return res @@ -782,9 +783,9 @@ class CMakeTraceParser: reg_start = re.compile(r'^([A-Za-z]:)?/(.*/)*[^./]+$') reg_end = re.compile(r'^.*\.[a-zA-Z]+$') - fixed_list = [] # type: T.List[str] - curr_str = None # type: T.Optional[str] - path_found = False # type: bool + fixed_list: T.List[str] = [] + curr_str: T.Optional[str] = None + path_found = False for i in broken_list: if curr_str is None: diff --git a/mesonbuild/cmake/tracetargets.py b/mesonbuild/cmake/tracetargets.py index d64d1a1..bb5faa3 100644 --- a/mesonbuild/cmake/tracetargets.py +++ b/mesonbuild/cmake/tracetargets.py @@ -1,5 +1,6 @@ # SPDX-License-Identifer: Apache-2.0 # Copyright 2021 The Meson development team +from __future__ import annotations from .common import cmake_is_debug from .. import mlog @@ -49,8 +50,8 @@ def resolve_cmake_trace_targets(target_name: str, res.libraries += [curr] elif Path(curr).is_absolute() and Path(curr).exists(): res.libraries += [curr] - elif env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(curr) and clib_compiler: - # On Windows, CMake library dependencies can be passed as bare library names, + elif reg_is_maybe_bare_lib.match(curr) and clib_compiler: + # CMake library dependencies can be passed as bare library names, # CMake brute-forces a combination of prefix/suffix combinations to find the # right library. Assume any bare argument passed which is not also a CMake # target must be a system library we should try to link against. diff --git a/mesonbuild/compilers/asm.py b/mesonbuild/compilers/asm.py index 6149313..09cf9e1 100644 --- a/mesonbuild/compilers/asm.py +++ b/mesonbuild/compilers/asm.py @@ -1,13 +1,19 @@ +from __future__ import annotations + import os import typing as T -from ..mesonlib import EnvironmentException, get_meson_command +from ..mesonlib import EnvironmentException, OptionKey, get_meson_command from .compilers import Compiler +from .mixins.metrowerks import MetrowerksCompiler, mwasmarm_instruction_set_args, mwasmeppc_instruction_set_args if T.TYPE_CHECKING: from ..environment import Environment + from ..linkers.linkers import DynamicLinker + from ..mesonlib import MachineChoice + from ..envconfig import MachineInfo -nasm_optimization_args = { +nasm_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-O0'], @@ -15,13 +21,30 @@ nasm_optimization_args = { '2': ['-Ox'], '3': ['-Ox'], 's': ['-Ox'], -} # type: T.Dict[str, T.List[str]] +} class NasmCompiler(Compiler): language = 'nasm' id = 'nasm' + # https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features + crt_args: T.Dict[str, T.List[str]] = { + 'none': [], + 'md': ['/DEFAULTLIB:ucrt.lib', '/DEFAULTLIB:vcruntime.lib', '/DEFAULTLIB:msvcrt.lib'], + 'mdd': ['/DEFAULTLIB:ucrtd.lib', '/DEFAULTLIB:vcruntimed.lib', '/DEFAULTLIB:msvcrtd.lib'], + 'mt': ['/DEFAULTLIB:libucrt.lib', '/DEFAULTLIB:libvcruntime.lib', '/DEFAULTLIB:libcmt.lib'], + 'mtd': ['/DEFAULTLIB:libucrtd.lib', '/DEFAULTLIB:libvcruntimed.lib', '/DEFAULTLIB:libcmtd.lib'], + } + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, + for_machine: 'MachineChoice', info: 'MachineInfo', + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None, is_cross: bool = False): + super().__init__(ccache, exelist, version, for_machine, info, linker, full_version, is_cross) + if 'link' in self.linker.id: + self.base_options.add(OptionKey('b_vscrt')) + def needs_static_linker(self) -> bool: return True @@ -47,6 +70,14 @@ class NasmCompiler(Compiler): def get_output_args(self, outputname: str) -> T.List[str]: return ['-o', outputname] + def unix_args_to_native(self, args: T.List[str]) -> T.List[str]: + outargs: T.List[str] = [] + for arg in args: + if arg == '-pthread': + continue + outargs.append(arg) + return outargs + def get_optimization_args(self, optimization_level: str) -> T.List[str]: return nasm_optimization_args[optimization_level] @@ -61,7 +92,7 @@ class NasmCompiler(Compiler): return 'd' def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: - return ['-MD', '-MQ', outtarget, '-MF', outfile] + return ['-MD', outfile, '-MQ', outtarget] def sanity_check(self, work_dir: str, environment: 'Environment') -> None: if self.info.cpu_family not in {'x86', 'x86_64'}: @@ -89,9 +120,21 @@ class NasmCompiler(Compiler): def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]: return [] + # Linking ASM-only objects into an executable or DLL + # require this, otherwise it'll fail to find + # _WinMain or _DllMainCRTStartup. + def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]: + if not self.info.is_windows(): + return [] + return self.crt_args[self.get_crt_val(crt_val, buildtype)] + class YasmCompiler(NasmCompiler): id = 'yasm' + def get_optimization_args(self, optimization_level: str) -> T.List[str]: + # Yasm is incompatible with Nasm optimization flags. + return [] + def get_exelist(self, ccache: bool = True) -> T.List[str]: # Wrap yasm executable with an internal script that will write depfile. exelist = super().get_exelist(ccache) @@ -221,3 +264,56 @@ class MasmARMCompiler(Compiler): def depfile_for_object(self, objfile: str) -> T.Optional[str]: return None + + +class MetrowerksAsmCompiler(MetrowerksCompiler, Compiler): + language = 'nasm' + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, + for_machine: 'MachineChoice', info: 'MachineInfo', + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None, is_cross: bool = False): + Compiler.__init__(self, ccache, exelist, version, for_machine, info, linker, full_version, is_cross) + MetrowerksCompiler.__init__(self) + + self.warn_args: T.Dict[str, T.List[str]] = { + '0': [], + '1': [], + '2': [], + '3': [], + 'everything': []} + self.can_compile_suffixes.add('s') + + def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]: + return [] + + def get_optimization_args(self, optimization_level: str) -> T.List[str]: + return [] + + def get_pic_args(self) -> T.List[str]: + return [] + + def needs_static_linker(self) -> bool: + return True + + +class MetrowerksAsmCompilerARM(MetrowerksAsmCompiler): + id = 'mwasmarm' + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwasmarm_instruction_set_args.get(instruction_set, None) + + def sanity_check(self, work_dir: str, environment: 'Environment') -> None: + if self.info.cpu_family not in {'arm'}: + raise EnvironmentException(f'ASM compiler {self.id!r} does not support {self.info.cpu_family} CPU family') + + +class MetrowerksAsmCompilerEmbeddedPowerPC(MetrowerksAsmCompiler): + id = 'mwasmeppc' + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwasmeppc_instruction_set_args.get(instruction_set, None) + + def sanity_check(self, work_dir: str, environment: 'Environment') -> None: + if self.info.cpu_family not in {'ppc'}: + raise EnvironmentException(f'ASM compiler {self.id!r} does not support {self.info.cpu_family} CPU family') diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py index 51ae246..7f9e584 100644 --- a/mesonbuild/compilers/c.py +++ b/mesonbuild/compilers/c.py @@ -34,6 +34,8 @@ from .mixins.clang import ClangCompiler from .mixins.elbrus import ElbrusCompiler from .mixins.pgi import PGICompiler from .mixins.emscripten import EmscriptenMixin +from .mixins.metrowerks import MetrowerksCompiler +from .mixins.metrowerks import mwccarm_instruction_set_args, mwcceppc_instruction_set_args from .compilers import ( gnu_winlibs, msvc_winlibs, @@ -45,7 +47,7 @@ if T.TYPE_CHECKING: from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..programs import ExternalProgram from .compilers import CompileCheckMode @@ -54,6 +56,10 @@ if T.TYPE_CHECKING: else: CompilerMixinBase = object +_ALL_STDS = ['c89', 'c9x', 'c90', 'c99', 'c1x', 'c11', 'c17', 'c18', 'c2x'] +_ALL_STDS += [f'gnu{std[1:]}' for std in _ALL_STDS] +_ALL_STDS += ['iso9899:1990', 'iso9899:199409', 'iso9899:1999', 'iso9899:2011', 'iso9899:2017', 'iso9899:2018'] + class CCompiler(CLikeCompiler, Compiler): def attribute_check_func(self, name: str) -> str: @@ -99,12 +105,9 @@ class CCompiler(CLikeCompiler, Compiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() + key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ - OptionKey('std', machine=self.for_machine, lang=self.language): coredata.UserComboOption( - 'C language standard to use', - ['none'], - 'none', - ) + key: coredata.UserStdOption('C', _ALL_STDS), }) return opts @@ -123,20 +126,18 @@ class _ClangCStds(CompilerMixinBase): def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - c_stds = ['c89', 'c99', 'c11'] - g_stds = ['gnu89', 'gnu99', 'gnu11'] + stds = ['c89', 'c99', 'c11'] # https://releases.llvm.org/6.0.0/tools/clang/docs/ReleaseNotes.html # https://en.wikipedia.org/wiki/Xcode#Latest_versions if version_compare(self.version, self._C17_VERSION): - c_stds += ['c17'] - g_stds += ['gnu17'] + stds += ['c17'] if version_compare(self.version, self._C18_VERSION): - c_stds += ['c18'] - g_stds += ['gnu18'] + stds += ['c18'] if version_compare(self.version, self._C2X_VERSION): - c_stds += ['c2x'] - g_stds += ['gnu2x'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds + stds += ['c2x'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) return opts @@ -214,6 +215,8 @@ class EmscriptenCCompiler(EmscriptenMixin, ClangCCompiler): full_version: T.Optional[str] = None): if not is_cross: raise MesonException('Emscripten compiler can only be used for cross compilation.') + if not version_compare(version, '>=1.39.19'): + raise MesonException('Meson requires Emscripten >= 1.39.19') ClangCCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, info, exe_wrapper=exe_wrapper, linker=linker, defines=defines, full_version=full_version) @@ -240,8 +243,9 @@ class ArmclangCCompiler(ArmclangCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c90', 'c99', 'c11', 'gnu90', 'gnu99', 'gnu11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c90', 'c99', 'c11'], gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -281,16 +285,15 @@ class GnuCCompiler(GnuCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - c_stds = ['c89', 'c99', 'c11'] - g_stds = ['gnu89', 'gnu99', 'gnu11'] + stds = ['c89', 'c99', 'c11'] if version_compare(self.version, self._C18_VERSION): - c_stds += ['c17', 'c18'] - g_stds += ['gnu17', 'gnu18'] + stds += ['c17', 'c18'] if version_compare(self.version, self._C2X_VERSION): - c_stds += ['c2x'] - g_stds += ['gnu2x'] + stds += ['c2x'] key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none'] + c_stds + g_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -366,7 +369,9 @@ class ElbrusCCompiler(ElbrusCompiler, CCompiler): stds += ['c90', 'c1x', 'gnu90', 'gnu1x', 'iso9899:2011'] if version_compare(self.version, '>=1.26.00'): stds += ['c17', 'c18', 'iso9899:2017', 'iso9899:2018', 'gnu17', 'gnu18'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + stds + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds) return opts # Elbrus C compiler does not have lchmod, but there is only linker warning, not compiler error. @@ -400,11 +405,12 @@ class IntelCCompiler(IntelGnuLikeCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - c_stds = ['c89', 'c99'] - g_stds = ['gnu89', 'gnu99'] + stds = ['c89', 'c99'] if version_compare(self.version, '>=16.0.0'): - c_stds += ['c11'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds + stds += ['c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -461,33 +467,23 @@ class VisualStudioCCompiler(MSVCCompiler, VisualStudioLikeCCompilerMixin, CCompi def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - c_stds = ['c89', 'c99'] - # Need to have these to be compatible with projects - # that set c_std to e.g. gnu99. - # https://github.com/mesonbuild/meson/issues/7611 - g_stds = ['gnu89', 'gnu90', 'gnu9x', 'gnu99'] + stds = ['c89', 'c99'] if version_compare(self.version, self._C11_VERSION): - c_stds += ['c11'] - g_stds += ['gnu1x', 'gnu11'] + stds += ['c11'] if version_compare(self.version, self._C17_VERSION): - c_stds += ['c17', 'c18'] - g_stds += ['gnu17', 'gnu18'] - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none'] + c_stds + g_stds + stds += ['c17', 'c18'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True, gnu_deprecated=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: args = [] std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] - if std.value.startswith('gnu'): - mlog.log_once( - 'cl.exe does not actually support gnu standards, and meson ' - 'will instead demote to the nearest ISO C standard. This ' - 'may cause compilation to fail.') # As of MVSC 16.8, /std:c11 and /std:c17 are the only valid C standard options. - if std.value in {'c11', 'gnu1x', 'gnu11'}: + if std.value in {'c11'}: args.append('/std:c11') - elif std.value in {'c17', 'c18', 'gnu17', 'gnu18'}: + elif std.value in {'c17', 'c18'}: args.append('/std:c17') return args @@ -527,8 +523,9 @@ class IntelClCCompiler(IntelVisualStudioLikeCompiler, VisualStudioLikeCCompilerM def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -536,7 +533,7 @@ class IntelClCCompiler(IntelVisualStudioLikeCompiler, VisualStudioLikeCCompilerM key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value == 'c89': - mlog.log_once("ICL doesn't explicitly implement c89, setting the standard to 'none', which is close.") + mlog.log("ICL doesn't explicitly implement c89, setting the standard to 'none', which is close.", once=True) elif std.value != 'none': args.append('/Qstd:' + std.value) return args @@ -560,8 +557,9 @@ class ArmCCompiler(ArmCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -589,8 +587,9 @@ class CcrxCCompiler(CcrxCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99']) return opts def get_no_stdinc_args(self) -> T.List[str]: @@ -636,8 +635,9 @@ class Xc16CCompiler(Xc16Compiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'gnu89', 'gnu99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99'], gnu=True) return opts def get_no_stdinc_args(self) -> T.List[str]: @@ -681,8 +681,9 @@ class CompCertCCompiler(CompCertCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -718,8 +719,9 @@ class TICCompiler(TICompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_no_stdinc_args(self) -> T.List[str]: @@ -736,3 +738,60 @@ class TICCompiler(TICompiler, CCompiler): class C2000CCompiler(TICCompiler): # Required for backwards compat with projects created before ti-cgt support existed id = 'c2000' + +class MetrowerksCCompilerARM(MetrowerksCompiler, CCompiler): + id = 'mwccarm' + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, + is_cross: bool, info: 'MachineInfo', + exe_wrapper: T.Optional['ExternalProgram'] = None, + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None): + CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, + info, exe_wrapper, linker=linker, full_version=full_version) + MetrowerksCompiler.__init__(self) + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwccarm_instruction_set_args.get(instruction_set, None) + + def get_options(self) -> 'MutableKeyedOptionDictType': + opts = CCompiler.get_options(self) + c_stds = ['c99'] + opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + return opts + + def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + args = [] + std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] + if std.value != 'none': + args.append('-lang') + args.append(std.value) + return args + +class MetrowerksCCompilerEmbeddedPowerPC(MetrowerksCompiler, CCompiler): + id = 'mwcceppc' + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, + is_cross: bool, info: 'MachineInfo', + exe_wrapper: T.Optional['ExternalProgram'] = None, + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None): + CCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, + info, exe_wrapper, linker=linker, full_version=full_version) + MetrowerksCompiler.__init__(self) + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwcceppc_instruction_set_args.get(instruction_set, None) + + def get_options(self) -> 'MutableKeyedOptionDictType': + opts = CCompiler.get_options(self) + c_stds = ['c99'] + opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + return opts + + def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + args = [] + std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] + if std.value != 'none': + args.append('-lang ' + std.value) + return args diff --git a/mesonbuild/compilers/c_function_attributes.py b/mesonbuild/compilers/c_function_attributes.py index f663bfc..eec872b 100644 --- a/mesonbuild/compilers/c_function_attributes.py +++ b/mesonbuild/compilers/c_function_attributes.py @@ -100,18 +100,20 @@ C_FUNC_ATTRIBUTES = { 'int foo(void) __attribute__((unused));', 'used': 'int foo(void) __attribute__((used));', + 'vector_size': + '__attribute__((vector_size(32))); int foo(void) { return 0; }', 'visibility': ''' - int foo_def(void) __attribute__((visibility("default"))); - int foo_hid(void) __attribute__((visibility("hidden"))); - int foo_int(void) __attribute__((visibility("internal")));''', + int foo_def(void) __attribute__((visibility("default"))); int foo_def(void) { return 0; } + int foo_hid(void) __attribute__((visibility("hidden"))); int foo_hid(void) { return 0; } + int foo_int(void) __attribute__((visibility("internal"))); int foo_int(void) { return 0; }''', 'visibility:default': - 'int foo(void) __attribute__((visibility("default")));', + 'int foo(void) __attribute__((visibility("default"))); int foo(void) { return 0; }', 'visibility:hidden': - 'int foo(void) __attribute__((visibility("hidden")));', + 'int foo(void) __attribute__((visibility("hidden"))); int foo(void) { return 0; }', 'visibility:internal': - 'int foo(void) __attribute__((visibility("internal")));', + 'int foo(void) __attribute__((visibility("internal"))); int foo(void) { return 0; }', 'visibility:protected': - 'int foo(void) __attribute__((visibility("protected")));', + 'int foo(void) __attribute__((visibility("protected"))); int foo(void) { return 0; }', 'warning': 'int foo(void) __attribute__((warning("")));', 'warn_unused_result': diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 771d543..a7bb6c4 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -26,17 +26,18 @@ from .. import mesonlib from ..mesonlib import ( HoldableObject, EnvironmentException, MesonException, - Popen_safe, LibType, TemporaryDirectoryWinProof, OptionKey, + Popen_safe_logged, LibType, TemporaryDirectoryWinProof, OptionKey, ) from ..arglist import CompilerArgs if T.TYPE_CHECKING: - from ..build import BuildTarget + from ..build import BuildTarget, DFeatures from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker, RSPFileSyntax + from ..linkers import RSPFileSyntax + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..dependencies import Dependency @@ -45,7 +46,7 @@ if T.TYPE_CHECKING: """This file contains the data files of all compilers Meson knows about. To support a new compiler, add its information below. -Also add corresponding autodetection code in environment.py.""" +Also add corresponding autodetection code in detect.py.""" header_suffixes = {'h', 'hh', 'hpp', 'hxx', 'H', 'ipp', 'moc', 'vapi', 'di'} obj_suffixes = {'o', 'obj', 'res'} @@ -77,26 +78,26 @@ all_languages = lang_suffixes.keys() c_cpp_suffixes = {'h'} cpp_suffixes = set(lang_suffixes['cpp']) | c_cpp_suffixes c_suffixes = set(lang_suffixes['c']) | c_cpp_suffixes -assembler_suffixes = {'s', 'S', 'asm', 'masm'} +assembler_suffixes = {'s', 'S', 'sx', 'asm', 'masm'} llvm_ir_suffixes = {'ll'} all_suffixes = set(itertools.chain(*lang_suffixes.values(), assembler_suffixes, llvm_ir_suffixes, c_cpp_suffixes)) source_suffixes = all_suffixes - header_suffixes # List of languages that by default consume and output libraries following the # C ABI; these can generally be used interchangeably # This must be sorted, see sort_clink(). -clib_langs = ('objcpp', 'cpp', 'objc', 'c', 'fortran') +clib_langs = ('objcpp', 'cpp', 'objc', 'c', 'nasm', 'fortran') # List of languages that can be linked with C code directly by the linker # used in build.py:process_compilers() and build.py:get_dynamic_linker() # This must be sorted, see sort_clink(). clink_langs = ('d', 'cuda') + clib_langs SUFFIX_TO_LANG = dict(itertools.chain(*( - [(suffix, lang) for suffix in v] for lang, v in lang_suffixes.items()))) # type: T.Dict[str, str] + [(suffix, lang) for suffix in v] for lang, v in lang_suffixes.items()))) # Languages that should use LDFLAGS arguments when linking. -LANGUAGES_USING_LDFLAGS = {'objcpp', 'cpp', 'objc', 'c', 'fortran', 'd', 'cuda'} # type: T.Set[str] +LANGUAGES_USING_LDFLAGS = {'objcpp', 'cpp', 'objc', 'c', 'fortran', 'd', 'cuda'} # Languages that should use CPPFLAGS arguments when linking. -LANGUAGES_USING_CPPFLAGS = {'c', 'cpp', 'objc', 'objcpp'} # type: T.Set[str] +LANGUAGES_USING_CPPFLAGS = {'c', 'cpp', 'objc', 'objcpp'} soregex = re.compile(r'.*\.so(\.[0-9]+)?(\.[0-9]+)?(\.[0-9]+)?$') # Environment variables that each lang uses. @@ -133,11 +134,14 @@ def is_header(fname: 'mesonlib.FileOrString') -> bool: suffix = fname.split('.')[-1] return suffix in header_suffixes +def is_source_suffix(suffix: str) -> bool: + return suffix in source_suffixes + def is_source(fname: 'mesonlib.FileOrString') -> bool: if isinstance(fname, mesonlib.File): fname = fname.fname suffix = fname.split('.')[-1].lower() - return suffix in source_suffixes + return is_source_suffix(suffix) def is_assembly(fname: 'mesonlib.FileOrString') -> bool: if isinstance(fname, mesonlib.File): @@ -186,99 +190,116 @@ class CompileCheckMode(enum.Enum): LINK = 'link' -cuda_buildtype_args = {'plain': [], - 'debug': ['-g', '-G'], - 'debugoptimized': ['-g', '-lineinfo'], - 'release': [], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] -java_buildtype_args = {'plain': [], - 'debug': ['-g'], - 'debugoptimized': ['-g'], - 'release': [], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -rust_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': [], - 'release': [], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -d_gdc_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': ['-finline-functions'], - 'release': ['-finline-functions'], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -d_ldc_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': ['-enable-inlining', '-Hkeep-all-bodies'], - 'release': ['-enable-inlining', '-Hkeep-all-bodies'], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -d_dmd_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': ['-inline'], - 'release': ['-inline'], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -mono_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': ['-optimize+'], - 'release': ['-optimize+'], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - -swift_buildtype_args = {'plain': [], - 'debug': [], - 'debugoptimized': [], - 'release': [], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] +cuda_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': ['-g', '-G'], + 'debugoptimized': ['-g', '-lineinfo'], + 'release': [], + 'minsize': [], + 'custom': [], +} + +java_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': ['-g'], + 'debugoptimized': ['-g'], + 'release': [], + 'minsize': [], + 'custom': [], +} + +rust_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': [], + 'minsize': [], + 'custom': [], +} + +d_gdc_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': ['-finline-functions'], + 'release': ['-finline-functions'], + 'minsize': [], + 'custom': [], +} + +d_ldc_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': ['-enable-inlining', '-Hkeep-all-bodies'], + 'release': ['-enable-inlining', '-Hkeep-all-bodies'], + 'minsize': [], + 'custom': [], +} + +d_dmd_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': ['-inline'], + 'release': ['-inline'], + 'minsize': [], + 'custom': [], +} + +mono_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': ['-optimize+'], + 'release': ['-optimize+'], + 'minsize': [], + 'custom': [], +} + +swift_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': [], + 'minsize': [], + 'custom': [], +} gnu_winlibs = ['-lkernel32', '-luser32', '-lgdi32', '-lwinspool', '-lshell32', - '-lole32', '-loleaut32', '-luuid', '-lcomdlg32', '-ladvapi32'] # type: T.List[str] + '-lole32', '-loleaut32', '-luuid', '-lcomdlg32', '-ladvapi32'] msvc_winlibs = ['kernel32.lib', 'user32.lib', 'gdi32.lib', 'winspool.lib', 'shell32.lib', 'ole32.lib', 'oleaut32.lib', - 'uuid.lib', 'comdlg32.lib', 'advapi32.lib'] # type: T.List[str] - -clike_optimization_args = {'plain': [], - '0': [], - 'g': [], - '1': ['-O1'], - '2': ['-O2'], - '3': ['-O3'], - 's': ['-Os'], - } # type: T.Dict[str, T.List[str]] - -cuda_optimization_args = {'plain': [], - '0': [], - 'g': ['-O0'], - '1': ['-O1'], - '2': ['-O2'], - '3': ['-O3'], - 's': ['-O3'] - } # type: T.Dict[str, T.List[str]] - -cuda_debug_args = {False: [], - True: ['-g']} # type: T.Dict[bool, T.List[str]] - -clike_debug_args = {False: [], - True: ['-g']} # type: T.Dict[bool, T.List[str]] + 'uuid.lib', 'comdlg32.lib', 'advapi32.lib'] + +clike_optimization_args: T.Dict[str, T.List[str]] = { + 'plain': [], + '0': [], + 'g': [], + '1': ['-O1'], + '2': ['-O2'], + '3': ['-O3'], + 's': ['-Os'], +} + +cuda_optimization_args: T.Dict[str, T.List[str]] = { + 'plain': [], + '0': [], + 'g': ['-O0'], + '1': ['-O1'], + '2': ['-O2'], + '3': ['-O3'], + 's': ['-O3'] +} + +cuda_debug_args: T.Dict[bool, T.List[str]] = { + False: [], + True: ['-g'] +} + +clike_debug_args: T.Dict[bool, T.List[str]] = { + False: [], + True: ['-g'] +} + +MSCRT_VALS = ['none', 'md', 'mdd', 'mt', 'mtd'] base_options: 'KeyedOptionDictType' = { OptionKey('b_pch'): coredata.UserBooleanOption('Use precompiled headers', True), @@ -306,7 +327,7 @@ base_options: 'KeyedOptionDictType' = { OptionKey('b_pie'): coredata.UserBooleanOption('Build executables as position independent', False), OptionKey('b_bitcode'): coredata.UserBooleanOption('Generate and embed bitcode (only macOS/iOS/tvOS)', False), OptionKey('b_vscrt'): coredata.UserComboOption('VS run-time library type to use.', - ['none', 'md', 'mdd', 'mt', 'mtd', 'from_buildtype', 'static_from_buildtype'], + MSCRT_VALS + ['from_buildtype', 'static_from_buildtype'], 'from_buildtype'), } @@ -334,8 +355,19 @@ def get_option_value(options: 'KeyedOptionDictType', opt: OptionKey, fallback: ' return v +def are_asserts_disabled(options: KeyedOptionDictType) -> bool: + """Should debug assertions be disabled + + :param options: OptionDictionary + :return: whether to disable assertions or not + """ + return (options[OptionKey('b_ndebug')].value == 'true' or + (options[OptionKey('b_ndebug')].value == 'if-release' and + options[OptionKey('buildtype')].value in {'release', 'plain'})) + + def get_base_compile_args(options: 'KeyedOptionDictType', compiler: 'Compiler') -> T.List[str]: - args = [] # type T.List[str] + args: T.List[str] = [] try: if options[OptionKey('b_lto')].value: args.extend(compiler.get_lto_compile_args( @@ -365,10 +397,7 @@ def get_base_compile_args(options: 'KeyedOptionDictType', compiler: 'Compiler') except KeyError: pass try: - if (options[OptionKey('b_ndebug')].value == 'true' or - (options[OptionKey('b_ndebug')].value == 'if-release' and - options[OptionKey('buildtype')].value in {'release', 'plain'})): - args += compiler.get_disable_assert_args() + args += compiler.get_assert_args(are_asserts_disabled(options)) except KeyError: pass # This does not need a try...except @@ -387,7 +416,7 @@ def get_base_compile_args(options: 'KeyedOptionDictType', compiler: 'Compiler') def get_base_link_args(options: 'KeyedOptionDictType', linker: 'Compiler', is_shared_module: bool, build_dir: str) -> T.List[str]: - args = [] # type: T.List[str] + args: T.List[str] = [] try: if options[OptionKey('b_lto')].value: thinlto_cache_dir = None @@ -456,11 +485,13 @@ class CrossNoRunException(MesonException): class RunResult(HoldableObject): def __init__(self, compiled: bool, returncode: int = 999, - stdout: str = 'UNDEFINED', stderr: str = 'UNDEFINED'): + stdout: str = 'UNDEFINED', stderr: str = 'UNDEFINED', + cached: bool = False): self.compiled = compiled self.returncode = returncode self.stdout = stdout self.stderr = stderr + self.cached = cached class CompileResult(HoldableObject): @@ -485,18 +516,18 @@ class CompileResult(HoldableObject): class Compiler(HoldableObject, metaclass=abc.ABCMeta): # Libraries to ignore in find_library() since they are provided by the # compiler or the C library. Currently only used for MSVC. - ignore_libs = [] # type: T.List[str] + ignore_libs: T.List[str] = [] # Libraries that are internal compiler implementations, and must not be # manually searched. - internal_libs = [] # type: T.List[str] + internal_libs: T.List[str] = [] - LINKER_PREFIX = None # type: T.Union[None, str, T.List[str]] + LINKER_PREFIX: T.Union[None, str, T.List[str]] = None INVOKES_LINKER = True language: str id: str warn_args: T.Dict[str, T.List[str]] - mode: str = 'COMPILER' + mode = 'COMPILER' def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, info: 'MachineInfo', @@ -508,7 +539,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): if not hasattr(self, 'file_suffixes'): self.file_suffixes = lang_suffixes[self.language] if not hasattr(self, 'can_compile_suffixes'): - self.can_compile_suffixes = set(self.file_suffixes) + self.can_compile_suffixes: T.Set[str] = set(self.file_suffixes) self.default_suffix = self.file_suffixes[0] self.version = version self.full_version = full_version @@ -598,7 +629,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): return self.exelist.copy() if ccache else self.exelist_no_ccache.copy() def get_linker_exelist(self) -> T.List[str]: - return self.linker.get_exelist() + return self.linker.get_exelist() if self.linker else self.get_exelist() @abc.abstractmethod def get_output_args(self, outputname: str) -> T.List[str]: @@ -689,14 +720,40 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): dependencies: T.Optional[T.List['Dependency']] = None) -> RunResult: raise EnvironmentException('Language %s does not support run checks.' % self.get_display_language()) + # Caching run() in general seems too risky (no way to know what the program + # depends on), but some callers know more about the programs they intend to + # run. + # For now we just accept code as a string, as that's what internal callers + # need anyway. If we wanted to accept files, the cache key would need to + # include mtime. + def cached_run(self, code: str, env: 'Environment', *, + extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]], None] = None, + dependencies: T.Optional[T.List['Dependency']] = None) -> RunResult: + run_check_cache = env.coredata.run_check_cache + args = self.build_wrapper_args(env, extra_args, dependencies, CompileCheckMode('link')) + key = (code, tuple(args)) + if key in run_check_cache: + p = run_check_cache[key] + p.cached = True + mlog.debug('Using cached run result:') + mlog.debug('Code:\n', code) + mlog.debug('Args:\n', extra_args) + mlog.debug('Cached run returncode:\n', p.returncode) + mlog.debug('Cached run stdout:\n', p.stdout) + mlog.debug('Cached run stderr:\n', p.stderr) + else: + p = self.run(code, env, extra_args=extra_args, dependencies=dependencies) + run_check_cache[key] = p + return p + def sizeof(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: raise EnvironmentException('Language %s does not support sizeof checks.' % self.get_display_language()) def alignment(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Optional[T.List[str]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: raise EnvironmentException('Language %s does not support alignment checks.' % self.get_display_language()) def has_function(self, funcname: str, prefix: str, env: 'Environment', *, @@ -724,7 +781,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): return args.copy() def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: raise EnvironmentException(f'Language {self.get_display_language()} does not support library finding.') def get_library_naming(self, env: 'Environment', libtype: LibType, @@ -744,22 +801,18 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def has_multi_link_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: return self.linker.has_multi_arguments(args, env) - def _get_compile_output(self, dirname: str, mode: str) -> str: - # TODO: mode should really be an enum - # In pre-processor mode, the output is sent to stdout and discarded - if mode == 'preprocess': - return None + def _get_compile_output(self, dirname: str, mode: CompileCheckMode) -> str: + assert mode != CompileCheckMode.PREPROCESS, 'In pre-processor mode, the output is sent to stdout and discarded' # Extension only matters if running results; '.exe' is # guaranteed to be executable on every platform. - if mode == 'link': + if mode == CompileCheckMode.LINK: suffix = 'exe' else: suffix = 'obj' return os.path.join(dirname, 'output.' + suffix) def get_compiler_args_for_mode(self, mode: CompileCheckMode) -> T.List[str]: - # TODO: mode should really be an enum - args = [] # type: T.List[str] + args: T.List[str] = [] args += self.get_always_args() if mode is CompileCheckMode.COMPILE: args += self.get_compile_only_args() @@ -776,9 +829,13 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): @contextlib.contextmanager def compile(self, code: 'mesonlib.FileOrString', extra_args: T.Union[None, CompilerArgs, T.List[str]] = None, - *, mode: str = 'link', want_output: bool = False, + *, mode: CompileCheckMode = CompileCheckMode.LINK, want_output: bool = False, temp_dir: T.Optional[str] = None) -> T.Iterator[T.Optional[CompileResult]]: # TODO: there isn't really any reason for this to be a contextmanager + + if mode == CompileCheckMode.PREPROCESS: + assert not want_output, 'In pre-processor mode, the output is sent to stdout and discarded' + if extra_args is None: extra_args = [] @@ -805,8 +862,8 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): commands.append(srcname) # Preprocess mode outputs to stdout, so no output args - output = self._get_compile_output(tmpdirname, mode) - if mode != 'preprocess': + if mode != CompileCheckMode.PREPROCESS: + output = self._get_compile_output(tmpdirname, mode) commands += self.get_output_args(output) commands.extend(self.get_compiler_args_for_mode(CompileCheckMode(mode))) @@ -819,15 +876,12 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): command_list = self.get_exelist(ccache=not no_ccache) + commands.to_native() mlog.debug('Running compile:') mlog.debug('Working directory: ', tmpdirname) - mlog.debug('Command line: ', ' '.join(command_list), '\n') mlog.debug('Code:\n', contents) os_env = os.environ.copy() os_env['LC_ALL'] = 'C' if no_ccache: os_env['CCACHE_DISABLE'] = '1' - p, stdo, stde = Popen_safe(command_list, cwd=tmpdirname, env=os_env) - mlog.debug('Compiler stdout:\n', stdo) - mlog.debug('Compiler stderr:\n', stde) + p, stdo, stde = Popen_safe_logged(command_list, msg='Command line', cwd=tmpdirname, env=os_env) result = CompileResult(stdo, stde, command_list, p.returncode, input_name=srcname) if want_output: @@ -837,17 +891,17 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): @contextlib.contextmanager def cached_compile(self, code: 'mesonlib.FileOrString', cdata: coredata.CoreData, *, extra_args: T.Union[None, T.List[str], CompilerArgs] = None, - mode: str = 'link', + mode: CompileCheckMode = CompileCheckMode.LINK, temp_dir: T.Optional[str] = None) -> T.Iterator[T.Optional[CompileResult]]: # TODO: There's isn't really any reason for this to be a context manager # Calculate the key - textra_args = tuple(extra_args) if extra_args is not None else tuple() # type: T.Tuple[str, ...] - key = (tuple(self.exelist), self.version, code, textra_args, mode) # type: coredata.CompilerCheckCacheKey + textra_args: T.Tuple[str, ...] = tuple(extra_args) if extra_args is not None else tuple() + key: coredata.CompilerCheckCacheKey = (tuple(self.exelist), self.version, code, textra_args, mode) # Check if not cached, and generate, otherwise get from the cache if key in cdata.compiler_check_cache: - p = cdata.compiler_check_cache[key] # type: CompileResult + p = cdata.compiler_check_cache[key] p.cached = True mlog.debug('Using cached compile:') mlog.debug('Cached command line: ', ' '.join(p.command), '\n') @@ -869,7 +923,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]: return [] - def get_link_debugfile_name(self, targetfile: str) -> str: + def get_link_debugfile_name(self, targetfile: str) -> T.Optional[str]: return self.linker.get_debugfile_name(targetfile) def get_link_debugfile_args(self, targetfile: str) -> T.List[str]: @@ -904,6 +958,14 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): return self.linker.build_rpath_args( env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath) + def get_archive_name(self, filename: str) -> str: + return self.linker.get_archive_name(filename) + + def get_command_to_archive_shlib(self) -> T.List[str]: + if not self.linker: + return [] + return self.linker.get_command_to_archive_shlib() + def thread_flags(self, env: 'Environment') -> T.List[str]: return [] @@ -922,10 +984,6 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]: return [] - def get_gui_app_args(self, value: bool) -> T.List[str]: - # Only used on Windows - return self.linker.get_gui_app_args(value) - def get_win_subsystem_args(self, value: str) -> T.List[str]: # By default the dynamic linker is going to return an empty # array in case it either doesn't support Windows subsystems @@ -970,7 +1028,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): rm_exact = ('-headerpad_max_install_names',) rm_prefixes = ('-Wl,', '-L',) rm_next = ('-L', '-framework',) - ret = [] # T.List[str] + ret: T.List[str] = [] iargs = iter(args) for arg in iargs: # Remove this argument @@ -1043,9 +1101,36 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def get_coverage_link_args(self) -> T.List[str]: return self.linker.get_coverage_args() - def get_disable_assert_args(self) -> T.List[str]: + def get_assert_args(self, disable: bool) -> T.List[str]: + """Get arguments to enable or disable assertion. + + :param disable: Whether to disable assertions + :return: A list of string arguments for this compiler + """ return [] + def get_crt_val(self, crt_val: str, buildtype: str) -> str: + if crt_val in MSCRT_VALS: + return crt_val + assert crt_val in {'from_buildtype', 'static_from_buildtype'} + + dbg = 'mdd' + rel = 'md' + if crt_val == 'static_from_buildtype': + dbg = 'mtd' + rel = 'mt' + + # Match what build type flags used to do. + if buildtype == 'plain': + return 'none' + elif buildtype == 'debug': + return dbg + elif buildtype in {'debugoptimized', 'release', 'minsize'}: + return rel + else: + assert buildtype == 'custom' + raise EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".') + def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]: raise EnvironmentException('This compiler does not support Windows CRT selection') @@ -1200,7 +1285,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): mode: CompileCheckMode = CompileCheckMode.COMPILE) -> CompilerArgs: """Arguments to pass the build_wrapper helper. - This generally needs to be set on a per-language baises. It provides + This generally needs to be set on a per-language basis. It provides a hook for languages to handle dependencies and extra args. The base implementation handles the most common cases, namely adding the check_arguments, unwrapping dependencies, and appending extra args. @@ -1235,15 +1320,14 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def _build_wrapper(self, code: 'mesonlib.FileOrString', env: 'Environment', extra_args: T.Union[None, CompilerArgs, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None, dependencies: T.Optional[T.List['Dependency']] = None, - mode: str = 'compile', want_output: bool = False, - disable_cache: bool = False, - temp_dir: str = None) -> T.Iterator[T.Optional[CompileResult]]: - """Helper for getting a cacched value when possible. + mode: CompileCheckMode = CompileCheckMode.COMPILE, want_output: bool = False, + disable_cache: bool = False) -> T.Iterator[T.Optional[CompileResult]]: + """Helper for getting a cached value when possible. This method isn't meant to be called externally, it's mean to be wrapped by other methods like compiles() and links(). """ - args = self.build_wrapper_args(env, extra_args, dependencies, CompileCheckMode(mode)) + args = self.build_wrapper_args(env, extra_args, dependencies, mode) if disable_cache or want_output: with self.compile(code, extra_args=args, mode=mode, want_output=want_output, temp_dir=env.scratch_dir) as r: yield r @@ -1254,7 +1338,7 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): def compiles(self, code: 'mesonlib.FileOrString', env: 'Environment', *, extra_args: T.Union[None, T.List[str], CompilerArgs, T.Callable[[CompileCheckMode], T.List[str]]] = None, dependencies: T.Optional[T.List['Dependency']] = None, - mode: str = 'compile', + mode: CompileCheckMode = CompileCheckMode.COMPILE, disable_cache: bool = False) -> T.Tuple[bool, bool]: with self._build_wrapper(code, env, extra_args, dependencies, mode, disable_cache=disable_cache) as p: return p.returncode == 0, p.cached @@ -1263,18 +1347,17 @@ class Compiler(HoldableObject, metaclass=abc.ABCMeta): compiler: T.Optional['Compiler'] = None, extra_args: T.Union[None, T.List[str], CompilerArgs, T.Callable[[CompileCheckMode], T.List[str]]] = None, dependencies: T.Optional[T.List['Dependency']] = None, - mode: str = 'compile', disable_cache: bool = False) -> T.Tuple[bool, bool]: if compiler: with compiler._build_wrapper(code, env, dependencies=dependencies, want_output=True) as r: objfile = mesonlib.File.from_absolute_file(r.output_name) return self.compiles(objfile, env, extra_args=extra_args, - dependencies=dependencies, mode='link', disable_cache=True) + dependencies=dependencies, mode=CompileCheckMode.LINK, disable_cache=True) return self.compiles(code, env, extra_args=extra_args, - dependencies=dependencies, mode='link', disable_cache=disable_cache) + dependencies=dependencies, mode=CompileCheckMode.LINK, disable_cache=disable_cache) - def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]: + def get_feature_args(self, kwargs: DFeatures, build_to_src: str) -> T.List[str]: """Used by D for extra language features.""" # TODO: using a TypeDict here would improve this raise EnvironmentException(f'{self.id} does not implement get_feature_args') @@ -1323,17 +1406,17 @@ def get_global_options(lang: str, cargs = coredata.UserArrayOption( description + ' compiler', - comp_options, split_args=True, user_input=True, allow_dups=True) + comp_options, split_args=True, allow_dups=True) largs = coredata.UserArrayOption( description + ' linker', - link_options, split_args=True, user_input=True, allow_dups=True) + link_options, split_args=True, allow_dups=True) if comp.INVOKES_LINKER and comp_key == envkey: # If the compiler acts as a linker driver, and we're using the # environment variable flags for both the compiler and linker # arguments, then put the compiler flags in the linker flags as well. - # This is how autotools works, and the env vars freature is for + # This is how autotools works, and the env vars feature is for # autotools compatibility. largs.extend_value(comp_options) diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py index 7e1cc54..43a5492 100644 --- a/mesonbuild/compilers/cpp.py +++ b/mesonbuild/compilers/cpp.py @@ -26,6 +26,7 @@ from .compilers import ( gnu_winlibs, msvc_winlibs, Compiler, + CompileCheckMode, ) from .c_function_attributes import CXX_FUNC_ATTRIBUTES, C_FUNC_ATTRIBUTES from .mixins.clike import CLikeCompiler @@ -39,27 +40,32 @@ from .mixins.clang import ClangCompiler from .mixins.elbrus import ElbrusCompiler from .mixins.pgi import PGICompiler from .mixins.emscripten import EmscriptenMixin +from .mixins.metrowerks import MetrowerksCompiler +from .mixins.metrowerks import mwccarm_instruction_set_args, mwcceppc_instruction_set_args if T.TYPE_CHECKING: - from .compilers import CompileCheckMode from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..programs import ExternalProgram CompilerMixinBase = CLikeCompiler else: CompilerMixinBase = object +_ALL_STDS = ['c++98', 'c++0x', 'c++03', 'c++1y', 'c++1z', 'c++11', 'c++14', 'c++17', 'c++2a', 'c++20', 'c++23', 'c++26'] +_ALL_STDS += [f'gnu{std[1:]}' for std in _ALL_STDS] +_ALL_STDS += ['vc++11', 'vc++14', 'vc++17', 'vc++20', 'vc++latest', 'c++latest'] + def non_msvc_eh_options(eh: str, args: T.List[str]) -> None: if eh == 'none': args.append('-fno-exceptions') elif eh in {'s', 'c'}: - mlog.warning('non-MSVC compilers do not support ' + eh + ' exception handling.' + - 'You may want to set eh to \'default\'.') + mlog.warning(f'non-MSVC compilers do not support {eh} exception handling. ' + 'You may want to set eh to \'default\'.', fatal=False) class CPPCompiler(CLikeCompiler, Compiler): def attribute_check_func(self, name: str) -> str: @@ -80,13 +86,16 @@ class CPPCompiler(CLikeCompiler, Compiler): full_version=full_version) CLikeCompiler.__init__(self, exe_wrapper) - @staticmethod - def get_display_language() -> str: + @classmethod + def get_display_language(cls) -> str: return 'C++' def get_no_stdinc_args(self) -> T.List[str]: return ['-nostdinc++'] + def get_no_stdlib_link_args(self) -> T.List[str]: + return ['-nostdlib++'] + def sanity_check(self, work_dir: str, environment: 'Environment') -> None: code = 'class breakCCompiler;int main(void) { return 0; }\n' return self._sanity_check_impl(work_dir, environment, 'sanitycheckcpp.cc', code) @@ -127,7 +136,7 @@ class CPPCompiler(CLikeCompiler, Compiler): # 2. even if it did have an env object, that might contain another more # recent -std= argument, which might lead to a cascaded failure. CPP_TEST = 'int i = static_cast(0);' - with self.compile(CPP_TEST, extra_args=[cpp_std_value], mode='compile') as p: + with self.compile(CPP_TEST, extra_args=[cpp_std_value], mode=CompileCheckMode.COMPILE) as p: if p.returncode == 0: mlog.debug(f'Compiler accepts {cpp_std_value}:', 'YES') return True @@ -152,6 +161,10 @@ class CPPCompiler(CLikeCompiler, Compiler): 'gnu++17': 'gnu++1z', 'c++20': 'c++2a', 'gnu++20': 'gnu++2a', + 'c++23': 'c++2b', + 'gnu++23': 'gnu++2b', + 'c++26': 'c++2c', + 'gnu++26': 'gnu++2c', } # Currently, remapping is only supported for Clang, Elbrus and GCC @@ -172,16 +185,54 @@ class CPPCompiler(CLikeCompiler, Compiler): opts = super().get_options() key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ - key: coredata.UserComboOption( - 'C++ language standard to use', - ['none'], - 'none', - ), + key: coredata.UserStdOption('C++', _ALL_STDS), }) return opts -class ClangCPPCompiler(ClangCompiler, CPPCompiler): +class _StdCPPLibMixin(CompilerMixinBase): + + """Detect whether to use libc++ or libstdc++.""" + + @functools.lru_cache(None) + def language_stdlib_only_link_flags(self, env: Environment) -> T.List[str]: + """Detect the C++ stdlib and default search dirs + + As an optimization, this method will cache the value, to avoid building the same values over and over + + :param env: An Environment object + :raises MesonException: If a stdlib cannot be determined + """ + + # We need to apply the search prefix here, as these link arguments may + # be passed to a different compiler with a different set of default + # search paths, such as when using Clang for C/C++ and gfortran for + # fortran. + search_dirs = [f'-L{d}' for d in self.get_compiler_dirs(env, 'libraries')] + + machine = env.machines[self.for_machine] + assert machine is not None, 'for mypy' + + # We need to determine whether to use libc++ or libstdc++. We can't + # really know the answer in most cases, only the most likely answer, + # because a user can install things themselves or build custom images. + search_order: T.List[str] = [] + if machine.system in {'android', 'darwin', 'dragonfly', 'freebsd', 'netbsd', 'openbsd'}: + search_order = ['c++', 'stdc++'] + else: + search_order = ['stdc++', 'c++'] + for lib in search_order: + if self.find_library(lib, env, []) is not None: + return search_dirs + [f'-l{lib}'] + # TODO: maybe a bug exception? + raise MesonException('Could not detect either libc++ or libstdc++ as your C++ stdlib implementation.') + + +class ClangCPPCompiler(_StdCPPLibMixin, ClangCompiler, CPPCompiler): + + _CPP23_VERSION = '>=12.0.0' + _CPP26_VERSION = '>=17.0.0' + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool, info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None, linker: T.Optional['DynamicLinker'] = None, @@ -208,11 +259,16 @@ class ClangCPPCompiler(ClangCompiler, CPPCompiler): ), key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True), }) - opts[key.evolve('std')].choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', - 'c++2a', 'c++20', 'gnu++11', 'gnu++14', 'gnu++17', 'gnu++1z', - 'gnu++2a', 'gnu++20', + cppstd_choices = [ + 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', 'c++2a', 'c++20', ] + if version_compare(self.version, self._CPP23_VERSION): + cppstd_choices.append('c++23') + if version_compare(self.version, self._CPP26_VERSION): + cppstd_choices.append('c++26') + std_opt = opts[key.evolve('std')] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cppstd_choices, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -223,7 +279,7 @@ class ClangCPPCompiler(ClangCompiler, CPPCompiler): return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -247,16 +303,6 @@ class ClangCPPCompiler(ClangCompiler, CPPCompiler): return libs return [] - def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]: - # We need to apply the search prefix here, as these link arguments may - # be passed to a different compiler with a different set of default - # search paths, such as when using Clang for C/C++ and gfortran for - # fortran, - search_dirs: T.List[str] = [] - for d in self.get_compiler_dirs(env, 'libraries'): - search_dirs.append(f'-L{d}') - return search_dirs + ['-lstdc++'] - class ArmLtdClangCPPCompiler(ClangCPPCompiler): @@ -264,15 +310,11 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): class AppleClangCPPCompiler(ClangCPPCompiler): - def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]: - # We need to apply the search prefix here, as these link arguments may - # be passed to a different compiler with a different set of default - # search paths, such as when using Clang for C/C++ and gfortran for - # fortran, - search_dirs: T.List[str] = [] - for d in self.get_compiler_dirs(env, 'libraries'): - search_dirs.append(f'-L{d}') - return search_dirs + ['-lc++'] + + _CPP23_VERSION = '>=13.0.0' + # TODO: We don't know which XCode version will include LLVM 17 yet, so + # use something absurd. + _CPP26_VERSION = '>=99.0.0' class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): @@ -286,12 +328,14 @@ class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): full_version: T.Optional[str] = None): if not is_cross: raise MesonException('Emscripten compiler can only be used for cross compilation.') + if not version_compare(version, '>=1.39.19'): + raise MesonException('Meson requires Emscripten >= 1.39.19') ClangCPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, info, exe_wrapper=exe_wrapper, linker=linker, defines=defines, full_version=full_version) def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -328,14 +372,13 @@ class ArmclangCPPCompiler(ArmclangCompiler, CPPCompiler): 'default', ), }) - opts[key].choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'gnu++98', - 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17', - ] + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++98', 'c++03', 'c++11', 'c++14', 'c++17'], gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -349,7 +392,7 @@ class ArmclangCPPCompiler(ArmclangCompiler, CPPCompiler): return [] -class GnuCPPCompiler(GnuCompiler, CPPCompiler): +class GnuCPPCompiler(_StdCPPLibMixin, GnuCompiler, CPPCompiler): def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool, info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None, linker: T.Optional['DynamicLinker'] = None, @@ -382,11 +425,17 @@ class GnuCPPCompiler(GnuCompiler, CPPCompiler): False, ) }) - opts[key].choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', - 'c++2a', 'c++20', 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17', - 'gnu++1z', 'gnu++2a', 'gnu++20', + cppstd_choices = [ + 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', + 'c++2a', 'c++20', ] + if version_compare(self.version, '>=11.0.0'): + cppstd_choices.append('c++23') + if version_compare(self.version, '>=14.0.0'): + cppstd_choices.append('c++26') + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cppstd_choices, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -397,7 +446,7 @@ class GnuCPPCompiler(GnuCompiler, CPPCompiler): return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -426,16 +475,6 @@ class GnuCPPCompiler(GnuCompiler, CPPCompiler): def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]: return ['-fpch-preprocess', '-include', os.path.basename(header)] - def language_stdlib_only_link_flags(self, env: 'Environment') -> T.List[str]: - # We need to apply the search prefix here, as these link arguments may - # be passed to a different compiler with a different set of default - # search paths, such as when using Clang for C/C++ and gfortran for - # fortran, - search_dirs: T.List[str] = [] - for d in self.get_compiler_dirs(env, 'libraries'): - search_dirs.append(f'-L{d}') - return ['-lstdc++'] - class PGICPPCompiler(PGICompiler, CPPCompiler): def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool, @@ -473,21 +512,21 @@ class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - cpp_stds = ['none', 'c++98', 'gnu++98'] + cpp_stds = ['c++98'] if version_compare(self.version, '>=1.20.00'): - cpp_stds += ['c++03', 'c++0x', 'c++11', 'gnu++03', 'gnu++0x', 'gnu++11'] + cpp_stds += ['c++03', 'c++0x', 'c++11'] if version_compare(self.version, '>=1.21.00') and version_compare(self.version, '<1.22.00'): - cpp_stds += ['c++14', 'gnu++14', 'c++1y', 'gnu++1y'] + cpp_stds += ['c++14', 'c++1y'] if version_compare(self.version, '>=1.22.00'): - cpp_stds += ['c++14', 'gnu++14'] + cpp_stds += ['c++14'] if version_compare(self.version, '>=1.23.00'): - cpp_stds += ['c++1y', 'gnu++1y'] + cpp_stds += ['c++1y'] if version_compare(self.version, '>=1.24.00'): - cpp_stds += ['c++1z', 'c++17', 'gnu++1z', 'gnu++17'] + cpp_stds += ['c++1z', 'c++17'] if version_compare(self.version, '>=1.25.00'): - cpp_stds += ['c++2a', 'gnu++2a'] + cpp_stds += ['c++2a'] if version_compare(self.version, '>=1.26.00'): - cpp_stds += ['c++20', 'gnu++20'] + cpp_stds += ['c++20'] key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ @@ -501,7 +540,9 @@ class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler): False, ), }) - opts[key].choices = cpp_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cpp_stds, gnu=True) return opts # Elbrus C++ compiler does not have lchmod, but there is only linker warning, not compiler error. @@ -518,7 +559,7 @@ class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler): # Elbrus C++ compiler does not support RTTI, so don't check for it. def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -575,11 +616,13 @@ class IntelCPPCompiler(IntelGnuLikeCompiler, CPPCompiler): key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True), key.evolve('debugstl'): coredata.UserBooleanOption('STL debug mode', False), }) - opts[key].choices = ['none'] + c_stds + g_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(c_stds + g_stds) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -642,11 +685,13 @@ class VisualStudioLikeCPPCompilerMixin(CompilerMixinBase): msvc_winlibs, ), }) - opts[key.evolve('std')].choices = cpp_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cpp_stds) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) eh = options[key.evolve('eh')] @@ -690,7 +735,8 @@ class CPP11AsCPP14Mixin(CompilerMixinBase): key = OptionKey('std', machine=self.for_machine, lang=self.language) if options[key].value in {'vc++11', 'c++11'}: mlog.warning(self.id, 'does not support C++11;', - 'attempting best effort; setting the standard to C++14', once=True) + 'attempting best effort; setting the standard to C++14', + once=True, fatal=False) # Don't mutate anything we're going to change, we need to use # deepcopy since we're messing with members, and we can't simply # copy the members because the option proxy doesn't support it. @@ -715,6 +761,12 @@ class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixi info, exe_wrapper, linker=linker, full_version=full_version) MSVCCompiler.__init__(self, target) + # By default, MSVC has a broken __cplusplus define that pretends to be c++98: + # https://docs.microsoft.com/en-us/cpp/build/reference/zc-cplusplus?view=msvc-160 + # Pass the flag to enable a truthful define, if possible. + if version_compare(self.version, '>= 19.14.26428'): + self.always_args = self.always_args + ['/Zc:__cplusplus'] + def get_options(self) -> 'MutableKeyedOptionDictType': cpp_stds = ['none', 'c++11', 'vc++11'] # Visual Studio 2015 and later @@ -730,7 +782,7 @@ class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixi def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: key = OptionKey('std', machine=self.for_machine, lang=self.language) if options[key].value != 'none' and version_compare(self.version, '<19.00.24210'): - mlog.warning('This version of MSVC does not support cpp_std arguments') + mlog.warning('This version of MSVC does not support cpp_std arguments', fatal=False) options = copy.copy(options) options[key].value = 'none' @@ -744,16 +796,6 @@ class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixi del args[i] return args - def get_always_args(self) -> T.List[str]: - args = super().get_always_args() - - # By default, MSVC has a broken __cplusplus define that pretends to be c++98: - # https://docs.microsoft.com/en-us/cpp/build/reference/zc-cplusplus?view=msvc-160 - # Pass the flag to enable a truthful define, if possible. - if version_compare(self.version, '>= 19.14.26428') and '/Zc:__cplusplus' not in args: - return args + ['/Zc:__cplusplus'] - return args - class ClangClCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, ClangClCompiler, CPPCompiler): id = 'clang-cl' @@ -768,7 +810,7 @@ class ClangClCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, Cl ClangClCompiler.__init__(self, target) def get_options(self) -> 'MutableKeyedOptionDictType': - cpp_stds = ['none', 'c++11', 'vc++11', 'c++14', 'vc++14', 'c++17', 'vc++17', 'c++latest'] + cpp_stds = ['none', 'c++11', 'vc++11', 'c++14', 'vc++14', 'c++17', 'vc++17', 'c++20', 'vc++20', 'c++latest'] return self._get_options_impl(super().get_options(), cpp_stds) @@ -809,12 +851,13 @@ class ArmCPPCompiler(ArmCompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c++03', 'c++11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++03', 'c++11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value == 'c++11': @@ -849,8 +892,8 @@ class CcrxCPPCompiler(CcrxCompiler, CPPCompiler): def get_compile_only_args(self) -> T.List[str]: return [] - def get_output_args(self, target: str) -> T.List[str]: - return [f'-output=obj={target}'] + def get_output_args(self, outputname: str) -> T.List[str]: + return [f'-output=obj={outputname}'] def get_option_link_args(self, options: 'KeyedOptionDictType') -> T.List[str]: return [] @@ -869,12 +912,13 @@ class TICPPCompiler(TICompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c++03'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++03']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -890,3 +934,60 @@ class TICPPCompiler(TICompiler, CPPCompiler): class C2000CPPCompiler(TICPPCompiler): # Required for backwards compat with projects created before ti-cgt support existed id = 'c2000' + +class MetrowerksCPPCompilerARM(MetrowerksCompiler, CPPCompiler): + id = 'mwccarm' + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, + is_cross: bool, info: 'MachineInfo', + exe_wrapper: T.Optional['ExternalProgram'] = None, + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None): + CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, + info, exe_wrapper, linker=linker, full_version=full_version) + MetrowerksCompiler.__init__(self) + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwccarm_instruction_set_args.get(instruction_set, None) + + def get_options(self) -> 'MutableKeyedOptionDictType': + opts = CPPCompiler.get_options(self) + key = OptionKey('std', machine=self.for_machine, lang=self.language) + opts[key].choices = ['none'] + return opts + + def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + args: T.List[str] = [] + std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] + if std.value != 'none': + args.append('-lang') + args.append(std.value) + return args + +class MetrowerksCPPCompilerEmbeddedPowerPC(MetrowerksCompiler, CPPCompiler): + id = 'mwcceppc' + + def __init__(self, ccache: T.List[str], exelist: T.List[str], version: str, for_machine: MachineChoice, + is_cross: bool, info: 'MachineInfo', + exe_wrapper: T.Optional['ExternalProgram'] = None, + linker: T.Optional['DynamicLinker'] = None, + full_version: T.Optional[str] = None): + CPPCompiler.__init__(self, ccache, exelist, version, for_machine, is_cross, + info, exe_wrapper, linker=linker, full_version=full_version) + MetrowerksCompiler.__init__(self) + + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: + return mwcceppc_instruction_set_args.get(instruction_set, None) + + def get_options(self) -> 'MutableKeyedOptionDictType': + opts = CPPCompiler.get_options(self) + key = OptionKey('std', machine=self.for_machine, lang=self.language) + opts[key].choices = ['none'] + return opts + + def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + args: T.List[str] = [] + std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] + if std.value != 'none': + args.append('-lang ' + std.value) + return args diff --git a/mesonbuild/compilers/cs.py b/mesonbuild/compilers/cs.py index 14fcfd7..cd99c81 100644 --- a/mesonbuild/compilers/cs.py +++ b/mesonbuild/compilers/cs.py @@ -28,7 +28,7 @@ if T.TYPE_CHECKING: from ..environment import Environment from ..mesonlib import MachineChoice -cs_optimization_args = { +cs_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': [], 'g': [], @@ -36,7 +36,7 @@ cs_optimization_args = { '2': ['-optimize+'], '3': ['-optimize+'], 's': ['-optimize+'], - } # type: T.Dict[str, T.List[str]] + } class CsCompiler(BasicLinkerIsCompilerMixin, Compiler): @@ -100,7 +100,7 @@ class CsCompiler(BasicLinkerIsCompilerMixin, Compiler): pc = subprocess.Popen(self.exelist + self.get_always_args() + [src], cwd=work_dir) pc.wait() if pc.returncode != 0: - raise EnvironmentException('C# compiler %s can not compile programs.' % self.name_string()) + raise EnvironmentException('C# compiler %s cannot compile programs.' % self.name_string()) if self.runner: cmdlist = [self.runner, obj] else: diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py index 25a7baf..09a7d6a 100644 --- a/mesonbuild/compilers/cuda.py +++ b/mesonbuild/compilers/cuda.py @@ -21,8 +21,8 @@ import typing as T from .. import coredata from .. import mlog from ..mesonlib import ( - EnvironmentException, Popen_safe, OptionOverrideProxy, - is_windows, LibType, OptionKey, + EnvironmentException, Popen_safe, + is_windows, LibType, OptionKey, version_compare, ) from .compilers import (Compiler, cuda_buildtype_args, cuda_optimization_args, cuda_debug_args) @@ -34,7 +34,7 @@ if T.TYPE_CHECKING: from ..dependencies import Dependency from ..environment import Environment # noqa: F401 from ..envconfig import MachineInfo - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..programs import ExternalProgram @@ -557,7 +557,7 @@ class CudaCompiler(Compiler): mlog.debug(stde) mlog.debug('-----') if pc.returncode != 0: - raise EnvironmentException(f'Compiler {self.name_string()} can not compile programs.') + raise EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.') # Run sanity check (if possible) if self.is_cross: @@ -615,13 +615,26 @@ class CudaCompiler(Compiler): }}''' return self.compiles(t.format_map(fargs), env, extra_args=extra_args, dependencies=dependencies) + _CPP14_VERSION = '>=9.0' + _CPP17_VERSION = '>=11.0' + _CPP20_VERSION = '>=12.0' + def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() std_key = OptionKey('std', machine=self.for_machine, lang=self.language) ccbindir_key = OptionKey('ccbindir', machine=self.for_machine, lang=self.language) + + cpp_stds = ['none', 'c++03', 'c++11'] + if version_compare(self.version, self._CPP14_VERSION): + cpp_stds += ['c++14'] + if version_compare(self.version, self._CPP17_VERSION): + cpp_stds += ['c++17'] + if version_compare(self.version, self._CPP20_VERSION): + cpp_stds += ['c++20'] + opts.update({ std_key: coredata.UserComboOption('C++ language standard to use with CUDA', - ['none', 'c++03', 'c++11', 'c++14', 'c++17'], 'none'), + cpp_stds, 'none'), ccbindir_key: coredata.UserStringOption('CUDA non-default toolchain directory to use (-ccbin)', ''), }) @@ -637,7 +650,7 @@ class CudaCompiler(Compiler): host_options = {key: options.get(key, opt) for key, opt in self.host_compiler.get_options().items()} std_key = OptionKey('std', machine=self.for_machine, lang=self.host_compiler.language) overrides = {std_key: 'none'} - return OptionOverrideProxy(overrides, host_options) + return coredata.OptionsView(host_options, overrides=overrides) def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: args = self.get_ccbin_args(options) @@ -736,7 +749,7 @@ class CudaCompiler(Compiler): return self._to_host_flags(self.host_compiler.get_std_exe_link_args(), _Phase.LINKER) def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: return ['-l' + libname] # FIXME def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]: @@ -774,5 +787,5 @@ class CudaCompiler(Compiler): def get_profile_use_args(self) -> T.List[str]: return ['-Xcompiler=' + x for x in self.host_compiler.get_profile_use_args()] - def get_disable_assert_args(self) -> T.List[str]: - return self.host_compiler.get_disable_assert_args() + def get_assert_args(self, disable: bool) -> T.List[str]: + return self.host_compiler.get_assert_args(disable) diff --git a/mesonbuild/compilers/cython.py b/mesonbuild/compilers/cython.py index ac5c934..9bbfebe 100644 --- a/mesonbuild/compilers/cython.py +++ b/mesonbuild/compilers/cython.py @@ -1,12 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright © 2021 Intel Corporation +from __future__ import annotations """Abstraction for Cython language compilers.""" import typing as T from .. import coredata -from ..mesonlib import EnvironmentException, OptionKey +from ..mesonlib import EnvironmentException, OptionKey, version_compare from .compilers import Compiler if T.TYPE_CHECKING: @@ -39,6 +40,14 @@ class CythonCompiler(Compiler): # compiler might though return [] + def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: + if version_compare(self.version, '>=0.29.33'): + return ['-M'] + return [] + + def get_depfile_suffix(self) -> str: + return 'dep' + def sanity_check(self, work_dir: str, environment: 'Environment') -> None: code = 'print("hello world")' with self.cached_compile(code, environment.coredata) as p: diff --git a/mesonbuild/compilers/d.py b/mesonbuild/compilers/d.py index 90c0498..d8a72fd 100644 --- a/mesonbuild/compilers/d.py +++ b/mesonbuild/compilers/d.py @@ -39,51 +39,58 @@ from .mixins.gnu import GnuCompiler from .mixins.gnu import gnu_common_warning_args if T.TYPE_CHECKING: + from ..build import DFeatures from ..dependencies import Dependency from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice CompilerMixinBase = Compiler else: CompilerMixinBase = object -d_feature_args = {'gcc': {'unittest': '-funittest', - 'debug': '-fdebug', - 'version': '-fversion', - 'import_dir': '-J' - }, - 'llvm': {'unittest': '-unittest', - 'debug': '-d-debug', - 'version': '-d-version', - 'import_dir': '-J' - }, - 'dmd': {'unittest': '-unittest', - 'debug': '-debug', - 'version': '-version', - 'import_dir': '-J' - } - } # type: T.Dict[str, T.Dict[str, str]] - -ldc_optimization_args = {'plain': [], - '0': [], - 'g': [], - '1': ['-O1'], - '2': ['-O2'], - '3': ['-O3'], - 's': ['-Oz'], - } # type: T.Dict[str, T.List[str]] - -dmd_optimization_args = {'plain': [], - '0': [], - 'g': [], - '1': ['-O'], - '2': ['-O'], - '3': ['-O'], - 's': ['-O'], - } # type: T.Dict[str, T.List[str]] +d_feature_args: T.Dict[str, T.Dict[str, str]] = { + 'gcc': { + 'unittest': '-funittest', + 'debug': '-fdebug', + 'version': '-fversion', + 'import_dir': '-J' + }, + 'llvm': { + 'unittest': '-unittest', + 'debug': '-d-debug', + 'version': '-d-version', + 'import_dir': '-J' + }, + 'dmd': { + 'unittest': '-unittest', + 'debug': '-debug', + 'version': '-version', + 'import_dir': '-J' + } +} + +ldc_optimization_args: T.Dict[str, T.List[str]] = { + 'plain': [], + '0': [], + 'g': [], + '1': ['-O1'], + '2': ['-O2'], + '3': ['-O3'], + 's': ['-Oz'], +} + +dmd_optimization_args: T.Dict[str, T.List[str]] = { + 'plain': [], + '0': [], + 'g': [], + '1': ['-O'], + '2': ['-O'], + '3': ['-O'], + 's': ['-O'], +} class DmdLikeCompilerMixin(CompilerMixinBase): @@ -102,7 +109,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): self._dmd_has_depfile = version_compare(dmd_frontend_version, ">=2.095.0") if T.TYPE_CHECKING: - mscrt_args = {} # type: T.Dict[str, T.List[str]] + mscrt_args: T.Dict[str, T.List[str]] = {} def _get_target_arch_args(self) -> T.List[str]: ... @@ -164,94 +171,6 @@ class DmdLikeCompilerMixin(CompilerMixinBase): return [] return ['-fPIC'] - def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]: - # TODO: using a TypeDict here would improve this - res = [] - # get_feature_args can be called multiple times for the same target when there is generated source - # so we have to copy the kwargs (target.d_features) dict before popping from it - kwargs = kwargs.copy() - if 'unittest' in kwargs: - unittest = kwargs.pop('unittest') - unittest_arg = d_feature_args[self.id]['unittest'] - if not unittest_arg: - raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string()) - if unittest: - res.append(unittest_arg) - - if 'debug' in kwargs: - debug_level = -1 - debugs = kwargs.pop('debug') - if not isinstance(debugs, list): - debugs = [debugs] - - debug_arg = d_feature_args[self.id]['debug'] - if not debug_arg: - raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string()) - - # Parse all debug identifiers and the largest debug level identifier - for d in debugs: - if isinstance(d, int): - if d > debug_level: - debug_level = d - elif isinstance(d, str) and d.isdigit(): - if int(d) > debug_level: - debug_level = int(d) - else: - res.append(f'{debug_arg}={d}') - - if debug_level >= 0: - res.append(f'{debug_arg}={debug_level}') - - if 'versions' in kwargs: - version_level = -1 - versions = kwargs.pop('versions') - if not isinstance(versions, list): - versions = [versions] - - version_arg = d_feature_args[self.id]['version'] - if not version_arg: - raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string()) - - # Parse all version identifiers and the largest version level identifier - for v in versions: - if isinstance(v, int): - if v > version_level: - version_level = v - elif isinstance(v, str) and v.isdigit(): - if int(v) > version_level: - version_level = int(v) - else: - res.append(f'{version_arg}={v}') - - if version_level >= 0: - res.append(f'{version_arg}={version_level}') - - if 'import_dirs' in kwargs: - import_dirs = kwargs.pop('import_dirs') - if not isinstance(import_dirs, list): - import_dirs = [import_dirs] - - import_dir_arg = d_feature_args[self.id]['import_dir'] - if not import_dir_arg: - raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string()) - for idir_obj in import_dirs: - basedir = idir_obj.get_curdir() - for idir in idir_obj.get_incdirs(): - bldtreedir = os.path.join(basedir, idir) - # Avoid superfluous '/.' at the end of paths when d is '.' - if idir not in ('', '.'): - expdir = bldtreedir - else: - expdir = basedir - srctreedir = os.path.join(build_to_src, expdir) - res.append(f'{import_dir_arg}{srctreedir}') - res.append(f'{import_dir_arg}{bldtreedir}') - - if kwargs: - raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys())) - - return res - def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]: if buildtype != 'plain': return self._get_target_arch_args() @@ -271,7 +190,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): # The way that dmd and ldc pass rpath to gcc is different than we would # do directly, each argument -rpath and the value to rpath, need to be # split into two separate arguments both prefaced with the -L=. - args = [] + args: T.List[str] = [] (rpath_args, rpath_dirs_to_remove) = super().build_rpath_args( env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath) for r in rpath_args: @@ -292,7 +211,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): # can understand. # The flags might have been added by pkg-config files, # and are therefore out of the user's control. - dcargs = [] + dcargs: T.List[str] = [] # whether we hit a linker argument that expect another arg # see the comment in the "-L" section link_expect_arg = False @@ -301,7 +220,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): ] for arg in args: # Translate OS specific arguments first. - osargs = [] # type: T.List[str] + osargs: T.List[str] = [] if info.is_windows(): osargs = cls.translate_arg_to_windows(arg) elif info.is_darwin(): @@ -415,7 +334,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): @classmethod def translate_arg_to_windows(cls, arg: str) -> T.List[str]: - args = [] + args: T.List[str] = [] if arg.startswith('-Wl,'): # Translate linker arguments here. linkargs = arg[arg.index(',') + 1:].split(',') @@ -441,7 +360,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): @classmethod def _translate_arg_to_osx(cls, arg: str) -> T.List[str]: - args = [] + args: T.List[str] = [] if arg.startswith('-install_name'): args.append('-L=' + arg) return args @@ -460,31 +379,7 @@ class DmdLikeCompilerMixin(CompilerMixinBase): def _get_crt_args(self, crt_val: str, buildtype: str) -> T.List[str]: if not self.info.is_windows(): return [] - - if crt_val in self.mscrt_args: - return self.mscrt_args[crt_val] - assert crt_val in {'from_buildtype', 'static_from_buildtype'} - - dbg = 'mdd' - rel = 'md' - if crt_val == 'static_from_buildtype': - dbg = 'mtd' - rel = 'mt' - - # Match what build type flags used to do. - if buildtype == 'plain': - return [] - elif buildtype == 'debug': - return self.mscrt_args[dbg] - elif buildtype == 'debugoptimized': - return self.mscrt_args[rel] - elif buildtype == 'release': - return self.mscrt_args[rel] - elif buildtype == 'minsize': - return self.mscrt_args[rel] - else: - assert buildtype == 'custom' - raise EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".') + return self.mscrt_args[self.get_crt_val(crt_val, buildtype)] def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, suffix: str, soversion: str, @@ -494,15 +389,14 @@ class DmdLikeCompilerMixin(CompilerMixinBase): # LDC and DMD actually do use a linker, but they proxy all of that with # their own arguments + soargs: T.List[str] = [] if self.linker.id.startswith('ld.'): - soargs = [] for arg in sargs: a, b = arg.split(',', maxsplit=1) soargs.append(a) soargs.append(self.LINKER_PREFIX + b) return soargs elif self.linker.id.startswith('ld64'): - soargs = [] for arg in sargs: if not arg.startswith(self.LINKER_PREFIX): soargs.append(self.LINKER_PREFIX + arg) @@ -559,7 +453,7 @@ class DCompiler(Compiler): pc = subprocess.Popen(self.exelist + self.get_output_args(output_name) + self._get_target_arch_args() + [source_name], cwd=work_dir) pc.wait() if pc.returncode != 0: - raise EnvironmentException('D compiler %s can not compile programs.' % self.name_string()) + raise EnvironmentException('D compiler %s cannot compile programs.' % self.name_string()) if self.is_cross: if self.exe_wrapper is None: # Can't check if the binaries run so we have to assume they do @@ -581,91 +475,69 @@ class DCompiler(Compiler): return [] return ['-fPIC'] - def get_feature_args(self, kwargs: T.Dict[str, T.Any], build_to_src: str) -> T.List[str]: - # TODO: using a TypeDict here would improve this - res = [] - # get_feature_args can be called multiple times for the same target when there is generated source - # so we have to copy the kwargs (target.d_features) dict before popping from it - kwargs = kwargs.copy() - if 'unittest' in kwargs: - unittest = kwargs.pop('unittest') - unittest_arg = d_feature_args[self.id]['unittest'] - if not unittest_arg: - raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string()) - if unittest: - res.append(unittest_arg) - - if 'debug' in kwargs: - debug_level = -1 - debugs = kwargs.pop('debug') - if not isinstance(debugs, list): - debugs = [debugs] - - debug_arg = d_feature_args[self.id]['debug'] - if not debug_arg: - raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string()) - - # Parse all debug identifiers and the largest debug level identifier - for d in debugs: - if isinstance(d, int): - if d > debug_level: - debug_level = d - elif isinstance(d, str) and d.isdigit(): - if int(d) > debug_level: - debug_level = int(d) - else: - res.append(f'{debug_arg}={d}') - - if debug_level >= 0: - res.append(f'{debug_arg}={debug_level}') - - if 'versions' in kwargs: - version_level = -1 - versions = kwargs.pop('versions') - if not isinstance(versions, list): - versions = [versions] - - version_arg = d_feature_args[self.id]['version'] - if not version_arg: - raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string()) - - # Parse all version identifiers and the largest version level identifier - for v in versions: - if isinstance(v, int): - if v > version_level: - version_level = v - elif isinstance(v, str) and v.isdigit(): - if int(v) > version_level: - version_level = int(v) + def get_feature_args(self, kwargs: DFeatures, build_to_src: str) -> T.List[str]: + res: T.List[str] = [] + unittest_arg = d_feature_args[self.id]['unittest'] + if not unittest_arg: + raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string()) + if kwargs['unittest']: + res.append(unittest_arg) + + debug_level = -1 + debug_arg = d_feature_args[self.id]['debug'] + if not debug_arg: + raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string()) + + # Parse all debug identifiers and the largest debug level identifier + for d in kwargs['debug']: + if isinstance(d, int): + if d > debug_level: + debug_level = d + elif isinstance(d, str) and d.isdigit(): + if int(d) > debug_level: + debug_level = int(d) + else: + res.append(f'{debug_arg}={d}') + + if debug_level >= 0: + res.append(f'{debug_arg}={debug_level}') + + version_level = -1 + version_arg = d_feature_args[self.id]['version'] + if not version_arg: + raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string()) + + # Parse all version identifiers and the largest version level identifier + for v in kwargs['versions']: + if isinstance(v, int): + if v > version_level: + version_level = v + elif isinstance(v, str) and v.isdigit(): + if int(v) > version_level: + version_level = int(v) + else: + res.append(f'{version_arg}={v}') + + if version_level >= 0: + res.append(f'{version_arg}={version_level}') + + import_dir_arg = d_feature_args[self.id]['import_dir'] + if not import_dir_arg: + raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string()) + # TODO: ImportDirs.to_string_list(), but we need both the project source + # root and project build root for that. + for idir_obj in kwargs['import_dirs']: + basedir = idir_obj.get_curdir() + for idir in idir_obj.get_incdirs(): + bldtreedir = os.path.join(basedir, idir) + # Avoid superfluous '/.' at the end of paths when d is '.' + if idir not in ('', '.'): + expdir = bldtreedir else: - res.append(f'{version_arg}={v}') - - if version_level >= 0: - res.append(f'{version_arg}={version_level}') - - if 'import_dirs' in kwargs: - import_dirs = kwargs.pop('import_dirs') - if not isinstance(import_dirs, list): - import_dirs = [import_dirs] - - import_dir_arg = d_feature_args[self.id]['import_dir'] - if not import_dir_arg: - raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string()) - for idir_obj in import_dirs: - basedir = idir_obj.get_curdir() - for idir in idir_obj.get_incdirs(): - bldtreedir = os.path.join(basedir, idir) - # Avoid superfluous '/.' at the end of paths when d is '.' - if idir not in ('', '.'): - expdir = bldtreedir - else: - expdir = basedir - srctreedir = os.path.join(build_to_src, expdir) - res.append(f'{import_dir_arg}{srctreedir}') - res.append(f'{import_dir_arg}{bldtreedir}') - - if kwargs: - raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys())) + expdir = basedir + srctreedir = os.path.join(build_to_src, expdir) + res.append(f'{import_dir_arg}{srctreedir}') + res.append(f'{import_dir_arg}{bldtreedir}') return res @@ -713,7 +585,7 @@ class DCompiler(Compiler): if need_exe_wrapper and self.exe_wrapper is None: raise compilers.CrossNoRunException('Can not run test applications in this cross environment.') extra_args = self._get_compile_extra_args(extra_args) - with self._build_wrapper(code, env, extra_args, dependencies, mode='link', want_output=True) as p: + with self._build_wrapper(code, env, extra_args, dependencies, mode=CompileCheckMode.LINK, want_output=True) as p: if p.returncode != 0: mlog.debug(f'Could not compile test file {p.input_name}: {p.returncode}\n') return compilers.RunResult(False) @@ -735,7 +607,7 @@ class DCompiler(Compiler): def sizeof(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: if extra_args is None: extra_args = [] t = f''' @@ -745,17 +617,17 @@ class DCompiler(Compiler): writeln(({typename}).sizeof); }} ''' - res = self.run(t, env, extra_args=extra_args, - dependencies=dependencies) + res = self.cached_run(t, env, extra_args=extra_args, + dependencies=dependencies) if not res.compiled: - return -1 + return -1, False if res.returncode != 0: raise mesonlib.EnvironmentException('Could not run sizeof test binary.') - return int(res.stdout) + return int(res.stdout), res.cached def alignment(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Optional[T.List[str]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: if extra_args is None: extra_args = [] t = f''' @@ -774,7 +646,7 @@ class DCompiler(Compiler): align = int(res.stdout) if align == 0: raise mesonlib.EnvironmentException(f'Could not determine alignment of {typename}. Sorry. You might want to file a bug.') - return align + return align, res.cached def has_header(self, hname: str, prefix: str, env: 'Environment', *, extra_args: T.Union[None, T.List[str], T.Callable[['CompileCheckMode'], T.List[str]]] = None, @@ -786,7 +658,7 @@ class DCompiler(Compiler): import {hname}; ''' return self.compiles(code, env, extra_args=extra_args, - dependencies=dependencies, mode='compile', disable_cache=disable_cache) + dependencies=dependencies, mode=CompileCheckMode.COMPILE, disable_cache=disable_cache) class GnuDCompiler(GnuCompiler, DCompiler): @@ -855,8 +727,10 @@ class GnuDCompiler(GnuCompiler, DCompiler): return args return args + ['-shared-libphobos'] - def get_disable_assert_args(self) -> T.List[str]: - return ['-frelease'] + def get_assert_args(self, disable: bool) -> T.List[str]: + if disable: + return ['-frelease'] + return [] # LDC uses the DMD frontend code to parse and analyse the code. # It then uses LLVM for the binary code generation and optimizations. @@ -927,8 +801,10 @@ class LLVMDCompiler(DmdLikeCompilerMixin, DCompiler): return args return args + ['-link-defaultlib-shared'] - def get_disable_assert_args(self) -> T.List[str]: - return ['--release'] + def get_assert_args(self, disable: bool) -> T.List[str]: + if disable: + return ['--release'] + return [] def rsp_file_syntax(self) -> RSPFileSyntax: # We use `mesonlib.is_windows` here because we want to know what the @@ -1015,8 +891,10 @@ class DmdDCompiler(DmdLikeCompilerMixin, DCompiler): return args return args + ['-defaultlib=phobos2', '-debuglib=phobos2'] - def get_disable_assert_args(self) -> T.List[str]: - return ['-release'] + def get_assert_args(self, disable: bool) -> T.List[str]: + if disable: + return ['-release'] + return [] def rsp_file_syntax(self) -> RSPFileSyntax: return RSPFileSyntax.MSVC diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py index 367bcf9..0bfedd1 100644 --- a/mesonbuild/compilers/detect.py +++ b/mesonbuild/compilers/detect.py @@ -15,7 +15,7 @@ from __future__ import annotations from ..mesonlib import ( MesonException, EnvironmentException, MachineChoice, join_args, - search_version, is_windows, Popen_safe, windows_proof_rm, + search_version, is_windows, Popen_safe, Popen_safe_logged, windows_proof_rm, ) from ..envconfig import BinaryTable from .. import mlog @@ -36,7 +36,7 @@ if T.TYPE_CHECKING: from .cpp import CPPCompiler from .fortran import FortranCompiler from .rust import RustCompiler - from ..linkers import StaticLinker + from ..linkers.linkers import StaticLinker, DynamicLinker from ..environment import Environment from ..programs import ExternalProgram @@ -111,11 +111,15 @@ def compiler_from_language(env: 'Environment', lang: str, for_machine: MachineCh } return lang_map[lang](env, for_machine) if lang in lang_map else None -def detect_compiler_for(env: 'Environment', lang: str, for_machine: MachineChoice) -> T.Optional[Compiler]: +def detect_compiler_for(env: 'Environment', lang: str, for_machine: MachineChoice, skip_sanity_check: bool) -> T.Optional[Compiler]: comp = compiler_from_language(env, lang, for_machine) - if comp is not None: - assert comp.for_machine == for_machine - env.coredata.process_new_compiler(lang, comp, env) + if comp is None: + return comp + assert comp.for_machine == for_machine + env.coredata.process_new_compiler(lang, comp, env) + if not skip_sanity_check: + comp.sanity_check(env.get_scratch_dir(), env) + env.coredata.compilers[comp.for_machine][lang] = comp return comp @@ -189,6 +193,9 @@ def detect_static_linker(env: 'Environment', compiler: Compiler) -> StaticLinker trials = [['xilib']] elif is_windows() and compiler.id == 'pgi': # this handles cpp / nvidia HPC, in addition to just c/fortran trials = [['ar']] # For PGI on Windows, "ar" is just a wrapper calling link/lib. + elif is_windows() and compiler.id == 'nasm': + # This may well be LINK.EXE if it's under a MSVC environment + trials = [defaults['vs_static_linker'], defaults['clang_cl_static_linker']] + default_linkers else: trials = default_linkers popen_exceptions = {} @@ -202,7 +209,7 @@ def detect_static_linker(env: 'Environment', compiler: Compiler) -> StaticLinker else: arg = '--version' try: - p, out, err = Popen_safe(linker + [arg]) + p, out, err = Popen_safe_logged(linker + [arg], msg='Detecting linker via') except OSError as e: popen_exceptions[join_args(linker + [arg])] = e continue @@ -234,6 +241,11 @@ def detect_static_linker(env: 'Environment', compiler: Compiler) -> StaticLinker return linkers.TILinker(linker) if out.startswith('The CompCert'): return linkers.CompCertLinker(linker) + if out.strip().startswith('Metrowerks') or out.strip().startswith('Freescale'): + if 'ARM' in out: + return linkers.MetrowerksStaticLinkerARM(linker) + else: + return linkers.MetrowerksStaticLinkerEmbeddedPowerPC(linker) if p.returncode == 0: return linkers.ArLinker(compiler.for_machine, linker) if p.returncode == 1 and err.startswith('usage'): # OSX @@ -266,6 +278,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin is_cross = env.is_cross_build(for_machine) info = env.machines[for_machine] cls: T.Union[T.Type[CCompiler], T.Type[CPPCompiler]] + lnk: T.Union[T.Type[StaticLinker], T.Type[DynamicLinker]] for compiler in compilers: if isinstance(compiler, str): @@ -314,12 +327,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin cmd = compiler + [arg] try: - mlog.debug('-----') - mlog.debug(f'Detecting compiler via: {join_args(cmd)}') - p, out, err = Popen_safe(cmd) - mlog.debug(f'compiler returned {p}') - mlog.debug(f'compiler stdout:\n{out}') - mlog.debug(f'compiler stderr:\n{err}') + p, out, err = Popen_safe_logged(cmd, msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(cmd)] = e continue @@ -331,7 +339,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin version = search_version(out) guess_gcc_or_lcc: T.Optional[str] = None - if 'Free Software Foundation' in out or 'xt-' in out: + if 'Free Software Foundation' in out or out.startswith('xt-'): guess_gcc_or_lcc = 'gcc' if 'e2k' in out and 'lcc' in out: guess_gcc_or_lcc = 'lcc' @@ -365,8 +373,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin # emcc requires a file input in order to pass arguments to the # linker. It'll exit with an error code, but still print the - # linker version. Old emcc versions ignore -Wl,--version completely, - # however. We'll report "unknown version" in that case. + # linker version. with tempfile.NamedTemporaryFile(suffix='.c') as f: cmd = compiler + [cls.LINKER_PREFIX + "--version", f.name] _, o, _ = Popen_safe(cmd) @@ -380,7 +387,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin if 'Arm C/C++/Fortran Compiler' in out: arm_ver_match = re.search(r'version (\d+)\.(\d+)\.?(\d+)? \(build number (\d+)\)', out) - assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None + assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaining that this could be None version = '.'.join([x for x in arm_ver_match.groups() if x is not None]) if lang == 'c': cls = c.ArmLtdClangCCompiler @@ -527,7 +534,6 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin ccache, compiler, version, for_machine, is_cross, info, exe_wrap, full_version=full_version, linker=l) if 'TMS320C2000 C/C++' in out or 'MSP430 C/C++' in out or 'TI ARM C/C++ Compiler' in out: - lnk: T.Union[T.Type[linkers.C2000DynamicLinker], T.Type[linkers.TIDynamicLinker]] if 'TMS320C2000 C/C++' in out: cls = c.C2000CCompiler if lang == 'c' else cpp.C2000CPPCompiler lnk = linkers.C2000DynamicLinker @@ -540,7 +546,7 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin return cls( ccache, compiler, version, for_machine, is_cross, info, exe_wrap, full_version=full_version, linker=linker) - if 'ARM' in out: + if 'ARM' in out and not ('Metrowerks' in out or 'Freescale' in out): cls = c.ArmCCompiler if lang == 'c' else cpp.ArmCPPCompiler env.coredata.add_lang_args(cls.language, cls, for_machine, env) linker = linkers.ArmDynamicLinker(for_machine, version=version) @@ -571,6 +577,36 @@ def _detect_c_or_cpp_compiler(env: 'Environment', lang: str, for_machine: Machin ccache, compiler, version, for_machine, is_cross, info, exe_wrap, full_version=full_version, linker=linker) + if 'Metrowerks C/C++' in out or 'Freescale C/C++' in out: + if 'ARM' in out: + cls = c.MetrowerksCCompilerARM if lang == 'c' else cpp.MetrowerksCPPCompilerARM + lnk = linkers.MetrowerksLinkerARM + else: + cls = c.MetrowerksCCompilerEmbeddedPowerPC if lang == 'c' else cpp.MetrowerksCPPCompilerEmbeddedPowerPC + lnk = linkers.MetrowerksLinkerEmbeddedPowerPC + + mwcc_ver_match = re.search(r'Version (\d+)\.(\d+)\.?(\d+)? build (\d+)', out) + assert mwcc_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None + compiler_version = '.'.join(x for x in mwcc_ver_match.groups() if x is not None) + + env.coredata.add_lang_args(cls.language, cls, for_machine, env) + ld = env.lookup_binary_entry(for_machine, cls.language + '_ld') + + if ld is not None: + _, o_ld, _ = Popen_safe(ld + ['--version']) + + mwld_ver_match = re.search(r'Version (\d+)\.(\d+)\.?(\d+)? build (\d+)', o_ld) + assert mwld_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None + linker_version = '.'.join(x for x in mwld_ver_match.groups() if x is not None) + + linker = lnk(ld, for_machine, version=linker_version) + else: + raise EnvironmentException(f'Failed to detect linker for {cls.id!r} compiler. Please update your cross file(s).') + + return cls( + ccache, compiler, compiler_version, for_machine, is_cross, info, + exe_wrap, full_version=full_version, linker=linker) + _handle_exceptions(popen_exceptions, compilers) raise EnvironmentException(f'Unknown compiler {compilers}') @@ -590,7 +626,7 @@ def detect_cuda_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp for compiler in compilers: arg = '--version' try: - p, out, err = Popen_safe(compiler + [arg]) + p, out, err = Popen_safe_logged(compiler + [arg], msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(compiler + [arg])] = e continue @@ -628,7 +664,7 @@ def detect_fortran_compiler(env: 'Environment', for_machine: MachineChoice) -> C for compiler in compilers: for arg in ['--version', '-V']: try: - p, out, err = Popen_safe(compiler + [arg]) + p, out, err = Popen_safe_logged(compiler + [arg], msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(compiler + [arg])] = e continue @@ -665,7 +701,7 @@ def detect_fortran_compiler(env: 'Environment', for_machine: MachineChoice) -> C if 'Arm C/C++/Fortran Compiler' in out: cls = fortran.ArmLtdFlangFortranCompiler arm_ver_match = re.search(r'version (\d+)\.(\d+)\.?(\d+)? \(build number (\d+)\)', out) - assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaning that this could be None + assert arm_ver_match is not None, 'for mypy' # because mypy *should* be complaining that this could be None version = '.'.join([x for x in arm_ver_match.groups() if x is not None]) linker = guess_nix_linker(env, compiler, cls, version, for_machine) return cls( @@ -713,7 +749,7 @@ def detect_fortran_compiler(env: 'Environment', for_machine: MachineChoice) -> C compiler, version, for_machine, is_cross, info, exe_wrap, full_version=full_version, linker=linker) - if 'ifx (IFORT)' in out: + if 'ifx (IFORT)' in out or 'ifx (IFX)' in out: cls = fortran.IntelLLVMFortranCompiler linker = guess_nix_linker(env, compiler, cls, version, for_machine) return cls( @@ -791,7 +827,7 @@ def _detect_objc_or_objcpp_compiler(env: 'Environment', lang: str, for_machine: for compiler in compilers: arg = ['--version'] try: - p, out, err = Popen_safe(compiler + arg) + p, out, err = Popen_safe_logged(compiler + arg, msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(compiler + arg)] = e continue @@ -841,7 +877,7 @@ def detect_java_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp exelist = [defaults['java'][0]] try: - p, out, err = Popen_safe(exelist + ['-version']) + p, out, err = Popen_safe_logged(exelist + ['-version'], msg='Detecting compiler via') except OSError: raise EnvironmentException('Could not execute Java compiler: {}'.format(join_args(exelist))) if 'javac' in out or 'javac' in err: @@ -862,7 +898,7 @@ def detect_cs_compiler(env: 'Environment', for_machine: MachineChoice) -> Compil info = env.machines[for_machine] for comp in compilers: try: - p, out, err = Popen_safe(comp + ['--version']) + p, out, err = Popen_safe_logged(comp + ['--version'], msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(comp + ['--version'])] = e continue @@ -891,13 +927,19 @@ def detect_cython_compiler(env: 'Environment', for_machine: MachineChoice) -> Co popen_exceptions: T.Dict[str, Exception] = {} for comp in compilers: try: - err = Popen_safe(comp + ['-V'])[2] + _, out, err = Popen_safe_logged(comp + ['-V'], msg='Detecting compiler via') except OSError as e: popen_exceptions[join_args(comp + ['-V'])] = e continue - version = search_version(err) - if 'Cython' in err: + version: T.Optional[str] = None + # 3.0 + if 'Cython' in out: + version = search_version(out) + # older + elif 'Cython' in err: + version = search_version(err) + if version is not None: comp_class = CythonCompiler env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env) return comp_class([], comp, version, for_machine, info, is_cross=is_cross) @@ -914,7 +956,7 @@ def detect_vala_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp exelist = [defaults['vala'][0]] try: - p, out = Popen_safe(exelist + ['--version'])[0:2] + p, out = Popen_safe_logged(exelist + ['--version'], msg='Detecting compiler via')[0:2] except OSError: raise EnvironmentException('Could not execute Vala compiler: {}'.format(join_args(exelist))) version = search_version(out) @@ -927,7 +969,7 @@ def detect_vala_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> RustCompiler: from . import rust from ..linkers import linkers - popen_exceptions = {} # type: T.Dict[str, Exception] + popen_exceptions: T.Dict[str, Exception] = {} compilers, _, exe_wrap = _get_compilers(env, 'rust', for_machine) is_cross = env.is_cross_build(for_machine) info = env.machines[for_machine] @@ -939,7 +981,7 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust for compiler in compilers: arg = ['--version'] try: - out = Popen_safe(compiler + arg)[1] + out = Popen_safe_logged(compiler + arg, msg='Detecting compiler via')[1] except OSError as e: popen_exceptions[join_args(compiler + arg)] = e continue @@ -950,7 +992,18 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust # Clippy is a wrapper around rustc, but it doesn't have rustc in it's # output. We can otherwise treat it as rustc. if 'clippy' in out: - out = 'rustc' + # clippy returns its own version and not the rustc version by + # default so try harder here to get the correct version. + # Also replace the whole output with the rustc output in + # case this is later used for other purposes. + arg = ['--rustc', '--version'] + try: + out = Popen_safe(compiler + arg)[1] + except OSError as e: + popen_exceptions[join_args(compiler + arg)] = e + continue + version = search_version(out) + cls = rust.ClippyRustCompiler if 'rustc' in out: @@ -967,7 +1020,7 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust mlog.warning( 'Please do not put -C linker= in your compiler ' 'command, set rust_ld=command in your cross file ' - 'or use the RUST_LD environment variable, otherwise meson ' + 'or use the RUSTC_LD environment variable, otherwise meson ' 'will override your selection.') compiler = compiler.copy() # avoid mutating the original list @@ -981,7 +1034,7 @@ def detect_rust_compiler(env: 'Environment', for_machine: MachineChoice) -> Rust extra_args['machine'] = cc.linker.machine else: exelist = cc.linker.exelist + cc.linker.get_always_args() - if 'ccache' in exelist[0]: + if os.path.basename(exelist[0]) in {'ccache', 'sccache'}: del exelist[0] c = exelist.pop(0) compiler.extend(cls.use_linker_args(c, '')) @@ -1071,7 +1124,7 @@ def detect_d_compiler(env: 'Environment', for_machine: MachineChoice) -> Compile if 'LLVM D compiler' in out: cls = d.LLVMDCompiler # LDC seems to require a file - # We cannot use NamedTemproraryFile on windows, its documented + # We cannot use NamedTemporaryFile on windows, its documented # to not work for our uses. So, just use mkstemp and only have # one path for simplicity. o, f = tempfile.mkstemp('.d') @@ -1109,7 +1162,7 @@ def detect_d_compiler(env: 'Environment', for_machine: MachineChoice) -> Compile elif 'The D Language Foundation' in out or 'Digital Mars' in out: cls = d.DmdDCompiler # DMD seems to require a file - # We cannot use NamedTemproraryFile on windows, its documented + # We cannot use NamedTemporaryFile on windows, its documented # to not work for our uses. So, just use mkstemp and only have # one path for simplicity. o, f = tempfile.mkstemp('.d') @@ -1151,7 +1204,7 @@ def detect_swift_compiler(env: 'Environment', for_machine: MachineChoice) -> Com exelist = [defaults['swift'][0]] try: - p, _, err = Popen_safe(exelist + ['-v']) + p, _, err = Popen_safe_logged(exelist + ['-v'], msg='Detecting compiler via') except OSError: raise EnvironmentException('Could not execute Swift compiler: {}'.format(join_args(exelist))) version = search_version(err) @@ -1168,7 +1221,7 @@ def detect_swift_compiler(env: 'Environment', for_machine: MachineChoice) -> Com raise EnvironmentException('Unknown compiler: ' + join_args(exelist)) def detect_nasm_compiler(env: 'Environment', for_machine: MachineChoice) -> Compiler: - from .asm import NasmCompiler, YasmCompiler + from .asm import NasmCompiler, YasmCompiler, MetrowerksAsmCompilerARM, MetrowerksAsmCompilerEmbeddedPowerPC compilers, _, _ = _get_compilers(env, 'nasm', for_machine) is_cross = env.is_cross_build(for_machine) @@ -1187,7 +1240,7 @@ def detect_nasm_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp default_path = os.path.join(os.environ['ProgramFiles'], 'NASM') comp[0] = shutil.which(comp[0], path=default_path) or comp[0] try: - output = Popen_safe(comp + ['--version'])[1] + output = Popen_safe_logged(comp + ['--version'], msg='Detecting compiler via')[1] except OSError as e: popen_exceptions[' '.join(comp + ['--version'])] = e continue @@ -1201,6 +1254,16 @@ def detect_nasm_compiler(env: 'Environment', for_machine: MachineChoice) -> Comp comp_class = YasmCompiler env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env) return comp_class([], comp, version, for_machine, info, cc.linker, is_cross=is_cross) + elif 'Metrowerks' in output or 'Freescale' in output: + if 'ARM' in output: + comp_class_mwasmarm = MetrowerksAsmCompilerARM + env.coredata.add_lang_args(comp_class_mwasmarm.language, comp_class_mwasmarm, for_machine, env) + return comp_class_mwasmarm([], comp, version, for_machine, info, cc.linker, is_cross=is_cross) + else: + comp_class_mwasmeppc = MetrowerksAsmCompilerEmbeddedPowerPC + env.coredata.add_lang_args(comp_class_mwasmeppc.language, comp_class_mwasmeppc, for_machine, env) + return comp_class_mwasmeppc([], comp, version, for_machine, info, cc.linker, is_cross=is_cross) + _handle_exceptions(popen_exceptions, compilers) raise EnvironmentException('Unreachable code (exception to make mypy happy)') diff --git a/mesonbuild/compilers/fortran.py b/mesonbuild/compilers/fortran.py index 90ca010..a80fdff 100644 --- a/mesonbuild/compilers/fortran.py +++ b/mesonbuild/compilers/fortran.py @@ -20,6 +20,7 @@ from .. import coredata from .compilers import ( clike_debug_args, Compiler, + CompileCheckMode, ) from .mixins.clike import CLikeCompiler from .mixins.gnu import ( @@ -40,10 +41,9 @@ if T.TYPE_CHECKING: from ..dependencies import Dependency from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..programs import ExternalProgram - from .compilers import CompileCheckMode class FortranCompiler(CLikeCompiler, Compiler): @@ -117,9 +117,9 @@ class FortranCompiler(CLikeCompiler, Compiler): return filename def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: code = 'stop; end program' - return self._find_library_impl(libname, env, extra_dirs, code, libtype) + return self._find_library_impl(libname, env, extra_dirs, code, libtype, lib_prefix_warning) def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: return self._has_multi_arguments(args, env, 'stop; end program') @@ -170,7 +170,7 @@ class GnuFortranCompiler(GnuCompiler, FortranCompiler): return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] if std.value != 'none': @@ -207,7 +207,7 @@ class GnuFortranCompiler(GnuCompiler, FortranCompiler): ''' code = f'{prefix}\n#include <{hname}>' return self.compiles(code, env, extra_args=extra_args, - dependencies=dependencies, mode='preprocess', disable_cache=disable_cache) + dependencies=dependencies, mode=CompileCheckMode.PREPROCESS, disable_cache=disable_cache) class ElbrusFortranCompiler(ElbrusCompiler, FortranCompiler): @@ -311,7 +311,7 @@ class IntelFortranCompiler(IntelGnuLikeCompiler, FortranCompiler): return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] stds = {'legacy': 'none', 'f95': 'f95', 'f2003': 'f03', 'f2008': 'f08', 'f2018': 'f18'} @@ -364,7 +364,7 @@ class IntelClFortranCompiler(IntelVisualStudioLikeCompiler, FortranCompiler): return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) std = options[key] stds = {'legacy': 'none', 'f95': 'f95', 'f2003': 'f03', 'f2008': 'f08', 'f2018': 'f18'} diff --git a/mesonbuild/compilers/java.py b/mesonbuild/compilers/java.py index ebae509..9f508d6 100644 --- a/mesonbuild/compilers/java.py +++ b/mesonbuild/compilers/java.py @@ -97,10 +97,10 @@ class JavaCompiler(BasicLinkerIsCompilerMixin, Compiler): pc = subprocess.Popen(self.exelist + [src], cwd=work_dir) pc.wait() if pc.returncode != 0: - raise EnvironmentException(f'Java compiler {self.name_string()} can not compile programs.') + raise EnvironmentException(f'Java compiler {self.name_string()} cannot compile programs.') runner = shutil.which(self.javarunner) if runner: - cmdlist = [runner, obj] + cmdlist = [runner, '-cp', '.', obj] pe = subprocess.Popen(cmdlist, cwd=work_dir) pe.wait() if pe.returncode != 0: diff --git a/mesonbuild/compilers/mixins/arm.py b/mesonbuild/compilers/mixins/arm.py index f0cbf59..3abc0c8 100644 --- a/mesonbuild/compilers/mixins/arm.py +++ b/mesonbuild/compilers/mixins/arm.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Representations specific to the arm family of compilers.""" @@ -18,7 +19,7 @@ import os import typing as T from ... import mesonlib -from ...linkers import ArmClangDynamicLinker +from ...linkers.linkers import ArmClangDynamicLinker from ...mesonlib import OptionKey from ..compilers import clike_debug_args from .clang import clang_color_args @@ -33,16 +34,16 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -arm_buildtype_args = { +arm_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -arm_optimization_args = { +arm_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-g'], @@ -50,18 +51,18 @@ arm_optimization_args = { '2': [], # Compiler defaults to -O2 '3': ['-O3', '-Otime'], 's': ['-O3'], # Compiler defaults to -Ospace -} # type: T.Dict[str, T.List[str]] +} -armclang_buildtype_args = { +armclang_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -armclang_optimization_args = { +armclang_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': [], # Compiler defaults to -O0 'g': ['-g'], @@ -69,7 +70,7 @@ armclang_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-Oz'] -} # type: T.Dict[str, T.List[str]] +} class ArmCompiler(Compiler): @@ -81,14 +82,15 @@ class ArmCompiler(Compiler): def __init__(self) -> None: if not self.is_cross: raise mesonlib.EnvironmentException('armcc supports only cross-compilation.') - default_warn_args = [] # type: T.List[str] + default_warn_args: T.List[str] = [] self.warn_args = {'0': [], '1': default_warn_args, '2': default_warn_args + [], '3': default_warn_args + [], - 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]] + 'everything': default_warn_args + []} # Assembly self.can_compile_suffixes.add('s') + self.can_compile_suffixes.add('sx') def get_pic_args(self) -> T.List[str]: # FIXME: Add /ropi, /rwpi, /fpic etc. qualifiers to --apcs @@ -160,6 +162,7 @@ class ArmclangCompiler(Compiler): 'b_ndebug', 'b_staticpic', 'b_colorout']} # Assembly self.can_compile_suffixes.add('s') + self.can_compile_suffixes.add('sx') def get_pic_args(self) -> T.List[str]: # PIC support is not enabled by default for ARM, diff --git a/mesonbuild/compilers/mixins/ccrx.py b/mesonbuild/compilers/mixins/ccrx.py index 1c22214..71c1033 100644 --- a/mesonbuild/compilers/mixins/ccrx.py +++ b/mesonbuild/compilers/mixins/ccrx.py @@ -31,35 +31,35 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -ccrx_buildtype_args = { +ccrx_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -ccrx_optimization_args = { +ccrx_optimization_args: T.Dict[str, T.List[str]] = { '0': ['-optimize=0'], 'g': ['-optimize=0'], '1': ['-optimize=1'], '2': ['-optimize=2'], '3': ['-optimize=max'], 's': ['-optimize=2', '-size'] -} # type: T.Dict[str, T.List[str]] +} -ccrx_debug_args = { +ccrx_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: ['-debug'] -} # type: T.Dict[bool, T.List[str]] +} class CcrxCompiler(Compiler): if T.TYPE_CHECKING: is_cross = True - can_compile_suffixes = set() # type: T.Set[str] + can_compile_suffixes: T.Set[str] = set() id = 'ccrx' @@ -68,12 +68,13 @@ class CcrxCompiler(Compiler): raise EnvironmentException('ccrx supports only cross-compilation.') # Assembly self.can_compile_suffixes.add('src') - default_warn_args = [] # type: T.List[str] - self.warn_args = {'0': [], - '1': default_warn_args, - '2': default_warn_args + [], - '3': default_warn_args + [], - 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]] + default_warn_args: T.List[str] = [] + self.warn_args: T.Dict[str, T.List[str]] = { + '0': [], + '1': default_warn_args, + '2': default_warn_args + [], + '3': default_warn_args + [], + 'everything': default_warn_args + []} def get_pic_args(self) -> T.List[str]: # PIC support is not enabled by default for CCRX, @@ -109,7 +110,7 @@ class CcrxCompiler(Compiler): @classmethod def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]: - result = [] + result: T.List[str] = [] for i in args: if i.startswith('-D'): i = '-define=' + i[2:] diff --git a/mesonbuild/compilers/mixins/clang.py b/mesonbuild/compilers/mixins/clang.py index 35de264..6a9c79b 100644 --- a/mesonbuild/compilers/mixins/clang.py +++ b/mesonbuild/compilers/mixins/clang.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Abstractions for the LLVM/Clang compiler family.""" @@ -19,7 +20,7 @@ import shutil import typing as T from ... import mesonlib -from ...linkers import AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker, \ +from ...linkers.linkers import AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker, \ MoldDynamicLinker from ...mesonlib import OptionKey from ..compilers import CompileCheckMode @@ -29,13 +30,13 @@ if T.TYPE_CHECKING: from ...environment import Environment from ...dependencies import Dependency # noqa: F401 -clang_color_args = { - 'auto': ['-fcolor-diagnostics'], - 'always': ['-fcolor-diagnostics'], - 'never': ['-fno-color-diagnostics'], -} # type: T.Dict[str, T.List[str]] +clang_color_args: T.Dict[str, T.List[str]] = { + 'auto': ['-fdiagnostics-color=auto'], + 'always': ['-fdiagnostics-color=always'], + 'never': ['-fdiagnostics-color=never'], +} -clang_optimization_args = { +clang_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-Og'], @@ -43,7 +44,7 @@ clang_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-Oz'], -} # type: T.Dict[str, T.List[str]] +} class ClangCompiler(GnuLikeCompiler): diff --git a/mesonbuild/compilers/mixins/clike.py b/mesonbuild/compilers/mixins/clike.py index 4449aa7..ca90999 100644 --- a/mesonbuild/compilers/mixins/clike.py +++ b/mesonbuild/compilers/mixins/clike.py @@ -11,11 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Mixin classes to be shared between C and C++ compilers. -Without this we'll end up with awful diamond inherintance problems. The goal +Without this we'll end up with awful diamond inheritance problems. The goal of this is to have mixin's, which are classes that are designed *not* to be standalone, they only work through inheritance. """ @@ -34,9 +35,8 @@ from pathlib import Path from ... import arglist from ... import mesonlib from ... import mlog -from ...linkers import GnuLikeDynamicLinkerMixin, SolarisDynamicLinker, CompCertDynamicLinker -from ...mesonlib import LibType -from ...coredata import OptionKey +from ...linkers.linkers import GnuLikeDynamicLinkerMixin, SolarisDynamicLinker, CompCertDynamicLinker +from ...mesonlib import LibType, OptionKey from .. import compilers from ..compilers import CompileCheckMode from .visualstudio import VisualStudioLikeCompiler @@ -102,7 +102,7 @@ class CLikeCompilerArgs(arglist.CompilerArgs): default_dirs = self.compiler.get_default_include_dirs() if default_dirs: real_default_dirs = [self._cached_realpath(i) for i in default_dirs] - bad_idx_list = [] # type: T.List[int] + bad_idx_list: T.List[int] = [] for i, each in enumerate(new): if not each.startswith('-isystem'): continue @@ -135,11 +135,11 @@ class CLikeCompiler(Compiler): """Shared bits for the C and CPP Compilers.""" if T.TYPE_CHECKING: - warn_args = {} # type: T.Dict[str, T.List[str]] + warn_args: T.Dict[str, T.List[str]] = {} # TODO: Replace this manual cache with functools.lru_cache - find_library_cache = {} # type: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], str, LibType], T.Optional[T.List[str]]] - find_framework_cache = {} # type: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], bool], T.Optional[T.List[str]]] + find_library_cache: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], str, LibType], T.Optional[T.List[str]]] = {} + find_framework_cache: T.Dict[T.Tuple[T.Tuple[str, ...], str, T.Tuple[str, ...], bool], T.Optional[T.List[str]]] = {} internal_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS def __init__(self, exe_wrapper: T.Optional['ExternalProgram'] = None): @@ -225,7 +225,7 @@ class CLikeCompiler(Compiler): # system directories aren't mixed, we only need to check one file for each # directory and go by that. If we can't check the file for some reason, assume # the compiler knows what it's doing, and accept the directory anyway. - retval = [] + retval: T.List[str] = [] for d in dirs: files = [f for f in os.listdir(d) if f.endswith('.so') and os.path.isfile(os.path.join(d, f))] # if no files, accept directory and move on @@ -326,7 +326,7 @@ class CLikeCompiler(Compiler): mlog.debug(stde) mlog.debug('-----') if pc.returncode != 0: - raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} can not compile programs.') + raise mesonlib.EnvironmentException(f'Compiler {self.name_string()} cannot compile programs.') # Run sanity check if self.is_cross: if self.exe_wrapper is None: @@ -369,7 +369,7 @@ class CLikeCompiler(Compiler): #include <{hname}> #endif''' return self.compiles(code, env, extra_args=extra_args, - dependencies=dependencies, mode='preprocess', disable_cache=disable_cache) + dependencies=dependencies, mode=CompileCheckMode.PREPROCESS, disable_cache=disable_cache) def has_header_symbol(self, hname: str, symbol: str, prefix: str, env: 'Environment', *, @@ -388,8 +388,8 @@ class CLikeCompiler(Compiler): dependencies=dependencies) def _get_basic_compiler_args(self, env: 'Environment', mode: CompileCheckMode) -> T.Tuple[T.List[str], T.List[str]]: - cargs = [] # type: T.List[str] - largs = [] # type: T.List[str] + cargs: T.List[str] = [] + largs: T.List[str] = [] if mode is CompileCheckMode.LINK: # Sometimes we need to manually select the CRT to use with MSVC. # One example is when trying to do a compiler check that involves @@ -431,7 +431,7 @@ class CLikeCompiler(Compiler): extra_args: T.Union[None, arglist.CompilerArgs, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]], dependencies: T.Optional[T.List['Dependency']], mode: CompileCheckMode = CompileCheckMode.COMPILE) -> arglist.CompilerArgs: - # TODO: the caller should handle the listfing of these arguments + # TODO: the caller should handle the listing of these arguments if extra_args is None: extra_args = [] else: @@ -445,11 +445,15 @@ class CLikeCompiler(Compiler): # TODO: we want to ensure the front end does the listifing here dependencies = [dependencies] # Collect compiler arguments - cargs = self.compiler_args() # type: arglist.CompilerArgs - largs = [] # type: T.List[str] + cargs: arglist.CompilerArgs = self.compiler_args() + largs: T.List[str] = [] for d in dependencies: # Add compile flags needed by dependencies cargs += d.get_compile_args() + system_incdir = d.get_include_type() == 'system' + for i in d.get_include_dirs(): + for idir in i.to_string_list(env.get_source_dir(), env.get_build_dir()): + cargs.extend(self.get_include_args(idir, system_incdir)) if mode is CompileCheckMode.LINK: # Add link flags needed to find dependencies largs += d.get_link_args() @@ -474,7 +478,7 @@ class CLikeCompiler(Compiler): need_exe_wrapper = env.need_exe_wrapper(self.for_machine) if need_exe_wrapper and self.exe_wrapper is None: raise compilers.CrossNoRunException('Can not run test applications in this cross environment.') - with self._build_wrapper(code, env, extra_args, dependencies, mode='link', want_output=True) as p: + with self._build_wrapper(code, env, extra_args, dependencies, mode=CompileCheckMode.LINK, want_output=True) as p: if p.returncode != 0: mlog.debug(f'Could not compile test file {p.input_name}: {p.returncode}\n') return compilers.RunResult(False) @@ -497,8 +501,8 @@ class CLikeCompiler(Compiler): def _compile_int(self, expression: str, prefix: str, env: 'Environment', extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]], dependencies: T.Optional[T.List['Dependency']]) -> bool: - t = f'''#include - {prefix} + t = f'''{prefix} + #include int main(void) {{ static int a[1-2*!({expression})]; a[0]=0; return 0; }}''' return self.compiles(t, env, extra_args=extra_args, dependencies=dependencies)[0] @@ -558,8 +562,9 @@ class CLikeCompiler(Compiler): extra_args = [] if self.is_cross: return self.cross_compute_int(expression, low, high, guess, prefix, env, extra_args, dependencies) - t = f'''#include - {prefix} + t = f'''{prefix} + #include + #include int main(void) {{ printf("%ld\\n", (long)({expression})); return 0; @@ -577,8 +582,8 @@ class CLikeCompiler(Compiler): dependencies: T.Optional[T.List['Dependency']] = None) -> int: if extra_args is None: extra_args = [] - t = f'''#include - {prefix} + t = f'''{prefix} + #include int main(void) {{ {typename} something; return 0; @@ -590,33 +595,35 @@ class CLikeCompiler(Compiler): def sizeof(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Union[None, T.List[str], T.Callable[[CompileCheckMode], T.List[str]]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: if extra_args is None: extra_args = [] if self.is_cross: - return self.cross_sizeof(typename, prefix, env, extra_args=extra_args, - dependencies=dependencies) - t = f'''#include - {prefix} + r = self.cross_sizeof(typename, prefix, env, extra_args=extra_args, + dependencies=dependencies) + return r, False + t = f'''{prefix} + #include + #include int main(void) {{ printf("%ld\\n", (long)(sizeof({typename}))); return 0; }}''' - res = self.run(t, env, extra_args=extra_args, - dependencies=dependencies) + res = self.cached_run(t, env, extra_args=extra_args, + dependencies=dependencies) if not res.compiled: - return -1 + return -1, False if res.returncode != 0: raise mesonlib.EnvironmentException('Could not run sizeof test binary.') - return int(res.stdout) + return int(res.stdout), res.cached def cross_alignment(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Optional[T.List[str]] = None, dependencies: T.Optional[T.List['Dependency']] = None) -> int: if extra_args is None: extra_args = [] - t = f'''#include - {prefix} + t = f'''{prefix} + #include int main(void) {{ {typename} something; return 0; @@ -624,8 +631,8 @@ class CLikeCompiler(Compiler): if not self.compiles(t, env, extra_args=extra_args, dependencies=dependencies)[0]: return -1 - t = f'''#include - {prefix} + t = f'''{prefix} + #include struct tmp {{ char c; {typename} target; @@ -634,15 +641,16 @@ class CLikeCompiler(Compiler): def alignment(self, typename: str, prefix: str, env: 'Environment', *, extra_args: T.Optional[T.List[str]] = None, - dependencies: T.Optional[T.List['Dependency']] = None) -> int: + dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[int, bool]: if extra_args is None: extra_args = [] if self.is_cross: - return self.cross_alignment(typename, prefix, env, extra_args=extra_args, - dependencies=dependencies) - t = f'''#include + r = self.cross_alignment(typename, prefix, env, extra_args=extra_args, + dependencies=dependencies) + return r, False + t = f'''{prefix} + #include #include - {prefix} struct tmp {{ char c; {typename} target; @@ -651,8 +659,8 @@ class CLikeCompiler(Compiler): printf("%d", (int)offsetof(struct tmp, target)); return 0; }}''' - res = self.run(t, env, extra_args=extra_args, - dependencies=dependencies) + res = self.cached_run(t, env, extra_args=extra_args, + dependencies=dependencies) if not res.compiled: raise mesonlib.EnvironmentException('Could not compile alignment test.') if res.returncode != 0: @@ -660,32 +668,45 @@ class CLikeCompiler(Compiler): align = int(res.stdout) if align == 0: raise mesonlib.EnvironmentException(f'Could not determine alignment of {typename}. Sorry. You might want to file a bug.') - return align + return align, res.cached def get_define(self, dname: str, prefix: str, env: 'Environment', extra_args: T.Union[T.List[str], T.Callable[[CompileCheckMode], T.List[str]]], dependencies: T.Optional[T.List['Dependency']], disable_cache: bool = False) -> T.Tuple[str, bool]: - delim = '"MESON_GET_DEFINE_DELIMITER"' + delim_start = '"MESON_GET_DEFINE_DELIMITER_START"\n' + delim_end = '\n"MESON_GET_DEFINE_DELIMITER_END"' + sentinel_undef = '"MESON_GET_DEFINE_UNDEFINED_SENTINEL"' code = f''' {prefix} #ifndef {dname} - # define {dname} + # define {dname} {sentinel_undef} #endif - {delim}\n{dname}''' + {delim_start}{dname}{delim_end}''' args = self.build_wrapper_args(env, extra_args, dependencies, mode=CompileCheckMode.PREPROCESS).to_native() - func = functools.partial(self.cached_compile, code, env.coredata, extra_args=args, mode='preprocess') + func = functools.partial(self.cached_compile, code, env.coredata, extra_args=args, mode=CompileCheckMode.PREPROCESS) if disable_cache: - func = functools.partial(self.compile, code, extra_args=args, mode='preprocess', temp_dir=env.scratch_dir) + func = functools.partial(self.compile, code, extra_args=args, mode=CompileCheckMode.PREPROCESS) with func() as p: cached = p.cached if p.returncode != 0: raise mesonlib.EnvironmentException(f'Could not get define {dname!r}') - # Get the preprocessed value after the delimiter, - # minus the extra newline at the end and - # merge string literals. - return self._concatenate_string_literals(p.stdout.split(delim + '\n')[-1][:-1]), cached + + # Get the preprocessed value between the delimiters + star_idx = p.stdout.find(delim_start) + end_idx = p.stdout.rfind(delim_end) + if (star_idx == -1) or (end_idx == -1) or (star_idx == end_idx): + raise mesonlib.MesonBugException('Delimiters not found in preprocessor output.') + define_value = p.stdout[star_idx + len(delim_start):end_idx] + + if define_value == sentinel_undef: + define_value = None + else: + # Merge string literals + define_value = self._concatenate_string_literals(define_value).strip() + + return define_value, cached def get_return_value(self, fname: str, rtype: str, prefix: str, env: 'Environment', extra_args: T.Optional[T.List[str]], @@ -802,7 +823,7 @@ class CLikeCompiler(Compiler): # # class StrProto(typing.Protocol): # def __str__(self) -> str: ... - fargs = {'prefix': prefix, 'func': funcname} # type: T.Dict[str, T.Union[str, bool, int]] + fargs: T.Dict[str, T.Union[str, bool, int]] = {'prefix': prefix, 'func': funcname} # glibc defines functions that are not available on Linux as stubs that # fail with ENOSYS (such as e.g. lchmod). In this case we want to fail @@ -881,9 +902,7 @@ class CLikeCompiler(Compiler): if extra_args is None: extra_args = [] # Create code that accesses all members - members = '' - for member in membernames: - members += f'foo.{member};\n' + members = ''.join(f'foo.{member};\n' for member in membernames) t = f'''{prefix} void bar(void) {{ {typename} foo; @@ -918,7 +937,7 @@ class CLikeCompiler(Compiler): ''' args = self.get_compiler_check_args(CompileCheckMode.COMPILE) n = '_symbols_have_underscore_prefix_searchbin' - with self._build_wrapper(code, env, extra_args=args, mode='compile', want_output=True, temp_dir=env.scratch_dir) as p: + with self._build_wrapper(code, env, extra_args=args, mode=CompileCheckMode.COMPILE, want_output=True) as p: if p.returncode != 0: raise RuntimeError(f'BUG: Unable to compile {n!r} check: {p.stderr}') if not os.path.isfile(p.output_name): @@ -953,7 +972,7 @@ class CLikeCompiler(Compiler): #endif {delim}MESON_UNDERSCORE_PREFIX ''' - with self._build_wrapper(code, env, mode='preprocess', want_output=False, temp_dir=env.scratch_dir) as p: + with self._build_wrapper(code, env, mode=CompileCheckMode.PREPROCESS, want_output=False) as p: if p.returncode != 0: raise RuntimeError(f'BUG: Unable to preprocess _symbols_have_underscore_prefix_define check: {p.stdout}') symbol_prefix = p.stdout.partition(delim)[-1].rstrip() @@ -1001,7 +1020,7 @@ class CLikeCompiler(Compiler): return self._symbols_have_underscore_prefix_searchbin(env) def _get_patterns(self, env: 'Environment', prefixes: T.List[str], suffixes: T.List[str], shared: bool = False) -> T.List[str]: - patterns = [] # type: T.List[str] + patterns: T.List[str] = [] for p in prefixes: for s in suffixes: patterns.append(p + '{}.' + s) @@ -1065,19 +1084,22 @@ class CLikeCompiler(Compiler): @staticmethod def _sort_shlibs_openbsd(libs: T.List[str]) -> T.List[str]: - filtered = [] # type: T.List[str] + def tuple_key(x: str) -> T.Tuple[int, ...]: + ver = x.rsplit('.so.', maxsplit=1)[1] + return tuple(int(i) for i in ver.split('.')) + + filtered: T.List[str] = [] for lib in libs: # Validate file as a shared library of type libfoo.so.X.Y ret = lib.rsplit('.so.', maxsplit=1) if len(ret) != 2: continue try: - float(ret[1]) + tuple(int(i) for i in ret[1].split('.')) except ValueError: continue filtered.append(lib) - float_cmp = lambda x: float(x.rsplit('.so.', maxsplit=1)[1]) - return sorted(filtered, key=float_cmp, reverse=True) + return sorted(filtered, key=tuple_key, reverse=True) @classmethod def _get_trials_from_pattern(cls, pattern: str, directory: str, libname: str) -> T.List[Path]: @@ -1090,27 +1112,25 @@ class CLikeCompiler(Compiler): return [f] @staticmethod - def _get_file_from_list(env: 'Environment', paths: T.List[Path]) -> Path: + def _get_file_from_list(env: Environment, paths: T.List[Path]) -> T.Optional[Path]: ''' We just check whether the library exists. We can't do a link check because the library might have unresolved symbols that require other libraries. On macOS we check if the library matches our target architecture. ''' - # If not building on macOS for Darwin, do a simple file check - if not env.machines.host.is_darwin() or not env.machines.build.is_darwin(): - for p in paths: - if p.is_file(): - return p - # Run `lipo` and check if the library supports the arch we want for p in paths: - if not p.is_file(): - continue - archs = mesonlib.darwin_get_object_archs(str(p)) - if archs and env.machines.host.cpu_family in archs: + if p.is_file(): + + if env.machines.host.is_darwin() and env.machines.build.is_darwin(): + # Run `lipo` and check if the library supports the arch we want + archs = mesonlib.darwin_get_object_archs(str(p)) + if not archs or env.machines.host.cpu_family not in archs: + mlog.debug(f'Rejected {p}, supports {archs} but need {env.machines.host.cpu_family}') + continue + return p - else: - mlog.debug(f'Rejected {p}, supports {archs} but need {env.machines.host.cpu_family}') + return None @functools.lru_cache() @@ -1118,9 +1138,9 @@ class CLikeCompiler(Compiler): ''' returns true if the output produced is 64-bit, false if 32-bit ''' - return self.sizeof('void *', '', env) == 8 + return self.sizeof('void *', '', env)[0] == 8 - def _find_library_real(self, libname: str, env: 'Environment', extra_dirs: T.List[str], code: str, libtype: LibType) -> T.Optional[T.List[str]]: + def _find_library_real(self, libname: str, env: 'Environment', extra_dirs: T.List[str], code: str, libtype: LibType, lib_prefix_warning: bool) -> T.Optional[T.List[str]]: # First try if we can just add the library as -l. # Gcc + co seem to prefer builtin lib dirs to -L dirs. # Only try to find std libs if no extra dirs specified. @@ -1159,13 +1179,13 @@ class CLikeCompiler(Compiler): trial = self._get_file_from_list(env, trials) if not trial: continue - if libname.startswith('lib') and trial.name.startswith(libname): + if libname.startswith('lib') and trial.name.startswith(libname) and lib_prefix_warning: mlog.warning(f'find_library({libname!r}) starting in "lib" only works by accident and is not portable') return [trial.as_posix()] return None def _find_library_impl(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - code: str, libtype: LibType) -> T.Optional[T.List[str]]: + code: str, libtype: LibType, lib_prefix_warning: bool) -> T.Optional[T.List[str]]: # These libraries are either built-in or invalid if libname in self.ignore_libs: return [] @@ -1173,7 +1193,7 @@ class CLikeCompiler(Compiler): extra_dirs = [extra_dirs] key = (tuple(self.exelist), libname, tuple(extra_dirs), code, libtype) if key not in self.find_library_cache: - value = self._find_library_real(libname, env, extra_dirs, code, libtype) + value = self._find_library_real(libname, env, extra_dirs, code, libtype, lib_prefix_warning) self.find_library_cache[key] = value else: value = self.find_library_cache[key] @@ -1182,9 +1202,9 @@ class CLikeCompiler(Compiler): return value.copy() def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: code = 'int main(void) { return 0; }\n' - return self._find_library_impl(libname, env, extra_dirs, code, libtype) + return self._find_library_impl(libname, env, extra_dirs, code, libtype, lib_prefix_warning) def find_framework_paths(self, env: 'Environment') -> T.List[str]: ''' @@ -1204,7 +1224,7 @@ class CLikeCompiler(Compiler): os_env = os.environ.copy() os_env['LC_ALL'] = 'C' _, _, stde = mesonlib.Popen_safe(commands, env=os_env, stdin=subprocess.PIPE) - paths = [] # T.List[str] + paths: T.List[str] = [] for line in stde.split('\n'): if '(framework directory)' not in line: continue @@ -1215,7 +1235,7 @@ class CLikeCompiler(Compiler): def _find_framework_real(self, name: str, env: 'Environment', extra_dirs: T.List[str], allow_system: bool) -> T.Optional[T.List[str]]: code = 'int main(void) { return 0; }' - link_args = [] + link_args: T.List[str] = [] for d in extra_dirs: link_args += ['-F' + d] # We can pass -Z to disable searching in the system frameworks, but @@ -1268,11 +1288,11 @@ class CLikeCompiler(Compiler): return args.copy() def has_arguments(self, args: T.List[str], env: 'Environment', code: str, - mode: str) -> T.Tuple[bool, bool]: + mode: CompileCheckMode) -> T.Tuple[bool, bool]: return self.compiles(code, env, extra_args=args, mode=mode) def _has_multi_arguments(self, args: T.List[str], env: 'Environment', code: str) -> T.Tuple[bool, bool]: - new_args = [] # type: T.List[str] + new_args: T.List[str] = [] for arg in args: # some compilers, e.g. GCC, don't warn for unsupported warning-disable # flags, so when we are testing a flag like "-Wno-forgotten-towel", also @@ -1288,7 +1308,7 @@ class CLikeCompiler(Compiler): 'the compiler you are using. has_link_argument or ' 'other similar method can be used instead.') new_args.append(arg) - return self.has_arguments(new_args, env, code, mode='compile') + return self.has_arguments(new_args, env, code, mode=CompileCheckMode.COMPILE) def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: return self._has_multi_arguments(args, env, 'extern int i;\nint i;\n') @@ -1299,7 +1319,7 @@ class CLikeCompiler(Compiler): # false positive. args = self.linker.fatal_warnings() + args args = self.linker_to_compiler_args(args) - return self.has_arguments(args, env, code, mode='link') + return self.has_arguments(args, env, code, mode=CompileCheckMode.LINK) def has_multi_link_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: return self._has_multi_link_arguments(args, env, 'int main(void) { return 0; }\n') @@ -1331,8 +1351,10 @@ class CLikeCompiler(Compiler): return self.compiles(self.attribute_check_func(name), env, extra_args=self.get_has_func_attribute_extra_args(name)) - def get_disable_assert_args(self) -> T.List[str]: - return ['-DNDEBUG'] + def get_assert_args(self, disable: bool) -> T.List[str]: + if disable: + return ['-DNDEBUG'] + return [] @functools.lru_cache(maxsize=None) def can_compile(self, src: 'mesonlib.FileOrString') -> bool: diff --git a/mesonbuild/compilers/mixins/compcert.py b/mesonbuild/compilers/mixins/compcert.py index a0394a9..ac4d5aa 100644 --- a/mesonbuild/compilers/mixins/compcert.py +++ b/mesonbuild/compilers/mixins/compcert.py @@ -30,16 +30,16 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -ccomp_buildtype_args = { +ccomp_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [''], 'debug': ['-O0', '-g'], 'debugoptimized': ['-O0', '-g'], 'release': ['-O3'], 'minsize': ['-Os'], 'custom': ['-Obranchless'], -} # type: T.Dict[str, T.List[str]] +} -ccomp_optimization_args = { +ccomp_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-O0'], @@ -47,19 +47,19 @@ ccomp_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-Os'] -} # type: T.Dict[str, T.List[str]] +} -ccomp_debug_args = { +ccomp_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: ['-g'] -} # type: T.Dict[bool, T.List[str]] +} # As of CompCert 20.04, these arguments should be passed to the underlying gcc linker (via -WUl,) # There are probably (many) more, but these are those used by picolibc -ccomp_args_to_wul = [ +ccomp_args_to_wul: T.List[str] = [ r"^-ffreestanding$", r"^-r$" -] # type: T.List[str] +] class CompCertCompiler(Compiler): @@ -68,12 +68,14 @@ class CompCertCompiler(Compiler): def __init__(self) -> None: # Assembly self.can_compile_suffixes.add('s') - default_warn_args = [] # type: T.List[str] - self.warn_args = {'0': [], - '1': default_warn_args, - '2': default_warn_args + [], - '3': default_warn_args + [], - 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]] + self.can_compile_suffixes.add('sx') + default_warn_args: T.List[str] = [] + self.warn_args: T.Dict[str, T.List[str]] = { + '0': [], + '1': default_warn_args, + '2': default_warn_args + [], + '3': default_warn_args + [], + 'everything': default_warn_args + []} def get_always_args(self) -> T.List[str]: return [] @@ -94,7 +96,7 @@ class CompCertCompiler(Compiler): @classmethod def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]: "Always returns a copy that can be independently mutated" - patched_args = [] # type: T.List[str] + patched_args: T.List[str] = [] for arg in args: added = 0 for ptrn in ccomp_args_to_wul: diff --git a/mesonbuild/compilers/mixins/elbrus.py b/mesonbuild/compilers/mixins/elbrus.py index 7362039..ad6b7ca 100644 --- a/mesonbuild/compilers/mixins/elbrus.py +++ b/mesonbuild/compilers/mixins/elbrus.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Abstractions for the Elbrus family of compilers.""" @@ -73,7 +74,7 @@ class ElbrusCompiler(GnuLikeCompiler): os_env['LC_ALL'] = 'C' p = subprocess.Popen(self.get_exelist(ccache=False) + ['-xc', '-E', '-v', '-'], env=os_env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stderr = p.stderr.read().decode('utf-8', errors='replace') - includes = [] + includes: T.List[str] = [] for line in stderr.split('\n'): if line.lstrip().startswith('--sys_include'): includes.append(re.sub(r'\s*\\$', '', re.sub(r'^\s*--sys_include\s*', '', line))) @@ -90,7 +91,7 @@ class ElbrusCompiler(GnuLikeCompiler): return 'pch' def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - args = [] + args: T.List[str] = [] std = options[OptionKey('std', lang=self.language, machine=self.for_machine)] if std.value != 'none': args.append('-std=' + std.value) diff --git a/mesonbuild/compilers/mixins/emscripten.py b/mesonbuild/compilers/mixins/emscripten.py index 0796d6f..fef22b9 100644 --- a/mesonbuild/compilers/mixins/emscripten.py +++ b/mesonbuild/compilers/mixins/emscripten.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Provides a mixin for shared code between C and C++ Emscripten compilers.""" @@ -21,6 +22,7 @@ from ... import coredata from ... import mesonlib from ...mesonlib import OptionKey from ...mesonlib import LibType +from mesonbuild.compilers.compilers import CompileCheckMode if T.TYPE_CHECKING: from ...environment import Environment @@ -35,7 +37,7 @@ else: def wrap_js_includes(args: T.List[str]) -> T.List[str]: - final_args = [] + final_args: T.List[str] = [] for i in args: if i.endswith('.js') and not i.startswith('-'): final_args += ['--js-library', i] @@ -45,27 +47,22 @@ def wrap_js_includes(args: T.List[str]) -> T.List[str]: class EmscriptenMixin(Compiler): - def _get_compile_output(self, dirname: str, mode: str) -> str: - # In pre-processor mode, the output is sent to stdout and discarded - if mode == 'preprocess': - return None + def _get_compile_output(self, dirname: str, mode: CompileCheckMode) -> str: + assert mode != CompileCheckMode.PREPROCESS, 'In pre-processor mode, the output is sent to stdout and discarded' # Unlike sane toolchains, emcc infers the kind of output from its name. # This is the only reason why this method is overridden; compiler tests # do not work well with the default exe/obj suffices. - if mode == 'link': + if mode == CompileCheckMode.LINK: suffix = 'js' else: suffix = 'o' return os.path.join(dirname, 'output.' + suffix) - def thread_flags(self, env: 'Environment') -> T.List[str]: - return ['-s', 'USE_PTHREADS=1'] - def thread_link_flags(self, env: 'Environment') -> T.List[str]: - args = ['-s', 'USE_PTHREADS=1'] + args = ['-pthread'] count: int = env.coredata.options[OptionKey('thread_count', lang=self.language, machine=self.for_machine)].value if count: - args.extend(['-s', f'PTHREAD_POOL_SIZE={count}']) + args.append(f'-sPTHREAD_POOL_SIZE={count}') return args def get_options(self) -> 'coredata.MutableKeyedOptionDictType': @@ -88,9 +85,9 @@ class EmscriptenMixin(Compiler): return wrap_js_includes(super().get_dependency_link_args(dep)) def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: if not libname.endswith('.js'): - return super().find_library(libname, env, extra_dirs, libtype) + return super().find_library(libname, env, extra_dirs, libtype, lib_prefix_warning) if os.path.isabs(libname): if os.path.exists(libname): return [libname] diff --git a/mesonbuild/compilers/mixins/gnu.py b/mesonbuild/compilers/mixins/gnu.py index 8152b25..b62435b 100644 --- a/mesonbuild/compilers/mixins/gnu.py +++ b/mesonbuild/compilers/mixins/gnu.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Provides mixins for GNU compilers and GNU-like compilers.""" @@ -26,6 +27,7 @@ import typing as T from ... import mesonlib from ... import mlog from ...mesonlib import OptionKey +from mesonbuild.compilers.compilers import CompileCheckMode if T.TYPE_CHECKING: from ..._typing import ImmutableListProtocol @@ -40,21 +42,21 @@ else: # XXX: prevent circular references. # FIXME: this really is a posix interface not a c-like interface -clike_debug_args = { +clike_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: ['-g'], -} # type: T.Dict[bool, T.List[str]] +} -gnulike_buildtype_args = { +gnulike_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -gnu_optimization_args = { +gnu_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-Og'], @@ -62,9 +64,9 @@ gnu_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-Os'], -} # type: T.Dict[str, T.List[str]] +} -gnulike_instruction_set_args = { +gnulike_instruction_set_args: T.Dict[str, T.List[str]] = { 'mmx': ['-mmmx'], 'sse': ['-msse'], 'sse2': ['-msse2'], @@ -75,22 +77,22 @@ gnulike_instruction_set_args = { 'avx': ['-mavx'], 'avx2': ['-mavx2'], 'neon': ['-mfpu=neon'], -} # type: T.Dict[str, T.List[str]] +} -gnu_symbol_visibility_args = { +gnu_symbol_visibility_args: T.Dict[str, T.List[str]] = { '': [], 'default': ['-fvisibility=default'], 'internal': ['-fvisibility=internal'], 'hidden': ['-fvisibility=hidden'], 'protected': ['-fvisibility=protected'], 'inlineshidden': ['-fvisibility=hidden', '-fvisibility-inlines-hidden'], -} # type: T.Dict[str, T.List[str]] +} -gnu_color_args = { +gnu_color_args: T.Dict[str, T.List[str]] = { 'auto': ['-fdiagnostics-color=auto'], 'always': ['-fdiagnostics-color=always'], 'never': ['-fdiagnostics-color=never'], -} # type: T.Dict[str, T.List[str]] +} # Warnings collected from the GCC source and documentation. This is an # objective set of all the warnings flags that apply to general projects: the @@ -116,7 +118,7 @@ gnu_color_args = { # # Omitted warnings enabled elsewhere in meson: # -Winvalid-pch (GCC 3.4.0) -gnu_common_warning_args = { +gnu_common_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wcast-qual", "-Wconversion", @@ -195,11 +197,13 @@ gnu_common_warning_args = { "-Wduplicated-branches", ], "8.1.0": [ - "-Wattribute-alias=2", "-Wcast-align=strict", "-Wsuggest-attribute=cold", "-Wsuggest-attribute=malloc", ], + "9.1.0": [ + "-Wattribute-alias=2", + ], "10.1.0": [ "-Wanalyzer-too-complex", "-Warith-conversion", @@ -209,7 +213,7 @@ gnu_common_warning_args = { "-Wopenacc-parallelism", "-Wtrivial-auto-var-init", ], -} # type: T.Dict[str, T.List[str]] +} # GCC warnings for C # Omitted non-general or legacy warnings: @@ -219,7 +223,7 @@ gnu_common_warning_args = { # -Wdeclaration-after-statement # -Wtraditional # -Wtraditional-conversion -gnu_c_warning_args = { +gnu_c_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wbad-function-cast", "-Wmissing-prototypes", @@ -236,7 +240,7 @@ gnu_c_warning_args = { "4.5.0": [ "-Wunsuffixed-float-constants", ], -} # type: T.Dict[str, T.List[str]] +} # GCC warnings for C++ # Omitted non-general or legacy warnings: @@ -246,7 +250,7 @@ gnu_c_warning_args = { # -Wctad-maybe-unsupported # -Wnamespaces # -Wtemplates -gnu_cpp_warning_args = { +gnu_cpp_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wctor-dtor-privacy", "-Weffc++", @@ -305,13 +309,13 @@ gnu_cpp_warning_args = { "-Wdeprecated-enum-float-conversion", "-Winvalid-imported-macros", ], -} # type: T.Dict[str, T.List[str]] +} # GCC warnings for Objective C and Objective C++ # Omitted non-general or legacy warnings: # -Wtraditional # -Wtraditional-conversion -gnu_objc_warning_args = { +gnu_objc_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wselector", ], @@ -322,26 +326,26 @@ gnu_objc_warning_args = { "-Wassign-intercept", "-Wstrict-selector-match", ], -} # type: T.Dict[str, T.List[str]] +} +_LANG_MAP = { + 'c': 'c', + 'cpp': 'c++', + 'objc': 'objective-c', + 'objcpp': 'objective-c++' +} @functools.lru_cache(maxsize=None) def gnulike_default_include_dirs(compiler: T.Tuple[str, ...], lang: str) -> 'ImmutableListProtocol[str]': - lang_map = { - 'c': 'c', - 'cpp': 'c++', - 'objc': 'objective-c', - 'objcpp': 'objective-c++' - } - if lang not in lang_map: + if lang not in _LANG_MAP: return [] - lang = lang_map[lang] + lang = _LANG_MAP[lang] env = os.environ.copy() env["LC_ALL"] = 'C' cmd = list(compiler) + [f'-x{lang}', '-E', '-v', '-'] _, stdout, _ = mesonlib.Popen_safe(cmd, stderr=subprocess.STDOUT, env=env) parse_state = 0 - paths = [] # type: T.List[str] + paths: T.List[str] = [] for line in stdout.split('\n'): line = line.strip(' \n\r\t') if parse_state == 0: @@ -386,6 +390,7 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): self.base_options.add(OptionKey('b_sanitize')) # All GCC-like backends can do assembly self.can_compile_suffixes.add('s') + self.can_compile_suffixes.add('sx') def get_pic_args(self) -> T.List[str]: if self.info.is_windows() or self.info.is_cygwin() or self.info.is_darwin(): @@ -444,10 +449,7 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): return ['-fprofile-generate'] def get_profile_use_args(self) -> T.List[str]: - return ['-fprofile-use', '-fprofile-correction'] - - def get_gui_app_args(self, value: bool) -> T.List[str]: - return ['-mwindows' if value else '-mconsole'] + return ['-fprofile-use'] def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]: for idx, i in enumerate(parameter_list): @@ -460,7 +462,7 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): def _get_search_dirs(self, env: 'Environment') -> str: extra_args = ['--print-search-dirs'] with self._build_wrapper('', env, extra_args=extra_args, - dependencies=None, mode='compile', + dependencies=None, mode=CompileCheckMode.COMPILE, want_output=True) as p: return p.stdout @@ -478,7 +480,7 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): # pathlib treats empty paths as '.', so filter those out paths = [p for p in pathstr.split(pathsep) if p] - result = [] + result: T.List[str] = [] for p in paths: # GCC returns paths like this: # /usr/lib/gcc/x86_64-linux-gnu/8/../../../../x86_64-linux-gnu/lib @@ -524,8 +526,8 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): args.append('-fno-omit-frame-pointer') return args - def get_output_args(self, target: str) -> T.List[str]: - return ['-o', target] + def get_output_args(self, outputname: str) -> T.List[str]: + return ['-o', outputname] def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: return ['-MD', '-MQ', outtarget, '-MF', outfile] @@ -554,7 +556,8 @@ class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): # We want to allow preprocessing files with any extension, such as # foo.c.in. In that case we need to tell GCC/CLANG to treat them as # assembly file. - return self.get_preprocess_only_args() + ['-x', 'assembler-with-cpp'] + lang = _LANG_MAP.get(self.language, 'assembler-with-cpp') + return self.get_preprocess_only_args() + [f'-x{lang}'] class GnuCompiler(GnuLikeCompiler): @@ -584,7 +587,7 @@ class GnuCompiler(GnuLikeCompiler): return args def supported_warn_args(self, warn_args_by_version: T.Dict[str, T.List[str]]) -> T.List[str]: - result = [] + result: T.List[str] = [] for version, warn_args in warn_args_by_version.items(): if mesonlib.version_compare(self.version, '>=' + version): result += warn_args @@ -608,7 +611,7 @@ class GnuCompiler(GnuLikeCompiler): return ['-fopenmp'] def has_arguments(self, args: T.List[str], env: 'Environment', code: str, - mode: str) -> T.Tuple[bool, bool]: + mode: CompileCheckMode) -> T.Tuple[bool, bool]: # For some compiler command line arguments, the GNU compilers will # emit a warning on stderr indicating that an option is valid for a # another language, but still complete with exit_success @@ -643,3 +646,6 @@ class GnuCompiler(GnuLikeCompiler): if linker == 'mold' and mesonlib.version_compare(version, '>=12.0.1'): return ['-fuse-ld=mold'] return super().use_linker_args(linker, version) + + def get_profile_use_args(self) -> T.List[str]: + return super().get_profile_use_args() + ['-fprofile-correction'] diff --git a/mesonbuild/compilers/mixins/intel.py b/mesonbuild/compilers/mixins/intel.py index 9877a54..9af05e0 100644 --- a/mesonbuild/compilers/mixins/intel.py +++ b/mesonbuild/compilers/mixins/intel.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Abstractions for the Intel Compiler families. @@ -49,14 +50,14 @@ class IntelGnuLikeCompiler(GnuLikeCompiler): minsize: -O2 """ - BUILD_ARGS = { + BUILD_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': ["-g", "-traceback"], 'debugoptimized': ["-g", "-traceback"], 'release': [], 'minsize': [], 'custom': [], - } # type: T.Dict[str, T.List[str]] + } OPTIM_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], @@ -88,8 +89,8 @@ class IntelGnuLikeCompiler(GnuLikeCompiler): return ['-pch', '-pch_dir', os.path.join(pch_dir), '-x', self.lang_header, '-include', header, '-x', 'none'] - def get_pch_name(self, header_name: str) -> str: - return os.path.basename(header_name) + '.' + self.get_pch_suffix() + def get_pch_name(self, name: str) -> str: + return os.path.basename(name) + '.' + self.get_pch_suffix() def openmp_flags(self) -> T.List[str]: if mesonlib.version_compare(self.version, '>=15.0.0'): @@ -128,14 +129,14 @@ class IntelVisualStudioLikeCompiler(VisualStudioLikeCompiler): """Abstractions for ICL, the Intel compiler on Windows.""" - BUILD_ARGS = { + BUILD_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': ["/Zi", "/traceback"], 'debugoptimized': ["/Zi", "/traceback"], 'release': [], 'minsize': [], 'custom': [], - } # type: T.Dict[str, T.List[str]] + } OPTIM_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], diff --git a/mesonbuild/compilers/mixins/islinker.py b/mesonbuild/compilers/mixins/islinker.py index 9da7ae8..cfdd746 100644 --- a/mesonbuild/compilers/mixins/islinker.py +++ b/mesonbuild/compilers/mixins/islinker.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Mixins for compilers that *are* linkers. @@ -119,8 +120,8 @@ class BasicLinkerIsCompilerMixin(Compiler): def get_buildtype_linker_args(self, buildtype: str) -> T.List[str]: return [] - def get_link_debugfile_name(self, targetfile: str) -> str: - return '' + def get_link_debugfile_name(self, targetfile: str) -> T.Optional[str]: + return None def thread_flags(self, env: 'Environment') -> T.List[str]: return [] diff --git a/mesonbuild/compilers/mixins/metrowerks.py b/mesonbuild/compilers/mixins/metrowerks.py new file mode 100644 index 0000000..8c3eca5 --- /dev/null +++ b/mesonbuild/compilers/mixins/metrowerks.py @@ -0,0 +1,301 @@ +# Copyright 2012-2019 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +"""Representations specific to the Metrowerks/Freescale Embedded C/C++ compiler family.""" + +import os +import typing as T + +from ...mesonlib import EnvironmentException, OptionKey + +if T.TYPE_CHECKING: + from ...envconfig import MachineInfo + from ...compilers.compilers import Compiler, CompileCheckMode +else: + # This is a bit clever, for mypy we pretend that these mixins descend from + # Compiler, so we get all of the methods and attributes defined for us, but + # for runtime we make them descend from object (which all classes normally + # do). This gives up DRYer type checking, with no runtime impact + Compiler = object + +mwcc_buildtype_args: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': [], + 'minsize': [], + 'custom': [], +} + +mwccarm_instruction_set_args: T.Dict[str, T.List[str]] = { + 'generic': ['-proc', 'generic'], + 'v4': ['-proc', 'v4'], + 'v4t': ['-proc', 'v4t'], + 'v5t': ['-proc', 'v5t'], + 'v5te': ['-proc', 'v5te'], + 'v6': ['-proc', 'v6'], + 'arm7tdmi': ['-proc', 'arm7tdmi'], + 'arm710t': ['-proc', 'arm710t'], + 'arm720t': ['-proc', 'arm720t'], + 'arm740t': ['-proc', 'arm740t'], + 'arm7ej': ['-proc', 'arm7ej'], + 'arm9tdmi': ['-proc', 'arm9tdmi'], + 'arm920t': ['-proc', 'arm920t'], + 'arm922t': ['-proc', 'arm922t'], + 'arm940t': ['-proc', 'arm940t'], + 'arm9ej': ['-proc', 'arm9ej'], + 'arm926ej': ['-proc', 'arm926ej'], + 'arm946e': ['-proc', 'arm946e'], + 'arm966e': ['-proc', 'arm966e'], + 'arm1020e': ['-proc', 'arm1020e'], + 'arm1022e': ['-proc', 'arm1022e'], + 'arm1026ej': ['-proc', 'arm1026ej'], + 'dbmx1': ['-proc', 'dbmx1'], + 'dbmxl': ['-proc', 'dbmxl'], + 'XScale': ['-proc', 'XScale'], + 'pxa255': ['-proc', 'pxa255'], + 'pxa261': ['-proc', 'pxa261'], + 'pxa262': ['-proc', 'pxa262'], + 'pxa263': ['-proc', 'pxa263'] +} + +mwcceppc_instruction_set_args: T.Dict[str, T.List[str]] = { + 'generic': ['-proc', 'generic'], + '401': ['-proc', '401'], + '403': ['-proc', '403'], + '505': ['-proc', '505'], + '509': ['-proc', '509'], + '555': ['-proc', '555'], + '601': ['-proc', '601'], + '602': ['-proc', '602'], + '603': ['-proc', '603'], + '603e': ['-proc', '603e'], + '604': ['-proc', '604'], + '604e': ['-proc', '604e'], + '740': ['-proc', '740'], + '750': ['-proc', '750'], + '801': ['-proc', '801'], + '821': ['-proc', '821'], + '823': ['-proc', '823'], + '850': ['-proc', '850'], + '860': ['-proc', '860'], + '7400': ['-proc', '7400'], + '7450': ['-proc', '7450'], + '8240': ['-proc', '8240'], + '8260': ['-proc', '8260'], + 'e500': ['-proc', 'e500'], + 'gekko': ['-proc', 'gekko'], +} + +mwasmarm_instruction_set_args: T.Dict[str, T.List[str]] = { + 'arm4': ['-proc', 'arm4'], + 'arm4t': ['-proc', 'arm4t'], + 'arm4xm': ['-proc', 'arm4xm'], + 'arm4txm': ['-proc', 'arm4txm'], + 'arm5': ['-proc', 'arm5'], + 'arm5T': ['-proc', 'arm5T'], + 'arm5xM': ['-proc', 'arm5xM'], + 'arm5TxM': ['-proc', 'arm5TxM'], + 'arm5TE': ['-proc', 'arm5TE'], + 'arm5TExP': ['-proc', 'arm5TExP'], + 'arm6': ['-proc', 'arm6'], + 'xscale': ['-proc', 'xscale'] +} + +mwasmeppc_instruction_set_args: T.Dict[str, T.List[str]] = { + '401': ['-proc', '401'], + '403': ['-proc', '403'], + '505': ['-proc', '505'], + '509': ['-proc', '509'], + '555': ['-proc', '555'], + '56X': ['-proc', '56X'], + '601': ['-proc', '601'], + '602': ['-proc', '602'], + '603': ['-proc', '603'], + '603e': ['-proc', '603e'], + '604': ['-proc', '604'], + '604e': ['-proc', '604e'], + '740': ['-proc', '740'], + '74X': ['-proc', '74X'], + '750': ['-proc', '750'], + '75X': ['-proc', '75X'], + '801': ['-proc', '801'], + '821': ['-proc', '821'], + '823': ['-proc', '823'], + '850': ['-proc', '850'], + '85X': ['-proc', '85X'], + '860': ['-proc', '860'], + '86X': ['-proc', '86X'], + '87X': ['-proc', '87X'], + '88X': ['-proc', '88X'], + '5100': ['-proc', '5100'], + '5200': ['-proc', '5200'], + '7400': ['-proc', '7400'], + '744X': ['-proc', '744X'], + '7450': ['-proc', '7450'], + '745X': ['-proc', '745X'], + '82XX': ['-proc', '82XX'], + '8240': ['-proc', '8240'], + '824X': ['-proc', '824X'], + '8260': ['-proc', '8260'], + '827X': ['-proc', '827X'], + '8280': ['-proc', '8280'], + 'e300': ['-proc', 'e300'], + 'e300c2': ['-proc', 'e300c2'], + 'e300c3': ['-proc', 'e300c3'], + 'e300c4': ['-proc', 'e300c4'], + 'e600': ['-proc', 'e600'], + '85xx': ['-proc', '85xx'], + 'e500': ['-proc', 'e500'], + 'e500v2': ['-proc', 'e500v2'], + 'Zen': ['-proc', 'Zen'], + '5565': ['-proc', '5565'], + '5674': ['-proc', '5674'], + 'gekko': ['-proc', 'gekko'], + 'generic': ['-proc', 'generic'], +} + +mwcc_optimization_args: T.Dict[str, T.List[str]] = { + 'plain': [], + '0': ['-O0'], + 'g': ['-Op'], + '1': ['-O1'], + '2': ['-O2'], + '3': ['-O4,p'], + 's': ['-Os'] +} + +mwcc_debug_args: T.Dict[bool, T.List[str]] = { + False: [], + True: ['-g'] +} + + +class MetrowerksCompiler(Compiler): + id = 'mwcc' + + # These compilers can actually invoke the linker, but they choke on + # linker-specific flags. So it's best to invoke the linker directly + INVOKES_LINKER = False + + def __init__(self) -> None: + if not self.is_cross: + raise EnvironmentException(f'{id} supports only cross-compilation.') + + self.base_options = { + OptionKey(o) for o in ['b_pch', 'b_ndebug']} + + default_warn_args: T.List[str] = [] + self.warn_args: T.Dict[str, T.List[str]] = { + '0': ['-w', 'off'], + '1': default_warn_args, + '2': default_warn_args + ['-w', 'most'], + '3': default_warn_args + ['-w', 'all'], + 'everything': default_warn_args + ['-w', 'full']} + + def depfile_for_object(self, objfile: str) -> T.Optional[str]: + # Earlier versions of these compilers do not support specifying + # a custom name for a depfile, and can only generate '.d' + return os.path.splitext(objfile)[0] + '.' + self.get_depfile_suffix() + + def get_always_args(self) -> T.List[str]: + return ['-gccinc'] + + def get_buildtype_args(self, buildtype: str) -> T.List[str]: + return mwcc_buildtype_args[buildtype] + + def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]: + return [] + + def get_compile_only_args(self) -> T.List[str]: + return ['-c'] + + def get_debug_args(self, is_debug: bool) -> T.List[str]: + return mwcc_debug_args[is_debug] + + def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: + # Check comment in depfile_for_object() + return ['-gccdep', '-MD'] + + def get_depfile_suffix(self) -> str: + return 'd' + + def get_include_args(self, path: str, is_system: bool) -> T.List[str]: + if not path: + path = '.' + return ['-I' + path] + + def get_no_optimization_args(self) -> T.List[str]: + return ['-opt', 'off'] + + def get_no_stdinc_args(self) -> T.List[str]: + return ['-nostdinc'] + + def get_no_stdlib_link_args(self) -> T.List[str]: + return ['-nostdlib'] + + def get_optimization_args(self, optimization_level: str) -> T.List[str]: + return mwcc_optimization_args[optimization_level] + + def get_output_args(self, outputname: str) -> T.List[str]: + return ['-o', outputname] + + def get_pic_args(self) -> T.List[str]: + return ['-pic'] + + def get_preprocess_only_args(self) -> T.List[str]: + return ['-E'] + + def get_preprocess_to_file_args(self) -> T.List[str]: + return ['-P'] + + def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]: + return ['-prefix', self.get_pch_name(header)] + + def get_pch_name(self, name: str) -> str: + return os.path.basename(name) + '.' + self.get_pch_suffix() + + def get_pch_suffix(self) -> str: + return 'mch' + + def get_warn_args(self, level: str) -> T.List[str]: + return self.warn_args[level] + + def get_werror_args(self) -> T.List[str]: + return ['-w', 'error'] + + @classmethod + def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]: + result: T.List[str] = [] + for i in args: + if i.startswith('-D'): + i = '-D' + i[2:] + if i.startswith('-I'): + i = '-I' + i[2:] + if i.startswith('-Wl,-rpath='): + continue + elif i == '--print-search-dirs': + continue + elif i.startswith('-L'): + continue + result.append(i) + return result + + def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]: + for idx, i in enumerate(parameter_list): + if i[:2] == '-I': + parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:])) + + return parameter_list diff --git a/mesonbuild/compilers/mixins/pgi.py b/mesonbuild/compilers/mixins/pgi.py index 212d130..6362b46 100644 --- a/mesonbuild/compilers/mixins/pgi.py +++ b/mesonbuild/compilers/mixins/pgi.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Abstractions for the PGI family of compilers.""" @@ -31,14 +32,14 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -pgi_buildtype_args = { +pgi_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} class PGICompiler(Compiler): diff --git a/mesonbuild/compilers/mixins/ti.py b/mesonbuild/compilers/mixins/ti.py index 950c97f..ae23c84 100644 --- a/mesonbuild/compilers/mixins/ti.py +++ b/mesonbuild/compilers/mixins/ti.py @@ -31,16 +31,16 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -ti_buildtype_args = { +ti_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -ti_optimization_args = { +ti_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-Ooff'], @@ -48,12 +48,12 @@ ti_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-O4'] -} # type: T.Dict[str, T.List[str]] +} -ti_debug_args = { +ti_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: ['-g'] -} # type: T.Dict[bool, T.List[str]] +} class TICompiler(Compiler): @@ -67,12 +67,13 @@ class TICompiler(Compiler): self.can_compile_suffixes.add('asm') # Assembly self.can_compile_suffixes.add('cla') # Control Law Accelerator (CLA) used in C2000 - default_warn_args = [] # type: T.List[str] - self.warn_args = {'0': [], - '1': default_warn_args, - '2': default_warn_args + [], - '3': default_warn_args + [], - 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]] + default_warn_args: T.List[str] = [] + self.warn_args: T.Dict[str, T.List[str]] = { + '0': [], + '1': default_warn_args, + '2': default_warn_args + [], + '3': default_warn_args + [], + 'everything': default_warn_args + []} def get_pic_args(self) -> T.List[str]: # PIC support is not enabled by default for TI compilers, @@ -112,8 +113,8 @@ class TICompiler(Compiler): def get_no_optimization_args(self) -> T.List[str]: return ['-Ooff'] - def get_output_args(self, target: str) -> T.List[str]: - return [f'--output_file={target}'] + def get_output_args(self, outputname: str) -> T.List[str]: + return [f'--output_file={outputname}'] def get_werror_args(self) -> T.List[str]: return ['--emit_warnings_as_errors'] @@ -125,7 +126,7 @@ class TICompiler(Compiler): @classmethod def _unix_args_to_native(cls, args: T.List[str], info: MachineInfo) -> T.List[str]: - result = [] + result: T.List[str] = [] for i in args: if i.startswith('-D'): i = '--define=' + i[2:] diff --git a/mesonbuild/compilers/mixins/visualstudio.py b/mesonbuild/compilers/mixins/visualstudio.py index 765d63d..810dddd 100644 --- a/mesonbuild/compilers/mixins/visualstudio.py +++ b/mesonbuild/compilers/mixins/visualstudio.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Abstractions to simplify compilers that implement an MSVC compatible interface. @@ -23,6 +24,7 @@ import typing as T from ... import arglist from ... import mesonlib from ... import mlog +from mesonbuild.compilers.compilers import CompileCheckMode if T.TYPE_CHECKING: from ...environment import Environment @@ -35,7 +37,7 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -vs32_instruction_set_args = { +vs32_instruction_set_args: T.Dict[str, T.Optional[T.List[str]]] = { 'mmx': ['/arch:SSE'], # There does not seem to be a flag just for MMX 'sse': ['/arch:SSE'], 'sse2': ['/arch:SSE2'], @@ -45,10 +47,10 @@ vs32_instruction_set_args = { 'avx': ['/arch:AVX'], 'avx2': ['/arch:AVX2'], 'neon': None, -} # T.Dicst[str, T.Optional[T.List[str]]] +} # The 64 bit compiler defaults to /arch:avx. -vs64_instruction_set_args = { +vs64_instruction_set_args: T.Dict[str, T.Optional[T.List[str]]] = { 'mmx': ['/arch:AVX'], 'sse': ['/arch:AVX'], 'sse2': ['/arch:AVX'], @@ -59,9 +61,9 @@ vs64_instruction_set_args = { 'avx': ['/arch:AVX'], 'avx2': ['/arch:AVX2'], 'neon': None, -} # T.Dicst[str, T.Optional[T.List[str]]] +} -msvc_optimization_args = { +msvc_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['/Od'], 'g': [], # No specific flag to optimize debugging, /Zi or /ZI will create debug information @@ -69,12 +71,12 @@ msvc_optimization_args = { '2': ['/O2'], '3': ['/O2', '/Gw'], 's': ['/O1', '/Gw'], -} # type: T.Dict[str, T.List[str]] +} -msvc_debug_args = { +msvc_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: ['/Zi'] -} # type: T.Dict[bool, T.List[str]] +} class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): @@ -90,28 +92,30 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): std_warn_args = ['/W3'] std_opt_args = ['/O2'] ignore_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS + ['execinfo'] - internal_libs = [] # type: T.List[str] + internal_libs: T.List[str] = [] - crt_args = { + crt_args: T.Dict[str, T.List[str]] = { 'none': [], 'md': ['/MD'], 'mdd': ['/MDd'], 'mt': ['/MT'], 'mtd': ['/MTd'], - } # type: T.Dict[str, T.List[str]] + } # /showIncludes is needed for build dependency tracking in Ninja # See: https://ninja-build.org/manual.html#_deps # Assume UTF-8 sources by default, but self.unix_args_to_native() removes it # if `/source-charset` is set too. + # It is also dropped if Visual Studio 2013 or earlier is used, since it would + # not be supported in that case. always_args = ['/nologo', '/showIncludes', '/utf-8'] - warn_args = { + warn_args: T.Dict[str, T.List[str]] = { '0': [], '1': ['/W2'], '2': ['/W3'], '3': ['/W4'], 'everything': ['/Wall'], - } # type: T.Dict[str, T.List[str]] + } INVOKES_LINKER = False @@ -137,13 +141,14 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): # Override CCompiler.get_always_args def get_always_args(self) -> T.List[str]: - return self.always_args + # TODO: use ImmutableListProtocol[str] here instead + return self.always_args.copy() def get_pch_suffix(self) -> str: return 'pch' - def get_pch_name(self, header: str) -> str: - chopped = os.path.basename(header).split('.')[:-1] + def get_pch_name(self, name: str) -> str: + chopped = os.path.basename(name).split('.')[:-1] chopped.append(self.get_pch_suffix()) pchname = '.'.join(chopped) return pchname @@ -176,12 +181,12 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): raise mesonlib.MesonException('VS only supports address sanitizer at the moment.') return ['/fsanitize=address'] - def get_output_args(self, target: str) -> T.List[str]: + def get_output_args(self, outputname: str) -> T.List[str]: if self.mode == 'PREPROCESSOR': - return ['/Fi' + target] - if target.endswith('.exe'): - return ['/Fe' + target] - return ['/Fo' + target] + return ['/Fi' + outputname] + if outputname.endswith('.exe'): + return ['/Fe' + outputname] + return ['/Fo' + outputname] def get_buildtype_args(self, buildtype: str) -> T.List[str]: return [] @@ -209,7 +214,7 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): return ['/DEF:' + defsfile] def gen_pch_args(self, header: str, source: str, pchname: str) -> T.Tuple[str, T.List[str]]: - objname = os.path.splitext(pchname)[0] + '.obj' + objname = os.path.splitext(source)[0] + '.obj' return objname, ['/Yc' + header, '/Fp' + pchname, '/Fo' + objname] def openmp_flags(self) -> T.List[str]: @@ -260,7 +265,9 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): continue # cl.exe does not allow specifying both, so remove /utf-8 that we # added automatically in the case the user overrides it manually. - elif i.startswith('/source-charset:') or i.startswith('/execution-charset:'): + elif (i.startswith('/source-charset:') + or i.startswith('/execution-charset:') + or i == '/validate-charset-'): try: result.remove('/utf-8') except ValueError: @@ -270,7 +277,7 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): @classmethod def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]: - result = [] + result: T.List[str] = [] for arg in args: if arg.startswith(('/LIBPATH:', '-LIBPATH:')): result.append('-L' + arg[9:]) @@ -301,8 +308,8 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): # Visual Studio is special. It ignores some arguments it does not # understand and you can't tell it to error out on those. # http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t - def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]: - warning_text = '4044' if mode == 'link' else '9002' + def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: CompileCheckMode) -> T.Tuple[bool, bool]: + warning_text = '4044' if mode == CompileCheckMode.LINK else '9002' with self._build_wrapper(code, env, extra_args=args, mode=mode) as p: if p.returncode != 0: return False, p.cached @@ -359,28 +366,8 @@ class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta): return os.environ['INCLUDE'].split(os.pathsep) def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]: - if crt_val in self.crt_args: - return self.crt_args[crt_val] - assert crt_val in {'from_buildtype', 'static_from_buildtype'} - dbg = 'mdd' - rel = 'md' - if crt_val == 'static_from_buildtype': - dbg = 'mtd' - rel = 'mt' - # Match what build type flags used to do. - if buildtype == 'plain': - return [] - elif buildtype == 'debug': - return self.crt_args[dbg] - elif buildtype == 'debugoptimized': - return self.crt_args[rel] - elif buildtype == 'release': - return self.crt_args[rel] - elif buildtype == 'minsize': - return self.crt_args[rel] - else: - assert buildtype == 'custom' - raise mesonlib.EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".') + crt_val = self.get_crt_val(crt_val, buildtype) + return self.crt_args[crt_val] def has_func_attribute(self, name: str, env: 'Environment') -> T.Tuple[bool, bool]: # MSVC doesn't have __attribute__ like Clang and GCC do, so just return @@ -414,6 +401,15 @@ class MSVCCompiler(VisualStudioLikeCompiler): id = 'msvc' + def __init__(self, target: str): + super().__init__(target) + + # Visual Studio 2013 and earlier don't support the /utf-8 argument. + # We want to remove it. We also want to make an explicit copy so we + # don't mutate class constant state + if mesonlib.version_compare(self.version, '<19.00') and '/utf-8' in self.always_args: + self.always_args = [r for r in self.always_args if r != '/utf-8'] + def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]: args = super().get_compile_debugfile_args(rel_obj, pch) # When generating a PDB file with PCH, all compile commands write @@ -426,11 +422,16 @@ class MSVCCompiler(VisualStudioLikeCompiler): args = ['/FS'] + args return args + # Override CCompiler.get_always_args + # We want to drop '/utf-8' for Visual Studio 2013 and earlier + def get_always_args(self) -> T.List[str]: + return self.always_args + def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: if self.version.split('.')[0] == '16' and instruction_set == 'avx': # VS documentation says that this exists and should work, but # it does not. The headers do not contain AVX intrinsics - # and they can not be called. + # and they cannot be called. return None return super().get_instruction_set_args(instruction_set) @@ -449,9 +450,10 @@ class ClangClCompiler(VisualStudioLikeCompiler): # Assembly self.can_compile_suffixes.add('s') + self.can_compile_suffixes.add('sx') - def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]: - if mode != 'link': + def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: CompileCheckMode) -> T.Tuple[bool, bool]: + if mode != CompileCheckMode.LINK: args = args + ['-Werror=unknown-argument', '-Werror=unknown-warning-option'] return super().has_arguments(args, env, code, mode) @@ -469,7 +471,7 @@ class ClangClCompiler(VisualStudioLikeCompiler): def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]: if dep.get_include_type() == 'system': - converted = [] + converted: T.List[str] = [] for i in dep.get_compile_args(): if i.startswith('-isystem'): converted += ['/clang:' + i] diff --git a/mesonbuild/compilers/mixins/xc16.py b/mesonbuild/compilers/mixins/xc16.py index 09949a2..2b39046 100644 --- a/mesonbuild/compilers/mixins/xc16.py +++ b/mesonbuild/compilers/mixins/xc16.py @@ -31,16 +31,16 @@ else: # do). This gives up DRYer type checking, with no runtime impact Compiler = object -xc16_buildtype_args = { +xc16_buildtype_args: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], -} # type: T.Dict[str, T.List[str]] +} -xc16_optimization_args = { +xc16_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-O0'], @@ -48,12 +48,12 @@ xc16_optimization_args = { '2': ['-O2'], '3': ['-O3'], 's': ['-Os'] -} # type: T.Dict[str, T.List[str]] +} -xc16_debug_args = { +xc16_debug_args: T.Dict[bool, T.List[str]] = { False: [], True: [] -} # type: T.Dict[bool, T.List[str]] +} class Xc16Compiler(Compiler): @@ -65,12 +65,13 @@ class Xc16Compiler(Compiler): raise EnvironmentException('xc16 supports only cross-compilation.') # Assembly self.can_compile_suffixes.add('s') - default_warn_args = [] # type: T.List[str] + self.can_compile_suffixes.add('sx') + default_warn_args: T.List[str] = [] self.warn_args = {'0': [], '1': default_warn_args, '2': default_warn_args + [], '3': default_warn_args + [], - 'everything': default_warn_args + []} # type: T.Dict[str, T.List[str]] + 'everything': default_warn_args + []} def get_always_args(self) -> T.List[str]: return [] diff --git a/mesonbuild/compilers/objc.py b/mesonbuild/compilers/objc.py index 83dcaad..cb8eb9e 100644 --- a/mesonbuild/compilers/objc.py +++ b/mesonbuild/compilers/objc.py @@ -27,7 +27,7 @@ if T.TYPE_CHECKING: from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice diff --git a/mesonbuild/compilers/objcpp.py b/mesonbuild/compilers/objcpp.py index 1f9f756..2297196 100644 --- a/mesonbuild/compilers/objcpp.py +++ b/mesonbuild/compilers/objcpp.py @@ -27,7 +27,7 @@ if T.TYPE_CHECKING: from ..programs import ExternalProgram from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice class ObjCPPCompiler(CLikeCompiler, Compiler): @@ -96,7 +96,9 @@ class ClangObjCPPCompiler(ClangCompiler, ObjCPPCompiler): opts.update({ OptionKey('std', machine=self.for_machine, lang='cpp'): coredata.UserComboOption( 'C++ language standard to use', - ['none', 'c++98', 'c++11', 'c++14', 'c++17', 'gnu++98', 'gnu++11', 'gnu++14', 'gnu++17'], + ['none', 'c++98', 'c++11', 'c++14', 'c++17', 'c++20', 'c++2b', + 'gnu++98', 'gnu++11', 'gnu++14', 'gnu++17', 'gnu++20', + 'gnu++2b'], 'none', ) }) diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 9e5ebc8..1fb94aa 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -15,22 +15,24 @@ from __future__ import annotations import subprocess, os.path import textwrap +import re import typing as T from .. import coredata -from ..mesonlib import EnvironmentException, MesonException, Popen_safe, OptionKey +from ..mesonlib import EnvironmentException, MesonException, Popen_safe_logged, OptionKey from .compilers import Compiler, rust_buildtype_args, clike_debug_args if T.TYPE_CHECKING: from ..coredata import MutableKeyedOptionDictType, KeyedOptionDictType from ..envconfig import MachineInfo from ..environment import Environment # noqa: F401 - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice from ..programs import ExternalProgram + from ..dependencies import Dependency -rust_optimization_args = { +rust_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': [], 'g': ['-C', 'opt-level=0'], @@ -38,7 +40,7 @@ rust_optimization_args = { '2': ['-C', 'opt-level=2'], '3': ['-C', 'opt-level=3'], 's': ['-C', 'opt-level=s'], -} # type: T.Dict[str, T.List[str]] +} class RustCompiler(Compiler): @@ -53,6 +55,17 @@ class RustCompiler(Compiler): '3': ['-W', 'warnings'], } + # Those are static libraries, but we use dylib= here as workaround to avoid + # rust --tests to use /WHOLEARCHIVE. + # https://github.com/rust-lang/rust/issues/116910 + MSVCRT_ARGS: T.Mapping[str, T.List[str]] = { + 'none': [], + 'md': [], # this is the default, no need to inject anything + 'mdd': ['-l', 'dylib=msvcrtd'], + 'mt': ['-l', 'dylib=libcmt'], + 'mtd': ['-l', 'dylib=libcmtd'], + } + def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoice, is_cross: bool, info: 'MachineInfo', exe_wrapper: T.Optional['ExternalProgram'] = None, @@ -62,9 +75,10 @@ class RustCompiler(Compiler): is_cross=is_cross, full_version=full_version, linker=linker) self.exe_wrapper = exe_wrapper - self.base_options.add(OptionKey('b_colorout')) + self.base_options.update({OptionKey(o) for o in ['b_colorout', 'b_ndebug']}) if 'link' in self.linker.id: self.base_options.add(OptionKey('b_vscrt')) + self.native_static_libs: T.List[str] = [] def needs_static_linker(self) -> bool: return False @@ -77,20 +91,11 @@ class RustCompiler(Compiler): '''fn main() { } ''')) - pc = subprocess.Popen(self.exelist + ['-o', output_name, source_name], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=work_dir) - _stdo, _stde = pc.communicate() - assert isinstance(_stdo, bytes) - assert isinstance(_stde, bytes) - stdo = _stdo.decode('utf-8', errors='replace') - stde = _stde.decode('utf-8', errors='replace') + + cmdlist = self.exelist + ['-o', output_name, source_name] + pc, stdo, stde = Popen_safe_logged(cmdlist, cwd=work_dir) if pc.returncode != 0: - raise EnvironmentException('Rust compiler {} can not compile programs.\n{}\n{}'.format( - self.name_string(), - stdo, - stde)) + raise EnvironmentException(f'Rust compiler {self.name_string()} cannot compile programs.') if self.is_cross: if self.exe_wrapper is None: # Can't check if the binaries run so we have to assume they do @@ -101,7 +106,19 @@ class RustCompiler(Compiler): pe = subprocess.Popen(cmdlist, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) pe.wait() if pe.returncode != 0: - raise EnvironmentException('Executables created by Rust compiler %s are not runnable.' % self.name_string()) + raise EnvironmentException(f'Executables created by Rust compiler {self.name_string()} are not runnable.') + # Get libraries needed to link with a Rust staticlib + cmdlist = self.exelist + ['--crate-type', 'staticlib', '--print', 'native-static-libs', source_name] + p, stdo, stde = Popen_safe_logged(cmdlist, cwd=work_dir) + if p.returncode == 0: + match = re.search('native-static-libs: (.*)$', stde, re.MULTILINE) + if match: + # Exclude some well known libraries that we don't need because they + # are always part of C/C++ linkers. Rustc probably should not print + # them, pkg-config for example never specify them. + # FIXME: https://github.com/rust-lang/rust/issues/55120 + exclude = {'-lc', '-lgcc_s', '-lkernel32', '-ladvapi32'} + self.native_static_libs = [i for i in match.group(1).split() if i not in exclude] def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: return ['--dep-info', outfile] @@ -111,7 +128,7 @@ class RustCompiler(Compiler): def get_sysroot(self) -> str: cmd = self.get_exelist(ccache=False) + ['--print', 'sysroot'] - p, stdo, stde = Popen_safe(cmd) + p, stdo, stde = Popen_safe_logged(cmd) return stdo.split('\n', maxsplit=1)[0] def get_debug_args(self, is_debug: bool) -> T.List[str]: @@ -153,6 +170,12 @@ class RustCompiler(Compiler): ), } + def get_dependency_compile_args(self, dep: 'Dependency') -> T.List[str]: + # Rust doesn't have dependency compile arguments so simply return + # nothing here. Dependencies are linked and all required metadata is + # provided by the linker flags. + return [] + def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: args = [] key = OptionKey('std', machine=self.for_machine, lang=self.language) @@ -165,6 +188,11 @@ class RustCompiler(Compiler): # Rust handles this for us, we don't need to do anything return [] + def get_crt_link_args(self, crt_val: str, buildtype: str) -> T.List[str]: + if self.linker.id not in {'link', 'lld-link'}: + return [] + return self.MSVCRT_ARGS[self.get_crt_val(crt_val, buildtype)] + def get_colorout_args(self, colortype: str) -> T.List[str]: if colortype in {'always', 'never', 'auto'}: return [f'--color={colortype}'] @@ -189,14 +217,18 @@ class RustCompiler(Compiler): return self._WARNING_LEVELS["0"] def get_pic_args(self) -> T.List[str]: - # This defaults to - return ['-C', 'relocation-model=pic'] + # relocation-model=pic is rustc's default already. + return [] def get_pie_args(self) -> T.List[str]: # Rustc currently has no way to toggle this, it's controlled by whether # pic is on by rustc return [] + def get_assert_args(self, disable: bool) -> T.List[str]: + action = "no" if disable else "yes" + return ['-C', f'debug-assertions={action}', '-C', 'overflow-checks=no'] + class ClippyRustCompiler(RustCompiler): diff --git a/mesonbuild/compilers/swift.py b/mesonbuild/compilers/swift.py index ec4c7a3..68ef992 100644 --- a/mesonbuild/compilers/swift.py +++ b/mesonbuild/compilers/swift.py @@ -23,10 +23,10 @@ from .compilers import Compiler, swift_buildtype_args, clike_debug_args if T.TYPE_CHECKING: from ..envconfig import MachineInfo from ..environment import Environment - from ..linkers import DynamicLinker + from ..linkers.linkers import DynamicLinker from ..mesonlib import MachineChoice -swift_optimization_args = { +swift_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': [], 'g': [], @@ -34,7 +34,7 @@ swift_optimization_args = { '2': ['-O'], '3': ['-O'], 's': ['-O'], -} # type: T.Dict[str, T.List[str]] +} class SwiftCompiler(Compiler): @@ -116,7 +116,7 @@ class SwiftCompiler(Compiler): pc = subprocess.Popen(self.exelist + extra_flags + ['-emit-executable', '-o', output_name, src], cwd=work_dir) pc.wait() if pc.returncode != 0: - raise EnvironmentException('Swift compiler %s can not compile programs.' % self.name_string()) + raise EnvironmentException('Swift compiler %s cannot compile programs.' % self.name_string()) if self.is_cross: # Can't check if the binaries run so we have to assume they do return diff --git a/mesonbuild/compilers/vala.py b/mesonbuild/compilers/vala.py index e56a9fc..ded158e 100644 --- a/mesonbuild/compilers/vala.py +++ b/mesonbuild/compilers/vala.py @@ -17,9 +17,8 @@ import os.path import typing as T from .. import mlog -from ..mesonlib import EnvironmentException, version_compare, OptionKey - -from .compilers import Compiler, LibType +from ..mesonlib import EnvironmentException, version_compare, LibType, OptionKey +from .compilers import CompileCheckMode, Compiler if T.TYPE_CHECKING: from ..envconfig import MachineInfo @@ -46,7 +45,7 @@ class ValaCompiler(Compiler): def get_debug_args(self, is_debug: bool) -> T.List[str]: return ['--debug'] if is_debug else [] - def get_output_args(self, target: str) -> T.List[str]: + def get_output_args(self, outputname: str) -> T.List[str]: return [] # Because compiles into C. def get_compile_only_args(self) -> T.List[str]: @@ -64,7 +63,7 @@ class ValaCompiler(Compiler): def get_always_args(self) -> T.List[str]: return ['-C'] - def get_warn_args(self, warning_level: str) -> T.List[str]: + def get_warn_args(self, level: str) -> T.List[str]: return [] def get_no_warn_args(self) -> T.List[str]: @@ -100,9 +99,9 @@ class ValaCompiler(Compiler): extra_flags += self.get_compile_only_args() else: extra_flags += environment.coredata.get_external_link_args(self.for_machine, self.language) - with self.cached_compile(code, environment.coredata, extra_args=extra_flags, mode='compile') as p: + with self.cached_compile(code, environment.coredata, extra_args=extra_flags, mode=CompileCheckMode.COMPILE) as p: if p.returncode != 0: - msg = f'Vala compiler {self.name_string()!r} can not compile programs' + msg = f'Vala compiler {self.name_string()!r} cannot compile programs' raise EnvironmentException(msg) def get_buildtype_args(self, buildtype: str) -> T.List[str]: @@ -111,7 +110,7 @@ class ValaCompiler(Compiler): return [] def find_library(self, libname: str, env: 'Environment', extra_dirs: T.List[str], - libtype: LibType = LibType.PREFER_SHARED) -> T.Optional[T.List[str]]: + libtype: LibType = LibType.PREFER_SHARED, lib_prefix_warning: bool = True) -> T.Optional[T.List[str]]: if extra_dirs and isinstance(extra_dirs, str): extra_dirs = [extra_dirs] # Valac always looks in the default vapi dir, so only search there if @@ -122,7 +121,7 @@ class ValaCompiler(Compiler): args += env.coredata.get_external_args(self.for_machine, self.language) vapi_args = ['--pkg', libname] args += vapi_args - with self.cached_compile(code, env.coredata, extra_args=args, mode='compile') as p: + with self.cached_compile(code, env.coredata, extra_args=args, mode=CompileCheckMode.COMPILE) as p: if p.returncode == 0: return vapi_args # Not found? Try to find the vapi file itself. diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 6d63625..ff5e795 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -1,5 +1,4 @@ -# Copyright 2012-2022 The Meson development team - +# Copyright 2013-2023 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -11,19 +10,26 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + +import copy from . import mlog, mparser import pickle, os, uuid import sys from itertools import chain from pathlib import PurePath -from collections import OrderedDict +from collections import OrderedDict, abc +from dataclasses import dataclass + from .mesonlib import ( HoldableObject, MesonException, EnvironmentException, MachineChoice, PerMachine, PerMachineDefaultable, default_libdir, default_libexecdir, - default_prefix, split_args, OptionKey, OptionType, stringlistify, - pickle_load, replace_if_different + default_prefix, default_datadir, default_includedir, default_infodir, + default_localedir, default_mandir, default_sbindir, default_sysconfdir, + split_args, OptionKey, OptionType, stringlistify, + pickle_load ) from .wrap import WrapMode import ast @@ -35,16 +41,18 @@ import typing as T if T.TYPE_CHECKING: from . import dependencies - from .compilers.compilers import Compiler, CompileResult + from .compilers.compilers import Compiler, CompileResult, RunResult, CompileCheckMode from .dependencies.detect import TV_DepID from .environment import Environment - from .mesonlib import OptionOverrideProxy, FileOrString + from .mesonlib import FileOrString from .cmake.traceparser import CMakeCacheEntry - OptionDictType = T.Union[T.Dict[str, 'UserOption[T.Any]'], OptionOverrideProxy] + OptionDictType = T.Union[T.Dict[str, 'UserOption[T.Any]'], 'OptionsView'] MutableKeyedOptionDictType = T.Dict['OptionKey', 'UserOption[T.Any]'] - KeyedOptionDictType = T.Union[MutableKeyedOptionDictType, OptionOverrideProxy] - CompilerCheckCacheKey = T.Tuple[T.Tuple[str, ...], str, FileOrString, T.Tuple[str, ...], str] + KeyedOptionDictType = T.Union[MutableKeyedOptionDictType, 'OptionsView'] + CompilerCheckCacheKey = T.Tuple[T.Tuple[str, ...], str, FileOrString, T.Tuple[str, ...], CompileCheckMode] + # code, args + RunCheckCacheKey = T.Tuple[str, T.Tuple[str, ...]] # typeshed StrOrBytesPath = T.Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] @@ -53,36 +61,56 @@ if T.TYPE_CHECKING: # # Pip requires that RCs are named like this: '0.1.0.rc1' # But the corresponding Git tag needs to be '0.1.0rc1' -version = '1.0.0' +version = '1.3.0' + +# The next stable version when we are in dev. This is used to allow projects to +# require meson version >=1.2.0 when using 1.1.99. FeatureNew won't warn when +# using a feature introduced in 1.2.0 when using Meson 1.1.99. +stable_version = version +if stable_version.endswith('.99'): + stable_version_array = stable_version.split('.') + stable_version_array[-1] = '0' + stable_version_array[-2] = str(int(stable_version_array[-2]) + 1) + stable_version = '.'.join(stable_version_array) -backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode'] +backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode', 'none'] +genvslitelist = ['vs2022'] +buildtypelist = ['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'] -default_yielding = False +DEFAULT_YIELDING = False # Can't bind this near the class method it seems, sadly. _T = T.TypeVar('_T') +def get_genvs_default_buildtype_list() -> list[str]: + # just debug, debugoptimized, and release for now + # but this should probably be configurable through some extra option, alongside --genvslite. + return buildtypelist[1:-2] + + class MesonVersionMismatchException(MesonException): '''Build directory generated with Meson version is incompatible with current version''' - def __init__(self, old_version: str, current_version: str) -> None: + def __init__(self, old_version: str, current_version: str, extra_msg: str = '') -> None: super().__init__(f'Build directory has been generated with Meson version {old_version}, ' - f'which is incompatible with the current version {current_version}.') + f'which is incompatible with the current version {current_version}.' + + extra_msg) self.old_version = old_version self.current_version = current_version class UserOption(T.Generic[_T], HoldableObject): - def __init__(self, description: str, choices: T.Optional[T.Union[str, T.List[_T]]], yielding: T.Optional[bool]): + def __init__(self, description: str, choices: T.Optional[T.Union[str, T.List[_T]]], + yielding: bool, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): super().__init__() self.choices = choices self.description = description - if yielding is None: - yielding = default_yielding if not isinstance(yielding, bool): raise MesonException('Value of "yielding" must be a boolean.') self.yielding = yielding - self.deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False + self.deprecated = deprecated + self.readonly = False def listify(self, value: T.Any) -> T.List[T.Any]: return [value] @@ -97,12 +125,15 @@ class UserOption(T.Generic[_T], HoldableObject): def validate_value(self, value: T.Any) -> _T: raise RuntimeError('Derived option class did not override validate_value.') - def set_value(self, newvalue: T.Any) -> None: + def set_value(self, newvalue: T.Any) -> bool: + oldvalue = getattr(self, 'value', None) self.value = self.validate_value(newvalue) + return self.value != oldvalue class UserStringOption(UserOption[str]): - def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None): - super().__init__(description, None, yielding) + def __init__(self, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, None, yielding, deprecated) self.set_value(value) def validate_value(self, value: T.Any) -> str: @@ -111,8 +142,9 @@ class UserStringOption(UserOption[str]): return value class UserBooleanOption(UserOption[bool]): - def __init__(self, description: str, value, yielding: T.Optional[bool] = None) -> None: - super().__init__(description, [True, False], yielding) + def __init__(self, description: str, value: bool, yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, [True, False], yielding, deprecated) self.set_value(value) def __bool__(self) -> bool: @@ -130,17 +162,18 @@ class UserBooleanOption(UserOption[bool]): raise MesonException('Value %s is not boolean (true or false).' % value) class UserIntegerOption(UserOption[int]): - def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None): + def __init__(self, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): min_value, max_value, default_value = value self.min_value = min_value self.max_value = max_value - c = [] + c: T.List[str] = [] if min_value is not None: c.append('>=' + str(min_value)) if max_value is not None: c.append('<=' + str(max_value)) choices = ', '.join(c) - super().__init__(description, choices, yielding) + super().__init__(description, choices, yielding, deprecated) self.set_value(default_value) def validate_value(self, value: T.Any) -> int: @@ -168,8 +201,9 @@ class OctalInt(int): return oct(int(self)) class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]): - def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None): - super().__init__(description, (0, 0o777, value), yielding) + def __init__(self, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, (0, 0o777, value), yielding, deprecated) self.choices = ['preserve', '0000-0777'] def printable_value(self) -> str: @@ -178,7 +212,7 @@ class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]): return format(self.value, '04o') def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]: - if value is None or value == 'preserve': + if value == 'preserve': return 'preserve' return OctalInt(super().validate_value(value)) @@ -189,8 +223,10 @@ class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]): raise MesonException(f'Invalid mode: {e}') class UserComboOption(UserOption[str]): - def __init__(self, description: str, choices: T.List[str], value: T.Any, yielding: T.Optional[bool] = None): - super().__init__(description, choices, yielding) + def __init__(self, description: str, choices: T.List[str], value: T.Any, + yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, choices, yielding, deprecated) if not isinstance(self.choices, list): raise MesonException('Combo choices must be an array.') for i in self.choices: @@ -213,20 +249,18 @@ class UserComboOption(UserOption[str]): return value class UserArrayOption(UserOption[T.List[str]]): - def __init__(self, description: str, value: T.Union[str, T.List[str]], split_args: bool = False, user_input: bool = False, allow_dups: bool = False, **kwargs: T.Any) -> None: - super().__init__(description, kwargs.get('choices', []), yielding=kwargs.get('yielding', None)) + def __init__(self, description: str, value: T.Union[str, T.List[str]], + split_args: bool = False, + allow_dups: bool = False, yielding: bool = DEFAULT_YIELDING, + choices: T.Optional[T.List[str]] = None, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, choices if choices is not None else [], yielding, deprecated) self.split_args = split_args self.allow_dups = allow_dups - self.value = self.validate_value(value, user_input=user_input) - - def listify(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]: - # User input is for options defined on the command line (via -D - # options). Users can put their input in as a comma separated - # string, but for defining options in meson_options.txt the format - # should match that of a combo - if not user_input and isinstance(value, str) and not value.startswith('['): - raise MesonException('Value does not define an array: ' + value) + self.set_value(value) + @staticmethod + def listify_value(value: T.Union[str, T.List[str]], shlex_split_args: bool = False) -> T.List[str]: if isinstance(value, str): if value.startswith('['): try: @@ -236,7 +270,7 @@ class UserArrayOption(UserOption[T.List[str]]): elif value == '': newvalue = [] else: - if self.split_args: + if shlex_split_args: newvalue = split_args(value) else: newvalue = [v.strip() for v in value.split(',')] @@ -246,8 +280,11 @@ class UserArrayOption(UserOption[T.List[str]]): raise MesonException(f'"{value}" should be a string array, but it is not') return newvalue - def validate_value(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]: - newvalue = self.listify(value, user_input) + def listify(self, value: T.Any) -> T.List[T.Any]: + return self.listify_value(value, self.split_args) + + def validate_value(self, value: T.Union[str, T.List[str]]) -> T.List[str]: + newvalue = self.listify(value) if not self.allow_dups and len(set(newvalue)) != len(newvalue): msg = 'Duplicated values in array option is deprecated. ' \ @@ -272,8 +309,9 @@ class UserArrayOption(UserOption[T.List[str]]): class UserFeatureOption(UserComboOption): static_choices = ['enabled', 'disabled', 'auto'] - def __init__(self, description: str, value: T.Any, yielding: T.Optional[bool] = None): - super().__init__(description, self.static_choices, value, yielding) + def __init__(self, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, + deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): + super().__init__(description, self.static_choices, value, yielding, deprecated) self.name: T.Optional[str] = None # TODO: Refactor options to all store their name def is_enabled(self) -> bool: @@ -285,6 +323,95 @@ class UserFeatureOption(UserComboOption): def is_auto(self) -> bool: return self.value == 'auto' +class UserStdOption(UserComboOption): + ''' + UserOption specific to c_std and cpp_std options. User can set a list of + STDs in preference order and it selects the first one supported by current + compiler. + + For historical reasons, some compilers (msvc) allowed setting a GNU std and + silently fell back to C std. This is now deprecated. Projects that support + both GNU and MSVC compilers should set e.g. c_std=gnu11,c11. + + This is not using self.deprecated mechanism we already have for project + options because we want to print a warning if ALL values are deprecated, not + if SOME values are deprecated. + ''' + def __init__(self, lang: str, all_stds: T.List[str]) -> None: + self.lang = lang.lower() + self.all_stds = ['none'] + all_stds + # Map a deprecated std to its replacement. e.g. gnu11 -> c11. + self.deprecated_stds: T.Dict[str, str] = {} + super().__init__(f'{lang} language standard to use', ['none'], 'none') + + def set_versions(self, versions: T.List[str], gnu: bool = False, gnu_deprecated: bool = False) -> None: + assert all(std in self.all_stds for std in versions) + self.choices += versions + if gnu: + gnu_stds_map = {f'gnu{std[1:]}': std for std in versions} + if gnu_deprecated: + self.deprecated_stds.update(gnu_stds_map) + else: + self.choices += gnu_stds_map.keys() + + def validate_value(self, value: T.Union[str, T.List[str]]) -> str: + candidates = UserArrayOption.listify_value(value) + unknown = [std for std in candidates if std not in self.all_stds] + if unknown: + raise MesonException(f'Unknown {self.lang.upper()} std {unknown}. Possible values are {self.all_stds}.') + # Check first if any of the candidates are not deprecated + for std in candidates: + if std in self.choices: + return std + # Fallback to a deprecated std if any + for std in candidates: + newstd = self.deprecated_stds.get(std) + if newstd is not None: + mlog.deprecation( + f'None of the values {candidates} are supported by the {self.lang} compiler.\n' + + f'However, the deprecated {std} std currently falls back to {newstd}.\n' + + 'This will be an error in the future.\n' + + 'If the project supports both GNU and MSVC compilers, a value such as\n' + + '"c_std=gnu11,c11" specifies that GNU is prefered but it can safely fallback to plain c11.') + return newstd + raise MesonException(f'None of values {candidates} are supported by the {self.lang.upper()} compiler. ' + + f'Possible values are {self.choices}') + +@dataclass +class OptionsView(abc.Mapping): + '''A view on an options dictionary for a given subproject and with overrides. + ''' + + # TODO: the typing here could be made more explicit using a TypeDict from + # python 3.8 or typing_extensions + options: KeyedOptionDictType + subproject: T.Optional[str] = None + overrides: T.Optional[T.Mapping[OptionKey, T.Union[str, int, bool, T.List[str]]]] = None + + def __getitem__(self, key: OptionKey) -> UserOption: + # FIXME: This is fundamentally the same algorithm than interpreter.get_option_internal(). + # We should try to share the code somehow. + key = key.evolve(subproject=self.subproject) + if not key.is_project(): + opt = self.options.get(key) + if opt is None or opt.yielding: + opt = self.options[key.as_root()] + else: + opt = self.options[key] + if opt.yielding: + opt = self.options.get(key.as_root(), opt) + if self.overrides: + override_value = self.overrides.get(key.as_root()) + if override_value is not None: + opt = copy.copy(opt) + opt.set_value(override_value) + return opt + + def __iter__(self) -> T.Iterator[OptionKey]: + return iter(self.options) + + def __len__(self) -> int: + return len(self.options) class DependencyCacheType(enum.Enum): @@ -294,11 +421,10 @@ class DependencyCacheType(enum.Enum): @classmethod def from_type(cls, dep: 'dependencies.Dependency') -> 'DependencyCacheType': - from . import dependencies # As more types gain search overrides they'll need to be added here - if isinstance(dep, dependencies.PkgConfigDependency): + if dep.type_name == 'pkgconfig': return cls.PKG_CONFIG - if isinstance(dep, dependencies.CMakeDependency): + if dep.type_name == 'cmake': return cls.CMAKE return cls.OTHER @@ -331,13 +457,13 @@ class DependencyCache: """ def __init__(self, builtins: 'KeyedOptionDictType', for_machine: MachineChoice): - self.__cache = OrderedDict() # type: T.MutableMapping[TV_DepID, DependencySubCache] + self.__cache: T.MutableMapping[TV_DepID, DependencySubCache] = OrderedDict() self.__builtins = builtins self.__pkg_conf_key = OptionKey('pkg_config_path', machine=for_machine) self.__cmake_key = OptionKey('cmake_prefix_path', machine=for_machine) def __calculate_subkey(self, type_: DependencyCacheType) -> T.Tuple[str, ...]: - data: T.Dict[str, T.List[str]] = { + data: T.Dict[DependencyCacheType, T.List[str]] = { DependencyCacheType.PKG_CONFIG: stringlistify(self.__builtins[self.__pkg_conf_key].value), DependencyCacheType.CMAKE: stringlistify(self.__builtins[self.__cmake_key].value), DependencyCacheType.OTHER: [], @@ -382,7 +508,7 @@ class DependencyCache: def items(self) -> T.Iterator[T.Tuple['TV_DepID', T.List['dependencies.Dependency']]]: for k, v in self.__cache.items(): - vs = [] + vs: T.List[dependencies.Dependency] = [] for t in v.types: subkey = self.__calculate_subkey(t) if subkey in v: @@ -445,20 +571,21 @@ class CoreData: self.version = version self.options: 'MutableKeyedOptionDictType' = {} self.cross_files = self.__load_config_files(options, scratch_dir, 'cross') - self.compilers = PerMachine(OrderedDict(), OrderedDict()) # type: PerMachine[T.Dict[str, Compiler]] + self.compilers: PerMachine[T.Dict[str, Compiler]] = PerMachine(OrderedDict(), OrderedDict()) # Set of subprojects that have already been initialized once, this is # required to be stored and reloaded with the coredata, as we don't # want to overwrite options for such subprojects. self.initialized_subprojects: T.Set[str] = set() - # For host == build configuraitons these caches should be the same. + # For host == build configurations these caches should be the same. self.deps: PerMachine[DependencyCache] = PerMachineDefaultable.default( self.is_cross_build(), DependencyCache(self.options, MachineChoice.BUILD), DependencyCache(self.options, MachineChoice.HOST)) self.compiler_check_cache: T.Dict['CompilerCheckCacheKey', 'CompileResult'] = OrderedDict() + self.run_check_cache: T.Dict['RunCheckCacheKey', 'RunResult'] = OrderedDict() # CMake cache self.cmake_cache: PerMachine[CMakeStateCache] = PerMachine(CMakeStateCache(), CMakeStateCache()) @@ -480,9 +607,9 @@ class CoreData: if not filenames: return [] - found_invalid = [] # type: T.List[str] - missing = [] # type: T.List[str] - real = [] # type: T.List[str] + found_invalid: T.List[str] = [] + missing: T.List[str] = [] + real: T.List[str] = [] for i, f in enumerate(filenames): f = os.path.expanduser(os.path.expandvars(f)) if os.path.exists(f): @@ -533,7 +660,7 @@ class CoreData: if self.cross_files: BUILTIN_OPTIONS[OptionKey('libdir')].default = 'lib' - def sanitize_prefix(self, prefix): + def sanitize_prefix(self, prefix: str) -> str: prefix = os.path.expanduser(prefix) if not os.path.isabs(prefix): raise MesonException(f'prefix value {prefix!r} must be an absolute path') @@ -565,7 +692,7 @@ class CoreData: except TypeError: return value if option.name.endswith('dir') and value.is_absolute() and \ - option not in BULITIN_DIR_NOPREFIX_OPTIONS: + option not in BUILTIN_DIR_NOPREFIX_OPTIONS: try: # Try to relativize the path. value = value.relative_to(prefix) @@ -633,7 +760,8 @@ class CoreData: raise MesonException(f'Tried to get unknown builtin option {str(key)}') - def set_option(self, key: OptionKey, value) -> None: + def set_option(self, key: OptionKey, value, first_invocation: bool = False) -> bool: + dirty = False if key.is_builtin(): if key.name == 'prefix': value = self.sanitize_prefix(value) @@ -673,24 +801,26 @@ class CoreData: newname = opt.deprecated newkey = OptionKey.from_string(newname).evolve(subproject=key.subproject) mlog.deprecation(f'Option {key.name!r} is replaced by {newname!r}') - self.set_option(newkey, value) + dirty |= self.set_option(newkey, value, first_invocation) - opt.set_value(value) + changed = opt.set_value(value) + if changed and opt.readonly and not first_invocation: + raise MesonException(f'Tried modify read only option {str(key)!r}') + dirty |= changed if key.name == 'buildtype': - self._set_others_from_buildtype(value) - elif key.name in {'wrap_mode', 'force_fallback_for'}: - # We could have the system dependency cached for a dependency that - # is now forced to use subproject fallback. We probably could have - # more fine grained cache invalidation, but better be safe. - self.clear_deps_cache() - - def clear_deps_cache(self): + dirty |= self._set_others_from_buildtype(value) + + return dirty + + def clear_cache(self) -> None: self.deps.host.clear() self.deps.build.clear() + self.compiler_check_cache.clear() + self.run_check_cache.clear() - def get_nondefault_buildtype_args(self): - result = [] + def get_nondefault_buildtype_args(self) -> T.List[T.Union[T.Tuple[str, str, str], T.Tuple[str, bool, bool]]]: + result: T.List[T.Union[T.Tuple[str, str, str], T.Tuple[str, bool, bool]]] = [] value = self.options[OptionKey('buildtype')].value if value == 'plain': opt = 'plain' @@ -718,7 +848,9 @@ class CoreData: result.append(('debug', actual_debug, debug)) return result - def _set_others_from_buildtype(self, value: str) -> None: + def _set_others_from_buildtype(self, value: str) -> bool: + dirty = False + if value == 'plain': opt = 'plain' debug = False @@ -736,9 +868,12 @@ class CoreData: debug = True else: assert value == 'custom' - return - self.options[OptionKey('optimization')].set_value(opt) - self.options[OptionKey('debug')].set_value(debug) + return False + + dirty |= self.options[OptionKey('optimization')].set_value(opt) + dirty |= self.options[OptionKey('debug')].set_value(debug) + + return dirty @staticmethod def is_per_machine_option(optname: OptionKey) -> bool: @@ -746,17 +881,6 @@ class CoreData: return True return optname.lang is not None - def validate_option_value(self, option_name: OptionKey, override_value): - try: - opt = self.options[option_name] - except KeyError: - raise MesonException(f'Tried to validate unknown option {str(option_name)}') - try: - return opt.validate_value(override_value) - except MesonException as e: - raise type(e)(('Validation failed for option %s: ' % option_name) + str(e)) \ - .with_traceback(sys.exc_info()[2]) - def get_external_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]: return self.options[OptionKey('args', machine=for_machine, lang=lang)].value @@ -772,7 +896,7 @@ class CoreData: continue oldval = self.options[key] - if type(oldval) != type(value): + if type(oldval) is not type(value): self.options[key] = value elif oldval.choices != value.choices: # If the choices have changed, use the new value, but attempt @@ -782,45 +906,50 @@ class CoreData: try: value.set_value(oldval.value) except MesonException: - mlog.warning(f'Old value(s) of {key} are no longer valid, resetting to default ({value.value}).') + mlog.warning(f'Old value(s) of {key} are no longer valid, resetting to default ({value.value}).', + fatal=False) def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool: if when_building_for == MachineChoice.BUILD: return False return len(self.cross_files) > 0 - def copy_build_options_from_regular_ones(self) -> None: + def copy_build_options_from_regular_ones(self) -> bool: + dirty = False assert not self.is_cross_build() for k in BUILTIN_OPTIONS_PER_MACHINE: o = self.options[k] - self.options[k.as_build()].set_value(o.value) + dirty |= self.options[k.as_build()].set_value(o.value) for bk, bv in self.options.items(): if bk.machine is MachineChoice.BUILD: hk = bk.as_host() try: hv = self.options[hk] - bv.set_value(hv.value) + dirty |= bv.set_value(hv.value) except KeyError: continue - def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '') -> None: + return dirty + + def set_options(self, options: T.Dict[OptionKey, T.Any], subproject: str = '', first_invocation: bool = False) -> bool: + dirty = False if not self.is_cross_build(): options = {k: v for k, v in options.items() if k.machine is not MachineChoice.BUILD} # Set prefix first because it's needed to sanitize other options pfk = OptionKey('prefix') if pfk in options: prefix = self.sanitize_prefix(options[pfk]) - self.options[OptionKey('prefix')].set_value(prefix) - for key in BULITIN_DIR_NOPREFIX_OPTIONS: + dirty |= self.options[OptionKey('prefix')].set_value(prefix) + for key in BUILTIN_DIR_NOPREFIX_OPTIONS: if key not in options: - self.options[key].set_value(BUILTIN_OPTIONS[key].prefixed_default(key, prefix)) + dirty |= self.options[key].set_value(BUILTIN_OPTIONS[key].prefixed_default(key, prefix)) unknown_options: T.List[OptionKey] = [] for k, v in options.items(): if k == pfk: continue elif k in self.options: - self.set_option(k, v) + dirty |= self.set_option(k, v, first_invocation) elif k.machine != MachineChoice.BUILD and k.type != OptionType.COMPILER: unknown_options.append(k) if unknown_options: @@ -829,11 +958,13 @@ class CoreData: raise MesonException(f'{sub}Unknown options: "{unknown_options_str}"') if not self.is_cross_build(): - self.copy_build_options_from_regular_ones() + dirty |= self.copy_build_options_from_regular_ones() + + return dirty def set_default_options(self, default_options: T.MutableMapping[OptionKey, str], subproject: str, env: 'Environment') -> None: # Main project can set default options on subprojects, but subprojects - # can only set default options on themself. + # can only set default options on themselves. # Preserve order: if env.options has 'buildtype' it must come after # 'optimization' if it is in default_options. options: T.MutableMapping[OptionKey, T.Any] = OrderedDict() @@ -867,7 +998,7 @@ class CoreData: continue options[k] = v - self.set_options(options, subproject=subproject) + self.set_options(options, subproject=subproject, first_invocation=env.first_invocation) def add_compiler_options(self, options: 'MutableKeyedOptionDictType', lang: str, for_machine: MachineChoice, env: 'Environment') -> None: @@ -889,14 +1020,13 @@ class CoreData: def process_new_compiler(self, lang: str, comp: 'Compiler', env: 'Environment') -> None: from . import compilers - self.compilers[comp.for_machine][lang] = comp self.add_compiler_options(comp.get_options(), lang, comp.for_machine, env) enabled_opts: T.List[OptionKey] = [] for key in comp.base_options: if key in self.options: continue - oobj = compilers.base_options[key] + oobj = copy.deepcopy(compilers.base_options[key]) if key in env.options: oobj.set_value(env.options[key]) enabled_opts.append(key) @@ -914,20 +1044,28 @@ class CmdLineFileParser(configparser.ConfigParser): # storing subproject options like "subproject:option=value" super().__init__(delimiters=['='], interpolation=None) - def read(self, filenames: T.Union['StrOrBytesPath', T.Iterable['StrOrBytesPath']], encoding: str = 'utf-8') -> T.List[str]: + def read(self, filenames: T.Union['StrOrBytesPath', T.Iterable['StrOrBytesPath']], encoding: T.Optional[str] = 'utf-8') -> T.List[str]: return super().read(filenames, encoding) - def optionxform(self, option: str) -> str: + def optionxform(self, optionstr: str) -> str: # Don't call str.lower() on keys - return option + return optionstr class MachineFileParser(): - def __init__(self, filenames: T.List[str]) -> None: + def __init__(self, filenames: T.List[str], sourcedir: str) -> None: self.parser = CmdLineFileParser() - self.constants = {'True': True, 'False': False} - self.sections = {} - - self.parser.read(filenames) + self.constants: T.Dict[str, T.Union[str, bool, int, T.List[str]]] = {'True': True, 'False': False} + self.sections: T.Dict[str, T.Dict[str, T.Union[str, bool, int, T.List[str]]]] = {} + + for fname in filenames: + with open(fname, encoding='utf-8') as f: + content = f.read() + content = content.replace('@GLOBAL_SOURCE_ROOT@', sourcedir) + content = content.replace('@DIRNAME@', os.path.dirname(fname)) + try: + self.parser.read_string(content, fname) + except configparser.Error as e: + raise EnvironmentException(f'Malformed machine file: {e}') # Parse [constants] first so they can be used in other sections if self.parser.has_section('constants'): @@ -938,9 +1076,9 @@ class MachineFileParser(): continue self.sections[s] = self._parse_section(s) - def _parse_section(self, s): + def _parse_section(self, s: str) -> T.Dict[str, T.Union[str, bool, int, T.List[str]]]: self.scope = self.constants.copy() - section = {} + section: T.Dict[str, T.Union[str, bool, int, T.List[str]]] = {} for entry, value in self.parser.items(s): if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry: raise EnvironmentException(f'Malformed variable name {entry!r} in machine file.') @@ -948,23 +1086,26 @@ class MachineFileParser(): value = value.replace('\\', '\\\\') try: ast = mparser.Parser(value, 'machinefile').parse() + if not ast.lines: + raise EnvironmentException('value cannot be empty') res = self._evaluate_statement(ast.lines[0]) - except MesonException: - raise EnvironmentException(f'Malformed value in machine file variable {entry!r}.') + except MesonException as e: + raise EnvironmentException(f'Malformed value in machine file variable {entry!r}: {str(e)}.') except KeyError as e: raise EnvironmentException(f'Undefined constant {e.args[0]!r} in machine file variable {entry!r}.') section[entry] = res self.scope[entry] = res return section - def _evaluate_statement(self, node): - if isinstance(node, (mparser.StringNode)): + def _evaluate_statement(self, node: mparser.BaseNode) -> T.Union[str, bool, int, T.List[str]]: + if isinstance(node, (mparser.BaseStringNode)): return node.value elif isinstance(node, mparser.BooleanNode): return node.value elif isinstance(node, mparser.NumberNode): return node.value elif isinstance(node, mparser.ArrayNode): + # TODO: This is where recursive types would come in handy return [self._evaluate_statement(arg) for arg in node.args.arguments] elif isinstance(node, mparser.IdNode): return self.scope[node.value] @@ -980,8 +1121,8 @@ class MachineFileParser(): return os.path.join(l, r) raise EnvironmentException('Unsupported node type') -def parse_machine_files(filenames): - parser = MachineFileParser(filenames) +def parse_machine_files(filenames: T.List[str], sourcedir: str): + parser = MachineFileParser(filenames, sourcedir) return parser.sections def get_cmd_line_file(build_dir: str) -> str: @@ -1013,7 +1154,7 @@ def write_cmd_line_file(build_dir: str, options: argparse.Namespace) -> None: filename = get_cmd_line_file(build_dir) config = CmdLineFileParser() - properties = OrderedDict() + properties: OrderedDict[str, str] = OrderedDict() if options.cross_file: properties['cross_file'] = options.cross_file if options.native_file: @@ -1046,11 +1187,9 @@ def major_versions_differ(v1: str, v2: str) -> bool: # Major version differ, or one is development version but not the other. return v1_major != v2_major or ('99' in {v1_minor, v2_minor} and v1_minor != v2_minor) -def load(build_dir: str) -> CoreData: +def load(build_dir: str, suggest_reconfigure: bool = True) -> CoreData: filename = os.path.join(build_dir, 'meson-private', 'coredata.dat') - obj = pickle_load(filename, 'Coredata', CoreData) - assert isinstance(obj, CoreData), 'for mypy' - return obj + return pickle_load(filename, 'Coredata', CoreData, suggest_reconfigure) def save(obj: CoreData, build_dir: str) -> str: @@ -1066,7 +1205,7 @@ def save(obj: CoreData, build_dir: str) -> str: pickle.dump(obj, f) f.flush() os.fsync(f.fileno()) - replace_if_different(filename, tempfilename) + os.replace(tempfilename, filename) return filename @@ -1122,12 +1261,13 @@ class BuiltinOption(T.Generic[_T, _U]): """ def __init__(self, opt_type: T.Type[_U], description: str, default: T.Any, yielding: bool = True, *, - choices: T.Any = None): + choices: T.Any = None, readonly: bool = False): self.opt_type = opt_type self.description = description self.default = default self.choices = choices self.yielding = yielding + self.readonly = readonly def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> _U: """Create an instance of opt_type and return it.""" @@ -1136,7 +1276,9 @@ class BuiltinOption(T.Generic[_T, _U]): keywords = {'yielding': self.yielding, 'value': value} if self.choices: keywords['choices'] = self.choices - return self.opt_type(self.description, **keywords) + o = self.opt_type(self.description, **keywords) + o.readonly = self.readonly + return o def _argparse_action(self) -> T.Optional[str]: # If the type is a boolean, the presence of the argument in --foo form @@ -1164,7 +1306,7 @@ class BuiltinOption(T.Generic[_T, _U]): if self.opt_type in [UserComboOption, UserIntegerOption]: return self.default try: - return BULITIN_DIR_NOPREFIX_OPTIONS[name][prefix] + return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix] except KeyError: pass return self.default @@ -1190,27 +1332,38 @@ class BuiltinOption(T.Generic[_T, _U]): # Update `docs/markdown/Builtin-options.md` after changing the options below # Also update mesonlib._BUILTIN_NAMES. See the comment there for why this is required. +# Please also update completion scripts in $MESONSRC/data/shell-completions/ BUILTIN_DIR_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([ (OptionKey('prefix'), BuiltinOption(UserStringOption, 'Installation prefix', default_prefix())), (OptionKey('bindir'), BuiltinOption(UserStringOption, 'Executable directory', 'bin')), - (OptionKey('datadir'), BuiltinOption(UserStringOption, 'Data file directory', 'share')), - (OptionKey('includedir'), BuiltinOption(UserStringOption, 'Header file directory', 'include')), - (OptionKey('infodir'), BuiltinOption(UserStringOption, 'Info page directory', 'share/info')), + (OptionKey('datadir'), BuiltinOption(UserStringOption, 'Data file directory', default_datadir())), + (OptionKey('includedir'), BuiltinOption(UserStringOption, 'Header file directory', default_includedir())), + (OptionKey('infodir'), BuiltinOption(UserStringOption, 'Info page directory', default_infodir())), (OptionKey('libdir'), BuiltinOption(UserStringOption, 'Library directory', default_libdir())), + (OptionKey('licensedir'), BuiltinOption(UserStringOption, 'Licenses directory', '')), (OptionKey('libexecdir'), BuiltinOption(UserStringOption, 'Library executable directory', default_libexecdir())), - (OptionKey('localedir'), BuiltinOption(UserStringOption, 'Locale data directory', 'share/locale')), + (OptionKey('localedir'), BuiltinOption(UserStringOption, 'Locale data directory', default_localedir())), (OptionKey('localstatedir'), BuiltinOption(UserStringOption, 'Localstate data directory', 'var')), - (OptionKey('mandir'), BuiltinOption(UserStringOption, 'Manual page directory', 'share/man')), - (OptionKey('sbindir'), BuiltinOption(UserStringOption, 'System executable directory', 'sbin')), + (OptionKey('mandir'), BuiltinOption(UserStringOption, 'Manual page directory', default_mandir())), + (OptionKey('sbindir'), BuiltinOption(UserStringOption, 'System executable directory', default_sbindir())), (OptionKey('sharedstatedir'), BuiltinOption(UserStringOption, 'Architecture-independent data directory', 'com')), - (OptionKey('sysconfdir'), BuiltinOption(UserStringOption, 'Sysconf data directory', 'etc')), + (OptionKey('sysconfdir'), BuiltinOption(UserStringOption, 'Sysconf data directory', default_sysconfdir())), ]) BUILTIN_CORE_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([ (OptionKey('auto_features'), BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')), - (OptionKey('backend'), BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist)), + (OptionKey('backend'), BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist, + readonly=True)), + (OptionKey('genvslite'), + BuiltinOption( + UserComboOption, + 'Setup multiple buildtype-suffixed ninja-backend build directories, ' + 'and a [builddir]_vs containing a Visual Studio meta-backend with multiple configurations that calls into them', + 'vs2022', + choices=genvslitelist) + ), (OptionKey('buildtype'), BuiltinOption(UserComboOption, 'Build type to use', 'debug', - choices=['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom'])), + choices=buildtypelist)), (OptionKey('debug'), BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)), (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'], yielding=False)), @@ -1227,18 +1380,23 @@ BUILTIN_CORE_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([ (OptionKey('werror'), BuiltinOption(UserBooleanOption, 'Treat warnings as errors', False, yielding=False)), (OptionKey('wrap_mode'), BuiltinOption(UserComboOption, 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote'])), (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])), + (OptionKey('vsenv'), BuiltinOption(UserBooleanOption, 'Activate Visual Studio environment', False, readonly=True)), # Pkgconfig module (OptionKey('relocatable', module='pkgconfig'), BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)), # Python module + (OptionKey('bytecompile', module='python'), + BuiltinOption(UserIntegerOption, 'Whether to compile bytecode', (-1, 2, 0))), (OptionKey('install_env', module='python'), BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])), (OptionKey('platlibdir', module='python'), BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')), (OptionKey('purelibdir', module='python'), BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')), + (OptionKey('allow_limited_api', module='python'), + BuiltinOption(UserBooleanOption, 'Whether to allow use of the Python Limited API', True)), ]) BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items())) @@ -1250,7 +1408,7 @@ BUILTIN_OPTIONS_PER_MACHINE: 'MutableKeyedOptionDictType' = OrderedDict([ # Special prefix-dependent defaults for installation directories that reside in # a path outside of the prefix in FHS and common usage. -BULITIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = { +BUILTIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = { OptionKey('sysconfdir'): {'/usr': '/etc'}, OptionKey('localstatedir'): {'/usr': '/var', '/usr/local': '/var/local'}, OptionKey('sharedstatedir'): {'/usr': '/var/lib', '/usr/local': '/var/local/lib'}, @@ -1258,24 +1416,25 @@ BULITIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = { OptionKey('purelibdir', module='python'): {}, } -FORBIDDEN_TARGET_NAMES = {'clean': None, - 'clean-ctlist': None, - 'clean-gcno': None, - 'clean-gcda': None, - 'coverage': None, - 'coverage-text': None, - 'coverage-xml': None, - 'coverage-html': None, - 'phony': None, - 'PHONY': None, - 'all': None, - 'test': None, - 'benchmark': None, - 'install': None, - 'uninstall': None, - 'build.ninja': None, - 'scan-build': None, - 'reconfigure': None, - 'dist': None, - 'distcheck': None, - } +FORBIDDEN_TARGET_NAMES = frozenset({ + 'clean', + 'clean-ctlist', + 'clean-gcno', + 'clean-gcda', + 'coverage', + 'coverage-text', + 'coverage-xml', + 'coverage-html', + 'phony', + 'PHONY', + 'all', + 'test', + 'benchmark', + 'install', + 'uninstall', + 'build.ninja', + 'scan-build', + 'reconfigure', + 'dist', + 'distcheck', +}) diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py index b6fdb18..c6dabc5 100644 --- a/mesonbuild/dependencies/__init__.py +++ b/mesonbuild/dependencies/__init__.py @@ -12,35 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .boost import BoostDependency -from .cuda import CudaDependency -from .hdf5 import hdf5_factory from .base import Dependency, InternalDependency, ExternalDependency, NotFoundDependency, MissingCompiler from .base import ( ExternalLibrary, DependencyException, DependencyMethods, BuiltinDependency, SystemDependency, get_leaf_external_dependencies) -from .cmake import CMakeDependency -from .configtool import ConfigToolDependency -from .dub import DubDependency -from .framework import ExtraFrameworkDependency -from .pkgconfig import PkgConfigDependency -from .factory import DependencyFactory from .detect import find_external_dependency, get_dep_identifier, packages, _packages_accept_language -from .dev import ( - ValgrindDependency, JNISystemDependency, JDKSystemDependency, gmock_factory, gtest_factory, - llvm_factory, zlib_factory) -from .coarrays import coarray_factory -from .mpi import mpi_factory -from .scalapack import scalapack_factory -from .misc import ( - BlocksDependency, OpenMPDependency, cups_factory, curses_factory, gpgme_factory, - libgcrypt_factory, libwmf_factory, netcdf_factory, pcap_factory, python3_factory, - shaderc_factory, threads_factory, ThreadDependency, iconv_factory, intl_factory, - dl_factory, openssl_factory, libcrypto_factory, libssl_factory, -) -from .platform import AppleFrameworks -from .qt import qt4_factory, qt5_factory, qt6_factory -from .ui import GnuStepDependency, WxDependency, gl_factory, sdl2_factory, vulkan_factory __all__ = [ 'Dependency', @@ -54,16 +30,6 @@ __all__ = [ 'DependencyMethods', 'MissingCompiler', - 'CMakeDependency', - 'ConfigToolDependency', - 'DubDependency', - 'ExtraFrameworkDependency', - 'PkgConfigDependency', - - 'DependencyFactory', - - 'ThreadDependency', - 'find_external_dependency', 'get_dep_identifier', 'get_leaf_external_dependencies', @@ -226,57 +192,66 @@ this approach, and no new dependencies should do this. # - An ExternalDependency subclass # - A DependencyFactory object # - A callable with a signature of (Environment, MachineChoice, Dict[str, Any]) -> List[Callable[[], ExternalDependency]] -packages.update({ +# +# The internal "defaults" attribute contains a separate dictionary mapping +# for lazy imports. The values must be: +# - a string naming the submodule that should be imported from `mesonbuild.dependencies` to populate the dependency +packages.defaults.update({ # From dev: - 'gtest': gtest_factory, - 'gmock': gmock_factory, - 'llvm': llvm_factory, - 'valgrind': ValgrindDependency, - 'zlib': zlib_factory, - 'jni': JNISystemDependency, - 'jdk': JDKSystemDependency, + 'gtest': 'dev', + 'gmock': 'dev', + 'llvm': 'dev', + 'valgrind': 'dev', + 'zlib': 'dev', + 'jni': 'dev', + 'jdk': 'dev', - 'boost': BoostDependency, - 'cuda': CudaDependency, + 'boost': 'boost', + 'cuda': 'cuda', # per-file - 'coarray': coarray_factory, - 'hdf5': hdf5_factory, - 'mpi': mpi_factory, - 'scalapack': scalapack_factory, + 'coarray': 'coarrays', + 'hdf5': 'hdf5', + 'mpi': 'mpi', + 'scalapack': 'scalapack', # From misc: - 'blocks': BlocksDependency, - 'curses': curses_factory, - 'netcdf': netcdf_factory, - 'openmp': OpenMPDependency, - 'python3': python3_factory, - 'threads': threads_factory, - 'pcap': pcap_factory, - 'cups': cups_factory, - 'libwmf': libwmf_factory, - 'libgcrypt': libgcrypt_factory, - 'gpgme': gpgme_factory, - 'shaderc': shaderc_factory, - 'iconv': iconv_factory, - 'intl': intl_factory, - 'dl': dl_factory, - 'openssl': openssl_factory, - 'libcrypto': libcrypto_factory, - 'libssl': libssl_factory, + 'blocks': 'misc', + 'curses': 'misc', + 'netcdf': 'misc', + 'openmp': 'misc', + 'threads': 'misc', + 'pcap': 'misc', + 'cups': 'misc', + 'libwmf': 'misc', + 'libgcrypt': 'misc', + 'gpgme': 'misc', + 'shaderc': 'misc', + 'iconv': 'misc', + 'intl': 'misc', + 'dl': 'misc', + 'openssl': 'misc', + 'libcrypto': 'misc', + 'libssl': 'misc', # From platform: - 'appleframeworks': AppleFrameworks, + 'appleframeworks': 'platform', + + # from python: + 'python3': 'python', + 'pybind11': 'python', # From ui: - 'gl': gl_factory, - 'gnustep': GnuStepDependency, - 'qt4': qt4_factory, - 'qt5': qt5_factory, - 'qt6': qt6_factory, - 'sdl2': sdl2_factory, - 'wxwidgets': WxDependency, - 'vulkan': vulkan_factory, + 'gl': 'ui', + 'gnustep': 'ui', + 'sdl2': 'ui', + 'wxwidgets': 'ui', + 'vulkan': 'ui', + + # from qt + 'qt4': 'qt', + 'qt5': 'qt', + 'qt6': 'qt', }) _packages_accept_language.update({ 'hdf5', diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index d826026..c4861ff 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -30,16 +30,14 @@ from ..mesonlib import version_compare_many #from ..interpreterbase import FeatureDeprecated, FeatureNew if T.TYPE_CHECKING: - from .._typing import ImmutableListProtocol - from ..build import StructuredSources from ..compilers.compilers import Compiler from ..environment import Environment from ..interpreterbase import FeatureCheckBase from ..build import ( CustomTarget, IncludeDirs, CustomTargetIndex, LibTypes, - StaticLibrary + StaticLibrary, StructuredSources, ExtractedObjects, GeneratedTypes ) - from ..mesonlib import FileOrString + from ..interpreter.type_checking import PkgConfigDefineType class DependencyException(MesonException): @@ -100,7 +98,7 @@ class Dependency(HoldableObject): return kwargs['include_type'] def __init__(self, type_name: DependencyTypeName, kwargs: T.Dict[str, T.Any]) -> None: - self.name = "null" + self.name = f'dep{id(self)}' self.version: T.Optional[str] = None self.language: T.Optional[str] = None # None means C-like self.is_found = False @@ -110,7 +108,8 @@ class Dependency(HoldableObject): # Raw -L and -l arguments without manual library searching # If None, self.link_args will be used self.raw_link_args: T.Optional[T.List[str]] = None - self.sources: T.List[T.Union['FileOrString', 'CustomTarget', 'StructuredSources']] = [] + self.sources: T.List[T.Union[mesonlib.File, GeneratedTypes, 'StructuredSources']] = [] + self.extra_files: T.List[mesonlib.File] = [] self.include_type = self._process_include_type_kw(kwargs) self.ext_deps: T.List[Dependency] = [] self.d_features: T.DefaultDict[str, T.List[T.Any]] = collections.defaultdict(list) @@ -167,11 +166,15 @@ class Dependency(HoldableObject): def found(self) -> bool: return self.is_found - def get_sources(self) -> T.List[T.Union['FileOrString', 'CustomTarget', 'StructuredSources']]: + def get_sources(self) -> T.List[T.Union[mesonlib.File, GeneratedTypes, 'StructuredSources']]: """Source files that need to be added to the target. As an example, gtest-all.cc when using GTest.""" return self.sources + def get_extra_files(self) -> T.List[mesonlib.File]: + """Mostly for introspection and IDEs""" + return self.extra_files + def get_name(self) -> str: return self.name @@ -190,14 +193,6 @@ class Dependency(HoldableObject): def get_exe_args(self, compiler: 'Compiler') -> T.List[str]: return [] - def get_pkgconfig_variable(self, variable_name: str, - define_variable: 'ImmutableListProtocol[str]', - default: T.Optional[str]) -> str: - raise DependencyException(f'{self.name!r} is not a pkgconfig dependency') - - def get_configtool_variable(self, variable_name: str) -> str: - raise DependencyException(f'{self.name!r} is not a config-tool dependency') - def get_partial_dependency(self, *, compile_args: bool = False, link_args: bool = False, links: bool = False, includes: bool = False, sources: bool = False) -> 'Dependency': @@ -235,7 +230,7 @@ class Dependency(HoldableObject): def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None, default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> str: + pkgconfig_define: PkgConfigDefineType = None) -> str: if default_value is not None: return default_value raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.') @@ -250,9 +245,11 @@ class InternalDependency(Dependency): link_args: T.List[str], libraries: T.List[LibTypes], whole_libraries: T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]], - sources: T.Sequence[T.Union[FileOrString, CustomTarget, StructuredSources]], + sources: T.Sequence[T.Union[mesonlib.File, GeneratedTypes, StructuredSources]], + extra_files: T.Sequence[mesonlib.File], ext_deps: T.List[Dependency], variables: T.Dict[str, str], - d_module_versions: T.List[T.Union[str, int]], d_import_dirs: T.List['IncludeDirs']): + d_module_versions: T.List[T.Union[str, int]], d_import_dirs: T.List['IncludeDirs'], + objects: T.List['ExtractedObjects']): super().__init__(DependencyTypeName('internal'), {}) self.version = version self.is_found = True @@ -262,8 +259,10 @@ class InternalDependency(Dependency): self.libraries = libraries self.whole_libraries = whole_libraries self.sources = list(sources) + self.extra_files = list(extra_files) self.ext_deps = ext_deps self.variables = variables + self.objects = objects if d_module_versions: self.d_features['versions'] = d_module_versions if d_import_dirs: @@ -290,24 +289,16 @@ class InternalDependency(Dependency): return True return any(d.is_built() for d in self.ext_deps) - def get_pkgconfig_variable(self, variable_name: str, - define_variable: 'ImmutableListProtocol[str]', - default: T.Optional[str]) -> str: - raise DependencyException('Method "get_pkgconfig_variable()" is ' - 'invalid for an internal dependency') - - def get_configtool_variable(self, variable_name: str) -> str: - raise DependencyException('Method "get_configtool_variable()" is ' - 'invalid for an internal dependency') - def get_partial_dependency(self, *, compile_args: bool = False, link_args: bool = False, links: bool = False, - includes: bool = False, sources: bool = False) -> 'InternalDependency': + includes: bool = False, sources: bool = False, + extra_files: bool = False) -> InternalDependency: final_compile_args = self.compile_args.copy() if compile_args else [] final_link_args = self.link_args.copy() if link_args else [] final_libraries = self.libraries.copy() if links else [] final_whole_libraries = self.whole_libraries.copy() if links else [] final_sources = self.sources.copy() if sources else [] + final_extra_files = self.extra_files.copy() if extra_files else [] final_includes = self.include_directories.copy() if includes else [] final_deps = [d.get_partial_dependency( compile_args=compile_args, link_args=link_args, links=links, @@ -315,7 +306,7 @@ class InternalDependency(Dependency): return InternalDependency( self.version, final_includes, final_compile_args, final_link_args, final_libraries, final_whole_libraries, - final_sources, final_deps, self.variables, [], []) + final_sources, final_extra_files, final_deps, self.variables, [], [], []) def get_include_dirs(self) -> T.List['IncludeDirs']: return self.include_directories @@ -323,7 +314,7 @@ class InternalDependency(Dependency): def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None, default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> str: + pkgconfig_define: PkgConfigDefineType = None) -> str: val = self.variables.get(internal, default_value) if val is not None: return val @@ -542,11 +533,22 @@ def strip_system_libdirs(environment: 'Environment', for_machine: MachineChoice, in the system path, and a different version not in the system path if they want to link against the non-system path version. """ - exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)} + exclude = {f'-L{p}' for p in environment.get_compiler_system_lib_dirs(for_machine)} return [l for l in link_args if l not in exclude] +def strip_system_includedirs(environment: 'Environment', for_machine: MachineChoice, include_args: T.List[str]) -> T.List[str]: + """Remove -I arguments. + + leaving these in will break builds where user want dependencies with system + include-type used in rust.bindgen targets as if will cause system headers + to not be found. + """ + + exclude = {f'-I{p}' for p in environment.get_compiler_system_include_dirs(for_machine)} + return [i for i in include_args if i not in exclude] + def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs: T.Dict[str, T.Any]) -> T.List[DependencyMethods]: - method = kwargs.get('method', 'auto') # type: T.Union[DependencyMethods, str] + method: T.Union[DependencyMethods, str] = kwargs.get('method', 'auto') if isinstance(method, DependencyMethods): return [method] # TODO: try/except? diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py index 4ebd88d..19c629d 100644 --- a/mesonbuild/dependencies/boost.py +++ b/mesonbuild/dependencies/boost.py @@ -23,11 +23,13 @@ from .. import mlog from .. import mesonlib from .base import DependencyException, SystemDependency +from .detect import packages from .pkgconfig import PkgConfigDependency from .misc import threads_factory if T.TYPE_CHECKING: - from ..environment import Environment, Properties + from ..envconfig import Properties + from ..environment import Environment # On windows 3 directory layouts are supported: # * The default layout (versioned) installed: @@ -80,7 +82,7 @@ if T.TYPE_CHECKING: # 2. Find all boost libraries # 2.1 Add all libraries in lib* # 2.2 Filter out non boost libraries -# 2.3 Filter the renaining libraries based on the meson requirements (static/shared, etc.) +# 2.3 Filter the remaining libraries based on the meson requirements (static/shared, etc.) # 2.4 Ensure that all libraries have the same boost tag (and are thus compatible) # 3. Select the libraries matching the requested modules @@ -243,10 +245,10 @@ class BoostLibraryFile(): return any(self.mod_name.startswith(x) for x in BoostLibraryFile.boost_python_libs) def fix_python_name(self, tags: T.List[str]) -> T.List[str]: - # Handle the boost_python naming madeness. + # Handle the boost_python naming madness. # See https://github.com/mesonbuild/meson/issues/4788 for some distro # specific naming variations. - other_tags = [] # type: T.List[str] + other_tags: T.List[str] = [] # Split the current modname into the base name and the version m_cur = BoostLibraryFile.reg_python_mod_split.match(self.mod_name) @@ -329,9 +331,9 @@ class BoostLibraryFile(): return True def get_compiler_args(self) -> T.List[str]: - args = [] # type: T.List[str] + args: T.List[str] = [] if self.mod_name in boost_libraries: - libdef = boost_libraries[self.mod_name] # type: BoostLibrary + libdef = boost_libraries[self.mod_name] if self.static: args += libdef.static else: @@ -353,19 +355,19 @@ class BoostDependency(SystemDependency): self.debug = buildtype.startswith('debug') self.multithreading = kwargs.get('threading', 'multi') == 'multi' - self.boost_root = None # type: T.Optional[Path] + self.boost_root: T.Optional[Path] = None self.explicit_static = 'static' in kwargs # Extract and validate modules - self.modules = mesonlib.extract_as_list(kwargs, 'modules') # type: T.List[str] + self.modules: T.List[str] = mesonlib.extract_as_list(kwargs, 'modules') for i in self.modules: if not isinstance(i, str): raise DependencyException('Boost module argument is not a string.') if i.startswith('boost_'): raise DependencyException('Boost modules must be passed without the boost_ prefix') - self.modules_found = [] # type: T.List[str] - self.modules_missing = [] # type: T.List[str] + self.modules_found: T.List[str] = [] + self.modules_missing: T.List[str] = [] # Do we need threads? if 'thread' in self.modules: @@ -448,7 +450,7 @@ class BoostDependency(SystemDependency): mlog.debug(' - potential include dirs: {}'.format([x.path.as_posix() for x in inc_dirs])) # 2. Find all boost libraries - libs = [] # type: T.List[BoostLibraryFile] + libs: T.List[BoostLibraryFile] = [] for i in lib_dirs: libs = self.detect_libraries(i) if libs: @@ -469,8 +471,8 @@ class BoostDependency(SystemDependency): mlog.debug(f' - {j}') # 3. Select the libraries matching the requested modules - not_found = [] # type: T.List[str] - selected_modules = [] # type: T.List[BoostLibraryFile] + not_found: T.List[str] = [] + selected_modules: T.List[BoostLibraryFile] = [] for mod in modules: found = False for l in f_libs: @@ -483,8 +485,8 @@ class BoostDependency(SystemDependency): # log the result mlog.debug(' - found:') - comp_args = [] # type: T.List[str] - link_args = [] # type: T.List[str] + comp_args: T.List[str] = [] + link_args: T.List[str] = [] for j in selected_modules: c_args = j.get_compiler_args() l_args = j.get_link_args() @@ -522,7 +524,7 @@ class BoostDependency(SystemDependency): return False def detect_inc_dirs(self, root: Path) -> T.List[BoostIncludeDir]: - candidates = [] # type: T.List[Path] + candidates: T.List[Path] = [] inc_root = root / 'include' candidates += [root / 'boost'] @@ -553,8 +555,8 @@ class BoostDependency(SystemDependency): # No system include paths were found --> fall back to manually looking # for library dirs in root - dirs = [] # type: T.List[Path] - subdirs = [] # type: T.List[Path] + dirs: T.List[Path] = [] + subdirs: T.List[Path] = [] for i in root.iterdir(): if i.is_dir() and i.name.startswith('lib'): dirs += [i] @@ -576,7 +578,7 @@ class BoostDependency(SystemDependency): raw_list = dirs + subdirs no_arch = [x for x in raw_list if not any(y in x.name for y in arch_list_32 + arch_list_64)] - matching_arch = [] # type: T.List[Path] + matching_arch: T.List[Path] = [] if '32' in self.arch: matching_arch = [x for x in raw_list if any(y in x.name for y in arch_list_32)] elif '64' in self.arch: @@ -622,7 +624,7 @@ class BoostDependency(SystemDependency): return libs def detect_libraries(self, libdir: Path) -> T.List[BoostLibraryFile]: - libs = set() # type: T.Set[BoostLibraryFile] + libs: T.Set[BoostLibraryFile] = set() for i in libdir.iterdir(): if not i.is_file(): continue @@ -653,14 +655,14 @@ class BoostDependency(SystemDependency): self.is_found = self.run_check([boost_inc_dir], [lib_dir]) def detect_roots(self) -> None: - roots = [] # type: T.List[Path] + roots: T.List[Path] = [] # Try getting the BOOST_ROOT from a boost.pc if it exists. This primarily # allows BoostDependency to find boost from Conan. See #5438 try: boost_pc = PkgConfigDependency('boost', self.env, {'required': False}) if boost_pc.found(): - boost_root = boost_pc.get_pkgconfig_variable('prefix', [], None) + boost_root = boost_pc.get_variable(pkgconfig='prefix') if boost_root: roots += [Path(boost_root)] except DependencyException: @@ -684,7 +686,7 @@ class BoostDependency(SystemDependency): # Where boost prebuilt binaries are local_boost = Path('C:/local') - candidates = [] # type: T.List[Path] + candidates: T.List[Path] = [] if prog_files.is_dir(): candidates += [*prog_files.iterdir()] if local_boost.is_dir(): @@ -692,7 +694,7 @@ class BoostDependency(SystemDependency): roots += [x for x in candidates if x.name.lower().startswith('boost') and x.is_dir()] else: - tmp = [] # type: T.List[Path] + tmp: T.List[Path] = [] # Add some default system paths tmp += [Path('/opt/local')] @@ -738,6 +740,7 @@ class BoostDependency(SystemDependency): # BOOST_ALL_DYN_LINK should not be required with the known defines below return ['-DBOOST_ALL_NO_LIB'] # Disable automatic linking +packages['boost'] = BoostDependency # See https://www.boost.org/doc/libs/1_72_0/more/getting_started/unix-variants.html#library-naming # See https://mesonbuild.com/Reference-tables.html#cpu-families diff --git a/mesonbuild/dependencies/cmake.py b/mesonbuild/dependencies/cmake.py index abd31a1..46a58c1 100644 --- a/mesonbuild/dependencies/cmake.py +++ b/mesonbuild/dependencies/cmake.py @@ -30,6 +30,7 @@ if T.TYPE_CHECKING: from ..cmake import CMakeTarget from ..environment import Environment from ..envconfig import MachineInfo + from ..interpreter.type_checking import PkgConfigDefineType class CMakeInfo(T.NamedTuple): module_paths: T.List[str] @@ -80,7 +81,7 @@ class CMakeDependency(ExternalDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None, force_use_global_compilers: bool = False) -> None: # Gather a list of all languages to support - self.language_list = [] # type: T.List[str] + self.language_list: T.List[str] = [] if language is None or force_use_global_compilers: compilers = None if kwargs.get('native', False): @@ -225,7 +226,7 @@ class CMakeDependency(ExternalDependency): module_paths = [x for x in module_paths if os.path.isdir(x)] archs = temp_parser.get_cmake_var('MESON_ARCH_LIST') - common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share'] + common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share', ''] for i in archs: common_paths += [os.path.join('lib', i)] @@ -312,7 +313,7 @@ class CMakeDependency(ExternalDependency): return True # Check PATH - system_env = [] # type: T.List[str] + system_env: T.List[str] = [] for i in os.environ.get('PATH', '').split(os.pathsep): if i.endswith('/bin') or i.endswith('\\bin'): i = i[:-4] @@ -388,6 +389,7 @@ class CMakeDependency(ExternalDependency): cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo.archs))] cmake_opts += [f'-DVERSION={package_version}'] cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] + cmake_opts += [f'-DSTATIC={self.static}'] cmake_opts += args cmake_opts += self.traceparser.trace_args() cmake_opts += toolchain.get_cmake_args() @@ -489,7 +491,7 @@ class CMakeDependency(ExternalDependency): libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] # CMake has a "fun" API, where certain keywords describing - # configurations can be in the *_LIBRARIES vraiables. See: + # configurations can be in the *_LIBRARIES variables. See: # - https://github.com/mesonbuild/meson/issues/9197 # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140 # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview (the last point in the section) @@ -505,7 +507,7 @@ class CMakeDependency(ExternalDependency): libs += [i] # According to the CMake docs, a keyword only works for the # directly the following item and all items without a keyword - # are implizitly `general` + # are implicitly `general` cfg_matches = True # Try to use old style variables if no module is specified @@ -631,7 +633,7 @@ class CMakeDependency(ExternalDependency): def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None, default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> str: + pkgconfig_define: PkgConfigDefineType = None) -> str: if cmake and self.traceparser is not None: try: v = self.traceparser.vars[cmake] @@ -651,3 +653,19 @@ class CMakeDependency(ExternalDependency): if default_value is not None: return default_value raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') + + +class CMakeDependencyFactory: + + def __init__(self, name: T.Optional[str] = None, modules: T.Optional[T.List[str]] = None): + self.name = name + self.modules = modules + + def __call__(self, name: str, env: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None, force_use_global_compilers: bool = False) -> CMakeDependency: + if self.modules: + kwargs['modules'] = self.modules + return CMakeDependency(self.name or name, env, kwargs, language, force_use_global_compilers) + + @staticmethod + def log_tried() -> str: + return CMakeDependency.log_tried() diff --git a/mesonbuild/dependencies/coarrays.py b/mesonbuild/dependencies/coarrays.py index b50fb0a..1c59792 100644 --- a/mesonbuild/dependencies/coarrays.py +++ b/mesonbuild/dependencies/coarrays.py @@ -11,18 +11,21 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import functools import typing as T from .base import DependencyMethods, detect_compiler, SystemDependency from .cmake import CMakeDependency +from .detect import packages from .pkgconfig import PkgConfigDependency from .factory import factory_methods if T.TYPE_CHECKING: from . factory import DependencyGenerator - from ..environment import Environment, MachineChoice + from ..environment import Environment + from ..mesonlib import MachineChoice @factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM}) @@ -50,6 +53,7 @@ def coarray_factory(env: 'Environment', candidates.append(functools.partial(CoarrayDependency, env, kwargs)) return candidates +packages['coarray'] = coarray_factory class CoarrayDependency(SystemDependency): diff --git a/mesonbuild/dependencies/configtool.py b/mesonbuild/dependencies/configtool.py index 8b7f1bb..3c52356 100644 --- a/mesonbuild/dependencies/configtool.py +++ b/mesonbuild/dependencies/configtool.py @@ -11,9 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .base import ExternalDependency, DependencyException, DependencyTypeName -from ..mesonlib import listify, Popen_safe, split_args, version_compare, version_compare_many +from ..mesonlib import listify, Popen_safe, Popen_safe_logged, split_args, version_compare, version_compare_many from ..programs import find_external_program from .. import mlog import re @@ -23,6 +24,7 @@ from mesonbuild import mesonlib if T.TYPE_CHECKING: from ..environment import Environment + from ..interpreter.type_checking import PkgConfigDefineType class ConfigToolDependency(ExternalDependency): @@ -31,6 +33,9 @@ class ConfigToolDependency(ExternalDependency): Takes the following extra keys in kwargs that it uses internally: :tools List[str]: A list of tool names to use :version_arg str: The argument to pass to the tool to get it's version + :skip_version str: The argument to pass to the tool to ignore its version + (if ``version_arg`` fails, but it may start accepting it in the future) + Because some tools are stupid and don't accept --version :returncode_value int: The value of the correct returncode Because some tools are stupid and don't return 0 """ @@ -38,6 +43,8 @@ class ConfigToolDependency(ExternalDependency): tools: T.Optional[T.List[str]] = None tool_name: T.Optional[str] = None version_arg = '--version' + skip_version: T.Optional[str] = None + allow_default_for_cross = False __strip_version = re.compile(r'^[0-9][0-9.]+') def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None): @@ -80,7 +87,7 @@ class ConfigToolDependency(ExternalDependency): best_match: T.Tuple[T.Optional[T.List[str]], T.Optional[str]] = (None, None) for potential_bin in find_external_program( self.env, self.for_machine, self.tool_name, - self.tool_name, self.tools, allow_default_for_cross=False): + self.tool_name, self.tools, allow_default_for_cross=self.allow_default_for_cross): if not potential_bin.found(): continue tool = potential_bin.get_command() @@ -89,7 +96,13 @@ class ConfigToolDependency(ExternalDependency): except (FileNotFoundError, PermissionError): continue if p.returncode != returncode: - continue + if self.skip_version: + # maybe the executable is valid even if it doesn't support --version + p = Popen_safe(tool + [self.skip_version])[0] + if p.returncode != returncode: + continue + else: + continue out = self._sanitize_version(out.strip()) # Some tools, like pcap-config don't supply a version, but also @@ -131,23 +144,15 @@ class ConfigToolDependency(ExternalDependency): return self.config is not None def get_config_value(self, args: T.List[str], stage: str) -> T.List[str]: - p, out, err = Popen_safe(self.config + args) + p, out, err = Popen_safe_logged(self.config + args) if p.returncode != 0: if self.required: raise DependencyException(f'Could not generate {stage} for {self.name}.\n{err}') return [] return split_args(out) - def get_configtool_variable(self, variable_name: str) -> str: - p, out, _ = Popen_safe(self.config + [f'--{variable_name}']) - if p.returncode != 0: - if self.required: - raise DependencyException( - 'Could not get variable "{}" for dependency {}'.format( - variable_name, self.name)) - variable = out.strip() - mlog.debug(f'Got config-tool variable {variable_name} : {variable}') - return variable + def get_variable_args(self, variable_name: str) -> T.List[str]: + return [f'--{variable_name}'] @staticmethod def log_tried() -> str: @@ -156,20 +161,13 @@ class ConfigToolDependency(ExternalDependency): def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None, default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> str: + pkgconfig_define: PkgConfigDefineType = None) -> str: if configtool: - # In the not required case '' (empty string) will be returned if the - # variable is not found. Since '' is a valid value to return we - # set required to True here to force and error, and use the - # finally clause to ensure it's restored. - restore = self.required - self.required = True - try: - return self.get_configtool_variable(configtool) - except DependencyException: - pass - finally: - self.required = restore + p, out, _ = Popen_safe(self.config + self.get_variable_args(configtool)) + if p.returncode == 0: + variable = out.strip() + mlog.debug(f'Got config-tool variable {configtool} : {variable}') + return variable if default_value is not None: return default_value raise DependencyException(f'Could not get config-tool variable and no default provided for {self!r}') diff --git a/mesonbuild/dependencies/cuda.py b/mesonbuild/dependencies/cuda.py index 608d4f7..aaed6b3 100644 --- a/mesonbuild/dependencies/cuda.py +++ b/mesonbuild/dependencies/cuda.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import glob import re @@ -22,13 +23,14 @@ from .. import mesonlib from .. import mlog from ..environment import detect_cpu_family from .base import DependencyException, SystemDependency +from .detect import packages if T.TYPE_CHECKING: from ..environment import Environment from ..compilers import Compiler -TV_ResultTuple = T.Tuple[T.Optional[str], T.Optional[str], bool] + TV_ResultTuple = T.Tuple[T.Optional[str], T.Optional[str], bool] class CudaDependency(SystemDependency): @@ -43,8 +45,18 @@ class CudaDependency(SystemDependency): super().__init__('cuda', environment, kwargs, language=language) self.lib_modules: T.Dict[str, T.List[str]] = {} self.requested_modules = self.get_requested(kwargs) - if 'cudart' not in self.requested_modules: - self.requested_modules = ['cudart'] + self.requested_modules + if not any(runtime in self.requested_modules for runtime in ['cudart', 'cudart_static']): + # By default, we prefer to link the static CUDA runtime, since this is what nvcc also does by default: + # https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#cudart-none-shared-static-cudart + req_modules = ['cudart'] + if kwargs.get('static', True): + req_modules = ['cudart_static'] + machine = self.env.machines[self.for_machine] + if machine.is_linux(): + # extracted by running + # nvcc -v foo.o + req_modules += ['rt', 'pthread', 'dl'] + self.requested_modules = req_modules + self.requested_modules (self.cuda_path, self.version, self.is_found) = self._detect_cuda_path_and_version() if not self.is_found: @@ -193,8 +205,8 @@ class CudaDependency(SystemDependency): except ValueError: continue # use // for floor instead of / which produces a float - major = vers_int // 1000 # type: int - minor = (vers_int - major * 1000) // 10 # type: int + major = vers_int // 1000 + minor = (vers_int - major * 1000) // 10 return f'{major}.{minor}' return None @@ -289,3 +301,5 @@ class CudaDependency(SystemDependency): for lib in self.requested_modules: args += self.lib_modules[lib] return args + +packages['cuda'] = CudaDependency diff --git a/mesonbuild/dependencies/data/CMakeLists.txt b/mesonbuild/dependencies/data/CMakeLists.txt index acbf648..d682cb8 100644 --- a/mesonbuild/dependencies/data/CMakeLists.txt +++ b/mesonbuild/dependencies/data/CMakeLists.txt @@ -8,6 +8,10 @@ set(PACKAGE_FOUND FALSE) set(_packageName "${NAME}") string(TOUPPER "${_packageName}" PACKAGE_NAME) +if("${STATIC}" STREQUAL "True") + set("${NAME}_USE_STATIC_LIBS" "ON") +endif() + while(TRUE) if ("${VERSION}" STREQUAL "") find_package("${NAME}" QUIET COMPONENTS ${COMPS}) diff --git a/mesonbuild/dependencies/data/CMakeListsLLVM.txt b/mesonbuild/dependencies/data/CMakeListsLLVM.txt index da23189..4a93822 100644 --- a/mesonbuild/dependencies/data/CMakeListsLLVM.txt +++ b/mesonbuild/dependencies/data/CMakeListsLLVM.txt @@ -1,8 +1,24 @@ +# fail noisily if attempt to use this file without setting: +# cmake_minimum_required(VERSION ${CMAKE_VERSION}) +# project(... LANGUAGES ...) + +cmake_policy(SET CMP0000 NEW) set(PACKAGE_FOUND FALSE) +list(REMOVE_DUPLICATES LLVM_MESON_VERSIONS) + while(TRUE) - find_package(LLVM REQUIRED CONFIG QUIET) + #Activate CMake version selection + foreach(i IN LISTS LLVM_MESON_VERSIONS) + find_package(LLVM ${i} + CONFIG + NAMES ${LLVM_MESON_PACKAGE_NAMES} + QUIET) + if(LLVM_FOUND) + break() + endif() + endforeach() # ARCHS has to be set via the CMD interface if(LLVM_FOUND OR "${ARCHS}" STREQUAL "") @@ -13,9 +29,70 @@ while(TRUE) list(REMOVE_AT ARCHS 0) endwhile() +function(meson_llvm_cmake_dynamic_available mod out) + # Check if we can only compare LLVM_DYLIB_COMPONENTS, because + # we do not need complex component translation logic, if all + # is covered by one variable + if(mod IN_LIST LLVM_DYLIB_COMPONENTS) + set(${out} TRUE PARENT_SCOPE) + return() + elseif((NOT (mod IN_LIST LLVM_DYLIB_COMPONENTS)) + AND (NOT("${LLVM_DYLIB_COMPONENTS}" STREQUAL "all"))) + set(${out} FALSE PARENT_SCOPE) + return() + endif() + + # Complex heuristic to filter all pseudo-components and skip invalid names + # LLVM_DYLIB_COMPONENTS will be 'all', because in other case we returned + # in previous check. 'all' is also handled there. + set(llvm_pseudo_components "native" "backend" "engine" "all-targets") + is_llvm_target_specifier(${mod} mod_spec INCLUDED_TARGETS) + string(TOUPPER "${LLVM_AVAILABLE_LIBS}" capitalized_libs) + string(TOUPPER "${LLVM_TARGETS_TO_BUILD}" capitalized_tgts) + if(mod_spec) + set(${out} TRUE PARENT_SCOPE) + elseif(mod IN_LIST capitalized_tgts) + set(${out} TRUE PARENT_SCOPE) + elseif(mod IN_LIST llvm_pseudo_components) + set(${out} TRUE PARENT_SCOPE) + elseif(LLVM${mod} IN_LIST capitalized_libs) + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +function(is_static target ret) + if(TARGET ${target}) + get_target_property(target_type ${target} TYPE) + if(target_type STREQUAL "STATIC_LIBRARY") + set(${ret} TRUE PARENT_SCOPE) + return() + endif() + endif() + set(${ret} FALSE PARENT_SCOPE) +endfunction() + +# Concatenate LLVM_MESON_REQUIRED_MODULES and LLVM_MESON_OPTIONAL_MODULES +set(LLVM_MESON_MODULES ${LLVM_MESON_REQUIRED_MODULES} ${LLVM_MESON_OPTIONAL_MODULES}) + + +# Check if LLVM exists in dynamic world +# Initialization before modules checking if(LLVM_FOUND) - set(PACKAGE_FOUND TRUE) + if(LLVM_MESON_DYLIB AND TARGET LLVM) + set(PACKAGE_FOUND TRUE) + elseif(NOT LLVM_MESON_DYLIB) + # Use LLVMSupport to check if static targets exist + set(static_tg FALSE) + is_static(LLVMSupport static_tg) + if(static_tg) + set(PACKAGE_FOUND TRUE) + endif(static_tg) + endif() +endif() +if(PACKAGE_FOUND) foreach(mod IN LISTS LLVM_MESON_MODULES) # Reset variables set(out_mods) @@ -25,23 +102,53 @@ if(LLVM_FOUND) string(TOLOWER "${mod}" mod_L) string(TOUPPER "${mod}" mod_U) - # Get the mapped components - llvm_map_components_to_libnames(out_mods ${mod} ${mod_L} ${mod_U}) - list(SORT out_mods) - list(REMOVE_DUPLICATES out_mods) - - # Make sure that the modules exist - foreach(i IN LISTS out_mods) - if(TARGET ${i}) - list(APPEND real_mods ${i}) - endif() - endforeach() - - # Set the output variables - set(MESON_LLVM_TARGETS_${mod} ${real_mods}) - foreach(i IN LISTS real_mods) - set(MESON_TARGET_TO_LLVM_${i} ${mod}) - endforeach() + # Special case - "all-targets" pseudo target + # Just append all targets, if pseudo-target exists + if("${mod}" STREQUAL "all-targets") + set(mod_L ${LLVM_TARGETS_TO_BUILD}) + string(TOUPPER "${LLVM_TARGETS_TO_BUILD}" mod_U) + endif() + + # Check if required module is linked is inside libLLVM.so. + # If not, skip this module + if(LLVM_MESON_DYLIB + AND DEFINED LLVM_DYLIB_COMPONENTS) + meson_llvm_cmake_dynamic_available(${mod} MOD_F) + meson_llvm_cmake_dynamic_available(${mod_L} MOD_L_F) + meson_llvm_cmake_dynamic_available(${mod_U} MOD_U_F) + if(MOD_F OR MOD_L_F OR MOD_U_F) + set(MESON_LLVM_TARGETS_${mod} LLVM) + endif() + elseif(LLVM_MESON_DYLIB AND (mod IN_LIST LLVM_MESON_REQUIRED_MODULES)) + # Dynamic was requested, but no required variables set, we cannot continue + set(PACKAGE_FOUND FALSE) + break() + elseif(LLVM_MESON_DYLIB) + # Dynamic was requested, and we request optional modules only. Continue + continue() + else() + # CMake only do this for static components, and we + # replicate its behaviour + # Get the mapped components + llvm_map_components_to_libnames(out_mods ${mod} ${mod_L} ${mod_U}) + list(SORT out_mods) + list(REMOVE_DUPLICATES out_mods) + + # Make sure that the modules exist + foreach(i IN LISTS out_mods) + set(static_tg FALSE) + is_static(${i} static_tg) + if(static_tg) + list(APPEND real_mods ${i}) + endif() + endforeach() + + # Set the output variables + set(MESON_LLVM_TARGETS_${mod} ${real_mods}) + foreach(i IN LISTS real_mods) + set(MESON_TARGET_TO_LLVM_${i} ${mod}) + endforeach() + endif() endforeach() # Check the following variables: @@ -62,7 +169,10 @@ if(LLVM_FOUND) # LLVM_LIBRARIES # LLVM_LIBS set(libs) - if(DEFINED LLVM_LIBRARIES) + #Hardcode LLVM, because we links with libLLVM.so when dynamic + if(LLVM_MESON_DYLIB) + get_target_property(libs LLVM IMPORTED_LOCATION) + elseif(DEFINED LLVM_LIBRARIES) set(libs LLVM_LIBRARIES) elseif(DEFINED LLVM_LIBS) set(libs LLVM_LIBS) diff --git a/mesonbuild/dependencies/detect.py b/mesonbuild/dependencies/detect.py index 782c2f1..9428d54 100644 --- a/mesonbuild/dependencies/detect.py +++ b/mesonbuild/dependencies/detect.py @@ -11,17 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + +import collections, functools, importlib +import typing as T from .base import ExternalDependency, DependencyException, DependencyMethods, NotFoundDependency -from .cmake import CMakeDependency -from .dub import DubDependency -from .framework import ExtraFrameworkDependency -from .pkgconfig import PkgConfigDependency from ..mesonlib import listify, MachineChoice, PerMachine from .. import mlog -import functools -import typing as T if T.TYPE_CHECKING: from ..environment import Environment @@ -29,12 +27,25 @@ if T.TYPE_CHECKING: TV_DepIDEntry = T.Union[str, bool, int, T.Tuple[str, ...]] TV_DepID = T.Tuple[T.Tuple[str, TV_DepIDEntry], ...] + PackageTypes = T.Union[T.Type[ExternalDependency], DependencyFactory, WrappedFactoryFunc] + +class DependencyPackages(collections.UserDict): + data: T.Dict[str, PackageTypes] + defaults: T.Dict[str, str] = {} + + def __missing__(self, key: str) -> PackageTypes: + if key in self.defaults: + modn = self.defaults[key] + importlib.import_module(f'mesonbuild.dependencies.{modn}') + + return self.data[key] + raise KeyError(key) + + def __contains__(self, key: object) -> bool: + return key in self.defaults or key in self.data # These must be defined in this file to avoid cyclical references. -packages: T.Dict[ - str, - T.Union[T.Type[ExternalDependency], 'DependencyFactory', 'WrappedFactoryFunc'] -] = {} +packages = DependencyPackages() _packages_accept_language: T.Set[str] = set() def get_dep_identifier(name: str, kwargs: T.Dict[str, T.Any]) -> 'TV_DepID': @@ -79,7 +90,7 @@ display_name_map = { 'wxwidgets': 'WxWidgets', } -def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, object]) -> T.Union['ExternalDependency', NotFoundDependency]: +def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, object], candidates: T.Optional[T.List['DependencyGenerator']] = None) -> T.Union['ExternalDependency', NotFoundDependency]: assert name required = kwargs.get('required', True) if not isinstance(required, bool): @@ -100,7 +111,8 @@ def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency' # build a list of dependency methods to try - candidates = _build_external_dependency_list(name, env, for_machine, kwargs) + if candidates is None: + candidates = _build_external_dependency_list(name, env, for_machine, kwargs) pkg_exc: T.List[DependencyException] = [] pkgdep: T.List[ExternalDependency] = [] @@ -190,37 +202,34 @@ def _build_external_dependency_list(name: str, env: 'Environment', for_machine: candidates: T.List['DependencyGenerator'] = [] - # If it's explicitly requested, use the dub detection method (only) - if 'dub' == kwargs.get('method', ''): - candidates.append(functools.partial(DubDependency, name, env, kwargs)) - return candidates - - # If it's explicitly requested, use the pkgconfig detection method (only) - if 'pkg-config' == kwargs.get('method', ''): - candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) - return candidates - - # If it's explicitly requested, use the CMake detection method (only) - if 'cmake' == kwargs.get('method', ''): - candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) - return candidates + if kwargs.get('method', 'auto') == 'auto': + # Just use the standard detection methods. + methods = ['pkg-config', 'extraframework', 'cmake'] + else: + # If it's explicitly requested, use that detection method (only). + methods = [kwargs['method']] - # If it's explicitly requested, use the Extraframework detection method (only) - if 'extraframework' == kwargs.get('method', ''): - # On OSX, also try framework dependency detector - if env.machines[for_machine].is_darwin(): - candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) - return candidates + # Exclusive to when it is explicitly requested + if 'dub' in methods: + from .dub import DubDependency + candidates.append(functools.partial(DubDependency, name, env, kwargs)) - # Otherwise, just use the pkgconfig and cmake dependency detector - if 'auto' == kwargs.get('method', 'auto'): + # Preferred first candidate for auto. + if 'pkg-config' in methods: + from .pkgconfig import PkgConfigDependency candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) - # On OSX, also try framework dependency detector + # On OSX only, try framework dependency detector. + if 'extraframework' in methods: if env.machines[for_machine].is_darwin(): + from .framework import ExtraFrameworkDependency candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) - # Only use CMake as a last resort, since it might not work 100% (see #6113) + # Only use CMake: + # - if it's explicitly requested + # - as a last resort, since it might not work 100% (see #6113) + if 'cmake' in methods: + from .cmake import CMakeDependency candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) return candidates diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py index cc02842..09f55b7 100644 --- a/mesonbuild/dependencies/dev.py +++ b/mesonbuild/dependencies/dev.py @@ -24,15 +24,17 @@ import pathlib import shutil import subprocess import typing as T +import functools from mesonbuild.interpreterbase.decorators import FeatureDeprecated from .. import mesonlib, mlog from ..environment import get_llvm_tool_names -from ..mesonlib import version_compare, stringlistify, extract_as_list -from .base import DependencyException, DependencyMethods, detect_compiler, strip_system_libdirs, SystemDependency, ExternalDependency, DependencyTypeName +from ..mesonlib import version_compare, version_compare_many, search_version, stringlistify, extract_as_list +from .base import DependencyException, DependencyMethods, detect_compiler, strip_system_includedirs, strip_system_libdirs, SystemDependency, ExternalDependency, DependencyTypeName from .cmake import CMakeDependency from .configtool import ConfigToolDependency +from .detect import packages from .factory import DependencyFactory from .misc import threads_factory from .pkgconfig import PkgConfigDependency @@ -418,14 +420,15 @@ class LLVMDependencyCMake(CMakeDependency): super().__init__(name, env, kwargs, language='cpp', force_use_global_compilers=True) - # Cmake will always create a statically linked binary, so don't use - # cmake if dynamic is required - if not self.static: - self.is_found = False - mlog.warning('Ignoring LLVM CMake dependency because dynamic was requested') + if self.traceparser is None: return - if self.traceparser is None: + if not self.is_found: + return + + #CMake will return not found due to not defined LLVM_DYLIB_COMPONENTS + if not self.static and version_compare(self.version, '< 7.0') and self.llvm_modules: + mlog.warning('Before version 7.0 cmake does not export modules for dynamic linking, cannot check required modules') return # Extract extra include directories and definitions @@ -436,6 +439,7 @@ class LLVMDependencyCMake(CMakeDependency): defs = defs[0].split(' ') temp = ['-I' + x for x in inc_dirs] + defs self.compile_args += [x for x in temp if x not in self.compile_args] + self.compile_args = strip_system_includedirs(env, self.for_machine, self.compile_args) if not self._add_sub_dependency(threads_factory(env, self.for_machine, {})): self.is_found = False return @@ -444,8 +448,33 @@ class LLVMDependencyCMake(CMakeDependency): # Use a custom CMakeLists.txt for LLVM return 'CMakeListsLLVM.txt' + # Check version in CMake to return exact version as config tool (latest allowed) + # It is safe to add .0 to latest argument, it will discarded if we use search_version + def llvm_cmake_versions(self) -> T.List[str]: + + def ver_from_suf(req: str) -> str: + return search_version(req.strip('-')+'.0') + + def version_sorter(a: str, b: str) -> int: + if version_compare(a, "="+b): + return 0 + if version_compare(a, "<"+b): + return 1 + return -1 + + llvm_requested_versions = [ver_from_suf(x) for x in get_llvm_tool_names('') if version_compare(ver_from_suf(x), '>=0')] + if self.version_reqs: + llvm_requested_versions = [ver_from_suf(x) for x in get_llvm_tool_names('') if version_compare_many(ver_from_suf(x), self.version_reqs)] + # CMake sorting before 3.18 is incorrect, sort it here instead + return sorted(llvm_requested_versions, key=functools.cmp_to_key(version_sorter)) + + # Split required and optional modules to distinguish it in CMake def _extra_cmake_opts(self) -> T.List[str]: - return ['-DLLVM_MESON_MODULES={}'.format(';'.join(self.llvm_modules + self.llvm_opt_modules))] + return ['-DLLVM_MESON_REQUIRED_MODULES={}'.format(';'.join(self.llvm_modules)), + '-DLLVM_MESON_OPTIONAL_MODULES={}'.format(';'.join(self.llvm_opt_modules)), + '-DLLVM_MESON_PACKAGE_NAMES={}'.format(';'.join(get_llvm_tool_names(self.name))), + '-DLLVM_MESON_VERSIONS={}'.format(';'.join(self.llvm_cmake_versions())), + '-DLLVM_MESON_DYLIB={}'.format('OFF' if self.static else 'ON')] def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: res = [] @@ -455,7 +484,7 @@ class LLVMDependencyCMake(CMakeDependency): if required: raise self._gen_exception(f'LLVM module {mod} was not found') else: - mlog.warning('Optional LLVM module', mlog.bold(mod), 'was not found') + mlog.warning('Optional LLVM module', mlog.bold(mod), 'was not found', fatal=False) continue for i in cm_targets: res += [(i, required)] @@ -479,6 +508,8 @@ class ValgrindDependency(PkgConfigDependency): def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: return [] +packages['valgrind'] = ValgrindDependency + class ZlibSystemDependency(SystemDependency): @@ -505,7 +536,7 @@ class ZlibSystemDependency(SystemDependency): else: libs = ['z'] for lib in libs: - l = self.clib_compiler.find_library(lib, environment, []) + l = self.clib_compiler.find_library(lib, environment, [], self.libtype) h = self.clib_compiler.has_header('zlib.h', '', environment, dependencies=[self]) if l and h[0]: self.is_found = True @@ -534,8 +565,11 @@ class JNISystemDependency(SystemDependency): modules: T.List[str] = mesonlib.listify(kwargs.get('modules', [])) for module in modules: if module not in {'jvm', 'awt'}: - log = mlog.error if self.required else mlog.debug - log(f'Unknown JNI module ({module})') + msg = f'Unknown JNI module ({module})' + if self.required: + mlog.error(msg) + else: + mlog.debug(msg) self.is_found = False return @@ -553,8 +587,11 @@ class JNISystemDependency(SystemDependency): res = subprocess.run(['/usr/libexec/java_home', '--failfast', '--arch', m.cpu_family], stdout=subprocess.PIPE) if res.returncode != 0: - log = mlog.error if self.required else mlog.debug - log('JAVA_HOME could not be discovered on the system. Please set it explicitly.') + msg = 'JAVA_HOME could not be discovered on the system. Please set it explicitly.' + if self.required: + mlog.error(msg) + else: + mlog.debug(msg) self.is_found = False return self.java_home = pathlib.Path(res.stdout.decode().strip()) @@ -637,6 +674,8 @@ class JNISystemDependency(SystemDependency): return None +packages['jni'] = JNISystemDependency + class JDKSystemDependency(JNISystemDependency): def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW): @@ -649,29 +688,31 @@ class JDKSystemDependency(JNISystemDependency): 'Use the jni system dependency instead' )) +packages['jdk'] = JDKSystemDependency + -llvm_factory = DependencyFactory( +packages['llvm'] = llvm_factory = DependencyFactory( 'LLVM', [DependencyMethods.CMAKE, DependencyMethods.CONFIG_TOOL], cmake_class=LLVMDependencyCMake, configtool_class=LLVMDependencyConfigTool, ) -gtest_factory = DependencyFactory( +packages['gtest'] = gtest_factory = DependencyFactory( 'gtest', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], pkgconfig_class=GTestDependencyPC, system_class=GTestDependencySystem, ) -gmock_factory = DependencyFactory( +packages['gmock'] = gmock_factory = DependencyFactory( 'gmock', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], pkgconfig_class=GMockDependencyPC, system_class=GMockDependencySystem, ) -zlib_factory = DependencyFactory( +packages['zlib'] = zlib_factory = DependencyFactory( 'zlib', [DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM], cmake_name='ZLIB', diff --git a/mesonbuild/dependencies/dub.py b/mesonbuild/dependencies/dub.py index a4a7676..c206ce9 100644 --- a/mesonbuild/dependencies/dub.py +++ b/mesonbuild/dependencies/dub.py @@ -11,10 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .base import ExternalDependency, DependencyException, DependencyTypeName from .pkgconfig import PkgConfigDependency -from ..mesonlib import (Popen_safe, OptionKey, join_args) +from ..mesonlib import (Popen_safe, OptionKey, join_args, version_compare) from ..programs import ExternalProgram from .. import mlog import re @@ -25,8 +26,11 @@ import typing as T if T.TYPE_CHECKING: from ..environment import Environment + class DubDependency(ExternalDependency): - class_dubbin = None + # dub program and version + class_dubbin: T.Optional[T.Tuple[ExternalProgram, str]] = None + class_dubbin_searched = False def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): super().__init__(DependencyTypeName('dub'), environment, kwargs, language='d') @@ -40,19 +44,27 @@ class DubDependency(ExternalDependency): if 'required' in kwargs: self.required = kwargs.get('required') + if DubDependency.class_dubbin is None and not DubDependency.class_dubbin_searched: + DubDependency.class_dubbin = self._check_dub() + DubDependency.class_dubbin_searched = True if DubDependency.class_dubbin is None: - self.dubbin = self._check_dub() - DubDependency.class_dubbin = self.dubbin - else: - self.dubbin = DubDependency.class_dubbin - - if not self.dubbin: if self.required: raise DependencyException('DUB not found.') self.is_found = False return + (self.dubbin, dubver) = DubDependency.class_dubbin # pylint: disable=unpacking-non-sequence + assert isinstance(self.dubbin, ExternalProgram) + + # Check if Dub version is compatible with Meson + if version_compare(dubver, '>1.31.1'): + if self.required: + raise DependencyException( + f"DUB version {dubver} is not compatible with Meson (can't locate artifacts in Dub cache)") + self.is_found = False + return + mlog.debug('Determining dependency {!r} with DUB executable ' '{!r}'.format(name, self.dubbin.get_path())) @@ -124,13 +136,16 @@ class DubDependency(ExternalDependency): elif 'compiler' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled with ', mlog.bold(dub_comp_id)) elif dub_comp_id != 'gdc' and 'compiler_version' not in compatibilities: - mlog.error(mlog.bold(pack_id), 'found but not compiled with', mlog.bold(f'{dub_comp_id}-{self.compiler.version}')) + mlog.error(mlog.bold(pack_id), 'found but not compiled with', + mlog.bold(f'{dub_comp_id}-{self.compiler.version}')) elif 'arch' not in compatibilities: mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold(dub_arch)) elif 'platform' not in compatibilities: - mlog.error(mlog.bold(pack_id), 'found but not compiled for', mlog.bold(description['platform'].join('.'))) + mlog.error(mlog.bold(pack_id), 'found but not compiled for', + mlog.bold(description['platform'].join('.'))) elif 'configuration' not in compatibilities: - mlog.error(mlog.bold(pack_id), 'found but not compiled for the', mlog.bold(pkg['configuration']), 'configuration') + mlog.error(mlog.bold(pack_id), 'found but not compiled for the', + mlog.bold(pkg['configuration']), 'configuration') else: mlog.error(mlog.bold(pack_id), 'not found') @@ -167,7 +182,7 @@ class DubDependency(ExternalDependency): self.is_found = False return - ## check that the main dependency is indeed a library + # check that the main dependency is indeed a library if pkg['name'] == name: self.is_found = True @@ -317,11 +332,12 @@ class DubDependency(ExternalDependency): if ret != 0: mlog.error('Failed to run {!r}', mlog.bold(dub_comp_id)) return (None, None) - d_ver_reg = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2 + d_ver_reg = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2 if d_ver_reg is not None: frontend_version = d_ver_reg.group() - frontend_id = frontend_version.rsplit('.', 1)[0].replace('v', '').replace('.', '') # Fix structure. Ex.: 2081 + frontend_id = frontend_version.rsplit('.', 1)[0].replace( + 'v', '').replace('.', '') # Fix structure. Ex.: 2081 comp_versions.extend([frontend_version, frontend_id]) compatibilities: T.Set[str] = set() @@ -379,25 +395,40 @@ class DubDependency(ExternalDependency): p, out, err = Popen_safe(self.compiler.get_exelist() + args, env=env) return p.returncode, out.strip(), err.strip() - def _check_dub(self) -> T.Union[bool, ExternalProgram]: - dubbin: T.Union[bool, ExternalProgram] = ExternalProgram('dub', silent=True) - assert isinstance(dubbin, ExternalProgram) - if dubbin.found(): + def _check_dub(self) -> T.Optional[T.Tuple[ExternalProgram, str]]: + + def find() -> T.Optional[T.Tuple[ExternalProgram, str]]: + dubbin = ExternalProgram('dub', silent=True) + + if not dubbin.found(): + return None + try: p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2] if p.returncode != 0: mlog.warning('Found dub {!r} but couldn\'t run it' ''.format(' '.join(dubbin.get_command()))) - # Set to False instead of None to signify that we've already - # searched for it and not found it - dubbin = False + return None + except (FileNotFoundError, PermissionError): - dubbin = False + return None + + vermatch = re.search(r'DUB version (\d+\.\d+\.\d+.*), ', out.strip()) + if vermatch: + dubver = vermatch.group(1) + else: + mlog.warning(f"Found dub {' '.join(dubbin.get_command())} but couldn't parse version in {out.strip()}") + return None + + return (dubbin, dubver) + + found = find() + + if found is None: + mlog.log('Found DUB:', mlog.red('NO')) else: - dubbin = False - if isinstance(dubbin, ExternalProgram): + (dubbin, dubver) = found mlog.log('Found DUB:', mlog.bold(dubbin.get_path()), - '(%s)' % out.strip()) - else: - mlog.log('Found DUB:', mlog.red('NO')) - return dubbin + '(version %s)' % dubver) + + return found diff --git a/mesonbuild/dependencies/framework.py b/mesonbuild/dependencies/framework.py index f288851..6c0b1f1 100644 --- a/mesonbuild/dependencies/framework.py +++ b/mesonbuild/dependencies/framework.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .base import DependencyTypeName, ExternalDependency, DependencyException from ..mesonlib import MesonException, Version, stringlistify @@ -75,7 +76,7 @@ class ExtraFrameworkDependency(ExternalDependency): # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html incdir = self._get_framework_include_path(framework_path) if incdir: - self.compile_args += ['-I' + incdir] + self.compile_args += ['-idirafter' + incdir] self.is_found = True return diff --git a/mesonbuild/dependencies/hdf5.py b/mesonbuild/dependencies/hdf5.py index 27f127d..a437e84 100644 --- a/mesonbuild/dependencies/hdf5.py +++ b/mesonbuild/dependencies/hdf5.py @@ -13,18 +13,18 @@ # limitations under the License. # This file contains the detection logic for miscellaneous external dependencies. +from __future__ import annotations import functools import os import re -import subprocess from pathlib import Path -from ..mesonlib import Popen_safe, OrderedSet, join_args -from ..programs import ExternalProgram +from ..mesonlib import OrderedSet, join_args from .base import DependencyException, DependencyMethods from .configtool import ConfigToolDependency -from .pkgconfig import PkgConfigDependency +from .detect import packages +from .pkgconfig import PkgConfigDependency, PkgConfigInterface from .factory import factory_methods import typing as T @@ -48,7 +48,7 @@ class HDF5PkgConfigDependency(PkgConfigDependency): return # some broken pkgconfig don't actually list the full path to the needed includes - newinc = [] # type: T.List[str] + newinc: T.List[str] = [] for arg in self.compile_args: if arg.startswith('-I'): stem = 'static' if self.static else 'shared' @@ -56,7 +56,7 @@ class HDF5PkgConfigDependency(PkgConfigDependency): newinc.append('-I' + str(Path(arg[2:]) / stem)) self.compile_args += newinc - link_args = [] # type: T.List[str] + link_args: T.List[str] = [] for larg in self.get_link_args(): lpath = Path(larg) # some pkg-config hdf5.pc (e.g. Ubuntu) don't include the commonly-used HL HDF5 libraries, @@ -96,12 +96,15 @@ class HDF5ConfigToolDependency(ConfigToolDependency): if language == 'c': cenv = 'CC' + lenv = 'C' tools = ['h5cc', 'h5pcc'] elif language == 'cpp': cenv = 'CXX' + lenv = 'CXX' tools = ['h5c++', 'h5pc++'] elif language == 'fortran': cenv = 'FC' + lenv = 'F' tools = ['h5fc', 'h5pfc'] else: raise DependencyException('How did you get here?') @@ -118,11 +121,11 @@ class HDF5ConfigToolDependency(ConfigToolDependency): compiler = environment.coredata.compilers[for_machine][language] try: os.environ[f'HDF5_{cenv}'] = join_args(compiler.get_exelist()) - os.environ[f'HDF5_{cenv}LINKER'] = join_args(compiler.get_linker_exelist()) + os.environ[f'HDF5_{lenv}LINKER'] = join_args(compiler.get_linker_exelist()) super().__init__(name, environment, nkwargs, language) finally: del os.environ[f'HDF5_{cenv}'] - del os.environ[f'HDF5_{cenv}LINKER'] + del os.environ[f'HDF5_{lenv}LINKER'] if not self.is_found: return @@ -138,13 +141,6 @@ class HDF5ConfigToolDependency(ConfigToolDependency): elif Path(arg).is_file(): self.link_args.append(arg) - # If the language is not C we need to add C as a subdependency - if language != 'c': - nkwargs = kwargs.copy() - nkwargs['language'] = 'c' - # I'm being too clever for mypy and pylint - self.is_found = self._add_sub_dependency(hdf5_factory(environment, for_machine, nkwargs)) # pylint: disable=no-value-for-parameter - def _sanitize_version(self, ver: str) -> str: v = re.search(r'\s*HDF5 Version: (\d+\.\d+\.\d+)', ver) return v.group(1) @@ -159,21 +155,18 @@ def hdf5_factory(env: 'Environment', for_machine: 'MachineChoice', if DependencyMethods.PKGCONFIG in methods: # Use an ordered set so that these remain the first tried pkg-config files pkgconfig_files = OrderedSet(['hdf5', 'hdf5-serial']) - PCEXE = PkgConfigDependency._detect_pkgbin(False, env, for_machine) - pcenv = PkgConfigDependency.setup_env(os.environ, env, for_machine) - if PCEXE: - assert isinstance(PCEXE, ExternalProgram) + pkg = PkgConfigInterface.instance(env, for_machine, silent=False) + if pkg: # some distros put hdf5-1.2.3.pc with version number in .pc filename. - ret, stdout, _ = Popen_safe(PCEXE.get_command() + ['--list-all'], stderr=subprocess.DEVNULL, env=pcenv) - if ret.returncode == 0: - for pkg in stdout.split('\n'): - if pkg.startswith('hdf5'): - pkgconfig_files.add(pkg.split(' ', 1)[0]) - - for pkg in pkgconfig_files: - candidates.append(functools.partial(HDF5PkgConfigDependency, pkg, env, kwargs, language)) + for mod in pkg.list_all(): + if mod.startswith('hdf5'): + pkgconfig_files.add(mod) + for mod in pkgconfig_files: + candidates.append(functools.partial(HDF5PkgConfigDependency, mod, env, kwargs, language)) if DependencyMethods.CONFIG_TOOL in methods: candidates.append(functools.partial(HDF5ConfigToolDependency, 'hdf5', env, kwargs, language)) return candidates + +packages['hdf5'] = hdf5_factory diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py index d23eeee..b41f3c2 100644 --- a/mesonbuild/dependencies/misc.py +++ b/mesonbuild/dependencies/misc.py @@ -13,31 +13,30 @@ # limitations under the License. # This file contains the detection logic for miscellaneous external dependencies. +from __future__ import annotations -from pathlib import Path import functools import re -import sysconfig import typing as T from .. import mesonlib from .. import mlog -from ..environment import detect_cpu_family from .base import DependencyException, DependencyMethods from .base import BuiltinDependency, SystemDependency -from .cmake import CMakeDependency +from .cmake import CMakeDependency, CMakeDependencyFactory from .configtool import ConfigToolDependency +from .detect import packages from .factory import DependencyFactory, factory_methods from .pkgconfig import PkgConfigDependency if T.TYPE_CHECKING: - from ..environment import Environment, MachineChoice + from ..environment import Environment from .factory import DependencyGenerator @factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE}) def netcdf_factory(env: 'Environment', - for_machine: 'MachineChoice', + for_machine: 'mesonlib.MachineChoice', kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']: language = kwargs.get('language', 'c') @@ -59,6 +58,8 @@ def netcdf_factory(env: 'Environment', return candidates +packages['netcdf'] = netcdf_factory + class DlBuiltinDependency(BuiltinDependency): def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): @@ -84,6 +85,8 @@ class DlSystemDependency(SystemDependency): class OpenMPDependency(SystemDependency): # Map date of specification release (which is the macro value) to a version. VERSIONS = { + '202111': '5.2', + '202011': '5.1', '201811': '5.0', '201611': '5.0-revision1', # This is supported by ICC 19.x '201511': '4.5', @@ -138,6 +141,8 @@ class OpenMPDependency(SystemDependency): if not self.is_found: mlog.log(mlog.yellow('WARNING:'), 'OpenMP found but omp.h missing.') +packages['openmp'] = OpenMPDependency + class ThreadDependency(SystemDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None: @@ -185,121 +190,27 @@ class BlocksDependency(SystemDependency): self.is_found = True +packages['blocks'] = BlocksDependency -class Python3DependencySystem(SystemDependency): - def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None: - super().__init__(name, environment, kwargs) - - if not environment.machines.matches_build_machine(self.for_machine): - return - if not environment.machines[self.for_machine].is_windows(): - return - - self.name = 'python3' - # We can only be sure that it is Python 3 at this point - self.version = '3' - self._find_libpy3_windows(environment) - - @staticmethod - def get_windows_python_arch() -> T.Optional[str]: - pyplat = sysconfig.get_platform() - if pyplat == 'mingw': - pycc = sysconfig.get_config_var('CC') - if pycc.startswith('x86_64'): - return '64' - elif pycc.startswith(('i686', 'i386')): - return '32' - else: - mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug') - return None - elif pyplat == 'win32': - return '32' - elif pyplat in {'win64', 'win-amd64'}: - return '64' - mlog.log(f'Unknown Windows Python platform {pyplat!r}') - return None - - def get_windows_link_args(self) -> T.Optional[T.List[str]]: - pyplat = sysconfig.get_platform() - if pyplat.startswith('win'): - vernum = sysconfig.get_config_var('py_version_nodot') - if self.static: - libpath = Path('libs') / f'libpython{vernum}.a' - else: - comp = self.get_compiler() - if comp.id == "gcc": - libpath = Path(f'python{vernum}.dll') - else: - libpath = Path('libs') / f'python{vernum}.lib' - lib = Path(sysconfig.get_config_var('base')) / libpath - elif pyplat == 'mingw': - if self.static: - libname = sysconfig.get_config_var('LIBRARY') - else: - libname = sysconfig.get_config_var('LDLIBRARY') - lib = Path(sysconfig.get_config_var('LIBDIR')) / libname - if not lib.exists(): - mlog.log('Could not find Python3 library {!r}'.format(str(lib))) - return None - return [str(lib)] - - def _find_libpy3_windows(self, env: 'Environment') -> None: - ''' - Find python3 libraries on Windows and also verify that the arch matches - what we are building for. - ''' - pyarch = self.get_windows_python_arch() - if pyarch is None: - self.is_found = False - return - arch = detect_cpu_family(env.coredata.compilers.host) - if arch == 'x86': - arch = '32' - elif arch == 'x86_64': - arch = '64' - else: - # We can't cross-compile Python 3 dependencies on Windows yet - mlog.log(f'Unknown architecture {arch!r} for', - mlog.bold(self.name)) - self.is_found = False - return - # Pyarch ends in '32' or '64' - if arch != pyarch: - mlog.log('Need', mlog.bold(self.name), 'for {}-bit, but ' - 'found {}-bit'.format(arch, pyarch)) - self.is_found = False - return - # This can fail if the library is not found - largs = self.get_windows_link_args() - if largs is None: - self.is_found = False - return - self.link_args = largs - # Compile args - inc = sysconfig.get_path('include') - platinc = sysconfig.get_path('platinclude') - self.compile_args = ['-I' + inc] - if inc != platinc: - self.compile_args.append('-I' + platinc) - self.version = sysconfig.get_config_var('py_version') - self.is_found = True - - @staticmethod - def log_tried() -> str: - return 'sysconfig' class PcapDependencyConfigTool(ConfigToolDependency): tools = ['pcap-config'] tool_name = 'pcap-config' + # version 1.10.2 added error checking for invalid arguments + # version 1.10.3 will hopefully add actual support for --version + skip_version = '--help' + def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): super().__init__(name, environment, kwargs) if not self.is_found: return self.compile_args = self.get_config_value(['--cflags'], 'compile_args') self.link_args = self.get_config_value(['--libs'], 'link_args') - self.version = self.get_pcap_lib_version() + if self.version is None: + # older pcap-config versions don't support this + self.version = self.get_pcap_lib_version() def get_pcap_lib_version(self) -> T.Optional[str]: # Since we seem to need to run a program to discover the pcap version, @@ -430,7 +341,7 @@ class CursesSystemDependency(SystemDependency): ('curses', ['curses.h']), ] - # Not sure how else to elegently break out of both loops + # Not sure how else to elegantly break out of both loops for lib, headers in candidates: l = self.clib_compiler.find_library(lib, env, []) if l: @@ -566,7 +477,7 @@ class OpensslSystemDependency(SystemDependency): @factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM}) def curses_factory(env: 'Environment', - for_machine: 'MachineChoice', + for_machine: 'mesonlib.MachineChoice', kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']: candidates: T.List['DependencyGenerator'] = [] @@ -587,11 +498,12 @@ def curses_factory(env: 'Environment', candidates.append(functools.partial(CursesSystemDependency, 'curses', env, kwargs)) return candidates +packages['curses'] = curses_factory @factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}) def shaderc_factory(env: 'Environment', - for_machine: 'MachineChoice', + for_machine: 'mesonlib.MachineChoice', kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']: """Custom DependencyFactory for ShaderC. @@ -622,96 +534,86 @@ def shaderc_factory(env: 'Environment', candidates.append(functools.partial(ShadercDependency, env, kwargs)) return candidates +packages['shaderc'] = shaderc_factory -cups_factory = DependencyFactory( +packages['cups'] = cups_factory = DependencyFactory( 'cups', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK, DependencyMethods.CMAKE], configtool_class=CupsDependencyConfigTool, cmake_name='Cups', ) -dl_factory = DependencyFactory( +packages['dl'] = dl_factory = DependencyFactory( 'dl', [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], builtin_class=DlBuiltinDependency, system_class=DlSystemDependency, ) -gpgme_factory = DependencyFactory( +packages['gpgme'] = gpgme_factory = DependencyFactory( 'gpgme', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], configtool_class=GpgmeDependencyConfigTool, ) -libgcrypt_factory = DependencyFactory( +packages['libgcrypt'] = libgcrypt_factory = DependencyFactory( 'libgcrypt', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], configtool_class=LibGCryptDependencyConfigTool, ) -libwmf_factory = DependencyFactory( +packages['libwmf'] = libwmf_factory = DependencyFactory( 'libwmf', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], configtool_class=LibWmfDependencyConfigTool, ) -pcap_factory = DependencyFactory( +packages['pcap'] = pcap_factory = DependencyFactory( 'pcap', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], configtool_class=PcapDependencyConfigTool, pkgconfig_name='libpcap', ) -python3_factory = DependencyFactory( - 'python3', - [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.EXTRAFRAMEWORK], - system_class=Python3DependencySystem, - # There is no version number in the macOS version number - framework_name='Python', - # There is a python in /System/Library/Frameworks, but that's python 2.x, - # Python 3 will always be in /Library - extra_kwargs={'paths': ['/Library/Frameworks']}, -) - -threads_factory = DependencyFactory( +packages['threads'] = threads_factory = DependencyFactory( 'threads', [DependencyMethods.SYSTEM, DependencyMethods.CMAKE], cmake_name='Threads', system_class=ThreadDependency, ) -iconv_factory = DependencyFactory( +packages['iconv'] = iconv_factory = DependencyFactory( 'iconv', [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], builtin_class=IconvBuiltinDependency, system_class=IconvSystemDependency, ) -intl_factory = DependencyFactory( +packages['intl'] = intl_factory = DependencyFactory( 'intl', [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], builtin_class=IntlBuiltinDependency, system_class=IntlSystemDependency, ) -openssl_factory = DependencyFactory( +packages['openssl'] = openssl_factory = DependencyFactory( 'openssl', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], system_class=OpensslSystemDependency, - cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::Crypto', 'OpenSSL::SSL'])), + cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::Crypto', 'OpenSSL::SSL']), ) -libcrypto_factory = DependencyFactory( +packages['libcrypto'] = libcrypto_factory = DependencyFactory( 'libcrypto', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], system_class=OpensslSystemDependency, - cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::Crypto'])), + cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::Crypto']), ) -libssl_factory = DependencyFactory( +packages['libssl'] = libssl_factory = DependencyFactory( 'libssl', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], system_class=OpensslSystemDependency, - cmake_class=lambda name, env, kwargs: CMakeDependency('OpenSSL', env, dict(kwargs, modules=['OpenSSL::SSL'])), + cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::SSL']), ) diff --git a/mesonbuild/dependencies/mpi.py b/mesonbuild/dependencies/mpi.py index ddb512a..d9a1585 100644 --- a/mesonbuild/dependencies/mpi.py +++ b/mesonbuild/dependencies/mpi.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import functools import typing as T @@ -20,12 +21,14 @@ import re from ..environment import detect_cpu_family from .base import DependencyMethods, detect_compiler, SystemDependency from .configtool import ConfigToolDependency +from .detect import packages from .factory import factory_methods from .pkgconfig import PkgConfigDependency if T.TYPE_CHECKING: from .factory import DependencyGenerator - from ..environment import Environment, MachineChoice + from ..environment import Environment + from ..mesonlib import MachineChoice @factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM}) @@ -71,7 +74,7 @@ def mpi_factory(env: 'Environment', elif language == 'fortran': tool_names = [os.environ.get('I_MPI_F90'), 'mpiifort'] - cls = IntelMPIConfigToolDependency # type: T.Type[ConfigToolDependency] + cls: T.Type[ConfigToolDependency] = IntelMPIConfigToolDependency else: # OpenMPI, which doesn't work with intel # # We try the environment variables for the tools first, but then @@ -99,6 +102,8 @@ def mpi_factory(env: 'Environment', return candidates +packages['mpi'] = mpi_factory + class _MPIConfigToolDependency(ConfigToolDependency): diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py index 231ec51..e86206b 100644 --- a/mesonbuild/dependencies/pkgconfig.py +++ b/mesonbuild/dependencies/pkgconfig.py @@ -16,141 +16,255 @@ from __future__ import annotations from pathlib import Path from .base import ExternalDependency, DependencyException, sort_libpaths, DependencyTypeName -from ..mesonlib import OptionKey, OrderedSet, PerMachine, Popen_safe +from ..mesonlib import EnvironmentVariables, OptionKey, OrderedSet, PerMachine, Popen_safe, Popen_safe_logged, MachineChoice, join_args from ..programs import find_external_program, ExternalProgram from .. import mlog from pathlib import PurePath +from functools import lru_cache import re import os import shlex import typing as T if T.TYPE_CHECKING: - from ..environment import Environment - from ..mesonlib import MachineChoice + from typing_extensions import Literal from .._typing import ImmutableListProtocol - from ..build import EnvironmentVariables -class PkgConfigDependency(ExternalDependency): - # The class's copy of the pkg-config path. Avoids having to search for it - # multiple times in the same Meson invocation. - class_pkgbin: PerMachine[T.Union[None, bool, ExternalProgram]] = PerMachine(None, None) - # We cache all pkg-config subprocess invocations to avoid redundant calls - pkgbin_cache: T.Dict[ - T.Tuple[ExternalProgram, T.Tuple[str, ...], T.FrozenSet[T.Tuple[str, str]]], - T.Tuple[int, str, str] - ] = {} - - def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: - super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language) - self.name = name - self.is_libtool = False - # Store a copy of the pkg-config path on the object itself so it is - # stored in the pickled coredata and recovered. - self.pkgbin = self._detect_pkgbin(self.silent, self.env, self.for_machine) - if self.pkgbin is False: - self.pkgbin = None - msg = f'Pkg-config binary for machine {self.for_machine} not found. Giving up.' - if self.required: - raise DependencyException(msg) - else: - mlog.debug(msg) - return + from ..environment import Environment + from ..utils.core import EnvironOrDict + from ..interpreter.type_checking import PkgConfigDefineType - assert isinstance(self.pkgbin, ExternalProgram) - mlog.debug('Determining dependency {!r} with pkg-config executable ' - '{!r}'.format(name, self.pkgbin.get_path())) - ret, self.version, _ = self._call_pkgbin(['--modversion', name]) - if ret != 0: - return +class PkgConfigInterface: + '''Base class wrapping a pkg-config implementation''' - self.is_found = True + class_impl: PerMachine[T.Union[Literal[False], T.Optional[PkgConfigInterface]]] = PerMachine(False, False) + class_cli_impl: PerMachine[T.Union[Literal[False], T.Optional[PkgConfigCLI]]] = PerMachine(False, False) - try: - # Fetch cargs to be used while using this dependency - self._set_cargs() - # Fetch the libraries and library paths needed for using this - self._set_libs() - except DependencyException as e: - mlog.debug(f"pkg-config error with '{name}': {e}") - if self.required: - raise - else: - self.compile_args = [] - self.link_args = [] - self.is_found = False - self.reason = e + @staticmethod + def instance(env: Environment, for_machine: MachineChoice, silent: bool) -> T.Optional[PkgConfigInterface]: + '''Return a pkg-config implementation singleton''' + for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST + impl = PkgConfigInterface.class_impl[for_machine] + if impl is False: + impl = PkgConfigCLI(env, for_machine, silent) + if not impl.found(): + impl = None + if not impl and not silent: + mlog.log('Found pkg-config:', mlog.red('NO')) + PkgConfigInterface.class_impl[for_machine] = impl + return impl - def __repr__(self) -> str: - s = '<{0} {1}: {2} {3}>' - return s.format(self.__class__.__name__, self.name, self.is_found, - self.version_reqs) + @staticmethod + def _cli(env: Environment, for_machine: MachineChoice, silent: bool = False) -> T.Optional[PkgConfigCLI]: + '''Return the CLI pkg-config implementation singleton + Even when we use another implementation internally, external tools might + still need the CLI implementation. + ''' + for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST + impl: T.Union[Literal[False], T.Optional[PkgConfigInterface]] # Help confused mypy + impl = PkgConfigInterface.instance(env, for_machine, silent) + if impl and not isinstance(impl, PkgConfigCLI): + impl = PkgConfigInterface.class_cli_impl[for_machine] + if impl is False: + impl = PkgConfigCLI(env, for_machine, silent) + if not impl.found(): + impl = None + PkgConfigInterface.class_cli_impl[for_machine] = impl + return T.cast('T.Optional[PkgConfigCLI]', impl) # Trust me, mypy - @classmethod - def _detect_pkgbin(cls, silent: bool, env: Environment, - for_machine: MachineChoice) -> T.Union[None, bool, ExternalProgram]: - # Only search for pkg-config for each machine the first time and store - # the result in the class definition - if cls.class_pkgbin[for_machine] is False: - mlog.debug(f'Pkg-config binary for {for_machine} is cached as not found.') - elif cls.class_pkgbin[for_machine] is not None: - mlog.debug(f'Pkg-config binary for {for_machine} is cached.') - else: - assert cls.class_pkgbin[for_machine] is None, 'for mypy' - mlog.debug(f'Pkg-config binary for {for_machine} is not cached.') - for potential_pkgbin in find_external_program( - env, for_machine, 'pkgconfig', 'Pkg-config', - env.default_pkgconfig, allow_default_for_cross=False): - version_if_ok = cls.check_pkgconfig(env, potential_pkgbin) - if not version_if_ok: - continue - if not silent: - mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()), - f'({version_if_ok})') - cls.class_pkgbin[for_machine] = potential_pkgbin - break - else: - if not silent: - mlog.log('Found Pkg-config:', mlog.red('NO')) - # Set to False instead of None to signify that we've already - # searched for it and not found it - cls.class_pkgbin[for_machine] = False + @staticmethod + def get_env(env: Environment, for_machine: MachineChoice, uninstalled: bool = False) -> EnvironmentVariables: + cli = PkgConfigInterface._cli(env, for_machine) + return cli._get_env(uninstalled) if cli else EnvironmentVariables() + + @staticmethod + def setup_env(environ: EnvironOrDict, env: Environment, for_machine: MachineChoice, + uninstalled: bool = False) -> EnvironOrDict: + cli = PkgConfigInterface._cli(env, for_machine) + return cli._setup_env(environ, uninstalled) if cli else environ + + def __init__(self, env: Environment, for_machine: MachineChoice) -> None: + self.env = env + self.for_machine = for_machine + + def found(self) -> bool: + '''Return whether pkg-config is supported''' + raise NotImplementedError + + def version(self, name: str) -> T.Optional[str]: + '''Return module version or None if not found''' + raise NotImplementedError + + def cflags(self, name: str, allow_system: bool = False, + define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]: + '''Return module cflags + @allow_system: If False, remove default system include paths + ''' + raise NotImplementedError - return cls.class_pkgbin[for_machine] + def libs(self, name: str, static: bool = False, allow_system: bool = False, + define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]: + '''Return module libs + @static: If True, also include private libraries + @allow_system: If False, remove default system libraries search paths + ''' + raise NotImplementedError - def _call_pkgbin_real(self, args: T.List[str], env: T.Dict[str, str]) -> T.Tuple[int, str, str]: - assert isinstance(self.pkgbin, ExternalProgram) - cmd = self.pkgbin.get_command() + args - p, out, err = Popen_safe(cmd, env=env) - rc, out, err = p.returncode, out.strip(), err.strip() - call = ' '.join(cmd) - mlog.debug(f"Called `{call}` -> {rc}\n{out}") - return rc, out, err + def variable(self, name: str, variable_name: str, + define_variable: PkgConfigDefineType) -> T.Optional[str]: + '''Return module variable or None if variable is not defined''' + raise NotImplementedError + + def list_all(self) -> ImmutableListProtocol[str]: + '''Return all available pkg-config modules''' + raise NotImplementedError + +class PkgConfigCLI(PkgConfigInterface): + '''pkg-config CLI implementation''' + + def __init__(self, env: Environment, for_machine: MachineChoice, silent: bool) -> None: + super().__init__(env, for_machine) + self._detect_pkgbin() + if self.pkgbin and not silent: + mlog.log('Found pkg-config:', mlog.green('YES'), mlog.bold(f'({self.pkgbin.get_path()})'), mlog.blue(self.pkgbin_version)) + + def found(self) -> bool: + return bool(self.pkgbin) + + @lru_cache(maxsize=None) + def version(self, name: str) -> T.Optional[str]: + mlog.debug(f'Determining dependency {name!r} with pkg-config executable {self.pkgbin.get_path()!r}') + ret, version, _ = self._call_pkgbin(['--modversion', name]) + return version if ret == 0 else None + + @staticmethod + def _define_variable_args(define_variable: PkgConfigDefineType) -> T.List[str]: + ret = [] + if define_variable: + for pair in define_variable: + ret.append('--define-variable=' + '='.join(pair)) + return ret + + @lru_cache(maxsize=None) + def cflags(self, name: str, allow_system: bool = False, + define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]: + env = None + if allow_system: + env = os.environ.copy() + env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' + args: T.List[str] = [] + args += self._define_variable_args(define_variable) + args += ['--cflags', name] + ret, out, err = self._call_pkgbin(args, env=env) + if ret != 0: + raise DependencyException(f'Could not generate cflags for {name}:\n{err}\n') + return self._split_args(out) + + @lru_cache(maxsize=None) + def libs(self, name: str, static: bool = False, allow_system: bool = False, + define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]: + env = None + if allow_system: + env = os.environ.copy() + env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1' + args: T.List[str] = [] + args += self._define_variable_args(define_variable) + if static: + args.append('--static') + args += ['--libs', name] + ret, out, err = self._call_pkgbin(args, env=env) + if ret != 0: + raise DependencyException(f'Could not generate libs for {name}:\n{err}\n') + return self._split_args(out) + + @lru_cache(maxsize=None) + def variable(self, name: str, variable_name: str, + define_variable: PkgConfigDefineType) -> T.Optional[str]: + args: T.List[str] = [] + args += self._define_variable_args(define_variable) + args += ['--variable=' + variable_name, name] + ret, out, err = self._call_pkgbin(args) + if ret != 0: + raise DependencyException(f'Could not get variable for {name}:\n{err}\n') + variable = out.strip() + # pkg-config doesn't distinguish between empty and nonexistent variables + # use the variable list to check for variable existence + if not variable: + ret, out, _ = self._call_pkgbin(['--print-variables', name]) + if not re.search(rf'^{variable_name}$', out, re.MULTILINE): + return None + mlog.debug(f'Got pkg-config variable {variable_name} : {variable}') + return variable + + @lru_cache(maxsize=None) + def list_all(self) -> ImmutableListProtocol[str]: + ret, out, err = self._call_pkgbin(['--list-all']) + if ret != 0: + raise DependencyException(f'could not list modules:\n{err}\n') + return [i.split(' ', 1)[0] for i in out.splitlines()] @staticmethod - def get_env(environment: 'Environment', for_machine: MachineChoice, - uninstalled: bool = False) -> 'EnvironmentVariables': - from ..build import EnvironmentVariables + def _split_args(cmd: str) -> T.List[str]: + # pkg-config paths follow Unix conventions, even on Windows; split the + # output using shlex.split rather than mesonlib.split_args + return shlex.split(cmd) + + def _detect_pkgbin(self) -> None: + for potential_pkgbin in find_external_program( + self.env, self.for_machine, 'pkg-config', 'Pkg-config', + self.env.default_pkgconfig, allow_default_for_cross=False): + version_if_ok = self._check_pkgconfig(potential_pkgbin) + if version_if_ok: + self.pkgbin = potential_pkgbin + self.pkgbin_version = version_if_ok + return + self.pkgbin = None + + def _check_pkgconfig(self, pkgbin: ExternalProgram) -> T.Optional[str]: + if not pkgbin.found(): + mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}') + return None + command_as_string = ' '.join(pkgbin.get_command()) + try: + helptext = Popen_safe(pkgbin.get_command() + ['--help'])[1] + if 'Pure-Perl' in helptext: + mlog.log(f'Found pkg-config {command_as_string!r} but it is Strawberry Perl and thus broken. Ignoring...') + return None + p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2] + if p.returncode != 0: + mlog.warning(f'Found pkg-config {command_as_string!r} but it failed when ran') + return None + except FileNotFoundError: + mlog.warning(f'We thought we found pkg-config {command_as_string!r} but now it\'s not there. How odd!') + return None + except PermissionError: + msg = f'Found pkg-config {command_as_string!r} but didn\'t have permissions to run it.' + if not self.env.machines.build.is_windows(): + msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' + mlog.warning(msg) + return None + return out.strip() + + def _get_env(self, uninstalled: bool = False) -> EnvironmentVariables: env = EnvironmentVariables() - key = OptionKey('pkg_config_path', machine=for_machine) - extra_paths: T.List[str] = environment.coredata.options[key].value[:] + key = OptionKey('pkg_config_path', machine=self.for_machine) + extra_paths: T.List[str] = self.env.coredata.options[key].value[:] if uninstalled: - uninstalled_path = Path(environment.get_build_dir(), 'meson-uninstalled').as_posix() + uninstalled_path = Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix() if uninstalled_path not in extra_paths: extra_paths.append(uninstalled_path) env.set('PKG_CONFIG_PATH', extra_paths) - sysroot = environment.properties[for_machine].get_sys_root() + sysroot = self.env.properties[self.for_machine].get_sys_root() if sysroot: env.set('PKG_CONFIG_SYSROOT_DIR', [sysroot]) - pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir() + pkg_config_libdir_prop = self.env.properties[self.for_machine].get_pkg_config_libdir() if pkg_config_libdir_prop: env.set('PKG_CONFIG_LIBDIR', pkg_config_libdir_prop) + env.set('PKG_CONFIG', [join_args(self.pkgbin.get_command())]) return env - @staticmethod - def setup_env(env: T.MutableMapping[str, str], environment: 'Environment', for_machine: MachineChoice, - uninstalled: bool = False) -> T.Dict[str, str]: - envvars = PkgConfigDependency.get_env(environment, for_machine, uninstalled) + def _setup_env(self, env: EnvironOrDict, uninstalled: bool = False) -> T.Dict[str, str]: + envvars = self._get_env(uninstalled) env = envvars.get_env(env) # Dump all PKG_CONFIG environment variables for key, value in env.items(): @@ -158,19 +272,57 @@ class PkgConfigDependency(ExternalDependency): mlog.debug(f'env[{key}]: {value}') return env - def _call_pkgbin(self, args: T.List[str], env: T.Optional[T.MutableMapping[str, str]] = None) -> T.Tuple[int, str, str]: + def _call_pkgbin(self, args: T.List[str], env: T.Optional[EnvironOrDict] = None) -> T.Tuple[int, str, str]: assert isinstance(self.pkgbin, ExternalProgram) env = env or os.environ - env = PkgConfigDependency.setup_env(env, self.env, self.for_machine) + env = self._setup_env(env) + cmd = self.pkgbin.get_command() + args + p, out, err = Popen_safe_logged(cmd, env=env) + return p.returncode, out.strip(), err.strip() + + +class PkgConfigDependency(ExternalDependency): - fenv = frozenset(env.items()) - targs = tuple(args) - cache = PkgConfigDependency.pkgbin_cache - if (self.pkgbin, targs, fenv) not in cache: - cache[(self.pkgbin, targs, fenv)] = self._call_pkgbin_real(args, env) - return cache[(self.pkgbin, targs, fenv)] + def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: + super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language) + self.name = name + self.is_libtool = False + self.pkgconfig = PkgConfigInterface.instance(self.env, self.for_machine, self.silent) + if not self.pkgconfig: + msg = f'Pkg-config for machine {self.for_machine} not found. Giving up.' + if self.required: + raise DependencyException(msg) + mlog.debug(msg) + return - def _convert_mingw_paths(self, args: T.List[str]) -> T.List[str]: + version = self.pkgconfig.version(name) + if version is None: + return + + self.version = version + self.is_found = True + + try: + # Fetch cargs to be used while using this dependency + self._set_cargs() + # Fetch the libraries and library paths needed for using this + self._set_libs() + except DependencyException as e: + mlog.debug(f"Pkg-config error with '{name}': {e}") + if self.required: + raise + else: + self.compile_args = [] + self.link_args = [] + self.is_found = False + self.reason = e + + def __repr__(self) -> str: + s = '<{0} {1}: {2} {3}>' + return s.format(self.__class__.__name__, self.name, self.is_found, + self.version_reqs) + + def _convert_mingw_paths(self, args: ImmutableListProtocol[str]) -> T.List[str]: ''' Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo paths so convert them to C:/foo. We cannot resolve other paths starting @@ -178,7 +330,7 @@ class PkgConfigDependency(ExternalDependency): error/warning from the compiler/linker. ''' if not self.env.machines.build.is_windows(): - return args + return args.copy() converted = [] for arg in args: pargs: T.Tuple[str, ...] = tuple() @@ -201,27 +353,19 @@ class PkgConfigDependency(ExternalDependency): converted.append(arg) return converted - def _split_args(self, cmd: str) -> T.List[str]: - # pkg-config paths follow Unix conventions, even on Windows; split the - # output using shlex.split rather than mesonlib.split_args - return shlex.split(cmd) - def _set_cargs(self) -> None: - env = None + allow_system = False if self.language == 'fortran': # gfortran doesn't appear to look in system paths for INCLUDE files, # so don't allow pkg-config to suppress -I flags for system paths - env = os.environ.copy() - env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' - ret, out, err = self._call_pkgbin(['--cflags', self.name], env=env) - if ret != 0: - raise DependencyException(f'Could not generate cargs for {self.name}:\n{err}\n') - self.compile_args = self._convert_mingw_paths(self._split_args(out)) + allow_system = True + cflags = self.pkgconfig.cflags(self.name, allow_system) + self.compile_args = self._convert_mingw_paths(cflags) - def _search_libs(self, out: str, out_raw: str) -> T.Tuple[T.List[str], T.List[str]]: + def _search_libs(self, libs_in: ImmutableListProtocol[str], raw_libs_in: ImmutableListProtocol[str]) -> T.Tuple[T.List[str], T.List[str]]: ''' - @out: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs - @out_raw: pkg-config --libs + @libs_in: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs + @raw_libs_in: pkg-config --libs We always look for the file ourselves instead of depending on the compiler to find it with -lfoo or foo.lib (if possible) because: @@ -245,7 +389,7 @@ class PkgConfigDependency(ExternalDependency): # always searched first. prefix_libpaths: OrderedSet[str] = OrderedSet() # We also store this raw_link_args on the object later - raw_link_args = self._convert_mingw_paths(self._split_args(out_raw)) + raw_link_args = self._convert_mingw_paths(raw_libs_in) for arg in raw_link_args: if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')): path = arg[2:] @@ -266,7 +410,7 @@ class PkgConfigDependency(ExternalDependency): pkg_config_path = self._convert_mingw_paths(pkg_config_path) prefix_libpaths = OrderedSet(sort_libpaths(list(prefix_libpaths), pkg_config_path)) system_libpaths: OrderedSet[str] = OrderedSet() - full_args = self._convert_mingw_paths(self._split_args(out)) + full_args = self._convert_mingw_paths(libs_in) for arg in full_args: if arg.startswith(('-L-l', '-L-L')): # These are D language arguments, not library paths @@ -318,7 +462,8 @@ class PkgConfigDependency(ExternalDependency): continue if self.clib_compiler: args = self.clib_compiler.find_library(lib[2:], self.env, - libpaths, self.libtype) + libpaths, self.libtype, + lib_prefix_warning=False) # If the project only uses a non-clib language such as D, Rust, # C#, Python, etc, all we can do is limp along by adding the # arguments as-is and then adding the libpaths at the end. @@ -370,83 +515,14 @@ class PkgConfigDependency(ExternalDependency): return link_args, raw_link_args def _set_libs(self) -> None: - env = None - libcmd = ['--libs'] - - if self.static: - libcmd.append('--static') - - libcmd.append(self.name) - # Force pkg-config to output -L fields even if they are system # paths so we can do manual searching with cc.find_library() later. - env = os.environ.copy() - env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1' - ret, out, err = self._call_pkgbin(libcmd, env=env) - if ret != 0: - raise DependencyException(f'Could not generate libs for {self.name}:\n{err}\n') + libs = self.pkgconfig.libs(self.name, self.static, allow_system=True) # Also get the 'raw' output without -Lfoo system paths for adding -L # args with -lfoo when a library can't be found, and also in # gnome.generate_gir + gnome.gtkdoc which need -L -l arguments. - ret, out_raw, err_raw = self._call_pkgbin(libcmd) - if ret != 0: - raise DependencyException(f'Could not generate libs for {self.name}:\n\n{out_raw}') - self.link_args, self.raw_link_args = self._search_libs(out, out_raw) - - def get_pkgconfig_variable(self, variable_name: str, - define_variable: 'ImmutableListProtocol[str]', - default: T.Optional[str]) -> str: - options = ['--variable=' + variable_name, self.name] - - if define_variable: - options = ['--define-variable=' + '='.join(define_variable)] + options - - ret, out, err = self._call_pkgbin(options) - variable = '' - if ret != 0: - if self.required: - raise DependencyException(f'dependency {self.name} not found:\n{err}\n') - else: - variable = out.strip() - - # pkg-config doesn't distinguish between empty and non-existent variables - # use the variable list to check for variable existence - if not variable: - ret, out, _ = self._call_pkgbin(['--print-variables', self.name]) - if not re.search(r'^' + variable_name + r'$', out, re.MULTILINE): - if default is not None: - variable = default - else: - mlog.warning(f"pkgconfig variable '{variable_name}' not defined for dependency {self.name}.") - - mlog.debug(f'Got pkgconfig variable {variable_name} : {variable}') - return variable - - @staticmethod - def check_pkgconfig(env: Environment, pkgbin: ExternalProgram) -> T.Optional[str]: - if not pkgbin.found(): - mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}') - return None - command_as_string = ' '.join(pkgbin.get_command()) - try: - helptext = Popen_safe(pkgbin.get_command() + ['--help'])[1] - if 'Pure-Perl' in helptext: - mlog.log(f'found pkg-config {command_as_string!r} but it is Strawberry Perl and thus broken. Ignoring...') - return None - p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2] - if p.returncode != 0: - mlog.warning(f'Found pkg-config {command_as_string!r} but it failed when run') - return None - except FileNotFoundError: - mlog.warning(f'We thought we found pkg-config {command_as_string!r} but now it\'s not there. How odd!') - return None - except PermissionError: - msg = f'Found pkg-config {command_as_string!r} but didn\'t have permissions to run it.' - if not env.machines.build.is_windows(): - msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' - mlog.warning(msg) - return None - return out.strip() + raw_libs = self.pkgconfig.libs(self.name, self.static, allow_system=False) + self.link_args, self.raw_link_args = self._search_libs(libs, raw_libs) def extract_field(self, la_file: str, fieldname: str) -> T.Optional[str]: with open(la_file, encoding='utf-8') as f: @@ -490,10 +566,12 @@ class PkgConfigDependency(ExternalDependency): def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None, default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> str: + pkgconfig_define: PkgConfigDefineType = None) -> str: if pkgconfig: try: - return self.get_pkgconfig_variable(pkgconfig, pkgconfig_define or [], default_value) + variable = self.pkgconfig.variable(self.name, pkgconfig, pkgconfig_define) + if variable is not None: + return variable except DependencyException: pass if default_value is not None: diff --git a/mesonbuild/dependencies/platform.py b/mesonbuild/dependencies/platform.py index 1fd1d78..87726b5 100644 --- a/mesonbuild/dependencies/platform.py +++ b/mesonbuild/dependencies/platform.py @@ -14,8 +14,10 @@ # This file contains the detection logic for external dependencies that are # platform-specific (generally speaking). +from __future__ import annotations from .base import DependencyTypeName, ExternalDependency, DependencyException +from .detect import packages from ..mesonlib import MesonException import typing as T @@ -57,3 +59,5 @@ class AppleFrameworks(ExternalDependency): @staticmethod def log_tried() -> str: return 'framework' + +packages['appleframeworks'] = AppleFrameworks diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py new file mode 100644 index 0000000..fe778af --- /dev/null +++ b/mesonbuild/dependencies/python.py @@ -0,0 +1,423 @@ +# Copyright 2022 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import functools, json, os, textwrap +from pathlib import Path +import typing as T + +from .. import mesonlib, mlog +from .base import process_method_kw, DependencyMethods, DependencyTypeName, ExternalDependency, SystemDependency +from .configtool import ConfigToolDependency +from .detect import packages +from .factory import DependencyFactory +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency +from ..environment import detect_cpu_family +from ..programs import ExternalProgram + +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from .factory import DependencyGenerator + from ..environment import Environment + from ..mesonlib import MachineChoice + + class PythonIntrospectionDict(TypedDict): + + install_paths: T.Dict[str, str] + is_pypy: bool + is_venv: bool + link_libpython: bool + sysconfig_paths: T.Dict[str, str] + paths: T.Dict[str, str] + platform: str + suffix: str + limited_api_suffix: str + variables: T.Dict[str, str] + version: str + + _Base = ExternalDependency +else: + _Base = object + + +class Pybind11ConfigToolDependency(ConfigToolDependency): + + tools = ['pybind11-config'] + + # any version of the tool is valid, since this is header-only + allow_default_for_cross = True + + # pybind11 in 2.10.4 added --version, sanity-check another flag unique to it + # in the meantime + skip_version = '--pkgconfigdir' + + def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any]): + super().__init__(name, environment, kwargs) + if not self.is_found: + return + self.compile_args = self.get_config_value(['--includes'], 'compile_args') + + +class BasicPythonExternalProgram(ExternalProgram): + def __init__(self, name: str, command: T.Optional[T.List[str]] = None, + ext_prog: T.Optional[ExternalProgram] = None): + if ext_prog is None: + super().__init__(name, command=command, silent=True) + else: + self.name = name + self.command = ext_prog.command + self.path = ext_prog.path + self.cached_version = None + + # We want strong key values, so we always populate this with bogus data. + # Otherwise to make the type checkers happy we'd have to do .get() for + # everycall, even though we know that the introspection data will be + # complete + self.info: 'PythonIntrospectionDict' = { + 'install_paths': {}, + 'is_pypy': False, + 'is_venv': False, + 'link_libpython': False, + 'sysconfig_paths': {}, + 'paths': {}, + 'platform': 'sentinel', + 'suffix': 'sentinel', + 'limited_api_suffix': 'sentinel', + 'variables': {}, + 'version': '0.0', + } + self.pure: bool = True + + def _check_version(self, version: str) -> bool: + if self.name == 'python2': + return mesonlib.version_compare(version, '< 3.0') + elif self.name == 'python3': + return mesonlib.version_compare(version, '>= 3.0') + return True + + def sanity(self) -> bool: + # Sanity check, we expect to have something that at least quacks in tune + + import importlib.resources + + with importlib.resources.path('mesonbuild.scripts', 'python_info.py') as f: + cmd = self.get_command() + [str(f)] + env = os.environ.copy() + env['SETUPTOOLS_USE_DISTUTILS'] = 'stdlib' + p, stdout, stderr = mesonlib.Popen_safe(cmd, env=env) + + try: + info = json.loads(stdout) + except json.JSONDecodeError: + info = None + mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode)) + mlog.debug('Program stdout:\n') + mlog.debug(stdout) + mlog.debug('Program stderr:\n') + mlog.debug(stderr) + + if info is not None and self._check_version(info['version']): + self.info = T.cast('PythonIntrospectionDict', info) + return True + else: + return False + + +class _PythonDependencyBase(_Base): + + def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool): + self.embed = embed + self.version: str = python_holder.info['version'] + self.platform = python_holder.info['platform'] + self.variables = python_holder.info['variables'] + self.paths = python_holder.info['paths'] + self.is_pypy = python_holder.info['is_pypy'] + # The "-embed" version of python.pc / python-config was introduced in 3.8, + # and distutils extension linking was changed to be considered a non embed + # usage. Before then, this dependency always uses the embed=True handling + # because that is the only one that exists. + # + # On macOS and some Linux distros (Debian) distutils doesn't link extensions + # against libpython, even on 3.7 and below. We call into distutils and + # mirror its behavior. See https://github.com/mesonbuild/meson/issues/4117 + self.link_libpython = python_holder.info['link_libpython'] or embed + self.info: T.Optional[T.Dict[str, str]] = None + if mesonlib.version_compare(self.version, '>= 3.0'): + self.major_version = 3 + else: + self.major_version = 2 + + +class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram', + libpc: bool = False): + if libpc: + mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC') + else: + mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths') + + PkgConfigDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) + + if libpc and not self.is_found: + mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation') + + # pkg-config files are usually accurate starting with python 3.8 + if not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'): + self.link_args = [] + + +class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'): + ExtraFrameworkDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) + + +class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'BasicPythonExternalProgram'): + SystemDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) + + # match pkg-config behavior + if self.link_libpython: + # link args + if mesonlib.is_windows(): + self.find_libpy_windows(environment, limited_api=False) + else: + self.find_libpy(environment) + else: + self.is_found = True + + # compile args + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), + self.paths.get('include'), + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] + + # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/ + # https://github.com/python/cpython/pull/100137 + if mesonlib.is_windows() and self.get_windows_python_arch().endswith('64') and mesonlib.version_compare(self.version, '<3.12'): + self.compile_args += ['-DMS_WIN64='] + + if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args): + self.is_found = False + + def find_libpy(self, environment: 'Environment') -> None: + if self.is_pypy: + if self.major_version == 3: + libname = 'pypy3-c' + else: + libname = 'pypy-c' + libdir = os.path.join(self.variables.get('base'), 'bin') + libdirs = [libdir] + else: + libname = f'python{self.version}' + if 'DEBUG_EXT' in self.variables: + libname += self.variables['DEBUG_EXT'] + if 'ABIFLAGS' in self.variables: + libname += self.variables['ABIFLAGS'] + libdirs = [] + + largs = self.clib_compiler.find_library(libname, environment, libdirs) + if largs is not None: + self.link_args = largs + self.is_found = True + + def get_windows_python_arch(self) -> T.Optional[str]: + if self.platform == 'mingw': + pycc = self.variables.get('CC') + if pycc.startswith('x86_64'): + return 'x86_64' + elif pycc.startswith(('i686', 'i386')): + return 'x86' + else: + mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug') + return None + elif self.platform == 'win32': + return 'x86' + elif self.platform in {'win64', 'win-amd64'}: + return 'x86_64' + elif self.platform in {'win-arm64'}: + return 'aarch64' + mlog.log(f'Unknown Windows Python platform {self.platform!r}') + return None + + def get_windows_link_args(self, limited_api: bool) -> T.Optional[T.List[str]]: + if self.platform.startswith('win'): + vernum = self.variables.get('py_version_nodot') + verdot = self.variables.get('py_version_short') + imp_lower = self.variables.get('implementation_lower', 'python') + if self.static: + libpath = Path('libs') / f'libpython{vernum}.a' + else: + comp = self.get_compiler() + if comp.id == "gcc": + if imp_lower == 'pypy' and verdot == '3.8': + # The naming changed between 3.8 and 3.9 + libpath = Path('libpypy3-c.dll') + elif imp_lower == 'pypy': + libpath = Path(f'libpypy{verdot}-c.dll') + else: + libpath = Path(f'python{vernum}.dll') + else: + if limited_api: + vernum = vernum[0] + libpath = Path('libs') / f'python{vernum}.lib' + # For a debug build, pyconfig.h may force linking with + # pythonX_d.lib (see meson#10776). This cannot be avoided + # and won't work unless we also have a debug build of + # Python itself (except with pybind11, which has an ugly + # hack to work around this) - so emit a warning to explain + # the cause of the expected link error. + buildtype = self.env.coredata.get_option(mesonlib.OptionKey('buildtype')) + assert isinstance(buildtype, str) + debug = self.env.coredata.get_option(mesonlib.OptionKey('debug')) + # `debugoptimized` buildtype may not set debug=True currently, see gh-11645 + is_debug_build = debug or buildtype == 'debug' + vscrt_debug = False + if mesonlib.OptionKey('b_vscrt') in self.env.coredata.options: + vscrt = self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value + if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: + vscrt_debug = True + if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): + mlog.warning(textwrap.dedent('''\ + Using a debug build type with MSVC or an MSVC-compatible compiler + when the Python interpreter is not also a debug build will almost + certainly result in a failed build. Prefer using a release build + type or a debug Python interpreter. + ''')) + # base_prefix to allow for virtualenvs. + lib = Path(self.variables.get('base_prefix')) / libpath + elif self.platform == 'mingw': + if self.static: + libname = self.variables.get('LIBRARY') + else: + libname = self.variables.get('LDLIBRARY') + lib = Path(self.variables.get('LIBDIR')) / libname + else: + raise mesonlib.MesonBugException( + 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.') + if not lib.exists(): + mlog.log('Could not find Python3 library {!r}'.format(str(lib))) + return None + return [str(lib)] + + def find_libpy_windows(self, env: 'Environment', limited_api: bool = False) -> None: + ''' + Find python3 libraries on Windows and also verify that the arch matches + what we are building for. + ''' + pyarch = self.get_windows_python_arch() + if pyarch is None: + self.is_found = False + return + arch = detect_cpu_family(env.coredata.compilers.host) + if arch != pyarch: + mlog.log('Need', mlog.bold(self.name), f'for {arch}, but found {pyarch}') + self.is_found = False + return + # This can fail if the library is not found + largs = self.get_windows_link_args(limited_api) + if largs is None: + self.is_found = False + return + self.link_args = largs + self.is_found = True + + @staticmethod + def log_tried() -> str: + return 'sysconfig' + +def python_factory(env: 'Environment', for_machine: 'MachineChoice', + kwargs: T.Dict[str, T.Any], + installation: T.Optional['BasicPythonExternalProgram'] = None) -> T.List['DependencyGenerator']: + # We can't use the factory_methods decorator here, as we need to pass the + # extra installation argument + methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs) + embed = kwargs.get('embed', False) + candidates: T.List['DependencyGenerator'] = [] + from_installation = installation is not None + # When not invoked through the python module, default installation. + if installation is None: + installation = BasicPythonExternalProgram('python3', mesonlib.python_command) + installation.sanity() + pkg_version = installation.info['variables'].get('LDVERSION') or installation.info['version'] + + if DependencyMethods.PKGCONFIG in methods: + if from_installation: + pkg_libdir = installation.info['variables'].get('LIBPC') + pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.info['version'], '>=3.8') else '' + pkg_name = f'python-{pkg_version}{pkg_embed}' + + # If python-X.Y.pc exists in LIBPC, we will try to use it + def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], + installation: 'BasicPythonExternalProgram') -> 'ExternalDependency': + if not pkg_libdir: + # there is no LIBPC, so we can't search in it + empty = ExternalDependency(DependencyTypeName('pkgconfig'), env, {}) + empty.name = 'python' + return empty + + old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) + old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) + os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir + try: + return PythonPkgConfigDependency(name, env, kwargs, installation, True) + finally: + def set_env(name: str, value: str) -> None: + if value is not None: + os.environ[name] = value + elif name in os.environ: + del os.environ[name] + set_env('PKG_CONFIG_LIBDIR', old_pkg_libdir) + set_env('PKG_CONFIG_PATH', old_pkg_path) + + candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) + # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, + # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. + if pkg_libdir is not None: + candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) + else: + candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) + + if DependencyMethods.SYSTEM in methods: + candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) + + if DependencyMethods.EXTRAFRAMEWORK in methods: + nkwargs = kwargs.copy() + if mesonlib.version_compare(pkg_version, '>= 3'): + # There is a python in /System/Library/Frameworks, but that's python 2.x, + # Python 3 will always be in /Library + nkwargs['paths'] = ['/Library/Frameworks'] + candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation)) + + return candidates + +packages['python3'] = python_factory + +packages['pybind11'] = pybind11_factory = DependencyFactory( + 'pybind11', + [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.CMAKE], + configtool_class=Pybind11ConfigToolDependency, +) diff --git a/mesonbuild/dependencies/qt.py b/mesonbuild/dependencies/qt.py index c6162ed..25cf610 100644 --- a/mesonbuild/dependencies/qt.py +++ b/mesonbuild/dependencies/qt.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Dependency finders for the Qt framework.""" @@ -23,6 +24,7 @@ import typing as T from .base import DependencyException, DependencyMethods from .configtool import ConfigToolDependency +from .detect import packages from .framework import ExtraFrameworkDependency from .pkgconfig import PkgConfigDependency from .factory import DependencyFactory @@ -51,7 +53,7 @@ def _qt_get_private_includes(mod_inc_dir: str, module: str, mod_version: str) -> private_dir = os.path.join(mod_inc_dir, mod_version) # fallback, let's try to find a directory with the latest version - if not os.path.exists(private_dir): + if os.path.isdir(mod_inc_dir) and not os.path.exists(private_dir): dirs = [filename for filename in os.listdir(mod_inc_dir) if os.path.isdir(os.path.join(mod_inc_dir, filename))] @@ -104,9 +106,10 @@ def _get_modules_lib_suffix(version: str, info: 'MachineInfo', is_debug: bool) - class QtExtraFrameworkDependency(ExtraFrameworkDependency): - def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None): + def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], qvars: T.Dict[str, str], language: T.Optional[str] = None): super().__init__(name, env, kwargs, language=language) self.mod_name = name[2:] + self.qt_extra_include_directory = qvars['QT_INSTALL_HEADERS'] def get_compile_args(self, with_private_headers: bool = False, qt_version: str = "0") -> T.List[str]: if self.found(): @@ -114,6 +117,8 @@ class QtExtraFrameworkDependency(ExtraFrameworkDependency): args = ['-I' + mod_inc_dir] if with_private_headers: args += ['-I' + dirname for dirname in _qt_get_private_includes(mod_inc_dir, self.mod_name, qt_version)] + if self.qt_extra_include_directory: + args += ['-I' + self.qt_extra_include_directory] return args return [] @@ -193,7 +198,7 @@ class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta) self.is_found = False return if self.private_headers: - qt_inc_dir = mod.get_pkgconfig_variable('includedir', [], None) + qt_inc_dir = mod.get_variable(pkgconfig='includedir') mod_private_dir = os.path.join(qt_inc_dir, 'Qt' + m) if not os.path.isdir(mod_private_dir): # At least some versions of homebrew don't seem to set this @@ -215,7 +220,7 @@ class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta) if arg == f'-l{debug_lib_name}' or arg.endswith(f'{debug_lib_name}.lib') or arg.endswith(f'{debug_lib_name}.a'): is_debug = True break - libdir = self.get_pkgconfig_variable('libdir', [], None) + libdir = self.get_variable(pkgconfig='libdir') if not self._link_with_qt_winmain(is_debug, libdir): self.is_found = False return @@ -223,7 +228,7 @@ class QtPkgConfigDependency(_QtBase, PkgConfigDependency, metaclass=abc.ABCMeta) self.bindir = self.get_pkgconfig_host_bins(self) if not self.bindir: # If exec_prefix is not defined, the pkg-config file is broken - prefix = self.get_pkgconfig_variable('exec_prefix', [], None) + prefix = self.get_variable(pkgconfig='exec_prefix') if prefix: self.bindir = os.path.join(prefix, 'bin') @@ -251,15 +256,15 @@ class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta): """Find Qt using Qmake as a config-tool.""" - tool_name = 'qmake' version_arg = '-v' def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]): _QtBase.__init__(self, name, kwargs) + self.tool_name = f'qmake{self.qtver}' self.tools = [f'qmake{self.qtver}', f'qmake-{self.name}', 'qmake'] # Add additional constraints that the Qt version is met, but preserve - # any version requrements the user has set as well. For example, if Qt5 + # any version requirements the user has set as well. For example, if Qt5 # is requested, add "">= 5, < 6", but if the user has ">= 5.6", don't # lose that. kwargs = kwargs.copy() @@ -324,7 +329,7 @@ class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta): self.compile_args.append('-I' + directory) libfiles = self.clib_compiler.find_library( self.qtpkgname + module + modules_lib_suffix, self.env, - mesonlib.listify(libdir)) # TODO: shouldn't be necissary + mesonlib.listify(libdir)) # TODO: shouldn't be necessary if libfiles: libfile = libfiles[0] else: @@ -345,6 +350,9 @@ class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta): return m.group(0).rstrip('.') return version + def get_variable_args(self, variable_name: str) -> T.List[str]: + return ['-query', f'{variable_name}'] + @abc.abstractmethod def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: pass @@ -360,7 +368,7 @@ class QmakeQtDependency(_QtBase, ConfigToolDependency, metaclass=abc.ABCMeta): for m in modules: fname = 'Qt' + m mlog.debug('Looking for qt framework ' + fname) - fwdep = QtExtraFrameworkDependency(fname, self.env, fw_kwargs, language=self.language) + fwdep = QtExtraFrameworkDependency(fname, self.env, fw_kwargs, qvars, language=self.language) if fwdep.found(): self.compile_args.append('-F' + libdir) self.compile_args += fwdep.get_compile_args(with_private_headers=self.private_headers, @@ -414,7 +422,7 @@ class Qt4PkgConfigDependency(QtPkgConfigDependency): applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease'] for application in applications: try: - return os.path.dirname(core.get_pkgconfig_variable(f'{application}_location', [], None)) + return os.path.dirname(core.get_variable(pkgconfig=f'{application}_location')) except mesonlib.MesonException: pass return None @@ -431,7 +439,7 @@ class Qt5PkgConfigDependency(QtPkgConfigDependency): @staticmethod def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: - return core.get_pkgconfig_variable('host_bins', [], None) + return core.get_variable(pkgconfig='host_bins') @staticmethod def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str: @@ -452,32 +460,32 @@ class Qt6PkgConfigDependency(Qt6WinMainMixin, QtPkgConfigDependency): @staticmethod def get_pkgconfig_host_bins(core: PkgConfigDependency) -> str: - return core.get_pkgconfig_variable('bindir', [], None) + return core.get_variable(pkgconfig='bindir') @staticmethod def get_pkgconfig_host_libexecs(core: PkgConfigDependency) -> str: # Qt6 pkg-config for Qt defines libexecdir from 6.3+ - return core.get_pkgconfig_variable('libexecdir', [], None) + return core.get_variable(pkgconfig='libexecdir') def get_private_includes(self, mod_inc_dir: str, module: str) -> T.List[str]: return _qt_get_private_includes(mod_inc_dir, module, self.version) -qt4_factory = DependencyFactory( +packages['qt4'] = qt4_factory = DependencyFactory( 'qt4', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], pkgconfig_class=Qt4PkgConfigDependency, configtool_class=Qt4ConfigToolDependency, ) -qt5_factory = DependencyFactory( +packages['qt5'] = qt5_factory = DependencyFactory( 'qt5', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], pkgconfig_class=Qt5PkgConfigDependency, configtool_class=Qt5ConfigToolDependency, ) -qt6_factory = DependencyFactory( +packages['qt6'] = qt6_factory = DependencyFactory( 'qt6', [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], pkgconfig_class=Qt6PkgConfigDependency, diff --git a/mesonbuild/dependencies/scalapack.py b/mesonbuild/dependencies/scalapack.py index 656978d..a8e20f4 100644 --- a/mesonbuild/dependencies/scalapack.py +++ b/mesonbuild/dependencies/scalapack.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from pathlib import Path import functools @@ -19,13 +20,14 @@ import typing as T from ..mesonlib import OptionKey from .base import DependencyMethods -from .base import DependencyException from .cmake import CMakeDependency +from .detect import packages from .pkgconfig import PkgConfigDependency from .factory import factory_methods if T.TYPE_CHECKING: - from ..environment import Environment, MachineChoice + from ..environment import Environment + from ..mesonlib import MachineChoice from .factory import DependencyGenerator @@ -51,6 +53,8 @@ def scalapack_factory(env: 'Environment', for_machine: 'MachineChoice', return candidates +packages['scalapack'] = scalapack_factory + class MKLPkgConfigDependency(PkgConfigDependency): @@ -139,17 +143,10 @@ class MKLPkgConfigDependency(PkgConfigDependency): self.link_args.insert(i + 1, '-lmkl_blacs_intelmpi_lp64') def _set_cargs(self) -> None: - env = None + allow_system = False if self.language == 'fortran': # gfortran doesn't appear to look in system paths for INCLUDE files, # so don't allow pkg-config to suppress -I flags for system paths - env = os.environ.copy() - env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' - ret, out, err = self._call_pkgbin([ - '--cflags', self.name, - '--define-variable=prefix=' + self.__mklroot.as_posix()], - env=env) - if ret != 0: - raise DependencyException('Could not generate cargs for %s:\n%s\n' % - (self.name, err)) - self.compile_args = self._convert_mingw_paths(self._split_args(out)) + allow_system = True + cflags = self.pkgconfig.cflags(self.name, allow_system, define_variable=(('prefix', self.__mklroot.as_posix()),)) + self.compile_args = self._convert_mingw_paths(cflags) diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py index 66b13ec..6de5534 100644 --- a/mesonbuild/dependencies/ui.py +++ b/mesonbuild/dependencies/ui.py @@ -14,12 +14,16 @@ # This file contains the detection logic for external dependencies that # are UI-related. +from __future__ import annotations + import os +import re import subprocess import typing as T from .. import mlog from .. import mesonlib +from ..compilers.compilers import CrossNoRunException from ..mesonlib import ( Popen_safe, extract_as_list, version_compare_many ) @@ -27,6 +31,7 @@ from ..environment import detect_cpu_family from .base import DependencyException, DependencyMethods, DependencyTypeName, SystemDependency from .configtool import ConfigToolDependency +from .detect import packages from .factory import DependencyFactory if T.TYPE_CHECKING: @@ -43,12 +48,20 @@ class GLDependencySystem(SystemDependency): self.link_args = ['-framework', 'OpenGL'] # FIXME: Detect version using self.clib_compiler return - if self.env.machines[self.for_machine].is_windows(): + elif self.env.machines[self.for_machine].is_windows(): self.is_found = True # FIXME: Use self.clib_compiler.find_library() self.link_args = ['-lopengl32'] # FIXME: Detect version using self.clib_compiler return + else: + links = self.clib_compiler.find_library('GL', environment, []) + has_header = self.clib_compiler.has_header('GL/gl.h', '', environment)[0] + if links and has_header: + self.is_found = True + self.link_args = links + elif links: + raise DependencyException('Found GL runtime library but no development header files') class GnuStepDependency(ConfigToolDependency): @@ -126,6 +139,8 @@ class GnuStepDependency(ConfigToolDependency): version = '1' return version +packages['gnustep'] = GnuStepDependency + class SDL2DependencyConfigTool(ConfigToolDependency): @@ -177,6 +192,7 @@ class WxDependency(ConfigToolDependency): raise DependencyException('wxwidgets module argument is not a string') return candidates +packages['wxwidgets'] = WxDependency class VulkanDependencySystem(SystemDependency): @@ -221,10 +237,6 @@ class VulkanDependencySystem(SystemDependency): self.compile_args.append('-I' + inc_path) self.link_args.append('-L' + lib_path) self.link_args.append('-l' + lib_name) - - # TODO: find a way to retrieve the version from the sdk? - # Usually it is a part of the path to it (but does not have to be) - return else: # simply try to guess it, usually works on linux libs = self.clib_compiler.find_library('vulkan', environment, []) @@ -232,21 +244,48 @@ class VulkanDependencySystem(SystemDependency): self.is_found = True for lib in libs: self.link_args.append(lib) - return -gl_factory = DependencyFactory( + if self.is_found: + get_version = '''\ +#include +#include + +int main() { + printf("%i.%i.%i", VK_VERSION_MAJOR(VK_HEADER_VERSION_COMPLETE), + VK_VERSION_MINOR(VK_HEADER_VERSION_COMPLETE), + VK_VERSION_PATCH(VK_HEADER_VERSION_COMPLETE)); + return 0; +} +''' + try: + run = self.clib_compiler.run(get_version, environment, extra_args=self.compile_args) + except CrossNoRunException: + run = None + if run and run.compiled and run.returncode == 0: + self.version = run.stdout + elif self.vulkan_sdk: + # fall back to heuristics: detect version number in path + # matches the default install path on Windows + match = re.search(rf'VulkanSDK{re.escape(os.path.sep)}([0-9]+(?:\.[0-9]+)+)', self.vulkan_sdk) + if match: + self.version = match.group(1) + else: + mlog.warning(f'Environment variable VULKAN_SDK={self.vulkan_sdk} is present, but Vulkan version could not be extracted.') + +packages['gl'] = gl_factory = DependencyFactory( 'gl', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], system_class=GLDependencySystem, ) -sdl2_factory = DependencyFactory( +packages['sdl2'] = sdl2_factory = DependencyFactory( 'sdl2', - [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK], + [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK, DependencyMethods.CMAKE], configtool_class=SDL2DependencyConfigTool, + cmake_name='SDL2', ) -vulkan_factory = DependencyFactory( +packages['vulkan'] = vulkan_factory = DependencyFactory( 'vulkan', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], system_class=VulkanDependencySystem, diff --git a/mesonbuild/depfile.py b/mesonbuild/depfile.py index 912a817..d346136 100644 --- a/mesonbuild/depfile.py +++ b/mesonbuild/depfile.py @@ -10,6 +10,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import typing as T diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py index 2069664..07f1229 100644 --- a/mesonbuild/envconfig.py +++ b/mesonbuild/envconfig.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from dataclasses import dataclass import subprocess @@ -27,7 +28,7 @@ from pathlib import Path # and cross file currently), and also assists with the reading environment # variables. # -# At this time there isn't an ironclad difference between this an other sources +# At this time there isn't an ironclad difference between this and other sources # of state like `coredata`. But one rough guide is much what is in `coredata` is # the *output* of the configuration process: the final decisions after tests. # This, on the other hand has *inputs*. The config files are parsed, but @@ -67,6 +68,7 @@ known_cpu_families = ( 'sh4', 'sparc', 'sparc64', + 'sw_64', 'wasm32', 'wasm64', 'x86', @@ -85,6 +87,7 @@ CPU_FAMILIES_64_BIT = [ 'riscv64', 's390x', 'sparc64', + 'sw_64', 'wasm64', 'x86_64', ] @@ -132,7 +135,6 @@ ENV_VAR_TOOL_MAP: T.Mapping[str, str] = { # Other tools 'cmake': 'CMAKE', 'qmake': 'QMAKE', - 'pkgconfig': 'PKG_CONFIG', 'pkg-config': 'PKG_CONFIG', 'make': 'MAKE', 'vapigen': 'VAPIGEN', @@ -160,13 +162,13 @@ class Properties: self, properties: T.Optional[T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]] = None, ): - self.properties = properties or {} # type: T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]] + self.properties = properties or {} def has_stdlib(self, language: str) -> bool: return language + '_stdlib' in self.properties # Some of get_stdlib, get_root, get_sys_root are wider than is actually - # true, but without heterogenious dict annotations it's not practical to + # true, but without heterogeneous dict annotations it's not practical to # narrow them def get_stdlib(self, language: str) -> T.Union[str, T.List[str]]: stdlib = self.properties[language + '_stdlib'] @@ -236,6 +238,12 @@ class Properties: value = T.cast('T.Optional[str]', self.properties.get('java_home')) return Path(value) if value else None + def get_bindgen_clang_args(self) -> T.List[str]: + value = mesonlib.listify(self.properties.get('bindgen_clang_arguments', [])) + if not all(isinstance(v, str) for v in value): + raise EnvironmentException('bindgen_clang_arguments must be a string or an array of strings') + return T.cast('T.List[str]', value) + def __eq__(self, other: object) -> bool: if isinstance(other, type(self)): return self.properties == other.properties @@ -259,6 +267,8 @@ class MachineInfo(HoldableObject): cpu_family: str cpu: str endian: str + kernel: T.Optional[str] + subsystem: T.Optional[str] def __post_init__(self) -> None: self.is_64_bit: bool = self.cpu_family in CPU_FAMILIES_64_BIT @@ -282,7 +292,11 @@ class MachineInfo(HoldableObject): if endian not in ('little', 'big'): mlog.warning(f'Unknown endian {endian}') - return cls(literal['system'], cpu_family, literal['cpu'], endian) + system = literal['system'] + kernel = literal.get('kernel', None) + subsystem = literal.get('subsystem', None) + + return cls(system, cpu_family, literal['cpu'], endian, kernel, subsystem) def is_windows(self) -> bool: """ @@ -350,6 +364,12 @@ class MachineInfo(HoldableObject): """ return self.system == 'gnu' + def is_aix(self) -> bool: + """ + Machine is aix? + """ + return self.system == 'aix' + def is_irix(self) -> bool: """Machine is IRIX?""" return self.system.startswith('irix') @@ -385,6 +405,20 @@ class BinaryTable: raise mesonlib.MesonException( f'Invalid type {command!r} for entry {name!r} in cross file') self.binaries[name] = mesonlib.listify(command) + if 'pkgconfig' in self.binaries: + if 'pkg-config' not in self.binaries: + mlog.deprecation('"pkgconfig" entry is deprecated and should be replaced by "pkg-config"', fatal=False) + self.binaries['pkg-config'] = self.binaries['pkgconfig'] + elif self.binaries['pkgconfig'] != self.binaries['pkg-config']: + raise mesonlib.MesonException('Mismatched pkgconfig and pkg-config binaries in the machine file.') + else: + # Both are defined with the same value, this is allowed + # for backward compatibility. + # FIXME: We should still print deprecation warning if the + # project targets Meson >= 1.3.0, but we have no way to know + # that here. + pass + del self.binaries['pkgconfig'] @staticmethod def detect_ccache() -> T.List[str]: @@ -441,7 +475,7 @@ class BinaryTable: class CMakeVariables: def __init__(self, variables: T.Optional[T.Dict[str, T.Any]] = None) -> None: variables = variables or {} - self.variables = {} # type: T.Dict[str, T.List[str]] + self.variables: T.Dict[str, T.List[str]] = {} for key, value in variables.items(): value = mesonlib.listify(value) diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index 2e2a228..2ba2054 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import itertools import os, platform, re, sys, shutil @@ -32,7 +33,6 @@ from .envconfig import ( ) from . import compilers from .compilers import ( - Compiler, is_assembly, is_header, is_library, @@ -48,11 +48,13 @@ if T.TYPE_CHECKING: import argparse from configparser import ConfigParser + from .compilers import Compiler from .wrap.wrap import Resolver -build_filename = 'meson.build' + CompilersDict = T.Dict[str, Compiler] + -CompilersDict = T.Dict[str, Compiler] +build_filename = 'meson.build' def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T.Optional[str]: @@ -80,7 +82,7 @@ def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T return value -def detect_gcovr(min_version='3.3', log=False): +def detect_gcovr(min_version: str = '3.3', log: bool = False): gcovr_exe = 'gcovr' try: p, found = Popen_safe([gcovr_exe, '--version'])[0:2] @@ -94,6 +96,20 @@ def detect_gcovr(min_version='3.3', log=False): return gcovr_exe, found return None, None +def detect_lcov(log: bool = False): + lcov_exe = 'lcov' + try: + p, found = Popen_safe([lcov_exe, '--version'])[0:2] + except (FileNotFoundError, PermissionError): + # Doesn't exist in PATH or isn't executable + return None, None + found = search_version(found) + if p.returncode == 0 and found: + if log: + mlog.log('Found lcov-{} at {}'.format(found, quote_arg(shutil.which(lcov_exe)))) + return lcov_exe, found + return None, None + def detect_llvm_cov(): tools = get_llvm_tool_names('llvm-cov') for tool in tools: @@ -101,20 +117,18 @@ def detect_llvm_cov(): return tool return None -def find_coverage_tools() -> T.Tuple[T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str]]: +def find_coverage_tools() -> T.Tuple[T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str]]: gcovr_exe, gcovr_version = detect_gcovr() llvm_cov_exe = detect_llvm_cov() - lcov_exe = 'lcov' + lcov_exe, lcov_version = detect_lcov() genhtml_exe = 'genhtml' - if not mesonlib.exe_exists([lcov_exe, '--version']): - lcov_exe = None if not mesonlib.exe_exists([genhtml_exe, '--version']): genhtml_exe = None - return gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe + return gcovr_exe, gcovr_version, lcov_exe, lcov_version, genhtml_exe, llvm_cov_exe def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.List[str]: r = detect_ninja_command_and_version(version, log) @@ -155,6 +169,9 @@ def get_llvm_tool_names(tool: str) -> T.List[str]: # unless it becomes a stable release. suffixes = [ '', # base (no suffix) + '-17', '17', + '-16', '16', + '-15', '15', '-14', '14', '-13', '13', '-12', '12', @@ -174,7 +191,7 @@ def get_llvm_tool_names(tool: str) -> T.List[str]: '-15', # Debian development snapshot '-devel', # FreeBSD development snapshot ] - names = [] + names: T.List[str] = [] for suffix in suffixes: names.append(tool + suffix) return names @@ -193,15 +210,16 @@ def detect_scanbuild() -> T.List[str]: Return: a single-element list of the found scan-build binary ready to be passed to Popen() """ - exelist = [] + exelist: T.List[str] = [] if 'SCANBUILD' in os.environ: exelist = split_args(os.environ['SCANBUILD']) else: tools = get_llvm_tool_names('scan-build') for tool in tools: - if shutil.which(tool) is not None: - exelist = [shutil.which(tool)] + which = shutil.which(tool) + if which is not None: + exelist = [which] break if exelist: @@ -266,7 +284,7 @@ def detect_windows_arch(compilers: CompilersDict) -> str: return 'x86' return os_arch -def any_compiler_has_define(compilers: CompilersDict, define): +def any_compiler_has_define(compilers: CompilersDict, define: str) -> bool: for c in compilers.values(): try: if c.has_builtin_define(define): @@ -289,7 +307,6 @@ def detect_cpu_family(compilers: CompilersDict) -> str: trial = platform.processor().lower() else: trial = platform.machine().lower() - mlog.debug(f'detecting CPU family based on trial={trial!r}') if trial.startswith('i') and trial.endswith('86'): trial = 'x86' elif trial == 'bepc': @@ -336,6 +353,11 @@ def detect_cpu_family(compilers: CompilersDict) -> str: # AIX always returns powerpc, check here for 64-bit if any_compiler_has_define(compilers, '__64BIT__'): trial = 'ppc64' + # MIPS64 is able to run MIPS32 code natively, so there is a chance that + # such mixture mentioned above exists. + elif trial == 'mips64': + if compilers and not any_compiler_has_define(compilers, '__mips64'): + trial = 'mips' if trial not in known_cpu_families: mlog.warning(f'Unknown CPU family {trial!r}, please report this at ' @@ -374,7 +396,10 @@ def detect_cpu(compilers: CompilersDict) -> str: if '64' not in trial: trial = 'mips' else: - trial = 'mips64' + if compilers and not any_compiler_has_define(compilers, '__mips64'): + trial = 'mips' + else: + trial = 'mips64' elif trial == 'ppc': # AIX always returns powerpc, check here for 64-bit if any_compiler_has_define(compilers, '__64BIT__'): @@ -384,6 +409,43 @@ def detect_cpu(compilers: CompilersDict) -> str: # detect_cpu_family() above. return trial +KERNEL_MAPPINGS: T.Mapping[str, str] = {'freebsd': 'freebsd', + 'openbsd': 'openbsd', + 'netbsd': 'netbsd', + 'windows': 'nt', + 'android': 'linux', + 'linux': 'linux', + 'cygwin': 'nt', + 'darwin': 'xnu', + 'dragonfly': 'dragonfly', + 'haiku': 'haiku', + } + +def detect_kernel(system: str) -> T.Optional[str]: + if system == 'sunos': + # Solaris 5.10 uname doesn't support the -o switch, and illumos started + # with version 5.11 so shortcut the logic to report 'solaris' in such + # cases where the version is 5.10 or below. + if mesonlib.version_compare(platform.uname().release, '<=5.10'): + return 'solaris' + # This needs to be /usr/bin/uname because gnu-uname could be installed and + # won't provide the necessary information + p, out, _ = Popen_safe(['/usr/bin/uname', '-o']) + if p.returncode != 0: + raise MesonException('Failed to run "/usr/bin/uname -o"') + out = out.lower().strip() + if out not in {'illumos', 'solaris'}: + mlog.warning(f'Got an unexpected value for kernel on a SunOS derived platform, expcted either "illumos" or "solaris", but got "{out}".' + "Please open a Meson issue with the OS you're running and the value detected for your kernel.") + return None + return out + return KERNEL_MAPPINGS.get(system, None) + +def detect_subsystem(system: str) -> T.Optional[str]: + if system == 'darwin': + return 'macos' + return system + def detect_system() -> str: if sys.platform == 'cygwin': return 'cygwin' @@ -400,11 +462,14 @@ def detect_machine_info(compilers: T.Optional[CompilersDict] = None) -> MachineI underlying ''detect_*'' method can be called to explicitly use the partial information. """ + system = detect_system() return MachineInfo( - detect_system(), + system, detect_cpu_family(compilers) if compilers is not None else None, detect_cpu(compilers) if compilers is not None else None, - sys.byteorder) + sys.byteorder, + detect_kernel(system), + detect_subsystem(system)) # TODO make this compare two `MachineInfo`s purely. How important is the # `detect_cpu_family({})` distinction? It is the one impediment to that. @@ -422,6 +487,7 @@ def machine_info_can_run(machine_info: MachineInfo): return \ (machine_info.cpu_family == true_build_cpu_family) or \ ((true_build_cpu_family == 'x86_64') and (machine_info.cpu_family == 'x86')) or \ + ((true_build_cpu_family == 'mips64') and (machine_info.cpu_family == 'mips')) or \ ((true_build_cpu_family == 'aarch64') and (machine_info.cpu_family == 'arm')) class Environment: @@ -429,7 +495,7 @@ class Environment: log_dir = 'meson-logs' info_dir = 'meson-info' - def __init__(self, source_dir: T.Optional[str], build_dir: T.Optional[str], options: 'argparse.Namespace') -> None: + def __init__(self, source_dir: str, build_dir: str, options: 'argparse.Namespace') -> None: self.source_dir = source_dir self.build_dir = build_dir # Do not try to create build directories when build_dir is none. @@ -442,7 +508,7 @@ class Environment: os.makedirs(self.log_dir, exist_ok=True) os.makedirs(self.info_dir, exist_ok=True) try: - self.coredata = coredata.load(self.get_build_dir()) # type: coredata.CoreData + self.coredata: coredata.CoreData = coredata.load(self.get_build_dir(), suggest_reconfigure=False) self.first_invocation = False except FileNotFoundError: self.create_new_coredata(options) @@ -455,12 +521,12 @@ class Environment: # If we stored previous command line options, we can recover from # a broken/outdated coredata. if os.path.isfile(coredata.get_cmd_line_file(self.build_dir)): - mlog.warning('Regenerating configuration from scratch.') + mlog.warning('Regenerating configuration from scratch.', fatal=False) mlog.log('Reason:', mlog.red(str(e))) coredata.read_cmd_line_file(self.build_dir, options) self.create_new_coredata(options) else: - raise e + raise MesonException(f'{str(e)} Try regenerating using "meson setup --wipe".') else: # Just create a fresh coredata in this case self.scratch_dir = '' @@ -475,13 +541,13 @@ class Environment: # Similar to coredata.compilers, but lower level in that there is no # meta data, only names/paths. - binaries = PerMachineDefaultable() # type: PerMachineDefaultable[BinaryTable] + binaries: PerMachineDefaultable[BinaryTable] = PerMachineDefaultable() # Misc other properties about each machine. - properties = PerMachineDefaultable() # type: PerMachineDefaultable[Properties] + properties: PerMachineDefaultable[Properties] = PerMachineDefaultable() # CMake toolchain variables - cmakevars = PerMachineDefaultable() # type: PerMachineDefaultable[CMakeVariables] + cmakevars: PerMachineDefaultable[CMakeVariables] = PerMachineDefaultable() ## Setup build machine defaults @@ -502,7 +568,7 @@ class Environment: ## Read in native file(s) to override build machine configuration if self.coredata.config_files is not None: - config = coredata.parse_machine_files(self.coredata.config_files) + config = coredata.parse_machine_files(self.coredata.config_files, self.source_dir) binaries.build = BinaryTable(config.get('binaries', {})) properties.build = Properties(config.get('properties', {})) cmakevars.build = CMakeVariables(config.get('cmake', {})) @@ -513,7 +579,7 @@ class Environment: ## Read in cross file(s) to override host machine configuration if self.coredata.cross_files: - config = coredata.parse_machine_files(self.coredata.cross_files) + config = coredata.parse_machine_files(self.coredata.cross_files, self.source_dir) properties.host = Properties(config.get('properties', {})) binaries.host = BinaryTable(config.get('binaries', {})) cmakevars.host = CMakeVariables(config.get('cmake', {})) @@ -551,7 +617,8 @@ class Environment: if bt in self.options and (db in self.options or op in self.options): mlog.warning('Recommend using either -Dbuildtype or -Doptimization + -Ddebug. ' 'Using both is redundant since they override each other. ' - 'See: https://mesonbuild.com/Builtin-options.html#build-type-options') + 'See: https://mesonbuild.com/Builtin-options.html#build-type-options', + fatal=False) exe_wrapper = self.lookup_binary_entry(MachineChoice.HOST, 'exe_wrapper') if exe_wrapper is not None: @@ -670,7 +737,7 @@ class Environment: # time) until we're instantiating that `Compiler` # object. This is required so that passing # `-Dc_args=` on the command line and `$CFLAGS` - # have subtely different behavior. `$CFLAGS` will be + # have subtly different behavior. `$CFLAGS` will be # added to the linker command line if the compiler # acts as a linker driver, `-Dc_args` will not. # @@ -698,7 +765,10 @@ class Environment: for (name, evar), for_machine in itertools.product(opts, MachineChoice): p_env = _get_env_var(for_machine, self.is_cross_build(), evar) if p_env is not None: - self.binaries[for_machine].binaries.setdefault(name, mesonlib.split_args(p_env)) + if os.path.exists(p_env): + self.binaries[for_machine].binaries.setdefault(name, [p_env]) + else: + self.binaries[for_machine].binaries.setdefault(name, mesonlib.split_args(p_env)) def _set_default_properties_from_env(self) -> None: """Properties which can also be set from the environment.""" @@ -766,7 +836,7 @@ class Environment: return is_object(fname) @lru_cache(maxsize=None) - def is_library(self, fname): + def is_library(self, fname: mesonlib.FileOrString): return is_library(fname) def lookup_binary_entry(self, for_machine: MachineChoice, name: str) -> T.Optional[T.List[str]]: @@ -826,7 +896,7 @@ class Environment: def get_datadir(self) -> str: return self.coredata.get_option(OptionKey('datadir')) - def get_compiler_system_dirs(self, for_machine: MachineChoice): + def get_compiler_system_lib_dirs(self, for_machine: MachineChoice) -> T.List[str]: for comp in self.coredata.compilers[for_machine].values(): if comp.id == 'clang': index = 1 @@ -845,6 +915,18 @@ class Environment: out = out.split('\n')[index].lstrip('libraries: =').split(':') return [os.path.normpath(p) for p in out] + def get_compiler_system_include_dirs(self, for_machine: MachineChoice) -> T.List[str]: + for comp in self.coredata.compilers[for_machine].values(): + if comp.id == 'clang': + break + elif comp.id == 'gcc': + break + else: + # This option is only supported by gcc and clang. If we don't get a + # GCC or Clang compiler return and empty list. + return [] + return comp.get_default_include_dirs() + def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST): value = self.properties[for_machine].get('needs_exe_wrapper', None) if value is not None: diff --git a/mesonbuild/interpreter/compiler.py b/mesonbuild/interpreter/compiler.py index 8b6efd2..5528abe 100644 --- a/mesonbuild/interpreter/compiler.py +++ b/mesonbuild/interpreter/compiler.py @@ -1,10 +1,13 @@ -# SPDX-Licnese-Identifier: Apache-2.0 +# SPDX-License-Identifier: Apache-2.0 # Copyright 2012-2021 The Meson development team # Copyright © 2021 Intel Corporation +from __future__ import annotations +import collections import enum import functools import os +import itertools import typing as T from .. import build @@ -27,6 +30,8 @@ if T.TYPE_CHECKING: from ..compilers import Compiler, RunResult from ..interpreterbase import TYPE_var, TYPE_kwargs from .kwargs import ExtractRequired, ExtractSearchDirs + from .interpreter.interpreter import SourceOutputs + from ..mlog import TV_LoggableList from typing_extensions import TypedDict, Literal @@ -40,23 +45,23 @@ if T.TYPE_CHECKING: args: T.List[str] dependencies: T.List[dependencies.Dependency] - class CompileKW(TypedDict): - - name: str + class BaseCompileKW(TypedDict): no_builtin_args: bool include_directories: T.List[build.IncludeDirs] args: T.List[str] + + class CompileKW(BaseCompileKW): + + name: str dependencies: T.List[dependencies.Dependency] + werror: bool - class CommonKW(TypedDict): + class CommonKW(BaseCompileKW): prefix: str - no_builtin_args: bool - include_directories: T.List[build.IncludeDirs] - args: T.List[str] dependencies: T.List[dependencies.Dependency] - class CompupteIntKW(CommonKW): + class ComputeIntKW(CommonKW): guess: T.Optional[int] high: T.Optional[int] @@ -65,6 +70,12 @@ if T.TYPE_CHECKING: class HeaderKW(CommonKW, ExtractRequired): pass + class HasKW(CommonKW, ExtractRequired): + pass + + class HasArgumentKW(ExtractRequired): + pass + class FindLibraryKW(ExtractRequired, ExtractSearchDirs): disabler: bool @@ -84,6 +95,7 @@ if T.TYPE_CHECKING: output: str compile_args: T.List[str] include_directories: T.List[build.IncludeDirs] + dependencies: T.List[dependencies.Dependency] class _TestMode(enum.Enum): @@ -151,17 +163,22 @@ _PREFIX_KW: KwargInfo[str] = KwargInfo( _NO_BUILTIN_ARGS_KW = KwargInfo('no_builtin_args', bool, default=False) _NAME_KW = KwargInfo('name', str, default='') +_WERROR_KW = KwargInfo('werror', bool, default=False, since='1.3.0') # Many of the compiler methods take this kwarg signature exactly, this allows # simplifying the `typed_kwargs` calls _COMMON_KWS: T.List[KwargInfo] = [_ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _PREFIX_KW, _NO_BUILTIN_ARGS_KW] # Common methods of compiles, links, runs, and similar -_COMPILES_KWS: T.List[KwargInfo] = [_NAME_KW, _ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _NO_BUILTIN_ARGS_KW] +_COMPILES_KWS: T.List[KwargInfo] = [_NAME_KW, _ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _NO_BUILTIN_ARGS_KW, + _WERROR_KW] _HEADER_KWS: T.List[KwargInfo] = [REQUIRED_KW.evolve(since='0.50.0', default=False), *_COMMON_KWS] +_HAS_REQUIRED_KW = REQUIRED_KW.evolve(since='1.3.0', default=False) class CompilerHolder(ObjectHolder['Compiler']): + preprocess_uid: T.Dict[str, itertools.count] = collections.defaultdict(itertools.count) + def __init__(self, compiler: 'Compiler', interpreter: 'Interpreter'): super().__init__(compiler, interpreter) self.environment = self.env @@ -172,6 +189,7 @@ class CompilerHolder(ObjectHolder['Compiler']): 'compute_int': self.compute_int_method, 'sizeof': self.sizeof_method, 'get_define': self.get_define_method, + 'has_define': self.has_define_method, 'check_header': self.check_header_method, 'has_header': self.has_header_method, 'has_header_symbol': self.has_header_symbol_method, @@ -236,20 +254,20 @@ class CompilerHolder(ObjectHolder['Compiler']): def cmd_array_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]: return self.compiler.exelist - def _determine_args(self, nobuiltins: bool, - incdirs: T.List[build.IncludeDirs], - extra_args: T.List[str], + def _determine_args(self, kwargs: BaseCompileKW, mode: CompileCheckMode = CompileCheckMode.LINK) -> T.List[str]: args: T.List[str] = [] - for i in incdirs: - for idir in i.to_string_list(self.environment.get_source_dir()): + for i in kwargs['include_directories']: + for idir in i.to_string_list(self.environment.get_source_dir(), self.environment.get_build_dir()): args.extend(self.compiler.get_include_args(idir, False)) - if not nobuiltins: - opts = self.environment.coredata.options + if not kwargs['no_builtin_args']: + opts = coredata.OptionsView(self.environment.coredata.options, self.subproject) args += self.compiler.get_option_compile_args(opts) if mode is CompileCheckMode.LINK: args.extend(self.compiler.get_option_link_args(opts)) - args.extend(extra_args) + if kwargs.get('werror', False): + args.extend(self.compiler.get_werror_args()) + args.extend(kwargs['args']) return args def _determine_dependencies(self, deps: T.List['dependencies.Dependency'], compile_only: bool = False, endl: str = ':') -> T.Tuple[T.List['dependencies.Dependency'], str]: @@ -266,10 +284,12 @@ class CompilerHolder(ObjectHolder['Compiler']): def alignment_method(self, args: T.Tuple[str], kwargs: 'AlignmentKw') -> int: typename = args[0] deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross) - result = self.compiler.alignment(typename, kwargs['prefix'], self.environment, - extra_args=kwargs['args'], - dependencies=deps) - mlog.log('Checking for alignment of', mlog.bold(typename, True), msg, result) + result, cached = self.compiler.alignment(typename, kwargs['prefix'], self.environment, + extra_args=kwargs['args'], + dependencies=deps) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log('Checking for alignment of', + mlog.bold(typename, True), msg, mlog.bold(str(result)), cached_msg) return result @typed_pos_args('compiler.run', (str, mesonlib.File)) @@ -281,7 +301,7 @@ class CompilerHolder(ObjectHolder['Compiler']): code = mesonlib.File.from_absolute_file( code.rel_to_builddir(self.environment.source_dir)) testname = kwargs['name'] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False, endl=None) result = self.compiler.run(code, self.environment, extra_args=extra_args, dependencies=deps) @@ -316,17 +336,23 @@ class CompilerHolder(ObjectHolder['Compiler']): return self.compiler.symbols_have_underscore_prefix(self.environment) @typed_pos_args('compiler.has_member', str, str) - @typed_kwargs('compiler.has_member', *_COMMON_KWS) - def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool: + @typed_kwargs('compiler.has_member', _HAS_REQUIRED_KW, *_COMMON_KWS) + def has_member_method(self, args: T.Tuple[str, str], kwargs: 'HasKW') -> bool: typename, membername = args - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + mlog.log('Type', mlog.bold(typename, True), 'has member', mlog.bold(membername, True), 'skipped: feature', mlog.bold(feature), 'disabled') + return False + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) had, cached = self.compiler.has_members(typename, [membername], kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) cached_msg = mlog.blue('(cached)') if cached else '' - if had: + if required and not had: + raise InterpreterException(f'{self.compiler.get_display_language()} member {membername!r} of type {typename!r} not usable') + elif had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') @@ -335,36 +361,49 @@ class CompilerHolder(ObjectHolder['Compiler']): return had @typed_pos_args('compiler.has_members', str, varargs=str, min_varargs=1) - @typed_kwargs('compiler.has_members', *_COMMON_KWS) - def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'CommonKW') -> bool: + @typed_kwargs('compiler.has_members', _HAS_REQUIRED_KW, *_COMMON_KWS) + def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'HasKW') -> bool: typename, membernames = args - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + members = mlog.bold(', '.join([f'"{m}"' for m in membernames])) + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + mlog.log('Type', mlog.bold(typename, True), 'has members', members, 'skipped: feature', mlog.bold(feature), 'disabled') + return False + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) had, cached = self.compiler.has_members(typename, membernames, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) cached_msg = mlog.blue('(cached)') if cached else '' - if had: + if required and not had: + # print members as array: ['member1', 'member2'] + raise InterpreterException(f'{self.compiler.get_display_language()} members {membernames!r} of type {typename!r} not usable') + elif had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') - members = mlog.bold(', '.join([f'"{m}"' for m in membernames])) mlog.log('Checking whether type', mlog.bold(typename, True), 'has members', members, msg, hadtxt, cached_msg) return had @typed_pos_args('compiler.has_function', str) - @typed_kwargs('compiler.has_function', *_COMMON_KWS) - def has_function_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool: + @typed_kwargs('compiler.has_function', _HAS_REQUIRED_KW, *_COMMON_KWS) + def has_function_method(self, args: T.Tuple[str], kwargs: 'HasKW') -> bool: funcname = args[0] - extra_args = self._determine_args(kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + mlog.log('Has function', mlog.bold(funcname, True), 'skipped: feature', mlog.bold(feature), 'disabled') + return False + extra_args = self._determine_args(kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False) had, cached = self.compiler.has_function(funcname, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) cached_msg = mlog.blue('(cached)') if cached else '' - if had: + if required and not had: + raise InterpreterException(f'{self.compiler.get_display_language()} function {funcname!r} not usable') + elif had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') @@ -372,15 +411,21 @@ class CompilerHolder(ObjectHolder['Compiler']): return had @typed_pos_args('compiler.has_type', str) - @typed_kwargs('compiler.has_type', *_COMMON_KWS) - def has_type_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool: + @typed_kwargs('compiler.has_type', _HAS_REQUIRED_KW, *_COMMON_KWS) + def has_type_method(self, args: T.Tuple[str], kwargs: 'HasKW') -> bool: typename = args[0] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + mlog.log('Has type', mlog.bold(typename, True), 'skipped: feature', mlog.bold(feature), 'disabled') + return False + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) had, cached = self.compiler.has_type(typename, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) cached_msg = mlog.blue('(cached)') if cached else '' - if had: + if required and not had: + raise InterpreterException(f'{self.compiler.get_display_language()} type {typename!r} not usable') + elif had: hadtxt = mlog.green('YES') else: hadtxt = mlog.red('NO') @@ -396,9 +441,9 @@ class CompilerHolder(ObjectHolder['Compiler']): KwargInfo('guess', (int, NoneType)), *_COMMON_KWS, ) - def compute_int_method(self, args: T.Tuple[str], kwargs: 'CompupteIntKW') -> int: + def compute_int_method(self, args: T.Tuple[str], kwargs: 'ComputeIntKW') -> int: expression = args[0] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross) res = self.compiler.compute_int(expression, kwargs['low'], kwargs['high'], kwargs['guess'], kwargs['prefix'], @@ -411,11 +456,13 @@ class CompilerHolder(ObjectHolder['Compiler']): @typed_kwargs('compiler.sizeof', *_COMMON_KWS) def sizeof_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> int: element = args[0] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=self.compiler.is_cross) - esize = self.compiler.sizeof(element, kwargs['prefix'], self.environment, - extra_args=extra_args, dependencies=deps) - mlog.log('Checking for size of', mlog.bold(element, True), msg, esize) + esize, cached = self.compiler.sizeof(element, kwargs['prefix'], self.environment, + extra_args=extra_args, dependencies=deps) + cached_msg = mlog.blue('(cached)') if cached else '' + mlog.log('Checking for size of', + mlog.bold(element, True), msg, mlog.bold(str(esize)), cached_msg) return esize @FeatureNew('compiler.get_define', '0.40.0') @@ -423,25 +470,45 @@ class CompilerHolder(ObjectHolder['Compiler']): @typed_kwargs('compiler.get_define', *_COMMON_KWS) def get_define_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> str: element = args[0] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) value, cached = self.compiler.get_define(element, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) cached_msg = mlog.blue('(cached)') if cached else '' - mlog.log('Fetching value of define', mlog.bold(element, True), msg, value, cached_msg) - return value + value_msg = '(undefined)' if value is None else value + mlog.log('Fetching value of define', mlog.bold(element, True), msg, value_msg, cached_msg) + return value if value is not None else '' + + @FeatureNew('compiler.has_define', '1.3.0') + @typed_pos_args('compiler.has_define', str) + @typed_kwargs('compiler.has_define', *_COMMON_KWS) + def has_define_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool: + define_name = args[0] + extra_args = functools.partial(self._determine_args, kwargs) + deps, msg = self._determine_dependencies(kwargs['dependencies'], endl=None) + value, cached = self.compiler.get_define(define_name, kwargs['prefix'], self.environment, + extra_args=extra_args, + dependencies=deps) + cached_msg = mlog.blue('(cached)') if cached else '' + h = mlog.green('YES') if value is not None else mlog.red('NO') + mlog.log('Checking if define', mlog.bold(define_name, True), msg, 'exists:', h, cached_msg) + + return value is not None @typed_pos_args('compiler.compiles', (str, mesonlib.File)) @typed_kwargs('compiler.compiles', *_COMPILES_KWS) def compiles_method(self, args: T.Tuple['mesonlib.FileOrString'], kwargs: 'CompileKW') -> bool: code = args[0] if isinstance(code, mesonlib.File): + if code.is_built: + FeatureNew.single_use('compiler.compiles with file created at setup time', '1.2.0', self.subproject, + 'It was broken and either errored or returned false.', self.current_node) self.interpreter.add_build_def_file(code) code = mesonlib.File.from_absolute_file( - code.rel_to_builddir(self.environment.source_dir)) + code.absolute_path(self.environment.source_dir, self.environment.build_dir)) testname = kwargs['name'] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], endl=None) result, cached = self.compiler.compiles(code, self.environment, extra_args=extra_args, @@ -461,9 +528,12 @@ class CompilerHolder(ObjectHolder['Compiler']): code = args[0] compiler = None if isinstance(code, mesonlib.File): + if code.is_built: + FeatureNew.single_use('compiler.links with file created at setup time', '1.2.0', self.subproject, + 'It was broken and either errored or returned false.', self.current_node) self.interpreter.add_build_def_file(code) code = mesonlib.File.from_absolute_file( - code.rel_to_builddir(self.environment.source_dir)) + code.absolute_path(self.environment.source_dir, self.environment.build_dir)) suffix = code.suffix if suffix not in self.compiler.file_suffixes: for_machine = self.compiler.for_machine @@ -477,7 +547,7 @@ class CompilerHolder(ObjectHolder['Compiler']): compiler = clist[SUFFIX_TO_LANG[suffix]] testname = kwargs['name'] - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False) result, cached = self.compiler.links(code, self.environment, compiler=compiler, @@ -501,7 +571,7 @@ class CompilerHolder(ObjectHolder['Compiler']): if disabled: mlog.log('Check usable header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) haz, cached = self.compiler.check_header(hname, kwargs['prefix'], self.environment, extra_args=extra_args, @@ -521,7 +591,7 @@ class CompilerHolder(ObjectHolder['Compiler']): if disabled: mlog.log('Has header', mlog.bold(hname, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) haz, cached = self.compiler.has_header(hname, kwargs['prefix'], self.environment, extra_args=extra_args, dependencies=deps) @@ -548,7 +618,7 @@ class CompilerHolder(ObjectHolder['Compiler']): if disabled: mlog.log('Header', mlog.bold(hname, True), 'has symbol', mlog.bold(symbol, True), 'skipped: feature', mlog.bold(feature), 'disabled') return False - extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args']) + extra_args = functools.partial(self._determine_args, kwargs) deps, msg = self._determine_dependencies(kwargs['dependencies']) haz, cached = self.compiler.has_header_symbol(hname, symbol, kwargs['prefix'], self.environment, extra_args=extra_args, @@ -629,33 +699,46 @@ class CompilerHolder(ObjectHolder['Compiler']): return lib def _has_argument_impl(self, arguments: T.Union[str, T.List[str]], - mode: _TestMode = _TestMode.COMPILER) -> bool: + mode: _TestMode = _TestMode.COMPILER, + kwargs: T.Optional['ExtractRequired'] = None) -> bool: """Shared implementation for methods checking compiler and linker arguments.""" # This simplifies the callers if isinstance(arguments, str): arguments = [arguments] - test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments - result, cached = test(arguments, self.environment) - cached_msg = mlog.blue('(cached)') if cached else '' - mlog.log( + logargs: TV_LoggableList = [ 'Compiler for', self.compiler.get_display_language(), 'supports{}'.format(' link' if mode is _TestMode.LINKER else ''), 'arguments {}:'.format(' '.join(arguments)), + ] + kwargs = kwargs or {'required': False} + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + logargs += ['skipped: feature', mlog.bold(feature), 'disabled'] + mlog.log(*logargs) + return False + test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments + result, cached = test(arguments, self.environment) + if required and not result: + logargs += ['not usable'] + raise InterpreterException(*logargs) + logargs += [ mlog.green('YES') if result else mlog.red('NO'), - cached_msg) + mlog.blue('(cached)') if cached else '', + ] + mlog.log(*logargs) return result - @noKwargs @typed_pos_args('compiler.has_argument', str) - def has_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: - return self._has_argument_impl([args[0]]) + @typed_kwargs('compiler.has_argument', _HAS_REQUIRED_KW) + def has_argument_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool: + return self._has_argument_impl([args[0]], kwargs=kwargs) - @noKwargs @typed_pos_args('compiler.has_multi_arguments', varargs=str) + @typed_kwargs('compiler.has_multi_arguments', _HAS_REQUIRED_KW) @FeatureNew('compiler.has_multi_arguments', '0.37.0') - def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool: - return self._has_argument_impl(args[0]) + def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'HasArgumentKW') -> bool: + return self._has_argument_impl(args[0], kwargs=kwargs) @FeatureNew('compiler.get_supported_arguments', '0.43.0') @typed_pos_args('compiler.get_supported_arguments', varargs=str) @@ -690,16 +773,16 @@ class CompilerHolder(ObjectHolder['Compiler']): return [] @FeatureNew('compiler.has_link_argument', '0.46.0') - @noKwargs @typed_pos_args('compiler.has_link_argument', str) - def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: - return self._has_argument_impl([args[0]], mode=_TestMode.LINKER) + @typed_kwargs('compiler.has_link_argument', _HAS_REQUIRED_KW) + def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool: + return self._has_argument_impl([args[0]], mode=_TestMode.LINKER, kwargs=kwargs) @FeatureNew('compiler.has_multi_link_argument', '0.46.0') - @noKwargs @typed_pos_args('compiler.has_multi_link_argument', varargs=str) - def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool: - return self._has_argument_impl(args[0], mode=_TestMode.LINKER) + @typed_kwargs('compiler.has_multi_link_argument', _HAS_REQUIRED_KW) + def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'HasArgumentKW') -> bool: + return self._has_argument_impl(args[0], mode=_TestMode.LINKER, kwargs=kwargs) @FeatureNew('compiler.get_supported_link_arguments', '0.46.0') @noKwargs @@ -722,19 +805,33 @@ class CompilerHolder(ObjectHolder['Compiler']): mlog.log('First supported link argument:', mlog.red('None')) return [] - def _has_function_attribute_impl(self, attr: str) -> bool: + def _has_function_attribute_impl(self, attr: str, kwargs: T.Optional['ExtractRequired'] = None) -> bool: """Common helper for function attribute testing.""" - result, cached = self.compiler.has_func_attribute(attr, self.environment) - cached_msg = mlog.blue('(cached)') if cached else '' - h = mlog.green('YES') if result else mlog.red('NO') - mlog.log(f'Compiler for {self.compiler.get_display_language()} supports function attribute {attr}:', h, cached_msg) - return result + logargs: TV_LoggableList = [ + f'Compiler for {self.compiler.get_display_language()} supports function attribute {attr}:', + ] + kwargs = kwargs or {'required': False} + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False) + if disabled: + logargs += ['skipped: feature', mlog.bold(feature), 'disabled'] + mlog.log(*logargs) + return False + had, cached = self.compiler.has_func_attribute(attr, self.environment) + if required and not had: + logargs += ['not usable'] + raise InterpreterException(*logargs) + logargs += [ + mlog.green('YES') if had else mlog.red('NO'), + mlog.blue('(cached)') if cached else '' + ] + mlog.log(*logargs) + return had @FeatureNew('compiler.has_function_attribute', '0.48.0') - @noKwargs @typed_pos_args('compiler.has_function_attribute', str) - def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: - return self._has_function_attribute_impl(args[0]) + @typed_kwargs('compiler.has_function_attribute', _HAS_REQUIRED_KW) + def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool: + return self._has_function_attribute_impl(args[0], kwargs) @FeatureNew('compiler.get_supported_function_attributes', '0.48.0') @noKwargs @@ -749,30 +846,38 @@ class CompilerHolder(ObjectHolder['Compiler']): return self.compiler.get_argument_syntax() @FeatureNew('compiler.preprocess', '0.64.0') - @typed_pos_args('compiler.preprocess', varargs=(mesonlib.File, str), min_varargs=1) + @typed_pos_args('compiler.preprocess', varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList), min_varargs=1) @typed_kwargs( 'compiler.preprocess', KwargInfo('output', str, default='@PLAINNAME@.i'), KwargInfo('compile_args', ContainerTypeInfo(list, str), listify=True, default=[]), _INCLUDE_DIRS_KW, + _DEPENDENCIES_KW.evolve(since='1.1.0'), ) def preprocess_method(self, args: T.Tuple[T.List['mesonlib.FileOrString']], kwargs: 'PreprocessKW') -> T.List[build.CustomTargetIndex]: compiler = self.compiler.get_preprocessor() - sources = self.interpreter.source_strings_to_files(args[0]) - tg_kwargs = { - f'{self.compiler.language}_args': kwargs['compile_args'], - 'build_by_default': False, - 'include_directories': kwargs['include_directories'], - } + sources: 'SourceOutputs' = self.interpreter.source_strings_to_files(args[0]) + if any(isinstance(s, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) for s in sources): + FeatureNew.single_use('compiler.preprocess with generated sources', '1.1.0', self.subproject, + location=self.current_node) + + tg_counter = next(self.preprocess_uid[self.interpreter.subdir]) + if tg_counter > 0: + FeatureNew.single_use('compiler.preprocess used multiple times', '1.1.0', self.subproject, + location=self.current_node) + tg_name = f'preprocessor_{tg_counter}' tg = build.CompileTarget( - 'preprocessor', + tg_name, self.interpreter.subdir, self.subproject, self.environment, sources, kwargs['output'], compiler, - tg_kwargs) + self.interpreter.backend, + kwargs['compile_args'], + kwargs['include_directories'], + kwargs['dependencies']) self.interpreter.add_target(tg.name, tg) # Expose this target as list of its outputs, so user can pass them to # other targets, list outputs, etc. diff --git a/mesonbuild/interpreter/dependencyfallbacks.py b/mesonbuild/interpreter/dependencyfallbacks.py index 54be990..eca6a2c 100644 --- a/mesonbuild/interpreter/dependencyfallbacks.py +++ b/mesonbuild/interpreter/dependencyfallbacks.py @@ -19,7 +19,7 @@ if T.TYPE_CHECKING: class DependencyFallbacksHolder(MesonInterpreterObject): def __init__(self, interpreter: 'Interpreter', names: T.List[str], allow_fallback: T.Optional[bool] = None, - default_options: T.Optional[T.List[str]] = None) -> None: + default_options: T.Optional[T.Dict[OptionKey, str]] = None) -> None: super().__init__(subproject=interpreter.subproject) self.interpreter = interpreter self.subproject = interpreter.subproject @@ -30,7 +30,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject): self.allow_fallback = allow_fallback self.subproject_name: T.Optional[str] = None self.subproject_varname: T.Optional[str] = None - self.subproject_kwargs = {'default_options': default_options or []} + self.subproject_kwargs = {'default_options': default_options or {}} self.names: T.List[str] = [] self.forcefallback: bool = False self.nofallback: bool = False @@ -114,12 +114,11 @@ class DependencyFallbacksHolder(MesonInterpreterObject): # dependency('foo', static: true) should implicitly add # default_options: ['default_library=static'] static = kwargs.get('static') - default_options = stringlistify(func_kwargs.get('default_options', [])) - if static is not None and not any('default_library' in i for i in default_options): + default_options = func_kwargs.get('default_options', {}) + if static is not None and 'default_library' not in default_options: default_library = 'static' if static else 'shared' - opt = f'default_library={default_library}' - mlog.log(f'Building fallback subproject with {opt}') - default_options.append(opt) + mlog.log(f'Building fallback subproject with default_library={default_library}') + default_options[OptionKey('default_library')] = default_library func_kwargs['default_options'] = default_options # Configure the subproject @@ -128,7 +127,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject): func_kwargs.setdefault('version', []) if 'default_options' in kwargs and isinstance(kwargs['default_options'], str): func_kwargs['default_options'] = listify(kwargs['default_options']) - self.interpreter.do_subproject(subp_name, 'meson', func_kwargs) + self.interpreter.do_subproject(subp_name, func_kwargs) return self._get_subproject_dep(subp_name, varname, kwargs) def _get_subproject(self, subp_name: str) -> T.Optional[SubprojectHolder]: @@ -220,6 +219,8 @@ class DependencyFallbacksHolder(MesonInterpreterObject): mlog.log('Dependency', mlog.bold(self._display_name), 'found:', mlog.red('NO'), *info) return cached_dep + elif self.forcefallback and self.subproject_name: + cached_dep = None else: info = [mlog.blue('(cached)')] cached_dep = self.coredata.deps[for_machine].get(identifier) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 44b2e72..e885010 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -23,8 +23,9 @@ from .. import compilers from .. import envconfig from ..wrap import wrap, WrapMode from .. import mesonlib -from ..mesonlib import (MesonBugException, HoldableObject, FileMode, MachineChoice, OptionKey, - listify, extract_as_list, has_path_sep, PerMachine) +from ..mesonlib import (EnvironmentVariables, ExecutableSerialisation, MesonBugException, MesonException, HoldableObject, + FileMode, MachineChoice, OptionKey, listify, + extract_as_list, has_path_sep, path_is_in_root, PerMachine) from ..programs import ExternalProgram, NonExistingExternalProgram from ..dependencies import Dependency from ..depfile import DepFile @@ -32,11 +33,11 @@ from ..interpreterbase import ContainerTypeInfo, InterpreterBase, KwargInfo, typ from ..interpreterbase import noPosargs, noKwargs, permittedKwargs, noArgsFlattening, noSecondLevelHolderResolving, unholder_return from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest from ..interpreterbase import Disabler, disablerIfNotFound -from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs, FeatureDeprecatedKwargs -from ..interpreterbase import ObjectHolder +from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureBroken, FeatureNewKwargs +from ..interpreterbase import ObjectHolder, ContextManagerObject +from ..interpreterbase import stringifyUserArguments from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule -from ..cmake import CMakeInterpreter -from ..backend.backends import ExecutableSerialisation +from ..optinterpreter import optname_regex from . import interpreterobjects as OBJ from . import compiler as compilerOBJ @@ -51,12 +52,16 @@ from .interpreterobjects import ( NullSubprojectInterpreter, ) from .type_checking import ( + BUILD_TARGET_KWS, COMMAND_KW, CT_BUILD_ALWAYS, CT_BUILD_ALWAYS_STALE, CT_BUILD_BY_DEFAULT, CT_INPUT_KW, CT_INSTALL_DIR_KW, + EXECUTABLE_KWS, + JAR_KWS, + LIBRARY_KWS, MULTI_OUTPUT_KW, OUTPUT_KW, DEFAULT_OPTIONS, @@ -73,6 +78,7 @@ from .type_checking import ( INSTALL_KW, INSTALL_DIR_KW, INSTALL_MODE_KW, + INSTALL_FOLLOW_SYMLINKS, LINK_WITH_KW, LINK_WHOLE_KW, CT_INSTALL_TAG_KW, @@ -81,7 +87,11 @@ from .type_checking import ( NATIVE_KW, PRESERVE_PATH_KW, REQUIRED_KW, - SOURCES_KW, + SHARED_LIB_KWS, + SHARED_MOD_KWS, + DEPENDENCY_SOURCES_KW, + SOURCES_VARARGS, + STATIC_LIB_KWS, VARIABLES_KW, TEST_KWS, NoneType, @@ -106,12 +116,11 @@ import copy if T.TYPE_CHECKING: import argparse - from typing_extensions import Literal - from . import kwargs as kwtypes from ..backend.backends import Backend from ..interpreterbase.baseobjects import InterpreterObject, TYPE_var, TYPE_kwargs from ..programs import OverrideProgram + from .type_checking import SourcesVarargsType # Input source types passed to Targets SourceInputs = T.Union[mesonlib.File, build.GeneratedList, build.BuildTarget, build.BothLibraries, @@ -122,6 +131,10 @@ if T.TYPE_CHECKING: build.BuildTarget, build.CustomTargetIndex, build.CustomTarget, build.ExtractedObjects, build.GeneratedList, build.StructuredSources] + BuildTargetSource = T.Union[mesonlib.FileOrString, build.GeneratedTypes, build.StructuredSources] + + ProgramVersionFunc = T.Callable[[T.Union[ExternalProgram, build.Executable, OverrideProgram]], str] + def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) -> T.Optional[str]: if isinstance(value, list): @@ -131,20 +144,6 @@ def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) return 'when passed as array must contain a File' return None - -def stringifyUserArguments(args: T.List[T.Any], quote: bool = False) -> str: - if isinstance(args, list): - return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args]) - elif isinstance(args, dict): - return '{%s}' % ', '.join(['{} : {}'.format(stringifyUserArguments(k, True), stringifyUserArguments(v, True)) for k, v in args.items()]) - elif isinstance(args, bool): - return 'true' if args else 'false' - elif isinstance(args, int): - return str(args) - elif isinstance(args, str): - return f"'{args}'" if quote else args - raise InvalidArguments('Function accepts only strings, integers, bools, lists, dictionaries and lists thereof.') - class Summary: def __init__(self, project_name: str, project_version: str): self.project_name = project_name @@ -159,9 +158,12 @@ class Summary: raise InterpreterException(f'Summary section {section!r} already have key {k!r}') formatted_values = [] for i in listify(v): - if isinstance(i, bool) and bool_yn: - formatted_values.append(mlog.green('YES') if i else mlog.red('NO')) - elif isinstance(i, (str, int, bool)): + if isinstance(i, bool): + if bool_yn: + formatted_values.append(mlog.green('YES') if i else mlog.red('NO')) + else: + formatted_values.append('true' if i else 'false') + elif isinstance(i, (str, int)): formatted_values.append(str(i)) elif isinstance(i, (ExternalProgram, Dependency)): FeatureNew.single_use('dependency or external program in summary', '0.57.0', subproject) @@ -196,7 +198,7 @@ class Summary: def dump_value(self, arr, list_sep, indent): lines_sep = '\n' + ' ' * indent if list_sep is None: - mlog.log(*arr, sep=lines_sep) + mlog.log(*arr, sep=lines_sep, display_timestamp=False) return max_len = shutil.get_terminal_size().columns line = [] @@ -210,11 +212,13 @@ class Summary: line = [] line.append(v) line_len += v_len - mlog.log(*line, sep=list_sep) + mlog.log(*line, sep=list_sep, display_timestamp=False) known_library_kwargs = ( build.known_shlib_kwargs | - build.known_stlib_kwargs + build.known_stlib_kwargs | + {f'{l}_shared_args' for l in compilers.all_languages - {'java'}} | + {f'{l}_static_args' for l in compilers.all_languages - {'java'}} ) known_build_target_kwargs = ( @@ -232,7 +236,7 @@ class InterpreterRuleRelaxation(Enum): generate a Meson AST via introspection, etc. ''' - ALLOW_BUILD_DIR_FILE_REFFERENCES = 1 + ALLOW_BUILD_DIR_FILE_REFERENCES = 1 permitted_dependency_kwargs = { 'allow_fallback', @@ -288,7 +292,6 @@ class Interpreter(InterpreterBase, HoldableObject): # be different for dependencies provided by wrap files. self.subproject_directory_name = subdir.split(os.path.sep)[-1] self.subproject_dir = subproject_dir - self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') self.relaxations = relaxations or set() if not mock and ast is None: self.load_root_meson_file() @@ -298,7 +301,7 @@ class Interpreter(InterpreterBase, HoldableObject): self.sanity_check_ast() self.builtin.update({'meson': MesonMain(self.build, self)}) self.generators: T.List[build.Generator] = [] - self.processed_buildfiles = set() # type: T.Set[str] + self.processed_buildfiles: T.Set[str] = set() self.project_args_frozen = False self.global_args_frozen = False # implies self.project_args_frozen self.subprojects: T.Dict[str, SubprojectHolder] = {} @@ -373,12 +376,10 @@ class Interpreter(InterpreterBase, HoldableObject): 'error': self.func_error, 'executable': self.func_executable, 'files': self.func_files, - 'find_library': self.func_find_library, 'find_program': self.func_find_program, 'generator': self.func_generator, 'get_option': self.func_get_option, 'get_variable': self.func_get_variable, - 'gettext': self.func_gettext, 'import': self.func_import, 'include_directories': self.func_include_directories, 'install_data': self.func_install_data, @@ -414,6 +415,8 @@ class Interpreter(InterpreterBase, HoldableObject): }) if 'MESON_UNIT_TEST' in os.environ: self.funcs.update({'exception': self.func_exception}) + if 'MESON_RUNNING_IN_PROJECT_TESTS' in os.environ: + self.funcs.update({'expect_error': self.func_expect_error}) def build_holder_map(self) -> None: ''' @@ -454,7 +457,7 @@ class Interpreter(InterpreterBase, HoldableObject): build.SymlinkData: OBJ.SymlinkDataHolder, build.InstallDir: OBJ.InstallDirHolder, build.IncludeDirs: OBJ.IncludeDirsHolder, - build.EnvironmentVariables: OBJ.EnvironmentVariablesHolder, + mesonlib.EnvironmentVariables: OBJ.EnvironmentVariablesHolder, build.StructuredSources: OBJ.StructuredSourcesHolder, compilers.RunResult: compilerOBJ.TryRunResultHolder, dependencies.ExternalLibrary: OBJ.ExternalLibraryHolder, @@ -521,6 +524,25 @@ class Interpreter(InterpreterBase, HoldableObject): else: raise InterpreterException(f'Module returned a value of unknown type {v!r}.') + def handle_meson_version(self, pv: str, location: mparser.BaseNode) -> None: + if not mesonlib.version_compare(coredata.stable_version, pv): + raise InterpreterException.from_node(f'Meson version is {coredata.version} but project requires {pv}', node=location) + mesonlib.project_meson_versions[self.subproject] = pv + + def handle_meson_version_from_ast(self) -> None: + if not self.ast.lines: + return + project = self.ast.lines[0] + # first line is always project() + if not isinstance(project, mparser.FunctionNode): + return + for kw, val in project.args.kwargs.items(): + assert isinstance(kw, mparser.IdNode), 'for mypy' + if kw.value == 'meson_version': + # mypy does not understand "and isinstance" + if isinstance(val, mparser.BaseStringNode): + self.handle_meson_version(val.value, val) + def get_build_def_files(self) -> mesonlib.OrderedSet[str]: return self.build_def_files @@ -533,7 +555,7 @@ class Interpreter(InterpreterBase, HoldableObject): if f.is_built: return f = os.path.normpath(f.relative_name()) - elif os.path.isfile(f) and not f.startswith('/dev'): + elif os.path.isfile(f) and not f.startswith('/dev/'): srcdir = Path(self.environment.get_source_dir()) builddir = Path(self.environment.get_build_dir()) try: @@ -667,16 +689,21 @@ class Interpreter(InterpreterBase, HoldableObject): INCLUDE_DIRECTORIES, LINK_WITH_KW, LINK_WHOLE_KW.evolve(since='0.46.0'), - SOURCES_KW, + DEPENDENCY_SOURCES_KW, + KwargInfo('extra_files', ContainerTypeInfo(list, (mesonlib.File, str)), listify=True, default=[], since='1.2.0'), VARIABLES_KW.evolve(since='0.54.0', since_values={list: '0.56.0'}), KwargInfo('version', (str, NoneType)), + KwargInfo('objects', ContainerTypeInfo(list, build.ExtractedObjects), listify=True, default=[], since='1.1.0'), ) - def func_declare_dependency(self, node, args, kwargs): + def func_declare_dependency(self, node: mparser.BaseNode, args: T.List[TYPE_var], + kwargs: kwtypes.FuncDeclareDependency) -> dependencies.Dependency: deps = kwargs['dependencies'] incs = self.extract_incdirs(kwargs) libs = kwargs['link_with'] libs_whole = kwargs['link_whole'] + objects = kwargs['objects'] sources = self.source_strings_to_files(kwargs['sources']) + extra_files = self.source_strings_to_files(kwargs['extra_files']) compile_args = kwargs['compile_args'] link_args = kwargs['link_args'] variables = kwargs['variables'] @@ -697,13 +724,11 @@ class Interpreter(InterpreterBase, HoldableObject): continue if p.is_absolute() and p.is_dir() and srcdir / self.root_subdir in [p] + list(Path(os.path.abspath(p)).parents): variables[k] = P_OBJ.DependencyVariableString(v) - for d in deps: - if not isinstance(d, dependencies.Dependency): - raise InterpreterException('Invalid dependency') dep = dependencies.InternalDependency(version, incs, compile_args, - link_args, libs, libs_whole, sources, deps, - variables, d_module_versions, d_import_dirs) + link_args, libs, libs_whole, sources, extra_files, + deps, variables, d_module_versions, d_import_dirs, + objects) return dep @typed_pos_args('assert', bool, optargs=[str]) @@ -747,10 +772,9 @@ class Interpreter(InterpreterBase, HoldableObject): args: T.Tuple[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str], T.List[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str]]], kwargs: 'kwtypes.RunCommand') -> RunProcess: - return self.run_command_impl(node, args, kwargs) + return self.run_command_impl(args, kwargs) def run_command_impl(self, - node: mparser.BaseNode, args: T.Tuple[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str], T.List[T.Union[build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str]]], kwargs: 'kwtypes.RunCommand', @@ -776,7 +800,7 @@ class Interpreter(InterpreterBase, HoldableObject): progname = name break else: - raise MesonBugException('cmd was a built executable but not found in overrides table') + raise InterpreterException(f'Program {cmd.description()!r} is a compiled executable and therefore cannot be used during configuration') raise InterpreterException(overridden_msg.format(progname, cmd.description())) if isinstance(cmd, ExternalProgram): if not cmd.found(): @@ -806,7 +830,7 @@ class Interpreter(InterpreterBase, HoldableObject): elif isinstance(a, ExternalProgram): expanded_args.append(a.get_path()) elif isinstance(a, compilers.Compiler): - FeatureNew.single_use('Compiler object as a variadic argument to `run_command`', '0.61.0', self.subproject, location=node) + FeatureNew.single_use('Compiler object as a variadic argument to `run_command`', '0.61.0', self.subproject, location=self.current_node) prog = ExternalProgram(a.exelist[0], silent=True) if not prog.found(): raise InterpreterException(f'Program {cmd!r} not found or not executable') @@ -826,9 +850,6 @@ class Interpreter(InterpreterBase, HoldableObject): self.environment.get_build_command() + ['introspect'], in_builddir=in_builddir, check=check, capture=capture) - def func_gettext(self, nodes, args, kwargs): - raise InterpreterException('Gettext() function has been moved to module i18n. Import it and use i18n.gettext() instead') - def func_option(self, nodes, args, kwargs): raise InterpreterException('Tried to call option() in build description file. All options must be in the option file.') @@ -847,7 +868,7 @@ class Interpreter(InterpreterBase, HoldableObject): 'options': None, 'cmake_options': [], } - return self.do_subproject(args[0], 'meson', kw) + return self.do_subproject(args[0], kw) def disabled_subproject(self, subp_name: str, disabled_feature: T.Optional[str] = None, exception: T.Optional[Exception] = None) -> SubprojectHolder: @@ -856,13 +877,13 @@ class Interpreter(InterpreterBase, HoldableObject): self.subprojects[subp_name] = sub return sub - def do_subproject(self, subp_name: str, method: Literal['meson', 'cmake'], kwargs: kwtypes.DoSubproject) -> SubprojectHolder: + def do_subproject(self, subp_name: str, kwargs: kwtypes.DoSubproject, force_method: T.Optional[wrap.Method] = None) -> SubprojectHolder: disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) if disabled: mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled') return self.disabled_subproject(subp_name, disabled_feature=feature) - default_options = coredata.create_options_dict(kwargs['default_options'], subp_name) + default_options = {k.evolve(subproject=subp_name): v for k, v in kwargs['default_options'].items()} if subp_name == '': raise InterpreterException('Subproject name must not be empty.') @@ -892,7 +913,7 @@ class Interpreter(InterpreterBase, HoldableObject): r = self.environment.wrap_resolver try: - subdir = r.resolve(subp_name, method) + subdir, method = r.resolve(subp_name, force_method) except wrap.WrapException as e: if not required: mlog.log(e) @@ -900,7 +921,6 @@ class Interpreter(InterpreterBase, HoldableObject): return self.disabled_subproject(subp_name, exception=e) raise e - subdir_abs = os.path.join(self.environment.get_source_dir(), subdir) os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True) self.global_args_frozen = True @@ -910,13 +930,14 @@ class Interpreter(InterpreterBase, HoldableObject): m += ['method', mlog.bold(method)] mlog.log(*m, '\n', nested=False) + methods_map: T.Dict[wrap.Method, T.Callable[[str, str, T.Dict[OptionKey, str, kwtypes.DoSubproject]], SubprojectHolder]] = { + 'meson': self._do_subproject_meson, + 'cmake': self._do_subproject_cmake, + 'cargo': self._do_subproject_cargo, + } + try: - if method == 'meson': - return self._do_subproject_meson(subp_name, subdir, default_options, kwargs) - elif method == 'cmake': - return self._do_subproject_cmake(subp_name, subdir, subdir_abs, default_options, kwargs) - else: - raise mesonlib.MesonBugException(f'The method {method} is invalid for the subproject {subp_name}') + return methods_map[method](subp_name, subdir, default_options, kwargs) # Invalid code is always an error except InvalidCode: raise @@ -935,12 +956,24 @@ class Interpreter(InterpreterBase, HoldableObject): kwargs: kwtypes.DoSubproject, ast: T.Optional[mparser.CodeBlockNode] = None, build_def_files: T.Optional[T.List[str]] = None, - is_translated: bool = False, relaxations: T.Optional[T.Set[InterpreterRuleRelaxation]] = None) -> SubprojectHolder: with mlog.nested(subp_name): + if ast: + # Debug print the generated meson file + from ..ast import AstIndentationGenerator, AstPrinter + printer = AstPrinter(update_ast_line_nos=True) + ast.accept(AstIndentationGenerator()) + ast.accept(printer) + printer.post_process() + meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build') + with open(meson_filename, "w", encoding='utf-8') as f: + f.write(printer.result) + mlog.log('Generated Meson AST:', meson_filename) + mlog.cmd_ci_include(meson_filename) + new_build = self.build.copy() subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir, - default_options, ast=ast, is_translated=is_translated, + default_options, ast=ast, is_translated=(ast is not None), relaxations=relaxations, user_defined_options=self.user_defined_options) # Those lists are shared by all interpreters. That means that @@ -954,12 +987,9 @@ class Interpreter(InterpreterBase, HoldableObject): subi.subproject_stack = self.subproject_stack + [subp_name] current_active = self.active_projectname - current_warnings_counter = mlog.log_warnings_counter - mlog.log_warnings_counter = 0 - subi.run() - subi_warnings = mlog.log_warnings_counter - mlog.log_warnings_counter = current_warnings_counter - + with mlog.nested_warnings(): + subi.run() + subi_warnings = mlog.get_warning_count() mlog.log('Subproject', mlog.bold(subp_name), 'finished.') mlog.log() @@ -971,65 +1001,56 @@ class Interpreter(InterpreterBase, HoldableObject): raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.') self.active_projectname = current_active self.subprojects.update(subi.subprojects) - self.subprojects[subp_name] = SubprojectHolder(subi, subdir, warnings=subi_warnings) + self.subprojects[subp_name] = SubprojectHolder(subi, subdir, warnings=subi_warnings, + callstack=self.subproject_stack) # Duplicates are possible when subproject uses files from project root if build_def_files: self.build_def_files.update(build_def_files) - # We always need the subi.build_def_files, to propgate sub-sub-projects + # We always need the subi.build_def_files, to propagate sub-sub-projects self.build_def_files.update(subi.build_def_files) self.build.merge(subi.build) self.build.subprojects[subp_name] = subi.project_version return self.subprojects[subp_name] - def _do_subproject_cmake(self, subp_name: str, subdir: str, subdir_abs: str, + def _do_subproject_cmake(self, subp_name: str, subdir: str, default_options: T.Dict[OptionKey, str], kwargs: kwtypes.DoSubproject) -> SubprojectHolder: + from ..cmake import CMakeInterpreter with mlog.nested(subp_name): - new_build = self.build.copy() prefix = self.coredata.options[OptionKey('prefix')].value from ..modules.cmake import CMakeSubprojectOptions - options = kwargs['options'] or CMakeSubprojectOptions() - cmake_options = kwargs['cmake_options'] + options.cmake_options - cm_int = CMakeInterpreter(new_build, Path(subdir), Path(subdir_abs), Path(prefix), new_build.environment, self.backend) + options = kwargs.get('options') or CMakeSubprojectOptions() + cmake_options = kwargs.get('cmake_options', []) + options.cmake_options + cm_int = CMakeInterpreter(Path(subdir), Path(prefix), self.build.environment, self.backend) cm_int.initialise(cmake_options) cm_int.analyse() # Generate a meson ast and execute it with the normal do_subproject_meson ast = cm_int.pretend_to_be_meson(options.target_options) - - mlog.log() - with mlog.nested('cmake-ast'): - mlog.log('Processing generated meson AST') - - # Debug print the generated meson file - from ..ast import AstIndentationGenerator, AstPrinter - printer = AstPrinter(update_ast_line_nos=True) - ast.accept(AstIndentationGenerator()) - ast.accept(printer) - printer.post_process() - meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build') - with open(meson_filename, "w", encoding='utf-8') as f: - f.write(printer.result) - - mlog.log('Build file:', meson_filename) - mlog.cmd_ci_include(meson_filename) - mlog.log() - result = self._do_subproject_meson( subp_name, subdir, default_options, kwargs, ast, [str(f) for f in cm_int.bs_files], - is_translated=True, relaxations={ - InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFFERENCES, + InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFERENCES, } ) result.cm_interpreter = cm_int - - mlog.log() return result + def _do_subproject_cargo(self, subp_name: str, subdir: str, + default_options: T.Dict[OptionKey, str], + kwargs: kwtypes.DoSubproject) -> SubprojectHolder: + from .. import cargo + FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node) + with mlog.nested(subp_name): + ast = cargo.interpret(subp_name, subdir, self.environment) + return self._do_subproject_meson( + subp_name, subdir, default_options, kwargs, ast, + # FIXME: Are there other files used by cargo interpreter? + [os.path.join(subdir, 'Cargo.toml')]) + def get_option_internal(self, optname: str) -> coredata.UserOption: key = OptionKey.from_string(optname).evolve(subproject=self.subproject) @@ -1074,6 +1095,10 @@ class Interpreter(InterpreterBase, HoldableObject): raise InterpreterException('Having a colon in option name is forbidden, ' 'projects are not allowed to directly access ' 'options of other subprojects.') + + if optname_regex.search(optname.split('.', maxsplit=1)[-1]) is not None: + raise InterpreterException(f'Invalid option name {optname!r}') + opt = self.get_option_internal(optname) if isinstance(opt, coredata.UserFeatureOption): opt.name = optname @@ -1101,21 +1126,30 @@ class Interpreter(InterpreterBase, HoldableObject): # The backend is already set when parsing subprojects if self.backend is not None: return - backend = self.coredata.get_option(OptionKey('backend')) from ..backend import backends - self.backend = backends.get_backend_from_name(backend, self.build, self) + + if OptionKey('genvslite') in self.user_defined_options.cmd_line_options.keys(): + # Use of the '--genvslite vsxxxx' option ultimately overrides any '--backend xxx' + # option the user may specify. + backend_name = self.coredata.get_option(OptionKey('genvslite')) + self.backend = backends.get_genvslite_backend(backend_name, self.build, self) + else: + backend_name = self.coredata.get_option(OptionKey('backend')) + self.backend = backends.get_backend_from_name(backend_name, self.build, self) if self.backend is None: - raise InterpreterException(f'Unknown backend "{backend}".') - if backend != self.backend.name: + raise InterpreterException(f'Unknown backend "{backend_name}".') + if backend_name != self.backend.name: if self.backend.name.startswith('vs'): mlog.log('Auto detected Visual Studio backend:', mlog.bold(self.backend.name)) - self.coredata.set_option(OptionKey('backend'), self.backend.name) + if not self.environment.first_invocation: + raise MesonBugException(f'Backend changed from {backend_name} to {self.backend.name}') + self.coredata.set_option(OptionKey('backend'), self.backend.name, first_invocation=True) # Only init backend options on first invocation otherwise it would # override values previously set from command line. if self.environment.first_invocation: - self.coredata.init_backend_options(backend) + self.coredata.init_backend_options(backend_name) options = {k: v for k, v in self.environment.options.items() if k.is_backend()} self.coredata.set_options(options) @@ -1132,7 +1166,8 @@ class Interpreter(InterpreterBase, HoldableObject): validator=_project_version_validator, convertor=lambda x: x[0] if isinstance(x, list) else x, ), - KwargInfo('license', ContainerTypeInfo(list, str), default=['unknown'], listify=True), + KwargInfo('license', (ContainerTypeInfo(list, str), NoneType), default=None, listify=True), + KwargInfo('license_files', ContainerTypeInfo(list, str), default=[], listify=True, since='1.1.0'), KwargInfo('subproject_dir', str, default='subprojects'), ) def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str]], kwargs: 'kwtypes.Project') -> None: @@ -1143,25 +1178,41 @@ class Interpreter(InterpreterBase, HoldableObject): # This needs to be evaluated as early as possible, as meson uses this # for things like deprecation testing. if kwargs['meson_version']: - cv = coredata.version - pv = kwargs['meson_version'] - if not mesonlib.version_compare(cv, pv): - raise InterpreterException(f'Meson version is {cv} but project requires {pv}') - mesonlib.project_meson_versions[self.subproject] = kwargs['meson_version'] - - if os.path.exists(self.option_file): + self.handle_meson_version(kwargs['meson_version'], node) + + # Load "meson.options" before "meson_options.txt", and produce a warning if + # it is being used with an old version. I have added check that if both + # exist the warning isn't raised + option_file = os.path.join(self.source_root, self.subdir, 'meson.options') + old_option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') + + if os.path.exists(option_file): + if os.path.exists(old_option_file): + if os.path.samefile(option_file, old_option_file): + mlog.debug("Not warning about meson.options with version minimum < 1.1 because meson_options.txt also exists") + else: + raise MesonException("meson.options and meson_options.txt both exist, but are not the same file.") + else: + FeatureNew.single_use('meson.options file', '1.1', self.subproject, 'Use meson_options.txt instead') + else: + option_file = old_option_file + if os.path.exists(option_file): oi = optinterpreter.OptionInterpreter(self.subproject) - oi.process(self.option_file) + oi.process(option_file) self.coredata.update_project_options(oi.options) - self.add_build_def_file(self.option_file) + self.add_build_def_file(option_file) + + if self.subproject: + self.project_default_options = {k.evolve(subproject=self.subproject): v + for k, v in kwargs['default_options'].items()} + else: + self.project_default_options = kwargs['default_options'] # Do not set default_options on reconfigure otherwise it would override # values previously set from command line. That means that changing # default_options in a project will trigger a reconfigure but won't # have any effect. - self.project_default_options = coredata.create_options_dict( - kwargs['default_options'], self.subproject) - + # # If this is the first invocation we always need to initialize # builtins, if this is a subproject that is new in a re-invocation we # need to initialize builtins for that @@ -1198,8 +1249,20 @@ class Interpreter(InterpreterBase, HoldableObject): if self.build.project_version is None: self.build.project_version = self.project_version - proj_license = kwargs['license'] - self.build.dep_manifest[proj_name] = build.DepManifest(self.project_version, proj_license) + + if kwargs['license'] is None: + proj_license = ['unknown'] + if kwargs['license_files']: + raise InvalidArguments('Project `license` name must be specified when `license_files` is set') + else: + proj_license = kwargs['license'] + proj_license_files = [] + for i in self.source_strings_to_files(kwargs['license_files']): + ifname = i.absolute_path(self.environment.source_dir, + self.environment.build_dir) + proj_license_files.append((ifname, i)) + self.build.dep_manifest[proj_name] = build.DepManifest(self.project_version, proj_license, + proj_license_files, self.subproject) if self.subproject in self.build.projects: raise InvalidCode('Second call to project().') @@ -1238,9 +1301,9 @@ class Interpreter(InterpreterBase, HoldableObject): # vs backend version we need. But after setting default_options in case # the project sets vs backend by default. backend = self.coredata.get_option(OptionKey('backend')) - force_vsenv = self.user_defined_options.vsenv or backend.startswith('vs') - if mesonlib.setup_vsenv(force_vsenv): - self.build.need_vsenv = True + vsenv = self.coredata.get_option(OptionKey('vsenv')) + force_vsenv = vsenv or backend.startswith('vs') + mesonlib.setup_vsenv(force_vsenv) self.add_languages(proj_langs, True, MachineChoice.HOST) self.add_languages(proj_langs, False, MachineChoice.BUILD) @@ -1273,12 +1336,18 @@ class Interpreter(InterpreterBase, HoldableObject): success &= self.add_languages(langs, required, MachineChoice.HOST) return success + def _stringify_user_arguments(self, args: T.List[TYPE_var], func_name: str) -> T.List[str]: + try: + return [stringifyUserArguments(i, self.subproject) for i in args] + except InvalidArguments as e: + raise InvalidArguments(f'{func_name}(): {str(e)}') + @noArgsFlattening @noKwargs def func_message(self, node: mparser.BaseNode, args, kwargs): if len(args) > 1: FeatureNew.single_use('message with more than one argument', '0.54.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'message') self.message_impl(args_str) def message_impl(self, args): @@ -1312,16 +1381,19 @@ class Interpreter(InterpreterBase, HoldableObject): section, values, kwargs['bool_yn'], kwargs['list_sep'], self.subproject) def _print_summary(self) -> None: - # Add automatic 'Supbrojects' section in main project. + # Add automatic 'Subprojects' section in main project. all_subprojects = collections.OrderedDict() for name, subp in sorted(self.subprojects.items()): - value = subp.found() + value = [subp.found()] if subp.disabled_feature: - value = [value, f'Feature {subp.disabled_feature!r} disabled'] + value += [f'Feature {subp.disabled_feature!r} disabled'] elif subp.exception: - value = [value, str(subp.exception)] + value += [str(subp.exception)] elif subp.warnings > 0: - value = [value, f'{subp.warnings} warnings'] + value += [f'{subp.warnings} warnings'] + if subp.callstack: + stack = ' => '.join(subp.callstack) + value += [f'(from {stack})'] all_subprojects[name] = value if all_subprojects: self.summary_impl('Subprojects', all_subprojects, @@ -1354,7 +1426,7 @@ class Interpreter(InterpreterBase, HoldableObject): def func_warning(self, node, args, kwargs): if len(args) > 1: FeatureNew.single_use('warning with more than one argument', '0.54.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'warning') mlog.warning(*args_str, location=node) @noArgsFlattening @@ -1362,20 +1434,43 @@ class Interpreter(InterpreterBase, HoldableObject): def func_error(self, node, args, kwargs): if len(args) > 1: FeatureNew.single_use('error with more than one argument', '0.58.0', self.subproject, location=node) - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'error') raise InterpreterException('Problem encountered: ' + ' '.join(args_str)) @noArgsFlattening @FeatureNew('debug', '0.63.0') @noKwargs def func_debug(self, node, args, kwargs): - args_str = [stringifyUserArguments(i) for i in args] + args_str = self._stringify_user_arguments(args, 'debug') mlog.debug('Debug:', *args_str) @noKwargs @noPosargs def func_exception(self, node, args, kwargs): - raise Exception() + raise RuntimeError('unit test traceback :)') + + @typed_pos_args('expect_error', str) + @typed_kwargs( + 'expect_error', + KwargInfo('how', str, default='literal', validator=in_set_validator({'literal', 're'})), + ) + def func_expect_error(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: TYPE_kwargs) -> ContextManagerObject: + class ExpectErrorObject(ContextManagerObject): + def __init__(self, msg: str, how: str, subproject: str) -> None: + super().__init__(subproject) + self.msg = msg + self.how = how + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_val is None: + raise InterpreterException('Expecting an error but code block succeeded') + if isinstance(exc_val, mesonlib.MesonException): + msg = str(exc_val) + if (self.how == 'literal' and self.msg != msg) or \ + (self.how == 're' and not re.match(self.msg, msg)): + raise InterpreterException(f'Expecting error {self.msg!r} but got {msg!r}') + return True + return ExpectErrorObject(args[0], kwargs['how'], self.subproject) def add_languages(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> bool: success = self.add_languages_for(args, required, for_machine) @@ -1418,14 +1513,13 @@ class Interpreter(InterpreterBase, HoldableObject): comp = self.coredata.compilers[for_machine].get(lang) if not comp: try: - comp = compilers.detect_compiler_for(self.environment, lang, for_machine) + skip_sanity_check = self.should_skip_sanity_check(for_machine) + if skip_sanity_check: + mlog.log('Cross compiler sanity tests disabled via the cross file.', once=True) + comp = compilers.detect_compiler_for(self.environment, lang, for_machine, skip_sanity_check) if comp is None: raise InvalidArguments(f'Tried to use unknown language "{lang}".') - if self.should_skip_sanity_check(for_machine): - mlog.log_once('Cross compiler sanity tests disabled via the cross file.') - else: - comp.sanity_check(self.environment.get_scratch_dir(), self.environment) - except Exception: + except mesonlib.MesonException: if not required: mlog.log('Compiler for language', mlog.bold(lang), 'for the', machine_name, @@ -1466,7 +1560,9 @@ class Interpreter(InterpreterBase, HoldableObject): if not isinstance(p, str): raise InterpreterException('Executable name must be a string') prog = ExternalProgram.from_bin_list(self.environment, for_machine, p) - if prog.found(): + # if the machine file specified something, it may be a regular + # not-found program but we still want to return that + if not isinstance(prog, NonExistingExternalProgram): return prog return None @@ -1532,49 +1628,27 @@ class Interpreter(InterpreterBase, HoldableObject): # the host machine. def find_program_impl(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice = MachineChoice.HOST, + default_options: T.Optional[T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]]] = None, required: bool = True, silent: bool = True, wanted: T.Union[str, T.List[str]] = '', search_dirs: T.Optional[T.List[str]] = None, - version_func: T.Optional[T.Callable[[T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']], str]] = None + version_func: T.Optional[ProgramVersionFunc] = None ) -> T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']: args = mesonlib.listify(args) extra_info: T.List[mlog.TV_Loggable] = [] - progobj = self.program_lookup(args, for_machine, required, search_dirs, extra_info) - if progobj is None: + progobj = self.program_lookup(args, for_machine, default_options, required, search_dirs, wanted, version_func, extra_info) + if progobj is None or not self.check_program_version(progobj, wanted, version_func, extra_info): progobj = self.notfound_program(args) if isinstance(progobj, ExternalProgram) and not progobj.found(): if not silent: - mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO')) + mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'), *extra_info) if required: m = 'Program {!r} not found or not executable' raise InterpreterException(m.format(progobj.get_name())) return progobj - if wanted: - if version_func: - version = version_func(progobj) - elif isinstance(progobj, build.Executable): - if progobj.subproject: - interp = self.subprojects[progobj.subproject].held_object - else: - interp = self - assert isinstance(interp, Interpreter) - version = interp.project_version - else: - version = progobj.get_version(self) - is_found, not_found, _ = mesonlib.version_compare_many(version, wanted) - if not is_found: - mlog.log('Program', mlog.bold(progobj.name), 'found:', mlog.red('NO'), - 'found', mlog.normal_cyan(version), 'but need:', - mlog.bold(', '.join([f"'{e}'" for e in not_found])), *extra_info) - if required: - m = 'Invalid version of program, need {!r} {!r} found {!r}.' - raise InterpreterException(m.format(progobj.name, not_found, version)) - return self.notfound_program(args) - extra_info.insert(0, mlog.normal_cyan(version)) - # Only store successful lookups self.store_name_lookups(args) if not silent: @@ -1584,18 +1658,27 @@ class Interpreter(InterpreterBase, HoldableObject): return progobj def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice, - required: bool, search_dirs: T.List[str], extra_info: T.List[mlog.TV_Loggable] + default_options: T.Optional[T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]]], + required: bool, + search_dirs: T.List[str], + wanted: T.Union[str, T.List[str]], + version_func: T.Optional[ProgramVersionFunc], + extra_info: T.List[mlog.TV_Loggable] ) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]: progobj = self.program_from_overrides(args, extra_info) if progobj: return progobj + if args[0] == 'meson': + # Override find_program('meson') to return what we were invoked with + return ExternalProgram('meson', self.environment.get_build_command(), silent=True) + fallback = None wrap_mode = self.coredata.get_option(OptionKey('wrap_mode')) if wrap_mode != WrapMode.nofallback and self.environment.wrap_resolver: fallback = self.environment.wrap_resolver.find_program_provider(args) if fallback and wrap_mode == WrapMode.forcefallback: - return self.find_program_fallback(fallback, args, required, extra_info) + return self.find_program_fallback(fallback, args, default_options, required, extra_info) progobj = self.program_from_file_for(for_machine, args) if progobj is None: @@ -1603,24 +1686,56 @@ class Interpreter(InterpreterBase, HoldableObject): if progobj is None and args[0].endswith('python3'): prog = ExternalProgram('python3', mesonlib.python_command, silent=True) progobj = prog if prog.found() else None + + if progobj and not self.check_program_version(progobj, wanted, version_func, extra_info): + progobj = None + if progobj is None and fallback and required: - progobj = self.find_program_fallback(fallback, args, required, extra_info) + progobj = self.notfound_program(args) + mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'), *extra_info) + extra_info.clear() + progobj = self.find_program_fallback(fallback, args, default_options, required, extra_info) return progobj + def check_program_version(self, progobj: T.Union[ExternalProgram, build.Executable, OverrideProgram], + wanted: T.Union[str, T.List[str]], + version_func: T.Optional[ProgramVersionFunc], + extra_info: T.List[mlog.TV_Loggable]) -> bool: + if wanted: + if version_func: + version = version_func(progobj) + elif isinstance(progobj, build.Executable): + if progobj.subproject: + interp = self.subprojects[progobj.subproject].held_object + else: + interp = self + assert isinstance(interp, Interpreter) + version = interp.project_version + else: + version = progobj.get_version(self) + is_found, not_found, _ = mesonlib.version_compare_many(version, wanted) + if not is_found: + extra_info[:0] = ['found', mlog.normal_cyan(version), 'but need:', + mlog.bold(', '.join([f"'{e}'" for e in not_found]))] + return False + extra_info.insert(0, mlog.normal_cyan(version)) + return True + def find_program_fallback(self, fallback: str, args: T.List[mesonlib.FileOrString], + default_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]], required: bool, extra_info: T.List[mlog.TV_Loggable] ) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]: mlog.log('Fallback to subproject', mlog.bold(fallback), 'which provides program', mlog.bold(' '.join(args))) sp_kwargs: kwtypes.DoSubproject = { 'required': required, - 'default_options': [], + 'default_options': default_options or {}, 'version': [], 'cmake_options': [], 'options': None, } - self.do_subproject(fallback, 'meson', sp_kwargs) + self.do_subproject(fallback, sp_kwargs) return self.program_from_overrides(args, extra_info) @typed_pos_args('find_program', varargs=(str, mesonlib.File), min_varargs=1) @@ -1631,6 +1746,7 @@ class Interpreter(InterpreterBase, HoldableObject): REQUIRED_KW, KwargInfo('dirs', ContainerTypeInfo(list, str), default=[], listify=True, since='0.53.0'), KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True, since='0.52.0'), + DEFAULT_OPTIONS.evolve(since='1.3.0') ) @disablerIfNotFound def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]], @@ -1642,16 +1758,11 @@ class Interpreter(InterpreterBase, HoldableObject): return self.notfound_program(args[0]) search_dirs = extract_search_dirs(kwargs) - return self.find_program_impl(args[0], kwargs['native'], required=required, + default_options = kwargs['default_options'] + return self.find_program_impl(args[0], kwargs['native'], default_options=default_options, required=required, silent=False, wanted=kwargs['version'], search_dirs=search_dirs) - def func_find_library(self, node, args, kwargs): - raise InvalidCode('find_library() is removed, use meson.get_compiler(\'name\').find_library() instead.\n' - 'Look here for documentation: http://mesonbuild.com/Reference-manual.html#compiler-object\n' - 'Look here for example: http://mesonbuild.com/howtox.html#add-math-library-lm-portably\n' - ) - # When adding kwargs, please check if they make sense in dependencies.get_dep_identifier() @FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version']) @FeatureNewKwargs('dependency', '0.56.0', ['allow_fallback']) @@ -1660,10 +1771,10 @@ class Interpreter(InterpreterBase, HoldableObject): @FeatureNewKwargs('dependency', '0.50.0', ['not_found_message', 'cmake_module_path', 'cmake_args']) @FeatureNewKwargs('dependency', '0.49.0', ['disabler']) @FeatureNewKwargs('dependency', '0.40.0', ['method']) - @FeatureNewKwargs('dependency', '0.38.0', ['default_options']) @disablerIfNotFound @permittedKwargs(permitted_dependency_kwargs) @typed_pos_args('dependency', varargs=str, min_varargs=1) + @typed_kwargs('dependency', DEFAULT_OPTIONS.evolve(since='0.38.0'), allow_unknown=True) def func_dependency(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs) -> Dependency: # Replace '' by empty list of names names = [n for n in args[0] if n] @@ -1710,54 +1821,80 @@ class Interpreter(InterpreterBase, HoldableObject): def func_disabler(self, node, args, kwargs): return Disabler() - @FeatureNewKwargs('executable', '0.42.0', ['implib']) - @FeatureNewKwargs('executable', '0.56.0', ['win_subsystem']) - @FeatureDeprecatedKwargs('executable', '0.56.0', ['gui_app'], extra_message="Use 'win_subsystem' instead.") @permittedKwargs(build.known_exe_kwargs) - def func_executable(self, node, args, kwargs): + @typed_pos_args('executable', str, varargs=SOURCES_VARARGS) + @typed_kwargs('executable', *EXECUTABLE_KWS, allow_unknown=True) + def func_executable(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.Executable) -> build.Executable: return self.build_target(node, args, kwargs, build.Executable) @permittedKwargs(build.known_stlib_kwargs) - def func_static_lib(self, node, args, kwargs): + @typed_pos_args('static_library', str, varargs=SOURCES_VARARGS) + @typed_kwargs('static_library', *STATIC_LIB_KWS, allow_unknown=True) + def func_static_lib(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.StaticLibrary) -> build.StaticLibrary: return self.build_target(node, args, kwargs, build.StaticLibrary) @permittedKwargs(build.known_shlib_kwargs) - def func_shared_lib(self, node, args, kwargs): + @typed_pos_args('shared_library', str, varargs=SOURCES_VARARGS) + @typed_kwargs('shared_library', *SHARED_LIB_KWS, allow_unknown=True) + def func_shared_lib(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.SharedLibrary) -> build.SharedLibrary: holder = self.build_target(node, args, kwargs, build.SharedLibrary) holder.shared_library_only = True return holder @permittedKwargs(known_library_kwargs) - def func_both_lib(self, node, args, kwargs): + @typed_pos_args('both_libraries', str, varargs=SOURCES_VARARGS) + @typed_kwargs('both_libraries', *LIBRARY_KWS, allow_unknown=True) + def func_both_lib(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.Library) -> build.BothLibraries: return self.build_both_libraries(node, args, kwargs) @FeatureNew('shared_module', '0.37.0') @permittedKwargs(build.known_shmod_kwargs) - def func_shared_module(self, node, args, kwargs): + @typed_pos_args('shared_module', str, varargs=SOURCES_VARARGS) + @typed_kwargs('shared_module', *SHARED_MOD_KWS, allow_unknown=True) + def func_shared_module(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.SharedModule) -> build.SharedModule: return self.build_target(node, args, kwargs, build.SharedModule) @permittedKwargs(known_library_kwargs) - def func_library(self, node, args, kwargs): + @typed_pos_args('library', str, varargs=SOURCES_VARARGS) + @typed_kwargs('library', *LIBRARY_KWS, allow_unknown=True) + def func_library(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.Library) -> build.Executable: return self.build_library(node, args, kwargs) @permittedKwargs(build.known_jar_kwargs) - def func_jar(self, node, args, kwargs): + @typed_pos_args('jar', str, varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList, build.ExtractedObjects, build.BuildTarget)) + @typed_kwargs('jar', *JAR_KWS, allow_unknown=True) + def func_jar(self, node: mparser.BaseNode, + args: T.Tuple[str, T.List[T.Union[str, mesonlib.File, build.GeneratedTypes]]], + kwargs: kwtypes.Jar) -> build.Jar: return self.build_target(node, args, kwargs, build.Jar) @FeatureNewKwargs('build_target', '0.40.0', ['link_whole', 'override_options']) @permittedKwargs(known_build_target_kwargs) - def func_build_target(self, node, args, kwargs): - if 'target_type' not in kwargs: - raise InterpreterException('Missing target_type keyword argument') - target_type = kwargs.pop('target_type') + @typed_pos_args('build_target', str, varargs=SOURCES_VARARGS) + @typed_kwargs('build_target', *BUILD_TARGET_KWS, allow_unknown=True) + def func_build_target(self, node: mparser.BaseNode, + args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.BuildTarget + ) -> T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary, + build.SharedModule, build.BothLibraries, build.Jar]: + target_type = kwargs['target_type'] if target_type == 'executable': return self.build_target(node, args, kwargs, build.Executable) elif target_type == 'shared_library': return self.build_target(node, args, kwargs, build.SharedLibrary) elif target_type == 'shared_module': - FeatureNew.single_use( - 'build_target(target_type: \'shared_module\')', - '0.51.0', self.subproject, location=node) return self.build_target(node, args, kwargs, build.SharedModule) elif target_type == 'static_library': return self.build_target(node, args, kwargs, build.StaticLibrary) @@ -1765,10 +1902,7 @@ class Interpreter(InterpreterBase, HoldableObject): return self.build_both_libraries(node, args, kwargs) elif target_type == 'library': return self.build_library(node, args, kwargs) - elif target_type == 'jar': - return self.build_target(node, args, kwargs, build.Jar) - else: - raise InterpreterException('Unknown target_type.') + return self.build_target(node, args, kwargs, build.Jar) @noPosargs @typed_kwargs( @@ -1899,7 +2033,7 @@ class Interpreter(InterpreterBase, HoldableObject): build_by_default = kwargs['build_always'] build_always_stale = kwargs['build_by_default'] - # These are are nullaable so that we can know whether they're explicitly + # These are nullable so that we can know whether they're explicitly # set or not. If they haven't been overwritten, set them to their true # default if build_by_default is None: @@ -1921,9 +2055,9 @@ class Interpreter(InterpreterBase, HoldableObject): command[0] = self.find_program_impl([command[0]]) if len(inputs) > 1 and kwargs['feed']: - raise InvalidArguments('custom_target: "feed" keyword argument can only be used used with a single input') + raise InvalidArguments('custom_target: "feed" keyword argument can only be used with a single input') if len(kwargs['output']) > 1 and kwargs['capture']: - raise InvalidArguments('custom_target: "capture" keyword argument can only be used used with a single output') + raise InvalidArguments('custom_target: "capture" keyword argument can only be used with a single output') if kwargs['capture'] and kwargs['console']: raise InvalidArguments('custom_target: "capture" and "console" keyword arguments are mutually exclusive') for c in command: @@ -2042,10 +2176,10 @@ class Interpreter(InterpreterBase, HoldableObject): kwargs: 'kwtypes.FuncTest') -> None: self.add_test(node, args, kwargs, True) - def unpack_env_kwarg(self, kwargs: T.Union[build.EnvironmentVariables, T.Dict[str, 'TYPE_var'], T.List['TYPE_var'], str]) -> build.EnvironmentVariables: + def unpack_env_kwarg(self, kwargs: T.Union[EnvironmentVariables, T.Dict[str, 'TYPE_var'], T.List['TYPE_var'], str]) -> EnvironmentVariables: envlist = kwargs.get('env') if envlist is None: - return build.EnvironmentVariables() + return EnvironmentVariables() msg = ENV_KW.validator(envlist) if msg: raise InvalidArguments(f'"env": {msg}') @@ -2094,7 +2228,9 @@ class Interpreter(InterpreterBase, HoldableObject): kwargs['priority'], kwargs['verbose']) - def add_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.Any], is_base_test: bool): + def add_test(self, node: mparser.BaseNode, + args: T.Tuple[str, T.Union[build.Executable, build.Jar, ExternalProgram, mesonlib.File]], + kwargs: T.Dict[str, T.Any], is_base_test: bool): t = self.make_test(node, args, kwargs) if is_base_test: self.build.tests.append(t) @@ -2110,6 +2246,7 @@ class Interpreter(InterpreterBase, HoldableObject): KwargInfo('subdir', (str, NoneType)), INSTALL_MODE_KW.evolve(since='0.47.0'), INSTALL_DIR_KW, + INSTALL_FOLLOW_SYMLINKS, ) def func_install_headers(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], @@ -2136,7 +2273,8 @@ class Interpreter(InterpreterBase, HoldableObject): for childdir in dirs: h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'], - install_mode, self.subproject) + install_mode, self.subproject, + follow_symlinks=kwargs['follow_symlinks']) ret_headers.append(h) self.build.headers.append(h) @@ -2272,7 +2410,7 @@ class Interpreter(InterpreterBase, HoldableObject): absname = os.path.join(self.environment.get_source_dir(), buildfilename) if not os.path.isfile(absname): self.subdir = prev_subdir - raise InterpreterException(f"Non-existent build file '{buildfilename!s}'") + raise InterpreterException(f"Nonexistent build file '{buildfilename!s}'") with open(absname, encoding='utf-8') as f: code = f.read() assert isinstance(code, str) @@ -2287,27 +2425,6 @@ class Interpreter(InterpreterBase, HoldableObject): pass self.subdir = prev_subdir - def _get_kwarg_install_mode(self, kwargs: T.Dict[str, T.Any]) -> T.Optional[FileMode]: - if kwargs.get('install_mode', None) is None: - return None - if isinstance(kwargs['install_mode'], FileMode): - return kwargs['install_mode'] - install_mode: T.List[str] = [] - mode = mesonlib.typeslistify(kwargs.get('install_mode', []), (str, int)) - for m in mode: - # We skip any arguments that are set to `false` - if m is False: - m = None - install_mode.append(m) - if len(install_mode) > 3: - raise InvalidArguments('Keyword argument install_mode takes at ' - 'most 3 arguments.') - if len(install_mode) > 0 and install_mode[0] is not None and \ - not isinstance(install_mode[0], str): - raise InvalidArguments('Keyword argument install_mode requires the ' - 'permissions arg to be a string or false') - return FileMode(*install_mode) - # This is either ignored on basically any OS nowadays, or silently gets # ignored (Solaris) or triggers an "illegal operation" error (FreeBSD). # It was likely added "because it exists", but should never be used. In @@ -2331,6 +2448,7 @@ class Interpreter(InterpreterBase, HoldableObject): INSTALL_TAG_KW.evolve(since='0.60.0'), INSTALL_DIR_KW, PRESERVE_PATH_KW.evolve(since='0.64.0'), + INSTALL_FOLLOW_SYMLINKS, ) def func_install_data(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], @@ -2343,25 +2461,32 @@ class Interpreter(InterpreterBase, HoldableObject): '"rename" and "sources" argument lists must be the same length if "rename" is given. ' f'Rename has {len(rename)} elements and sources has {len(sources)}.') + install_dir = kwargs['install_dir'] + if not install_dir: + subdir = self.active_projectname + install_dir = P_OBJ.OptionString(os.path.join(self.environment.get_datadir(), subdir), os.path.join('{datadir}', subdir)) + if self.is_subproject(): + FeatureNew.single_use('install_data() without install_dir inside of a subproject', '1.3.0', self.subproject, + 'This was broken and would install to the project name of the parent project instead', + node) + if kwargs['preserve_path']: + FeatureNew.single_use('install_data() with preserve_path and without install_dir', '1.3.0', self.subproject, + 'This was broken and would not add the project name to the install path', + node) + install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode']) - return self.install_data_impl(sources, kwargs['install_dir'], install_mode, - rename, kwargs['install_tag'], - preserve_path=kwargs['preserve_path']) + return self.install_data_impl(sources, install_dir, install_mode, rename, kwargs['install_tag'], + preserve_path=kwargs['preserve_path'], + follow_symlinks=kwargs['follow_symlinks']) - def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: T.Optional[str], + def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, install_mode: FileMode, rename: T.Optional[str], tag: T.Optional[str], - install_dir_name: T.Optional[str] = None, install_data_type: T.Optional[str] = None, - preserve_path: bool = False) -> build.Data: - - """Just the implementation with no validation.""" - idir = install_dir or '' - idir_name = install_dir_name or idir or '{datadir}' - if isinstance(idir_name, P_OBJ.OptionString): - idir_name = idir_name.optname + preserve_path: bool = False, + follow_symlinks: T.Optional[bool] = None) -> build.Data: + install_dir_name = install_dir.optname if isinstance(install_dir, P_OBJ.OptionString) else install_dir dirs = collections.defaultdict(list) - ret_data = [] if preserve_path: for file in sources: dirname = os.path.dirname(file.fname) @@ -2369,9 +2494,11 @@ class Interpreter(InterpreterBase, HoldableObject): else: dirs[''].extend(sources) + ret_data = [] for childdir, files in dirs.items(): - d = build.Data(files, os.path.join(idir, childdir), os.path.join(idir_name, childdir), - install_mode, self.subproject, rename, tag, install_data_type) + d = build.Data(files, os.path.join(install_dir, childdir), os.path.join(install_dir_name, childdir), + install_mode, self.subproject, rename, tag, install_data_type, + follow_symlinks) ret_data.append(d) self.build.data.extend(ret_data) @@ -2390,13 +2517,14 @@ class Interpreter(InterpreterBase, HoldableObject): validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None), INSTALL_MODE_KW.evolve(since='0.38.0'), INSTALL_TAG_KW.evolve(since='0.60.0'), + INSTALL_FOLLOW_SYMLINKS, ) def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir: exclude = (set(kwargs['exclude_files']), set(kwargs['exclude_directories'])) srcdir = os.path.join(self.environment.source_dir, self.subdir, args[0]) - if not os.path.isdir(srcdir) or not any(os.scandir(srcdir)): + if not os.path.isdir(srcdir) or not any(os.listdir(srcdir)): FeatureNew.single_use('install_subdir with empty directory', '0.47.0', self.subproject, location=node) FeatureDeprecated.single_use('install_subdir with empty directory', '0.60.0', self.subproject, 'It worked by accident and is buggy. Use install_emptydir instead.', node) @@ -2415,7 +2543,8 @@ class Interpreter(InterpreterBase, HoldableObject): exclude, kwargs['strip_directory'], self.subproject, - install_tag=kwargs['install_tag']) + install_tag=kwargs['install_tag'], + follow_symlinks=kwargs['follow_symlinks']) self.build.install_dirs.append(idir) return idir @@ -2453,8 +2582,9 @@ class Interpreter(InterpreterBase, HoldableObject): KwargInfo('install_dir', (str, bool), default='', validator=lambda x: 'must be `false` if boolean' if x is True else None), OUTPUT_KW, - KwargInfo('output_format', str, default='c', since='0.47.0', - validator=in_set_validator({'c', 'nasm'})), + KwargInfo('output_format', str, default='c', since='0.47.0', since_values={'json': '1.3.0'}, + validator=in_set_validator({'c', 'json', 'nasm'})), + KwargInfo('macro_name', (str, NoneType), default=None, since='1.3.0'), ) def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var], kwargs: kwtypes.ConfigureFile): @@ -2531,7 +2661,7 @@ class Interpreter(InterpreterBase, HoldableObject): file_encoding = kwargs['encoding'] missing_variables, confdata_useless = \ mesonlib.do_conf_file(inputs_abs[0], ofile_abs, conf, - fmt, file_encoding) + fmt, file_encoding, self.subproject) if missing_variables: var_list = ", ".join(repr(m) for m in sorted(missing_variables)) mlog.warning( @@ -2546,7 +2676,8 @@ class Interpreter(InterpreterBase, HoldableObject): 'copy a file to the build dir, use the \'copy:\' keyword ' 'argument added in 0.47.0', location=node) else: - mesonlib.dump_conf_header(ofile_abs, conf, output_format) + macro_name = kwargs['macro_name'] + mesonlib.dump_conf_header(ofile_abs, conf, output_format, macro_name) conf.used = True elif kwargs['command'] is not None: if len(inputs) > 1: @@ -2562,8 +2693,8 @@ class Interpreter(InterpreterBase, HoldableObject): _cmd = mesonlib.substitute_values(kwargs['command'], values) mlog.log('Configuring', mlog.bold(output), 'with command') cmd, *args = _cmd - res = self.run_command_impl(node, (cmd, args), - {'capture': True, 'check': True, 'env': build.EnvironmentVariables()}, + res = self.run_command_impl((cmd, args), + {'capture': True, 'check': True, 'env': EnvironmentVariables()}, True) if kwargs['capture']: dst_tmp = ofile_abs + '~' @@ -2609,7 +2740,7 @@ class Interpreter(InterpreterBase, HoldableObject): install_tag=install_tag, data_type='configure')) return mesonlib.File.from_built_file(self.subdir, output) - def extract_incdirs(self, kwargs, key: str = 'include_directories'): + def extract_incdirs(self, kwargs, key: str = 'include_directories') -> T.List[build.IncludeDirs]: prospectives = extract_as_list(kwargs, key) if key == 'include_directories': for i in prospectives: @@ -2618,7 +2749,7 @@ class Interpreter(InterpreterBase, HoldableObject): f'Use include_directories({i!r}) instead', location=self.current_node) break - result = [] + result: T.List[build.IncludeDirs] = [] for p in prospectives: if isinstance(p, build.IncludeDirs): result.append(p) @@ -2643,7 +2774,7 @@ class Interpreter(InterpreterBase, HoldableObject): absbase_build = os.path.join(build_root, self.subdir) for a in incdir_strings: - if a.startswith(src_root): + if path_is_in_root(Path(a), Path(src_root)): raise InvalidArguments(textwrap.dedent('''\ Tried to form an absolute path to a dir in the source tree. You should not do that but use relative paths instead, for @@ -2810,7 +2941,7 @@ class Interpreter(InterpreterBase, HoldableObject): def _add_global_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.List[str]], args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None: if self.is_subproject(): - msg = f'Function \'{node.func_name}\' cannot be used in subprojects because ' \ + msg = f'Function \'{node.func_name.value}\' cannot be used in subprojects because ' \ 'there is no way to make that reliable.\nPlease only call ' \ 'this if is_subproject() returns false. Alternatively, ' \ 'define a variable that\ncontains your language-specific ' \ @@ -2830,7 +2961,7 @@ class Interpreter(InterpreterBase, HoldableObject): def _add_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.List[str]], args_frozen: bool, args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None: if args_frozen: - msg = f'Tried to use \'{node.func_name}\' after a build target has been declared.\n' \ + msg = f'Tried to use \'{node.func_name.value}\' after a build target has been declared.\n' \ 'This is not permitted. Please declare all arguments before your targets.' raise InvalidCode(msg) @@ -2843,7 +2974,7 @@ class Interpreter(InterpreterBase, HoldableObject): @typed_pos_args('environment', optargs=[(str, list, dict)]) @typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0')) def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[None, str, T.List['TYPE_var'], T.Dict[str, 'TYPE_var']]], - kwargs: 'TYPE_kwargs') -> build.EnvironmentVariables: + kwargs: 'TYPE_kwargs') -> EnvironmentVariables: init = args[0] if init is not None: FeatureNew.single_use('environment positional arguments', '0.52.0', self.subproject, location=node) @@ -2853,7 +2984,7 @@ class Interpreter(InterpreterBase, HoldableObject): if isinstance(init, dict) and any(i for i in init.values() if isinstance(i, list)): FeatureNew.single_use('List of string in dictionary value', '0.62.0', self.subproject, location=node) return env_convertor_with_method(init, kwargs['method'], kwargs['separator']) - return build.EnvironmentVariables() + return EnvironmentVariables() @typed_pos_args('join_paths', varargs=str, min_varargs=1) @noKwargs @@ -2874,6 +3005,7 @@ class Interpreter(InterpreterBase, HoldableObject): mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets)))) FeatureNew.report(self.subproject) FeatureDeprecated.report(self.subproject) + FeatureBroken.report(self.subproject) if not self.is_subproject(): self.print_extra_warnings() self._print_summary() @@ -2892,10 +3024,12 @@ class Interpreter(InterpreterBase, HoldableObject): return if (self.coredata.options[OptionKey('b_lundef')].value and self.coredata.options[OptionKey('b_sanitize')].value != 'none'): - mlog.warning('''Trying to use {} sanitizer on Clang with b_lundef. -This will probably not work. -Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey('b_sanitize')].value), - location=self.current_node) + value = self.coredata.options[OptionKey('b_sanitize')].value + mlog.warning(textwrap.dedent(f'''\ + Trying to use {value} sanitizer on Clang with b_lundef. + This will probably not work. + Try setting b_lundef to false instead.'''), + location=self.current_node) # noqa: E128 # Check that the indicated file is within the same subproject # as we currently are. This is to stop people doing @@ -2934,7 +3068,7 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey inputtype = 'directory' else: inputtype = 'file' - if InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFFERENCES in self.relaxations and builddir in norm.parents: + if InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFERENCES in self.relaxations and builddir in norm.parents: return if srcdir not in norm.parents: # Grabbing files outside the source tree is ok. @@ -2958,11 +3092,14 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey def source_strings_to_files(self, sources: T.List['mesonlib.FileOrString'], strict: bool = False) -> T.List['mesonlib.FileOrString']: ... # noqa: F811 @T.overload - def source_strings_to_files(self, sources: T.List[mesonlib.FileOrString, build.GeneratedTypes]) -> T.List[T.Union[mesonlib.File, build.GeneratedTypes]]: ... # noqa: F811 + def source_strings_to_files(self, sources: T.List[T.Union[mesonlib.FileOrString, build.GeneratedTypes]]) -> T.List[T.Union[mesonlib.File, build.GeneratedTypes]]: ... # noqa: F811 @T.overload def source_strings_to_files(self, sources: T.List['SourceInputs'], strict: bool = True) -> T.List['SourceOutputs']: ... # noqa: F811 + @T.overload + def source_strings_to_files(self, sources: T.List[SourcesVarargsType], strict: bool = True) -> T.List['SourceOutputs']: ... # noqa: F811 + def source_strings_to_files(self, sources: T.List['SourceInputs'], strict: bool = True) -> T.List['SourceOutputs']: # noqa: F811 """Lower inputs to a list of Targets and Files, replacing any strings. @@ -3008,6 +3145,8 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey "internal use. Please rename.") def add_target(self, name: str, tobj: build.Target) -> None: + if self.backend.name == 'none': + raise InterpreterException('Install-only backend cannot generate target rules, try using `--backend=ninja`.') if name == '': raise InterpreterException('Target name must not be empty.') if name.strip() == '': @@ -3024,32 +3163,33 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey # To permit an executable and a shared library to have the # same name, such as "foo.exe" and "libfoo.a". idname = tobj.get_id() + subdir = tobj.get_subdir() + namedir = (name, subdir) + if idname in self.build.targets: raise InvalidCode(f'Tried to create target "{name}", but a target of that name already exists.') + if isinstance(tobj, build.Executable) and namedir in self.build.targetnames: + FeatureNew.single_use(f'multiple executables with the same name, "{tobj.name}", but different suffixes in the same directory', + '1.3.0', self.subproject, location=self.current_node) + if isinstance(tobj, build.BuildTarget): - missing_languages = tobj.process_compilers() - self.add_languages(missing_languages, True, tobj.for_machine) - tobj.process_compilers_late(missing_languages) + self.add_languages(tobj.missing_languages, True, tobj.for_machine) + tobj.process_compilers_late() self.add_stdlib_info(tobj) self.build.targets[idname] = tobj + # Only need to add executables to this set + if isinstance(tobj, build.Executable): + self.build.targetnames.update([namedir]) if idname not in self.coredata.target_guids: self.coredata.target_guids[idname] = str(uuid.uuid4()).upper() @FeatureNew('both_libraries', '0.46.0') - def build_both_libraries(self, node, args, kwargs): + def build_both_libraries(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.Library) -> build.BothLibraries: shared_lib = self.build_target(node, args, kwargs, build.SharedLibrary) static_lib = self.build_target(node, args, kwargs, build.StaticLibrary) - # Check if user forces non-PIC static library. - pic = True - key = OptionKey('b_staticpic') - if 'pic' in kwargs: - pic = kwargs['pic'] - elif key in self.environment.coredata.options: - pic = self.environment.coredata.options[key].value - if self.backend.name == 'xcode': # Xcode is a bit special in that you can't (at least for the moment) # form a library only from object file inputs. The simple but inefficient @@ -3058,8 +3198,15 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey # Feel free to submit patches to get this fixed if it is an # issue for you. reuse_object_files = False + elif shared_lib.uses_rust(): + # FIXME: rustc supports generating both libraries in a single invocation, + # but for now compile twice. + reuse_object_files = False + elif any(k.endswith(('static_args', 'shared_args')) and v for k, v in kwargs.items()): + # Ensure not just the keyword arguments exist, but that they are non-empty. + reuse_object_files = False else: - reuse_object_files = pic + reuse_object_files = static_lib.pic if reuse_object_files: # Replace sources with objects from the shared library to avoid @@ -3075,49 +3222,134 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey return build.BothLibraries(shared_lib, static_lib) - def build_library(self, node, args, kwargs): + def build_library(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.Library): default_library = self.coredata.get_option(OptionKey('default_library', subproject=self.subproject)) + assert isinstance(default_library, str), 'for mypy' if default_library == 'shared': - return self.build_target(node, args, kwargs, build.SharedLibrary) + return self.build_target(node, args, T.cast('kwtypes.StaticLibrary', kwargs), build.SharedLibrary) elif default_library == 'static': - return self.build_target(node, args, kwargs, build.StaticLibrary) + return self.build_target(node, args, T.cast('kwtypes.SharedLibrary', kwargs), build.StaticLibrary) elif default_library == 'both': return self.build_both_libraries(node, args, kwargs) else: raise InterpreterException(f'Unknown default_library value: {default_library}.') - def build_target(self, node: mparser.BaseNode, args, kwargs, targetclass): - @FeatureNewKwargs('build target', '0.42.0', ['rust_crate_type', 'build_rpath', 'implicit_include_directories']) - @FeatureNewKwargs('build target', '0.41.0', ['rust_args']) - @FeatureNewKwargs('build target', '0.38.0', ['build_by_default']) - @FeatureNewKwargs('build target', '0.48.0', ['gnu_symbol_visibility']) - def build_target_decorator_caller(self, node, args, kwargs): - return True - - build_target_decorator_caller(self, node, args, kwargs) - - if not args: - raise InterpreterException('Target does not have a name.') - name, *sources = args - for_machine = self.machine_from_native_kwarg(kwargs) - if 'sources' in kwargs: - sources += listify(kwargs['sources']) + def __convert_file_args(self, raw: T.List[mesonlib.FileOrString]) -> T.Tuple[T.List[mesonlib.File], T.List[str]]: + """Convert raw target arguments from File | str to File. + + This removes files from the command line and replaces them with string + values, but adds the files to depends list + + :param raw: the raw arguments + :return: A tuple of file dependencies and raw arguments + """ + depend_files: T.List[mesonlib.File] = [] + args: T.List[str] = [] + build_to_source = mesonlib.relpath(self.environment.get_source_dir(), + self.environment.get_build_dir()) + + for a in raw: + if isinstance(a, mesonlib.File): + depend_files.append(a) + args.append(a.rel_to_builddir(build_to_source)) + else: + args.append(a) + + return depend_files, args + + def __process_language_args(self, kwargs: T.Dict[str, T.List[mesonlib.FileOrString]]) -> None: + """Convert split language args into a combined dictionary. + + The Meson DSL takes arguments in the form `_args : args`, but in the + build layer we store these in a single dictionary as `{: args}`. + This function extracts the arguments from the DSL format and prepares + them for the IR. + """ + d = kwargs.setdefault('depend_files', []) + new_args: T.DefaultDict[str, T.List[str]] = collections.defaultdict(list) + + for l in compilers.all_languages: + deps, args = self.__convert_file_args(kwargs[f'{l}_args']) + new_args[l] = args + d.extend(deps) + kwargs['language_args'] = new_args + + @T.overload + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.Executable, targetclass: T.Type[build.Executable]) -> build.Executable: ... + + @T.overload + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.StaticLibrary, targetclass: T.Type[build.StaticLibrary]) -> build.StaticLibrary: ... + + @T.overload + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.SharedLibrary, targetclass: T.Type[build.SharedLibrary]) -> build.SharedLibrary: ... + + @T.overload + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.SharedModule, targetclass: T.Type[build.SharedModule]) -> build.SharedModule: ... + + @T.overload + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: kwtypes.Jar, targetclass: T.Type[build.Jar]) -> build.Jar: ... + + def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: T.Union[kwtypes.Executable, kwtypes.StaticLibrary, kwtypes.SharedLibrary, kwtypes.SharedModule, kwtypes.Jar], + targetclass: T.Type[T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]] + ) -> T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]: + name, sources = args + for_machine = kwargs['native'] + if kwargs.get('rust_crate_type') == 'proc-macro': + # Silently force to native because that's the only sensible value + # and rust_crate_type is deprecated any way. + for_machine = MachineChoice.BUILD + # Avoid mutating, since there could be other references to sources + sources = sources + kwargs['sources'] + if any(isinstance(s, build.BuildTarget) for s in sources): + FeatureBroken.single_use('passing references to built targets as a source file', '1.1.0', self.subproject, + 'Consider using `link_with` or `link_whole` if you meant to link, or dropping them as otherwise they are ignored.', + node) + if any(isinstance(s, build.ExtractedObjects) for s in sources): + FeatureBroken.single_use('passing object files as sources', '1.1.0', self.subproject, + 'Pass these to the `objects` keyword instead, they are ignored when passed as sources.', + node) + # Go ahead and drop these here, since they're only allowed through for + # backwards compatibility anyway + sources = [s for s in sources + if not isinstance(s, (build.BuildTarget, build.ExtractedObjects))] + + # due to lack of type checking, these are "allowed" for legacy reasons + if not isinstance(kwargs['install'], bool): + FeatureBroken.single_use('install kwarg with non-boolean value', '1.3.0', self.subproject, + 'This was never intended to work, and is essentially the same as using `install: true` regardless of value.', + node) + sources = self.source_strings_to_files(sources) - objs = extract_as_list(kwargs, 'objects') + objs = kwargs['objects'] kwargs['dependencies'] = extract_as_list(kwargs, 'dependencies') - kwargs['install_mode'] = self._get_kwarg_install_mode(kwargs) - if 'extra_files' in kwargs: - ef = extract_as_list(kwargs, 'extra_files') - kwargs['extra_files'] = self.source_strings_to_files(ef) + kwargs['extra_files'] = self.source_strings_to_files(kwargs['extra_files']) self.check_sources_exist(os.path.join(self.source_root, self.subdir), sources) if targetclass not in {build.Executable, build.SharedLibrary, build.SharedModule, build.StaticLibrary, build.Jar}: mlog.debug('Unknown target type:', str(targetclass)) raise RuntimeError('Unreachable code') - self.kwarg_strings_to_includedirs(kwargs) + self.__process_language_args(kwargs) + if targetclass is build.StaticLibrary: + for lang in compilers.all_languages - {'java'}: + deps, args = self.__convert_file_args(kwargs.get(f'{lang}_static_args', [])) + kwargs['language_args'][lang].extend(args) + kwargs['depend_files'].extend(deps) + elif targetclass is build.SharedLibrary: + for lang in compilers.all_languages - {'java'}: + deps, args = self.__convert_file_args(kwargs.get(f'{lang}_shared_args', [])) + kwargs['language_args'][lang].extend(args) + kwargs['depend_files'].extend(deps) + if targetclass is not build.Jar: + self.kwarg_strings_to_includedirs(kwargs) # Filter out kwargs from other target types. For example 'soversion' # passed to library() when default_library == 'static'. - kwargs = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs} + kwargs = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs | {'language_args'}} srcs: T.List['SourceInputs'] = [] struct: T.Optional[build.StructuredSources] = build.StructuredSources() @@ -3151,18 +3383,45 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey outputs.update(o) kwargs['include_directories'] = self.extract_incdirs(kwargs) + + if targetclass is build.Executable: + kwargs = T.cast('kwtypes.Executable', kwargs) + if kwargs['gui_app'] is not None: + if kwargs['win_subsystem'] is not None: + raise InvalidArguments.from_node( + 'Executable got both "gui_app", and "win_subsystem" arguments, which are mutually exclusive', + node=node) + if kwargs['gui_app']: + kwargs['win_subsystem'] = 'windows' + if kwargs['win_subsystem'] is None: + kwargs['win_subsystem'] = 'console' + + if kwargs['implib']: + if kwargs['export_dynamic'] is False: + FeatureDeprecated.single_use('implib overrides explict export_dynamic off', '1.3.0', self.subprojct, + 'Do not set ths if want export_dynamic disabled if implib is enabled', + location=node) + kwargs['export_dynamic'] = True + elif kwargs['export_dynamic']: + if kwargs['implib'] is False: + raise InvalidArguments('"implib" keyword" must not be false if "export_dynamic" is set and not false.') + kwargs['implib'] = True + if kwargs['export_dynamic'] is None: + kwargs['export_dynamic'] = False + if kwargs['implib'] is None: + kwargs['implib'] = False + target = targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs, self.environment, self.compilers[for_machine], kwargs) - target.project_version = self.project_version self.add_target(name, target) self.project_args_frozen = True return target - def kwarg_strings_to_includedirs(self, kwargs): - if 'd_import_dirs' in kwargs: - items = mesonlib.extract_as_list(kwargs, 'd_import_dirs') - cleaned_items = [] + def kwarg_strings_to_includedirs(self, kwargs: kwtypes._BuildTarget) -> None: + if kwargs['d_import_dirs']: + items = kwargs['d_import_dirs'] + cleaned_items: T.List[build.IncludeDirs] = [] for i in items: if isinstance(i, str): # BW compatibility. This was permitted so we must support it diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 00412a0..f13e3ff 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -21,7 +21,7 @@ from ..interpreterbase import ( typed_pos_args, typed_kwargs, typed_operator, noArgsFlattening, noPosargs, noKwargs, unholder_return, flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode) -from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW +from ..interpreter.type_checking import NoneType, ENV_KW, ENV_SEPARATOR_KW, PKGCONFIG_DEFINE_KW from ..dependencies import Dependency, ExternalLibrary, InternalDependency from ..programs import ExternalProgram from ..mesonlib import HoldableObject, OptionKey, listify, Popen_safe @@ -41,6 +41,8 @@ if T.TYPE_CHECKING: separator: str +_ERROR_MSG_KW: KwargInfo[T.Optional[str]] = KwargInfo('error_message', (str, NoneType)) + def extract_required_kwarg(kwargs: 'kwargs.ExtractRequired', subproject: 'SubProject', @@ -97,6 +99,9 @@ class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]): 'auto': self.auto_method, 'require': self.require_method, 'disable_auto_if': self.disable_auto_if_method, + 'enable_auto_if': self.enable_auto_if_method, + 'disable_if': self.disable_if_method, + 'enable_if': self.enable_if_method, }) @property @@ -108,6 +113,11 @@ class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]): disabled.value = 'disabled' return disabled + def as_enabled(self) -> coredata.UserFeatureOption: + enabled = copy.deepcopy(self.held_object) + enabled.value = 'enabled' + return enabled + @noPosargs @noKwargs def enabled_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: @@ -129,22 +139,51 @@ class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]): def auto_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: return self.value == 'auto' + def _disable_if(self, condition: bool, message: T.Optional[str]) -> coredata.UserFeatureOption: + if not condition: + return copy.deepcopy(self.held_object) + + if self.value == 'enabled': + err_msg = f'Feature {self.held_object.name} cannot be enabled' + if message: + err_msg += f': {message}' + raise InterpreterException(err_msg) + return self.as_disabled() + @FeatureNew('feature_option.require()', '0.59.0') @typed_pos_args('feature_option.require', bool) @typed_kwargs( 'feature_option.require', - KwargInfo('error_message', (str, NoneType)) + _ERROR_MSG_KW, ) def require_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption: - if args[0]: + return self._disable_if(not args[0], kwargs['error_message']) + + @FeatureNew('feature_option.disable_if()', '1.1.0') + @typed_pos_args('feature_option.disable_if', bool) + @typed_kwargs( + 'feature_option.disable_if', + _ERROR_MSG_KW, + ) + def disable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption: + return self._disable_if(args[0], kwargs['error_message']) + + @FeatureNew('feature_option.enable_if()', '1.1.0') + @typed_pos_args('feature_option.enable_if', bool) + @typed_kwargs( + 'feature_option.enable_if', + _ERROR_MSG_KW, + ) + def enable_if_method(self, args: T.Tuple[bool], kwargs: 'kwargs.FeatureOptionRequire') -> coredata.UserFeatureOption: + if not args[0]: return copy.deepcopy(self.held_object) - if self.value == 'enabled': - err_msg = f'Feature {self.held_object.name} cannot be enabled' + if self.value == 'disabled': + err_msg = f'Feature {self.held_object.name} cannot be disabled' if kwargs['error_message']: err_msg += f': {kwargs["error_message"]}' raise InterpreterException(err_msg) - return self.as_disabled() + return self.as_enabled() @FeatureNew('feature_option.disable_auto_if()', '0.59.0') @noKwargs @@ -152,13 +191,19 @@ class FeatureOptionHolder(ObjectHolder[coredata.UserFeatureOption]): def disable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> coredata.UserFeatureOption: return copy.deepcopy(self.held_object) if self.value != 'auto' or not args[0] else self.as_disabled() + @FeatureNew('feature_option.enable_auto_if()', '1.1.0') + @noKwargs + @typed_pos_args('feature_option.enable_auto_if', bool) + def enable_auto_if_method(self, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> coredata.UserFeatureOption: + return self.as_enabled() if self.value == 'auto' and args[0] else copy.deepcopy(self.held_object) + class RunProcess(MesonInterpreterObject): def __init__(self, cmd: ExternalProgram, args: T.List[str], - env: build.EnvironmentVariables, + env: mesonlib.EnvironmentVariables, source_dir: str, build_dir: str, subdir: str, @@ -179,7 +224,7 @@ class RunProcess(MesonInterpreterObject): def run_command(self, cmd: ExternalProgram, args: T.List[str], - env: build.EnvironmentVariables, + env: mesonlib.EnvironmentVariables, source_dir: str, build_dir: str, subdir: str, @@ -200,7 +245,7 @@ class RunProcess(MesonInterpreterObject): child_env.update(menv) child_env = env.get_env(child_env) stdout = subprocess.PIPE if self.capture else subprocess.DEVNULL - mlog.debug('Running command:', ' '.join(command_array)) + mlog.debug('Running command:', mesonlib.join_args(command_array)) try: p, o, e = Popen_safe(command_array, stdout=stdout, env=child_env, cwd=cwd) if self.capture: @@ -214,11 +259,11 @@ class RunProcess(MesonInterpreterObject): mlog.debug('') if check and p.returncode != 0: - raise InterpreterException('Command "{}" failed with status {}.'.format(' '.join(command_array), p.returncode)) + raise InterpreterException('Command `{}` failed with status {}.'.format(mesonlib.join_args(command_array), p.returncode)) return p.returncode, o, e except FileNotFoundError: - raise InterpreterException('Could not execute command "%s".' % ' '.join(command_array)) + raise InterpreterException('Could not execute command `%s`.' % mesonlib.join_args(command_array)) @noPosargs @noKwargs @@ -235,9 +280,9 @@ class RunProcess(MesonInterpreterObject): def stderr_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: return self.stderr -class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], MutableInterpreterObject): +class EnvironmentVariablesHolder(ObjectHolder[mesonlib.EnvironmentVariables], MutableInterpreterObject): - def __init__(self, obj: build.EnvironmentVariables, interpreter: 'Interpreter'): + def __init__(self, obj: mesonlib.EnvironmentVariables, interpreter: 'Interpreter'): super().__init__(obj, interpreter) self.methods.update({'set': self.set_method, 'append': self.append_method, @@ -442,17 +487,21 @@ class DependencyHolder(ObjectHolder[Dependency]): @typed_pos_args('dependency.get_pkgconfig_variable', str) @typed_kwargs( 'dependency.get_pkgconfig_variable', - KwargInfo('default', (str, NoneType)), - KwargInfo( - 'define_variable', - ContainerTypeInfo(list, str, pairs=True), - default=[], - listify=True, - validator=lambda x: 'must be of length 2 or empty' if len(x) not in {0, 2} else None, - ), + KwargInfo('default', str, default=''), + PKGCONFIG_DEFINE_KW.evolve(name='define_variable') ) def pkgconfig_method(self, args: T.Tuple[str], kwargs: 'kwargs.DependencyPkgConfigVar') -> str: - return self.held_object.get_pkgconfig_variable(args[0], **kwargs) + from ..dependencies.pkgconfig import PkgConfigDependency + if not isinstance(self.held_object, PkgConfigDependency): + raise InvalidArguments(f'{self.held_object.get_name()!r} is not a pkgconfig dependency') + if kwargs['define_variable'] and len(kwargs['define_variable']) > 1: + FeatureNew.single_use('dependency.get_pkgconfig_variable keyword argument "define_variable" with more than one pair', + '1.3.0', self.subproject, location=self.current_node) + return self.held_object.get_variable( + pkgconfig=args[0], + default_value=kwargs['default'], + pkgconfig_define=kwargs['define_variable'], + ) @FeatureNew('dependency.get_configtool_variable', '0.44.0') @FeatureDeprecated('dependency.get_configtool_variable', '0.56.0', @@ -460,7 +509,13 @@ class DependencyHolder(ObjectHolder[Dependency]): @noKwargs @typed_pos_args('dependency.get_config_tool_variable', str) def configtool_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> str: - return self.held_object.get_configtool_variable(args[0]) + from ..dependencies.configtool import ConfigToolDependency + if not isinstance(self.held_object, ConfigToolDependency): + raise InvalidArguments(f'{self.held_object.get_name()!r} is not a config-tool dependency') + return self.held_object.get_variable( + configtool=args[0], + default_value='', + ) @FeatureNew('dependency.partial_dependency', '0.46.0') @noPosargs @@ -478,12 +533,16 @@ class DependencyHolder(ObjectHolder[Dependency]): KwargInfo('configtool', (str, NoneType)), KwargInfo('internal', (str, NoneType), since='0.54.0'), KwargInfo('default_value', (str, NoneType)), - KwargInfo('pkgconfig_define', ContainerTypeInfo(list, str, pairs=True), default=[], listify=True), + PKGCONFIG_DEFINE_KW, ) def variable_method(self, args: T.Tuple[T.Optional[str]], kwargs: 'kwargs.DependencyGetVariable') -> str: default_varname = args[0] if default_varname is not None: FeatureNew('Positional argument to dependency.get_variable()', '0.58.0').use(self.subproject, self.current_node) + if kwargs['pkgconfig_define'] and len(kwargs['pkgconfig_define']) > 1: + FeatureNew.single_use('dependency.get_variable keyword argument "pkgconfig_define" with more than one pair', + '1.3.0', self.subproject, 'In previous versions, this silently returned a malformed value.', + self.current_node) return self.held_object.get_variable( cmake=kwargs['cmake'] or default_varname, pkgconfig=kwargs['pkgconfig'] or default_varname, @@ -514,8 +573,10 @@ class DependencyHolder(ObjectHolder[Dependency]): new_dep = self.held_object.generate_link_whole_dependency() return new_dep -class ExternalProgramHolder(ObjectHolder[ExternalProgram]): - def __init__(self, ep: ExternalProgram, interpreter: 'Interpreter') -> None: +_EXTPROG = T.TypeVar('_EXTPROG', bound=ExternalProgram) + +class _ExternalProgramHolder(ObjectHolder[_EXTPROG]): + def __init__(self, ep: _EXTPROG, interpreter: 'Interpreter') -> None: super().__init__(ep, interpreter) self.methods.update({'found': self.found_method, 'path': self.path_method, @@ -561,6 +622,9 @@ class ExternalProgramHolder(ObjectHolder[ExternalProgram]): def found(self) -> bool: return self.held_object.found() +class ExternalProgramHolder(_ExternalProgramHolder[ExternalProgram]): + pass + class ExternalLibraryHolder(ObjectHolder[ExternalLibrary]): def __init__(self, el: ExternalLibrary, interpreter: 'Interpreter'): super().__init__(el, interpreter) @@ -594,6 +658,8 @@ class MachineHolder(ObjectHolder['MachineInfo']): 'cpu': self.cpu_method, 'cpu_family': self.cpu_family_method, 'endian': self.endian_method, + 'kernel': self.kernel_method, + 'subsystem': self.subsystem_method, }) @noPosargs @@ -616,6 +682,21 @@ class MachineHolder(ObjectHolder['MachineInfo']): def endian_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: return self.held_object.endian + @noPosargs + @noKwargs + def kernel_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: + if self.held_object.kernel is not None: + return self.held_object.kernel + raise InterpreterException('Kernel not defined or could not be autodetected.') + + @noPosargs + @noKwargs + def subsystem_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: + if self.held_object.subsystem is not None: + return self.held_object.subsystem + raise InterpreterException('Subsystem not defined or could not be autodetected.') + + class IncludeDirsHolder(ObjectHolder[build.IncludeDirs]): pass @@ -649,7 +730,7 @@ class Test(MesonInterpreterObject): depends: T.List[T.Union[build.CustomTarget, build.BuildTarget]], is_parallel: bool, cmd_args: T.List[T.Union[str, mesonlib.File, build.Target]], - env: build.EnvironmentVariables, + env: mesonlib.EnvironmentVariables, should_fail: bool, timeout: int, workdir: T.Optional[str], protocol: str, priority: int, verbose: bool): super().__init__() @@ -686,7 +767,8 @@ class SubprojectHolder(MesonInterpreterObject): subdir: str, warnings: int = 0, disabled_feature: T.Optional[str] = None, - exception: T.Optional[Exception] = None) -> None: + exception: T.Optional[Exception] = None, + callstack: T.Optional[T.List[str]] = None) -> None: super().__init__() self.held_object = subinterpreter self.warnings = warnings @@ -694,6 +776,7 @@ class SubprojectHolder(MesonInterpreterObject): self.exception = exception self.subdir = PurePath(subdir).as_posix() self.cm_interpreter: T.Optional[CMakeInterpreter] = None + self.callstack = callstack self.methods.update({'get_variable': self.get_variable_method, 'found': self.found_method, }) @@ -840,6 +923,9 @@ class BuildTargetHolder(ObjectHolder[_BuildTarget]): @noPosargs @noKwargs + @FeatureDeprecated('BuildTarget.get_id', '1.2.0', + 'This was never formally documented and does not seem to have a real world use. ' + + 'See https://github.com/mesonbuild/meson/pull/6061') def get_id_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: return self._target_object.get_id() @@ -902,8 +988,10 @@ class CustomTargetIndexHolder(ObjectHolder[build.CustomTargetIndex]): assert self.interpreter.backend is not None return self.interpreter.backend.get_target_filename_abs(self.held_object) -class CustomTargetHolder(ObjectHolder[build.CustomTarget]): - def __init__(self, target: 'build.CustomTarget', interp: 'Interpreter'): +_CT = T.TypeVar('_CT', bound=build.CustomTarget) + +class _CustomTargetHolder(ObjectHolder[_CT]): + def __init__(self, target: _CT, interp: 'Interpreter'): super().__init__(target, interp) self.methods.update({'full_path': self.full_path_method, 'to_list': self.to_list_method, @@ -940,6 +1028,9 @@ class CustomTargetHolder(ObjectHolder[build.CustomTarget]): except IndexError: raise InvalidArguments(f'Index {other} out of bounds of custom target {self.held_object.name} output of size {len(self.held_object)}.') +class CustomTargetHolder(_CustomTargetHolder[build.CustomTarget]): + pass + class RunTargetHolder(ObjectHolder[build.RunTarget]): pass @@ -959,6 +1050,7 @@ class GeneratorHolder(ObjectHolder[build.Generator]): 'generator.process', KwargInfo('preserve_path_from', (str, NoneType), since='0.45.0'), KwargInfo('extra_args', ContainerTypeInfo(list, str), listify=True, default=[]), + ENV_KW.evolve(since='1.3.0') ) def process_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File, 'build.GeneratedTypes']]], @@ -976,7 +1068,7 @@ class GeneratorHolder(ObjectHolder[build.Generator]): '0.57.0', self.interpreter.subproject) gl = self.held_object.process_files(args[0], self.interpreter, - preserve_path_from, extra_args=kwargs['extra_args']) + preserve_path_from, extra_args=kwargs['extra_args'], env=kwargs['env']) return gl diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 1589c65..17f7876 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -1,20 +1,22 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright © 2021 The Meson Developers # Copyright © 2021 Intel Corporation +from __future__ import annotations """Keyword Argument type annotations.""" import typing as T -from typing_extensions import TypedDict, Literal, Protocol +from typing_extensions import TypedDict, Literal, Protocol, NotRequired from .. import build from .. import coredata from ..compilers import Compiler -from ..mesonlib import MachineChoice, File, FileMode, FileOrString +from ..dependencies.base import Dependency +from ..mesonlib import EnvironmentVariables, MachineChoice, File, FileMode, FileOrString, OptionKey from ..modules.cmake import CMakeSubprojectOptions from ..programs import ExternalProgram - +from .type_checking import PkgConfigDefineType, SourcesVarargsType class FuncAddProjectArgs(TypedDict): @@ -41,7 +43,7 @@ class BaseTest(TypedDict): workdir: T.Optional[str] depends: T.List[T.Union[build.CustomTarget, build.BuildTarget]] priority: int - env: build.EnvironmentVariables + env: EnvironmentVariables suite: T.List[str] @@ -56,7 +58,7 @@ class FuncTest(FuncBenchmark): """Keyword Arguments for `test` - `test` only adds the `is_prallel` argument over benchmark, so inherintance + `test` only adds the `is_parallel` argument over benchmark, so inheritance is helpful here. """ @@ -101,6 +103,7 @@ class GeneratorProcess(TypedDict): preserve_path_from: T.Optional[str] extra_args: T.List[str] + env: EnvironmentVariables class DependencyMethodPartialDependency(TypedDict): @@ -122,6 +125,7 @@ class FuncInstallSubdir(TypedDict): exclude_files: T.List[str] exclude_directories: T.List[str] install_mode: FileMode + follow_symlinks: T.Optional[bool] class FuncInstallData(TypedDict): @@ -130,6 +134,7 @@ class FuncInstallData(TypedDict): sources: T.List[FileOrString] rename: T.List[str] install_mode: FileMode + follow_symlinks: T.Optional[bool] class FuncInstallHeaders(TypedDict): @@ -137,6 +142,7 @@ class FuncInstallHeaders(TypedDict): install_dir: T.Optional[str] install_mode: FileMode subdir: T.Optional[str] + follow_symlinks: T.Optional[bool] class FuncInstallMan(TypedDict): @@ -163,7 +169,7 @@ class RunTarget(TypedDict): command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, ExternalProgram, File]] depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]] - env: build.EnvironmentVariables + env: EnvironmentVariables class CustomTarget(TypedDict): @@ -178,7 +184,7 @@ class CustomTarget(TypedDict): depend_files: T.List[FileOrString] depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]] depfile: T.Optional[str] - env: build.EnvironmentVariables + env: EnvironmentVariables feed: bool input: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, build.ExtractedObjects, build.GeneratedList, ExternalProgram, File]] @@ -195,14 +201,14 @@ class AddTestSetup(TypedDict): timeout_multiplier: int is_default: bool exclude_suites: T.List[str] - env: build.EnvironmentVariables + env: EnvironmentVariables class Project(TypedDict): version: T.Optional[FileOrString] meson_version: T.Optional[str] - default_options: T.List[str] + default_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] license: T.List[str] subproject_dir: str @@ -231,6 +237,7 @@ class Summary(TypedDict): class FindProgram(ExtractRequired, ExtractSearchDirs): + default_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] native: MachineChoice version: T.List[str] @@ -239,7 +246,7 @@ class RunCommand(TypedDict): check: bool capture: T.Optional[bool] - env: build.EnvironmentVariables + env: EnvironmentVariables class FeatureOptionRequire(TypedDict): @@ -250,7 +257,7 @@ class FeatureOptionRequire(TypedDict): class DependencyPkgConfigVar(TypedDict): default: T.Optional[str] - define_variable: T.List[str] + define_variable: PkgConfigDefineType class DependencyGetVariable(TypedDict): @@ -260,7 +267,7 @@ class DependencyGetVariable(TypedDict): configtool: T.Optional[str] internal: T.Optional[str] default_value: T.Optional[str] - pkgconfig_define: T.List[str] + pkgconfig_define: PkgConfigDefineType class ConfigurationDataSet(TypedDict): @@ -283,7 +290,7 @@ class ConfigureFile(TypedDict): output: str capture: bool format: T.Literal['meson', 'cmake', 'cmake@'] - output_format: T.Literal['c', 'nasm'] + output_format: T.Literal['c', 'json', 'nasm'] depfile: T.Optional[str] install: T.Optional[bool] install_dir: T.Union[str, T.Literal[False]] @@ -293,17 +300,178 @@ class ConfigureFile(TypedDict): command: T.Optional[T.List[T.Union[build.Executable, ExternalProgram, Compiler, File, str]]] input: T.List[FileOrString] configuration: T.Optional[T.Union[T.Dict[str, T.Union[str, int, bool]], build.ConfigurationData]] + macro_name: T.Optional[str] class Subproject(ExtractRequired): - default_options: T.List[str] + default_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] version: T.List[str] class DoSubproject(ExtractRequired): - default_options: T.List[str] + default_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] version: T.List[str] cmake_options: T.List[str] options: T.Optional[CMakeSubprojectOptions] + + +class _BaseBuildTarget(TypedDict): + + """Arguments used by all BuildTarget like functions. + + This really exists because Jar is so different than all of the other + BuildTarget functions. + """ + + build_by_default: bool + build_rpath: str + extra_files: T.List[FileOrString] + gnu_symbol_visibility: str + install: bool + install_mode: FileMode + install_rpath: str + implicit_include_directories: bool + link_depends: T.List[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex, build.BuildTarget]] + link_language: T.Optional[str] + name_prefix: T.Optional[str] + name_suffix: T.Optional[str] + native: MachineChoice + objects: T.List[build.ObjectTypes] + override_options: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] + depend_files: NotRequired[T.List[File]] + resources: T.List[str] + + +class _BuildTarget(_BaseBuildTarget): + + """Arguments shared by non-JAR functions""" + + d_debug: T.List[T.Union[str, int]] + d_import_dirs: T.List[T.Union[str, build.IncludeDirs]] + d_module_versions: T.List[T.Union[str, int]] + d_unittest: bool + rust_dependency_map: T.Dict[str, str] + sources: SourcesVarargsType + c_args: T.List[str] + cpp_args: T.List[str] + cuda_args: T.List[str] + fortran_args: T.List[str] + d_args: T.List[str] + objc_args: T.List[str] + objcpp_args: T.List[str] + rust_args: T.List[str] + vala_args: T.List[T.Union[str, File]] # Yes, Vala is really special + cs_args: T.List[str] + swift_args: T.List[str] + cython_args: T.List[str] + nasm_args: T.List[str] + masm_args: T.List[str] + + +class _LibraryMixin(TypedDict): + + rust_abi: T.Optional[Literal['c', 'rust']] + + +class Executable(_BuildTarget): + + export_dynamic: T.Optional[bool] + gui_app: T.Optional[bool] + implib: T.Optional[T.Union[str, bool]] + pie: T.Optional[bool] + vs_module_defs: T.Optional[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex]] + win_subsystem: T.Optional[str] + + +class _StaticLibMixin(TypedDict): + + prelink: bool + pic: T.Optional[bool] + + +class StaticLibrary(_BuildTarget, _StaticLibMixin, _LibraryMixin): + pass + + +class _SharedLibMixin(TypedDict): + + darwin_versions: T.Optional[T.Tuple[str, str]] + soversion: T.Optional[str] + version: T.Optional[str] + vs_module_defs: T.Optional[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex]] + + +class SharedLibrary(_BuildTarget, _SharedLibMixin, _LibraryMixin): + pass + + +class SharedModule(_BuildTarget, _LibraryMixin): + + vs_module_defs: T.Optional[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex]] + + +class Library(_BuildTarget, _SharedLibMixin, _StaticLibMixin, _LibraryMixin): + + """For library, both_library, and as a base for build_target""" + + c_static_args: NotRequired[T.List[str]] + c_shared_args: NotRequired[T.List[str]] + cpp_static_args: NotRequired[T.List[str]] + cpp_shared_args: NotRequired[T.List[str]] + cuda_static_args: NotRequired[T.List[str]] + cuda_shared_args: NotRequired[T.List[str]] + fortran_static_args: NotRequired[T.List[str]] + fortran_shared_args: NotRequired[T.List[str]] + d_static_args: NotRequired[T.List[str]] + d_shared_args: NotRequired[T.List[str]] + objc_static_args: NotRequired[T.List[str]] + objc_shared_args: NotRequired[T.List[str]] + objcpp_static_args: NotRequired[T.List[str]] + objcpp_shared_args: NotRequired[T.List[str]] + rust_static_args: NotRequired[T.List[str]] + rust_shared_args: NotRequired[T.List[str]] + vala_static_args: NotRequired[T.List[T.Union[str, File]]] # Yes, Vala is really special + vala_shared_args: NotRequired[T.List[T.Union[str, File]]] # Yes, Vala is really special + cs_static_args: NotRequired[T.List[str]] + cs_shared_args: NotRequired[T.List[str]] + swift_static_args: NotRequired[T.List[str]] + swift_shared_args: NotRequired[T.List[str]] + cython_static_args: NotRequired[T.List[str]] + cython_shared_args: NotRequired[T.List[str]] + nasm_static_args: NotRequired[T.List[str]] + nasm_shared_args: NotRequired[T.List[str]] + masm_static_args: NotRequired[T.List[str]] + masm_shared_args: NotRequired[T.List[str]] + + +class BuildTarget(Library): + + target_type: Literal['executable', 'shared_library', 'static_library', + 'shared_module', 'both_libraries', 'library', 'jar'] + + +class Jar(_BaseBuildTarget): + + main_class: str + java_resources: T.Optional[build.StructuredSources] + sources: T.Union[str, File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList, build.ExtractedObjects, build.BuildTarget] + java_args: T.List[str] + + +class FuncDeclareDependency(TypedDict): + + compile_args: T.List[str] + d_import_dirs: T.List[T.Union[build.IncludeDirs, str]] + d_module_versions: T.List[T.Union[str, int]] + dependencies: T.List[Dependency] + extra_files: T.List[FileOrString] + include_directories: T.List[T.Union[build.IncludeDirs, str]] + link_args: T.List[str] + link_whole: T.List[T.Union[build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]] + link_with: T.List[build.LibTypes] + objects: T.List[build.ExtractedObjects] + sources: T.List[T.Union[FileOrString, build.GeneratedTypes]] + variables: T.Dict[str, str] + version: T.Optional[str] diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py index 01d0029..66029a0 100644 --- a/mesonbuild/interpreter/mesonmain.py +++ b/mesonbuild/interpreter/mesonmain.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2012-2021 The Meson development team # Copyright © 2021 Intel Corporation +from __future__ import annotations import os import typing as T @@ -8,7 +9,7 @@ import typing as T from .. import mesonlib from .. import dependencies from .. import build -from .. import mlog +from .. import mlog, coredata from ..mesonlib import MachineChoice, OptionKey from ..programs import OverrideProgram, ExternalProgram @@ -20,14 +21,13 @@ from .primitives import MesonVersionString from .type_checking import NATIVE_KW, NoneType if T.TYPE_CHECKING: - from typing_extensions import Literal - from ..backend.backends import ExecutableSerialisation + from typing_extensions import Literal, TypedDict + from ..compilers import Compiler from ..interpreterbase import TYPE_kwargs, TYPE_var + from ..mesonlib import ExecutableSerialisation from .interpreter import Interpreter - from typing_extensions import TypedDict - class FuncOverrideDependency(TypedDict): native: mesonlib.MachineChoice @@ -37,6 +37,7 @@ if T.TYPE_CHECKING: skip_if_destdir: bool install_tag: str + dry_run: bool class NativeKW(TypedDict): @@ -52,35 +53,37 @@ class MesonMain(MesonInterpreterObject): super().__init__(subproject=interpreter.subproject) self.build = build self.interpreter = interpreter - self.methods.update({'get_compiler': self.get_compiler_method, - 'is_cross_build': self.is_cross_build_method, - 'has_exe_wrapper': self.has_exe_wrapper_method, + self.methods.update({'add_devenv': self.add_devenv_method, + 'add_dist_script': self.add_dist_script_method, + 'add_install_script': self.add_install_script_method, + 'add_postconf_script': self.add_postconf_script_method, + 'backend': self.backend_method, + 'build_options': self.build_options_method, + 'build_root': self.build_root_method, 'can_run_host_binaries': self.can_run_host_binaries_method, - 'is_unity': self.is_unity_method, - 'is_subproject': self.is_subproject_method, 'current_source_dir': self.current_source_dir_method, 'current_build_dir': self.current_build_dir_method, - 'source_root': self.source_root_method, - 'build_root': self.build_root_method, - 'project_source_root': self.project_source_root_method, - 'project_build_root': self.project_build_root_method, - 'global_source_root': self.global_source_root_method, + 'get_compiler': self.get_compiler_method, + 'get_cross_property': self.get_cross_property_method, + 'get_external_property': self.get_external_property_method, 'global_build_root': self.global_build_root_method, - 'add_install_script': self.add_install_script_method, - 'add_postconf_script': self.add_postconf_script_method, - 'add_dist_script': self.add_dist_script_method, + 'global_source_root': self.global_source_root_method, + 'has_exe_wrapper': self.has_exe_wrapper_method, + 'has_external_property': self.has_external_property_method, 'install_dependency_manifest': self.install_dependency_manifest_method, + 'is_cross_build': self.is_cross_build_method, + 'is_subproject': self.is_subproject_method, + 'is_unity': self.is_unity_method, 'override_dependency': self.override_dependency_method, 'override_find_program': self.override_find_program_method, - 'project_version': self.project_version_method, + 'project_build_root': self.project_build_root_method, 'project_license': self.project_license_method, - 'version': self.version_method, + 'project_license_files': self.project_license_files_method, 'project_name': self.project_name_method, - 'get_cross_property': self.get_cross_property_method, - 'get_external_property': self.get_external_property_method, - 'has_external_property': self.has_external_property_method, - 'backend': self.backend_method, - 'add_devenv': self.add_devenv_method, + 'project_source_root': self.project_source_root_method, + 'project_version': self.project_version_method, + 'source_root': self.source_root_method, + 'version': self.version_method, }) def _find_source_script( @@ -100,7 +103,7 @@ class MesonMain(MesonInterpreterObject): largs.append(found) largs.extend(args) - es = self.interpreter.backend.get_executable_serialisation(largs) + es = self.interpreter.backend.get_executable_serialisation(largs, verbose=True) es.subproject = self.interpreter.subproject return es @@ -150,6 +153,7 @@ class MesonMain(MesonInterpreterObject): 'meson.add_install_script', KwargInfo('skip_if_destdir', bool, default=False, since='0.57.0'), KwargInfo('install_tag', (str, NoneType), since='0.60.0'), + KwargInfo('dry_run', bool, default=False, since='1.1.0'), ) def add_install_script_method( self, @@ -160,6 +164,7 @@ class MesonMain(MesonInterpreterObject): script = self._find_source_script('add_install_script', args[0], script_args) script.skip_if_destdir = kwargs['skip_if_destdir'] script.tag = kwargs['install_tag'] + script.dry_run = kwargs['dry_run'] self.build.install_scripts.append(script) @typed_pos_args( @@ -371,7 +376,7 @@ class MesonMain(MesonInterpreterObject): def _override_dependency_impl(self, name: str, dep: dependencies.Dependency, kwargs: 'FuncOverrideDependency', static: T.Optional[bool], permissive: bool = False) -> None: # We need the cast here as get_dep_identifier works on such a dict, - # which FuncOverrideDependency is, but mypy can't fgure that out + # which FuncOverrideDependency is, but mypy can't figure that out nkwargs = T.cast('T.Dict[str, T.Any]', kwargs.copy()) if static is None: del nkwargs['static'] @@ -400,6 +405,12 @@ class MesonMain(MesonInterpreterObject): def project_license_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]: return self.build.dep_manifest[self.interpreter.active_projectname].license + @FeatureNew('meson.project_license_files()', '1.1.0') + @noPosargs + @noKwargs + def project_license_files_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.List[mesonlib.File]: + return [l[1] for l in self.build.dep_manifest[self.interpreter.active_projectname].license_files] + @noPosargs @noKwargs def version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> MesonVersionString: @@ -444,13 +455,22 @@ class MesonMain(MesonInterpreterObject): @FeatureNew('add_devenv', '0.58.0') @typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0')) - @typed_pos_args('add_devenv', (str, list, dict, build.EnvironmentVariables)) - def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.EnvironmentVariables]], + @typed_pos_args('add_devenv', (str, list, dict, mesonlib.EnvironmentVariables)) + def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, mesonlib.EnvironmentVariables]], kwargs: 'AddDevenvKW') -> None: env = args[0] msg = ENV_KW.validator(env) if msg: raise build.InvalidArguments(f'"add_devenv": {msg}') converted = env_convertor_with_method(env, kwargs['method'], kwargs['separator']) - assert isinstance(converted, build.EnvironmentVariables) + assert isinstance(converted, mesonlib.EnvironmentVariables) self.build.devenv.append(converted) + + @noPosargs + @noKwargs + @FeatureNew('meson.build_options', '1.1.0') + def build_options_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: + options = self.interpreter.user_defined_options + if options is None: + return '' + return coredata.format_cmd_line_options(options) diff --git a/mesonbuild/interpreter/primitives/integer.py b/mesonbuild/interpreter/primitives/integer.py index f433f57..50def2c 100644 --- a/mesonbuild/interpreter/primitives/integer.py +++ b/mesonbuild/interpreter/primitives/integer.py @@ -3,13 +3,8 @@ from __future__ import annotations from ...interpreterbase import ( - ObjectHolder, - MesonOperator, - typed_operator, - noKwargs, - noPosargs, - - InvalidArguments + FeatureBroken, InvalidArguments, MesonOperator, ObjectHolder, KwargInfo, + noKwargs, noPosargs, typed_operator, typed_kwargs ) import typing as T @@ -53,6 +48,13 @@ class IntegerHolder(ObjectHolder[int]): def display_name(self) -> str: return 'int' + def operator_call(self, operator: MesonOperator, other: TYPE_var) -> TYPE_var: + if isinstance(other, bool): + FeatureBroken.single_use('int operations with non-int', '1.2.0', self.subproject, + 'It is not commutative and only worked because of leaky Python abstractions.', + location=self.current_node) + return super().operator_call(operator, other) + @noKwargs @noPosargs def is_even_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: @@ -63,10 +65,13 @@ class IntegerHolder(ObjectHolder[int]): def is_odd_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool: return self.held_object % 2 != 0 - @noKwargs + @typed_kwargs( + 'to_string', + KwargInfo('fill', int, default=0, since='1.3.0') + ) @noPosargs - def to_string_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: - return str(self.held_object) + def to_string_method(self, args: T.List[TYPE_var], kwargs: T.Dict[str, T.Any]) -> str: + return str(self.held_object).zfill(kwargs['fill']) @typed_operator(MesonOperator.DIV, int) def op_div(self, other: int) -> int: diff --git a/mesonbuild/interpreter/primitives/range.py b/mesonbuild/interpreter/primitives/range.py index dd99205..5eb5e03 100644 --- a/mesonbuild/interpreter/primitives/range.py +++ b/mesonbuild/interpreter/primitives/range.py @@ -1,5 +1,6 @@ # Copyright 2021 The Meson development team # SPDX-license-identifier: Apache-2.0 +from __future__ import annotations import typing as T diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py index d9f6a06..bc98934 100644 --- a/mesonbuild/interpreter/primitives/string.py +++ b/mesonbuild/interpreter/primitives/string.py @@ -17,8 +17,9 @@ from ...interpreterbase import ( noKwargs, noPosargs, typed_pos_args, - InvalidArguments, + FeatureBroken, + stringifyUserArguments, ) @@ -38,6 +39,7 @@ class StringHolder(ObjectHolder[str]): 'join': self.join_method, 'replace': self.replace_method, 'split': self.split_method, + 'splitlines': self.splitlines_method, 'strip': self.strip_method, 'substring': self.substring_method, 'to_int': self.to_int_method, @@ -89,12 +91,14 @@ class StringHolder(ObjectHolder[str]): @noArgsFlattening @noKwargs @typed_pos_args('str.format', varargs=object) - def format_method(self, args: T.Tuple[T.List[object]], kwargs: TYPE_kwargs) -> str: + def format_method(self, args: T.Tuple[T.List[TYPE_var]], kwargs: TYPE_kwargs) -> str: arg_strings: T.List[str] = [] for arg in args[0]: - if isinstance(arg, bool): # Python boolean is upper case. - arg = str(arg).lower() - arg_strings.append(str(arg)) + try: + arg_strings.append(stringifyUserArguments(arg, self.subproject)) + except InvalidArguments as e: + FeatureBroken.single_use(f'str.format: {str(e)}', '1.3.0', self.subproject, location=self.current_node) + arg_strings.append(str(arg)) def arg_replace(match: T.Match[str]) -> str: idx = int(match.group(1)) @@ -105,11 +109,18 @@ class StringHolder(ObjectHolder[str]): return re.sub(r'@(\d+)@', arg_replace, self.held_object) @noKwargs + @noPosargs + @FeatureNew('str.splitlines', '1.2.0') + def splitlines_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> T.List[str]: + return self.held_object.splitlines() + + @noKwargs @typed_pos_args('str.join', varargs=str) def join_method(self, args: T.Tuple[T.List[str]], kwargs: TYPE_kwargs) -> str: return self.held_object.join(args[0]) @noKwargs + @FeatureNew('str.replace', '0.58.0') @typed_pos_args('str.replace', str, str) def replace_method(self, args: T.Tuple[str, str], kwargs: TYPE_kwargs) -> str: return self.held_object.replace(args[0], args[1]) @@ -122,9 +133,12 @@ class StringHolder(ObjectHolder[str]): @noKwargs @typed_pos_args('str.strip', optargs=[str]) def strip_method(self, args: T.Tuple[T.Optional[str]], kwargs: TYPE_kwargs) -> str: + if args[0]: + FeatureNew.single_use('str.strip with a positional argument', '0.43.0', self.subproject, location=self.current_node) return self.held_object.strip(args[0]) @noKwargs + @FeatureNew('str.substring', '0.56.0') @typed_pos_args('str.substring', optargs=[int, int]) def substring_method(self, args: T.Tuple[T.Optional[int], T.Optional[int]], kwargs: TYPE_kwargs) -> str: start = args[0] if args[0] is not None else 0 diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index 0783c2c..616f4ef 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -4,20 +4,18 @@ """Helpers for strict type checking.""" from __future__ import annotations -import os +import itertools, os, re import typing as T from .. import compilers from ..build import (CustomTarget, BuildTarget, CustomTargetIndex, ExtractedObjects, GeneratedList, IncludeDirs, - BothLibraries, SharedLibrary, StaticLibrary, Jar, Executable) + BothLibraries, SharedLibrary, StaticLibrary, Jar, Executable, StructuredSources) from ..coredata import UserFeatureOption from ..dependencies import Dependency, InternalDependency -from ..interpreterbase import FeatureNew from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo -from ..mesonlib import ( - File, FileMode, MachineChoice, listify, has_path_sep, OptionKey, - EnvInitValueType, EnvironmentVariables) +from ..mesonlib import (File, FileMode, MachineChoice, listify, has_path_sep, + OptionKey, EnvironmentVariables) from ..programs import ExternalProgram # Helper definition for type checks that are `Optional[T]` @@ -26,8 +24,14 @@ NoneType: T.Type[None] = type(None) if T.TYPE_CHECKING: from typing_extensions import Literal + from ..build import ObjectTypes from ..interpreterbase import TYPE_var - from ..interpreterbase.decorators import FeatureCheckBase + from ..mesonlib import EnvInitValueType + + _FullEnvInitValueType = T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], EnvInitValueType, str, None] + PkgConfigDefineType = T.Optional[T.Tuple[T.Tuple[str, str], ...]] + SourcesVarargsType = T.List[T.Union[str, File, CustomTarget, CustomTargetIndex, GeneratedList, StructuredSources, ExtractedObjects, BuildTarget]] + def in_set_validator(choices: T.Set[str]) -> T.Callable[[str], T.Optional[str]]: """Check that the choice given was one of the given set.""" @@ -86,23 +90,27 @@ def _install_mode_validator(mode: T.List[T.Union[str, bool, int]]) -> T.Optional return f'permission character 9 must be "-", "t", "T", or "x", not {perms[8]}' if len(mode) >= 2 and not isinstance(mode[1], (int, str, bool)): - return 'second componenent can only be a string, number, or False' + return 'second component can only be a string, number, or False' if len(mode) >= 3 and not isinstance(mode[2], (int, str, bool)): - return 'third componenent can only be a string, number, or False' + return 'third component can only be a string, number, or False' return None def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) -> FileMode: - """Convert the DSL form of the `install_mode` keyword argument to `FileMode` + """Convert the DSL form of the `install_mode` keyword argument to `FileMode`""" - This is not required, and if not required returns None + if not mode: + return FileMode() - TODO: It's not clear to me why this needs to be None and not just return an - empty FileMode. - """ - # this has already been validated by the validator - return FileMode(*(m if isinstance(m, str) else None for m in mode)) + # This has already been validated by the validator. False denotes "use + # default". mypy is totally incapable of understanding it, because + # generators clobber types via homogeneous return. But also we *must* + # convert the first element different from the rest + m1 = mode[0] if isinstance(mode[0], str) else None + rest = (m if isinstance(m, (str, int)) else None for m in mode[1:]) + + return FileMode(m1, *rest) def _lower_strlist(input: T.List[str]) -> T.List[str]: @@ -113,6 +121,13 @@ def _lower_strlist(input: T.List[str]) -> T.List[str]: return [i.lower() for i in input] +def _validate_shlib_version(val: T.Optional[str]) -> T.Optional[str]: + if val is not None and not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', val): + return (f'Invalid Shared library version "{val}". ' + 'Must be of the form X.Y.Z where all three are numbers. Y and Z are optional.') + return None + + def variables_validator(contents: T.Union[str, T.List[str], T.Dict[str, str]]) -> T.Optional[str]: if isinstance(contents, str): contents = [contents] @@ -179,7 +194,7 @@ REQUIRED_KW: KwargInfo[T.Union[bool, UserFeatureOption]] = KwargInfo( DISABLER_KW: KwargInfo[bool] = KwargInfo('disabler', bool, default=False) def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None], - allow_dict_list: bool = True) -> T.Optional[str]: + only_dict_str: bool = True) -> T.Optional[str]: def _splitter(v: str) -> T.Optional[str]: split = v.split('=', 1) if len(split) == 1: @@ -200,18 +215,21 @@ def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Di elif isinstance(value, dict): # We don't need to spilt here, just do the type checking for k, dv in value.items(): - if allow_dict_list: + if only_dict_str: if any(i for i in listify(dv) if not isinstance(i, str)): return f"Dictionary element {k} must be a string or list of strings not {dv!r}" - elif not isinstance(dv, str): - return f"Dictionary element {k} must be a string not {dv!r}" + elif isinstance(dv, list): + if any(not isinstance(i, str) for i in dv): + return f"Dictionary element {k} must be a string, bool, integer or list of strings, not {dv!r}" + elif not isinstance(dv, (str, bool, int)): + return f"Dictionary element {k} must be a string, bool, integer or list of strings, not {dv!r}" # We know that otherwise we have an EnvironmentVariables object or None, and # we're okay at this point return None def _options_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None]) -> T.Optional[str]: - # Reusing the env validator is a littl overkill, but nicer than duplicating the code - return _env_validator(value, allow_dict_list=False) + # Reusing the env validator is a little overkill, but nicer than duplicating the code + return _env_validator(value, only_dict_str=False) def split_equal_string(input: str) -> T.Tuple[str, str]: """Split a string in the form `x=y` @@ -221,8 +239,6 @@ def split_equal_string(input: str) -> T.Tuple[str, str]: a, b = input.split('=', 1) return (a, b) -_FullEnvInitValueType = T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], EnvInitValueType, str, None] - # Split _env_convertor() and env_convertor_with_method() to make mypy happy. # It does not want extra arguments in KwargInfo convertor callable. def env_convertor_with_method(value: _FullEnvInitValueType, @@ -278,25 +294,36 @@ COMMAND_KW: KwargInfo[T.List[T.Union[str, BuildTarget, CustomTarget, CustomTarge default=[], ) -def _override_options_convertor(raw: T.List[str]) -> T.Dict[OptionKey, str]: - output: T.Dict[OptionKey, str] = {} - for each in raw: - k, v = split_equal_string(each) - output[OptionKey.from_string(k)] = v - return output +def _override_options_convertor(raw: T.Union[str, T.List[str], T.Dict[str, T.Union[str, int, bool, T.List[str]]]]) -> T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]]: + if isinstance(raw, str): + raw = [raw] + if isinstance(raw, list): + output: T.Dict[OptionKey, T.Union[str, int, bool, T.List[str]]] = {} + for each in raw: + k, v = split_equal_string(each) + output[OptionKey.from_string(k)] = v + return output + return {OptionKey.from_string(k): v for k, v in raw.items()} -OVERRIDE_OPTIONS_KW: KwargInfo[T.List[str]] = KwargInfo( +OVERRIDE_OPTIONS_KW: KwargInfo[T.Union[str, T.Dict[str, T.Union[str, int, bool, T.List[str]]], T.List[str]]] = KwargInfo( 'override_options', - ContainerTypeInfo(list, str), - listify=True, - default=[], + (str, ContainerTypeInfo(list, str), ContainerTypeInfo(dict, (str, int, bool, list))), + default={}, validator=_options_validator, convertor=_override_options_convertor, + since_values={dict: '1.2.0'}, ) def _output_validator(outputs: T.List[str]) -> T.Optional[str]: + output_set = set(outputs) + if len(output_set) != len(outputs): + seen = set() + for el in outputs: + if el in seen: + return f"contains {el!r} multiple times, but no duplicates are allowed." + seen.add(el) for i in outputs: if i == '': return 'Output must not be empty.' @@ -343,6 +370,12 @@ CT_INSTALL_TAG_KW: KwargInfo[T.List[T.Union[str, bool]]] = KwargInfo( INSTALL_TAG_KW: KwargInfo[T.Optional[str]] = KwargInfo('install_tag', (str, NoneType)) +INSTALL_FOLLOW_SYMLINKS: KwargInfo[T.Optional[bool]] = KwargInfo( + 'follow_symlinks', + (bool, NoneType), + since='1.3.0', +) + INSTALL_KW = KwargInfo('install', bool, default=False) CT_INSTALL_DIR_KW: KwargInfo[T.List[T.Union[str, Literal[False]]]] = KwargInfo( @@ -375,21 +408,7 @@ INCLUDE_DIRECTORIES: KwargInfo[T.List[T.Union[str, IncludeDirs]]] = KwargInfo( default=[], ) -def include_dir_string_new(val: T.List[T.Union[str, IncludeDirs]]) -> T.Iterable[FeatureCheckBase]: - strs = [v for v in val if isinstance(v, str)] - if strs: - str_msg = ", ".join(f"'{s}'" for s in strs) - yield FeatureNew('include_directories kwarg of type string', '1.0.0', - f'Use include_directories({str_msg}) instead') - -# for cases like default_options and override_options -DEFAULT_OPTIONS: KwargInfo[T.List[str]] = KwargInfo( - 'default_options', - ContainerTypeInfo(list, str), - listify=True, - default=[], - validator=_options_validator, -) +DEFAULT_OPTIONS = OVERRIDE_OPTIONS_KW.evolve(name='default_options') ENV_METHOD_KW = KwargInfo('method', str, default='set', since='0.62.0', validator=in_set_validator({'set', 'prepend', 'append'})) @@ -439,13 +458,22 @@ LINK_WHOLE_KW: KwargInfo[T.List[T.Union[BothLibraries, StaticLibrary, CustomTarg validator=link_whole_validator, ) -SOURCES_KW: KwargInfo[T.List[T.Union[str, File, CustomTarget, CustomTargetIndex, GeneratedList]]] = KwargInfo( +DEPENDENCY_SOURCES_KW: KwargInfo[T.List[T.Union[str, File, CustomTarget, CustomTargetIndex, GeneratedList]]] = KwargInfo( 'sources', ContainerTypeInfo(list, (str, File, CustomTarget, CustomTargetIndex, GeneratedList)), listify=True, default=[], ) +SOURCES_VARARGS = (str, File, CustomTarget, CustomTargetIndex, GeneratedList, StructuredSources, ExtractedObjects, BuildTarget) + +BT_SOURCES_KW: KwargInfo[SourcesVarargsType] = KwargInfo( + 'sources', + (NoneType, ContainerTypeInfo(list, SOURCES_VARARGS)), + listify=True, + default=[], +) + VARIABLES_KW: KwargInfo[T.Dict[str, str]] = KwargInfo( 'variables', # str is listified by validator/convertor, cannot use listify=True here because @@ -476,3 +504,352 @@ TEST_KWS: T.List[KwargInfo] = [ KwargInfo('suite', ContainerTypeInfo(list, str), listify=True, default=['']), # yes, a list of empty string KwargInfo('verbose', bool, default=False, since='0.62.0'), ] + +# Cannot have a default value because we need to check that rust_crate_type and +# rust_abi are mutually exclusive. +RUST_CRATE_TYPE_KW: KwargInfo[T.Union[str, None]] = KwargInfo( + 'rust_crate_type', (str, NoneType), + since='0.42.0', + since_values={'proc-macro': '0.62.0'}, + deprecated='1.3.0', + deprecated_message='Use rust_abi or rust.proc_macro() instead.', + validator=in_set_validator({'bin', 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'})) + +RUST_ABI_KW: KwargInfo[T.Union[str, None]] = KwargInfo( + 'rust_abi', (str, NoneType), + since='1.3.0', + validator=in_set_validator({'rust', 'c'})) + +_VS_MODULE_DEFS_KW: KwargInfo[T.Optional[T.Union[str, File, CustomTarget, CustomTargetIndex]]] = KwargInfo( + 'vs_module_defs', + (str, File, CustomTarget, CustomTargetIndex, NoneType), + since_values={CustomTargetIndex: '1.3.0'} +) + +_BASE_LANG_KW: KwargInfo[T.List[str]] = KwargInfo( + 'UNKNOWN', + ContainerTypeInfo(list, (str)), + listify=True, + default=[], +) + +_LANGUAGE_KWS: T.List[KwargInfo[T.List[str]]] = [ + _BASE_LANG_KW.evolve(name=f'{lang}_args') + for lang in compilers.all_languages - {'rust', 'vala', 'java'} +] +# Cannot use _BASE_LANG_KW here because Vala is special for types +_LANGUAGE_KWS.append(KwargInfo( + 'vala_args', ContainerTypeInfo(list, (str, File)), listify=True, default=[])) +_LANGUAGE_KWS.append(_BASE_LANG_KW.evolve(name='rust_args', since='0.41.0')) + +# We need this deprecated values more than the non-deprecated values. So we'll evolve them out elsewhere. +_JAVA_LANG_KW: KwargInfo[T.List[str]] = _BASE_LANG_KW.evolve( + name='java_args', + deprecated='1.3.0', + deprecated_message='This does not, and never has, done anything. It should be removed' +) + +def _objects_validator(vals: T.List[ObjectTypes]) -> T.Optional[str]: + non_objects: T.List[str] = [] + + for val in vals: + if isinstance(val, (str, File, ExtractedObjects)): + continue + else: + non_objects.extend(o for o in val.get_outputs() if not compilers.is_object(o)) + + if non_objects: + return f'{", ".join(non_objects)!r} are not objects' + + return None + + +# Applies to all build_target like classes +_ALL_TARGET_KWS: T.List[KwargInfo] = [ + OVERRIDE_OPTIONS_KW, + KwargInfo('build_by_default', bool, default=True, since='0.38.0'), + KwargInfo('extra_files', ContainerTypeInfo(list, (str, File)), default=[], listify=True), + # Accursed. We allow this for backwards compat and warn in the interpreter. + KwargInfo('install', object, default=False), + INSTALL_MODE_KW, + KwargInfo('implicit_include_directories', bool, default=True, since='0.42.0'), + NATIVE_KW, + KwargInfo('resources', ContainerTypeInfo(list, str), default=[], listify=True), + KwargInfo( + 'objects', + ContainerTypeInfo(list, (str, File, CustomTarget, CustomTargetIndex, GeneratedList, ExtractedObjects)), + listify=True, + default=[], + validator=_objects_validator, + since_values={ + ContainerTypeInfo(list, (GeneratedList, CustomTarget, CustomTargetIndex)): + ('1.1.0', 'generated sources as positional "objects" arguments') + }, + ), +] + + +def _name_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str]: + if isinstance(arg, list) and arg: + return 'must be empty when passed as an array to signify the default value.' + return None + + +def _name_suffix_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str]: + if arg == '': + return 'must not be a empty string. An empty array may be passed if you want Meson to use the default behavior.' + return _name_validator(arg) + + +_NAME_PREFIX_KW: KwargInfo[T.Optional[T.Union[str, T.List]]] = KwargInfo( + 'name_prefix', + (str, NoneType, list), + validator=_name_validator, + convertor=lambda x: None if isinstance(x, list) else x, +) + + +# Applies to all build_target classes except jar +_BUILD_TARGET_KWS: T.List[KwargInfo] = [ + *_ALL_TARGET_KWS, + *_LANGUAGE_KWS, + BT_SOURCES_KW, + INCLUDE_DIRECTORIES.evolve(name='d_import_dirs'), + _NAME_PREFIX_KW, + _NAME_PREFIX_KW.evolve(name='name_suffix', validator=_name_suffix_validator), + RUST_CRATE_TYPE_KW, + KwargInfo('d_debug', ContainerTypeInfo(list, (str, int)), default=[], listify=True), + D_MODULE_VERSIONS_KW, + KwargInfo('d_unittest', bool, default=False), + KwargInfo( + 'rust_dependency_map', + ContainerTypeInfo(dict, str), + default={}, + since='1.2.0', + ), + KwargInfo('build_rpath', str, default='', since='0.42.0'), + KwargInfo( + 'gnu_symbol_visibility', + str, + default='', + validator=in_set_validator({'', 'default', 'internal', 'hidden', 'protected', 'inlineshidden'}), + since='0.48.0', + ), + KwargInfo('install_rpath', str, default=''), + KwargInfo( + 'link_depends', + ContainerTypeInfo(list, (str, File, CustomTarget, CustomTargetIndex, BuildTarget)), + default=[], + listify=True, + ), + KwargInfo( + 'link_language', + (str, NoneType), + validator=in_set_validator(set(compilers.all_languages)), + since='0.51.0', + ), +] + +def _validate_win_subsystem(value: T.Optional[str]) -> T.Optional[str]: + if value is not None: + if re.fullmatch(r'(boot_application|console|efi_application|efi_boot_service_driver|efi_rom|efi_runtime_driver|native|posix|windows)(,\d+(\.\d+)?)?', value) is None: + return f'Invalid value for win_subsystem: {value}.' + return None + + +def _validate_darwin_versions(darwin_versions: T.List[T.Union[str, int]]) -> T.Optional[str]: + if len(darwin_versions) > 2: + return f"Must contain between 0 and 2 elements, not {len(darwin_versions)}" + if len(darwin_versions) == 1: + darwin_versions = 2 * darwin_versions + for v in darwin_versions: + if isinstance(v, int): + v = str(v) + if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', v): + return 'must be X.Y.Z where X, Y, Z are numbers, and Y and Z are optional' + try: + parts = v.split('.') + except ValueError: + return f'badly formed value: "{v}, not in X.Y.Z form' + if len(parts) in {1, 2, 3} and int(parts[0]) > 65535: + return 'must be X.Y.Z where X is [0, 65535] and Y, Z are optional' + if len(parts) in {2, 3} and int(parts[1]) > 255: + return 'must be X.Y.Z where Y is [0, 255] and Y, Z are optional' + if len(parts) == 3 and int(parts[2]) > 255: + return 'must be X.Y.Z where Z is [0, 255] and Y, Z are optional' + return None + + +def _convert_darwin_versions(val: T.List[T.Union[str, int]]) -> T.Optional[T.Tuple[str, str]]: + if not val: + return None + elif len(val) == 1: + v = str(val[0]) + return (v, v) + return (str(val[0]), str(val[1])) + + +_DARWIN_VERSIONS_KW: KwargInfo[T.List[T.Union[str, int]]] = KwargInfo( + 'darwin_versions', + ContainerTypeInfo(list, (str, int)), + default=[], + listify=True, + validator=_validate_darwin_versions, + convertor=_convert_darwin_versions, + since='0.48.0', +) + +# Arguments exclusive to Executable. These are separated to make integrating +# them into build_target easier +_EXCLUSIVE_EXECUTABLE_KWS: T.List[KwargInfo] = [ + KwargInfo('export_dynamic', (bool, NoneType), since='0.45.0'), + KwargInfo('gui_app', (bool, NoneType), deprecated='0.56.0', deprecated_message="Use 'win_subsystem' instead"), + KwargInfo('implib', (bool, str, NoneType), since='0.42.0'), + KwargInfo('pie', (bool, NoneType)), + KwargInfo( + 'win_subsystem', + (str, NoneType), + convertor=lambda x: x.lower() if isinstance(x, str) else None, + validator=_validate_win_subsystem, + ), +] + +# The total list of arguments used by Executable +EXECUTABLE_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_EXECUTABLE_KWS, + _VS_MODULE_DEFS_KW.evolve(since='1.3.0', since_values=None), + _JAVA_LANG_KW, +] + +# Arguments exclusive to library types +_EXCLUSIVE_LIB_KWS: T.List[KwargInfo] = [ + RUST_ABI_KW, +] + +# Arguments exclusive to StaticLibrary. These are separated to make integrating +# them into build_target easier +_EXCLUSIVE_STATIC_LIB_KWS: T.List[KwargInfo] = [ + KwargInfo('prelink', bool, default=False, since='0.57.0'), + KwargInfo('pic', (bool, NoneType), since='0.36.0'), +] + +# The total list of arguments used by StaticLibrary +STATIC_LIB_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_STATIC_LIB_KWS, + *_EXCLUSIVE_LIB_KWS, + _JAVA_LANG_KW, +] + +# Arguments exclusive to SharedLibrary. These are separated to make integrating +# them into build_target easier +_EXCLUSIVE_SHARED_LIB_KWS: T.List[KwargInfo] = [ + _DARWIN_VERSIONS_KW, + KwargInfo('soversion', (str, int, NoneType), convertor=lambda x: str(x) if x is not None else None), + KwargInfo('version', (str, NoneType), validator=_validate_shlib_version), +] + +# The total list of arguments used by SharedLibrary +SHARED_LIB_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_SHARED_LIB_KWS, + *_EXCLUSIVE_LIB_KWS, + _VS_MODULE_DEFS_KW, + _JAVA_LANG_KW, +] + +# Arguments exclusive to SharedModule. These are separated to make integrating +# them into build_target easier +_EXCLUSIVE_SHARED_MOD_KWS: T.List[KwargInfo] = [] + +# The total list of arguments used by SharedModule +SHARED_MOD_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_SHARED_MOD_KWS, + *_EXCLUSIVE_LIB_KWS, + _VS_MODULE_DEFS_KW, + _JAVA_LANG_KW, +] + +# Arguments exclusive to JAR. These are separated to make integrating +# them into build_target easier +_EXCLUSIVE_JAR_KWS: T.List[KwargInfo] = [ + KwargInfo('main_class', str, default=''), + KwargInfo('java_resources', (StructuredSources, NoneType), since='0.62.0'), + _JAVA_LANG_KW.evolve(deprecated=None, deprecated_message=None), +] + +# The total list of arguments used by JAR +JAR_KWS = [ + *_ALL_TARGET_KWS, + *_EXCLUSIVE_JAR_KWS, + KwargInfo( + 'sources', + ContainerTypeInfo(list, (str, File, CustomTarget, CustomTargetIndex, GeneratedList, ExtractedObjects, BuildTarget)), + listify=True, + default=[], + ), + *[a.evolve(deprecated='1.3.0', deprecated_message='This argument has never done anything in jar(), and should be removed') + for a in _LANGUAGE_KWS], +] + +_SHARED_STATIC_ARGS: T.List[KwargInfo[T.List[str]]] = [ + *[l.evolve(name=l.name.replace('_', '_static_'), since='1.3.0') + for l in _LANGUAGE_KWS], + *[l.evolve(name=l.name.replace('_', '_shared_'), since='1.3.0') + for l in _LANGUAGE_KWS], +] + +# Arguments used by both_library and library +LIBRARY_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_LIB_KWS, + *_EXCLUSIVE_SHARED_LIB_KWS, + *_EXCLUSIVE_SHARED_MOD_KWS, + *_EXCLUSIVE_STATIC_LIB_KWS, + *_SHARED_STATIC_ARGS, + _VS_MODULE_DEFS_KW, + _JAVA_LANG_KW, +] + +# Arguments used by build_Target +BUILD_TARGET_KWS = [ + *_BUILD_TARGET_KWS, + *_EXCLUSIVE_SHARED_LIB_KWS, + *_EXCLUSIVE_SHARED_MOD_KWS, + *_EXCLUSIVE_STATIC_LIB_KWS, + *_EXCLUSIVE_EXECUTABLE_KWS, + *_SHARED_STATIC_ARGS, + *[a.evolve(deprecated='1.3.0', deprecated_message='The use of "jar" in "build_target()" is deprecated, and this argument is only used by jar()') + for a in _EXCLUSIVE_JAR_KWS], + KwargInfo( + 'target_type', + str, + required=True, + validator=in_set_validator({ + 'executable', 'shared_library', 'static_library', 'shared_module', + 'both_libraries', 'library', 'jar' + }), + since_values={ + 'shared_module': '0.51.0', + }, + deprecated_values={ + 'jar': ('1.3.0', 'use the "jar()" function directly'), + } + ) +] + +def _pkgconfig_define_convertor(x: T.List[str]) -> PkgConfigDefineType: + if x: + keys = itertools.islice(x, 0, None, 2) + vals = itertools.islice(x, 1, None, 2) + return tuple(zip(keys, vals)) + return None + +PKGCONFIG_DEFINE_KW: KwargInfo = KwargInfo( + 'pkgconfig_define', + ContainerTypeInfo(list, str, pairs=True), + default=[], + convertor=_pkgconfig_define_convertor, +) diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index 13f55e5..8a2e078 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -18,6 +18,7 @@ __all__ = [ 'ObjectHolder', 'IterableObject', 'MutableInterpreterObject', + 'ContextManagerObject', 'MesonOperator', @@ -34,6 +35,7 @@ __all__ = [ 'default_resolve_key', 'flatten', 'resolve_second_level_holders', + 'stringifyUserArguments', 'noPosargs', 'noKwargs', @@ -44,7 +46,6 @@ __all__ = [ 'disablerIfNotFound', 'permittedKwargs', 'typed_operator', - 'unary_operator', 'typed_pos_args', 'ContainerTypeInfo', 'KwargInfo', @@ -52,6 +53,7 @@ __all__ = [ 'FeatureCheckBase', 'FeatureNew', 'FeatureDeprecated', + 'FeatureBroken', 'FeatureNewKwargs', 'FeatureDeprecatedKwargs', @@ -59,9 +61,6 @@ __all__ = [ 'SubProject', - 'TV_fw_var', - 'TV_fw_args', - 'TV_fw_kwargs', 'TV_func', 'TYPE_elementary', 'TYPE_var', @@ -80,10 +79,8 @@ from .baseobjects import ( ObjectHolder, IterableObject, MutableInterpreterObject, + ContextManagerObject, - TV_fw_var, - TV_fw_args, - TV_fw_kwargs, TV_func, TYPE_elementary, TYPE_var, @@ -111,11 +108,11 @@ from .decorators import ( ContainerTypeInfo, KwargInfo, typed_operator, - unary_operator, typed_kwargs, FeatureCheckBase, FeatureNew, FeatureDeprecated, + FeatureBroken, FeatureNewKwargs, FeatureDeprecatedKwargs, ) @@ -130,6 +127,11 @@ from .exceptions import ( ) from .disabler import Disabler, is_disabled -from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .helpers import ( + default_resolve_key, + flatten, + resolve_second_level_holders, + stringifyUserArguments, +) from .interpreterbase import InterpreterBase from .operator import MesonOperator diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index a65b053..38b181e 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .. import mparser from .exceptions import InvalidCode, InvalidArguments @@ -21,6 +22,7 @@ import textwrap import typing as T from abc import ABCMeta +from contextlib import AbstractContextManager if T.TYPE_CHECKING: from typing_extensions import Protocol @@ -33,9 +35,6 @@ if T.TYPE_CHECKING: class OperatorCall(Protocol[__T]): def __call__(self, other: __T) -> 'TYPE_var': ... -TV_fw_var = T.Union[str, int, bool, list, dict, 'InterpreterObject'] -TV_fw_args = T.List[T.Union[mparser.BaseNode, TV_fw_var]] -TV_fw_kwargs = T.Dict[str, T.Union[mparser.BaseNode, TV_fw_var]] TV_func = T.TypeVar('TV_func', bound=T.Callable[..., T.Any]) @@ -117,12 +116,12 @@ class InterpreterObject: # We use `type(...) == type(...)` here to enforce an *exact* match for comparison. We # don't want comparisons to be possible where `isinstance(derived_obj, type(base_obj))` # would pass because this comparison must never be true: `derived_obj == base_obj` - if type(self) != type(other): + if type(self) is not type(other): self._throw_comp_exception(other, '==') return self == other def op_not_equals(self, other: TYPE_var) -> bool: - if type(self) != type(other): + if type(self) is not type(other): self._throw_comp_exception(other, '!=') return self != other @@ -155,12 +154,12 @@ class ObjectHolder(InterpreterObject, T.Generic[InterpreterObjectTypeVar]): # Override default comparison operators for the held object def op_equals(self, other: TYPE_var) -> bool: # See the comment from InterpreterObject why we are using `type()` here. - if type(self.held_object) != type(other): + if type(self.held_object) is not type(other): self._throw_comp_exception(other, '==') return self.held_object == other def op_not_equals(self, other: TYPE_var) -> bool: - if type(self.held_object) != type(other): + if type(self.held_object) is not type(other): self._throw_comp_exception(other, '!=') return self.held_object != other @@ -179,3 +178,7 @@ class IterableObject(metaclass=ABCMeta): def size(self) -> int: raise MesonBugException(f'size not implemented for {self.__class__.__name__}') + +class ContextManagerObject(MesonInterpreterObject, AbstractContextManager): + def __init__(self, subproject: 'SubProject') -> None: + super().__init__(subproject=subproject) diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index 173cedc..17c6c8c 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -29,8 +29,7 @@ if T.TYPE_CHECKING: from typing_extensions import Protocol from .. import mparser - from .baseobjects import InterpreterObject, TV_func, TYPE_var, TYPE_kwargs - from .interpreterbase import SubProject + from .baseobjects import InterpreterObject, SubProject, TV_func, TYPE_var, TYPE_kwargs from .operator import MesonOperator _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True) @@ -143,22 +142,6 @@ def typed_operator(operator: MesonOperator, return T.cast('_TV_FN_Operator', wrapper) return inner -def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']: - """Decorator that does type checking for unary operator calls. - - This decorator is for unary operators that do not take any other objects. - It should be impossible for a user to accidentally break this. Triggering - this check always indicates a bug in the Meson interpreter. - """ - def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator': - @wraps(f) - def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var: - if other is not None: - raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}') - return f(self, other) - return T.cast('_TV_FN_Operator', wrapper) - return inner - def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None, @@ -271,7 +254,7 @@ def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]], diff = num_types + len(optargs) - num_args nargs[i] = tuple(list(args) + [None] * diff) else: - nargs[i] = args + nargs[i] = tuple(args) else: nargs[i] = tuple(args) return f(*nargs, **wrapped_kwargs) @@ -313,15 +296,25 @@ class ContainerTypeInfo: if not isinstance(value, self.container): return False iter_ = iter(value.values()) if isinstance(value, dict) else iter(value) - for each in iter_: - if not isinstance(each, self.contains): - return False + if any(not isinstance(i, self.contains) for i in iter_): + return False if self.pairs and len(value) % 2 != 0: return False if not value and not self.allow_empty: return False return True + def check_any(self, value: T.Any) -> bool: + """Check a value should emit new/deprecated feature. + + :param value: A value to check + :return: True if any of the items in value matches, False otherwise + """ + if not isinstance(value, self.container): + return False + iter_ = iter(value.values()) if isinstance(value, dict) else iter(value) + return any(isinstance(i, self.contains) for i in iter_) + def description(self) -> str: """Human readable description of this container type. @@ -382,7 +375,6 @@ class KwargInfo(T.Generic[_T]): added in. :param not_set_warning: A warning message that is logged if the kwarg is not set by the user. - :param feature_validator: A callable returning an iterable of FeatureNew | FeatureDeprecated objects. """ def __init__(self, name: str, types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo], @@ -390,11 +382,10 @@ class KwargInfo(T.Generic[_T]): default: T.Optional[_T] = None, since: T.Optional[str] = None, since_message: T.Optional[str] = None, - since_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None, + since_values: T.Optional[T.Dict[T.Union[_T, ContainerTypeInfo, type], T.Union[str, T.Tuple[str, str]]]] = None, deprecated: T.Optional[str] = None, deprecated_message: T.Optional[str] = None, - deprecated_values: T.Optional[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]]] = None, - feature_validator: T.Optional[T.Callable[[_T], T.Iterable[FeatureCheckBase]]] = None, + deprecated_values: T.Optional[T.Dict[T.Union[_T, ContainerTypeInfo, type], T.Union[str, T.Tuple[str, str]]]] = None, validator: T.Optional[T.Callable[[T.Any], T.Optional[str]]] = None, convertor: T.Optional[T.Callable[[_T], object]] = None, not_set_warning: T.Optional[str] = None): @@ -406,7 +397,6 @@ class KwargInfo(T.Generic[_T]): self.since = since self.since_message = since_message self.since_values = since_values - self.feature_validator = feature_validator self.deprecated = deprecated self.deprecated_message = deprecated_message self.deprecated_values = deprecated_values @@ -421,11 +411,10 @@ class KwargInfo(T.Generic[_T]): default: T.Union[_T, None, _NULL_T] = _NULL, since: T.Union[str, None, _NULL_T] = _NULL, since_message: T.Union[str, None, _NULL_T] = _NULL, - since_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, + since_values: T.Union[T.Dict[T.Union[_T, ContainerTypeInfo, type], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, deprecated: T.Union[str, None, _NULL_T] = _NULL, deprecated_message: T.Union[str, None, _NULL_T] = _NULL, - deprecated_values: T.Union[T.Dict[T.Union[_T, T.Type[T.List], T.Type[T.Dict]], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, - feature_validator: T.Union[T.Callable[[_T], T.Iterable[FeatureCheckBase]], None, _NULL_T] = _NULL, + deprecated_values: T.Union[T.Dict[T.Union[_T, ContainerTypeInfo, type], T.Union[str, T.Tuple[str, str]]], None, _NULL_T] = _NULL, validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL, convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo': """Create a shallow copy of this KwargInfo, with modifications. @@ -451,7 +440,6 @@ class KwargInfo(T.Generic[_T]): deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated, deprecated_message=deprecated_message if not isinstance(deprecated_message, _NULL_T) else self.deprecated_message, deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values, - feature_validator=feature_validator if not isinstance(feature_validator, _NULL_T) else self.feature_validator, validator=validator if not isinstance(validator, _NULL_T) else self.validator, convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor, ) @@ -465,7 +453,7 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T information. For non-required values it sets the value to a default, which means the value will always be provided. - If type tyhpe is a :class:ContainerTypeInfo, then the default value will be + If type is a :class:ContainerTypeInfo, then the default value will be passed as an argument to the container initializer, making a shallow copy :param name: the name of the function, including the object it's attached to @@ -512,23 +500,28 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T def emit_feature_change(values: T.Dict[_T, T.Union[str, T.Tuple[str, str]]], feature: T.Union[T.Type['FeatureDeprecated'], T.Type['FeatureNew']]) -> None: for n, version in values.items(): - warn = False if isinstance(version, tuple): version, msg = version else: msg = None - if n in {dict, list}: - assert isinstance(n, type), 'for mypy' + warning: T.Optional[str] = None + if isinstance(n, ContainerTypeInfo): + if n.check_any(value): + warning = f'of type {n.description()}' + elif isinstance(n, type): if isinstance(value, n): - feature.single_use(f'"{name}" keyword argument "{info.name}" of type {n.__name__}', version, subproject, msg, location=node) - elif isinstance(value, (dict, list)): - warn = n in value - else: - warn = n == value - - if warn: - feature.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject, msg, location=node) + warning = f'of type {n.__name__}' + elif isinstance(value, list): + if n in value: + warning = f'value "{n}" in list' + elif isinstance(value, dict): + if n in value.keys(): + warning = f'value "{n}" in dict keys' + elif n == value: + warning = f'value "{n}"' + if warning: + feature.single_use(f'"{name}" keyword argument "{info.name}" {warning}', version, subproject, msg, location=node) node, _, _kwargs, subproject = get_callee_args(wrapped_args) # Cast here, as the convertor function may place something other than a TYPE_var in the kwargs @@ -562,10 +555,6 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T if msg is not None: raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}') - if info.feature_validator is not None: - for each in info.feature_validator(value): - each.use(subproject, node) - if info.deprecated_values is not None: emit_feature_change(info.deprecated_values, FeatureDeprecated) @@ -577,7 +566,7 @@ def typed_kwargs(name: str, *types: KwargInfo, allow_unknown: bool = False) -> T else: # set the value to the default, this ensuring all kwargs are present # This both simplifies the typing checking and the usage - assert check_value_type(types_tuple, info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description(types_tuple)}' + assert check_value_type(types_tuple, info.default), f'In function {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description(types_tuple)}' # Create a shallow copy of the container. This allows mutable # types to be used safely as default values kwargs[info.name] = copy.copy(info.default) @@ -598,11 +587,12 @@ class FeatureCheckBase(metaclass=abc.ABCMeta): feature_registry: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional['mparser.BaseNode']]]]]] emit_notice = False + unconditional = False def __init__(self, feature_name: str, feature_version: str, extra_message: str = ''): - self.feature_name = feature_name # type: str - self.feature_version = feature_version # type: str - self.extra_message = extra_message # type: str + self.feature_name = feature_name + self.feature_version = feature_version + self.extra_message = extra_message @staticmethod def get_target_version(subproject: str) -> str: @@ -619,7 +609,7 @@ class FeatureCheckBase(metaclass=abc.ABCMeta): def use(self, subproject: 'SubProject', location: T.Optional['mparser.BaseNode'] = None) -> None: tv = self.get_target_version(subproject) # No target version - if tv == '': + if tv == '' and not self.unconditional: return # Target version is new enough, don't warn if self.check_version(tv, self.feature_version) and not self.emit_notice: @@ -652,10 +642,11 @@ class FeatureCheckBase(metaclass=abc.ABCMeta): fv = cls.feature_registry[subproject] tv = cls.get_target_version(subproject) for version in sorted(fv.keys()): + message = ', '.join(sorted({f"'{i[0]}'" for i in fv[version]})) if cls.check_version(tv, version): - notice_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]}) + notice_str += '\n * {}: {{{}}}'.format(version, message) else: - warning_str += '\n * {}: {}'.format(version, {i[0] for i in fv[version]}) + warning_str += '\n * {}: {{{}}}'.format(version, message) if '\n' in notice_str: mlog.notice(notice_str, fatal=False) if '\n' in warning_str: @@ -695,7 +686,7 @@ class FeatureNew(FeatureCheckBase): # Class variable, shared across all instances # # Format: {subproject: {feature_version: set(feature_names)}} - feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]] + feature_registry = {} @staticmethod def check_version(target_version: str, feature_version: str) -> bool: @@ -726,7 +717,7 @@ class FeatureDeprecated(FeatureCheckBase): # Class variable, shared across all instances # # Format: {subproject: {feature_version: set(feature_names)}} - feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[T.Tuple[str, T.Optional[mparser.BaseNode]]]]]] + feature_registry = {} emit_notice = True @staticmethod @@ -754,6 +745,40 @@ class FeatureDeprecated(FeatureCheckBase): mlog.warning(*args, location=location) +class FeatureBroken(FeatureCheckBase): + """Checks for broken features""" + + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} + unconditional = True + + @staticmethod + def check_version(target_version: str, feature_version: str) -> bool: + # always warn for broken stuff + return False + + @staticmethod + def get_warning_str_prefix(tv: str) -> str: + return 'Broken features used:' + + @staticmethod + def get_notice_str_prefix(tv: str) -> str: + return '' + + def log_usage_warning(self, tv: str, location: T.Optional['mparser.BaseNode']) -> None: + args = [ + 'Project uses feature that was always broken,', + 'and is now deprecated since', + f"'{self.feature_version}':", + f'{self.feature_name}.', + ] + if self.extra_message: + args.append(self.extra_message) + mlog.deprecation(*args, location=location) + + # This cannot be a dataclass due to https://github.com/python/mypy/issues/5374 class FeatureCheckKwargsBase(metaclass=abc.ABCMeta): diff --git a/mesonbuild/interpreterbase/helpers.py b/mesonbuild/interpreterbase/helpers.py index 12fa813..917969b 100644 --- a/mesonbuild/interpreterbase/helpers.py +++ b/mesonbuild/interpreterbase/helpers.py @@ -11,18 +11,21 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .. import mesonlib, mparser -from .exceptions import InterpreterException +from .exceptions import InterpreterException, InvalidArguments +from ..coredata import UserOption + import collections.abc import typing as T if T.TYPE_CHECKING: - from .baseobjects import TYPE_var, TYPE_kwargs + from .baseobjects import TYPE_var, TYPE_kwargs, SubProject def flatten(args: T.Union['TYPE_var', T.List['TYPE_var']]) -> T.List['TYPE_var']: - if isinstance(args, mparser.StringNode): + if isinstance(args, mparser.BaseStringNode): assert isinstance(args.value, str) return [args.value] if not isinstance(args, collections.abc.Sequence): @@ -32,7 +35,7 @@ def flatten(args: T.Union['TYPE_var', T.List['TYPE_var']]) -> T.List['TYPE_var'] if isinstance(a, list): rest = flatten(a) result = result + rest - elif isinstance(a, mparser.StringNode): + elif isinstance(a, mparser.BaseStringNode): result.append(a.value) else: result.append(a) @@ -53,3 +56,22 @@ def default_resolve_key(key: mparser.BaseNode) -> str: if not isinstance(key, mparser.IdNode): raise InterpreterException('Invalid kwargs format.') return key.value + +def stringifyUserArguments(args: TYPE_var, subproject: SubProject, quote: bool = False) -> str: + if isinstance(args, str): + return f"'{args}'" if quote else args + elif isinstance(args, bool): + return 'true' if args else 'false' + elif isinstance(args, int): + return str(args) + elif isinstance(args, list): + return '[%s]' % ', '.join([stringifyUserArguments(x, subproject, True) for x in args]) + elif isinstance(args, dict): + l = ['{} : {}'.format(stringifyUserArguments(k, subproject, True), + stringifyUserArguments(v, subproject, True)) for k, v in args.items()] + return '{%s}' % ', '.join(l) + elif isinstance(args, UserOption): + from .decorators import FeatureNew + FeatureNew.single_use('User option in string format', '1.3.0', subproject) + return stringifyUserArguments(args.printable_value(), subproject) + raise InvalidArguments('Value other than strings, integers, bools, options, dictionaries and lists thereof.') diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 76fb465..315bdf4 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -16,34 +16,31 @@ # or an interpreter-based tool. from __future__ import annotations -from .. import mparser, mesonlib -from .. import environment +from .. import environment, mparser, mesonlib from .baseobjects import ( InterpreterObject, MesonInterpreterObject, MutableInterpreterObject, - InterpreterObjectTypeVar, ObjectHolder, IterableObject, - - TYPE_var, + ContextManagerObject, HoldableTypes, ) from .exceptions import ( + BreakRequest, + ContinueRequest, InterpreterException, - InvalidCode, InvalidArguments, + InvalidCode, SubdirDoneRequest, - ContinueRequest, - BreakRequest ) from .decorators import FeatureNew from .disabler import Disabler, is_disabled -from .helpers import default_resolve_key, flatten, resolve_second_level_holders +from .helpers import default_resolve_key, flatten, resolve_second_level_holders, stringifyUserArguments from .operator import MesonOperator from ._unholder import _unholder @@ -52,26 +49,33 @@ import typing as T import textwrap if T.TYPE_CHECKING: - from .baseobjects import SubProject, TYPE_kwargs + from .baseobjects import InterpreterObjectTypeVar, SubProject, TYPE_kwargs, TYPE_var from ..interpreter import Interpreter -HolderMapType = T.Dict[ - T.Union[ - T.Type[mesonlib.HoldableObject], - T.Type[int], - T.Type[bool], - T.Type[str], - T.Type[list], - T.Type[dict], - ], - # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar] - T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]] -] - -FunctionType = T.Dict[ - str, - T.Callable[[mparser.BaseNode, T.List[TYPE_var], T.Dict[str, TYPE_var]], TYPE_var] -] + HolderMapType = T.Dict[ + T.Union[ + T.Type[mesonlib.HoldableObject], + T.Type[int], + T.Type[bool], + T.Type[str], + T.Type[list], + T.Type[dict], + ], + # For some reason, this has to be a callable and can't just be ObjectHolder[InterpreterObjectTypeVar] + T.Callable[[InterpreterObjectTypeVar, 'Interpreter'], ObjectHolder[InterpreterObjectTypeVar]] + ] + + FunctionType = T.Dict[ + str, + T.Callable[[mparser.BaseNode, T.List[TYPE_var], T.Dict[str, TYPE_var]], TYPE_var] + ] + + +class InvalidCodeOnVoid(InvalidCode): + + def __init__(self, op_type: str) -> None: + super().__init__(f'Cannot perform {op_type!r} operation on void statement.') + class InterpreterBase: def __init__(self, source_root: str, subdir: str, subproject: 'SubProject'): @@ -89,12 +93,16 @@ class InterpreterBase: self.current_lineno = -1 # Current node set during a function call. This can be used as location # when printing a warning message during a method call. - self.current_node = None # type: mparser.BaseNode + self.current_node: mparser.BaseNode = None # This is set to `version_string` when this statement is evaluated: # meson.version().compare_version(version_string) # If it was part of a if-clause, it is used to temporally override the # current meson version target within that if-block. - self.tmp_meson_version = None # type: T.Optional[str] + self.tmp_meson_version: T.Optional[str] = None + + def handle_meson_version_from_ast(self, strict: bool = True) -> None: + # do nothing in an AST interpreter + return def load_root_meson_file(self) -> None: mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) @@ -107,8 +115,13 @@ class InterpreterBase: assert isinstance(code, str) try: self.ast = mparser.Parser(code, mesonfile).parse() - except mesonlib.MesonException as me: + self.handle_meson_version_from_ast() + except mparser.ParseException as me: me.file = mesonfile + # try to detect parser errors from new syntax added by future + # meson versions, and just tell the user to update meson + self.ast = me.ast + self.handle_meson_version_from_ast() raise me def parse_project(self) -> None: @@ -119,20 +132,30 @@ class InterpreterBase: self.evaluate_codeblock(self.ast, end=1) def sanity_check_ast(self) -> None: - if not isinstance(self.ast, mparser.CodeBlockNode): - raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') - if not self.ast.lines: - raise InvalidCode('No statements in code.') - first = self.ast.lines[0] - if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': + def _is_project(ast: mparser.CodeBlockNode) -> object: + if not isinstance(ast, mparser.CodeBlockNode): + raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') + if not ast.lines: + raise InvalidCode('No statements in code.') + first = ast.lines[0] + return isinstance(first, mparser.FunctionNode) and first.func_name.value == 'project' + + if not _is_project(self.ast): p = pathlib.Path(self.source_root).resolve() found = p for parent in p.parents: if (parent / 'meson.build').is_file(): with open(parent / 'meson.build', encoding='utf-8') as f: - if f.readline().startswith('project('): - found = parent - break + code = f.read() + + try: + ast = mparser.Parser(code, 'empty').parse() + except mparser.ParseException: + continue + + if _is_project(ast): + found = parent + break else: break @@ -168,8 +191,9 @@ class InterpreterBase: except Exception as e: if getattr(e, 'lineno', None) is None: # We are doing the equivalent to setattr here and mypy does not like it - e.lineno = cur.lineno # type: ignore - e.colno = cur.colno # type: ignore + # NOTE: self.current_node is continually updated during processing + e.lineno = self.current_node.lineno # type: ignore + e.colno = self.current_node.colno # type: ignore e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) # type: ignore raise e i += 1 # In THE FUTURE jump over blocks and stuff. @@ -178,12 +202,19 @@ class InterpreterBase: self.current_node = cur if isinstance(cur, mparser.FunctionNode): return self.function_call(cur) + elif isinstance(cur, mparser.PlusAssignmentNode): + self.evaluate_plusassign(cur) elif isinstance(cur, mparser.AssignmentNode): self.assignment(cur) elif isinstance(cur, mparser.MethodNode): return self.method_call(cur) - elif isinstance(cur, mparser.StringNode): - return self._holderify(cur.value) + elif isinstance(cur, mparser.BaseStringNode): + if isinstance(cur, mparser.MultilineFormatStringNode): + return self.evaluate_multiline_fstring(cur) + elif isinstance(cur, mparser.FormatStringNode): + return self.evaluate_fstring(cur) + else: + return self._holderify(cur.value) elif isinstance(cur, mparser.BooleanNode): return self._holderify(cur.value) elif isinstance(cur, mparser.IfClauseNode): @@ -210,21 +241,18 @@ class InterpreterBase: return self.evaluate_arithmeticstatement(cur) elif isinstance(cur, mparser.ForeachClauseNode): self.evaluate_foreach(cur) - elif isinstance(cur, mparser.PlusAssignmentNode): - self.evaluate_plusassign(cur) elif isinstance(cur, mparser.IndexNode): return self.evaluate_indexing(cur) elif isinstance(cur, mparser.TernaryNode): return self.evaluate_ternary(cur) - elif isinstance(cur, mparser.FormatStringNode): - if isinstance(cur, mparser.MultilineFormatStringNode): - return self.evaluate_multiline_fstring(cur) - else: - return self.evaluate_fstring(cur) elif isinstance(cur, mparser.ContinueNode): raise ContinueRequest() elif isinstance(cur, mparser.BreakNode): raise BreakRequest() + elif isinstance(cur, mparser.ParenthesizedNode): + return self.evaluate_statement(cur.inner) + elif isinstance(cur, mparser.TestCaseClauseNode): + return self.evaluate_testcase(cur) else: raise InvalidCode("Unknown statement.") return None @@ -238,9 +266,12 @@ class InterpreterBase: @FeatureNew('dict', '0.47.0') def evaluate_dictstatement(self, cur: mparser.DictNode) -> InterpreterObject: def resolve_key(key: mparser.BaseNode) -> str: - if not isinstance(key, mparser.StringNode): + if not isinstance(key, mparser.BaseStringNode): FeatureNew.single_use('Dictionary entry using non literal key', '0.53.0', self.subproject) - str_key = _unholder(self.evaluate_statement(key)) + key_holder = self.evaluate_statement(key) + if key_holder is None: + raise InvalidArguments('Key cannot be void.') + str_key = _unholder(key_holder) if not isinstance(str_key, str): raise InvalidArguments('Key must be a string') return str_key @@ -250,6 +281,8 @@ class InterpreterBase: def evaluate_notstatement(self, cur: mparser.NotNode) -> InterpreterObject: v = self.evaluate_statement(cur.value) + if v is None: + raise InvalidCodeOnVoid('not') if isinstance(v, Disabler): return v return self._holderify(v.operator_call(MesonOperator.NOT, None)) @@ -261,10 +294,12 @@ class InterpreterBase: # statement evaluation. self.tmp_meson_version = None result = self.evaluate_statement(i.condition) + if result is None: + raise InvalidCodeOnVoid('if') if isinstance(result, Disabler): return result if not isinstance(result, InterpreterObject): - raise mesonlib.MesonBugException(f'Argument to not ({result}) is not an InterpreterObject but {type(result).__name__}.') + raise mesonlib.MesonBugException(f'Argument to if ({result}) is not an InterpreterObject but {type(result).__name__}.') res = result.operator_call(MesonOperator.BOOL, None) if not isinstance(res, bool): raise InvalidCode(f'If clause {result!r} does not evaluate to true or false.') @@ -278,14 +313,28 @@ class InterpreterBase: mesonlib.project_meson_versions[self.subproject] = prev_meson_version return None if not isinstance(node.elseblock, mparser.EmptyNode): - self.evaluate_codeblock(node.elseblock) + self.evaluate_codeblock(node.elseblock.block) + return None + + def evaluate_testcase(self, node: mparser.TestCaseClauseNode) -> T.Optional[Disabler]: + result = self.evaluate_statement(node.condition) + if isinstance(result, Disabler): + return result + if not isinstance(result, ContextManagerObject): + raise InvalidCode(f'testcase clause {result!r} does not evaluate to a context manager.') + with result: + self.evaluate_codeblock(node.block) return None def evaluate_comparison(self, node: mparser.ComparisonNode) -> InterpreterObject: val1 = self.evaluate_statement(node.left) + if val1 is None: + raise mesonlib.MesonException('Cannot compare a void statement on the left-hand side') if isinstance(val1, Disabler): return val1 val2 = self.evaluate_statement(node.right) + if val2 is None: + raise mesonlib.MesonException('Cannot compare a void statement on the right-hand side') if isinstance(val2, Disabler): return val2 @@ -310,30 +359,40 @@ class InterpreterBase: def evaluate_andstatement(self, cur: mparser.AndNode) -> InterpreterObject: l = self.evaluate_statement(cur.left) + if l is None: + raise mesonlib.MesonException('Cannot compare a void statement on the left-hand side') if isinstance(l, Disabler): return l l_bool = l.operator_call(MesonOperator.BOOL, None) if not l_bool: return self._holderify(l_bool) r = self.evaluate_statement(cur.right) + if r is None: + raise mesonlib.MesonException('Cannot compare a void statement on the right-hand side') if isinstance(r, Disabler): return r return self._holderify(r.operator_call(MesonOperator.BOOL, None)) def evaluate_orstatement(self, cur: mparser.OrNode) -> InterpreterObject: l = self.evaluate_statement(cur.left) + if l is None: + raise mesonlib.MesonException('Cannot compare a void statement on the left-hand side') if isinstance(l, Disabler): return l l_bool = l.operator_call(MesonOperator.BOOL, None) if l_bool: return self._holderify(l_bool) r = self.evaluate_statement(cur.right) + if r is None: + raise mesonlib.MesonException('Cannot compare a void statement on the right-hand side') if isinstance(r, Disabler): return r return self._holderify(r.operator_call(MesonOperator.BOOL, None)) def evaluate_uminusstatement(self, cur: mparser.UMinusNode) -> InterpreterObject: v = self.evaluate_statement(cur.value) + if v is None: + raise InvalidCodeOnVoid('unary minus') if isinstance(v, Disabler): return v v.current_node = cur @@ -346,6 +405,8 @@ class InterpreterBase: r = self.evaluate_statement(cur.right) if isinstance(r, Disabler): return r + if l is None or r is None: + raise InvalidCodeOnVoid(cur.operation) mapping: T.Dict[str, MesonOperator] = { 'add': MesonOperator.PLUS, @@ -361,6 +422,8 @@ class InterpreterBase: def evaluate_ternary(self, node: mparser.TernaryNode) -> T.Optional[InterpreterObject]: assert isinstance(node, mparser.TernaryNode) result = self.evaluate_statement(node.condition) + if result is None: + raise mesonlib.MesonException('Cannot use a void statement as condition for ternary operator.') if isinstance(result, Disabler): return result result.current_node = node @@ -375,18 +438,17 @@ class InterpreterBase: return self.evaluate_fstring(node) @FeatureNew('format strings', '0.58.0') - def evaluate_fstring(self, node: mparser.FormatStringNode) -> InterpreterObject: - assert isinstance(node, mparser.FormatStringNode) - + def evaluate_fstring(self, node: T.Union[mparser.FormatStringNode, mparser.MultilineFormatStringNode]) -> InterpreterObject: def replace(match: T.Match[str]) -> str: var = str(match.group(1)) try: val = _unholder(self.variables[var]) - if not isinstance(val, (str, int, float, bool)): - raise InvalidCode(f'Identifier "{var}" does not name a formattable variable ' + - '(has to be an integer, a string, a floating point number or a boolean).') - - return str(val) + if isinstance(val, (list, dict)): + FeatureNew.single_use('List or dictionary in f-string', '1.3.0', self.subproject, location=self.current_node) + try: + return stringifyUserArguments(val, self.subproject) + except InvalidArguments as e: + raise InvalidArguments(f'f-string: {str(e)}') except KeyError: raise InvalidCode(f'Identifier "{var}" does not name a variable.') @@ -407,14 +469,14 @@ class InterpreterBase: if tsize is None: if isinstance(i, tuple): raise mesonlib.MesonBugException(f'Iteration of {items} returned a tuple even though iter_tuple_size() is None') - self.set_variable(node.varnames[0], self._holderify(i)) + self.set_variable(node.varnames[0].value, self._holderify(i)) else: if not isinstance(i, tuple): raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') if len(i) != tsize: raise mesonlib.MesonBugException(f'Iteration of {items} did not return a tuple even though iter_tuple_size() is {tsize}') for j in range(tsize): - self.set_variable(node.varnames[j], self._holderify(i[j])) + self.set_variable(node.varnames[j].value, self._holderify(i[j])) try: self.evaluate_codeblock(node.block) except ContinueRequest: @@ -424,8 +486,10 @@ class InterpreterBase: def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None: assert isinstance(node, mparser.PlusAssignmentNode) - varname = node.var_name + varname = node.var_name.value addition = self.evaluate_statement(node.value) + if addition is None: + raise InvalidCodeOnVoid('plus assign') # Remember that all variables are immutable. We must always create a # full new variable and then assign it. @@ -437,17 +501,20 @@ class InterpreterBase: def evaluate_indexing(self, node: mparser.IndexNode) -> InterpreterObject: assert isinstance(node, mparser.IndexNode) iobject = self.evaluate_statement(node.iobject) + if iobject is None: + raise InterpreterException('Tried to evaluate indexing on void.') if isinstance(iobject, Disabler): return iobject - index = _unholder(self.evaluate_statement(node.index)) + index_holder = self.evaluate_statement(node.index) + if index_holder is None: + raise InvalidArguments('Cannot use void statement as index.') + index = _unholder(index_holder) - if iobject is None: - raise InterpreterException('Tried to evaluate indexing on None') iobject.current_node = node return self._holderify(iobject.operator_call(MesonOperator.INDEX, index)) def function_call(self, node: mparser.FunctionNode) -> T.Optional[InterpreterObject]: - func_name = node.func_name + func_name = node.func_name.value (h_posargs, h_kwargs) = self.reduce_arguments(node.args) (posargs, kwargs) = self._unholder_args(h_posargs, h_kwargs) if is_disabled(posargs, kwargs) and func_name not in {'get_variable', 'set_variable', 'unset_variable', 'is_disabler'}: @@ -459,6 +526,7 @@ class InterpreterBase: func_args = flatten(posargs) if not getattr(func, 'no-second-level-holder-flattening', False): func_args, kwargs = resolve_second_level_holders(func_args, kwargs) + self.current_node = node res = func(node, func_args, kwargs) return self._holderify(res) if res is not None else None else: @@ -466,15 +534,15 @@ class InterpreterBase: return None def method_call(self, node: mparser.MethodNode) -> T.Optional[InterpreterObject]: - invokable = node.source_object + invocable = node.source_object obj: T.Optional[InterpreterObject] - if isinstance(invokable, mparser.IdNode): - object_display_name = f'variable "{invokable.value}"' - obj = self.get_variable(invokable.value) + if isinstance(invocable, mparser.IdNode): + object_display_name = f'variable "{invocable.value}"' + obj = self.get_variable(invocable.value) else: - object_display_name = invokable.__class__.__name__ - obj = self.evaluate_statement(invokable) - method_name = node.name + object_display_name = invocable.__class__.__name__ + obj = self.evaluate_statement(invocable) + method_name = node.name.value (h_args, h_kwargs) = self.reduce_arguments(node.args) (args, kwargs) = self._unholder_args(h_args, h_kwargs) if is_disabled(args, kwargs): @@ -487,7 +555,7 @@ class InterpreterBase: self.validate_extraction(obj.held_object) elif not isinstance(obj, Disabler): raise InvalidArguments(f'Invalid operation "extract_objects" on {object_display_name} of type {type(obj).__name__}') - obj.current_node = node + obj.current_node = self.current_node = node res = obj.method_call(method_name, args, kwargs) return self._holderify(res) if res is not None else None @@ -541,6 +609,7 @@ class InterpreterBase: reduced_val = self.evaluate_statement(val) if reduced_val is None: raise InvalidArguments(f'Value of key {reduced_key} is void.') + self.current_node = key if duplicate_key_error and reduced_key in reduced_kw: raise InvalidArguments(duplicate_key_error.format(reduced_key)) reduced_kw[reduced_key] = reduced_val @@ -569,7 +638,7 @@ class InterpreterBase: Tried to assign values inside an argument list. To specify a keyword argument, use : instead of =. ''')) - var_name = node.var_name + var_name = node.var_name.value if not isinstance(var_name, str): raise InvalidArguments('Tried to assign value to a non-variable.') value = self.evaluate_statement(node.value) @@ -580,7 +649,7 @@ class InterpreterBase: def set_variable(self, varname: str, variable: T.Union[TYPE_var, InterpreterObject], *, holderify: bool = False) -> None: if variable is None: - raise InvalidCode('Can not assign None to variable.') + raise InvalidCode('Can not assign void to variable.') if holderify: variable = self._holderify(variable) else: diff --git a/mesonbuild/linkers/__init__.py b/mesonbuild/linkers/__init__.py index 298e901..7c35694 100644 --- a/mesonbuild/linkers/__init__.py +++ b/mesonbuild/linkers/__init__.py @@ -12,125 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. + +from .base import ArLikeLinker, RSPFileSyntax from .detect import ( defaults, guess_win_linker, guess_nix_linker, ) -from .linkers import ( - RSPFileSyntax, - - StaticLinker, - VisualStudioLikeLinker, - VisualStudioLinker, - IntelVisualStudioLinker, - AppleArLinker, - ArLinker, - ArmarLinker, - DLinker, - CcrxLinker, - Xc16Linker, - CompCertLinker, - C2000Linker, - TILinker, - AIXArLinker, - PGIStaticLinker, - NvidiaHPC_StaticLinker, - - DynamicLinker, - PosixDynamicLinkerMixin, - GnuLikeDynamicLinkerMixin, - AppleDynamicLinker, - GnuDynamicLinker, - GnuGoldDynamicLinker, - GnuBFDDynamicLinker, - LLVMDynamicLinker, - MoldDynamicLinker, - WASMDynamicLinker, - CcrxDynamicLinker, - Xc16DynamicLinker, - CompCertDynamicLinker, - C2000DynamicLinker, - TIDynamicLinker, - ArmDynamicLinker, - ArmClangDynamicLinker, - QualcommLLVMDynamicLinker, - PGIDynamicLinker, - NvidiaHPC_DynamicLinker, - NAGDynamicLinker, - - VisualStudioLikeLinkerMixin, - MSVCDynamicLinker, - ClangClDynamicLinker, - XilinkDynamicLinker, - SolarisDynamicLinker, - AIXDynamicLinker, - OptlinkDynamicLinker, - CudaLinker, - - prepare_rpaths, - order_rpaths, - evaluate_rpath, -) __all__ = [ + # base.py + 'ArLikeLinker', + 'RSPFileSyntax', + # detect.py 'defaults', 'guess_win_linker', 'guess_nix_linker', - - # linkers.py - 'RSPFileSyntax', - - 'StaticLinker', - 'VisualStudioLikeLinker', - 'VisualStudioLinker', - 'IntelVisualStudioLinker', - 'ArLinker', - 'ArmarLinker', - 'DLinker', - 'CcrxLinker', - 'Xc16Linker', - 'CompCertLinker', - 'C2000Linker', - 'TILinker', - 'AIXArLinker', - 'AppleArLinker', - 'PGIStaticLinker', - 'NvidiaHPC_StaticLinker', - - 'DynamicLinker', - 'PosixDynamicLinkerMixin', - 'GnuLikeDynamicLinkerMixin', - 'AppleDynamicLinker', - 'GnuDynamicLinker', - 'GnuGoldDynamicLinker', - 'GnuBFDDynamicLinker', - 'LLVMDynamicLinker', - 'MoldDynamicLinker', - 'WASMDynamicLinker', - 'CcrxDynamicLinker', - 'Xc16DynamicLinker', - 'CompCertDynamicLinker', - 'C2000DynamicLinker', - 'TIDynamicLinker', - 'ArmDynamicLinker', - 'ArmClangDynamicLinker', - 'QualcommLLVMDynamicLinker', - 'PGIDynamicLinker', - 'NvidiaHPC_DynamicLinker', - 'NAGDynamicLinker', - - 'VisualStudioLikeLinkerMixin', - 'MSVCDynamicLinker', - 'ClangClDynamicLinker', - 'XilinkDynamicLinker', - 'SolarisDynamicLinker', - 'AIXDynamicLinker', - 'OptlinkDynamicLinker', - 'CudaLinker', - - 'prepare_rpaths', - 'order_rpaths', - 'evaluate_rpath', ] diff --git a/mesonbuild/linkers/base.py b/mesonbuild/linkers/base.py new file mode 100644 index 0000000..a656bb4 --- /dev/null +++ b/mesonbuild/linkers/base.py @@ -0,0 +1,50 @@ +# Copyright 2012-2023 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# https://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Core public classes for linkers. +from __future__ import annotations + +import enum +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + + +@enum.unique +class RSPFileSyntax(enum.Enum): + + """Which RSP file syntax the compiler supports.""" + + MSVC = enum.auto() + GCC = enum.auto() + + +class ArLikeLinker: + # POSIX requires supporting the dash, GNU permits omitting it + std_args = ['-csr'] + + def can_linker_accept_rsp(self) -> bool: + # armar / AIX can't accept arguments using the @rsp syntax + # in fact, only the 'ar' id can + return False + + def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]: + return self.std_args + + def get_output_args(self, target: str) -> T.List[str]: + return [target] + + def rsp_file_syntax(self) -> RSPFileSyntax: + return RSPFileSyntax.GCC diff --git a/mesonbuild/linkers/detect.py b/mesonbuild/linkers/detect.py index 8e25f53..4261144 100644 --- a/mesonbuild/linkers/detect.py +++ b/mesonbuild/linkers/detect.py @@ -17,20 +17,7 @@ from __future__ import annotations from .. import mlog from ..mesonlib import ( EnvironmentException, - Popen_safe, join_args, search_version -) -from .linkers import ( - AppleDynamicLinker, - GnuGoldDynamicLinker, - GnuBFDDynamicLinker, - MoldDynamicLinker, - LLVMDynamicLinker, - QualcommLLVMDynamicLinker, - MSVCDynamicLinker, - ClangClDynamicLinker, - SolarisDynamicLinker, - AIXDynamicLinker, - OptlinkDynamicLinker, + Popen_safe, Popen_safe_logged, join_args, search_version ) import re @@ -61,6 +48,7 @@ def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty comp_version: str, for_machine: MachineChoice, *, use_linker_prefix: bool = True, invoked_directly: bool = True, extra_args: T.Optional[T.List[str]] = None) -> 'DynamicLinker': + from . import linkers env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env) # Explicitly pass logo here so that we can get the version of link.exe @@ -73,7 +61,7 @@ def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty check_args += env.coredata.get_external_link_args(for_machine, comp_class.language) - override = [] # type: T.List[str] + override: T.List[str] = [] value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld') if value is not None: override = comp_class.use_linker_args(value[0], comp_version) @@ -85,27 +73,27 @@ def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty p, o, _ = Popen_safe(compiler + check_args) if 'LLD' in o.split('\n', maxsplit=1)[0]: if '(compatible with GNU linkers)' in o: - return LLVMDynamicLinker( + return linkers.LLVMDynamicLinker( compiler, for_machine, comp_class.LINKER_PREFIX, override, version=search_version(o)) elif not invoked_directly: - return ClangClDynamicLinker( + return linkers.ClangClDynamicLinker( for_machine, override, exelist=compiler, prefix=comp_class.LINKER_PREFIX, version=search_version(o), direct=False, machine=None) if value is not None and invoked_directly: compiler = value - # We've already hanedled the non-direct case above + # We've already handled the non-direct case above p, o, e = Popen_safe(compiler + check_args) if 'LLD' in o.split('\n', maxsplit=1)[0]: - return ClangClDynamicLinker( + return linkers.ClangClDynamicLinker( for_machine, [], prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [], exelist=compiler, version=search_version(o), direct=invoked_directly) elif 'OPTLINK' in o: # Optlink's stdout *may* begin with a \r character. - return OptlinkDynamicLinker(compiler, for_machine, version=search_version(o)) + return linkers.OptlinkDynamicLinker(compiler, for_machine, version=search_version(o)) elif o.startswith('Microsoft') or e.startswith('Microsoft'): out = o or e match = re.search(r'.*(X86|X64|ARM|ARM64).*', out) @@ -114,7 +102,7 @@ def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty else: target = 'x86' - return MSVCDynamicLinker( + return linkers.MSVCDynamicLinker( for_machine, [], machine=target, exelist=compiler, prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [], version=search_version(out), direct=invoked_directly) @@ -138,6 +126,7 @@ def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty :for_machine: which machine this linker targets :extra_args: Any additional arguments required (such as a source file) """ + from . import linkers env.coredata.add_lang_args(comp_class.language, comp_class, for_machine, env) extra_args = extra_args or [] @@ -149,26 +138,34 @@ def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty else: check_args = comp_class.LINKER_PREFIX + ['--version'] + extra_args - override = [] # type: T.List[str] + override: T.List[str] = [] value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld') if value is not None: override = comp_class.use_linker_args(value[0], comp_version) check_args += override mlog.debug('-----') - mlog.debug(f'Detecting linker via: {join_args(compiler + check_args)}') - p, o, e = Popen_safe(compiler + check_args) - mlog.debug(f'linker returned {p}') - mlog.debug(f'linker stdout:\n{o}') - mlog.debug(f'linker stderr:\n{e}') + p, o, e = Popen_safe_logged(compiler + check_args, msg='Detecting linker via') v = search_version(o + e) linker: DynamicLinker if 'LLD' in o.split('\n', maxsplit=1)[0]: - linker = LLVMDynamicLinker( + if isinstance(comp_class.LINKER_PREFIX, str): + cmd = compiler + override + [comp_class.LINKER_PREFIX + '-v'] + extra_args + else: + cmd = compiler + override + comp_class.LINKER_PREFIX + ['-v'] + extra_args + _, newo, newerr = Popen_safe_logged(cmd, msg='Detecting LLD linker via') + + lld_cls: T.Type[DynamicLinker] + if 'ld64.lld' in newerr: + lld_cls = linkers.LLVMLD64DynamicLinker + else: + lld_cls = linkers.LLVMDynamicLinker + + linker = lld_cls( compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) elif 'Snapdragon' in e and 'LLVM' in e: - linker = QualcommLLVMDynamicLinker( + linker = linkers.QualcommLLVMDynamicLinker( compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) elif e.startswith('lld-link: '): # The LLD MinGW frontend didn't respond to --version before version 9.0.0, @@ -187,37 +184,33 @@ def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty _, o, e = Popen_safe([linker_cmd, '--version']) v = search_version(o) - linker = LLVMDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) + linker = linkers.LLVMDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) # first might be apple clang, second is for real gcc, the third is icc elif e.endswith('(use -v to see invocation)\n') or 'macosx_version' in e or 'ld: unknown option:' in e: if isinstance(comp_class.LINKER_PREFIX, str): cmd = compiler + [comp_class.LINKER_PREFIX + '-v'] + extra_args else: cmd = compiler + comp_class.LINKER_PREFIX + ['-v'] + extra_args - mlog.debug('-----') - mlog.debug(f'Detecting Apple linker via: {join_args(cmd)}') - _, newo, newerr = Popen_safe(cmd) - mlog.debug(f'linker stdout:\n{newo}') - mlog.debug(f'linker stderr:\n{newerr}') + _, newo, newerr = Popen_safe_logged(cmd, msg='Detecting Apple linker via') for line in newerr.split('\n'): - if 'PROJECT:ld' in line: + if 'PROJECT:ld' in line or 'PROJECT:dyld' in line: v = line.split('-')[1] break else: __failed_to_detect_linker(compiler, check_args, o, e) - linker = AppleDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) + linker = linkers.AppleDynamicLinker(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) elif 'GNU' in o or 'GNU' in e: - cls: T.Type[GnuDynamicLinker] + gnu_cls: T.Type[GnuDynamicLinker] # this is always the only thing on stdout, except for swift # which may or may not redirect the linker stdout to stderr if o.startswith('GNU gold') or e.startswith('GNU gold'): - cls = GnuGoldDynamicLinker + gnu_cls = linkers.GnuGoldDynamicLinker elif o.startswith('mold') or e.startswith('mold'): - cls = MoldDynamicLinker + gnu_cls = linkers.MoldDynamicLinker else: - cls = GnuBFDDynamicLinker - linker = cls(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) + gnu_cls = linkers.GnuBFDDynamicLinker + linker = gnu_cls(compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) elif 'Solaris' in e or 'Solaris' in o: for line in (o+e).split('\n'): if 'ld: Software Generation Utilities' in line: @@ -225,7 +218,7 @@ def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty break else: v = 'unknown version' - linker = SolarisDynamicLinker( + linker = linkers.SolarisDynamicLinker( compiler, for_machine, comp_class.LINKER_PREFIX, override, version=v) elif 'ld: 0706-012 The -- flag is not recognized' in e: @@ -233,7 +226,7 @@ def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Ty _, _, e = Popen_safe(compiler + [comp_class.LINKER_PREFIX + '-V'] + extra_args) else: _, _, e = Popen_safe(compiler + comp_class.LINKER_PREFIX + ['-V'] + extra_args) - linker = AIXDynamicLinker( + linker = linkers.AIXDynamicLinker( compiler, for_machine, comp_class.LINKER_PREFIX, override, version=search_version(e)) else: diff --git a/mesonbuild/linkers/linkers.py b/mesonbuild/linkers/linkers.py index 3385dd3..dbb5e57 100644 --- a/mesonbuild/linkers/linkers.py +++ b/mesonbuild/linkers/linkers.py @@ -11,12 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import abc -import enum import os import typing as T +import re +from .base import ArLikeLinker, RSPFileSyntax from .. import mesonlib from ..mesonlib import EnvironmentException, MesonException from ..arglist import CompilerArgs @@ -27,15 +29,6 @@ if T.TYPE_CHECKING: from ..mesonlib import MachineChoice -@enum.unique -class RSPFileSyntax(enum.Enum): - - """Which RSP file syntax the compiler supports.""" - - MSVC = enum.auto() - GCC = enum.auto() - - class StaticLinker: id: str @@ -93,7 +86,7 @@ class StaticLinker: def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]: return args[:] - def get_link_debugfile_name(self, targetfile: str) -> str: + def get_link_debugfile_name(self, targetfile: str) -> T.Optional[str]: return None def get_link_debugfile_args(self, targetfile: str) -> T.List[str]: @@ -116,7 +109,210 @@ class StaticLinker: raise EnvironmentException(f'{self.id} does not implement rsp format, this shouldn\'t be called') -class VisualStudioLikeLinker: +class DynamicLinker(metaclass=abc.ABCMeta): + + """Base class for dynamic linkers.""" + + _BUILDTYPE_ARGS: T.Dict[str, T.List[str]] = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': [], + 'minsize': [], + 'custom': [], + } + + @abc.abstractproperty + def id(self) -> str: + pass + + def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: + args = [arg] if isinstance(arg, str) else arg + if self.prefix_arg is None: + return args + elif isinstance(self.prefix_arg, str): + return [self.prefix_arg + arg for arg in args] + ret: T.List[str] = [] + for arg in args: + ret += self.prefix_arg + [arg] + return ret + + def __init__(self, exelist: T.List[str], + for_machine: mesonlib.MachineChoice, prefix_arg: T.Union[str, T.List[str]], + always_args: T.List[str], *, version: str = 'unknown version'): + self.exelist = exelist + self.for_machine = for_machine + self.version = version + self.prefix_arg = prefix_arg + self.always_args = always_args + self.machine: T.Optional[str] = None + + def __repr__(self) -> str: + return '<{}: v{} `{}`>'.format(type(self).__name__, self.version, ' '.join(self.exelist)) + + def get_id(self) -> str: + return self.id + + def get_version_string(self) -> str: + return f'({self.id} {self.version})' + + def get_exelist(self) -> T.List[str]: + return self.exelist.copy() + + def get_accepts_rsp(self) -> bool: + # rsp files are only used when building on Windows because we want to + # avoid issues with quoting and max argument length + return mesonlib.is_windows() + + def rsp_file_syntax(self) -> RSPFileSyntax: + """The format of the RSP file that this compiler supports. + + If `self.can_linker_accept_rsp()` returns True, then this needs to + be implemented + """ + return RSPFileSyntax.GCC + + def get_always_args(self) -> T.List[str]: + return self.always_args.copy() + + def get_lib_prefix(self) -> str: + return '' + + # XXX: is use_ldflags a compiler or a linker attribute? + + def get_option_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + return [] + + def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: + raise EnvironmentException(f'Language {self.id} does not support has_multi_link_arguments.') + + def get_debugfile_name(self, targetfile: str) -> T.Optional[str]: + '''Name of debug file written out (see below)''' + return None + + def get_debugfile_args(self, targetfile: str) -> T.List[str]: + """Some compilers (MSVC) write debug into a separate file. + + This method takes the target object path and returns a list of + commands to append to the linker invocation to control where that + file is written. + """ + return [] + + def get_std_shared_lib_args(self) -> T.List[str]: + return [] + + def get_std_shared_module_args(self, options: 'KeyedOptionDictType') -> T.List[str]: + return self.get_std_shared_lib_args() + + def get_pie_args(self) -> T.List[str]: + # TODO: this really needs to take a boolean and return the args to + # disable pie, otherwise it only acts to enable pie if pie *isn't* the + # default. + raise EnvironmentException(f'Linker {self.id} does not support position-independent executable') + + def get_lto_args(self) -> T.List[str]: + return [] + + def get_thinlto_cache_args(self, path: str) -> T.List[str]: + return [] + + def sanitizer_args(self, value: str) -> T.List[str]: + return [] + + def get_buildtype_args(self, buildtype: str) -> T.List[str]: + # We can override these in children by just overriding the + # _BUILDTYPE_ARGS value. + return self._BUILDTYPE_ARGS[buildtype] + + def get_asneeded_args(self) -> T.List[str]: + return [] + + def get_link_whole_for(self, args: T.List[str]) -> T.List[str]: + raise EnvironmentException( + f'Linker {self.id} does not support link_whole') + + def get_allow_undefined_args(self) -> T.List[str]: + raise EnvironmentException( + f'Linker {self.id} does not support allow undefined') + + @abc.abstractmethod + def get_output_args(self, outputname: str) -> T.List[str]: + pass + + def get_coverage_args(self) -> T.List[str]: + raise EnvironmentException(f"Linker {self.id} doesn't implement coverage data generation.") + + @abc.abstractmethod + def get_search_args(self, dirname: str) -> T.List[str]: + pass + + def export_dynamic_args(self, env: 'Environment') -> T.List[str]: + return [] + + def import_library_args(self, implibname: str) -> T.List[str]: + """The name of the outputted import library. + + This implementation is used only on Windows by compilers that use GNU ld + """ + return [] + + def thread_flags(self, env: 'Environment') -> T.List[str]: + return [] + + def no_undefined_args(self) -> T.List[str]: + """Arguments to error if there are any undefined symbols at link time. + + This is the inverse of get_allow_undefined_args(). + + TODO: A future cleanup might merge this and + get_allow_undefined_args() into a single method taking a + boolean + """ + return [] + + def fatal_warnings(self) -> T.List[str]: + """Arguments to make all warnings errors.""" + return [] + + def headerpad_args(self) -> T.List[str]: + # Only used by the Apple linker + return [] + + def get_win_subsystem_args(self, value: str) -> T.List[str]: + # Only used if supported by the dynamic linker and + # only when targeting Windows + return [] + + def bitcode_args(self) -> T.List[str]: + raise MesonException('This linker does not support bitcode bundles') + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: T.Tuple[str, ...], build_rpath: str, + install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]: + return ([], set()) + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]: + return [] + + def get_archive_name(self, filename: str) -> str: + #Only used by AIX. + return str() + + def get_command_to_archive_shlib(self) -> T.List[str]: + #Only used by AIX. + return [] + + +if T.TYPE_CHECKING: + StaticLinkerBase = StaticLinker + DynamicLinkerBase = DynamicLinker +else: + StaticLinkerBase = DynamicLinkerBase = object + + +class VisualStudioLikeLinker(StaticLinkerBase): always_args = ['/NOLOGO'] def __init__(self, machine: str): @@ -129,7 +325,7 @@ class VisualStudioLikeLinker: return self.always_args.copy() def get_output_args(self, target: str) -> T.List[str]: - args = [] # type: T.List[str] + args: T.List[str] = [] if self.machine: args += ['/MACHINE:' + self.machine] args += ['/OUT:' + target] @@ -153,6 +349,8 @@ class VisualStudioLinker(VisualStudioLikeLinker, StaticLinker): """Microsoft's lib static linker.""" + id = 'lib' + def __init__(self, exelist: T.List[str], machine: str): StaticLinker.__init__(self, exelist) VisualStudioLikeLinker.__init__(self, machine) @@ -162,31 +360,14 @@ class IntelVisualStudioLinker(VisualStudioLikeLinker, StaticLinker): """Intel's xilib static linker.""" + id = 'xilib' + def __init__(self, exelist: T.List[str], machine: str): StaticLinker.__init__(self, exelist) VisualStudioLikeLinker.__init__(self, machine) -class ArLikeLinker(StaticLinker): - # POSIX requires supporting the dash, GNU permits omitting it - std_args = ['-csr'] - - def can_linker_accept_rsp(self) -> bool: - # armar / AIX can't accept arguments using the @rsp syntax - # in fact, only the 'ar' id can - return False - - def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]: - return self.std_args - - def get_output_args(self, target: str) -> T.List[str]: - return [target] - - def rsp_file_syntax(self) -> RSPFileSyntax: - return RSPFileSyntax.GCC - - -class ArLinker(ArLikeLinker): +class ArLinker(ArLikeLinker, StaticLinker): id = 'ar' def __init__(self, for_machine: mesonlib.MachineChoice, exelist: T.List[str]): @@ -226,7 +407,7 @@ class AppleArLinker(ArLinker): id = 'applear' -class ArmarLinker(ArLikeLinker): +class ArmarLinker(ArLikeLinker, StaticLinker): id = 'armar' @@ -321,11 +502,33 @@ class C2000Linker(TILinker): id = 'ar2000' -class AIXArLinker(ArLikeLinker): +class AIXArLinker(ArLikeLinker, StaticLinker): id = 'aixar' std_args = ['-csr', '-Xany'] +class MetrowerksStaticLinker(StaticLinker): + + def can_linker_accept_rsp(self) -> bool: + return True + + def get_linker_always_args(self) -> T.List[str]: + return ['-library'] + + def get_output_args(self, target: str) -> T.List[str]: + return ['-o', target] + + def rsp_file_syntax(self) -> RSPFileSyntax: + return RSPFileSyntax.GCC + + +class MetrowerksStaticLinkerARM(MetrowerksStaticLinker): + id = 'mwldarm' + + +class MetrowerksStaticLinkerEmbeddedPowerPC(MetrowerksStaticLinker): + id = 'mwldeppc' + def prepare_rpaths(raw_rpaths: T.Tuple[str, ...], build_dir: str, from_dir: str) -> T.List[str]: # The rpaths we write must be relative if they point to the build dir, # because otherwise they have different length depending on the build @@ -355,199 +558,8 @@ def evaluate_rpath(p: str, build_dir: str, from_dir: str) -> str: else: return os.path.relpath(os.path.join(build_dir, p), os.path.join(build_dir, from_dir)) -class DynamicLinker(metaclass=abc.ABCMeta): - """Base class for dynamic linkers.""" - - _BUILDTYPE_ARGS = { - 'plain': [], - 'debug': [], - 'debugoptimized': [], - 'release': [], - 'minsize': [], - 'custom': [], - } # type: T.Dict[str, T.List[str]] - - @abc.abstractproperty - def id(self) -> str: - pass - - def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: - args = [arg] if isinstance(arg, str) else arg - if self.prefix_arg is None: - return args - elif isinstance(self.prefix_arg, str): - return [self.prefix_arg + arg for arg in args] - ret = [] - for arg in args: - ret += self.prefix_arg + [arg] - return ret - - def __init__(self, exelist: T.List[str], - for_machine: mesonlib.MachineChoice, prefix_arg: T.Union[str, T.List[str]], - always_args: T.List[str], *, version: str = 'unknown version'): - self.exelist = exelist - self.for_machine = for_machine - self.version = version - self.prefix_arg = prefix_arg - self.always_args = always_args - self.machine = None # type: T.Optional[str] - - def __repr__(self) -> str: - return '<{}: v{} `{}`>'.format(type(self).__name__, self.version, ' '.join(self.exelist)) - - def get_id(self) -> str: - return self.id - - def get_version_string(self) -> str: - return f'({self.id} {self.version})' - - def get_exelist(self) -> T.List[str]: - return self.exelist.copy() - - def get_accepts_rsp(self) -> bool: - # rsp files are only used when building on Windows because we want to - # avoid issues with quoting and max argument length - return mesonlib.is_windows() - - def rsp_file_syntax(self) -> RSPFileSyntax: - """The format of the RSP file that this compiler supports. - - If `self.can_linker_accept_rsp()` returns True, then this needs to - be implemented - """ - return RSPFileSyntax.GCC - - def get_always_args(self) -> T.List[str]: - return self.always_args.copy() - - def get_lib_prefix(self) -> str: - return '' - - # XXX: is use_ldflags a compiler or a linker attribute? - - def get_option_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - return [] - - def has_multi_arguments(self, args: T.List[str], env: 'Environment') -> T.Tuple[bool, bool]: - raise EnvironmentException(f'Language {self.id} does not support has_multi_link_arguments.') - - def get_debugfile_name(self, targetfile: str) -> str: - '''Name of debug file written out (see below)''' - return None - - def get_debugfile_args(self, targetfile: str) -> T.List[str]: - """Some compilers (MSVC) write debug into a separate file. - - This method takes the target object path and returns a list of - commands to append to the linker invocation to control where that - file is written. - """ - return [] - - def get_std_shared_lib_args(self) -> T.List[str]: - return [] - - def get_std_shared_module_args(self, options: 'KeyedOptionDictType') -> T.List[str]: - return self.get_std_shared_lib_args() - - def get_pie_args(self) -> T.List[str]: - # TODO: this really needs to take a boolean and return the args to - # disable pie, otherwise it only acts to enable pie if pie *isn't* the - # default. - raise EnvironmentException(f'Linker {self.id} does not support position-independent executable') - - def get_lto_args(self) -> T.List[str]: - return [] - - def get_thinlto_cache_args(self, path: str) -> T.List[str]: - return [] - - def sanitizer_args(self, value: str) -> T.List[str]: - return [] - - def get_buildtype_args(self, buildtype: str) -> T.List[str]: - # We can override these in children by just overriding the - # _BUILDTYPE_ARGS value. - return self._BUILDTYPE_ARGS[buildtype] - - def get_asneeded_args(self) -> T.List[str]: - return [] - - def get_link_whole_for(self, args: T.List[str]) -> T.List[str]: - raise EnvironmentException( - f'Linker {self.id} does not support link_whole') - - def get_allow_undefined_args(self) -> T.List[str]: - raise EnvironmentException( - f'Linker {self.id} does not support allow undefined') - - @abc.abstractmethod - def get_output_args(self, outname: str) -> T.List[str]: - pass - - def get_coverage_args(self) -> T.List[str]: - raise EnvironmentException(f"Linker {self.id} doesn't implement coverage data generation.") - - @abc.abstractmethod - def get_search_args(self, dirname: str) -> T.List[str]: - pass - - def export_dynamic_args(self, env: 'Environment') -> T.List[str]: - return [] - - def import_library_args(self, implibname: str) -> T.List[str]: - """The name of the outputted import library. - - This implementation is used only on Windows by compilers that use GNU ld - """ - return [] - - def thread_flags(self, env: 'Environment') -> T.List[str]: - return [] - - def no_undefined_args(self) -> T.List[str]: - """Arguments to error if there are any undefined symbols at link time. - - This is the inverse of get_allow_undefined_args(). - - TODO: A future cleanup might merge this and - get_allow_undefined_args() into a single method taking a - boolean - """ - return [] - - def fatal_warnings(self) -> T.List[str]: - """Arguments to make all warnings errors.""" - return [] - - def headerpad_args(self) -> T.List[str]: - # Only used by the Apple linker - return [] - - def get_gui_app_args(self, value: bool) -> T.List[str]: - # Only used by VisualStudioLikeLinkers - return [] - - def get_win_subsystem_args(self, value: str) -> T.List[str]: - # Only used if supported by the dynamic linker and - # only when targeting Windows - return [] - - def bitcode_args(self) -> T.List[str]: - raise MesonException('This linker does not support bitcode bundles') - - def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, - rpath_paths: T.Tuple[str, ...], build_rpath: str, - install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]: - return ([], set()) - - def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, - suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]: - return [] - - -class PosixDynamicLinkerMixin: +class PosixDynamicLinkerMixin(DynamicLinkerBase): """Mixin class for POSIX-ish linkers. @@ -556,8 +568,8 @@ class PosixDynamicLinkerMixin: GNU-like that it makes sense to split this out. """ - def get_output_args(self, outname: str) -> T.List[str]: - return ['-o', outname] + def get_output_args(self, outputname: str) -> T.List[str]: + return ['-o', outputname] def get_std_shared_lib_args(self) -> T.List[str]: return ['-shared'] @@ -566,7 +578,7 @@ class PosixDynamicLinkerMixin: return ['-L' + dirname] -class GnuLikeDynamicLinkerMixin: +class GnuLikeDynamicLinkerMixin(DynamicLinkerBase): """Mixin class for dynamic linkers that provides gnu-like interface. @@ -578,14 +590,26 @@ class GnuLikeDynamicLinkerMixin: for_machine = MachineChoice.HOST def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: ... - _BUILDTYPE_ARGS = { + _BUILDTYPE_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': ['-O1'], 'minsize': [], 'custom': [], - } # type: T.Dict[str, T.List[str]] + } + + _SUBSYSTEMS: T.Dict[str, str] = { + "native": "1", + "windows": "windows", + "console": "console", + "posix": "7", + "efi_application": "10", + "efi_boot_service_driver": "11", + "efi_runtime_driver": "12", + "efi_rom": "13", + "boot_application": "16", + } def get_buildtype_args(self, buildtype: str) -> T.List[str]: # We can override these in children by just overriding the @@ -654,14 +678,14 @@ class GnuLikeDynamicLinkerMixin: return ([], set()) if not rpath_paths and not install_rpath and not build_rpath: return ([], set()) - args = [] + args: T.List[str] = [] origin_placeholder = '$ORIGIN' processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) # Need to deduplicate rpaths, as macOS's install_name_tool # is *very* allergic to duplicate -delete_rpath arguments # when calling depfixer on installation. all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths]) - rpath_dirs_to_remove = set() + rpath_dirs_to_remove: T.Set[bytes] = set() for p in all_paths: rpath_dirs_to_remove.add(p.encode('utf8')) # Build_rpath is used as-is (it is usually absolute). @@ -720,21 +744,10 @@ class GnuLikeDynamicLinkerMixin: # as well, and is always accepted, so we manually map the # other types here. List of all types: # https://github.com/wine-mirror/wine/blob/3ded60bd1654dc689d24a23305f4a93acce3a6f2/include/winnt.h#L2492-L2507 - subsystems = { - "native": "1", - "windows": "windows", - "console": "console", - "posix": "7", - "efi_application": "10", - "efi_boot_service_driver": "11", - "efi_runtime_driver": "12", - "efi_rom": "13", - "boot_application": "16", - } versionsuffix = None if ',' in value: value, versionsuffix = value.split(',', 1) - newvalue = subsystems.get(value) + newvalue = self._SUBSYSTEMS.get(value) if newvalue is not None: if versionsuffix is not None: newvalue += f':{versionsuffix}' @@ -764,7 +777,7 @@ class AppleDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): return [] def get_link_whole_for(self, args: T.List[str]) -> T.List[str]: - result = [] # type: T.List[str] + result: T.List[str] = [] for a in args: result.extend(self._apply_prefix('-force_load')) result.append(a) @@ -807,7 +820,7 @@ class AppleDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]: if not rpath_paths and not install_rpath and not build_rpath: return ([], set()) - args = [] + args: T.List[str] = [] # @loader_path is the equivalent of $ORIGIN on macOS # https://stackoverflow.com/q/26280738 origin_placeholder = '@loader_path' @@ -824,6 +837,11 @@ class AppleDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): return ["-Wl,-cache_path_lto," + path] +class LLVMLD64DynamicLinker(AppleDynamicLinker): + + id = 'ld64.lld' + + class GnuDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker): """Representation of GNU ld.bfd and ld.gold.""" @@ -869,7 +887,7 @@ class LLVMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, Dyna super().__init__(exelist, for_machine, prefix_arg, always_args, version=version) # Some targets don't seem to support this argument (windows, wasm, ...) - _, _, e = mesonlib.Popen_safe(self.exelist + self._apply_prefix('--allow-shlib-undefined')) + _, _, e = mesonlib.Popen_safe(self.exelist + always_args + self._apply_prefix('--allow-shlib-undefined')) # Versions < 9 do not have a quoted argument self.has_allow_shlib_undefined = ('unknown argument: --allow-shlib-undefined' not in e) and ("unknown argument: '--allow-shlib-undefined'" not in e) @@ -881,6 +899,18 @@ class LLVMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, Dyna def get_thinlto_cache_args(self, path: str) -> T.List[str]: return ['-Wl,--thinlto-cache-dir=' + path] + def get_win_subsystem_args(self, value: str) -> T.List[str]: + # lld does not support a numeric subsystem value + version = None + if ',' in value: + value, version = value.split(',', 1) + if value in self._SUBSYSTEMS: + if version is not None: + value += f':{version}' + return self._apply_prefix([f'--subsystem,{value}']) + else: + raise mesonlib.MesonBugException(f'win_subsystem: {value} not handled in lld linker. This should not be possible.') + class WASMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker): @@ -889,10 +919,10 @@ class WASMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, Dyna id = 'ld.wasm' def get_allow_undefined_args(self) -> T.List[str]: - return ['-s', 'ERROR_ON_UNDEFINED_SYMBOLS=0'] + return ['-sERROR_ON_UNDEFINED_SYMBOLS=0'] def no_undefined_args(self) -> T.List[str]: - return ['-s', 'ERROR_ON_UNDEFINED_SYMBOLS=1'] + return ['-sERROR_ON_UNDEFINED_SYMBOLS=1'] def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]: @@ -909,7 +939,7 @@ class WASMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, Dyna class CcrxDynamicLinker(DynamicLinker): - """Linker for Renesis CCrx compiler.""" + """Linker for Renesas CCrx compiler.""" id = 'rlink' @@ -1130,7 +1160,7 @@ class NAGDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]: if not rpath_paths and not install_rpath and not build_rpath: return ([], set()) - args = [] + args: T.List[str] = [] origin_placeholder = '$ORIGIN' processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths]) @@ -1195,15 +1225,15 @@ class PGIStaticLinker(StaticLinker): NvidiaHPC_StaticLinker = PGIStaticLinker -class VisualStudioLikeLinkerMixin: +class VisualStudioLikeLinkerMixin(DynamicLinkerBase): - """Mixin class for for dynamic linkers that act like Microsoft's link.exe.""" + """Mixin class for dynamic linkers that act like Microsoft's link.exe.""" if T.TYPE_CHECKING: for_machine = MachineChoice.HOST def _apply_prefix(self, arg: T.Union[str, T.List[str]]) -> T.List[str]: ... - _BUILDTYPE_ARGS = { + _BUILDTYPE_ARGS: T.Dict[str, T.List[str]] = { 'plain': [], 'debug': [], 'debugoptimized': [], @@ -1212,13 +1242,13 @@ class VisualStudioLikeLinkerMixin: 'release': ['/OPT:REF'], 'minsize': ['/INCREMENTAL:NO', '/OPT:REF'], 'custom': [], - } # type: T.Dict[str, T.List[str]] + } def __init__(self, exelist: T.List[str], for_machine: mesonlib.MachineChoice, prefix_arg: T.Union[str, T.List[str]], always_args: T.List[str], *, version: str = 'unknown version', direct: bool = True, machine: str = 'x86'): # There's no way I can find to make mypy understand what's going on here - super().__init__(exelist, for_machine, prefix_arg, always_args, version=version) # type: ignore + super().__init__(exelist, for_machine, prefix_arg, always_args, version=version) self.machine = machine self.direct = direct @@ -1232,8 +1262,8 @@ class VisualStudioLikeLinkerMixin: return self._apply_prefix(['/MACHINE:' + self.machine, '/OUT:' + outputname]) def get_always_args(self) -> T.List[str]: - parent = super().get_always_args() # type: ignore - return self._apply_prefix('/nologo') + T.cast('T.List[str]', parent) + parent = super().get_always_args() + return self._apply_prefix('/nologo') + parent def get_search_args(self, dirname: str) -> T.List[str]: return self._apply_prefix('/LIBPATH:' + dirname) @@ -1242,8 +1272,7 @@ class VisualStudioLikeLinkerMixin: return self._apply_prefix('/DLL') def get_debugfile_name(self, targetfile: str) -> str: - basename = targetfile.rsplit('.', maxsplit=1)[0] - return basename + '.pdb' + return targetfile def get_debugfile_args(self, targetfile: str) -> T.List[str]: return self._apply_prefix(['/DEBUG', '/PDB:' + self.get_debugfile_name(targetfile)]) @@ -1251,7 +1280,7 @@ class VisualStudioLikeLinkerMixin: def get_link_whole_for(self, args: T.List[str]) -> T.List[str]: # Only since VS2015 args = mesonlib.listify(args) - l = [] # T.List[str] + l: T.List[str] = [] for a in args: l.extend(self._apply_prefix('/WHOLEARCHIVE:' + a)) return l @@ -1286,10 +1315,7 @@ class MSVCDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): prefix, always_args, machine=machine, version=version, direct=direct) def get_always_args(self) -> T.List[str]: - return self._apply_prefix(['/nologo', '/release']) + super().get_always_args() - - def get_gui_app_args(self, value: bool) -> T.List[str]: - return self.get_win_subsystem_args("windows" if value else "console") + return self._apply_prefix(['/release']) + super().get_always_args() def get_win_subsystem_args(self, value: str) -> T.List[str]: return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}']) @@ -1317,9 +1343,6 @@ class ClangClDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): return super().get_output_args(outputname) - def get_gui_app_args(self, value: bool) -> T.List[str]: - return self.get_win_subsystem_args("windows" if value else "console") - def get_win_subsystem_args(self, value: str) -> T.List[str]: return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}']) @@ -1340,9 +1363,6 @@ class XilinkDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): direct: bool = True): super().__init__(['xilink.exe'], for_machine, '', always_args, version=version) - def get_gui_app_args(self, value: bool) -> T.List[str]: - return self.get_win_subsystem_args("windows" if value else "console") - def get_win_subsystem_args(self, value: str) -> T.List[str]: return self._apply_prefix([f'/SUBSYSTEM:{value.upper()}']) @@ -1387,7 +1407,7 @@ class SolarisDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): return ([], set()) processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) all_paths = mesonlib.OrderedSet([os.path.join('$ORIGIN', p) for p in processed_rpaths]) - rpath_dirs_to_remove = set() + rpath_dirs_to_remove: T.Set[bytes] = set() for p in all_paths: rpath_dirs_to_remove.add(p.encode('utf8')) if build_rpath != '': @@ -1427,6 +1447,21 @@ class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): def get_allow_undefined_args(self) -> T.List[str]: return self._apply_prefix(['-berok']) + def get_archive_name(self, filename: str) -> str: + # In AIX we allow the shared library name to have the lt_version and so_version. + # But the archive name must just be .a . + # For Example shared object can have the name libgio.so.0.7200.1 but the archive + # must have the name libgio.a having libgio.a (libgio.so.0.7200.1) in the + # archive. This regular expression is to do the same. + filename = re.sub('[.][a]([.]?([0-9]+))*([.]?([a-z]+))*', '.a', filename.replace('.so', '.a')) + return filename + + def get_command_to_archive_shlib(self) -> T.List[str]: + # Archive shared library object and remove the shared library object, + # since it already exists in the archive. + command = ['ar', '-q', '-v', '$out', '$in', '&&', 'rm', '-f', '$in'] + return command + def get_link_whole_for(self, args: T.List[str]) -> T.List[str]: # AIX's linker always links the whole archive: "The ld command # processes all input files in the same manner, whether they are @@ -1436,7 +1471,7 @@ class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, rpath_paths: T.Tuple[str, ...], build_rpath: str, install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]: - all_paths = mesonlib.OrderedSet() # type: mesonlib.OrderedSet[str] + all_paths: mesonlib.OrderedSet[str] = mesonlib.OrderedSet() # install_rpath first, followed by other paths, and the system path last if install_rpath != '': all_paths.add(install_rpath) @@ -1446,9 +1481,9 @@ class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): all_paths.add(os.path.join(build_dir, p)) # We should consider allowing the $LIBPATH environment variable # to override sys_path. - sys_path = env.get_compiler_system_dirs(self.for_machine) + sys_path = env.get_compiler_system_lib_dirs(self.for_machine) if len(sys_path) == 0: - # get_compiler_system_dirs doesn't support our compiler. + # get_compiler_system_lib_dirs doesn't support our compiler. # Use the default system library path all_paths.update(['/usr/lib', '/lib']) else: @@ -1535,3 +1570,46 @@ class CudaLinker(PosixDynamicLinkerMixin, DynamicLinker): def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, suffix: str, soversion: str, darwin_versions: T.Tuple[str, str]) -> T.List[str]: return [] + + +class MetrowerksLinker(DynamicLinker): + + def __init__(self, exelist: T.List[str], for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(exelist, for_machine, '', [], + version=version) + + def fatal_warnings(self) -> T.List[str]: + return ['-w', 'error'] + + def get_allow_undefined_args(self) -> T.List[str]: + return [] + + def get_accepts_rsp(self) -> bool: + return True + + def get_lib_prefix(self) -> str: + return "" + + def get_linker_always_args(self) -> T.List[str]: + return [] + + def get_output_args(self, outputname: str) -> T.List[str]: + return ['-o', outputname] + + def get_search_args(self, dirname: str) -> T.List[str]: + return self._apply_prefix('-L' + dirname) + + def invoked_by_compiler(self) -> bool: + return False + + def rsp_file_syntax(self) -> RSPFileSyntax: + return RSPFileSyntax.GCC + + +class MetrowerksLinkerARM(MetrowerksLinker): + id = 'mwldarm' + + +class MetrowerksLinkerEmbeddedPowerPC(MetrowerksLinker): + id = 'mwldeppc' diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py index 27ef46c..60d7133 100644 --- a/mesonbuild/mcompile.py +++ b/mesonbuild/mcompile.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Entrypoint script for backend agnostic compile.""" @@ -25,7 +26,6 @@ from pathlib import Path from . import mlog from . import mesonlib -from . import coredata from .mesonlib import MesonException, RealPathAction, join_args, setup_vsenv from mesonbuild.environment import detect_ninja from mesonbuild.coredata import UserArrayOption @@ -35,7 +35,7 @@ if T.TYPE_CHECKING: import argparse def array_arg(value: str) -> T.List[str]: - return UserArrayOption(None, value, allow_dups=True, user_input=True).value + return UserArrayOption.listify_value(value) def validate_builddir(builddir: Path) -> None: if not (builddir / 'meson-private' / 'coredata.dat').is_file(): @@ -54,16 +54,18 @@ def parse_introspect_data(builddir: Path) -> T.Dict[str, T.List[dict]]: with path_to_intro.open(encoding='utf-8') as f: schema = json.load(f) - parsed_data = defaultdict(list) # type: T.Dict[str, T.List[dict]] + parsed_data: T.Dict[str, T.List[dict]] = defaultdict(list) for target in schema: parsed_data[target['name']] += [target] return parsed_data class ParsedTargetName: full_name = '' + base_name = '' name = '' type = '' path = '' + suffix = '' def __init__(self, target: str): self.full_name = target @@ -80,6 +82,13 @@ class ParsedTargetName: else: self.name = split[0] + split = self.name.rsplit('.', 1) + if len(split) > 1: + self.base_name = split[0] + self.suffix = split[1] + else: + self.base_name = split[0] + @staticmethod def _is_valid_type(type: str) -> bool: # Amend docs in Commands.md when editing this list @@ -89,29 +98,41 @@ class ParsedTargetName: 'shared_library', 'shared_module', 'custom', + 'alias', 'run', 'jar', } return type in allowed_types def get_target_from_intro_data(target: ParsedTargetName, builddir: Path, introspect_data: T.Dict[str, T.Any]) -> T.Dict[str, T.Any]: - if target.name not in introspect_data: + if target.name not in introspect_data and target.base_name not in introspect_data: raise MesonException(f'Can\'t invoke target `{target.full_name}`: target not found') intro_targets = introspect_data[target.name] - found_targets = [] # type: T.List[T.Dict[str, T.Any]] + # if target.name doesn't find anything, try just the base name + if not intro_targets: + intro_targets = introspect_data[target.base_name] + found_targets: T.List[T.Dict[str, T.Any]] = [] resolved_bdir = builddir.resolve() - if not target.type and not target.path: + if not target.type and not target.path and not target.suffix: found_targets = intro_targets else: for intro_target in intro_targets: - if (intro_target['subproject'] or - (target.type and target.type != intro_target['type'].replace(' ', '_')) or - (target.path - and intro_target['filename'] != 'no_name' - and Path(target.path) != Path(intro_target['filename'][0]).relative_to(resolved_bdir).parent)): + # Parse out the name from the id if needed + intro_target_name = intro_target['name'] + split = intro_target['id'].rsplit('@', 1) + if len(split) > 1: + split = split[0].split('@@', 1) + if len(split) > 1: + intro_target_name = split[1] + else: + intro_target_name = split[0] + if ((target.type and target.type != intro_target['type'].replace(' ', '_')) or + (target.name != intro_target_name) or + (target.path and intro_target['filename'] != 'no_name' and + Path(target.path) != Path(intro_target['filename'][0]).relative_to(resolved_bdir).parent)): continue found_targets += [intro_target] @@ -120,19 +141,27 @@ def get_target_from_intro_data(target: ParsedTargetName, builddir: Path, introsp elif len(found_targets) > 1: suggestions: T.List[str] = [] for i in found_targets: - p = Path(i['filename'][0]).relative_to(resolved_bdir) + i_name = i['name'] + split = i['id'].rsplit('@', 1) + if len(split) > 1: + split = split[0].split('@@', 1) + if len(split) > 1: + i_name = split[1] + else: + i_name = split[0] + p = Path(i['filename'][0]).relative_to(resolved_bdir).parent / i_name t = i['type'].replace(' ', '_') suggestions.append(f'- ./{p}:{t}') suggestions_str = '\n'.join(suggestions) raise MesonException(f'Can\'t invoke target `{target.full_name}`: ambiguous name.' - f'Add target type and/or path:\n{suggestions_str}') + f' Add target type and/or path:\n{suggestions_str}') return found_targets[0] def generate_target_names_ninja(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> T.List[str]: intro_target = get_target_from_intro_data(target, builddir, introspect_data) - if intro_target['type'] == 'run': + if intro_target['type'] in {'alias', 'run'}: return [target.name] else: return [str(Path(out_file).relative_to(builddir.resolve())) for out_file in intro_target['filename']] @@ -171,11 +200,11 @@ def get_parsed_args_ninja(options: 'argparse.Namespace', builddir: Path) -> T.Tu def generate_target_name_vs(target: ParsedTargetName, builddir: Path, introspect_data: dict) -> str: intro_target = get_target_from_intro_data(target, builddir, introspect_data) - assert intro_target['type'] != 'run', 'Should not reach here: `run` targets must be handle above' + assert intro_target['type'] not in {'alias', 'run'}, 'Should not reach here: `run` targets must be handle above' # Normalize project name # Source: https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-build-specific-targets-in-solutions-by-using-msbuild-exe - target_name = re.sub(r"[\%\$\@\;\.\(\)']", '_', intro_target['id']) # type: str + target_name = re.sub(r"[\%\$\@\;\.\(\)']", '_', intro_target['id']) rel_path = Path(intro_target['filename'][0]).relative_to(builddir.resolve()).parent if rel_path != Path('.'): target_name = str(rel_path / target_name) @@ -191,7 +220,7 @@ def get_parsed_args_vs(options: 'argparse.Namespace', builddir: Path) -> T.Tuple if options.targets: intro_data = parse_introspect_data(builddir) has_run_target = any( - get_target_from_intro_data(ParsedTargetName(t), builddir, intro_data)['type'] == 'run' + get_target_from_intro_data(ParsedTargetName(t), builddir, intro_data)['type'] in {'alias', 'run'} for t in options.targets) if has_run_target: @@ -272,6 +301,8 @@ def get_parsed_args_xcode(options: 'argparse.Namespace', builddir: Path) -> T.Tu cmd += options.xcode_args return cmd, None +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: """Add compile specific arguments.""" parser.add_argument( @@ -279,7 +310,7 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None: metavar='TARGET', nargs='*', default=None, - help='Targets to build. Target has the following format: [PATH_TO_TARGET/]TARGET_NAME[:TARGET_TYPE].') + help='Targets to build. Target has the following format: [PATH_TO_TARGET/]TARGET_NAME.TARGET_SUFFIX[:TARGET_TYPE].') parser.add_argument( '--clean', action='store_true', @@ -332,14 +363,14 @@ def run(options: 'argparse.Namespace') -> int: if options.targets and options.clean: raise MesonException('`TARGET` and `--clean` can\'t be used simultaneously') - cdata = coredata.load(options.wd) b = build.load(options.wd) - vsenv_active = setup_vsenv(b.need_vsenv) - if vsenv_active: + cdata = b.environment.coredata + need_vsenv = T.cast('bool', cdata.get_option(mesonlib.OptionKey('vsenv'))) + if setup_vsenv(need_vsenv): mlog.log(mlog.green('INFO:'), 'automatically activated MSVC compiler environment') - cmd = [] # type: T.List[str] - env = None # type: T.Optional[T.Dict[str, str]] + cmd: T.List[str] = [] + env: T.Optional[T.Dict[str, str]] = None backend = cdata.get_option(mesonlib.OptionKey('backend')) assert isinstance(backend, str) diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py index 54eacef..04f6d5b 100644 --- a/mesonbuild/mconf.py +++ b/mesonbuild/mconf.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import itertools import shutil @@ -25,12 +26,17 @@ from . import environment from . import mesonlib from . import mintro from . import mlog -from .ast import AstIDGenerator +from .ast import AstIDGenerator, IntrospectionInterpreter from .mesonlib import MachineChoice, OptionKey if T.TYPE_CHECKING: import argparse + # cannot be TV_Loggable, because non-ansidecorators do direct string concat + LOGLINE = T.Union[str, mlog.AnsiDecorator] + +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: coredata.register_builtin_arguments(parser) parser.add_argument('builddir', nargs='?', default='.') @@ -39,7 +45,7 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None: parser.add_argument('--no-pager', action='store_false', dest='pager', help='Do not redirect output to a pager') -def stringify(val: T.Any) -> T.Union[str, T.List[T.Any]]: # T.Any because of recursion... +def stringify(val: T.Any) -> str: if isinstance(val, bool): return str(val).lower() elif isinstance(val, list): @@ -56,43 +62,41 @@ class ConfException(mesonlib.MesonException): class Conf: - def __init__(self, build_dir): + def __init__(self, build_dir: str): self.build_dir = os.path.abspath(os.path.realpath(build_dir)) if 'meson.build' in [os.path.basename(self.build_dir), self.build_dir]: self.build_dir = os.path.dirname(self.build_dir) self.build = None self.max_choices_line_length = 60 - self.name_col = [] - self.value_col = [] - self.choices_col = [] - self.descr_col = [] + self.name_col: T.List[LOGLINE] = [] + self.value_col: T.List[LOGLINE] = [] + self.choices_col: T.List[LOGLINE] = [] + self.descr_col: T.List[LOGLINE] = [] self.all_subprojects: T.Set[str] = set() if os.path.isdir(os.path.join(self.build_dir, 'meson-private')): self.build = build.load(self.build_dir) self.source_dir = self.build.environment.get_source_dir() - self.coredata = coredata.load(self.build_dir) + self.coredata = self.build.environment.coredata self.default_values_only = False elif os.path.isfile(os.path.join(self.build_dir, environment.build_filename)): # Make sure that log entries in other parts of meson don't interfere with the JSON output - mlog.disable() - self.source_dir = os.path.abspath(os.path.realpath(self.build_dir)) - intr = mintro.IntrospectionInterpreter(self.source_dir, '', 'ninja', visitors = [AstIDGenerator()]) - intr.analyze() - # Re-enable logging just in case - mlog.enable() + with mlog.no_logging(): + self.source_dir = os.path.abspath(os.path.realpath(self.build_dir)) + intr = IntrospectionInterpreter(self.source_dir, '', 'ninja', visitors = [AstIDGenerator()]) + intr.analyze() self.coredata = intr.coredata self.default_values_only = True else: raise ConfException(f'Directory {build_dir} is neither a Meson build directory nor a project source directory.') - def clear_cache(self): - self.coredata.clear_deps_cache() + def clear_cache(self) -> None: + self.coredata.clear_cache() - def set_options(self, options): - self.coredata.set_options(options) + def set_options(self, options: T.Dict[OptionKey, str]) -> bool: + return self.coredata.set_options(options) - def save(self): + def save(self) -> None: # Do nothing when using introspection if self.default_values_only: return @@ -128,12 +132,16 @@ class Conf: mlog.log(line[0]) continue - def wrap_text(text, width): + def wrap_text(text: LOGLINE, width: int) -> mlog.TV_LoggableList: raw = text.text if isinstance(text, mlog.AnsiDecorator) else text indent = ' ' if raw.startswith('[') else '' - wrapped = textwrap.wrap(raw, width, subsequent_indent=indent) + wrapped_ = textwrap.wrap(raw, width, subsequent_indent=indent) + # We cast this because https://github.com/python/mypy/issues/1965 + # mlog.TV_LoggableList does not provide __len__ for stringprotocol if isinstance(text, mlog.AnsiDecorator): - wrapped = [mlog.AnsiDecorator(i, text.code) for i in wrapped] + wrapped = T.cast('T.List[LOGLINE]', [mlog.AnsiDecorator(i, text.code) for i in wrapped_]) + else: + wrapped = T.cast('T.List[LOGLINE]', wrapped_) # Add padding here to get even rows, as `textwrap.wrap()` will # only shorten, not lengthen each item return [str(i) + ' ' * (width - len(i)) for i in wrapped] @@ -150,15 +158,15 @@ class Conf: items = [l[i] if l[i] else ' ' * four_column[i] for i in range(4)] mlog.log(*items) - def split_options_per_subproject(self, options: 'coredata.KeyedOptionDictType') -> T.Dict[str, 'coredata.KeyedOptionDictType']: - result: T.Dict[str, 'coredata.KeyedOptionDictType'] = {} + def split_options_per_subproject(self, options: 'coredata.KeyedOptionDictType') -> T.Dict[str, 'coredata.MutableKeyedOptionDictType']: + result: T.Dict[str, 'coredata.MutableKeyedOptionDictType'] = {} for k, o in options.items(): if k.subproject: self.all_subprojects.add(k.subproject) result.setdefault(k.subproject, {})[k] = o return result - def _add_line(self, name, value, choices, descr) -> None: + def _add_line(self, name: LOGLINE, value: LOGLINE, choices: LOGLINE, descr: LOGLINE) -> None: if isinstance(name, mlog.AnsiDecorator): name.text = ' ' * self.print_margin + name.text else: @@ -168,21 +176,21 @@ class Conf: self.choices_col.append(choices) self.descr_col.append(descr) - def add_option(self, name, descr, value, choices): + def add_option(self, name: str, descr: str, value: T.Any, choices: T.Any) -> None: value = stringify(value) choices = stringify(choices) self._add_line(mlog.green(name), mlog.yellow(value), mlog.blue(choices), descr) - def add_title(self, title): - title = mlog.cyan(title) + def add_title(self, title: str) -> None: + newtitle = mlog.cyan(title) descr = mlog.cyan('Description') value = mlog.cyan('Default Value' if self.default_values_only else 'Current Value') choices = mlog.cyan('Possible Values') self._add_line('', '', '', '') - self._add_line(title, value, choices, descr) - self._add_line('-' * len(title), '-' * len(value), '-' * len(choices), '-' * len(descr)) + self._add_line(newtitle, value, choices, descr) + self._add_line('-' * len(newtitle), '-' * len(value), '-' * len(choices), '-' * len(descr)) - def add_section(self, section): + def add_section(self, section: str) -> None: self.print_margin = 0 self._add_line('', '', '', '') self._add_line(mlog.normal_yellow(section + ':'), '', '', '') @@ -193,20 +201,23 @@ class Conf: return if title: self.add_title(title) + auto = T.cast('coredata.UserFeatureOption', self.coredata.options[OptionKey('auto_features')]) for k, o in sorted(options.items()): printable_value = o.printable_value() root = k.as_root() if o.yielding and k.subproject and root in self.coredata.options: printable_value = '' + if isinstance(o, coredata.UserFeatureOption) and o.is_auto(): + printable_value = auto.printable_value() self.add_option(str(root), o.description, printable_value, o.choices) - def print_conf(self, pager: bool): + def print_conf(self, pager: bool) -> None: if pager: mlog.start_pager() - def print_default_values_warning(): + def print_default_values_warning() -> None: mlog.warning('The source directory instead of the build directory was specified.') - mlog.warning('Only the default values for the project are printed, and all command line parameters are ignored.') + mlog.warning('Only the default values for the project are printed.') if self.default_values_only: print_default_values_warning() @@ -221,10 +232,10 @@ class Conf: test_option_names = {OptionKey('errorlogs'), OptionKey('stdsplit')} - dir_options: 'coredata.KeyedOptionDictType' = {} - test_options: 'coredata.KeyedOptionDictType' = {} - core_options: 'coredata.KeyedOptionDictType' = {} - module_options: T.Dict[str, 'coredata.KeyedOptionDictType'] = collections.defaultdict(dict) + dir_options: 'coredata.MutableKeyedOptionDictType' = {} + test_options: 'coredata.MutableKeyedOptionDictType' = {} + core_options: 'coredata.MutableKeyedOptionDictType' = {} + module_options: T.Dict[str, 'coredata.MutableKeyedOptionDictType'] = collections.defaultdict(dict) for k, v in self.coredata.options.items(): if k in dir_option_names: dir_options[k] = v @@ -283,7 +294,7 @@ class Conf: self.print_nondefault_buildtype_options() - def print_nondefault_buildtype_options(self): + def print_nondefault_buildtype_options(self) -> None: mismatching = self.coredata.get_nondefault_buildtype_args() if not mismatching: return @@ -292,26 +303,24 @@ class Conf: for m in mismatching: mlog.log(f'{m[0]:21}{m[1]:10}{m[2]:10}') -def run(options): - coredata.parse_cmd_line_options(options) - builddir = os.path.abspath(os.path.realpath(options.builddir)) +def run_impl(options: argparse.Namespace, builddir: str) -> int: + print_only = not options.cmd_line_options and not options.clearcache c = None try: c = Conf(builddir) - if c.default_values_only: + if c.default_values_only and not print_only: + raise mesonlib.MesonException('No valid build directory found, cannot modify options.') + if c.default_values_only or print_only: c.print_conf(options.pager) return 0 save = False if options.cmd_line_options: - c.set_options(options.cmd_line_options) + save = c.set_options(options.cmd_line_options) coredata.update_cmd_line_file(builddir, options) - save = True - elif options.clearcache: + if options.clearcache: c.clear_cache() save = True - else: - c.print_conf(options.pager) if save: c.save() mintro.update_build_options(c.coredata, c.build.environment.info_dir) @@ -325,3 +334,8 @@ def run(options): # Pager quit before we wrote everything. pass return 0 + +def run(options: argparse.Namespace) -> int: + coredata.parse_cmd_line_options(options) + builddir = os.path.abspath(os.path.realpath(options.builddir)) + return run_impl(options, builddir) diff --git a/mesonbuild/mdevenv.py b/mesonbuild/mdevenv.py index b25d73b..9f3d1b9 100644 --- a/mesonbuild/mdevenv.py +++ b/mesonbuild/mdevenv.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os, subprocess import argparse import tempfile @@ -5,8 +7,8 @@ import shutil import itertools from pathlib import Path -from . import build, minstall, dependencies -from .mesonlib import (MesonException, is_windows, setup_vsenv, OptionKey, +from . import build, minstall +from .mesonlib import (EnvironmentVariables, MesonException, is_windows, setup_vsenv, OptionKey, get_wine_shortpath, MachineChoice) from . import mlog @@ -16,13 +18,19 @@ if T.TYPE_CHECKING: POWERSHELL_EXES = {'pwsh.exe', 'powershell.exe'} +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument('-C', dest='builddir', type=Path, default='.', help='Path to build directory') parser.add_argument('--workdir', '-w', type=Path, default=None, help='Directory to cd into before running (default: builddir, Since 1.0.0)') - parser.add_argument('--dump', action='store_true', - help='Only print required environment (Since 0.62.0)') + parser.add_argument('--dump', nargs='?', const=True, + help='Only print required environment (Since 0.62.0) ' + + 'Takes an optional file path (Since 1.1.0)') + parser.add_argument('--dump-format', default='export', + choices=['sh', 'export', 'vscode'], + help='Format used with --dump (Since 1.1.0)') parser.add_argument('devcmd', nargs=argparse.REMAINDER, metavar='command', help='Command to run in developer environment (default: interactive shell)') @@ -48,8 +56,8 @@ def reduce_winepath(env: T.Dict[str, str]) -> None: env['WINEPATH'] = get_wine_shortpath([winecmd], winepath.split(';')) mlog.log('Meson detected wine and has set WINEPATH accordingly') -def get_env(b: build.Build, dump: bool) -> T.Tuple[T.Dict[str, str], T.Set[str]]: - extra_env = build.EnvironmentVariables() +def get_env(b: build.Build, dump_fmt: T.Optional[str]) -> T.Tuple[T.Dict[str, str], T.Set[str]]: + extra_env = EnvironmentVariables() extra_env.set('MESON_DEVENV', ['1']) extra_env.set('MESON_PROJECT_NAME', [b.project_name]) @@ -57,10 +65,11 @@ def get_env(b: build.Build, dump: bool) -> T.Tuple[T.Dict[str, str], T.Set[str]] if sysroot: extra_env.set('QEMU_LD_PREFIX', [sysroot]) - env = {} if dump else os.environ.copy() + env = {} if dump_fmt else os.environ.copy() + default_fmt = '${0}' if dump_fmt in {'sh', 'export'} else None varnames = set() for i in itertools.chain(b.devenv, {extra_env}): - env = i.get_env(env, dump) + env = i.get_env(env, default_fmt) varnames |= i.get_names() reduce_winepath(env) @@ -68,16 +77,17 @@ def get_env(b: build.Build, dump: bool) -> T.Tuple[T.Dict[str, str], T.Set[str]] return env, varnames def bash_completion_files(b: build.Build, install_data: 'InstallData') -> T.List[str]: + from .dependencies.pkgconfig import PkgConfigDependency result = [] - dep = dependencies.PkgConfigDependency('bash-completion', b.environment, - {'required': False, 'silent': True, 'version': '>=2.10'}) + dep = PkgConfigDependency('bash-completion', b.environment, + {'required': False, 'silent': True, 'version': '>=2.10'}) if dep.found(): prefix = b.environment.coredata.get_option(OptionKey('prefix')) assert isinstance(prefix, str), 'for mypy' datadir = b.environment.coredata.get_option(OptionKey('datadir')) assert isinstance(datadir, str), 'for mypy' datadir_abs = os.path.join(prefix, datadir) - completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=['datadir', datadir_abs]) + completionsdir = dep.get_variable(pkgconfig='completionsdir', pkgconfig_define=(('datadir', datadir_abs),)) assert isinstance(completionsdir, str), 'for mypy' completionsdir_path = Path(completionsdir) for f in install_data.data: @@ -138,6 +148,12 @@ def write_gdb_script(privatedir: Path, install_data: 'InstallData', workdir: Pat mlog.log(' - Change current workdir to', mlog.bold(str(rel_path.parent)), 'or use', mlog.bold(f'--init-command {rel_path}')) +def dump(devenv: T.Dict[str, str], varnames: T.Set[str], dump_format: T.Optional[str], output: T.Optional[T.TextIO] = None) -> None: + for name in varnames: + print(f'{name}="{devenv[name]}"', file=output) + if dump_format == 'export': + print(f'export {name}', file=output) + def run(options: argparse.Namespace) -> int: privatedir = Path(options.builddir) / 'meson-private' buildfile = privatedir / 'build.dat' @@ -146,13 +162,18 @@ def run(options: argparse.Namespace) -> int: b = build.load(options.builddir) workdir = options.workdir or options.builddir - devenv, varnames = get_env(b, options.dump) + need_vsenv = T.cast('bool', b.environment.coredata.get_option(OptionKey('vsenv'))) + setup_vsenv(need_vsenv) # Call it before get_env to get vsenv vars as well + dump_fmt = options.dump_format if options.dump else None + devenv, varnames = get_env(b, dump_fmt) if options.dump: if options.devcmd: raise MesonException('--dump option does not allow running other command.') - for name in varnames: - print(f'{name}="{devenv[name]}"') - print(f'export {name}') + if options.dump is True: + dump(devenv, varnames, dump_fmt) + else: + with open(options.dump, "w", encoding='utf-8') as output: + dump(devenv, varnames, dump_fmt, output) return 0 if b.environment.need_exe_wrapper(): @@ -166,8 +187,6 @@ def run(options: argparse.Namespace) -> int: install_data = minstall.load_install_data(str(privatedir / 'install.dat')) write_gdb_script(privatedir, install_data, workdir) - setup_vsenv(b.need_vsenv) - args = options.devcmd if not args: prompt_prefix = f'[{b.project_name}]' diff --git a/mesonbuild/mdist.py b/mesonbuild/mdist.py index 0aa4d07..c46670e 100644 --- a/mesonbuild/mdist.py +++ b/mesonbuild/mdist.py @@ -11,8 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import abc import argparse import gzip import os @@ -23,23 +25,32 @@ import subprocess import tarfile import tempfile import hashlib +import typing as T + +from dataclasses import dataclass from glob import glob from pathlib import Path from mesonbuild.environment import detect_ninja from mesonbuild.mesonlib import (MesonException, RealPathAction, quiet_git, - windows_proof_rmtree, setup_vsenv) + windows_proof_rmtree, setup_vsenv, OptionKey) from mesonbuild.msetup import add_arguments as msetup_argparse from mesonbuild.wrap import wrap from mesonbuild import mlog, build, coredata from .scripts.meson_exe import run_exe +if T.TYPE_CHECKING: + from ._typing import ImmutableListProtocol + from .mesonlib import ExecutableSerialisation + archive_choices = ['gztar', 'xztar', 'zip'] archive_extension = {'gztar': '.tar.gz', 'xztar': '.tar.xz', 'zip': '.zip'} -def add_arguments(parser): +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ +def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument('-C', dest='wd', action=RealPathAction, help='directory to cd into before running') parser.add_argument('--allow-dirty', action='store_true', @@ -52,7 +63,7 @@ def add_arguments(parser): help='Do not build and test generated packages.') -def create_hash(fname): +def create_hash(fname: str) -> None: hashname = fname + '.sha256sum' m = hashlib.sha256() m.update(open(fname, 'rb').read()) @@ -62,178 +73,200 @@ def create_hash(fname): f.write('{} *{}\n'.format(m.hexdigest(), os.path.basename(fname))) -def copy_git(src, distdir, revision='HEAD', prefix=None, subdir=None): - cmd = ['git', 'archive', '--format', 'tar', revision] - if prefix is not None: - cmd.insert(2, f'--prefix={prefix}/') - if subdir is not None: - cmd.extend(['--', subdir]) - with tempfile.TemporaryFile() as f: - subprocess.check_call(cmd, cwd=src, stdout=f) - f.seek(0) - t = tarfile.open(fileobj=f) # [ignore encoding] - t.extractall(path=distdir) - msg_uncommitted_changes = 'Repository has uncommitted changes that will not be included in the dist tarball' -def handle_dirty_opt(msg, allow_dirty: bool): +def handle_dirty_opt(msg: str, allow_dirty: bool) -> None: if allow_dirty: mlog.warning(msg) else: mlog.error(msg + '\n' + 'Use --allow-dirty to ignore the warning and proceed anyway') sys.exit(1) -def process_submodules(src, distdir, options): - module_file = os.path.join(src, '.gitmodules') - if not os.path.exists(module_file): - return - cmd = ['git', 'submodule', 'status', '--cached', '--recursive'] - modlist = subprocess.check_output(cmd, cwd=src, universal_newlines=True).splitlines() - for submodule in modlist: - status = submodule[:1] - sha1, rest = submodule[1:].split(' ', 1) - subpath = rest.rsplit(' ', 1)[0] - - if status == '-': - mlog.warning(f'Submodule {subpath!r} is not checked out and cannot be added to the dist') - continue - elif status in {'+', 'U'}: - handle_dirty_opt(f'Submodule {subpath!r} has uncommitted changes that will not be included in the dist tarball', options.allow_dirty) - - copy_git(os.path.join(src, subpath), distdir, revision=sha1, prefix=subpath) - - -def run_dist_scripts(src_root, bld_root, dist_root, dist_scripts, subprojects): - assert os.path.isabs(dist_root) - env = {} - env['MESON_DIST_ROOT'] = dist_root - env['MESON_SOURCE_ROOT'] = src_root - env['MESON_BUILD_ROOT'] = bld_root - for d in dist_scripts: - if d.subproject and d.subproject not in subprojects: - continue - subdir = subprojects.get(d.subproject, '') - env['MESON_PROJECT_DIST_ROOT'] = os.path.join(dist_root, subdir) - env['MESON_PROJECT_SOURCE_ROOT'] = os.path.join(src_root, subdir) - env['MESON_PROJECT_BUILD_ROOT'] = os.path.join(bld_root, subdir) - name = ' '.join(d.cmd_args) - print(f'Running custom dist script {name!r}') - try: - rc = run_exe(d, env) - if rc != 0: - sys.exit('Dist script errored out') - except OSError: - print(f'Failed to run dist script {name!r}') - sys.exit(1) - -def git_root(src_root): - # Cannot use --show-toplevel here because git in our CI prints cygwin paths - # that python cannot resolve. Workaround this by taking parent of src_root. - prefix = quiet_git(['rev-parse', '--show-prefix'], src_root, check=True)[1].strip() - if not prefix: - return Path(src_root) - prefix_level = len(Path(prefix).parents) - return Path(src_root).parents[prefix_level - 1] - -def is_git(src_root): +def is_git(src_root: str) -> bool: ''' Checks if meson.build file at the root source directory is tracked by git. It could be a subproject part of the parent project git repository. ''' return quiet_git(['ls-files', '--error-unmatch', 'meson.build'], src_root)[0] -def git_have_dirty_index(src_root): - '''Check whether there are uncommitted changes in git''' - ret = subprocess.call(['git', '-C', src_root, 'diff-index', '--quiet', 'HEAD']) - return ret == 1 - -def process_git_project(src_root, distdir, options): - if git_have_dirty_index(src_root): - handle_dirty_opt(msg_uncommitted_changes, options.allow_dirty) - if os.path.exists(distdir): - windows_proof_rmtree(distdir) - repo_root = git_root(src_root) - if repo_root.samefile(src_root): - os.makedirs(distdir) - copy_git(src_root, distdir) - else: - subdir = Path(src_root).relative_to(repo_root) - tmp_distdir = distdir + '-tmp' - if os.path.exists(tmp_distdir): - windows_proof_rmtree(tmp_distdir) - os.makedirs(tmp_distdir) - copy_git(repo_root, tmp_distdir, subdir=str(subdir)) - Path(tmp_distdir, subdir).rename(distdir) - windows_proof_rmtree(tmp_distdir) - process_submodules(src_root, distdir, options) - -def create_dist_git(dist_name, archives, src_root, bld_root, dist_sub, dist_scripts, subprojects, options): - distdir = os.path.join(dist_sub, dist_name) - process_git_project(src_root, distdir, options) - for path in subprojects.values(): - sub_src_root = os.path.join(src_root, path) - sub_distdir = os.path.join(distdir, path) - if os.path.exists(sub_distdir): - continue - if is_git(sub_src_root): - process_git_project(sub_src_root, sub_distdir, options) - else: - shutil.copytree(sub_src_root, sub_distdir) - run_dist_scripts(src_root, bld_root, distdir, dist_scripts, subprojects) - output_names = [] - for a in archives: - compressed_name = distdir + archive_extension[a] - shutil.make_archive(distdir, a, root_dir=dist_sub, base_dir=dist_name) - output_names.append(compressed_name) - windows_proof_rmtree(distdir) - return output_names - -def is_hg(src_root): +def is_hg(src_root: str) -> bool: return os.path.isdir(os.path.join(src_root, '.hg')) -def hg_have_dirty_index(src_root): - '''Check whether there are uncommitted changes in hg''' - out = subprocess.check_output(['hg', '-R', src_root, 'summary']) - return b'commit: (clean)' not in out - -def create_dist_hg(dist_name, archives, src_root, bld_root, dist_sub, dist_scripts, options): - if hg_have_dirty_index(src_root): - handle_dirty_opt(msg_uncommitted_changes, options.allow_dirty) - if dist_scripts: - mlog.warning('dist scripts are not supported in Mercurial projects') - - os.makedirs(dist_sub, exist_ok=True) - tarname = os.path.join(dist_sub, dist_name + '.tar') - xzname = tarname + '.xz' - gzname = tarname + '.gz' - zipname = os.path.join(dist_sub, dist_name + '.zip') - # Note that -X interprets relative paths using the current working - # directory, not the repository root, so this must be an absolute path: - # https://bz.mercurial-scm.org/show_bug.cgi?id=6267 - # - # .hg[a-z]* is used instead of .hg* to keep .hg_archival.txt, which may - # be useful to link the tarball to the Mercurial revision for either - # manual inspection or in case any code interprets it for a --version or - # similar. - subprocess.check_call(['hg', 'archive', '-R', src_root, '-S', '-t', 'tar', - '-X', src_root + '/.hg[a-z]*', tarname]) - output_names = [] - if 'xztar' in archives: - import lzma - with lzma.open(xzname, 'wb') as xf, open(tarname, 'rb') as tf: - shutil.copyfileobj(tf, xf) - output_names.append(xzname) - if 'gztar' in archives: - with gzip.open(gzname, 'wb') as zf, open(tarname, 'rb') as tf: - shutil.copyfileobj(tf, zf) - output_names.append(gzname) - os.unlink(tarname) - if 'zip' in archives: - subprocess.check_call(['hg', 'archive', '-R', src_root, '-S', '-t', 'zip', zipname]) - output_names.append(zipname) - return output_names - -def run_dist_steps(meson_command, unpacked_src_dir, builddir, installdir, ninja_args): + +@dataclass +class Dist(metaclass=abc.ABCMeta): + dist_name: str + src_root: str + bld_root: str + dist_scripts: T.List[ExecutableSerialisation] + subprojects: T.Dict[str, str] + options: argparse.Namespace + + def __post_init__(self) -> None: + self.dist_sub = os.path.join(self.bld_root, 'meson-dist') + self.distdir = os.path.join(self.dist_sub, self.dist_name) + + @abc.abstractmethod + def create_dist(self, archives: T.List[str]) -> T.List[str]: + pass + + def run_dist_scripts(self) -> None: + assert os.path.isabs(self.distdir) + env = {} + env['MESON_DIST_ROOT'] = self.distdir + env['MESON_SOURCE_ROOT'] = self.src_root + env['MESON_BUILD_ROOT'] = self.bld_root + for d in self.dist_scripts: + if d.subproject and d.subproject not in self.subprojects: + continue + subdir = self.subprojects.get(d.subproject, '') + env['MESON_PROJECT_DIST_ROOT'] = os.path.join(self.distdir, subdir) + env['MESON_PROJECT_SOURCE_ROOT'] = os.path.join(self.src_root, subdir) + env['MESON_PROJECT_BUILD_ROOT'] = os.path.join(self.bld_root, subdir) + name = ' '.join(d.cmd_args) + print(f'Running custom dist script {name!r}') + try: + rc = run_exe(d, env) + if rc != 0: + sys.exit('Dist script errored out') + except OSError: + print(f'Failed to run dist script {name!r}') + sys.exit(1) + + +class GitDist(Dist): + def git_root(self, dir_: str) -> Path: + # Cannot use --show-toplevel here because git in our CI prints cygwin paths + # that python cannot resolve. Workaround this by taking parent of src_root. + prefix = quiet_git(['rev-parse', '--show-prefix'], dir_, check=True)[1].strip() + if not prefix: + return Path(dir_) + prefix_level = len(Path(prefix).parents) + return Path(dir_).parents[prefix_level - 1] + + def have_dirty_index(self) -> bool: + '''Check whether there are uncommitted changes in git''' + ret = subprocess.call(['git', '-C', self.src_root, 'diff-index', '--quiet', 'HEAD']) + return ret == 1 + + def copy_git(self, src: T.Union[str, os.PathLike], distdir: str, revision: str = 'HEAD', + prefix: T.Optional[str] = None, subdir: T.Optional[str] = None) -> None: + cmd = ['git', 'archive', '--format', 'tar', revision] + if prefix is not None: + cmd.insert(2, f'--prefix={prefix}/') + if subdir is not None: + cmd.extend(['--', subdir]) + with tempfile.TemporaryFile() as f: + subprocess.check_call(cmd, cwd=src, stdout=f) + f.seek(0) + t = tarfile.open(fileobj=f) # [ignore encoding] + t.extractall(path=distdir) + + def process_git_project(self, src_root: str, distdir: str) -> None: + if self.have_dirty_index(): + handle_dirty_opt(msg_uncommitted_changes, self.options.allow_dirty) + if os.path.exists(distdir): + windows_proof_rmtree(distdir) + repo_root = self.git_root(src_root) + if repo_root.samefile(src_root): + os.makedirs(distdir) + self.copy_git(src_root, distdir) + else: + subdir = Path(src_root).relative_to(repo_root) + tmp_distdir = distdir + '-tmp' + if os.path.exists(tmp_distdir): + windows_proof_rmtree(tmp_distdir) + os.makedirs(tmp_distdir) + self.copy_git(repo_root, tmp_distdir, subdir=str(subdir)) + Path(tmp_distdir, subdir).rename(distdir) + windows_proof_rmtree(tmp_distdir) + self.process_submodules(src_root, distdir) + + def process_submodules(self, src: str, distdir: str) -> None: + module_file = os.path.join(src, '.gitmodules') + if not os.path.exists(module_file): + return + cmd = ['git', 'submodule', 'status', '--cached', '--recursive'] + modlist = subprocess.check_output(cmd, cwd=src, universal_newlines=True).splitlines() + for submodule in modlist: + status = submodule[:1] + sha1, rest = submodule[1:].split(' ', 1) + subpath = rest.rsplit(' ', 1)[0] + + if status == '-': + mlog.warning(f'Submodule {subpath!r} is not checked out and cannot be added to the dist') + continue + elif status in {'+', 'U'}: + handle_dirty_opt(f'Submodule {subpath!r} has uncommitted changes that will not be included in the dist tarball', self.options.allow_dirty) + + self.copy_git(os.path.join(src, subpath), distdir, revision=sha1, prefix=subpath) + + def create_dist(self, archives: T.List[str]) -> T.List[str]: + self.process_git_project(self.src_root, self.distdir) + for path in self.subprojects.values(): + sub_src_root = os.path.join(self.src_root, path) + sub_distdir = os.path.join(self.distdir, path) + if os.path.exists(sub_distdir): + continue + if is_git(sub_src_root): + self.process_git_project(sub_src_root, sub_distdir) + else: + shutil.copytree(sub_src_root, sub_distdir) + self.run_dist_scripts() + output_names = [] + for a in archives: + compressed_name = self.distdir + archive_extension[a] + shutil.make_archive(self.distdir, a, root_dir=self.dist_sub, base_dir=self.dist_name) + output_names.append(compressed_name) + windows_proof_rmtree(self.distdir) + return output_names + + +class HgDist(Dist): + def have_dirty_index(self) -> bool: + '''Check whether there are uncommitted changes in hg''' + out = subprocess.check_output(['hg', '-R', self.src_root, 'summary']) + return b'commit: (clean)' not in out + + def create_dist(self, archives: T.List[str]) -> T.List[str]: + if self.have_dirty_index(): + handle_dirty_opt(msg_uncommitted_changes, self.options.allow_dirty) + if self.dist_scripts: + mlog.warning('dist scripts are not supported in Mercurial projects') + + os.makedirs(self.dist_sub, exist_ok=True) + tarname = os.path.join(self.dist_sub, self.dist_name + '.tar') + xzname = tarname + '.xz' + gzname = tarname + '.gz' + zipname = os.path.join(self.dist_sub, self.dist_name + '.zip') + # Note that -X interprets relative paths using the current working + # directory, not the repository root, so this must be an absolute path: + # https://bz.mercurial-scm.org/show_bug.cgi?id=6267 + # + # .hg[a-z]* is used instead of .hg* to keep .hg_archival.txt, which may + # be useful to link the tarball to the Mercurial revision for either + # manual inspection or in case any code interprets it for a --version or + # similar. + subprocess.check_call(['hg', 'archive', '-R', self.src_root, '-S', '-t', 'tar', + '-X', self.src_root + '/.hg[a-z]*', tarname]) + output_names = [] + if 'xztar' in archives: + import lzma + with lzma.open(xzname, 'wb') as xf, open(tarname, 'rb') as tf: + shutil.copyfileobj(tf, xf) + output_names.append(xzname) + if 'gztar' in archives: + with gzip.open(gzname, 'wb') as zf, open(tarname, 'rb') as tf: + shutil.copyfileobj(tf, zf) + output_names.append(gzname) + os.unlink(tarname) + if 'zip' in archives: + subprocess.check_call(['hg', 'archive', '-R', self.src_root, '-S', '-t', 'zip', zipname]) + output_names.append(zipname) + return output_names + + +def run_dist_steps(meson_command: T.List[str], unpacked_src_dir: str, builddir: str, installdir: str, ninja_args: T.List[str]) -> int: if subprocess.call(meson_command + ['--backend=ninja', unpacked_src_dir, builddir]) != 0: print('Running Meson on distribution package failed') return 1 @@ -250,7 +283,7 @@ def run_dist_steps(meson_command, unpacked_src_dir, builddir, installdir, ninja_ return 1 return 0 -def check_dist(packagename, meson_command, extra_meson_args, bld_root, privdir): +def check_dist(packagename: str, meson_command: ImmutableListProtocol[str], extra_meson_args: T.List[str], bld_root: str, privdir: str) -> int: print(f'Testing distribution package {packagename}') unpackdir = os.path.join(privdir, 'dist-unpack') builddir = os.path.join(privdir, 'dist-build') @@ -264,6 +297,7 @@ def check_dist(packagename, meson_command, extra_meson_args, bld_root, privdir): unpacked_files = glob(os.path.join(unpackdir, '*')) assert len(unpacked_files) == 1 unpacked_src_dir = unpacked_files[0] + meson_command += ['setup'] meson_command += create_cmdline_args(bld_root) meson_command += extra_meson_args @@ -277,16 +311,16 @@ def check_dist(packagename, meson_command, extra_meson_args, bld_root, privdir): print(f'Distribution package {packagename} tested') return ret -def create_cmdline_args(bld_root): +def create_cmdline_args(bld_root: str) -> T.List[str]: parser = argparse.ArgumentParser() msetup_argparse(parser) args = parser.parse_args([]) coredata.parse_cmd_line_options(args) coredata.read_cmd_line_file(bld_root, args) - args.cmd_line_options.pop(coredata.OptionKey('backend'), '') + args.cmd_line_options.pop(OptionKey('backend'), '') return shlex.split(coredata.format_cmd_line_options(args)) -def determine_archives_to_generate(options): +def determine_archives_to_generate(options: argparse.Namespace) -> T.List[str]: result = [] for i in options.formats.split(','): if i not in archive_choices: @@ -296,19 +330,19 @@ def determine_archives_to_generate(options): sys.exit('No archive types specified.') return result -def run(options): +def run(options: argparse.Namespace) -> int: buildfile = Path(options.wd) / 'meson-private' / 'build.dat' if not buildfile.is_file(): raise MesonException(f'Directory {options.wd!r} does not seem to be a Meson build directory.') b = build.load(options.wd) - setup_vsenv(b.need_vsenv) + need_vsenv = T.cast('bool', b.environment.coredata.get_option(OptionKey('vsenv'))) + setup_vsenv(need_vsenv) # This import must be load delayed, otherwise it will get the default # value of None. from mesonbuild.mesonlib import get_meson_command src_root = b.environment.source_dir bld_root = b.environment.build_dir priv_dir = os.path.join(bld_root, 'meson-private') - dist_sub = os.path.join(bld_root, 'meson-dist') dist_name = b.project_name + '-' + b.project_version @@ -323,16 +357,21 @@ def run(options): subprojects[sub] = os.path.join(b.subproject_dir, directory) extra_meson_args.append('-Dwrap_mode=nodownload') + cls: T.Type[Dist] if is_git(src_root): - names = create_dist_git(dist_name, archives, src_root, bld_root, dist_sub, b.dist_scripts, subprojects, options) + cls = GitDist elif is_hg(src_root): if subprojects: print('--include-subprojects option currently not supported with Mercurial') return 1 - names = create_dist_hg(dist_name, archives, src_root, bld_root, dist_sub, b.dist_scripts, options) + cls = HgDist else: print('Dist currently only works with Git or Mercurial repos') return 1 + + project = cls(dist_name, src_root, bld_root, b.dist_scripts, subprojects, options) + names = project.create_dist(archives) + if names is None: return 1 rc = 0 diff --git a/mesonbuild/mesondata.py b/mesonbuild/mesondata.py index 508c041..da641fd 100644 --- a/mesonbuild/mesondata.py +++ b/mesonbuild/mesondata.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import importlib.resources diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py index ca38f29..72a7ab9 100644 --- a/mesonbuild/mesonmain.py +++ b/mesonbuild/mesonmain.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations # Work around some pathlib bugs... @@ -139,11 +140,11 @@ class CommandLineParser: parser.add_argument('script_args', nargs=argparse.REMAINDER) def run_runpython_command(self, options): - import runpy + sys.argv[1:] = options.script_args if options.eval_arg: exec(options.script_file) else: - sys.argv[1:] = options.script_args + import runpy sys.path.insert(0, os.path.dirname(options.script_file)) runpy.run_path(options.script_file, run_name='__main__') return 0 @@ -235,6 +236,13 @@ def set_meson_command(mainfile): mesonlib.set_meson_command(mainfile) def run(original_args, mainfile): + if os.environ.get('MESON_SHOW_DEPRECATIONS'): + # workaround for https://bugs.python.org/issue34624 + import warnings + for typ in [DeprecationWarning, SyntaxWarning, FutureWarning, PendingDeprecationWarning]: + warnings.filterwarnings('error', category=typ, module='mesonbuild') + warnings.filterwarnings('ignore', message=".*importlib-resources.*") + if sys.version_info >= (3, 10) and os.environ.get('MESON_RUNNING_IN_PROJECT_TESTS'): # workaround for https://bugs.python.org/issue34624 import warnings @@ -282,7 +290,7 @@ def main(): assert os.path.isabs(sys.executable) launcher = sys.executable else: - launcher = os.path.realpath(sys.argv[0]) + launcher = os.path.abspath(sys.argv[0]) return run(sys.argv[1:], launcher) if __name__ == '__main__': diff --git a/mesonbuild/minit.py b/mesonbuild/minit.py index 1897950..39027fd 100644 --- a/mesonbuild/minit.py +++ b/mesonbuild/minit.py @@ -14,6 +14,8 @@ """Code that creates simple startup projects.""" +from __future__ import annotations + from pathlib import Path from enum import Enum import subprocess @@ -22,19 +24,33 @@ import sys import os import re from glob import glob -from mesonbuild import mesonlib +import typing as T + +from mesonbuild import build, mesonlib, mlog from mesonbuild.coredata import FORBIDDEN_TARGET_NAMES from mesonbuild.environment import detect_ninja -from mesonbuild.templates.samplefactory import sameple_generator -import typing as T +from mesonbuild.templates.mesontemplates import create_meson_build +from mesonbuild.templates.samplefactory import sample_generator if T.TYPE_CHECKING: import argparse -''' -we currently have one meson template at this time. -''' -from mesonbuild.templates.mesontemplates import create_meson_build + from typing_extensions import Protocol, Literal + + class Arguments(Protocol): + + srcfiles: T.List[Path] + wd: str + name: str + executable: str + deps: str + language: Literal['c', 'cpp', 'cs', 'cuda', 'd', 'fortran', 'java', 'rust', 'objc', 'objcpp', 'vala'] + build: bool + builddir: str + force: bool + type: Literal['executable', 'library'] + version: str + FORTRAN_SUFFIXES = {'.f', '.for', '.F', '.f90', '.F90'} LANG_SUFFIXES = {'.c', '.cc', '.cpp', '.cs', '.cu', '.d', '.m', '.mm', '.rs', '.java', '.vala'} | FORTRAN_SUFFIXES @@ -54,12 +70,12 @@ meson compile -C builddir ''' -def create_sample(options: 'argparse.Namespace') -> None: +def create_sample(options: Arguments) -> None: ''' Based on what arguments are passed we check for a match in language then check for project type and create new Meson samples project. ''' - sample_gen = sameple_generator(options) + sample_gen = sample_generator(options) if options.type == DEFAULT_TYPES['EXE'].value: sample_gen.create_executable() elif options.type == DEFAULT_TYPES['LIB'].value: @@ -68,7 +84,7 @@ def create_sample(options: 'argparse.Namespace') -> None: raise RuntimeError('Unreachable code') print(INFO_MESSAGE) -def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> None: +def autodetect_options(options: Arguments, sample: bool = False) -> None: ''' Here we autodetect options for args not passed in so don't have to think about it. @@ -89,7 +105,7 @@ def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> N # The rest of the autodetection is not applicable to generating sample projects. return if not options.srcfiles: - srcfiles = [] + srcfiles: T.List[Path] = [] for f in (f for f in Path().iterdir() if f.is_file()): if f.suffix in LANG_SUFFIXES: srcfiles.append(f) @@ -98,7 +114,6 @@ def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> N 'Run meson init in an empty directory to create a sample project.') options.srcfiles = srcfiles print("Detected source files: " + ' '.join(str(s) for s in srcfiles)) - options.srcfiles = [Path(f) for f in options.srcfiles] if not options.language: for f in options.srcfiles: if f.suffix == '.c': @@ -138,12 +153,14 @@ def autodetect_options(options: 'argparse.Namespace', sample: bool = False) -> N raise SystemExit("Can't autodetect language, please specify it with -l.") print("Detected language: " + options.language) +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: ''' Here we add args for that the user can passed when making a new Meson project. ''' - parser.add_argument("srcfiles", metavar="sourcefile", nargs="*", help="source files. default: all recognized files in current directory") + parser.add_argument("srcfiles", metavar="sourcefile", nargs="*", type=Path, help="source files. default: all recognized files in current directory") parser.add_argument('-C', dest='wd', action=mesonlib.RealPathAction, help='directory to cd into before running') parser.add_argument("-n", "--name", help="project name. default: name of current directory") @@ -156,7 +173,7 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None: parser.add_argument('--type', default=DEFAULT_PROJECT, choices=('executable', 'library'), help=f"project type. default: {DEFAULT_PROJECT} based project") parser.add_argument('--version', default=DEFAULT_VERSION, help=f"project version. default: {DEFAULT_VERSION}") -def run(options: 'argparse.Namespace') -> int: +def run(options: Arguments) -> int: ''' Here we generate the new Meson sample project. ''' @@ -180,10 +197,17 @@ def run(options: 'argparse.Namespace') -> int: print('Build directory already exists, deleting it.') shutil.rmtree(options.builddir) print('Building...') - cmd = mesonlib.get_meson_command() + [options.builddir] + cmd = mesonlib.get_meson_command() + ['setup', options.builddir] ret = subprocess.run(cmd) if ret.returncode: raise SystemExit + + b = build.load(options.builddir) + need_vsenv = T.cast('bool', b.environment.coredata.get_option(mesonlib.OptionKey('vsenv'))) + vsenv_active = mesonlib.setup_vsenv(need_vsenv) + if vsenv_active: + mlog.log(mlog.green('INFO:'), 'automatically activated MSVC compiler environment') + cmd = detect_ninja() + ['-C', options.builddir] ret = subprocess.run(cmd) if ret.returncode: diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 8c74990..297afb4 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -14,20 +14,21 @@ from __future__ import annotations from glob import glob -from pathlib import Path import argparse import errno import os +import selectors import shlex import shutil import subprocess import sys import typing as T +import re -from . import build -from . import environment +from . import build, environment from .backend.backends import InstallData -from .mesonlib import MesonException, Popen_safe, RealPathAction, is_windows, setup_vsenv, pickle_load, is_osx +from .mesonlib import (MesonException, Popen_safe, RealPathAction, is_windows, + is_aix, setup_vsenv, pickle_load, is_osx, OptionKey) from .scripts import depfixer, destdir_join from .scripts.meson_exe import run_exe try: @@ -39,10 +40,10 @@ except ImportError: if T.TYPE_CHECKING: from .backend.backends import ( - ExecutableSerialisation, InstallDataBase, InstallEmptyDir, + InstallDataBase, InstallEmptyDir, InstallSymlinkData, TargetInstallData ) - from .mesonlib import FileMode + from .mesonlib import FileMode, EnvironOrDict, ExecutableSerialisation try: from typing import Protocol @@ -63,12 +64,16 @@ if T.TYPE_CHECKING: strip: bool -symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, -but this will be changed in a future version of Meson to copy the symlink as is. Please update your -build definitions so that it will not break when the change happens.''' +symlink_warning = '''\ +Warning: trying to copy a symlink that points to a file. This currently copies +the file by default, but will be changed in a future version of Meson to copy +the link instead. Set follow_symlinks to true to preserve current behavior, or +false to copy the link.''' selinux_updates: T.List[str] = [] +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument('-C', dest='wd', action=RealPathAction, help='directory to cd into before running') @@ -130,9 +135,7 @@ class DirMaker: def load_install_data(fname: str) -> InstallData: - obj = pickle_load(fname, 'InstallData', InstallData) - assert isinstance(obj, InstallData), 'fo mypy' - return obj + return pickle_load(fname, 'InstallData', InstallData) def is_executable(path: str, follow_symlinks: bool = False) -> bool: '''Checks whether any of the "x" bits are set in the source file mode.''' @@ -209,10 +212,10 @@ def set_mode(path: str, mode: T.Optional['FileMode'], default_umask: T.Union[str except PermissionError as e: print(f'{path!r}: Unable to set owner {mode.owner!r} and group {mode.group!r}: {e.strerror}, ignoring...') except LookupError: - print(f'{path!r}: Non-existent owner {mode.owner!r} or group {mode.group!r}: ignoring...') + print(f'{path!r}: Nonexistent owner {mode.owner!r} or group {mode.group!r}: ignoring...') except OSError as e: if e.errno == errno.EINVAL: - print(f'{path!r}: Non-existent numeric owner {mode.owner!r} or group {mode.group!r}: ignoring...') + print(f'{path!r}: Nonexistent numeric owner {mode.owner!r} or group {mode.group!r}: ignoring...') else: raise # Must set permissions *after* setting owner/group otherwise the @@ -361,9 +364,9 @@ class Installer: return p.returncode, o, e return 0, '', '' - def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int: - if not self.dry_run: - return run_exe(*args, **kwargs) + def run_exe(self, exe: ExecutableSerialisation, extra_env: T.Optional[T.Dict[str, str]] = None) -> int: + if (not self.dry_run) or exe.dry_run: + return run_exe(exe, extra_env) return 0 def should_install(self, d: T.Union[TargetInstallData, InstallEmptyDir, @@ -390,7 +393,8 @@ class Installer: return from_time <= to_time def do_copyfile(self, from_file: str, to_file: str, - makedirs: T.Optional[T.Tuple[T.Any, str]] = None) -> bool: + makedirs: T.Optional[T.Tuple[T.Any, str]] = None, + follow_symlinks: T.Optional[bool] = None) -> bool: outdir = os.path.split(to_file)[0] if not os.path.isfile(from_file) and not os.path.islink(from_file): raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}') @@ -404,22 +408,24 @@ class Installer: append_to_log(self.lf, f'# Preserving old file {to_file}\n') self.preserved_file_count += 1 return False + self.log(f'Installing {from_file} to {outdir}') self.remove(to_file) - elif makedirs: - # Unpack tuple - dirmaker, outdir = makedirs - # Create dirs if needed - dirmaker.makedirs(outdir, exist_ok=True) - self.log(f'Installing {from_file} to {outdir}') + else: + self.log(f'Installing {from_file} to {outdir}') + if makedirs: + # Unpack tuple + dirmaker, outdir = makedirs + # Create dirs if needed + dirmaker.makedirs(outdir, exist_ok=True) if os.path.islink(from_file): if not os.path.exists(from_file): # Dangling symlink. Replicate as is. self.copy(from_file, outdir, follow_symlinks=False) else: - # Remove this entire branch when changing the behaviour to duplicate - # symlinks rather than copying what they point to. - print(symlink_warning) - self.copy2(from_file, to_file) + if follow_symlinks is None: + follow_symlinks = True # TODO: change to False when removing the warning + print(symlink_warning) + self.copy2(from_file, to_file, follow_symlinks=follow_symlinks) else: self.copy2(from_file, to_file) selinux_updates.append(to_file) @@ -453,7 +459,7 @@ class Installer: def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str, exclude: T.Optional[T.Tuple[T.Set[str], T.Set[str]]], - install_mode: 'FileMode', dm: DirMaker) -> None: + install_mode: 'FileMode', dm: DirMaker, follow_symlinks: T.Optional[bool] = None) -> None: ''' Copies the contents of directory @src_dir into @dst_dir. @@ -482,6 +488,8 @@ class Installer: raise ValueError(f'dst_dir must be absolute, got {dst_dir}') if exclude is not None: exclude_files, exclude_dirs = exclude + exclude_files = {os.path.normpath(x) for x in exclude_files} + exclude_dirs = {os.path.normpath(x) for x in exclude_dirs} else: exclude_files = exclude_dirs = set() for root, dirs, files in os.walk(src_dir): @@ -516,7 +524,7 @@ class Installer: dm.makedirs(parent_dir) self.copystat(os.path.dirname(abs_src), parent_dir) # FIXME: what about symlinks? - self.do_copyfile(abs_src, abs_dst) + self.do_copyfile(abs_src, abs_dst, follow_symlinks=follow_symlinks) self.set_mode(abs_dst, install_mode, data.install_umask) def do_install(self, datafilename: str) -> None: @@ -556,19 +564,42 @@ class Installer: self.log('Preserved {} unchanged files, see {} for the full list' .format(self.preserved_file_count, os.path.normpath(self.lf.name))) except PermissionError: - if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ and destdir == '': - print('Installation failed due to insufficient permissions.') - print('Attempting to use polkit to gain elevated privileges...') - os.execlp('pkexec', 'pkexec', sys.executable, main_file, *sys.argv[1:], - '-C', os.getcwd()) - else: + if is_windows() or destdir != '' or not os.isatty(sys.stdout.fileno()) or not os.isatty(sys.stderr.fileno()): + # can't elevate to root except in an interactive unix environment *and* when not doing a destdir install raise + rootcmd = os.environ.get('MESON_ROOT_CMD') or shutil.which('sudo') or shutil.which('doas') + pkexec = shutil.which('pkexec') + if rootcmd is None and pkexec is not None and 'PKEXEC_UID' not in os.environ: + rootcmd = pkexec + + if rootcmd is not None: + print('Installation failed due to insufficient permissions.') + s = selectors.DefaultSelector() + s.register(sys.stdin, selectors.EVENT_READ) + ans = None + for attempt in range(5): + print(f'Attempt to use {rootcmd} to gain elevated privileges? [y/n] ', end='', flush=True) + if s.select(30): + # we waited on sys.stdin *only* + ans = sys.stdin.readline().rstrip('\n') + else: + print() + break + if ans in {'y', 'n'}: + break + else: + if ans is not None: + raise MesonException('Answer not one of [y/n]') + if ans == 'y': + os.execlp(rootcmd, rootcmd, sys.executable, main_file, *sys.argv[1:], + '-C', os.getcwd(), '--no-rebuild') + raise def do_strip(self, strip_bin: T.List[str], fname: str, outname: str) -> None: self.log(f'Stripping target {fname!r}.') if is_osx(): # macOS expects dynamic objects to be stripped with -x maximum. - # To also strip the debug info, -S must be added. + # To also strip the debug info, -S must be added. # See: https://www.unix.com/man-page/osx/1/strip/ returncode, stdo, stde = self.Popen_safe(strip_bin + ['-S', '-x', outname]) else: @@ -587,7 +618,8 @@ class Installer: full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path) self.log(f'Installing subdir {i.path} to {full_dst_dir}') dm.makedirs(full_dst_dir, exist_ok=True) - self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm) + self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm, + follow_symlinks=i.follow_symlinks) def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for i in d.data: @@ -596,7 +628,7 @@ class Installer: fullfilename = i.path outfilename = get_destdir_path(destdir, fullprefix, i.install_path) outdir = os.path.dirname(outfilename) - if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): + if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir), follow_symlinks=i.follow_symlinks): self.did_install_something = True self.set_mode(outfilename, i.install_mode, d.install_umask) @@ -642,23 +674,33 @@ class Installer: fname = os.path.basename(fullfilename) outdir = get_destdir_path(destdir, fullprefix, t.install_path) outfilename = os.path.join(outdir, fname) - if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): + if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir), + follow_symlinks=t.follow_symlinks): self.did_install_something = True self.set_mode(outfilename, t.install_mode, d.install_umask) def run_install_script(self, d: InstallData, destdir: str, fullprefix: str) -> None: env = {'MESON_SOURCE_ROOT': d.source_dir, 'MESON_BUILD_ROOT': d.build_dir, - 'MESON_INSTALL_PREFIX': d.prefix, - 'MESON_INSTALL_DESTDIR_PREFIX': fullprefix, 'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in d.mesonintrospect]), } if self.options.quiet: env['MESON_INSTALL_QUIET'] = '1' + if self.dry_run: + env['MESON_INSTALL_DRY_RUN'] = '1' for i in d.install_scripts: if not self.should_install(i): continue + + if i.installdir_map is not None: + mapp = i.installdir_map + else: + mapp = {'prefix': d.prefix} + localenv = env.copy() + localenv.update({'MESON_INSTALL_'+k.upper(): os.path.join(d.prefix, v) for k, v in mapp.items()}) + localenv.update({'MESON_INSTALL_DESTDIR_'+k.upper(): get_destdir_path(destdir, fullprefix, v) for k, v in mapp.items()}) + name = ' '.join(i.cmd_args) if i.skip_if_destdir and destdir: self.log(f'Skipping custom install script because DESTDIR is set {name!r}') @@ -666,7 +708,7 @@ class Installer: self.did_install_something = True # Custom script must report itself if it does nothing. self.log(f'Running custom install script {name!r}') try: - rc = self.run_exe(i, env) + rc = self.run_exe(i, localenv) except OSError: print(f'FAILED: install script \'{name}\' could not be run, stopped') # POSIX shells return 127 when a command could not be found @@ -677,6 +719,13 @@ class Installer: def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for t in d.targets: + # In AIX, we archive our shared libraries. When we install any package in AIX we need to + # install the archive in which the shared library exists. The below code does the same. + # We change the .so files having lt_version or so_version to archive file install. + # If .so does not exist then it means it is in the archive. Otherwise it is a .so that exists. + if is_aix(): + if not os.path.exists(t.fname) and '.so' in t.fname: + t.fname = re.sub('[.][a]([.]?([0-9]+))*([.]?([a-z]+))*', '.a', t.fname.replace('.so', '.a')) if not self.should_install(t): continue if not os.path.exists(t.fname): @@ -731,8 +780,11 @@ class Installer: # file mode needs to be set last, after strip/depfixer editing self.set_mode(outname, install_mode, d.install_umask) -def rebuild_all(wd: str) -> bool: - if not (Path(wd) / 'build.ninja').is_file(): +def rebuild_all(wd: str, backend: str) -> bool: + if backend == 'none': + # nothing to build... + return True + if backend != 'ninja': print('Only ninja backend is supported to rebuild the project before installation.') return True @@ -741,7 +793,53 @@ def rebuild_all(wd: str) -> bool: print("Can't find ninja, can't rebuild test.") return False - ret = subprocess.run(ninja + ['-C', wd]).returncode + def drop_privileges() -> T.Tuple[T.Optional[EnvironOrDict], T.Optional[T.Callable[[], None]]]: + if not is_windows() and os.geteuid() == 0: + import pwd + env = os.environ.copy() + + if os.environ.get('SUDO_USER') is not None: + orig_user = env.pop('SUDO_USER') + orig_uid = env.pop('SUDO_UID', 0) + orig_gid = env.pop('SUDO_GID', 0) + try: + homedir = pwd.getpwuid(int(orig_uid)).pw_dir + except KeyError: + # `sudo chroot` leaves behind stale variable and builds as root without a user + return None, None + elif os.environ.get('DOAS_USER') is not None: + orig_user = env.pop('DOAS_USER') + try: + pwdata = pwd.getpwnam(orig_user) + except KeyError: + # `doas chroot` leaves behind stale variable and builds as root without a user + return None, None + orig_uid = pwdata.pw_uid + orig_gid = pwdata.pw_gid + homedir = pwdata.pw_dir + else: + return None, None + + if os.stat(os.path.join(wd, 'build.ninja')).st_uid != int(orig_uid): + # the entire build process is running with sudo, we can't drop privileges + return None, None + + env['USER'] = orig_user + env['HOME'] = homedir + + def wrapped() -> None: + print(f'Dropping privileges to {orig_user!r} before running ninja...') + if orig_gid is not None: + os.setgid(int(orig_gid)) + if orig_uid is not None: + os.setuid(int(orig_uid)) + + return env, wrapped + else: + return None, None + + env, preexec_fn = drop_privileges() + ret = subprocess.run(ninja + ['-C', wd], env=env, preexec_fn=preexec_fn).returncode if ret != 0: print(f'Could not rebuild {wd}') return False @@ -757,8 +855,10 @@ def run(opts: 'ArgumentType') -> int: sys.exit('Install data not found. Run this command in build directory root.') if not opts.no_rebuild: b = build.load(opts.wd) - setup_vsenv(b.need_vsenv) - if not rebuild_all(opts.wd): + need_vsenv = T.cast('bool', b.environment.coredata.get_option(OptionKey('vsenv'))) + setup_vsenv(need_vsenv) + backend = T.cast('str', b.environment.coredata.get_option(OptionKey('backend'))) + if not rebuild_all(opts.wd, backend): sys.exit(-1) os.chdir(opts.wd) with open(os.path.join(log_dir, 'install-log.txt'), 'w', encoding='utf-8') as lf: diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 2312674..a980c7c 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -20,17 +20,23 @@ tests and so on. All output is in JSON for simple parsing. Currently only works for the Ninja backend. Others use generated project files and don't need this info.""" +from contextlib import redirect_stdout import collections +import dataclasses import json import os from pathlib import Path, PurePath +import sys import typing as T -from . import build, mesonlib, mlog, coredata as cdata +from . import build, mesonlib, coredata as cdata from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstJSONPrinter from .backend import backends +from .dependencies import Dependency +from . import environment +from .interpreterbase import ObjectHolder from .mesonlib import OptionKey -from .mparser import FunctionNode, ArrayNode, ArgumentNode, StringNode +from .mparser import FunctionNode, ArrayNode, ArgumentNode, BaseStringNode if T.TYPE_CHECKING: import argparse @@ -58,8 +64,7 @@ class IntroCommand: def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None, builddata: T.Optional[build.Build] = None, - backend: T.Optional[backends.Backend] = None, - sourcedir: T.Optional[str] = None) -> 'T.Mapping[str, IntroCommand]': + backend: T.Optional[backends.Backend] = None) -> 'T.Mapping[str, IntroCommand]': if backend and builddata: benchmarkdata = backend.create_test_serialisation(builddata.get_benchmarks()) testdata = backend.create_test_serialisation(builddata.get_tests()) @@ -74,15 +79,19 @@ def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None, ('benchmarks', IntroCommand('List all benchmarks', func=lambda: list_benchmarks(benchmarkdata))), ('buildoptions', IntroCommand('List all build options', func=lambda: list_buildoptions(coredata), no_bd=list_buildoptions_from_source)), ('buildsystem_files', IntroCommand('List files that make up the build system', func=lambda: list_buildsystem_files(builddata, interpreter))), - ('dependencies', IntroCommand('List external dependencies', func=lambda: list_deps(coredata), no_bd=list_deps_from_source)), + ('compilers', IntroCommand('List used compilers', func=lambda: list_compilers(coredata))), + ('dependencies', IntroCommand('List external dependencies', func=lambda: list_deps(coredata, backend), no_bd=list_deps_from_source)), ('scan_dependencies', IntroCommand('Scan for dependencies used in the meson.build file', no_bd=list_deps_from_source)), ('installed', IntroCommand('List all installed files and directories', func=lambda: list_installed(installdata))), ('install_plan', IntroCommand('List all installed files and directories with their details', func=lambda: list_install_plan(installdata))), + ('machines', IntroCommand('Information about host, build, and target machines', func=lambda: list_machines(builddata))), ('projectinfo', IntroCommand('Information about projects', func=lambda: list_projinfo(builddata), no_bd=list_projinfo_from_source)), ('targets', IntroCommand('List top level targets', func=lambda: list_targets(builddata, installdata, backend), no_bd=list_targets_from_source)), ('tests', IntroCommand('List all unit tests', func=lambda: list_tests(testdata))), ]) +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: intro_types = get_meson_introspection_types() for key, val in intro_types.items(): @@ -124,15 +133,16 @@ def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]: return res def list_install_plan(installdata: backends.InstallData) -> T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]: - plan = { + plan: T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]] = { 'targets': { os.path.join(installdata.build_dir, target.fname): { 'destination': target.out_name, 'tag': target.tag or None, + 'subproject': target.subproject or None, } for target in installdata.targets }, - } # type: T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]] + } for key, data_list in { 'data': installdata.data, 'man': installdata.man, @@ -146,11 +156,20 @@ def list_install_plan(installdata: backends.InstallData) -> T.Dict[str, T.Dict[s if key == 'headers': # in the headers, install_path_name is the directory install_path_name = os.path.join(install_path_name, os.path.basename(data.path)) - plan[data_type] = plan.get(data_type, {}) - plan[data_type][data.path] = { + entry = { 'destination': install_path_name, 'tag': data.tag or None, + 'subproject': data.subproject or None, } + + if key == 'install_subdirs': + exclude_files, exclude_dirs = data.exclude or ([], []) + entry['exclude_dirs'] = list(exclude_dirs) + entry['exclude_files'] = list(exclude_files) + + plan[data_type] = plan.get(data_type, {}) + plan[data_type][data.path] = entry + return plan def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str: @@ -160,23 +179,23 @@ def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str: return subdir def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]]: - tlist = [] # type: T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]] + tlist: T.List[T.Dict[str, T.Union[bool, str, T.List[T.Union[str, T.Dict[str, T.Union[str, T.List[str], bool]]]]]]] = [] root_dir = Path(intr.source_root) def nodes_to_paths(node_list: T.List[BaseNode]) -> T.List[Path]: - res = [] # type: T.List[Path] + res: T.List[Path] = [] for n in node_list: - args = [] # type: T.List[BaseNode] + args: T.List[BaseNode] = [] if isinstance(n, FunctionNode): args = list(n.args.arguments) - if n.func_name in BUILD_TARGET_FUNCTIONS: + if n.func_name.value in BUILD_TARGET_FUNCTIONS: args.pop(0) elif isinstance(n, ArrayNode): args = n.args.arguments elif isinstance(n, ArgumentNode): args = n.arguments for j in args: - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): assert isinstance(j.value, str) res += [Path(j.value)] elif isinstance(j, str): @@ -204,6 +223,7 @@ def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[st 'sources': [str(x) for x in sources], 'generated_sources': [] }], + 'depends': [], 'extra_files': [str(x) for x in extra_f], 'subproject': None, # Subprojects are not supported 'installed': i['installed'] @@ -212,7 +232,7 @@ def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[st return tlist def list_targets(builddata: build.Build, installdata: backends.InstallData, backend: backends.Backend) -> T.List[T.Any]: - tlist = [] # type: T.List[T.Any] + tlist: T.List[T.Any] = [] build_dir = builddata.environment.get_build_dir() src_dir = builddata.environment.get_source_dir() @@ -240,14 +260,23 @@ def list_targets(builddata: build.Build, installdata: backends.InstallData, back 'name': target.get_basename(), 'id': idname, 'type': target.get_typename(), - 'defined_in': os.path.normpath(os.path.join(src_dir, target.subdir, 'meson.build')), + 'defined_in': os.path.normpath(os.path.join(src_dir, target.subdir, environment.build_filename)), 'filename': [os.path.join(build_dir, outdir, x) for x in target.get_outputs()], 'build_by_default': target.build_by_default, 'target_sources': backend.get_introspection_data(idname, target), 'extra_files': [os.path.normpath(os.path.join(src_dir, x.subdir, x.fname)) for x in target.extra_files], - 'subproject': target.subproject or None + 'subproject': target.subproject or None, + 'dependencies': [d.name for d in getattr(target, 'external_deps', [])], + 'depends': [lib.get_id() for lib in getattr(target, 'dependencies', [])] } + vs_module_defs = getattr(target, 'vs_module_defs', None) + if vs_module_defs is not None: + t['vs_module_defs'] = vs_module_defs.relative_name() + win_subsystem = getattr(target, 'win_subsystem', None) + if win_subsystem is not None: + t['win_subsystem'] = win_subsystem + if installdata and target.should_install(): t['installed'] = True ifn = [install_lookuptable.get(x, [None]) for x in target.get_outputs()] @@ -262,7 +291,7 @@ def list_buildoptions_from_source(intr: IntrospectionInterpreter) -> T.List[T.Di return list_buildoptions(intr.coredata, subprojects) def list_buildoptions(coredata: cdata.CoreData, subprojects: T.Optional[T.List[str]] = None) -> T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]]: - optlist = [] # type: T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]] + optlist: T.List[T.Dict[str, T.Union[str, bool, int, T.List[str]]]] = [] subprojects = subprojects or [] dir_option_names = set(cdata.BUILTIN_DIR_OPTIONS) @@ -319,12 +348,12 @@ def list_buildoptions(coredata: cdata.CoreData, subprojects: T.Optional[T.List[s return optlist def find_buildsystem_files_list(src_dir: str) -> T.List[str]: + build_files = frozenset({'meson.build', 'meson.options', 'meson_options.txt'}) # I feel dirty about this. But only slightly. - filelist = [] # type: T.List[str] + filelist: T.List[str] = [] for root, _, files in os.walk(src_dir): - for f in files: - if f in {'meson.build', 'meson_options.txt'}: - filelist.append(os.path.relpath(os.path.join(root, f), src_dir)) + filelist.extend(os.path.relpath(os.path.join(root, f), src_dir) + for f in build_files.intersection(files)) return filelist def list_buildsystem_files(builddata: build.Build, interpreter: Interpreter) -> T.List[str]: @@ -333,8 +362,25 @@ def list_buildsystem_files(builddata: build.Build, interpreter: Interpreter) -> filelist = [PurePath(src_dir, x).as_posix() for x in filelist] return filelist +def list_compilers(coredata: cdata.CoreData) -> T.Dict[str, T.Dict[str, T.Dict[str, str]]]: + compilers: T.Dict[str, T.Dict[str, T.Dict[str, str]]] = {} + for machine in ('host', 'build'): + compilers[machine] = {} + for language, compiler in getattr(coredata.compilers, machine).items(): + compilers[machine][language] = { + 'id': compiler.get_id(), + 'exelist': compiler.get_exelist(), + 'linker_exelist': compiler.get_linker_exelist(), + 'file_suffixes': compiler.file_suffixes, + 'default_suffix': compiler.get_default_suffix(), + 'version': compiler.version, + 'full_version': compiler.full_version, + 'linker_id': compiler.get_linker_id(), + } + return compilers + def list_deps_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, T.Union[str, bool]]]: - result = [] # type: T.List[T.Dict[str, T.Union[str, bool]]] + result: T.List[T.Dict[str, T.Union[str, bool]]] = [] for i in intr.dependencies: keys = [ 'name', @@ -346,26 +392,60 @@ def list_deps_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[str, result += [{k: v for k, v in i.items() if k in keys}] return result -def list_deps(coredata: cdata.CoreData) -> T.List[T.Dict[str, T.Union[str, T.List[str]]]]: - result = [] # type: T.List[T.Dict[str, T.Union[str, T.List[str]]]] +def list_deps(coredata: cdata.CoreData, backend: backends.Backend) -> T.List[T.Dict[str, T.Union[str, T.List[str]]]]: + result: T.Dict[str, T.Dict[str, T.Union[str, T.List[str]]]] = {} + + def _src_to_str(src_file: T.Union[mesonlib.FileOrString, build.CustomTarget, build.StructuredSources, build.CustomTargetIndex, build.GeneratedList]) -> T.List[str]: + if isinstance(src_file, str): + return [src_file] + if isinstance(src_file, mesonlib.File): + return [src_file.absolute_path(backend.source_dir, backend.build_dir)] + if isinstance(src_file, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)): + return src_file.get_outputs() + if isinstance(src_file, build.StructuredSources): + return [f for s in src_file.as_list() for f in _src_to_str(s)] + raise mesonlib.MesonBugException(f'Invalid file type {type(src_file)}.') + + def _create_result(d: Dependency, varname: T.Optional[str] = None) -> T.Dict[str, T.Any]: + return { + 'name': d.name, + 'type': d.type_name, + 'version': d.get_version(), + 'compile_args': d.get_compile_args(), + 'link_args': d.get_link_args(), + 'include_directories': [i for idirs in d.get_include_dirs() for i in idirs.to_string_list(backend.source_dir, backend.build_dir)], + 'sources': [f for s in d.get_sources() for f in _src_to_str(s)], + 'extra_files': [f for s in d.get_extra_files() for f in _src_to_str(s)], + 'dependencies': [e.name for e in d.ext_deps], + 'depends': [lib.get_id() for lib in getattr(d, 'libraries', [])], + 'meson_variables': [varname] if varname else [], + } + for d in coredata.deps.host.values(): if d.found(): - result += [{'name': d.name, - 'version': d.get_version(), - 'compile_args': d.get_compile_args(), - 'link_args': d.get_link_args()}] - return result + result[d.name] = _create_result(d) + + for varname, holder in backend.interpreter.variables.items(): + if isinstance(holder, ObjectHolder): + d = holder.held_object + if isinstance(d, Dependency) and d.found(): + if d.name in result: + T.cast('T.List[str]', result[d.name]['meson_variables']).append(varname) + else: + result[d.name] = _create_result(d, varname) + + return list(result.values()) def get_test_list(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]: - result = [] # type: T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]] + result: T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]] = [] for t in testdata: - to = {} # type: T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]] + to: T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]] = {} if isinstance(t.fname, str): fname = [t.fname] else: fname = t.fname to['cmd'] = fname + t.cmd_args - if isinstance(t.env, build.EnvironmentVariables): + if isinstance(t.env, mesonlib.EnvironmentVariables): to['env'] = t.env.get_env({}) else: to['env'] = t.env @@ -377,6 +457,7 @@ def get_test_list(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict to['priority'] = t.priority to['protocol'] = str(t.protocol) to['depends'] = t.depends + to['extra_paths'] = t.extra_paths result.append(to) return result @@ -386,15 +467,29 @@ def list_tests(testdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[st def list_benchmarks(benchdata: T.List[backends.TestSerialisation]) -> T.List[T.Dict[str, T.Union[str, int, T.List[str], T.Dict[str, str]]]]: return get_test_list(benchdata) +def list_machines(builddata: build.Build) -> T.Dict[str, T.Dict[str, T.Union[str, bool]]]: + machines: T.Dict[str, T.Dict[str, T.Union[str, bool]]] = {} + for m in ('host', 'build', 'target'): + machine = getattr(builddata.environment.machines, m) + machines[m] = dataclasses.asdict(machine) + machines[m]['is_64_bit'] = machine.is_64_bit + machines[m]['exe_suffix'] = machine.get_exe_suffix() + machines[m]['object_suffix'] = machine.get_object_suffix() + return machines + def list_projinfo(builddata: build.Build) -> T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]]: - result = {'version': builddata.project_version, - 'descriptive_name': builddata.project_name, - 'subproject_dir': builddata.subproject_dir} # type: T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]] + result: T.Dict[str, T.Union[str, T.List[T.Dict[str, str]]]] = { + 'version': builddata.project_version, + 'descriptive_name': builddata.project_name, + 'subproject_dir': builddata.subproject_dir, + } subprojects = [] for k, v in builddata.subprojects.items(): - c = {'name': k, - 'version': v, - 'descriptive_name': builddata.projects.get(k)} # type: T.Dict[str, str] + c: T.Dict[str, str] = { + 'name': k, + 'version': v, + 'descriptive_name': builddata.projects.get(k), + } subprojects.append(c) result['subprojects'] = subprojects return result @@ -413,7 +508,7 @@ def list_projinfo_from_source(intr: IntrospectionInterpreter) -> T.Dict[str, T.U intr.project_data['subproject_dir'] = intr.subproject_dir return intr.project_data -def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], indent: int) -> int: +def print_results(options: argparse.Namespace, results: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], indent: T.Optional[int]) -> int: if not results and not options.force_dict: print('No command specified') return 1 @@ -447,19 +542,18 @@ def run(options: argparse.Namespace) -> int: if options.builddir is not None: datadir = os.path.join(options.builddir, datadir) indent = 4 if options.indent else None - results = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] + results: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] = [] sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11] - intro_types = get_meson_introspection_types(sourcedir=sourcedir) + intro_types = get_meson_introspection_types() if 'meson.build' in [os.path.basename(options.builddir), options.builddir]: # Make sure that log entries in other parts of meson don't interfere with the JSON output - mlog.disable() - backend = backends.get_backend_from_name(options.backend) - assert backend is not None - intr = IntrospectionInterpreter(sourcedir, '', backend.name, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) - intr.analyze() - # Re-enable logging just in case - mlog.enable() + with redirect_stdout(sys.stderr): + backend = backends.get_backend_from_name(options.backend) + assert backend is not None + intr = IntrospectionInterpreter(sourcedir, '', backend.name, visitors = [AstIDGenerator(), AstIndentationGenerator(), AstConditionLevel()]) + intr.analyze() + for key, val in intro_types.items(): if (not options.all and not getattr(options, key, False)) or not val.no_bd: continue @@ -501,7 +595,7 @@ def run(options: argparse.Namespace) -> int: return print_results(options, results, indent) -updated_introspection_files = [] # type: T.List[str] +updated_introspection_files: T.List[str] = [] def write_intro_info(intro_info: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T.Any]]]], info_dir: str) -> None: for kind, data in intro_info: @@ -516,7 +610,7 @@ def write_intro_info(intro_info: T.Sequence[T.Tuple[str, T.Union[dict, T.List[T. def generate_introspection_file(builddata: build.Build, backend: backends.Backend) -> None: coredata = builddata.environment.get_coredata() intro_types = get_meson_introspection_types(coredata=coredata, builddata=builddata, backend=backend) - intro_info = [] # type: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] + intro_info: T.List[T.Tuple[str, T.Union[dict, T.List[T.Any]]]] = [] for key, val in intro_types.items(): if not val.func: diff --git a/mesonbuild/mlog.py b/mesonbuild/mlog.py index 2b310ec..0e62a57 100644 --- a/mesonbuild/mlog.py +++ b/mesonbuild/mlog.py @@ -12,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""This is (mostly) a standalone module used to write logging +information about Meson runs. Some output goes to screen, +some to logging dir and some goes to both.""" + +from __future__ import annotations + +import enum import os import io import sys @@ -22,14 +29,16 @@ import subprocess import shutil import typing as T from contextlib import contextmanager +from dataclasses import dataclass, field from pathlib import Path if T.TYPE_CHECKING: from ._typing import StringProtocol, SizedStringProtocol -"""This is (mostly) a standalone module used to write logging -information about Meson runs. Some output goes to screen, -some to logging dir and some goes to both.""" + from .mparser import BaseNode + + TV_Loggable = T.Union[str, 'AnsiDecorator', StringProtocol] + TV_LoggableList = T.List[TV_Loggable] def is_windows() -> bool: platname = platform.system().lower() @@ -51,7 +60,7 @@ def _windows_ansi() -> bool: return bool(kernel.SetConsoleMode(stdout, mode.value | 0x4) or os.environ.get('ANSICON')) def colorize_console() -> bool: - _colorize_console = getattr(sys.stdout, 'colorize_console', None) # type: bool + _colorize_console: bool = getattr(sys.stdout, 'colorize_console', None) if _colorize_console is not None: return _colorize_console @@ -76,55 +85,358 @@ def setup_console() -> None: except AttributeError: pass -log_dir = None # type: T.Optional[str] -log_file = None # type: T.Optional[T.TextIO] -log_fname = 'meson-log.txt' # type: str -log_depth = [] # type: T.List[str] -log_timestamp_start = None # type: T.Optional[float] -log_fatal_warnings = False # type: bool -log_disable_stdout = False # type: bool -log_errors_only = False # type: bool -_in_ci = 'CI' in os.environ # type: bool -_logged_once = set() # type: T.Set[T.Tuple[str, ...]] -log_warnings_counter = 0 # type: int -log_pager: T.Optional['subprocess.Popen'] = None - -def disable() -> None: - global log_disable_stdout # pylint: disable=global-statement - log_disable_stdout = True - -def enable() -> None: - global log_disable_stdout # pylint: disable=global-statement - log_disable_stdout = False +_in_ci = 'CI' in os.environ + + +class _Severity(enum.Enum): + + NOTICE = enum.auto() + WARNING = enum.auto() + ERROR = enum.auto() + DEPRECATION = enum.auto() -def set_quiet() -> None: - global log_errors_only # pylint: disable=global-statement - log_errors_only = True +@dataclass +class _Logger: -def set_verbose() -> None: - global log_errors_only # pylint: disable=global-statement + log_dir: T.Optional[str] = None + log_depth: T.List[str] = field(default_factory=list) + log_file: T.Optional[T.TextIO] = None + log_timestamp_start: T.Optional[float] = None + log_fatal_warnings = False + log_disable_stdout = False log_errors_only = False + logged_once: T.Set[T.Tuple[str, ...]] = field(default_factory=set) + log_warnings_counter = 0 + log_pager: T.Optional['subprocess.Popen'] = None -def initialize(logdir: str, fatal_warnings: bool = False) -> None: - global log_dir, log_file, log_fatal_warnings # pylint: disable=global-statement - log_dir = logdir - log_file = open(os.path.join(logdir, log_fname), 'w', encoding='utf-8') - log_fatal_warnings = fatal_warnings - -def set_timestamp_start(start: float) -> None: - global log_timestamp_start # pylint: disable=global-statement - log_timestamp_start = start - -def shutdown() -> T.Optional[str]: - global log_file # pylint: disable=global-statement - if log_file is not None: - path = log_file.name - exception_around_goer = log_file - log_file = None - exception_around_goer.close() - return path - stop_pager() - return None + _LOG_FNAME: T.ClassVar[str] = 'meson-log.txt' + + @contextmanager + def no_logging(self) -> T.Iterator[None]: + self.log_disable_stdout = True + try: + yield + finally: + self.log_disable_stdout = False + + @contextmanager + def force_logging(self) -> T.Iterator[None]: + restore = self.log_disable_stdout + self.log_disable_stdout = False + try: + yield + finally: + self.log_disable_stdout = restore + + def set_quiet(self) -> None: + self.log_errors_only = True + + def set_verbose(self) -> None: + self.log_errors_only = False + + def set_timestamp_start(self, start: float) -> None: + self.log_timestamp_start = start + + def shutdown(self) -> T.Optional[str]: + if self.log_file is not None: + path = self.log_file.name + exception_around_goer = self.log_file + self.log_file = None + exception_around_goer.close() + return path + self.stop_pager() + return None + + def start_pager(self) -> None: + if not colorize_console(): + return + pager_cmd = [] + if 'PAGER' in os.environ: + pager_cmd = shlex.split(os.environ['PAGER']) + else: + less = shutil.which('less') + if not less and is_windows(): + git = shutil.which('git') + if git: + path = Path(git).parents[1] / 'usr' / 'bin' + less = shutil.which('less', path=str(path)) + if less: + pager_cmd = [less] + if not pager_cmd: + return + try: + # Set 'LESS' environment variable, rather than arguments in + # pager_cmd, to also support the case where the user has 'PAGER' + # set to 'less'. Arguments set are: + # "R" : support color + # "X" : do not clear the screen when leaving the pager + # "F" : skip the pager if content fits into the screen + env = os.environ.copy() + if 'LESS' not in env: + env['LESS'] = 'RXF' + # Set "-c" for lv to support color + if 'LV' not in env: + env['LV'] = '-c' + self.log_pager = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE, + text=True, encoding='utf-8', env=env) + except Exception as e: + # Ignore errors, unless it is a user defined pager. + if 'PAGER' in os.environ: + from .mesonlib import MesonException + raise MesonException(f'Failed to start pager: {str(e)}') + + def stop_pager(self) -> None: + if self.log_pager: + try: + self.log_pager.stdin.flush() + self.log_pager.stdin.close() + except BrokenPipeError: + pass + self.log_pager.wait() + self.log_pager = None + + def initialize(self, logdir: str, fatal_warnings: bool = False) -> None: + self.log_dir = logdir + self.log_file = open(os.path.join(logdir, self._LOG_FNAME), 'w', encoding='utf-8') + self.log_fatal_warnings = fatal_warnings + + def process_markup(self, args: T.Sequence[TV_Loggable], keep: bool, display_timestamp: bool = True) -> T.List[str]: + arr: T.List[str] = [] + if self.log_timestamp_start is not None and display_timestamp: + arr = ['[{:.3f}]'.format(time.monotonic() - self.log_timestamp_start)] + for arg in args: + if arg is None: + continue + if isinstance(arg, str): + arr.append(arg) + elif isinstance(arg, AnsiDecorator): + arr.append(arg.get_text(keep)) + else: + arr.append(str(arg)) + return arr + + def force_print(self, *args: str, nested: bool, sep: T.Optional[str] = None, + end: T.Optional[str] = None) -> None: + if self.log_disable_stdout: + return + iostr = io.StringIO() + print(*args, sep=sep, end=end, file=iostr) + + raw = iostr.getvalue() + if self.log_depth: + prepend = self.log_depth[-1] + '| ' if nested else '' + lines = [] + for l in raw.split('\n'): + l = l.strip() + lines.append(prepend + l if l else '') + raw = '\n'.join(lines) + + # _Something_ is going to get printed. + try: + output = self.log_pager.stdin if self.log_pager else None + print(raw, end='', file=output) + except UnicodeEncodeError: + cleaned = raw.encode('ascii', 'replace').decode('ascii') + print(cleaned, end='') + + def debug(self, *args: TV_Loggable, sep: T.Optional[str] = None, + end: T.Optional[str] = None, display_timestamp: bool = True) -> None: + arr = process_markup(args, False, display_timestamp) + if self.log_file is not None: + print(*arr, file=self.log_file, sep=sep, end=end) + self.log_file.flush() + + def _log(self, *args: TV_Loggable, is_error: bool = False, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None, display_timestamp: bool = True) -> None: + arr = process_markup(args, False, display_timestamp) + if self.log_file is not None: + print(*arr, file=self.log_file, sep=sep, end=end) + self.log_file.flush() + if colorize_console(): + arr = process_markup(args, True, display_timestamp) + if not self.log_errors_only or is_error: + force_print(*arr, nested=nested, sep=sep, end=end) + + def _debug_log_cmd(self, cmd: str, args: T.List[str]) -> None: + if not _in_ci: + return + args = [f'"{x}"' for x in args] # Quote all args, just in case + self.debug('!meson_ci!/{} {}'.format(cmd, ' '.join(args))) + + def cmd_ci_include(self, file: str) -> None: + self._debug_log_cmd('ci_include', [file]) + + def log(self, *args: TV_Loggable, is_error: bool = False, + once: bool = False, nested: bool = True, + sep: T.Optional[str] = None, + end: T.Optional[str] = None, + display_timestamp: bool = True) -> None: + if once: + self._log_once(*args, is_error=is_error, nested=nested, sep=sep, end=end, display_timestamp=display_timestamp) + else: + self._log(*args, is_error=is_error, nested=nested, sep=sep, end=end, display_timestamp=display_timestamp) + + def log_timestamp(self, *args: TV_Loggable) -> None: + if self.log_timestamp_start: + self.log(*args) + + def _log_once(self, *args: TV_Loggable, is_error: bool = False, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None, display_timestamp: bool = True) -> None: + """Log variant that only prints a given message one time per meson invocation. + + This considers ansi decorated values by the values they wrap without + regard for the AnsiDecorator itself. + """ + def to_str(x: TV_Loggable) -> str: + if isinstance(x, str): + return x + if isinstance(x, AnsiDecorator): + return x.text + return str(x) + t = tuple(to_str(a) for a in args) + if t in self.logged_once: + return + self.logged_once.add(t) + self._log(*args, is_error=is_error, nested=nested, sep=sep, end=end, display_timestamp=display_timestamp) + + def _log_error(self, severity: _Severity, *rargs: TV_Loggable, + once: bool = False, fatal: bool = True, + location: T.Optional[BaseNode] = None, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None, + is_error: bool = True) -> None: + from .mesonlib import MesonException, relpath + + # The typing requirements here are non-obvious. Lists are invariant, + # therefore T.List[A] and T.List[T.Union[A, B]] are not able to be joined + if severity is _Severity.NOTICE: + label: TV_LoggableList = [bold('NOTICE:')] + elif severity is _Severity.WARNING: + label = [yellow('WARNING:')] + elif severity is _Severity.ERROR: + label = [red('ERROR:')] + elif severity is _Severity.DEPRECATION: + label = [red('DEPRECATION:')] + # rargs is a tuple, not a list + args = label + list(rargs) + + if location is not None: + location_file = relpath(location.filename, os.getcwd()) + location_str = get_error_location_string(location_file, location.lineno) + # Unions are frankly awful, and we have to T.cast here to get mypy + # to understand that the list concatenation is safe + location_list = T.cast('TV_LoggableList', [location_str]) + args = location_list + args + + log(*args, once=once, nested=nested, sep=sep, end=end, is_error=is_error) + + self.log_warnings_counter += 1 + + if self.log_fatal_warnings and fatal: + raise MesonException("Fatal warnings enabled, aborting") + + def error(self, *args: TV_Loggable, + once: bool = False, fatal: bool = True, + location: T.Optional[BaseNode] = None, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None) -> None: + return self._log_error(_Severity.ERROR, *args, once=once, fatal=fatal, location=location, + nested=nested, sep=sep, end=end, is_error=True) + + def warning(self, *args: TV_Loggable, + once: bool = False, fatal: bool = True, + location: T.Optional[BaseNode] = None, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None) -> None: + return self._log_error(_Severity.WARNING, *args, once=once, fatal=fatal, location=location, + nested=nested, sep=sep, end=end, is_error=True) + + def deprecation(self, *args: TV_Loggable, + once: bool = False, fatal: bool = True, + location: T.Optional[BaseNode] = None, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None) -> None: + return self._log_error(_Severity.DEPRECATION, *args, once=once, fatal=fatal, location=location, + nested=nested, sep=sep, end=end, is_error=True) + + def notice(self, *args: TV_Loggable, + once: bool = False, fatal: bool = True, + location: T.Optional[BaseNode] = None, + nested: bool = True, sep: T.Optional[str] = None, + end: T.Optional[str] = None) -> None: + return self._log_error(_Severity.NOTICE, *args, once=once, fatal=fatal, location=location, + nested=nested, sep=sep, end=end, is_error=False) + + def exception(self, e: Exception, prefix: T.Optional[AnsiDecorator] = None) -> None: + if prefix is None: + prefix = red('ERROR:') + self.log() + args: T.List[T.Union[AnsiDecorator, str]] = [] + if all(getattr(e, a, None) is not None for a in ['file', 'lineno', 'colno']): + # Mypy doesn't follow hasattr, and it's pretty easy to visually inspect + # that this is correct, so we'll just ignore it. + path = get_relative_path(Path(e.file), Path(os.getcwd())) # type: ignore + args.append(f'{path}:{e.lineno}:{e.colno}:') # type: ignore + if prefix: + args.append(prefix) + args.append(str(e)) + + with self.force_logging(): + self.log(*args, is_error=True) + + @contextmanager + def nested(self, name: str = '') -> T.Generator[None, None, None]: + self.log_depth.append(name) + try: + yield + finally: + self.log_depth.pop() + + def get_log_dir(self) -> str: + return self.log_dir + + def get_log_depth(self) -> int: + return len(self.log_depth) + + @contextmanager + def nested_warnings(self) -> T.Iterator[None]: + old = self.log_warnings_counter + self.log_warnings_counter = 0 + try: + yield + finally: + self.log_warnings_counter = old + + def get_warning_count(self) -> int: + return self.log_warnings_counter + +_logger = _Logger() +cmd_ci_include = _logger.cmd_ci_include +debug = _logger.debug +deprecation = _logger.deprecation +error = _logger.error +exception = _logger.exception +force_print = _logger.force_print +get_log_depth = _logger.get_log_depth +get_log_dir = _logger.get_log_dir +get_warning_count = _logger.get_warning_count +initialize = _logger.initialize +log = _logger.log +log_timestamp = _logger.log_timestamp +nested = _logger.nested +nested_warnings = _logger.nested_warnings +no_logging = _logger.no_logging +notice = _logger.notice +process_markup = _logger.process_markup +set_quiet = _logger.set_quiet +set_timestamp_start = _logger.set_timestamp_start +set_verbose = _logger.set_verbose +shutdown = _logger.shutdown +start_pager = _logger.start_pager +stop_pager = _logger.stop_pager +warning = _logger.warning class AnsiDecorator: plain_code = "\033[0m" @@ -148,9 +460,6 @@ class AnsiDecorator: def __str__(self) -> str: return self.get_text(colorize_console()) -TV_Loggable = T.Union[str, AnsiDecorator, 'StringProtocol'] -TV_LoggableList = T.List[TV_Loggable] - class AnsiText: def __init__(self, *args: 'SizedStringProtocol'): self.args = args @@ -201,161 +510,9 @@ def normal_blue(text: str) -> AnsiDecorator: def normal_cyan(text: str) -> AnsiDecorator: return AnsiDecorator(text, "\033[36m") -# This really should be AnsiDecorator or anything that implements -# __str__(), but that requires protocols from typing_extensions -def process_markup(args: T.Sequence[TV_Loggable], keep: bool) -> T.List[str]: - arr = [] # type: T.List[str] - if log_timestamp_start is not None: - arr = ['[{:.3f}]'.format(time.monotonic() - log_timestamp_start)] - for arg in args: - if arg is None: - continue - if isinstance(arg, str): - arr.append(arg) - elif isinstance(arg, AnsiDecorator): - arr.append(arg.get_text(keep)) - else: - arr.append(str(arg)) - return arr - -def force_print(*args: str, nested: bool, **kwargs: T.Any) -> None: - if log_disable_stdout: - return - iostr = io.StringIO() - kwargs['file'] = iostr - print(*args, **kwargs) - - raw = iostr.getvalue() - if log_depth: - prepend = log_depth[-1] + '| ' if nested else '' - lines = [] - for l in raw.split('\n'): - l = l.strip() - lines.append(prepend + l if l else '') - raw = '\n'.join(lines) - - # _Something_ is going to get printed. - try: - output = log_pager.stdin if log_pager else None - print(raw, end='', file=output) - except UnicodeEncodeError: - cleaned = raw.encode('ascii', 'replace').decode('ascii') - print(cleaned, end='') - -# We really want a heterogeneous dict for this, but that's in typing_extensions -def debug(*args: TV_Loggable, **kwargs: T.Any) -> None: - arr = process_markup(args, False) - if log_file is not None: - print(*arr, file=log_file, **kwargs) - log_file.flush() - -def _debug_log_cmd(cmd: str, args: T.List[str]) -> None: - if not _in_ci: - return - args = [f'"{x}"' for x in args] # Quote all args, just in case - debug('!meson_ci!/{} {}'.format(cmd, ' '.join(args))) - -def cmd_ci_include(file: str) -> None: - _debug_log_cmd('ci_include', [file]) - - -def log(*args: TV_Loggable, is_error: bool = False, - once: bool = False, **kwargs: T.Any) -> None: - if once: - log_once(*args, is_error=is_error, **kwargs) - else: - _log(*args, is_error=is_error, **kwargs) - - -def _log(*args: TV_Loggable, is_error: bool = False, - **kwargs: T.Any) -> None: - nested = kwargs.pop('nested', True) - arr = process_markup(args, False) - if log_file is not None: - print(*arr, file=log_file, **kwargs) - log_file.flush() - if colorize_console(): - arr = process_markup(args, True) - if not log_errors_only or is_error: - force_print(*arr, nested=nested, **kwargs) - -def log_once(*args: TV_Loggable, is_error: bool = False, - **kwargs: T.Any) -> None: - """Log variant that only prints a given message one time per meson invocation. - - This considers ansi decorated values by the values they wrap without - regard for the AnsiDecorator itself. - """ - def to_str(x: TV_Loggable) -> str: - if isinstance(x, str): - return x - if isinstance(x, AnsiDecorator): - return x.text - return str(x) - t = tuple(to_str(a) for a in args) - if t in _logged_once: - return - _logged_once.add(t) - _log(*args, is_error=is_error, **kwargs) - -# This isn't strictly correct. What we really want here is something like: -# class StringProtocol(typing_extensions.Protocol): -# -# def __str__(self) -> str: ... -# -# This would more accurately embody what this function can handle, but we -# don't have that yet, so instead we'll do some casting to work around it -def get_error_location_string(fname: str, lineno: int) -> str: +def get_error_location_string(fname: StringProtocol, lineno: int) -> str: return f'{fname}:{lineno}:' -def _log_error(severity: str, *rargs: TV_Loggable, - once: bool = False, fatal: bool = True, **kwargs: T.Any) -> None: - from .mesonlib import MesonException, relpath - - # The typing requirements here are non-obvious. Lists are invariant, - # therefore T.List[A] and T.List[T.Union[A, B]] are not able to be joined - if severity == 'notice': - label = [bold('NOTICE:')] # type: TV_LoggableList - elif severity == 'warning': - label = [yellow('WARNING:')] - elif severity == 'error': - label = [red('ERROR:')] - elif severity == 'deprecation': - label = [red('DEPRECATION:')] - else: - raise MesonException('Invalid severity ' + severity) - # rargs is a tuple, not a list - args = label + list(rargs) - - location = kwargs.pop('location', None) - if location is not None: - location_file = relpath(location.filename, os.getcwd()) - location_str = get_error_location_string(location_file, location.lineno) - # Unions are frankly awful, and we have to T.cast here to get mypy - # to understand that the list concatenation is safe - location_list = T.cast('TV_LoggableList', [location_str]) - args = location_list + args - - log(*args, once=once, **kwargs) - - global log_warnings_counter # pylint: disable=global-statement - log_warnings_counter += 1 - - if log_fatal_warnings and fatal: - raise MesonException("Fatal warnings enabled, aborting") - -def error(*args: TV_Loggable, **kwargs: T.Any) -> None: - return _log_error('error', *args, **kwargs, is_error=True) - -def warning(*args: TV_Loggable, **kwargs: T.Any) -> None: - return _log_error('warning', *args, **kwargs, is_error=True) - -def deprecation(*args: TV_Loggable, **kwargs: T.Any) -> None: - return _log_error('deprecation', *args, **kwargs, is_error=True) - -def notice(*args: TV_Loggable, **kwargs: T.Any) -> None: - return _log_error('notice', *args, **kwargs, is_error=False) - def get_relative_path(target: Path, current: Path) -> Path: """Get the path to target from current""" # Go up "current" until we find a common ancestor to target @@ -371,21 +528,6 @@ def get_relative_path(target: Path, current: Path) -> Path: # we failed, should not get here return target -def exception(e: Exception, prefix: T.Optional[AnsiDecorator] = None) -> None: - if prefix is None: - prefix = red('ERROR:') - log() - args = [] # type: T.List[T.Union[AnsiDecorator, str]] - if all(getattr(e, a, None) is not None for a in ['file', 'lineno', 'colno']): - # Mypy doesn't follow hasattr, and it's pretty easy to visually inspect - # that this is correct, so we'll just ignore it. - path = get_relative_path(Path(e.file), Path(os.getcwd())) # type: ignore - args.append(f'{path}:{e.lineno}:{e.colno}:') # type: ignore - if prefix: - args.append(prefix) - args.append(str(e)) - log(*args) - # Format a list for logging purposes as a string. It separates # all but the last item with commas, and the last with 'and'. def format_list(input_list: T.List[str]) -> str: @@ -399,61 +541,13 @@ def format_list(input_list: T.List[str]) -> str: else: return '' -@contextmanager -def nested(name: str = '') -> T.Generator[None, None, None]: - log_depth.append(name) - try: - yield - finally: - log_depth.pop() - -def start_pager() -> None: - if not colorize_console(): - return - pager_cmd = [] - if 'PAGER' in os.environ: - pager_cmd = shlex.split(os.environ['PAGER']) - else: - less = shutil.which('less') - if not less and is_windows(): - git = shutil.which('git') - if git: - path = Path(git).parents[1] / 'usr' / 'bin' - less = shutil.which('less', path=str(path)) - if less: - pager_cmd = [less] - if not pager_cmd: - return - global log_pager # pylint: disable=global-statement - assert log_pager is None - try: - # Set 'LESS' environment variable, rather than arguments in - # pager_cmd, to also support the case where the user has 'PAGER' - # set to 'less'. Arguments set are: - # "R" : support color - # "X" : do not clear the screen when leaving the pager - # "F" : skip the pager if content fits into the screen - env = os.environ.copy() - if 'LESS' not in env: - env['LESS'] = 'RXF' - # Set "-c" for lv to support color - if 'LV' not in env: - env['LV'] = '-c' - log_pager = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE, - text=True, encoding='utf-8', env=env) - except Exception as e: - # Ignore errors, unless it is a user defined pager. - if 'PAGER' in os.environ: - from .mesonlib import MesonException - raise MesonException(f'Failed to start pager: {str(e)}') -def stop_pager() -> None: - global log_pager # pylint: disable=global-statement - if log_pager: - try: - log_pager.stdin.flush() - log_pager.stdin.close() - except BrokenPipeError: - pass - log_pager.wait() - log_pager = None +def code_line(text: str, line: str, colno: int) -> str: + """Print a line with a caret pointing to the colno + + :param text: A message to display before the line + :param line: The line of code to be pointed to + :param colno: The column number to point at + :return: A formatted string of the text, line, and a caret + """ + return f'{text}\n{line}\n{" " * colno}^' diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index b63a5da..bbfb5bd 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -18,20 +18,19 @@ from __future__ import annotations import dataclasses import typing as T -from .. import mesonlib +from .. import build, mesonlib from ..build import IncludeDirs from ..interpreterbase.decorators import noKwargs, noPosargs from ..mesonlib import relpath, HoldableObject, MachineChoice from ..programs import ExternalProgram if T.TYPE_CHECKING: - from .. import build from ..interpreter import Interpreter + from ..interpreter.interpreter import ProgramVersionFunc from ..interpreter.interpreterobjects import MachineHolder from ..interpreterbase import TYPE_var, TYPE_kwargs from ..programs import OverrideProgram from ..wrap import WrapMode - from ..build import EnvironmentVariables, Executable from ..dependencies import Dependency class ModuleState: @@ -86,15 +85,18 @@ class ModuleState: return dirs_str - def find_program(self, prog: T.Union[str, T.List[str]], required: bool = True, - version_func: T.Optional[T.Callable[['ExternalProgram'], str]] = None, + def find_program(self, prog: T.Union[mesonlib.FileOrString, T.List[mesonlib.FileOrString]], + required: bool = True, + version_func: T.Optional[ProgramVersionFunc] = None, wanted: T.Optional[str] = None, silent: bool = False, - for_machine: MachineChoice = MachineChoice.HOST) -> 'ExternalProgram': + for_machine: MachineChoice = MachineChoice.HOST) -> T.Union[ExternalProgram, build.Executable, OverrideProgram]: + if not isinstance(prog, list): + prog = [prog] return self._interpreter.find_program_impl(prog, required=required, version_func=version_func, wanted=wanted, silent=silent, for_machine=for_machine) def find_tool(self, name: str, depname: str, varname: str, required: bool = True, - wanted: T.Optional[str] = None) -> T.Union['Executable', ExternalProgram, 'OverrideProgram']: + wanted: T.Optional[str] = None) -> T.Union['build.Executable', ExternalProgram, 'OverrideProgram']: # Look in overrides in case it's built as subproject progobj = self._interpreter.program_from_overrides([name], []) if progobj is not None: @@ -110,14 +112,19 @@ class ModuleState: if dep.found() and dep.type_name == 'pkgconfig': value = dep.get_variable(pkgconfig=varname) if value: - return ExternalProgram(name, [value]) + progobj = ExternalProgram(value) + if not progobj.found(): + msg = (f'Dependency {depname!r} tool variable {varname!r} contains erroneous value: {value!r}\n\n' + 'This is a distributor issue -- please report it to your {depname} provider.') + raise mesonlib.MesonException(msg) + return progobj # Normal program lookup return self.find_program(name, required=required, wanted=wanted) def dependency(self, depname: str, native: bool = False, required: bool = True, wanted: T.Optional[str] = None) -> 'Dependency': - kwargs = {'native': native, 'required': required} + kwargs: T.Dict[str, object] = {'native': native, 'required': required} if wanted: kwargs['version'] = wanted # FIXME: Even if we fix the function, mypy still can't figure out what's @@ -142,7 +149,7 @@ class ModuleState: def get_option(self, name: str, subproject: str = '', machine: MachineChoice = MachineChoice.HOST, lang: T.Optional[str] = None, - module: T.Optional[str] = None) -> T.Union[str, int, bool, 'WrapMode']: + module: T.Optional[str] = None) -> T.Union[T.List[str], str, int, bool, 'WrapMode']: return self.environment.coredata.get_option(mesonlib.OptionKey(name, subproject, machine, lang, module)) def is_user_defined_option(self, name: str, subproject: str = '', @@ -165,6 +172,8 @@ class ModuleState: else: yield self._interpreter.build_incdir_object([d]) + def add_language(self, lang: str, for_machine: MachineChoice) -> None: + self._interpreter.add_languages([lang], True, for_machine) class ModuleObject(HoldableObject): """Base class for all objects returned by modules @@ -216,8 +225,8 @@ class NewExtensionModule(ModuleObject): def found() -> bool: return True - def get_devenv(self) -> T.Optional['EnvironmentVariables']: - return None + def postconf_hook(self, b: build.Build) -> None: + pass # FIXME: Port all modules to stop using self.interpreter and use API on # ModuleState instead. Modules should stop using this class and instead use @@ -243,20 +252,33 @@ class NotFoundExtensionModule(NewExtensionModule): return False -def is_module_library(fname): +def is_module_library(fname: mesonlib.FileOrString) -> bool: ''' Check if the file is a library-like file generated by a module-specific target, such as GirTarget or TypelibTarget ''' - if hasattr(fname, 'fname'): - fname = fname.fname suffix = fname.split('.')[-1] return suffix in {'gir', 'typelib'} class ModuleReturnValue: def __init__(self, return_value: T.Optional['TYPE_var'], - new_objects: T.Sequence[T.Union['TYPE_var', 'build.ExecutableSerialisation']]) -> None: + new_objects: T.Sequence[T.Union['TYPE_var', 'mesonlib.ExecutableSerialisation']]) -> None: self.return_value = return_value assert isinstance(new_objects, list) - self.new_objects: T.List[T.Union['TYPE_var', 'build.ExecutableSerialisation']] = new_objects + self.new_objects: T.List[T.Union['TYPE_var', 'mesonlib.ExecutableSerialisation']] = new_objects + +class GResourceTarget(build.CustomTarget): + pass + +class GResourceHeaderTarget(build.CustomTarget): + pass + +class GirTarget(build.CustomTarget): + pass + +class TypelibTarget(build.CustomTarget): + pass + +class VapiTarget(build.CustomTarget): + pass diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index ee40b44..ee4e844 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -47,7 +47,9 @@ if T.TYPE_CHECKING: from . import ModuleState from ..cmake import SingleTargetOptions - from ..interpreter import kwargs + from ..environment import Environment + from ..interpreter import Interpreter, kwargs + from ..interpreterbase import TYPE_kwargs, TYPE_var class WriteBasicPackageVersionFile(TypedDict): @@ -118,7 +120,7 @@ class CMakeSubproject(ModuleObject): 'found': self.found_method, }) - def _args_to_info(self, args): + def _args_to_info(self, args: T.List[str]) -> T.Dict[str, str]: if len(args) != 1: raise InterpreterException('Exactly one argument is required.') @@ -135,13 +137,13 @@ class CMakeSubproject(ModuleObject): @noKwargs @stringArgs - def get_variable(self, state, args, kwargs): + def get_variable(self, state: ModuleState, args: T.List[str], kwargs: TYPE_kwargs) -> TYPE_var: return self.subp.get_variable_method(args, kwargs) @FeatureNewKwargs('dependency', '0.56.0', ['include_type']) @permittedKwargs({'include_type'}) @stringArgs - def dependency(self, state, args, kwargs): + def dependency(self, state: ModuleState, args: T.List[str], kwargs: T.Dict[str, str]) -> dependencies.Dependency: info = self._args_to_info(args) if info['func'] == 'executable': raise InvalidArguments(f'{args[0]} is an executable and does not support the dependency() method. Use target() instead.') @@ -155,38 +157,38 @@ class CMakeSubproject(ModuleObject): @noKwargs @stringArgs - def include_directories(self, state, args, kwargs): + def include_directories(self, state: ModuleState, args: T.List[str], kwargs: TYPE_kwargs) -> build.IncludeDirs: info = self._args_to_info(args) return self.get_variable(state, [info['inc']], kwargs) @noKwargs @stringArgs - def target(self, state, args, kwargs): + def target(self, state: ModuleState, args: T.List[str], kwargs: TYPE_kwargs) -> build.Target: info = self._args_to_info(args) return self.get_variable(state, [info['tgt']], kwargs) @noKwargs @stringArgs - def target_type(self, state, args, kwargs): + def target_type(self, state: ModuleState, args: T.List[str], kwargs: TYPE_kwargs) -> str: info = self._args_to_info(args) return info['func'] @noPosargs @noKwargs - def target_list(self, state, args, kwargs): + def target_list(self, state: ModuleState, args: TYPE_var, kwargs: TYPE_kwargs) -> T.List[str]: return self.cm_interpreter.target_list() @noPosargs @noKwargs @FeatureNew('CMakeSubproject.found()', '0.53.2') - def found_method(self, state, args, kwargs): + def found_method(self, state: ModuleState, args: TYPE_var, kwargs: TYPE_kwargs) -> bool: return self.subp is not None class CMakeSubprojectOptions(ModuleObject): def __init__(self) -> None: super().__init__() - self.cmake_options = [] # type: T.List[str] + self.cmake_options: T.List[str] = [] self.target_options = TargetOptions() self.methods.update( @@ -205,40 +207,34 @@ class CMakeSubprojectOptions(ModuleObject): return self.target_options[kwargs['target']] return self.target_options.global_options + @typed_pos_args('subproject_options.add_cmake_defines', varargs=dict) @noKwargs - def add_cmake_defines(self, state, args, kwargs) -> None: - self.cmake_options += cmake_defines_to_args(args) + def add_cmake_defines(self, state: ModuleState, args: T.Tuple[T.List[T.Dict[str, TYPE_var]]], kwargs: TYPE_kwargs) -> None: + self.cmake_options += cmake_defines_to_args(args[0]) - @stringArgs + @typed_pos_args('subproject_options.set_override_option', str, str) @permittedKwargs({'target'}) - def set_override_option(self, state, args, kwargs) -> None: - if len(args) != 2: - raise InvalidArguments('set_override_option takes exactly 2 positional arguments') + def set_override_option(self, state: ModuleState, args: T.Tuple[str, str], kwargs: TYPE_kwargs) -> None: self._get_opts(kwargs).set_opt(args[0], args[1]) + @typed_pos_args('subproject_options.set_install', bool) @permittedKwargs({'target'}) - def set_install(self, state, args, kwargs) -> None: - if len(args) != 1 or not isinstance(args[0], bool): - raise InvalidArguments('set_install takes exactly 1 boolean argument') + def set_install(self, state: ModuleState, args: T.Tuple[bool], kwargs: TYPE_kwargs) -> None: self._get_opts(kwargs).set_install(args[0]) - @stringArgs + @typed_pos_args('subproject_options.append_compile_args', str, varargs=str, min_varargs=1) @permittedKwargs({'target'}) - def append_compile_args(self, state, args, kwargs) -> None: - if len(args) < 2: - raise InvalidArguments('append_compile_args takes at least 2 positional arguments') - self._get_opts(kwargs).append_args(args[0], args[1:]) + def append_compile_args(self, state: ModuleState, args: T.Tuple[str, T.List[str]], kwargs: TYPE_kwargs) -> None: + self._get_opts(kwargs).append_args(args[0], args[1]) - @stringArgs + @typed_pos_args('subproject_options.append_compile_args', varargs=str, min_varargs=1) @permittedKwargs({'target'}) - def append_link_args(self, state, args, kwargs) -> None: - if not args: - raise InvalidArguments('append_link_args takes at least 1 positional argument') - self._get_opts(kwargs).append_link_args(args) + def append_link_args(self, state: ModuleState, args: T.Tuple[T.List[str]], kwargs: TYPE_kwargs) -> None: + self._get_opts(kwargs).append_link_args(args[0]) @noPosargs @noKwargs - def clear(self, state, args, kwargs) -> None: + def clear(self, state: ModuleState, args: TYPE_var, kwargs: TYPE_kwargs) -> None: self.cmake_options.clear() self.target_options = TargetOptions() @@ -249,7 +245,7 @@ class CmakeModule(ExtensionModule): INFO = ModuleInfo('cmake', '0.50.0') - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter) -> None: super().__init__(interpreter) self.methods.update({ 'write_basic_package_version_file': self.write_basic_package_version_file, @@ -258,7 +254,7 @@ class CmakeModule(ExtensionModule): 'subproject_options': self.subproject_options, }) - def detect_voidp_size(self, env): + def detect_voidp_size(self, env: Environment) -> int: compilers = env.coredata.compilers.host compiler = compilers.get('c', None) if not compiler: @@ -267,9 +263,9 @@ class CmakeModule(ExtensionModule): if not compiler: raise mesonlib.MesonException('Requires a C or C++ compiler to compute sizeof(void *).') - return compiler.sizeof('void *', '', env) + return compiler.sizeof('void *', '', env)[0] - def detect_cmake(self, state): + def detect_cmake(self, state: ModuleState) -> bool: if self.cmake_detected: return True @@ -301,7 +297,7 @@ class CmakeModule(ExtensionModule): KwargInfo('version', str, required=True), INSTALL_DIR_KW, ) - def write_basic_package_version_file(self, state, args, kwargs: 'WriteBasicPackageVersionFile'): + def write_basic_package_version_file(self, state: ModuleState, args: TYPE_var, kwargs: 'WriteBasicPackageVersionFile') -> ModuleReturnValue: arch_independent = kwargs['arch_independent'] compatibility = kwargs['compatibility'] name = kwargs['name'] @@ -321,17 +317,17 @@ class CmakeModule(ExtensionModule): version_file = os.path.join(state.environment.scratch_dir, f'{name}ConfigVersion.cmake') - conf = { - 'CVF_VERSION': (version, ''), - 'CMAKE_SIZEOF_VOID_P': (str(self.detect_voidp_size(state.environment)), ''), - 'CVF_ARCH_INDEPENDENT': (arch_independent, ''), + conf: T.Dict[str, T.Union[str, bool, int]] = { + 'CVF_VERSION': version, + 'CMAKE_SIZEOF_VOID_P': str(self.detect_voidp_size(state.environment)), + 'CVF_ARCH_INDEPENDENT': arch_independent, } - mesonlib.do_conf_file(template_file, version_file, conf, 'meson') + mesonlib.do_conf_file(template_file, version_file, build.ConfigurationData(conf), 'meson') res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), version_file)], pkgroot, pkgroot_name, None, state.subproject) return ModuleReturnValue(res, [res]) - def create_package_file(self, infile, outfile, PACKAGE_RELATIVE_PATH, extra, confdata): + def create_package_file(self, infile: str, outfile: str, PACKAGE_RELATIVE_PATH: str, extra: str, confdata: build.ConfigurationData) -> None: package_init = PACKAGE_INIT_BASE.replace('@PACKAGE_RELATIVE_PATH@', PACKAGE_RELATIVE_PATH) package_init = package_init.replace('@inputFileName@', os.path.basename(infile)) package_init += extra @@ -369,7 +365,7 @@ class CmakeModule(ExtensionModule): KwargInfo('name', str, required=True), INSTALL_DIR_KW, ) - def configure_package_config_file(self, state, args, kwargs: 'ConfigurePackageConfigFile'): + def configure_package_config_file(self, state: ModuleState, args: TYPE_var, kwargs: 'ConfigurePackageConfigFile') -> build.Data: inputfile = kwargs['input'] if isinstance(inputfile, str): inputfile = mesonlib.File.from_source_file(state.environment.source_dir, state.subdir, inputfile) @@ -395,7 +391,8 @@ class CmakeModule(ExtensionModule): if not os.path.isabs(abs_install_dir): abs_install_dir = os.path.join(prefix, install_dir) - PACKAGE_RELATIVE_PATH = os.path.relpath(prefix, abs_install_dir) + # path used in cmake scripts are POSIX even on Windows + PACKAGE_RELATIVE_PATH = pathlib.PurePath(os.path.relpath(prefix, abs_install_dir)).as_posix() extra = '' if re.match('^(/usr)?/lib(64)?/.+', abs_install_dir): extra = PACKAGE_INIT_EXT.replace('@absInstallDir@', abs_install_dir) @@ -435,10 +432,10 @@ class CmakeModule(ExtensionModule): 'required': kwargs_['required'], 'options': kwargs_['options'], 'cmake_options': kwargs_['cmake_options'], - 'default_options': [], + 'default_options': {}, 'version': [], } - subp = self.interpreter.do_subproject(dirname, 'cmake', kw) + subp = self.interpreter.do_subproject(dirname, kw, force_method='cmake') if not subp.found(): return subp return CMakeSubproject(subp) @@ -446,8 +443,8 @@ class CmakeModule(ExtensionModule): @FeatureNew('subproject_options', '0.55.0') @noKwargs @noPosargs - def subproject_options(self, state, args, kwargs) -> CMakeSubprojectOptions: + def subproject_options(self, state: ModuleState, args: TYPE_var, kwargs: TYPE_kwargs) -> CMakeSubprojectOptions: return CMakeSubprojectOptions() -def initialize(*args, **kwargs): +def initialize(*args: T.Any, **kwargs: T.Any) -> CmakeModule: return CmakeModule(*args, **kwargs) diff --git a/mesonbuild/modules/cuda.py b/mesonbuild/modules/cuda.py index 67ed8ec..6f809cb 100644 --- a/mesonbuild/modules/cuda.py +++ b/mesonbuild/modules/cuda.py @@ -237,6 +237,10 @@ class CudaModule(NewExtensionModule): cuda_common_gpu_architectures += ['7.5+PTX'] # noqa: E221 cuda_hi_limit_gpu_architecture = '8.0' # noqa: E221 + # need to account for the fact that Ampere is commonly assumed to include + # SM8.0 and SM8.6 even though CUDA 11.0 doesn't support SM8.6 + cuda_ampere_bin = ['8.0'] + cuda_ampere_ptx = ['8.0'] if version_compare(cuda_version, '>=11.0'): cuda_known_gpu_architectures += ['Ampere'] # noqa: E221 cuda_common_gpu_architectures += ['8.0'] # noqa: E221 @@ -249,6 +253,9 @@ class CudaModule(NewExtensionModule): cuda_hi_limit_gpu_architecture = '8.6' # noqa: E221 if version_compare(cuda_version, '>=11.1'): + cuda_ampere_bin += ['8.6'] # noqa: E221 + cuda_ampere_ptx = ['8.6'] # noqa: E221 + cuda_common_gpu_architectures += ['8.6'] # noqa: E221 cuda_all_gpu_architectures += ['8.6'] # noqa: E221 @@ -320,7 +327,7 @@ class CudaModule(NewExtensionModule): 'Volta': (['7.0'], ['7.0']), 'Xavier': (['7.2'], []), 'Turing': (['7.5'], ['7.5']), - 'Ampere': (['8.0'], ['8.0']), + 'Ampere': (cuda_ampere_bin, cuda_ampere_ptx), 'Orin': (['8.7'], []), 'Lovelace': (['8.9'], ['8.9']), 'Hopper': (['9.0'], ['9.0']), @@ -336,8 +343,8 @@ class CudaModule(NewExtensionModule): arch_ptx = arch_bin cuda_arch_ptx += arch_ptx - cuda_arch_bin = sorted(list(set(cuda_arch_bin))) - cuda_arch_ptx = sorted(list(set(cuda_arch_ptx))) + cuda_arch_bin = sorted(set(cuda_arch_bin)) + cuda_arch_ptx = sorted(set(cuda_arch_ptx)) nvcc_flags = [] nvcc_archs_readable = [] diff --git a/mesonbuild/modules/dlang.py b/mesonbuild/modules/dlang.py index 7a9b99f..6d5359f 100644 --- a/mesonbuild/modules/dlang.py +++ b/mesonbuild/modules/dlang.py @@ -14,13 +14,15 @@ # This file contains the detection logic for external dependencies that # are UI-related. +from __future__ import annotations import json import os from . import ExtensionModule, ModuleInfo -from .. import dependencies from .. import mlog +from ..dependencies import Dependency +from ..dependencies.dub import DubDependency from ..interpreterbase import typed_pos_args from ..mesonlib import Popen_safe, MesonException @@ -38,7 +40,7 @@ class DlangModule(ExtensionModule): def _init_dub(self, state): if DlangModule.class_dubbin is None: - self.dubbin = dependencies.DubDependency.class_dubbin + self.dubbin = DubDependency.class_dubbin DlangModule.class_dubbin = self.dubbin else: self.dubbin = DlangModule.class_dubbin @@ -81,7 +83,7 @@ class DlangModule(ExtensionModule): config[key] = {} if isinstance(value, list): for dep in value: - if isinstance(dep, dependencies.Dependency): + if isinstance(dep, Dependency): name = dep.get_name() ret, res = self._call_dubbin(['describe', name]) if ret == 0: @@ -90,7 +92,7 @@ class DlangModule(ExtensionModule): config[key][name] = '' else: config[key][name] = version - elif isinstance(value, dependencies.Dependency): + elif isinstance(value, Dependency): name = value.get_name() ret, res = self._call_dubbin(['describe', name]) if ret == 0: diff --git a/mesonbuild/modules/external_project.py b/mesonbuild/modules/external_project.py index c3b01c8..bd6eba4 100644 --- a/mesonbuild/modules/external_project.py +++ b/mesonbuild/modules/external_project.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from pathlib import Path import os @@ -22,7 +23,8 @@ from . import ExtensionModule, ModuleReturnValue, NewExtensionModule, ModuleInfo from .. import mlog, build from ..compilers.compilers import CFLAGS_MAPPING from ..envconfig import ENV_VAR_PROG_MAP -from ..dependencies import InternalDependency, PkgConfigDependency +from ..dependencies import InternalDependency +from ..dependencies.pkgconfig import PkgConfigInterface from ..interpreterbase import FeatureNew from ..interpreter.type_checking import ENV_KW, DEPENDS_KW from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args @@ -33,9 +35,12 @@ if T.TYPE_CHECKING: from typing_extensions import TypedDict from . import ModuleState + from .._typing import ImmutableListProtocol + from ..build import BuildTarget, CustomTarget from ..interpreter import Interpreter from ..interpreterbase import TYPE_var - from ..build import BuildTarget, CustomTarget + from ..mesonlib import EnvironmentVariables + from ..utils.core import EnvironOrDict class Dependency(TypedDict): @@ -46,17 +51,20 @@ if T.TYPE_CHECKING: configure_options: T.List[str] cross_configure_options: T.List[str] verbose: bool - env: build.EnvironmentVariables + env: EnvironmentVariables depends: T.List[T.Union[BuildTarget, CustomTarget]] class ExternalProject(NewExtensionModule): + + make: ImmutableListProtocol[str] + def __init__(self, state: 'ModuleState', configure_command: str, configure_options: T.List[str], cross_configure_options: T.List[str], - env: build.EnvironmentVariables, + env: EnvironmentVariables, verbose: bool, extra_depends: T.List[T.Union['BuildTarget', 'CustomTarget']]): super().__init__() @@ -137,7 +145,7 @@ class ExternalProject(NewExtensionModule): # Set common env variables like CFLAGS, CC, etc. link_exelist: T.List[str] = [] link_args: T.List[str] = [] - self.run_env = os.environ.copy() + self.run_env: EnvironOrDict = os.environ.copy() for lang, compiler in self.env.coredata.compilers[MachineChoice.HOST].items(): if any(lang not in i for i in (ENV_VAR_PROG_MAP, CFLAGS_MAPPING)): continue @@ -158,8 +166,8 @@ class ExternalProject(NewExtensionModule): self.run_env['LDFLAGS'] = self._quote_and_join(link_args) self.run_env = self.user_env.get_env(self.run_env) - self.run_env = PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST, - uninstalled=True) + self.run_env = PkgConfigInterface.setup_env(self.run_env, self.env, MachineChoice.HOST, + uninstalled=True) self.build_dir.mkdir(parents=True, exist_ok=True) self._run('configure', configure_cmd, workdir) @@ -199,7 +207,7 @@ class ExternalProject(NewExtensionModule): def _run(self, step: str, command: T.List[str], workdir: Path) -> None: mlog.log(f'External project {self.name}:', mlog.bold(step)) m = 'Running command ' + str(command) + ' in directory ' + str(workdir) + '\n' - log_filename = Path(mlog.log_dir, f'{self.name}-{step}.log') + log_filename = Path(mlog.get_log_dir(), f'{self.name}-{step}.log') output = None if not self.verbose: output = open(log_filename, 'w', encoding='utf-8') @@ -223,7 +231,7 @@ class ExternalProject(NewExtensionModule): '--srcdir', self.src_dir.as_posix(), '--builddir', self.build_dir.as_posix(), '--installdir', self.install_dir.as_posix(), - '--logdir', mlog.log_dir, + '--logdir', mlog.get_log_dir(), '--make', join_args(self.make), ] if self.verbose: @@ -240,6 +248,7 @@ class ExternalProject(NewExtensionModule): depfile=f'{self.name}.d', console=True, extra_depends=extra_depends, + description='Generating external project {}', ) idir = build.InstallDir(self.subdir.as_posix(), @@ -269,7 +278,7 @@ class ExternalProject(NewExtensionModule): link_args = [f'-L{abs_libdir}', f'-l{libname}'] sources = self.target dep = InternalDependency(version, [], compile_args, link_args, [], - [], [sources], [], {}, [], []) + [], [sources], [], [], {}, [], [], []) return dep diff --git a/mesonbuild/modules/fs.py b/mesonbuild/modules/fs.py index 7d96995..5a9533c 100644 --- a/mesonbuild/modules/fs.py +++ b/mesonbuild/modules/fs.py @@ -20,19 +20,16 @@ import typing as T from . import ExtensionModule, ModuleReturnValue, ModuleInfo from .. import mlog -from ..build import CustomTarget, InvalidArguments +from ..build import BuildTarget, CustomTarget, CustomTargetIndex, InvalidArguments from ..interpreter.type_checking import INSTALL_KW, INSTALL_MODE_KW, INSTALL_TAG_KW, NoneType from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs -from ..mesonlib import ( - File, - MesonException, - has_path_sep, - path_is_in_root, -) +from ..mesonlib import File, MesonException, has_path_sep, path_is_in_root, relpath if T.TYPE_CHECKING: from . import ModuleState + from ..build import BuildTargetTypes from ..interpreter import Interpreter + from ..interpreterbase import TYPE_kwargs from ..mesonlib import FileOrString, FileMode from typing_extensions import TypedDict @@ -75,6 +72,7 @@ class FSModule(ExtensionModule): 'stem': self.stem, 'read': self.read, 'copyfile': self.copyfile, + 'relative_to': self.relative_to, }) def _absolute_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path: @@ -261,8 +259,10 @@ class FSModule(ExtensionModule): try: with open(path, encoding=encoding) as f: data = f.read() + except FileNotFoundError: + raise MesonException(f'File {args[0]} does not exist.') except UnicodeDecodeError: - raise MesonException(f'decoding failed for {path}') + raise MesonException(f'decoding failed for {args[0]}') # Reconfigure when this file changes as it can contain data used by any # part of the build configuration (e.g. `project(..., version: # fs.read_file('VERSION')` or `configure_file(...)` @@ -306,10 +306,28 @@ class FSModule(ExtensionModule): install_mode=kwargs['install_mode'], install_tag=[kwargs['install_tag']], backend=state.backend, + description='Copying file {}', ) return ModuleReturnValue(ct, [ct]) + @FeatureNew('fs.relative_to', '1.3.0') + @typed_pos_args('fs.relative_to', (str, File, CustomTarget, CustomTargetIndex, BuildTarget), (str, File, CustomTarget, CustomTargetIndex, BuildTarget)) + @noKwargs + def relative_to(self, state: ModuleState, args: T.Tuple[T.Union[FileOrString, BuildTargetTypes], T.Union[FileOrString, BuildTargetTypes]], kwargs: TYPE_kwargs) -> str: + def to_path(arg: T.Union[FileOrString, CustomTarget, CustomTargetIndex, BuildTarget]) -> str: + if isinstance(arg, File): + return arg.absolute_path(state.environment.source_dir, state.environment.build_dir) + elif isinstance(arg, (CustomTarget, CustomTargetIndex, BuildTarget)): + return state.backend.get_target_filename_abs(arg) + else: + return os.path.join(state.environment.source_dir, state.subdir, arg) + + t = to_path(args[0]) + f = to_path(args[1]) + + return relpath(t, f) + def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule: return FSModule(*args, **kwargs) diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index d447f09..d8367dd 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -24,20 +24,23 @@ import subprocess import textwrap import typing as T -from . import ExtensionModule, ModuleInfo -from . import ModuleReturnValue +from . import ( + ExtensionModule, GirTarget, GResourceHeaderTarget, GResourceTarget, ModuleInfo, + ModuleReturnValue, TypelibTarget, VapiTarget, +) from .. import build from .. import interpreter from .. import mesonlib from .. import mlog from ..build import CustomTarget, CustomTargetIndex, Executable, GeneratedList, InvalidArguments -from ..dependencies import Dependency, PkgConfigDependency, InternalDependency -from ..interpreter.type_checking import DEPENDS_KW, DEPEND_FILES_KW, INSTALL_DIR_KW, INSTALL_KW, NoneType, SOURCES_KW, in_set_validator +from ..dependencies import Dependency, InternalDependency +from ..dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface +from ..interpreter.type_checking import DEPENDS_KW, DEPEND_FILES_KW, ENV_KW, INSTALL_DIR_KW, INSTALL_KW, NoneType, DEPENDENCY_SOURCES_KW, in_set_validator from ..interpreterbase import noPosargs, noKwargs, FeatureNew, FeatureDeprecated from ..interpreterbase import typed_kwargs, KwargInfo, ContainerTypeInfo from ..interpreterbase.decorators import typed_pos_args from ..mesonlib import ( - MachineChoice, MesonException, OrderedSet, Popen_safe, join_args, + MachineChoice, MesonException, OrderedSet, Popen_safe, join_args, quote_arg ) from ..programs import OverrideProgram from ..scripts.gettext import read_linguas @@ -76,7 +79,7 @@ if T.TYPE_CHECKING: build_by_default: bool c_name: T.Optional[str] - dependencies: T.List[T.Union[mesonlib.File, build.CustomTarget, build.CustomTargetIndex]] + dependencies: T.List[T.Union[mesonlib.File, CustomTarget, CustomTargetIndex]] export: bool extra_args: T.List[str] gresource_bundle: bool @@ -173,7 +176,6 @@ if T.TYPE_CHECKING: class _MkEnumsCommon(TypedDict): - sources: T.List[T.Union[FileOrString, build.GeneratedTypes]] install_header: bool install_dir: T.Optional[str] identifier_prefix: T.Optional[str] @@ -181,6 +183,7 @@ if T.TYPE_CHECKING: class MkEnumsSimple(_MkEnumsCommon): + sources: T.List[FileOrString] header_prefix: str decorator: str function_prefix: str @@ -188,6 +191,7 @@ if T.TYPE_CHECKING: class MkEnums(_MkEnumsCommon): + sources: T.List[T.Union[FileOrString, build.GeneratedTypes]] c_template: T.Optional[FileOrString] h_template: T.Optional[FileOrString] comments: T.Optional[str] @@ -200,6 +204,8 @@ if T.TYPE_CHECKING: vtail: T.Optional[str] depends: T.List[T.Union[BuildTarget, CustomTarget, CustomTargetIndex]] + ToolType = T.Union[Executable, ExternalProgram, OverrideProgram] + # Differs from the CustomTarget version in that it straight defaults to True _BUILD_BY_DEFAULT: KwargInfo[bool] = KwargInfo( @@ -216,12 +222,6 @@ _EXTRA_ARGS_KW: KwargInfo[T.List[str]] = KwargInfo( _MK_ENUMS_COMMON_KWS: T.List[KwargInfo] = [ INSTALL_KW.evolve(name='install_header'), INSTALL_DIR_KW, - KwargInfo( - 'sources', - ContainerTypeInfo(list, (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)), - listify=True, - required=True, - ), KwargInfo('identifier_prefix', (str, NoneType)), KwargInfo('symbol_prefix', (str, NoneType)), ] @@ -248,21 +248,6 @@ def annotations_validator(annotations: T.List[T.Union[str, T.List[str]]]) -> T.O return f'element {c+1} {badlist}' return None -class GResourceTarget(build.CustomTarget): - pass - -class GResourceHeaderTarget(build.CustomTarget): - pass - -class GirTarget(build.CustomTarget): - pass - -class TypelibTarget(build.CustomTarget): - pass - -class VapiTarget(build.CustomTarget): - pass - # gresource compilation is broken due to the way # the resource compiler and Ninja clash about it # @@ -277,14 +262,14 @@ class GnomeModule(ExtensionModule): def __init__(self, interpreter: 'Interpreter') -> None: super().__init__(interpreter) self.gir_dep: T.Optional[Dependency] = None - self.giscanner: T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]] = None - self.gicompiler: T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]] = None + self.giscanner: T.Optional[T.Union[ExternalProgram, Executable, OverrideProgram]] = None + self.gicompiler: T.Optional[T.Union[ExternalProgram, Executable, OverrideProgram]] = None self.install_glib_compile_schemas = False self.install_gio_querymodules: T.List[str] = [] self.install_gtk_update_icon_cache = False self.install_update_desktop_database = False self.install_update_mime_database = False - self.devenv: T.Optional[build.EnvironmentVariables] = None + self.devenv: T.Optional[mesonlib.EnvironmentVariables] = None self.native_glib_version: T.Optional[str] = None self.methods.update({ 'post_install': self.post_install, @@ -319,14 +304,31 @@ class GnomeModule(ExtensionModule): gresource_dep_needed_version): mlog.warning('GLib compiled dependencies do not work reliably with \n' 'the current version of GLib. See the following upstream issue:', - mlog.bold('https://bugzilla.gnome.org/show_bug.cgi?id=774368')) + mlog.bold('https://bugzilla.gnome.org/show_bug.cgi?id=774368'), + once=True, fatal=False) @staticmethod def _print_gdbus_warning() -> None: mlog.warning('Code generated with gdbus_codegen() requires the root directory be added to\n' ' include_directories of targets with GLib < 2.51.3:', mlog.bold('https://github.com/mesonbuild/meson/issues/1387'), - once=True) + once=True, fatal=False) + + @staticmethod + def _find_tool(state: 'ModuleState', tool: str) -> 'ToolType': + tool_map = { + 'gio-querymodules': 'gio-2.0', + 'glib-compile-schemas': 'gio-2.0', + 'glib-compile-resources': 'gio-2.0', + 'gdbus-codegen': 'gio-2.0', + 'glib-genmarshal': 'glib-2.0', + 'glib-mkenums': 'glib-2.0', + 'g-ir-scanner': 'gobject-introspection-1.0', + 'g-ir-compiler': 'gobject-introspection-1.0', + } + depname = tool_map[tool] + varname = tool.replace('-', '_') + return state.find_tool(tool, depname, varname) @typed_kwargs( 'gnome.post_install', @@ -339,11 +341,11 @@ class GnomeModule(ExtensionModule): @noPosargs @FeatureNew('gnome.post_install', '0.57.0') def post_install(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'PostInstall') -> ModuleReturnValue: - rv: T.List['build.ExecutableSerialisation'] = [] + rv: T.List['mesonlib.ExecutableSerialisation'] = [] datadir_abs = os.path.join(state.environment.get_prefix(), state.environment.get_datadir()) if kwargs['glib_compile_schemas'] and not self.install_glib_compile_schemas: self.install_glib_compile_schemas = True - prog = state.find_tool('glib-compile-schemas', 'gio-2.0', 'glib_compile_schemas') + prog = self._find_tool(state, 'glib-compile-schemas') schemasdir = os.path.join(datadir_abs, 'glib-2.0', 'schemas') script = state.backend.get_executable_serialisation([prog, schemasdir]) script.skip_if_destdir = True @@ -351,7 +353,7 @@ class GnomeModule(ExtensionModule): for d in kwargs['gio_querymodules']: if d not in self.install_gio_querymodules: self.install_gio_querymodules.append(d) - prog = state.find_tool('gio-querymodules', 'gio-2.0', 'gio_querymodules') + prog = self._find_tool(state, 'gio-querymodules') moduledir = os.path.join(state.environment.get_prefix(), d) script = state.backend.get_executable_serialisation([prog, moduledir]) script.skip_if_destdir = True @@ -359,7 +361,7 @@ class GnomeModule(ExtensionModule): if kwargs['gtk_update_icon_cache'] and not self.install_gtk_update_icon_cache: self.install_gtk_update_icon_cache = True prog = state.find_program('gtk4-update-icon-cache', required=False) - found = isinstance(prog, build.Executable) or prog.found() + found = isinstance(prog, Executable) or prog.found() if not found: prog = state.find_program('gtk-update-icon-cache') icondir = os.path.join(datadir_abs, 'icons', 'hicolor') @@ -382,7 +384,7 @@ class GnomeModule(ExtensionModule): rv.append(script) return ModuleReturnValue(None, rv) - @typed_pos_args('gnome.compile_resources', str, (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)) + @typed_pos_args('gnome.compile_resources', str, (str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList)) @typed_kwargs( 'gnome.compile_resources', _BUILD_BY_DEFAULT, @@ -391,7 +393,7 @@ class GnomeModule(ExtensionModule): INSTALL_KW.evolve(name='install_header', since='0.37.0'), INSTALL_DIR_KW, KwargInfo('c_name', (str, NoneType)), - KwargInfo('dependencies', ContainerTypeInfo(list, (mesonlib.File, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True), + KwargInfo('dependencies', ContainerTypeInfo(list, (mesonlib.File, CustomTarget, CustomTargetIndex)), default=[], listify=True), KwargInfo('export', bool, default=False, since='0.37.0'), KwargInfo('gresource_bundle', bool, default=False, since='0.37.0'), KwargInfo('source_dir', ContainerTypeInfo(list, str), default=[], listify=True), @@ -401,8 +403,8 @@ class GnomeModule(ExtensionModule): self.__print_gresources_warning(state) glib_version = self._get_native_glib_version(state) - glib_compile_resources = state.find_program('glib-compile-resources') - cmd: T.List[T.Union[ExternalProgram, str]] = [glib_compile_resources, '@INPUT@'] + glib_compile_resources = self._find_tool(state, 'glib-compile-resources') + cmd: T.List[T.Union['ToolType', str]] = [glib_compile_resources, '@INPUT@'] source_dirs = kwargs['source_dir'] dependencies = kwargs['dependencies'] @@ -411,7 +413,7 @@ class GnomeModule(ExtensionModule): # Validate dependencies subdirs: T.List[str] = [] - depends: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]] = [] + depends: T.List[T.Union[CustomTarget, CustomTargetIndex]] = [] for dep in dependencies: if isinstance(dep, mesonlib.File): subdirs.append(dep.subdir) @@ -419,7 +421,7 @@ class GnomeModule(ExtensionModule): depends.append(dep) subdirs.append(dep.get_subdir()) if not mesonlib.version_compare(glib_version, gresource_dep_needed_version): - m = 'The "dependencies" argument of gnome.compile_resources() can not\n' \ + m = 'The "dependencies" argument of gnome.compile_resources() cannot\n' \ 'be used with the current version of glib-compile-resources due to\n' \ '' raise MesonException(m) @@ -438,7 +440,7 @@ class GnomeModule(ExtensionModule): else: ifile = os.path.join(input_file.subdir, input_file.fname) - elif isinstance(input_file, (build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)): + elif isinstance(input_file, (CustomTarget, CustomTargetIndex, GeneratedList)): raise MesonException('Resource xml files generated at build-time cannot be used with ' 'gnome.compile_resources() in the current version of glib-compile-resources ' 'because we need to scan the xml for dependencies due to ' @@ -492,7 +494,7 @@ class GnomeModule(ExtensionModule): raise MesonException('GResource header is installed yet export is not enabled') depfile: T.Optional[str] = None - target_cmd: T.List[T.Union[ExternalProgram, str]] + target_cmd: T.List[T.Union['ToolType', str]] if not mesonlib.version_compare(glib_version, gresource_dep_needed_version): # This will eventually go out of sync if dependencies are added target_cmd = cmd @@ -542,8 +544,8 @@ class GnomeModule(ExtensionModule): @staticmethod def _get_gresource_dependencies( state: 'ModuleState', input_file: str, source_dirs: T.List[str], - dependencies: T.Sequence[T.Union[mesonlib.File, build.CustomTarget, build.CustomTargetIndex]] - ) -> T.Tuple[T.List[mesonlib.FileOrString], T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]], T.List[str]]: + dependencies: T.Sequence[T.Union[mesonlib.File, CustomTarget, CustomTargetIndex]] + ) -> T.Tuple[T.List[mesonlib.FileOrString], T.List[T.Union[CustomTarget, CustomTargetIndex]], T.List[str]]: cmd = ['glib-compile-resources', input_file, @@ -565,7 +567,7 @@ class GnomeModule(ExtensionModule): raw_dep_files: T.List[str] = stdout.split('\n')[:-1] - depends: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex]] = [] + depends: T.List[T.Union[CustomTarget, CustomTargetIndex]] = [] subdirs: T.List[str] = [] dep_files: T.List[mesonlib.FileOrString] = [] for resfile in raw_dep_files.copy(): @@ -578,7 +580,7 @@ class GnomeModule(ExtensionModule): dep_files.append(dep) subdirs.append(dep.subdir) break - elif isinstance(dep, (build.CustomTarget, build.CustomTargetIndex)): + elif isinstance(dep, (CustomTarget, CustomTargetIndex)): fname = None outputs = {(o, os.path.basename(o)) for o in dep.get_outputs()} for o, baseo in outputs: @@ -644,7 +646,7 @@ class GnomeModule(ExtensionModule): return link_command, new_depends def _get_dependencies_flags_raw( - self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]], + self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, CustomTarget, CustomTargetIndex]], state: 'ModuleState', depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]], include_rpath: bool, @@ -737,7 +739,7 @@ class GnomeModule(ExtensionModule): return cflags, internal_ldflags, external_ldflags, gi_includes, depends def _get_dependencies_flags( - self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]], + self, deps: T.Sequence[T.Union['Dependency', build.BuildTarget, CustomTarget, CustomTargetIndex]], state: 'ModuleState', depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]], include_rpath: bool = False, @@ -763,9 +765,9 @@ class GnomeModule(ExtensionModule): return cflags, internal_ldflags, external_ldflags, gi_includes, depends - def _unwrap_gir_target(self, girtarget: T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary], state: 'ModuleState' - ) -> T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary]: - if not isinstance(girtarget, (build.Executable, build.SharedLibrary, + def _unwrap_gir_target(self, girtarget: T.Union[Executable, build.StaticLibrary, build.SharedLibrary], state: 'ModuleState' + ) -> T.Union[Executable, build.StaticLibrary, build.SharedLibrary]: + if not isinstance(girtarget, (Executable, build.SharedLibrary, build.StaticLibrary)): raise MesonException(f'Gir target must be an executable or library but is "{girtarget}" of type {type(girtarget).__name__}') @@ -780,18 +782,19 @@ class GnomeModule(ExtensionModule): def _devenv_prepend(self, varname: str, value: str) -> None: if self.devenv is None: - self.devenv = build.EnvironmentVariables() + self.devenv = mesonlib.EnvironmentVariables() self.devenv.prepend(varname, [value]) - def get_devenv(self) -> T.Optional[build.EnvironmentVariables]: - return self.devenv + def postconf_hook(self, b: build.Build) -> None: + if self.devenv is not None: + b.devenv.append(self.devenv) - def _get_gir_dep(self, state: 'ModuleState') -> T.Tuple[Dependency, T.Union[build.Executable, 'ExternalProgram', 'OverrideProgram'], - T.Union[build.Executable, 'ExternalProgram', 'OverrideProgram']]: + def _get_gir_dep(self, state: 'ModuleState') -> T.Tuple[Dependency, T.Union[Executable, 'ExternalProgram', 'OverrideProgram'], + T.Union[Executable, 'ExternalProgram', 'OverrideProgram']]: if not self.gir_dep: self.gir_dep = state.dependency('gobject-introspection-1.0') - self.giscanner = state.find_tool('g-ir-scanner', 'gobject-introspection-1.0', 'g_ir_scanner') - self.gicompiler = state.find_tool('g-ir-compiler', 'gobject-introspection-1.0', 'g_ir_compiler') + self.giscanner = self._find_tool(state, 'g-ir-scanner') + self.gicompiler = self._find_tool(state, 'g-ir-compiler') return self.gir_dep, self.giscanner, self.gicompiler @functools.lru_cache(maxsize=None) @@ -835,11 +838,11 @@ class GnomeModule(ExtensionModule): return ret @staticmethod - def _scan_gir_targets(state: 'ModuleState', girtargets: T.Sequence[build.BuildTarget]) -> T.List[T.Union[str, build.Executable]]: - ret: T.List[T.Union[str, build.Executable]] = [] + def _scan_gir_targets(state: 'ModuleState', girtargets: T.Sequence[build.BuildTarget]) -> T.List[T.Union[str, Executable]]: + ret: T.List[T.Union[str, Executable]] = [] for girtarget in girtargets: - if isinstance(girtarget, build.Executable): + if isinstance(girtarget, Executable): ret += ['--program', girtarget] else: # Because of https://gitlab.gnome.org/GNOME/gobject-introspection/merge_requests/72 @@ -882,18 +885,18 @@ class GnomeModule(ExtensionModule): @staticmethod def _get_gir_targets_deps(girtargets: T.Sequence[build.BuildTarget] - ) -> T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, Dependency]]: - ret: T.List[T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, Dependency]] = [] + ) -> T.List[T.Union[build.BuildTarget, CustomTarget, CustomTargetIndex, Dependency]]: + ret: T.List[T.Union[build.BuildTarget, CustomTarget, CustomTargetIndex, Dependency]] = [] for girtarget in girtargets: ret += girtarget.get_all_link_deps() ret += girtarget.get_external_deps() return ret @staticmethod - def _get_gir_targets_inc_dirs(girtargets: T.Sequence[build.BuildTarget]) -> T.List[build.IncludeDirs]: - ret: T.List[build.IncludeDirs] = [] + def _get_gir_targets_inc_dirs(girtargets: T.Sequence[build.BuildTarget]) -> OrderedSet[build.IncludeDirs]: + ret: OrderedSet = OrderedSet() for girtarget in girtargets: - ret += girtarget.get_include_dirs() + ret.update(girtarget.get_include_dirs()) return ret @staticmethod @@ -929,8 +932,8 @@ class GnomeModule(ExtensionModule): def _make_gir_filelist(state: 'ModuleState', srcdir: str, ns: str, nsversion: str, girtargets: T.Sequence[build.BuildTarget], libsources: T.Sequence[T.Union[ - str, mesonlib.File, build.GeneratedList, - build.CustomTarget, build.CustomTargetIndex]] + str, mesonlib.File, GeneratedList, + CustomTarget, CustomTargetIndex]] ) -> str: gir_filelist_dir = state.backend.get_target_private_dir_abs(girtargets[0]) if not os.path.isdir(gir_filelist_dir): @@ -939,14 +942,14 @@ class GnomeModule(ExtensionModule): with open(gir_filelist_filename, 'w', encoding='utf-8') as gir_filelist: for s in libsources: - if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)): + if isinstance(s, (CustomTarget, CustomTargetIndex)): for custom_output in s.get_outputs(): gir_filelist.write(os.path.join(state.environment.get_build_dir(), state.backend.get_target_dir(s), custom_output) + '\n') elif isinstance(s, mesonlib.File): gir_filelist.write(s.rel_to_builddir(state.build_to_src) + '\n') - elif isinstance(s, build.GeneratedList): + elif isinstance(s, GeneratedList): for gen_src in s.get_outputs(): gir_filelist.write(os.path.join(srcdir, gen_src) + '\n') else: @@ -959,7 +962,7 @@ class GnomeModule(ExtensionModule): state: 'ModuleState', girfile: str, scan_command: T.Sequence[T.Union['FileOrString', Executable, ExternalProgram, OverrideProgram]], - generated_files: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]], + generated_files: T.Sequence[T.Union[str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList]], depends: T.Sequence[T.Union['FileOrString', build.BuildTarget, 'build.GeneratedTypes', build.StructuredSources]], kwargs: T.Dict[str, T.Any]) -> GirTarget: install = kwargs['install_gir'] @@ -977,7 +980,11 @@ class GnomeModule(ExtensionModule): # -uninstalled.pc files Meson generated. It also must respect pkgconfig # settings user could have set in machine file, like PKG_CONFIG_LIBDIR, # SYSROOT, etc. - run_env = PkgConfigDependency.get_env(state.environment, MachineChoice.HOST, uninstalled=True) + run_env = PkgConfigInterface.get_env(state.environment, MachineChoice.HOST, uninstalled=True) + # g-ir-scanner uses Python's distutils to find the compiler, which uses 'CC' + cc_exelist = state.environment.coredata.compilers.host['c'].get_exelist() + run_env.set('CC', [quote_arg(x) for x in cc_exelist], ' ') + run_env.merge(kwargs['env']) return GirTarget( girfile, @@ -997,8 +1004,8 @@ class GnomeModule(ExtensionModule): @staticmethod def _make_typelib_target(state: 'ModuleState', typelib_output: str, - typelib_cmd: T.Sequence[T.Union[str, build.Executable, ExternalProgram, build.CustomTarget]], - generated_files: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]], + typelib_cmd: T.Sequence[T.Union[str, Executable, ExternalProgram, CustomTarget]], + generated_files: T.Sequence[T.Union[str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList]], kwargs: T.Dict[str, T.Any]) -> TypelibTarget: install = kwargs['install_typelib'] if install is None: @@ -1022,12 +1029,13 @@ class GnomeModule(ExtensionModule): install_dir=[install_dir], install_tag=['typelib'], build_by_default=kwargs['build_by_default'], + env=kwargs['env'], ) @staticmethod def _gather_typelib_includes_and_update_depends( state: 'ModuleState', - deps: T.Sequence[T.Union[Dependency, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]], + deps: T.Sequence[T.Union[Dependency, build.BuildTarget, CustomTarget, CustomTargetIndex]], depends: T.Sequence[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]] ) -> T.Tuple[T.List[str], T.List[T.Union[build.BuildTarget, 'build.GeneratedTypes', 'FileOrString', build.StructuredSources]]]: # Need to recursively add deps on GirTarget sources from our @@ -1088,12 +1096,13 @@ class GnomeModule(ExtensionModule): if f.startswith(('-L', '-l', '--extra-library')): yield f - @typed_pos_args('gnome.generate_gir', varargs=(build.Executable, build.SharedLibrary, build.StaticLibrary), min_varargs=1) + @typed_pos_args('gnome.generate_gir', varargs=(Executable, build.SharedLibrary, build.StaticLibrary), min_varargs=1) @typed_kwargs( 'gnome.generate_gir', INSTALL_KW, _BUILD_BY_DEFAULT.evolve(since='0.40.0'), _EXTRA_ARGS_KW, + ENV_KW.evolve(since='1.2.0'), KwargInfo('dependencies', ContainerTypeInfo(list, Dependency), default=[], listify=True), KwargInfo('export_packages', ContainerTypeInfo(list, str), default=[], listify=True), KwargInfo('fatal_warnings', bool, default=False, since='0.55.0'), @@ -1112,13 +1121,16 @@ class GnomeModule(ExtensionModule): KwargInfo('link_with', ContainerTypeInfo(list, (build.SharedLibrary, build.StaticLibrary)), default=[], listify=True), KwargInfo('namespace', str, required=True), KwargInfo('nsversion', str, required=True), - KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True), + KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File, GeneratedList, CustomTarget, CustomTargetIndex)), default=[], listify=True), KwargInfo('symbol_prefix', ContainerTypeInfo(list, str), default=[], listify=True), ) - def generate_gir(self, state: 'ModuleState', args: T.Tuple[T.List[T.Union[build.Executable, build.SharedLibrary, build.StaticLibrary]]], + def generate_gir(self, state: 'ModuleState', args: T.Tuple[T.List[T.Union[Executable, build.SharedLibrary, build.StaticLibrary]]], kwargs: 'GenerateGir') -> ModuleReturnValue: + # Ensure we have a C compiler even in C++ projects. + state.add_language('c', MachineChoice.HOST) + girtargets = [self._unwrap_gir_target(arg, state) for arg in args[0]] - if len(girtargets) > 1 and any(isinstance(el, build.Executable) for el in girtargets): + if len(girtargets) > 1 and any(isinstance(el, Executable) for el in girtargets): raise MesonException('generate_gir only accepts a single argument when one of the arguments is an executable') gir_dep, giscanner, gicompiler = self._get_gir_dep(state) @@ -1161,7 +1173,7 @@ class GnomeModule(ExtensionModule): gir_inc_dirs: T.List[str] = [] - scan_command: T.List[T.Union[str, build.Executable, 'ExternalProgram', 'OverrideProgram']] = [giscanner] + scan_command: T.List[T.Union[str, Executable, 'ExternalProgram', 'OverrideProgram']] = [giscanner] scan_command += ['--quiet'] scan_command += ['--no-libtool'] scan_command += ['--namespace=' + ns, '--nsversion=' + nsversion] @@ -1231,12 +1243,12 @@ class GnomeModule(ExtensionModule): srcdir = os.path.join(state.build_to_src, state.subdir) outdir = state.subdir - cmd: T.List[T.Union[ExternalProgram, str]] = [state.find_program('glib-compile-schemas'), '--targetdir', outdir, srcdir] + cmd: T.List[T.Union['ToolType', str]] = [self._find_tool(state, 'glib-compile-schemas'), '--targetdir', outdir, srcdir] if state.subdir == '': targetname = 'gsettings-compile' else: targetname = 'gsettings-compile-' + state.subdir.replace('/', '_') - target_g = build.CustomTarget( + target_g = CustomTarget( targetname, state.subdir, state.subproject, @@ -1246,6 +1258,7 @@ class GnomeModule(ExtensionModule): ['gschemas.compiled'], build_by_default=kwargs['build_by_default'], depend_files=kwargs['depend_files'], + description='Compiling gschemas {}', ) self._devenv_prepend('GSETTINGS_SCHEMA_DIR', os.path.join(state.environment.get_build_dir(), state.subdir)) return ModuleReturnValue(target_g, [target_g]) @@ -1310,7 +1323,7 @@ class GnomeModule(ExtensionModule): pot_file = os.path.join('@SOURCE_ROOT@', state.subdir, 'C', project_id + '.pot') pot_sources = [os.path.join('@SOURCE_ROOT@', state.subdir, 'C', s) for s in sources] - pot_args: T.List[T.Union['ExternalProgram', str]] = [itstool, '-o', pot_file] + pot_args: T.List[T.Union[ExternalProgram, Executable, OverrideProgram, str]] = [itstool, '-o', pot_file] pot_args.extend(pot_sources) pottarget = build.RunTarget(f'help-{project_id}-pot', pot_args, [], os.path.join(state.subdir, 'C'), state.subproject, @@ -1339,7 +1352,7 @@ class GnomeModule(ExtensionModule): targets.append(l_data) po_file = l + '.po' - po_args: T.List[T.Union['ExternalProgram', str]] = [ + po_args: T.List[T.Union[ExternalProgram, Executable, OverrideProgram, str]] = [ msgmerge, '-q', '-o', os.path.join('@SOURCE_ROOT@', l_subdir, po_file), os.path.join('@SOURCE_ROOT@', l_subdir, po_file), pot_file] @@ -1350,7 +1363,7 @@ class GnomeModule(ExtensionModule): potargets.append(potarget) gmo_file = project_id + '-' + l + '.gmo' - gmotarget = build.CustomTarget( + gmotarget = CustomTarget( f'help-{project_id}-{l}-gmo', l_subdir, state.subproject, @@ -1359,10 +1372,11 @@ class GnomeModule(ExtensionModule): [po_file], [gmo_file], install_tag=['doc'], + description='Generating yelp doc {}', ) targets.append(gmotarget) - mergetarget = build.CustomTarget( + mergetarget = CustomTarget( f'help-{project_id}-{l}', l_subdir, state.subproject, @@ -1374,6 +1388,7 @@ class GnomeModule(ExtensionModule): install=True, install_dir=[l_install_dir], install_tag=['doc'], + description='Generating yelp doc {}', ) targets.append(mergetarget) @@ -1388,7 +1403,7 @@ class GnomeModule(ExtensionModule): 'gnome.gtkdoc', KwargInfo('c_args', ContainerTypeInfo(list, str), since='0.48.0', default=[], listify=True), KwargInfo('check', bool, default=False, since='0.52.0'), - KwargInfo('content_files', ContainerTypeInfo(list, (str, mesonlib.File, build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)), default=[], listify=True), + KwargInfo('content_files', ContainerTypeInfo(list, (str, mesonlib.File, GeneratedList, CustomTarget, CustomTargetIndex)), default=[], listify=True), KwargInfo( 'dependencies', ContainerTypeInfo(list, (Dependency, build.SharedLibrary, build.StaticLibrary)), @@ -1431,6 +1446,9 @@ class GnomeModule(ExtensionModule): namespace = kwargs['namespace'] + # Ensure we have a C compiler even in C++ projects. + state.add_language('c', MachineChoice.HOST) + def abs_filenames(files: T.Iterable['FileOrString']) -> T.Iterator[str]: for f in files: if isinstance(f, mesonlib.File): @@ -1461,6 +1479,7 @@ class GnomeModule(ExtensionModule): program_name = 'gtkdoc-' + tool program = state.find_program(program_name) path = program.get_path() + assert path is not None, "This shouldn't be possible since program should be found" t_args.append(f'--{program_name}={path}') if namespace: t_args.append('--namespace=' + namespace) @@ -1478,7 +1497,7 @@ class GnomeModule(ExtensionModule): depends: T.List['build.GeneratedTypes'] = [] content_files = [] for s in kwargs['content_files']: - if isinstance(s, (build.CustomTarget, build.CustomTargetIndex)): + if isinstance(s, (CustomTarget, CustomTargetIndex)): depends.append(s) for o in s.get_outputs(): content_files.append(os.path.join(state.environment.get_build_dir(), @@ -1487,7 +1506,7 @@ class GnomeModule(ExtensionModule): elif isinstance(s, mesonlib.File): content_files.append(s.absolute_path(state.environment.get_source_dir(), state.environment.get_build_dir())) - elif isinstance(s, build.GeneratedList): + elif isinstance(s, GeneratedList): depends.append(s) for gen_src in s.get_outputs(): content_files.append(os.path.join(state.environment.get_source_dir(), @@ -1506,7 +1525,7 @@ class GnomeModule(ExtensionModule): kwargs['dependencies'], state, depends) t_args.extend(build_args) new_depends.extend(depends) - custom_target = build.CustomTarget( + custom_target = CustomTarget( targetname, state.subdir, state.subproject, @@ -1516,6 +1535,7 @@ class GnomeModule(ExtensionModule): [f'{modulename}-decl.txt'], build_always_stale=True, extra_depends=new_depends, + description='Generating gtkdoc {}', ) alias_target = build.AliasTarget(targetname, [custom_target], state.subdir, state.subproject, state.environment) if kwargs['check']: @@ -1525,7 +1545,7 @@ class GnomeModule(ExtensionModule): check_args = (targetname + '-check', check_cmd) check_workdir = os.path.join(state.environment.get_build_dir(), state.subdir) state.test(check_args, env=check_env, workdir=check_workdir, depends=[custom_target]) - res: T.List[T.Union[build.Target, build.ExecutableSerialisation]] = [custom_target, alias_target] + res: T.List[T.Union[build.Target, mesonlib.ExecutableSerialisation]] = [custom_target, alias_target] if kwargs['install']: res.append(state.backend.get_executable_serialisation(command + t_args, tag='doc')) return ModuleReturnValue(custom_target, res) @@ -1569,11 +1589,11 @@ class GnomeModule(ExtensionModule): def gtkdoc_html_dir(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> str: return os.path.join('share/gtk-doc/html', args[0]) - @typed_pos_args('gnome.gdbus_codegen', str, optargs=[(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList)]) + @typed_pos_args('gnome.gdbus_codegen', str, optargs=[(str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList)]) @typed_kwargs( 'gnome.gdbus_codegen', _BUILD_BY_DEFAULT.evolve(since='0.40.0'), - SOURCES_KW.evolve(since='0.46.0'), + DEPENDENCY_SOURCES_KW.evolve(since='0.46.0'), KwargInfo('extra_args', ContainerTypeInfo(list, str), since='0.47.0', default=[], listify=True), KwargInfo('interface_prefix', (str, NoneType)), KwargInfo('namespace', (str, NoneType)), @@ -1595,7 +1615,7 @@ class GnomeModule(ExtensionModule): kwargs: 'GdbusCodegen') -> ModuleReturnValue: namebase = args[0] xml_files: T.List[T.Union['FileOrString', build.GeneratedTypes]] = [args[1]] if args[1] else [] - cmd: T.List[T.Union['ExternalProgram', str]] = [state.find_program('gdbus-codegen')] + cmd: T.List[T.Union['ToolType', str]] = [self._find_tool(state, 'gdbus-codegen')] cmd.extend(kwargs['extra_args']) # Autocleanup supported? @@ -1650,7 +1670,7 @@ class GnomeModule(ExtensionModule): cmd += ['--generate-c-code', '@OUTDIR@/' + namebase, '@INPUT@'] c_cmd = cmd - cfile_custom_target = build.CustomTarget( + cfile_custom_target = CustomTarget( output, state.subdir, state.subproject, @@ -1659,6 +1679,7 @@ class GnomeModule(ExtensionModule): xml_files, [output], build_by_default=build_by_default, + description='Generating gdbus source {}', ) targets.append(cfile_custom_target) @@ -1670,7 +1691,7 @@ class GnomeModule(ExtensionModule): hfile_cmd = cmd depends = [cfile_custom_target] - hfile_custom_target = build.CustomTarget( + hfile_custom_target = CustomTarget( output, state.subdir, state.subproject, @@ -1683,6 +1704,7 @@ class GnomeModule(ExtensionModule): install=install_header, install_dir=[install_dir], install_tag=['devel'], + description='Generating gdbus header {}', ) targets.append(hfile_custom_target) @@ -1701,7 +1723,7 @@ class GnomeModule(ExtensionModule): docbook_cmd = cmd depends = [cfile_custom_target] - docbook_custom_target = build.CustomTarget( + docbook_custom_target = CustomTarget( output, state.subdir, state.subproject, @@ -1711,6 +1733,7 @@ class GnomeModule(ExtensionModule): outputs, build_by_default=build_by_default, extra_depends=depends, + description='Generating gdbus docbook {}', ) targets.append(docbook_custom_target) @@ -1721,6 +1744,13 @@ class GnomeModule(ExtensionModule): 'gnome.mkenums', *_MK_ENUMS_COMMON_KWS, DEPENDS_KW, + KwargInfo( + 'sources', + ContainerTypeInfo(list, (str, mesonlib.File, CustomTarget, CustomTargetIndex, + GeneratedList)), + listify=True, + required=True, + ), KwargInfo('c_template', (str, mesonlib.File, NoneType)), KwargInfo('h_template', (str, mesonlib.File, NoneType)), KwargInfo('comments', (str, NoneType)), @@ -1747,7 +1777,7 @@ class GnomeModule(ExtensionModule): 'identifier_prefix', 'symbol_prefix', 'vhead', 'vprod', 'vtail'] for arg in known_kwargs: - # mypy can't figure this out + # Mypy can't figure out that this TypedDict index is correct, without repeating T.Literal for the entire list if kwargs[arg]: # type: ignore cmd += ['--' + arg.replace('_', '-'), kwargs[arg]] # type: ignore @@ -1796,6 +1826,12 @@ class GnomeModule(ExtensionModule): @typed_kwargs( 'gnome.mkenums_simple', *_MK_ENUMS_COMMON_KWS, + KwargInfo( + 'sources', + ContainerTypeInfo(list, (str, mesonlib.File)), + listify=True, + required=True, + ), KwargInfo('header_prefix', str, default=''), KwargInfo('function_prefix', str, default=''), KwargInfo('body_prefix', str, default=''), @@ -1823,8 +1859,9 @@ class GnomeModule(ExtensionModule): if body_prefix != '': fhead += '%s\n' % body_prefix fhead += '#include "%s"\n' % hdr_filename - for hdr in kwargs['sources']: - fhead += '#include "{}"\n'.format(os.path.basename(str(hdr))) + for hdr in self.interpreter.source_strings_to_files(kwargs['sources']): + hdr_path = os.path.relpath(hdr.relative_name(), state.subdir) + fhead += f'#include "{hdr_path}"\n' fhead += textwrap.dedent( ''' #define C_ENUM(v) ((gint) v) @@ -1902,10 +1939,10 @@ class GnomeModule(ExtensionModule): return ModuleReturnValue([c_file, h_file], [c_file, h_file]) - @staticmethod def _make_mkenum_impl( + self, state: 'ModuleState', - sources: T.Sequence[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList]], + sources: T.Sequence[T.Union[str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList]], output: str, cmd: T.List[str], *, @@ -1913,12 +1950,12 @@ class GnomeModule(ExtensionModule): install_dir: T.Optional[T.Sequence[T.Union[str, bool]]] = None, depends: T.Optional[T.Sequence[T.Union[CustomTarget, CustomTargetIndex, BuildTarget]]] = None ) -> build.CustomTarget: - real_cmd: T.List[T.Union[str, ExternalProgram]] = [state.find_program(['glib-mkenums', 'mkenums'])] + real_cmd: T.List[T.Union[str, 'ToolType']] = [self._find_tool(state, 'glib-mkenums')] real_cmd.extend(cmd) _install_dir = install_dir or state.environment.coredata.get_option(mesonlib.OptionKey('includedir')) assert isinstance(_install_dir, str), 'for mypy' - return build.CustomTarget( + return CustomTarget( output, state.subdir, state.subproject, @@ -1933,6 +1970,7 @@ class GnomeModule(ExtensionModule): extra_depends=depends, # https://github.com/mesonbuild/meson/issues/973 absolute_paths=True, + description='Generating GObject enum file {}', ) @typed_pos_args('gnome.genmarshal', str) @@ -1957,7 +1995,7 @@ class GnomeModule(ExtensionModule): new_genmarshal = mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.3') - cmd: T.List[T.Union['ExternalProgram', str]] = [state.find_program('glib-genmarshal')] + cmd: T.List[T.Union['ToolType', str]] = [self._find_tool(state, 'glib-genmarshal')] if kwargs['prefix']: cmd.extend(['--prefix', kwargs['prefix']]) if kwargs['extra_args']: @@ -1966,9 +2004,10 @@ class GnomeModule(ExtensionModule): else: mlog.warning('The current version of GLib does not support extra arguments \n' 'for glib-genmarshal. You need at least GLib 2.53.3. See ', - mlog.bold('https://github.com/mesonbuild/meson/pull/2049')) + mlog.bold('https://github.com/mesonbuild/meson/pull/2049'), + once=True, fatal=False) for k in ['internal', 'nostdinc', 'skip_source', 'stdinc', 'valist_marshallers']: - # Mypy can't figure out that this is correct + # Mypy can't figure out that this TypedDict index is correct, without repeating T.Literal for the entire list if kwargs[k]: # type: ignore cmd.append(f'--{k.replace("_", "-")}') @@ -1985,7 +2024,7 @@ class GnomeModule(ExtensionModule): h_cmd = cmd + ['--header', '@INPUT@'] if new_genmarshal: h_cmd += ['--pragma-once'] - header = build.CustomTarget( + header = CustomTarget( output + '_h', state.subdir, state.subproject, @@ -1998,15 +2037,16 @@ class GnomeModule(ExtensionModule): install_tag=['devel'], capture=capture, depend_files=kwargs['depend_files'], + description='Generating glib marshaller header {}', ) c_cmd = cmd + ['--body', '@INPUT@'] - extra_deps: T.List[build.CustomTarget] = [] + extra_deps: T.List[CustomTarget] = [] if mesonlib.version_compare(self._get_native_glib_version(state), '>= 2.53.4'): # Silence any warnings about missing prototypes c_cmd += ['--include-header', header_file] extra_deps.append(header) - body = build.CustomTarget( + body = CustomTarget( output + '_c', state.subdir, state.subproject, @@ -2017,6 +2057,7 @@ class GnomeModule(ExtensionModule): capture=capture, depend_files=kwargs['depend_files'], extra_depends=extra_deps, + description='Generating glib marshaller source {}', ) rv = [body, header] @@ -2070,7 +2111,7 @@ class GnomeModule(ExtensionModule): ofile.write(package + '\n') return build.Data([mesonlib.File(True, outdir, fname)], install_dir, install_dir, mesonlib.FileMode(), state.subproject) - def _get_vapi_link_with(self, target: build.CustomTarget) -> T.List[build.LibTypes]: + def _get_vapi_link_with(self, target: CustomTarget) -> T.List[build.LibTypes]: link_with: T.List[build.LibTypes] = [] for dep in target.get_target_dependencies(): if isinstance(dep, build.SharedLibrary): @@ -2101,7 +2142,7 @@ class GnomeModule(ExtensionModule): build_dir = os.path.join(state.environment.get_build_dir(), state.subdir) source_dir = os.path.join(state.environment.get_source_dir(), state.subdir) pkg_cmd, vapi_depends, vapi_packages, vapi_includes, packages = self._extract_vapi_packages(state, kwargs['packages']) - cmd: T.List[T.Union[str, 'ExternalProgram']] + cmd: T.List[T.Union[ExternalProgram, Executable, OverrideProgram, str]] cmd = [state.find_program('vapigen'), '--quiet', f'--library={library}', f'--directory={build_dir}'] cmd.extend([f'--vapidir={d}' for d in kwargs['vapi_dirs']]) cmd.extend([f'--metadatadir={d}' for d in kwargs['metadata_dirs']]) @@ -2146,12 +2187,12 @@ class GnomeModule(ExtensionModule): ) # So to try our best to get this to just work we need: - # - link with with the correct library + # - link with the correct library # - include the vapi and dependent vapi files in sources # - add relevant directories to include dirs incs = [build.IncludeDirs(state.subdir, ['.'] + vapi_includes, False)] sources = [vapi_target] + vapi_depends - rv = InternalDependency(None, incs, [], [], link_with, [], sources, [], {}, [], []) + rv = InternalDependency(None, incs, [], [], link_with, [], sources, [], [], {}, [], [], []) created_values.append(rv) return ModuleReturnValue(rv, created_values) diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py index 3dac9be..f113549 100644 --- a/mesonbuild/modules/hotdoc.py +++ b/mesonbuild/modules/hotdoc.py @@ -11,28 +11,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations '''This module provides helper functions for generating documentation using hotdoc''' -import os -import subprocess +import os, subprocess +import typing as T -from mesonbuild import mesonlib -from mesonbuild import mlog, build -from mesonbuild.coredata import MesonException -from . import ModuleReturnValue, ModuleInfo -from . import ExtensionModule +from . import ExtensionModule, ModuleReturnValue, ModuleInfo +from .. import build, mesonlib, mlog +from ..build import CustomTarget, CustomTargetIndex from ..dependencies import Dependency, InternalDependency from ..interpreterbase import ( InvalidArguments, noPosargs, noKwargs, typed_kwargs, FeatureDeprecated, ContainerTypeInfo, KwargInfo, typed_pos_args ) -from ..interpreter import CustomTargetHolder +from ..interpreter.interpreterobjects import _CustomTargetHolder from ..interpreter.type_checking import NoneType +from ..mesonlib import File, MesonException from ..programs import ExternalProgram - -def ensure_list(value): +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from . import ModuleState + from ..environment import Environment + from ..interpreter import Interpreter + from ..interpreterbase import TYPE_kwargs, TYPE_var + + _T = T.TypeVar('_T') + + class GenerateDocKwargs(TypedDict): + sitemap: T.Union[str, File, CustomTarget, CustomTargetIndex] + index: T.Union[str, File, CustomTarget, CustomTargetIndex] + project_version: str + html_extra_theme: T.Optional[str] + include_paths: T.List[str] + dependencies: T.List[T.Union[Dependency, build.StaticLibrary, build.SharedLibrary, CustomTarget, CustomTargetIndex]] + depends: T.List[T.Union[CustomTarget, CustomTargetIndex]] + gi_c_source_roots: T.List[str] + extra_assets: T.List[str] + extra_extension_paths: T.List[str] + subprojects: T.List['HotdocTarget'] + install: bool + +def ensure_list(value: T.Union[_T, T.List[_T]]) -> T.List[_T]: if not isinstance(value, list): return [value] return value @@ -40,34 +63,39 @@ def ensure_list(value): MIN_HOTDOC_VERSION = '0.8.100' -file_types = (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex) +file_types = (str, File, CustomTarget, CustomTargetIndex) + + +class HotdocExternalProgram(ExternalProgram): + def run_hotdoc(self, cmd: T.List[str]) -> int: + return subprocess.run(self.get_command() + cmd, stdout=subprocess.DEVNULL).returncode class HotdocTargetBuilder: - def __init__(self, name, state, hotdoc, interpreter, kwargs): + def __init__(self, name: str, state: ModuleState, hotdoc: HotdocExternalProgram, interpreter: Interpreter, kwargs): self.hotdoc = hotdoc self.build_by_default = kwargs.pop('build_by_default', False) self.kwargs = kwargs self.name = name self.state = state self.interpreter = interpreter - self.include_paths = mesonlib.OrderedSet() + self.include_paths: mesonlib.OrderedSet[str] = mesonlib.OrderedSet() self.builddir = state.environment.get_build_dir() self.sourcedir = state.environment.get_source_dir() self.subdir = state.subdir self.build_command = state.environment.get_build_command() - self.cmd = ['conf', '--project-name', name, "--disable-incremental-build", - '--output', os.path.join(self.builddir, self.subdir, self.name + '-doc')] + self.cmd: T.List[TYPE_var] = ['conf', '--project-name', name, "--disable-incremental-build", + '--output', os.path.join(self.builddir, self.subdir, self.name + '-doc')] self._extra_extension_paths = set() self.extra_assets = set() self.extra_depends = [] self._subprojects = [] - def process_known_arg(self, option, argname=None, value_processor=None): + def process_known_arg(self, option: str, argname: T.Optional[str] = None, value_processor: T.Optional[T.Callable] = None) -> None: if not argname: argname = option.strip("-").replace("-", "_") @@ -77,7 +105,7 @@ class HotdocTargetBuilder: self.set_arg_value(option, value) - def set_arg_value(self, option, value): + def set_arg_value(self, option: str, value: TYPE_var) -> None: if value is None: return @@ -110,18 +138,18 @@ class HotdocTargetBuilder: else: self.cmd.extend([option, value]) - def check_extra_arg_type(self, arg, value): + def check_extra_arg_type(self, arg: str, value: TYPE_var) -> None: if isinstance(value, list): for v in value: self.check_extra_arg_type(arg, v) return - valid_types = (str, bool, mesonlib.File, build.IncludeDirs, build.CustomTarget, build.CustomTargetIndex, build.BuildTarget) + valid_types = (str, bool, File, build.IncludeDirs, CustomTarget, CustomTargetIndex, build.BuildTarget) if not isinstance(value, valid_types): raise InvalidArguments('Argument "{}={}" should be of type: {}.'.format( arg, value, [t.__name__ for t in valid_types])) - def process_extra_args(self): + def process_extra_args(self) -> None: for arg, value in self.kwargs.items(): option = "--" + arg.replace("_", "-") self.check_extra_arg_type(arg, value) @@ -152,7 +180,7 @@ class HotdocTargetBuilder: return None, None - def add_extension_paths(self, paths): + def add_extension_paths(self, paths: T.Union[T.List[str], T.Set[str]]) -> None: for path in paths: if path in self._extra_extension_paths: continue @@ -160,10 +188,10 @@ class HotdocTargetBuilder: self._extra_extension_paths.add(path) self.cmd.extend(["--extra-extension-path", path]) - def replace_dirs_in_string(self, string): + def replace_dirs_in_string(self, string: str) -> str: return string.replace("@SOURCE_ROOT@", self.sourcedir).replace("@BUILD_ROOT@", self.builddir) - def process_gi_c_source_roots(self): + def process_gi_c_source_roots(self) -> None: if self.hotdoc.run_hotdoc(['--has-extension=gi-extension']) != 0: return @@ -175,7 +203,7 @@ class HotdocTargetBuilder: self.cmd += ['--gi-c-source-roots'] + value - def process_dependencies(self, deps): + def process_dependencies(self, deps: T.List[T.Union[Dependency, build.StaticLibrary, build.SharedLibrary, CustomTarget, CustomTargetIndex]]) -> T.List[str]: cflags = set() for dep in mesonlib.listify(ensure_list(deps)): if isinstance(dep, InternalDependency): @@ -199,29 +227,29 @@ class HotdocTargetBuilder: self.include_paths.add(os.path.join(self.builddir, dep.hotdoc_conf.subdir)) self.cmd += ['--extra-assets=' + p for p in dep.extra_assets] self.add_extension_paths(dep.extra_extension_paths) - elif isinstance(dep, (build.CustomTarget, build.BuildTarget)): + elif isinstance(dep, (CustomTarget, build.BuildTarget)): self.extra_depends.append(dep) - elif isinstance(dep, build.CustomTargetIndex): + elif isinstance(dep, CustomTargetIndex): self.extra_depends.append(dep.target) return [f.strip('-I') for f in cflags] - def process_extra_assets(self): + def process_extra_assets(self) -> None: self._extra_assets = self.kwargs.pop('extra_assets') for assets_path in self._extra_assets: self.cmd.extend(["--extra-assets", assets_path]) - def process_subprojects(self): + def process_subprojects(self) -> None: value = self.kwargs.pop('subprojects') self.process_dependencies(value) self._subprojects.extend(value) - def flatten_config_command(self): + def flatten_config_command(self) -> T.List[str]: cmd = [] for arg in mesonlib.listify(self.cmd, flatten=True): - if isinstance(arg, mesonlib.File): + if isinstance(arg, File): arg = arg.absolute_path(self.state.environment.get_source_dir(), self.state.environment.get_build_dir()) elif isinstance(arg, build.IncludeDirs): @@ -230,10 +258,10 @@ class HotdocTargetBuilder: cmd.append(os.path.join(self.builddir, arg.get_curdir(), inc_dir)) continue - elif isinstance(arg, (build.BuildTarget, build.CustomTarget)): + elif isinstance(arg, (build.BuildTarget, CustomTarget)): self.extra_depends.append(arg) arg = self.interpreter.backend.get_target_filename_abs(arg) - elif isinstance(arg, build.CustomTargetIndex): + elif isinstance(arg, CustomTargetIndex): self.extra_depends.append(arg.target) arg = self.interpreter.backend.get_target_filename_abs(arg) @@ -241,15 +269,16 @@ class HotdocTargetBuilder: return cmd - def generate_hotdoc_config(self): + def generate_hotdoc_config(self) -> None: cwd = os.path.abspath(os.curdir) ncwd = os.path.join(self.sourcedir, self.subdir) mlog.log('Generating Hotdoc configuration for: ', mlog.bold(self.name)) os.chdir(ncwd) - self.hotdoc.run_hotdoc(self.flatten_config_command()) + if self.hotdoc.run_hotdoc(self.flatten_config_command()) != 0: + raise MesonException('hotdoc failed to configure') os.chdir(cwd) - def ensure_file(self, value): + def ensure_file(self, value: T.Union[str, File, CustomTarget, CustomTargetIndex]) -> T.Union[File, CustomTarget, CustomTargetIndex]: if isinstance(value, list): res = [] for val in value: @@ -257,11 +286,11 @@ class HotdocTargetBuilder: return res if isinstance(value, str): - return mesonlib.File.from_source_file(self.sourcedir, self.subdir, value) + return File.from_source_file(self.sourcedir, self.subdir, value) return value - def ensure_dir(self, value): + def ensure_dir(self, value: str) -> str: if os.path.isabs(value): _dir = value else: @@ -272,12 +301,12 @@ class HotdocTargetBuilder: return os.path.relpath(_dir, os.path.join(self.builddir, self.subdir)) - def check_forbidden_args(self): + def check_forbidden_args(self) -> None: for arg in ['conf_file']: if arg in self.kwargs: raise InvalidArguments(f'Argument "{arg}" is forbidden.') - def make_targets(self): + def make_targets(self) -> T.Tuple[HotdocTarget, mesonlib.ExecutableSerialisation]: self.check_forbidden_args() self.process_known_arg("--index", value_processor=self.ensure_file) self.process_known_arg("--project-version") @@ -323,7 +352,7 @@ class HotdocTargetBuilder: subdir=self.subdir, subproject=self.state.subproject, environment=self.state.environment, - hotdoc_conf=mesonlib.File.from_built_file( + hotdoc_conf=File.from_built_file( self.subdir, hotdoc_config_name), extra_extension_paths=self._extra_extension_paths, extra_assets=self._extra_assets, @@ -337,9 +366,22 @@ class HotdocTargetBuilder: install_script = None if install: + datadir = os.path.join(self.state.get_option('prefix'), self.state.get_option('datadir')) + devhelp = self.kwargs.get('devhelp_activate', False) + if not isinstance(devhelp, bool): + FeatureDeprecated.single_use('hotdoc.generate_doc() devhelp_activate must be boolean', '1.1.0', self.state.subproject) + devhelp = False + if devhelp: + install_from = os.path.join(fullname, 'devhelp') + install_to = os.path.join(datadir, 'devhelp') + else: + install_from = os.path.join(fullname, 'html') + install_to = os.path.join(datadir, 'doc', self.name, 'html') + install_script = self.state.backend.get_executable_serialisation(self.build_command + [ "--internal", "hotdoc", - "--install", os.path.join(fullname, 'html'), + "--install", install_from, + "--docdir", install_to, '--name', self.name, '--builddir', os.path.join(self.builddir, self.subdir)] + self.hotdoc.get_command() + @@ -349,29 +391,30 @@ class HotdocTargetBuilder: return (target, install_script) -class HotdocTargetHolder(CustomTargetHolder): - def __init__(self, target, interp): +class HotdocTargetHolder(_CustomTargetHolder['HotdocTarget']): + def __init__(self, target: HotdocTarget, interp: Interpreter): super().__init__(target, interp) self.methods.update({'config_path': self.config_path_method}) @noPosargs @noKwargs - def config_path_method(self, *args, **kwargs): + def config_path_method(self, *args: T.Any, **kwargs: T.Any) -> str: conf = self.held_object.hotdoc_conf.absolute_path(self.interpreter.environment.source_dir, self.interpreter.environment.build_dir) return conf -class HotdocTarget(build.CustomTarget): - def __init__(self, name, subdir, subproject, hotdoc_conf, extra_extension_paths, extra_assets, - subprojects, environment, **kwargs): +class HotdocTarget(CustomTarget): + def __init__(self, name: str, subdir: str, subproject: str, hotdoc_conf: File, + extra_extension_paths: T.Set[str], extra_assets: T.List[str], + subprojects: T.List['HotdocTarget'], environment: Environment, **kwargs: T.Any): super().__init__(name, subdir, subproject, environment, **kwargs, absolute_paths=True) self.hotdoc_conf = hotdoc_conf self.extra_extension_paths = extra_extension_paths self.extra_assets = extra_assets self.subprojects = subprojects - def __getstate__(self): + def __getstate__(self) -> dict: # Make sure we do not try to pickle subprojects res = self.__dict__.copy() res['subprojects'] = [] @@ -383,19 +426,15 @@ class HotDocModule(ExtensionModule): INFO = ModuleInfo('hotdoc', '0.48.0') - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter): super().__init__(interpreter) - self.hotdoc = ExternalProgram('hotdoc') + self.hotdoc = HotdocExternalProgram('hotdoc') if not self.hotdoc.found(): raise MesonException('hotdoc executable not found') version = self.hotdoc.get_version(interpreter) if not mesonlib.version_compare(version, f'>={MIN_HOTDOC_VERSION}'): raise MesonException(f'hotdoc {MIN_HOTDOC_VERSION} required but not found.)') - def run_hotdoc(cmd): - return subprocess.run(self.hotdoc.get_command() + cmd, stdout=subprocess.DEVNULL).returncode - - self.hotdoc.run_hotdoc = run_hotdoc self.methods.update({ 'has_extensions': self.has_extensions, 'generate_doc': self.generate_doc, @@ -403,7 +442,7 @@ class HotDocModule(ExtensionModule): @noKwargs @typed_pos_args('hotdoc.has_extensions', varargs=str, min_varargs=1) - def has_extensions(self, state, args, kwargs): + def has_extensions(self, state: ModuleState, args: T.Tuple[T.List[str]], kwargs: TYPE_kwargs) -> bool: return self.hotdoc.run_hotdoc([f'--has-extension={extension}' for extension in args[0]]) == 0 @typed_pos_args('hotdoc.generate_doc', str) @@ -418,13 +457,13 @@ class HotDocModule(ExtensionModule): KwargInfo( 'dependencies', ContainerTypeInfo(list, (Dependency, build.StaticLibrary, build.SharedLibrary, - build.CustomTarget, build.CustomTargetIndex)), + CustomTarget, CustomTargetIndex)), listify=True, default=[], ), KwargInfo( 'depends', - ContainerTypeInfo(list, (build.CustomTarget, build.CustomTargetIndex)), + ContainerTypeInfo(list, (CustomTarget, CustomTargetIndex)), listify=True, default=[], since='0.64.1', @@ -436,21 +475,21 @@ class HotDocModule(ExtensionModule): KwargInfo('install', bool, default=False), allow_unknown=True ) - def generate_doc(self, state, args, kwargs): + def generate_doc(self, state: ModuleState, args: T.Tuple[str], kwargs: GenerateDocKwargs) -> ModuleReturnValue: project_name = args[0] - if any(isinstance(x, (build.CustomTarget, build.CustomTargetIndex)) for x in kwargs['dependencies']): + if any(isinstance(x, (CustomTarget, CustomTargetIndex)) for x in kwargs['dependencies']): FeatureDeprecated.single_use('hotdoc.generate_doc dependencies argument with custom_target', '0.64.1', state.subproject, 'use `depends`', state.current_node) builder = HotdocTargetBuilder(project_name, state, self.hotdoc, self.interpreter, kwargs) target, install_script = builder.make_targets() - targets = [target] + targets: T.List[T.Union[HotdocTarget, mesonlib.ExecutableSerialisation]] = [target] if install_script: targets.append(install_script) - return ModuleReturnValue(targets[0], targets) + return ModuleReturnValue(target, targets) -def initialize(interpreter): +def initialize(interpreter: Interpreter) -> HotDocModule: mod = HotDocModule(interpreter) mod.interpreter.append_holder_map(HotdocTarget, HotdocTargetHolder) return mod diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py index fcb0aa7..e375674 100644 --- a/mesonbuild/modules/i18n.py +++ b/mesonbuild/modules/i18n.py @@ -14,6 +14,7 @@ from __future__ import annotations from os import path +import shlex import typing as T from . import ExtensionModule, ModuleReturnValue, ModuleInfo @@ -23,6 +24,7 @@ from .. import mlog from ..interpreter.type_checking import CT_BUILD_BY_DEFAULT, CT_INPUT_KW, INSTALL_TAG_KW, OUTPUT_KW, INSTALL_DIR_KW, INSTALL_KW, NoneType, in_set_validator from ..interpreterbase import FeatureNew from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, noPosargs, typed_kwargs, typed_pos_args +from ..programs import ExternalProgram from ..scripts.gettext import read_linguas if T.TYPE_CHECKING: @@ -32,7 +34,6 @@ if T.TYPE_CHECKING: from ..build import Target from ..interpreter import Interpreter from ..interpreterbase import TYPE_var - from ..programs import ExternalProgram class MergeFile(TypedDict): @@ -134,7 +135,7 @@ class I18nModule(ExtensionModule): 'gettext': self.gettext, 'itstool_join': self.itstool_join, }) - self.tools: T.Dict[str, T.Optional[ExternalProgram]] = { + self.tools: T.Dict[str, T.Optional[T.Union[ExternalProgram, build.Executable]]] = { 'itstool': None, 'msgfmt': None, 'msginit': None, @@ -166,6 +167,15 @@ class I18nModule(ExtensionModule): def merge_file(self, state: 'ModuleState', args: T.List['TYPE_var'], kwargs: 'MergeFile') -> ModuleReturnValue: if self.tools['msgfmt'] is None or not self.tools['msgfmt'].found(): self.tools['msgfmt'] = state.find_program('msgfmt', for_machine=mesonlib.MachineChoice.BUILD) + if isinstance(self.tools['msgfmt'], ExternalProgram): + try: + have_version = self.tools['msgfmt'].get_version() + except mesonlib.MesonException as e: + raise mesonlib.MesonException('i18n.merge_file requires GNU msgfmt') from e + want_version = '>=0.19' if kwargs['type'] == 'desktop' else '>=0.19.7' + if not mesonlib.version_compare(have_version, want_version): + msg = f'i18n.merge_file requires GNU msgfmt {want_version} to produce files of type: ' + kwargs['type'] + f' (got: {have_version})' + raise mesonlib.MesonException(msg) podir = path.join(state.build_to_src, state.subdir, kwargs['po_dir']) ddirs = self._get_data_dirs(state, kwargs['data_dirs']) @@ -203,11 +213,12 @@ class I18nModule(ExtensionModule): install=kwargs['install'], install_dir=[kwargs['install_dir']] if kwargs['install_dir'] is not None else None, install_tag=install_tag, + description='Merging translations for {}', ) return ModuleReturnValue(ct, [ct]) - @typed_pos_args('i81n.gettext', str) + @typed_pos_args('i18n.gettext', str) @typed_kwargs( 'i18n.gettext', _ARGS, @@ -285,7 +296,7 @@ class I18nModule(ExtensionModule): path.join(state.subdir, l, 'LC_MESSAGES'), state.subproject, state.environment, - [self.tools['msgfmt'], '@INPUT@', '-o', '@OUTPUT@'], + [self.tools['msgfmt'], '-o', '@OUTPUT@', '@INPUT@'], [po_file], [f'{packagename}.mo'], install=install, @@ -295,6 +306,7 @@ class I18nModule(ExtensionModule): # Bonus: the build tree has something usable as an uninstalled bindtextdomain() target dir. install_dir=[path.join(install_dir, l, 'LC_MESSAGES')], install_tag=['i18n'], + description='Building translation {}', ) targets.append(gmotarget) gmotargets.append(gmotarget) @@ -349,11 +361,14 @@ class I18nModule(ExtensionModule): command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, 'ExternalProgram', mesonlib.File]] = [] command.extend(state.environment.get_build_command()) + + itstool_cmd = self.tools['itstool'].get_command() + # TODO: python 3.8 can use shlex.join() command.extend([ '--internal', 'itstool', 'join', '-i', '@INPUT@', '-o', '@OUTPUT@', - '--itstool=' + self.tools['itstool'].get_path(), + '--itstool=' + ' '.join(shlex.quote(c) for c in itstool_cmd), ]) if its_files: for fname in its_files: @@ -381,6 +396,7 @@ class I18nModule(ExtensionModule): install=kwargs['install'], install_dir=[kwargs['install_dir']] if kwargs['install_dir'] is not None else None, install_tag=install_tag, + description='Merging translations for {}', ) return ModuleReturnValue(ct, [ct]) diff --git a/mesonbuild/modules/icestorm.py b/mesonbuild/modules/icestorm.py index c579148..8c1c6f1 100644 --- a/mesonbuild/modules/icestorm.py +++ b/mesonbuild/modules/icestorm.py @@ -40,7 +40,7 @@ class IceStormModule(ExtensionModule): def __init__(self, interpreter: Interpreter) -> None: super().__init__(interpreter) - self.tools: T.Dict[str, ExternalProgram] = {} + self.tools: T.Dict[str, T.Union[ExternalProgram, build.Executable]] = {} self.methods.update({ 'project': self.project, }) diff --git a/mesonbuild/modules/java.py b/mesonbuild/modules/java.py index 792d70b..f6e4484 100644 --- a/mesonbuild/modules/java.py +++ b/mesonbuild/modules/java.py @@ -42,7 +42,7 @@ class JavaModule(NewExtensionModule): def __get_java_compiler(self, state: ModuleState) -> Compiler: if 'java' not in state.environment.coredata.compilers[MachineChoice.BUILD]: - detect_compiler_for(state.environment, 'java', MachineChoice.BUILD) + detect_compiler_for(state.environment, 'java', MachineChoice.BUILD, False) return state.environment.coredata.compilers[MachineChoice.BUILD]['java'] @FeatureNew('java.generate_native_headers', '0.62.0') @@ -75,13 +75,16 @@ class JavaModule(NewExtensionModule): classes = T.cast('T.List[str]', kwargs.get('classes')) package = kwargs.get('package') + if package: + sanitized_package = package.replace("-", "_").replace(".", "_") + headers: T.List[str] = [] for clazz in classes: - underscore_clazz = clazz.replace(".", "_") + sanitized_clazz = clazz.replace(".", "_") if package: - headers.append(f'{package.replace(".", "_")}_{underscore_clazz}.h') + headers.append(f'{sanitized_package}_{sanitized_clazz}.h') else: - headers.append(f'{underscore_clazz}.h') + headers.append(f'{sanitized_clazz}.h') javac = self.__get_java_compiler(state) diff --git a/mesonbuild/modules/keyval.py b/mesonbuild/modules/keyval.py index e900a2b..48afe81 100644 --- a/mesonbuild/modules/keyval.py +++ b/mesonbuild/modules/keyval.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import typing as T @@ -54,7 +55,7 @@ class KeyvalModule(ExtensionModule): return result @noKwargs - @typed_pos_args('keyval.laod', (str, mesonlib.File)) + @typed_pos_args('keyval.load', (str, mesonlib.File)) def load(self, state: 'ModuleState', args: T.Tuple['mesonlib.FileOrString'], kwargs: T.Dict[str, T.Any]) -> T.Dict[str, str]: s = args[0] is_built = False diff --git a/mesonbuild/modules/modtest.py b/mesonbuild/modules/modtest.py index 15f8237..5c25840 100644 --- a/mesonbuild/modules/modtest.py +++ b/mesonbuild/modules/modtest.py @@ -15,7 +15,7 @@ from __future__ import annotations import typing as T -from . import ExtensionModule, ModuleInfo +from . import NewExtensionModule, ModuleInfo from ..interpreterbase import noKwargs, noPosargs if T.TYPE_CHECKING: @@ -24,12 +24,12 @@ if T.TYPE_CHECKING: from ..interpreterbase.baseobjects import TYPE_kwargs, TYPE_var -class TestModule(ExtensionModule): +class TestModule(NewExtensionModule): INFO = ModuleInfo('modtest') def __init__(self, interpreter: Interpreter) -> None: - super().__init__(interpreter) + super().__init__() self.methods.update({ 'print_hello': self.print_hello, }) diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index d475618..ee12293 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -26,7 +26,7 @@ from .. import dependencies from .. import mesonlib from .. import mlog from ..coredata import BUILTIN_DIR_OPTIONS -from ..dependencies import ThreadDependency +from ..dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface from ..interpreter.type_checking import D_MODULE_VERSIONS_KW, INSTALL_DIR_KW, VARIABLES_KW, NoneType from ..interpreterbase import FeatureNew, FeatureDeprecated from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args @@ -99,6 +99,7 @@ class DependenciesHelper: def __init__(self, state: ModuleState, name: str, metadata: T.Dict[str, MetaData]) -> None: self.state = state self.name = name + self.metadata = metadata self.pub_libs: T.List[LIBS] = [] self.pub_reqs: T.List[str] = [] self.priv_libs: T.List[LIBS] = [] @@ -106,7 +107,7 @@ class DependenciesHelper: self.cflags: T.List[str] = [] self.version_reqs: T.DefaultDict[str, T.Set[str]] = defaultdict(set) self.link_whole_targets: T.List[T.Union[build.CustomTarget, build.CustomTargetIndex, build.StaticLibrary]] = [] - self.metadata = metadata + self.uninstalled_incdirs: mesonlib.OrderedSet[str] = mesonlib.OrderedSet() def add_pub_libs(self, libs: T.List[ANY_DEP]) -> None: p_libs, reqs, cflags = self._process_libs(libs, True) @@ -152,7 +153,7 @@ class DependenciesHelper: and obj.get_id() in self.metadata): self._check_generated_pc_deprecation(obj) processed_reqs.append(self.metadata[obj.get_id()].filebase) - elif isinstance(obj, dependencies.PkgConfigDependency): + elif isinstance(obj, PkgConfigDependency): if obj.found(): processed_reqs.append(obj.name) self.add_version_reqs(obj.name, obj.version_reqs) @@ -162,7 +163,7 @@ class DependenciesHelper: self.add_version_reqs(name, [version_req] if version_req is not None else None) elif isinstance(obj, dependencies.Dependency) and not obj.found(): pass - elif isinstance(obj, ThreadDependency): + elif isinstance(obj, dependencies.ExternalDependency) and obj.name == 'threads': pass else: raise mesonlib.MesonException('requires argument not a string, ' @@ -173,6 +174,15 @@ class DependenciesHelper: def add_cflags(self, cflags: T.List[str]) -> None: self.cflags += mesonlib.stringlistify(cflags) + def _add_uninstalled_incdirs(self, incdirs: T.List[build.IncludeDirs], subdir: T.Optional[str] = None) -> None: + for i in incdirs: + curdir = i.get_curdir() + for d in i.get_incdirs(): + path = os.path.join(curdir, d) + self.uninstalled_incdirs.add(path) + if subdir is not None: + self.uninstalled_incdirs.add(subdir) + def _process_libs( self, libs: T.List[ANY_DEP], public: bool ) -> T.Tuple[T.List[T.Union[str, build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex]], T.List[str], T.List[str]]: @@ -185,17 +195,20 @@ class DependenciesHelper: and obj.get_id() in self.metadata): self._check_generated_pc_deprecation(obj) processed_reqs.append(self.metadata[obj.get_id()].filebase) - elif isinstance(obj, dependencies.ValgrindDependency): + elif isinstance(obj, dependencies.ExternalDependency) and obj.name == 'valgrind': pass - elif isinstance(obj, dependencies.PkgConfigDependency): + elif isinstance(obj, PkgConfigDependency): if obj.found(): processed_reqs.append(obj.name) self.add_version_reqs(obj.name, obj.version_reqs) elif isinstance(obj, dependencies.InternalDependency): if obj.found(): + if obj.objects: + raise mesonlib.MesonException('.pc file cannot refer to individual object files.') processed_libs += obj.get_link_args() processed_cflags += obj.get_compile_args() self._add_lib_dependencies(obj.libraries, obj.whole_libraries, obj.ext_deps, public, private_external_deps=True) + self._add_uninstalled_incdirs(obj.get_include_dirs()) elif isinstance(obj, dependencies.Dependency): if obj.found(): processed_libs += obj.get_link_args() @@ -208,8 +221,10 @@ class DependenciesHelper: # than needed build deps. # See https://bugs.freedesktop.org/show_bug.cgi?id=105572 processed_libs.append(obj) + self._add_uninstalled_incdirs(obj.get_include_dirs(), obj.get_subdir()) elif isinstance(obj, (build.SharedLibrary, build.StaticLibrary)): processed_libs.append(obj) + self._add_uninstalled_incdirs(obj.get_include_dirs(), obj.get_subdir()) # If there is a static library in `Libs:` all its deps must be # public too, otherwise the generated pc file will never be # usable without --static. @@ -366,6 +381,7 @@ class PkgConfigModule(NewExtensionModule): # Track already generated pkg-config files This is stored as a class # variable so that multiple `import()`s share metadata + devenv: T.Optional[mesonlib.EnvironmentVariables] = None _metadata: T.ClassVar[T.Dict[str, MetaData]] = {} def __init__(self) -> None: @@ -374,6 +390,10 @@ class PkgConfigModule(NewExtensionModule): 'generate': self.generate, }) + def postconf_hook(self, b: build.Build) -> None: + if self.devenv is not None: + b.devenv.append(self.devenv) + def _get_lname(self, l: T.Union[build.SharedLibrary, build.StaticLibrary, build.CustomTarget, build.CustomTargetIndex], msg: str, pcfile: str) -> str: if isinstance(l, (build.CustomTargetIndex, build.CustomTarget)): @@ -422,8 +442,8 @@ class PkgConfigModule(NewExtensionModule): return ('${prefix}' / libdir).as_posix() def _generate_pkgconfig_file(self, state: ModuleState, deps: DependenciesHelper, - subdirs: T.List[str], name: T.Optional[str], - description: T.Optional[str], url: str, version: str, + subdirs: T.List[str], name: str, + description: str, url: str, version: str, pcfile: str, conflicts: T.List[str], variables: T.List[T.Tuple[str, str]], unescaped_variables: T.List[T.Tuple[str, str]], @@ -554,27 +574,6 @@ class PkgConfigModule(NewExtensionModule): if isinstance(l, (build.CustomTarget, build.CustomTargetIndex)) or 'cs' not in l.compilers: yield f'-l{lname}' - def get_uninstalled_include_dirs(libs: T.List[LIBS]) -> T.List[str]: - result: T.List[str] = [] - for l in libs: - if isinstance(l, (str, build.CustomTarget, build.CustomTargetIndex)): - continue - if l.get_subdir() not in result: - result.append(l.get_subdir()) - for i in l.get_include_dirs(): - curdir = i.get_curdir() - for d in i.get_incdirs(): - path = os.path.join(curdir, d) - if path not in result: - result.append(path) - return result - - def generate_uninstalled_cflags(libs: T.List[LIBS]) -> T.Iterable[str]: - for d in get_uninstalled_include_dirs(libs): - for basedir in ['${prefix}', '${srcdir}']: - path = PurePath(basedir, d) - yield '-I%s' % self._escape(path.as_posix()) - if len(deps.pub_libs) > 0: ofile.write('Libs: {}\n'.format(' '.join(generate_libs_flags(deps.pub_libs)))) if len(deps.priv_libs) > 0: @@ -582,7 +581,10 @@ class PkgConfigModule(NewExtensionModule): cflags: T.List[str] = [] if uninstalled: - cflags += generate_uninstalled_cflags(deps.pub_libs + deps.priv_libs) + for d in deps.uninstalled_incdirs: + for basedir in ['${prefix}', '${srcdir}']: + path = self._escape(PurePath(basedir, d).as_posix()) + cflags.append(f'-I{path}') else: for d in subdirs: if d == '.': @@ -636,16 +638,19 @@ class PkgConfigModule(NewExtensionModule): else: if kwargs['version'] is None: FeatureNew.single_use('pkgconfig.generate implicit version keyword', '0.46.0', state.subproject) + msg = ('pkgconfig.generate: if a library is not passed as a ' + 'positional argument, the {!r} keyword argument is ' + 'required.') if kwargs['name'] is None: - raise build.InvalidArguments( - 'pkgconfig.generate: if a library is not passed as a ' - 'positional argument, the name keyword argument is ' - 'required.') + raise build.InvalidArguments(msg.format('name')) + if kwargs['description'] is None: + raise build.InvalidArguments(msg.format('description')) dataonly = kwargs['dataonly'] if dataonly: default_subdirs = [] blocked_vars = ['libraries', 'libraries_private', 'requires_private', 'extra_cflags', 'subdirs'] + # Mypy can't figure out that this TypedDict index is correct, without repeating T.Literal for the entire list if any(kwargs[k] for k in blocked_vars): # type: ignore raise mesonlib.MesonException(f'Cannot combine dataonly with any of {blocked_vars}') default_install_dir = os.path.join(state.environment.get_datadir(), 'pkgconfig') @@ -677,7 +682,8 @@ class PkgConfigModule(NewExtensionModule): if dversions: compiler = state.environment.coredata.compilers.host.get('d') if compiler: - deps.add_cflags(compiler.get_feature_args({'versions': dversions}, None)) + deps.add_cflags(compiler.get_feature_args( + {'versions': dversions, 'import_dirs': [], 'debug': [], 'unittest': False}, None)) deps.remove_dups() @@ -699,6 +705,9 @@ class PkgConfigModule(NewExtensionModule): if mesonlib.is_freebsd(): pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'libdata', 'pkgconfig') pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') + elif mesonlib.is_haiku(): + pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') + pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') else: pkgroot = os.path.join(_as_str(state.environment.coredata.get_option(mesonlib.OptionKey('libdir'))), 'pkgconfig') pkgroot_name = os.path.join('{libdir}', 'pkgconfig') @@ -732,6 +741,8 @@ class PkgConfigModule(NewExtensionModule): if not isinstance(lib, str) and lib.get_id() not in self._metadata: self._metadata[lib.get_id()] = MetaData( filebase, name, state.current_node) + if self.devenv is None: + self.devenv = PkgConfigInterface.get_env(state.environment, mesonlib.MachineChoice.HOST, uninstalled=True) return ModuleReturnValue(res, [res]) diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index f74d10e..ec95374 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -13,58 +13,38 @@ # limitations under the License. from __future__ import annotations -from pathlib import Path -import copy -import functools -import json -import os -import shutil +import copy, json, os, shutil, re import typing as T from . import ExtensionModule, ModuleInfo from .. import mesonlib from .. import mlog from ..coredata import UserFeatureOption -from ..build import known_shmod_kwargs -from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency, ExtraFrameworkDependency -from ..dependencies.base import process_method_kw -from ..dependencies.detect import get_dep_identifier -from ..environment import detect_cpu_family -from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs -from ..interpreter import primitives as P_OBJ -from ..interpreter.type_checking import NoneType, PRESERVE_PATH_KW +from ..build import known_shmod_kwargs, CustomTarget, CustomTargetIndex, BuildTarget, GeneratedList, StructuredSources, ExtractedObjects, SharedModule +from ..dependencies import NotFoundDependency +from ..dependencies.detect import get_dep_identifier, find_external_dependency +from ..dependencies.python import BasicPythonExternalProgram, python_factory, _PythonDependencyBase +from ..interpreter import extract_required_kwarg, permitted_dependency_kwargs, primitives as P_OBJ +from ..interpreter.interpreterobjects import _ExternalProgramHolder +from ..interpreter.type_checking import NoneType, PRESERVE_PATH_KW, SHARED_MOD_KWS from ..interpreterbase import ( noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo, InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, FeatureNew, FeatureNewKwargs, disablerIfNotFound ) -from ..mesonlib import MachineChoice +from ..mesonlib import MachineChoice, OptionKey from ..programs import ExternalProgram, NonExistingExternalProgram if T.TYPE_CHECKING: - from typing_extensions import TypedDict + from typing_extensions import TypedDict, NotRequired from . import ModuleState - from ..build import SharedModule, Data - from ..dependencies import ExternalDependency, Dependency - from ..dependencies.factory import DependencyGenerator - from ..environment import Environment + from ..build import Build, Data + from ..dependencies import Dependency from ..interpreter import Interpreter - from ..interpreter.kwargs import ExtractRequired - from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs - - class PythonIntrospectionDict(TypedDict): - - install_paths: T.Dict[str, str] - is_pypy: bool - is_venv: bool - link_libpython: bool - sysconfig_paths: T.Dict[str, str] - paths: T.Dict[str, str] - platform: str - suffix: str - variables: T.Dict[str, str] - version: str + from ..interpreter.interpreter import BuildTargetSource + from ..interpreter.kwargs import ExtractRequired, SharedModule as SharedModuleKw + from ..interpreterbase.baseobjects import TYPE_var, TYPE_kwargs class PyInstallKw(TypedDict): @@ -78,401 +58,40 @@ if T.TYPE_CHECKING: modules: T.List[str] pure: T.Optional[bool] - _Base = ExternalDependency -else: - _Base = object + class ExtensionModuleKw(SharedModuleKw): + subdir: NotRequired[T.Optional[str]] -mod_kwargs = {'subdir'} -mod_kwargs.update(known_shmod_kwargs) -mod_kwargs -= {'name_prefix', 'name_suffix'} - - -class _PythonDependencyBase(_Base): - - def __init__(self, python_holder: 'PythonInstallation', embed: bool): - self.embed = embed - self.version: str = python_holder.version - self.platform = python_holder.platform - self.variables = python_holder.variables - self.paths = python_holder.paths - self.link_libpython = python_holder.link_libpython - self.info: T.Optional[T.Dict[str, str]] = None - if mesonlib.version_compare(self.version, '>= 3.0'): - self.major_version = 3 - else: - self.major_version = 2 - - -class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): - - def __init__(self, name: str, environment: 'Environment', - kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation', - libpc: bool = False): - if libpc: - mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC') - else: - mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths') - - PkgConfigDependency.__init__(self, name, environment, kwargs) - _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - - if libpc and not self.is_found: - mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation') - - # The "-embed" version of python.pc was introduced in 3.8, and distutils - # extension linking was changed to be considered a non embed usage. Before - # then, this dependency always uses the embed=True file because that is the - # only one that exists, - # - # On macOS and some Linux distros (Debian) distutils doesn't link extensions - # against libpython, even on 3.7 and below. We call into distutils and - # mirror its behavior. See https://github.com/mesonbuild/meson/issues/4117 - if not self.embed and not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'): - self.link_args = [] + MaybePythonProg = T.Union[NonExistingExternalProgram, 'PythonExternalProgram'] -class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase): - - def __init__(self, name: str, environment: 'Environment', - kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): - ExtraFrameworkDependency.__init__(self, name, environment, kwargs) - _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) +mod_kwargs = {'subdir', 'limited_api'} +mod_kwargs.update(known_shmod_kwargs) +mod_kwargs -= {'name_prefix', 'name_suffix'} +_MOD_KWARGS = [k for k in SHARED_MOD_KWS if k.name not in {'name_prefix', 'name_suffix'}] -class PythonSystemDependency(SystemDependency, _PythonDependencyBase): - def __init__(self, name: str, environment: 'Environment', - kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): - SystemDependency.__init__(self, name, environment, kwargs) - _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) +class PythonExternalProgram(BasicPythonExternalProgram): - if mesonlib.is_windows(): - self._find_libpy_windows(environment) - else: - self._find_libpy(installation, environment) - - def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: - if python_holder.is_pypy: - if self.major_version == 3: - libname = 'pypy3-c' - else: - libname = 'pypy-c' - libdir = os.path.join(self.variables.get('base'), 'bin') - libdirs = [libdir] - else: - libname = f'python{self.version}' - if 'DEBUG_EXT' in self.variables: - libname += self.variables['DEBUG_EXT'] - if 'ABIFLAGS' in self.variables: - libname += self.variables['ABIFLAGS'] - libdirs = [] - - largs = self.clib_compiler.find_library(libname, environment, libdirs) - if largs is not None: - self.link_args = largs - - self.is_found = largs is not None or not self.link_libpython - - inc_paths = mesonlib.OrderedSet([ - self.variables.get('INCLUDEPY'), - self.paths.get('include'), - self.paths.get('platinclude')]) - - self.compile_args += ['-I' + path for path in inc_paths if path] - - def _get_windows_python_arch(self) -> T.Optional[str]: - if self.platform == 'mingw': - pycc = self.variables.get('CC') - if pycc.startswith('x86_64'): - return '64' - elif pycc.startswith(('i686', 'i386')): - return '32' - else: - mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug') - return None - elif self.platform == 'win32': - return '32' - elif self.platform in {'win64', 'win-amd64'}: - return '64' - mlog.log(f'Unknown Windows Python platform {self.platform!r}') - return None - - def _get_windows_link_args(self) -> T.Optional[T.List[str]]: - if self.platform.startswith('win'): - vernum = self.variables.get('py_version_nodot') - verdot = self.variables.get('py_version_short') - imp_lower = self.variables.get('implementation_lower', 'python') - if self.static: - libpath = Path('libs') / f'libpython{vernum}.a' - else: - comp = self.get_compiler() - if comp.id == "gcc": - if imp_lower == 'pypy' and verdot == '3.8': - # The naming changed between 3.8 and 3.9 - libpath = Path('libpypy3-c.dll') - elif imp_lower == 'pypy': - libpath = Path(f'libpypy{verdot}-c.dll') - else: - libpath = Path(f'python{vernum}.dll') - else: - libpath = Path('libs') / f'python{vernum}.lib' - # base_prefix to allow for virtualenvs. - lib = Path(self.variables.get('base_prefix')) / libpath - elif self.platform == 'mingw': - if self.static: - libname = self.variables.get('LIBRARY') - else: - libname = self.variables.get('LDLIBRARY') - lib = Path(self.variables.get('LIBDIR')) / libname - else: - raise mesonlib.MesonBugException( - 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.') - if not lib.exists(): - mlog.log('Could not find Python3 library {!r}'.format(str(lib))) - return None - return [str(lib)] - - def _find_libpy_windows(self, env: 'Environment') -> None: - ''' - Find python3 libraries on Windows and also verify that the arch matches - what we are building for. - ''' - pyarch = self._get_windows_python_arch() - if pyarch is None: - self.is_found = False - return - arch = detect_cpu_family(env.coredata.compilers.host) - if arch == 'x86': - arch = '32' - elif arch == 'x86_64': - arch = '64' - else: - # We can't cross-compile Python 3 dependencies on Windows yet - mlog.log(f'Unknown architecture {arch!r} for', - mlog.bold(self.name)) - self.is_found = False - return - # Pyarch ends in '32' or '64' - if arch != pyarch: - mlog.log('Need', mlog.bold(self.name), f'for {arch}-bit, but found {pyarch}-bit') - self.is_found = False - return - # This can fail if the library is not found - largs = self._get_windows_link_args() - if largs is None: - self.is_found = False - return - self.link_args = largs - # Compile args - inc_paths = mesonlib.OrderedSet([ - self.variables.get('INCLUDEPY'), - self.paths.get('include'), - self.paths.get('platinclude')]) - - self.compile_args += ['-I' + path for path in inc_paths if path] - - # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/ - if pyarch == '64' and self.major_version == 2: - self.compile_args += ['-DMS_WIN64'] - - self.is_found = True - - -def python_factory(env: 'Environment', for_machine: 'MachineChoice', - kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods], - installation: 'PythonInstallation') -> T.List['DependencyGenerator']: - # We can't use the factory_methods decorator here, as we need to pass the - # extra installation argument - embed = kwargs.get('embed', False) - candidates: T.List['DependencyGenerator'] = [] - pkg_version = installation.variables.get('LDVERSION') or installation.version - - if DependencyMethods.PKGCONFIG in methods: - pkg_libdir = installation.variables.get('LIBPC') - pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.version, '>=3.8') else '' - pkg_name = f'python-{pkg_version}{pkg_embed}' - - # If python-X.Y.pc exists in LIBPC, we will try to use it - def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], - installation: 'PythonInstallation') -> 'ExternalDependency': - if not pkg_libdir: - # there is no LIBPC, so we can't search in it - return NotFoundDependency('python', env) - - old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) - old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) - os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir - try: - return PythonPkgConfigDependency(name, env, kwargs, installation, True) - finally: - def set_env(name, value): - if value is not None: - os.environ[name] = value - elif name in os.environ: - del os.environ[name] - set_env('PKG_CONFIG_LIBDIR', old_pkg_libdir) - set_env('PKG_CONFIG_PATH', old_pkg_path) - - candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) - # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, - # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. - if pkg_libdir is not None: - candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) - - if DependencyMethods.SYSTEM in methods: - candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) - - if DependencyMethods.EXTRAFRAMEWORK in methods: - nkwargs = kwargs.copy() - if mesonlib.version_compare(pkg_version, '>= 3'): - # There is a python in /System/Library/Frameworks, but that's python 2.x, - # Python 3 will always be in /Library - nkwargs['paths'] = ['/Library/Frameworks'] - candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation)) - - return candidates - - -INTROSPECT_COMMAND = '''\ -import os.path -import sysconfig -import json -import sys -import distutils.command.install - -def get_distutils_paths(scheme=None, prefix=None): - import distutils.dist - distribution = distutils.dist.Distribution() - install_cmd = distribution.get_command_obj('install') - if prefix is not None: - install_cmd.prefix = prefix - if scheme: - install_cmd.select_scheme(scheme) - install_cmd.finalize_options() - return { - 'data': install_cmd.install_data, - 'include': os.path.dirname(install_cmd.install_headers), - 'platlib': install_cmd.install_platlib, - 'purelib': install_cmd.install_purelib, - 'scripts': install_cmd.install_scripts, - } - -# On Debian derivatives, the Python interpreter shipped by the distribution uses -# a custom install scheme, deb_system, for the system install, and changes the -# default scheme to a custom one pointing to /usr/local and replacing -# site-packages with dist-packages. -# See https://github.com/mesonbuild/meson/issues/8739. -# XXX: We should be using sysconfig, but Debian only patches distutils. - -if 'deb_system' in distutils.command.install.INSTALL_SCHEMES: - paths = get_distutils_paths(scheme='deb_system') - install_paths = get_distutils_paths(scheme='deb_system', prefix='') -else: - paths = sysconfig.get_paths() - empty_vars = {'base': '', 'platbase': '', 'installed_base': ''} - install_paths = sysconfig.get_paths(vars=empty_vars) - -def links_against_libpython(): - from distutils.core import Distribution, Extension - cmd = Distribution().get_command_obj('build_ext') - cmd.ensure_finalized() - return bool(cmd.get_libraries(Extension('dummy', []))) - -variables = sysconfig.get_config_vars() -variables.update({'base_prefix': getattr(sys, 'base_prefix', sys.prefix)}) - -if sys.version_info < (3, 0): - suffix = variables.get('SO') -elif sys.version_info < (3, 8, 7): - # https://bugs.python.org/issue?@action=redirect&bpo=39825 - from distutils.sysconfig import get_config_var - suffix = get_config_var('EXT_SUFFIX') -else: - suffix = variables.get('EXT_SUFFIX') - -print(json.dumps({ - 'variables': variables, - 'paths': paths, - 'sysconfig_paths': sysconfig.get_paths(), - 'install_paths': install_paths, - 'version': sysconfig.get_python_version(), - 'platform': sysconfig.get_platform(), - 'is_pypy': '__pypy__' in sys.builtin_module_names, - 'is_venv': sys.prefix != variables['base_prefix'], - 'link_libpython': links_against_libpython(), - 'suffix': suffix, -})) -''' - - -class PythonExternalProgram(ExternalProgram): - def __init__(self, name: str, command: T.Optional[T.List[str]] = None, - ext_prog: T.Optional[ExternalProgram] = None): - if ext_prog is None: - super().__init__(name, command=command, silent=True) - else: - self.name = name - self.command = ext_prog.command - self.path = ext_prog.path - - # We want strong key values, so we always populate this with bogus data. - # Otherwise to make the type checkers happy we'd have to do .get() for - # everycall, even though we know that the introspection data will be - # complete - self.info: 'PythonIntrospectionDict' = { - 'install_paths': {}, - 'is_pypy': False, - 'is_venv': False, - 'link_libpython': False, - 'sysconfig_paths': {}, - 'paths': {}, - 'platform': 'sentinal', - 'variables': {}, - 'version': '0.0', - } - self.pure: bool = True - - def _check_version(self, version: str) -> bool: - if self.name == 'python2': - return mesonlib.version_compare(version, '< 3.0') - elif self.name == 'python3': - return mesonlib.version_compare(version, '>= 3.0') - return True + # This is a ClassVar instead of an instance bool, because although an + # installation is cached, we actually copy it, modify attributes such as pure, + # and return a temporary one rather than the cached object. + run_bytecompile: T.ClassVar[T.Dict[str, bool]] = {} def sanity(self, state: T.Optional['ModuleState'] = None) -> bool: - # Sanity check, we expect to have something that at least quacks in tune - from tempfile import NamedTemporaryFile - with NamedTemporaryFile(suffix='.py', delete=False, mode='w', encoding='utf-8') as tf: - tmpfilename = tf.name - tf.write(INTROSPECT_COMMAND) - cmd = self.get_command() + [tmpfilename] - p, stdout, stderr = mesonlib.Popen_safe(cmd) - os.unlink(tmpfilename) - try: - info = json.loads(stdout) - except json.JSONDecodeError: - info = None - mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode)) - mlog.debug('Program stdout:\n') - mlog.debug(stdout) - mlog.debug('Program stderr:\n') - mlog.debug(stderr) - - if info is not None and self._check_version(info['version']): - self.info = T.cast('PythonIntrospectionDict', info) + ret = super().sanity() + if ret: self.platlib = self._get_path(state, 'platlib') self.purelib = self._get_path(state, 'purelib') - return True - else: - return False + return ret - def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None: + def _get_path(self, state: T.Optional['ModuleState'], key: str) -> str: rel_path = self.info['install_paths'][key][1:] if not state: # This happens only from run_project_tests.py return rel_path - value = state.get_option(f'{key}dir', module='python') + value = T.cast('str', state.get_option(f'{key}dir', module='python')) if value: if state.is_user_defined_option('install_env', module='python'): raise mesonlib.MesonException(f'python.{key}dir and python.install_env are mutually exclusive') @@ -495,16 +114,18 @@ class PythonExternalProgram(ExternalProgram): _PURE_KW = KwargInfo('pure', (bool, NoneType)) _SUBDIR_KW = KwargInfo('subdir', str, default='') +_LIMITED_API_KW = KwargInfo('limited_api', str, default='', since='1.3.0') +_DEFAULTABLE_SUBDIR_KW = KwargInfo('subdir', (str, NoneType)) - -class PythonInstallation(ExternalProgramHolder): +class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']): def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): - ExternalProgramHolder.__init__(self, python, interpreter) + _ExternalProgramHolder.__init__(self, python, interpreter) info = python.info prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix')) assert isinstance(prefix, str), 'for mypy' self.variables = info['variables'] self.suffix = info['suffix'] + self.limited_api_suffix = info['limited_api_suffix'] self.paths = info['paths'] self.pure = python.pure self.platlib_install_path = os.path.join(prefix, python.platlib) @@ -528,20 +149,25 @@ class PythonInstallation(ExternalProgramHolder): }) @permittedKwargs(mod_kwargs) - def extension_module_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule': + @typed_pos_args('python.extension_module', str, varargs=(str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList, StructuredSources, ExtractedObjects, BuildTarget)) + @typed_kwargs('python.extension_module', *_MOD_KWARGS, _DEFAULTABLE_SUBDIR_KW, _LIMITED_API_KW, allow_unknown=True) + def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]], kwargs: ExtensionModuleKw) -> 'SharedModule': if 'install_dir' in kwargs: - if 'subdir' in kwargs: + if kwargs['subdir'] is not None: raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') else: - subdir = kwargs.pop('subdir', '') - if not isinstance(subdir, str): - raise InvalidArguments('"subdir" argument must be a string.') + # We want to remove 'subdir', but it may be None and we want to replace it with '' + # It must be done this way since we don't allow both `install_dir` + # and `subdir` to be set at the same time + subdir = kwargs.pop('subdir') or '' kwargs['install_dir'] = self._get_install_dir_impl(False, subdir) + target_suffix = self.suffix + new_deps = mesonlib.extract_as_list(kwargs, 'dependencies') - has_pydep = any(isinstance(dep, _PythonDependencyBase) for dep in new_deps) - if not has_pydep: + pydep = next((dep for dep in new_deps if isinstance(dep, _PythonDependencyBase)), None) + if pydep is None: pydep = self._dependency_method_impl({}) if not pydep.found(): raise mesonlib.MesonException('Python dependency not found') @@ -549,21 +175,84 @@ class PythonInstallation(ExternalProgramHolder): FeatureNew.single_use('python_installation.extension_module with implicit dependency on python', '0.63.0', self.subproject, 'use python_installation.dependency()', self.current_node) + + limited_api_version = kwargs.pop('limited_api') + allow_limited_api = self.interpreter.environment.coredata.get_option(OptionKey('allow_limited_api', module='python')) + if limited_api_version != '' and allow_limited_api: + + target_suffix = self.limited_api_suffix + + limited_api_version_hex = self._convert_api_version_to_py_version_hex(limited_api_version, pydep.version) + limited_api_definition = f'-DPy_LIMITED_API={limited_api_version_hex}' + + new_c_args = mesonlib.extract_as_list(kwargs, 'c_args') + new_c_args.append(limited_api_definition) + kwargs['c_args'] = new_c_args + + new_cpp_args = mesonlib.extract_as_list(kwargs, 'cpp_args') + new_cpp_args.append(limited_api_definition) + kwargs['cpp_args'] = new_cpp_args + + # When compiled under MSVC, Python's PC/pyconfig.h forcibly inserts pythonMAJOR.MINOR.lib + # into the linker path when not running in debug mode via a series #pragma comment(lib, "") + # directives. We manually override these here as this interferes with the intended + # use of the 'limited_api' kwarg + for_machine = kwargs['native'] + compilers = self.interpreter.environment.coredata.compilers[for_machine] + if any(compiler.get_id() == 'msvc' for compiler in compilers.values()): + pydep_copy = copy.copy(pydep) + pydep_copy.find_libpy_windows(self.env, limited_api=True) + if not pydep_copy.found(): + raise mesonlib.MesonException('Python dependency supporting limited API not found') + + new_deps.remove(pydep) + new_deps.append(pydep_copy) + + pyver = pydep.version.replace('.', '') + python_windows_debug_link_exception = f'/NODEFAULTLIB:python{pyver}_d.lib' + python_windows_release_link_exception = f'/NODEFAULTLIB:python{pyver}.lib' + + new_link_args = mesonlib.extract_as_list(kwargs, 'link_args') + + is_debug = self.interpreter.environment.coredata.options[OptionKey('debug')].value + if is_debug: + new_link_args.append(python_windows_debug_link_exception) + else: + new_link_args.append(python_windows_release_link_exception) + + kwargs['link_args'] = new_link_args + kwargs['dependencies'] = new_deps # msys2's python3 has "-cpython-36m.dll", we have to be clever # FIXME: explain what the specific cleverness is here - split, suffix = self.suffix.rsplit('.', 1) - args[0] += split + split, target_suffix = target_suffix.rsplit('.', 1) + args = (args[0] + split, args[1]) kwargs['name_prefix'] = '' - kwargs['name_suffix'] = suffix + kwargs['name_suffix'] = target_suffix - if 'gnu_symbol_visibility' not in kwargs and \ + if kwargs['gnu_symbol_visibility'] == '' and \ (self.is_pypy or mesonlib.version_compare(self.version, '>=3.9')): kwargs['gnu_symbol_visibility'] = 'inlineshidden' - return self.interpreter.func_shared_module(None, args, kwargs) + return self.interpreter.build_target(self.current_node, args, kwargs, SharedModule) + + def _convert_api_version_to_py_version_hex(self, api_version: str, detected_version: str) -> str: + python_api_version_format = re.compile(r'[0-9]\.[0-9]{1,2}') + decimal_match = python_api_version_format.fullmatch(api_version) + if not decimal_match: + raise InvalidArguments(f'Python API version invalid: "{api_version}".') + if mesonlib.version_compare(api_version, '<3.2'): + raise InvalidArguments(f'Python Limited API version invalid: {api_version} (must be greater than 3.2)') + if mesonlib.version_compare(api_version, '>' + detected_version): + raise InvalidArguments(f'Python Limited API version too high: {api_version} (detected {detected_version})') + + version_components = api_version.split('.') + major = int(version_components[0]) + minor = int(version_components[1]) + + return '0x{:02x}{:02x}0000'.format(major, minor) def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency: for_machine = self.interpreter.machine_from_native_kwarg(kwargs) @@ -575,13 +264,8 @@ class PythonInstallation(ExternalProgramHolder): new_kwargs = kwargs.copy() new_kwargs['required'] = False - methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs) - # it's theoretically (though not practically) possible to not bind dep, let's ensure it is. - dep: Dependency = NotFoundDependency('python', self.interpreter.environment) - for d in python_factory(self.interpreter.environment, for_machine, new_kwargs, methods, self): - dep = d() - if dep.found(): - break + candidates = python_factory(self.interpreter.environment, for_machine, new_kwargs, self.held_object) + dep = find_external_dependency('python', self.interpreter.environment, new_kwargs, candidates) self.interpreter.coredata.deps[for_machine].put(identifier, dep) return dep @@ -611,19 +295,20 @@ class PythonInstallation(ExternalProgramHolder): ) def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], kwargs: 'PyInstallKw') -> 'Data': - tag = kwargs['install_tag'] or 'runtime' + self.held_object.run_bytecompile[self.version] = True + tag = kwargs['install_tag'] or 'python-runtime' pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure install_dir = self._get_install_dir_impl(pure, kwargs['subdir']) return self.interpreter.install_data_impl( self.interpreter.source_strings_to_files(args[0]), install_dir, mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python', - install_dir_name=install_dir.optname, preserve_path=kwargs['preserve_path']) @noPosargs @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: + self.held_object.run_bytecompile[self.version] = True pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure return self._get_install_dir_impl(pure, kwargs['subdir']) @@ -687,20 +372,72 @@ class PythonModule(ExtensionModule): def __init__(self, interpreter: 'Interpreter') -> None: super().__init__(interpreter) - self.installations: T.Dict[str, ExternalProgram] = {} + self.installations: T.Dict[str, MaybePythonProg] = {} self.methods.update({ 'find_installation': self.find_installation, }) + def _get_install_scripts(self) -> T.List[mesonlib.ExecutableSerialisation]: + backend = self.interpreter.backend + ret = [] + optlevel = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('bytecompile', module='python')) + if optlevel == -1: + return ret + if not any(PythonExternalProgram.run_bytecompile.values()): + return ret + + installdata = backend.create_install_data() + py_files = [] + + def should_append(f, isdir: bool = False): + # This uses the install_plan decorated names to see if the original source was propagated via + # install_sources() or get_install_dir(). + return f.startswith(('{py_platlib}', '{py_purelib}')) and (f.endswith('.py') or isdir) + + for t in installdata.targets: + if should_append(t.out_name): + py_files.append((t.out_name, os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname)))) + for d in installdata.data: + if should_append(d.install_path_name): + py_files.append((d.install_path_name, os.path.join(installdata.prefix, d.install_path))) + for d in installdata.install_subdirs: + if should_append(d.install_path_name, True): + py_files.append((d.install_path_name, os.path.join(installdata.prefix, d.install_path))) + + import importlib.resources + pycompile = os.path.join(self.interpreter.environment.get_scratch_dir(), 'pycompile.py') + with open(pycompile, 'wb') as f: + f.write(importlib.resources.read_binary('mesonbuild.scripts', 'pycompile.py')) + + for i in self.installations.values(): + if isinstance(i, PythonExternalProgram) and i.run_bytecompile[i.info['version']]: + i = T.cast('PythonExternalProgram', i) + manifest = f'python-{i.info["version"]}-installed.json' + manifest_json = [] + for name, f in py_files: + if f.startswith((os.path.join(installdata.prefix, i.platlib), os.path.join(installdata.prefix, i.purelib))): + manifest_json.append(name) + with open(os.path.join(self.interpreter.environment.get_scratch_dir(), manifest), 'w', encoding='utf-8') as f: + json.dump(manifest_json, f) + cmd = i.command + [pycompile, manifest, str(optlevel)] + + script = backend.get_executable_serialisation(cmd, verbose=True, tag='python-runtime', + installdir_map={'py_purelib': i.purelib, 'py_platlib': i.platlib}) + ret.append(script) + return ret + + def postconf_hook(self, b: Build) -> None: + b.install_scripts.extend(self._get_install_scripts()) + # https://www.python.org/dev/peps/pep-0397/ @staticmethod def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: - if name_or_path not in ['python2', 'python3']: + if not name_or_path.startswith(('python2', 'python3')): return None if not shutil.which('py'): # program not installed, return without an exception return None - ver = {'python2': '-2', 'python3': '-3'}[name_or_path] + ver = f'-{name_or_path[6:]}' cmd = ['py', ver, '-c', "import sysconfig; print(sysconfig.get_config_var('BINDIR'))"] _, stdout, _ = mesonlib.Popen_safe(cmd) directory = stdout.strip() @@ -709,7 +446,7 @@ class PythonModule(ExtensionModule): else: return None - def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> ExternalProgram: + def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> MaybePythonProg: if not name_or_path: python = PythonExternalProgram('python3', mesonlib.python_command) else: @@ -727,7 +464,8 @@ class PythonModule(ExtensionModule): # named python is available and has a compatible version, let's use # it if not python.found() and name_or_path in {'python2', 'python3'}: - python = PythonExternalProgram('python') + tmp_python = ExternalProgram.from_entry(display_name, 'python') + python = PythonExternalProgram(name_or_path, ext_prog=tmp_python) if python.found(): if python.sanity(state): @@ -739,7 +477,7 @@ class PythonModule(ExtensionModule): else: mlog.warning(sanitymsg, location=state.current_node) - return NonExistingExternalProgram() + return NonExistingExternalProgram(python.name) @disablerIfNotFound @typed_pos_args('python.find_installation', optargs=[str]) @@ -751,7 +489,7 @@ class PythonModule(ExtensionModule): _PURE_KW.evolve(default=True, since='0.64.0'), ) def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]], - kwargs: 'FindInstallationKw') -> ExternalProgram: + kwargs: 'FindInstallationKw') -> MaybePythonProg: feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0') disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check) @@ -807,14 +545,16 @@ class PythonModule(ExtensionModule): if not python.found(): if required: raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python')) - return NonExistingExternalProgram() + return NonExistingExternalProgram(python.name) elif missing_modules: if required: raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules))) - return NonExistingExternalProgram() + return NonExistingExternalProgram(python.name) else: + assert isinstance(python, PythonExternalProgram), 'for mypy' python = copy.copy(python) python.pure = kwargs['pure'] + python.run_bytecompile.setdefault(python.info['version'], False) return python raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') diff --git a/mesonbuild/modules/python3.py b/mesonbuild/modules/python3.py index 52f8531..bc4d9af 100644 --- a/mesonbuild/modules/python3.py +++ b/mesonbuild/modules/python3.py @@ -11,15 +11,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sysconfig -from .. import mesonlib +import typing as T -from . import ExtensionModule, ModuleInfo -from ..interpreterbase import typed_pos_args, noPosargs, noKwargs, permittedKwargs -from ..build import known_shmod_kwargs +from .. import mesonlib +from . import ExtensionModule, ModuleInfo, ModuleState +from ..build import ( + BuildTarget, CustomTarget, CustomTargetIndex, ExtractedObjects, + GeneratedList, SharedModule, StructuredSources, known_shmod_kwargs +) +from ..interpreter.type_checking import SHARED_MOD_KWS +from ..interpreterbase import typed_kwargs, typed_pos_args, noPosargs, noKwargs, permittedKwargs from ..programs import ExternalProgram +if T.TYPE_CHECKING: + from ..interpreter.interpreter import BuildTargetSource + from ..interpreter.kwargs import SharedModule as SharedModuleKW + + +_MOD_KWARGS = [k for k in SHARED_MOD_KWS if k.name not in {'name_prefix', 'name_suffix'}] + class Python3Module(ExtensionModule): @@ -34,12 +47,10 @@ class Python3Module(ExtensionModule): 'sysconfig_path': self.sysconfig_path, }) - @permittedKwargs(known_shmod_kwargs) - def extension_module(self, state, args, kwargs): - if 'name_prefix' in kwargs: - raise mesonlib.MesonException('Name_prefix is set automatically, specifying it is forbidden.') - if 'name_suffix' in kwargs: - raise mesonlib.MesonException('Name_suffix is set automatically, specifying it is forbidden.') + @permittedKwargs(known_shmod_kwargs - {'name_prefix', 'name_suffix'}) + @typed_pos_args('python3.extension_module', str, varargs=(str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList, StructuredSources, ExtractedObjects, BuildTarget)) + @typed_kwargs('python3.extension_module', *_MOD_KWARGS, allow_unknown=True) + def extension_module(self, state: ModuleState, args: T.Tuple[str, T.List[BuildTargetSource]], kwargs: SharedModuleKW): host_system = state.host_machine.system if host_system == 'darwin': # Default suffix is 'dylib' but Python does not use it for extensions. @@ -51,7 +62,7 @@ class Python3Module(ExtensionModule): suffix = [] kwargs['name_prefix'] = '' kwargs['name_suffix'] = suffix - return self.interpreter.func_shared_module(None, args, kwargs) + return self.interpreter.build_target(state.current_node, args, kwargs, SharedModule) @noPosargs @noKwargs diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index 73160c0..83dcf31 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -23,7 +23,7 @@ from . import ModuleReturnValue, ExtensionModule from .. import build from .. import coredata from .. import mlog -from ..dependencies import find_external_dependency, Dependency, ExternalLibrary +from ..dependencies import find_external_dependency, Dependency, ExternalLibrary, InternalDependency from ..mesonlib import MesonException, File, version_compare, Popen_safe from ..interpreter import extract_required_kwarg from ..interpreter.type_checking import INSTALL_DIR_KW, INSTALL_KW, NoneType @@ -108,7 +108,7 @@ class QtBaseModule(ExtensionModule): self.qt_version = qt_version # It is important that this list does not change order as the order of # the returned ExternalPrograms will change as well - self.tools: T.Dict[str, ExternalProgram] = { + self.tools: T.Dict[str, T.Union[ExternalProgram, build.Executable]] = { 'moc': NonExistingExternalProgram('moc'), 'uic': NonExistingExternalProgram('uic'), 'rcc': NonExistingExternalProgram('rcc'), @@ -152,7 +152,7 @@ class QtBaseModule(ExtensionModule): arg = ['-v'] # Ensure that the version of qt and each tool are the same - def get_version(p: ExternalProgram) -> str: + def get_version(p: T.Union[ExternalProgram, build.Executable]) -> str: _, out, err = Popen_safe(p.get_command() + arg) if name == 'lrelease' or not qt_dep.version.startswith('4'): care = out @@ -348,6 +348,7 @@ class QtBaseModule(ExtensionModule): [f'{name}.cpp'], depend_files=qrc_deps, depfile=f'{name}.d', + description='Compiling Qt resources {}', ) targets.append(res_target) else: @@ -368,6 +369,7 @@ class QtBaseModule(ExtensionModule): [f'{name}.cpp'], depend_files=qrc_deps, depfile=f'{name}.d', + description='Compiling Qt resources {}', ) targets.append(res_target) @@ -455,7 +457,10 @@ class QtBaseModule(ExtensionModule): inc = state.get_include_args(include_dirs=kwargs['include_directories']) compile_args: T.List[str] = [] for dep in kwargs['dependencies']: - compile_args.extend([a for a in dep.get_all_compile_args() if a.startswith(('-I', '-D'))]) + compile_args.extend(a for a in dep.get_all_compile_args() if a.startswith(('-I', '-D'))) + if isinstance(dep, InternalDependency): + for incl in dep.include_directories: + compile_args.extend(f'-I{i}' for i in incl.to_string_list(self.interpreter.source_root, self.interpreter.environment.build_dir)) output: T.List[build.GeneratedList] = [] @@ -472,7 +477,7 @@ class QtBaseModule(ExtensionModule): if kwargs['sources']: moc_gen = build.Generator( self.tools['moc'], arguments, ['@BASENAME@.moc'], - depfile='@BASENAME.moc.d@', + depfile='@BASENAME@.moc.d', name=f'Qt{self.qt_version} moc source') output.append(moc_gen.process_files(kwargs['sources'], state)) @@ -587,7 +592,7 @@ class QtBaseModule(ExtensionModule): ts = os.path.basename(ts) else: outdir = state.subdir - cmd: T.List[T.Union[ExternalProgram, str]] = [self.tools['lrelease'], '@INPUT@', '-qm', '@OUTPUT@'] + cmd: T.List[T.Union[ExternalProgram, build.Executable, str]] = [self.tools['lrelease'], '@INPUT@', '-qm', '@OUTPUT@'] lrelease_target = build.CustomTarget( f'qt{self.qt_version}-compile-{ts}', outdir, @@ -600,6 +605,7 @@ class QtBaseModule(ExtensionModule): install_dir=[kwargs['install_dir']], install_tag=['i18n'], build_by_default=kwargs['build_by_default'], + description='Compiling Qt translations {}', ) translations.append(lrelease_target) if qresource: diff --git a/mesonbuild/modules/qt4.py b/mesonbuild/modules/qt4.py index b8948f7..6bdf1c5 100644 --- a/mesonbuild/modules/qt4.py +++ b/mesonbuild/modules/qt4.py @@ -12,17 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import typing as T + from .qt import QtBaseModule from . import ModuleInfo +if T.TYPE_CHECKING: + from ..interpreter import Interpreter + class Qt4Module(QtBaseModule): INFO = ModuleInfo('qt4') - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter): QtBaseModule.__init__(self, interpreter, qt_version=4) -def initialize(*args, **kwargs): - return Qt4Module(*args, **kwargs) +def initialize(interp: Interpreter) -> Qt4Module: + return Qt4Module(interp) diff --git a/mesonbuild/modules/qt5.py b/mesonbuild/modules/qt5.py index 3933ea0..d9f0a5e 100644 --- a/mesonbuild/modules/qt5.py +++ b/mesonbuild/modules/qt5.py @@ -12,17 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import typing as T + from .qt import QtBaseModule from . import ModuleInfo +if T.TYPE_CHECKING: + from ..interpreter import Interpreter + class Qt5Module(QtBaseModule): INFO = ModuleInfo('qt5') - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter): QtBaseModule.__init__(self, interpreter, qt_version=5) -def initialize(*args, **kwargs): - return Qt5Module(*args, **kwargs) +def initialize(interp: Interpreter) -> Qt5Module: + return Qt5Module(interp) diff --git a/mesonbuild/modules/qt6.py b/mesonbuild/modules/qt6.py index 66fc43f..cafc531 100644 --- a/mesonbuild/modules/qt6.py +++ b/mesonbuild/modules/qt6.py @@ -12,17 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import typing as T + from .qt import QtBaseModule from . import ModuleInfo +if T.TYPE_CHECKING: + from ..interpreter import Interpreter class Qt6Module(QtBaseModule): INFO = ModuleInfo('qt6', '0.57.0') - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter): QtBaseModule.__init__(self, interpreter, qt_version=6) -def initialize(*args, **kwargs): - return Qt6Module(*args, **kwargs) +def initialize(interp: Interpreter) -> Qt6Module: + return Qt6Module(interp) diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index d41b99c..8e55ad4 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -1,4 +1,4 @@ -# Copyright © 2020-2022 Intel Corporation +# Copyright © 2020-2023 Intel Corporation # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,25 +11,31 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - from __future__ import annotations +import itertools import os import typing as T +from mesonbuild.interpreterbase.decorators import FeatureNew + from . import ExtensionModule, ModuleReturnValue, ModuleInfo from .. import mlog -from ..build import BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, IncludeDirs, CustomTarget, StructuredSources -from ..dependencies import Dependency, ExternalLibrary -from ..interpreter.type_checking import DEPENDENCIES_KW, TEST_KWS, OUTPUT_KW, INCLUDE_DIRECTORIES, include_dir_string_new -from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs +from ..build import (BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, + CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary) +from ..compilers.compilers import are_asserts_disabled +from ..interpreter.type_checking import DEPENDENCIES_KW, LINK_WITH_KW, SHARED_LIB_KWS, TEST_KWS, OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS +from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs, permittedKwargs from ..mesonlib import File if T.TYPE_CHECKING: from . import ModuleState + from ..build import IncludeDirs, LibTypes + from ..dependencies import Dependency, ExternalLibrary from ..interpreter import Interpreter from ..interpreter import kwargs as _kwargs from ..interpreter.interpreter import SourceInputs, SourceOutputs - from ..programs import ExternalProgram + from ..programs import ExternalProgram, OverrideProgram + from ..interpreter.type_checking import SourcesVarargsType from typing_extensions import TypedDict @@ -37,6 +43,8 @@ if T.TYPE_CHECKING: dependencies: T.List[T.Union[Dependency, ExternalLibrary]] is_parallel: bool + link_with: T.List[LibTypes] + rust_args: T.List[str] class FuncBindgen(TypedDict): @@ -56,10 +64,11 @@ class RustModule(ExtensionModule): def __init__(self, interpreter: Interpreter) -> None: super().__init__(interpreter) - self._bindgen_bin: T.Optional[ExternalProgram] = None + self._bindgen_bin: T.Optional[T.Union[ExternalProgram, Executable, OverrideProgram]] = None self.methods.update({ 'test': self.test, 'bindgen': self.bindgen, + 'proc_macro': self.proc_macro, }) @typed_pos_args('rust.test', str, BuildTarget) @@ -67,6 +76,14 @@ class RustModule(ExtensionModule): 'rust.test', *TEST_KWS, DEPENDENCIES_KW, + LINK_WITH_KW.evolve(since='1.2.0'), + KwargInfo( + 'rust_args', + ContainerTypeInfo(list, str), + listify=True, + default=[], + since='1.2.0', + ), KwargInfo('is_parallel', bool, default=False), ) def test(self, state: ModuleState, args: T.Tuple[str, BuildTarget], kwargs: FuncTest) -> ModuleReturnValue: @@ -111,6 +128,9 @@ class RustModule(ExtensionModule): rust.test('rust_lib_test', rust_lib) ``` """ + if any(isinstance(t, Jar) for t in kwargs.get('link_with', [])): + raise InvalidArguments('Rust tests cannot link with Jar targets') + name = args[0] base_target: BuildTarget = args[1] if not base_target.uses_rust(): @@ -138,12 +158,17 @@ class RustModule(ExtensionModule): tkwargs['args'] = extra_args + ['--test', '--format', 'pretty'] tkwargs['protocol'] = 'rust' - new_target_kwargs = base_target.kwargs.copy() + new_target_kwargs = base_target.original_kwargs.copy() # Don't mutate the shallow copied list, instead replace it with a new # one - new_target_kwargs['rust_args'] = new_target_kwargs.get('rust_args', []) + ['--test'] new_target_kwargs['install'] = False new_target_kwargs['dependencies'] = new_target_kwargs.get('dependencies', []) + kwargs['dependencies'] + new_target_kwargs['link_with'] = new_target_kwargs.get('link_with', []) + kwargs['link_with'] + del new_target_kwargs['rust_crate_type'] + + lang_args = base_target.extra_args.copy() + lang_args['rust'] = base_target.extra_args['rust'] + kwargs['rust_args'] + ['--test'] + new_target_kwargs['language_args'] = lang_args sources = T.cast('T.List[SourceOutputs]', base_target.sources.copy()) sources.extend(base_target.generated) @@ -172,7 +197,7 @@ class RustModule(ExtensionModule): listify=True, required=True, ), - INCLUDE_DIRECTORIES.evolve(feature_validator=include_dir_string_new), + INCLUDE_DIRECTORIES.evolve(since_values={ContainerTypeInfo(list, str): '1.0.0'}), OUTPUT_KW, DEPENDENCIES_KW.evolve(since='1.0.0'), ) @@ -193,11 +218,16 @@ class RustModule(ExtensionModule): else: depends.append(d) - clang_args: T.List[str] = [] + # Copy to avoid subsequent calls mutating the original + # TODO: if we want this to be per-machine we'll need a native kwarg + clang_args = state.environment.properties.host.get_bindgen_clang_args().copy() + for i in state.process_include_dirs(kwargs['include_directories']): # bindgen always uses clang, so it's safe to hardcode -I here clang_args.extend([f'-I{x}' for x in i.to_string_list( state.environment.get_source_dir(), state.environment.get_build_dir())]) + if are_asserts_disabled(state.environment.coredata.options): + clang_args.append('-DNDEBUG') for de in kwargs['dependencies']: for i in de.get_include_dirs(): @@ -210,6 +240,13 @@ class RustModule(ExtensionModule): elif isinstance(s, CustomTarget): depends.append(s) + # We only want include directories and defines, other things may not be valid + cargs = state.get_option('args', state.subproject, lang='c') + assert isinstance(cargs, list), 'for mypy' + for a in itertools.chain(state.global_args.get('c', []), state.project_args.get('c', []), cargs): + if a.startswith(('-I', '/I', '-D', '/D', '-U', '/U')): + clang_args.append(a) + if self._bindgen_bin is None: self._bindgen_bin = state.find_program('bindgen') @@ -241,10 +278,24 @@ class RustModule(ExtensionModule): extra_depends=depends, depend_files=depend_files, backend=state.backend, + description='Generating bindings for Rust {}', ) return ModuleReturnValue([target], [target]) + # Allow a limited set of kwargs, but still use the full set of typed_kwargs() + # because it could be setting required default values. + @FeatureNew('rust.proc_macro', '1.3.0') + @permittedKwargs({'rust_args', 'rust_dependency_map', 'sources', 'dependencies', 'extra_files', + 'link_args', 'link_depends', 'link_with', 'override_options'}) + @typed_pos_args('rust.proc_macro', str, varargs=SOURCES_VARARGS) + @typed_kwargs('rust.proc_macro', *SHARED_LIB_KWS, allow_unknown=True) + def proc_macro(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], kwargs: _kwargs.SharedLibrary) -> SharedLibrary: + kwargs['native'] = True # type: ignore + kwargs['rust_crate_type'] = 'proc-macro' # type: ignore + target = state._interpreter.build_target(state.current_node, args, kwargs, SharedLibrary) + return target + def initialize(interp: Interpreter) -> RustModule: return RustModule(interp) diff --git a/mesonbuild/modules/simd.py b/mesonbuild/modules/simd.py index a33022d..b8baf39 100644 --- a/mesonbuild/modules/simd.py +++ b/mesonbuild/modules/simd.py @@ -11,77 +11,114 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -from .. import mesonlib, compilers, mlog +import typing as T + +from .. import mesonlib, mlog from .. import build +from ..compilers import Compiler +from ..interpreter.type_checking import BT_SOURCES_KW, STATIC_LIB_KWS +from ..interpreterbase.decorators import KwargInfo, permittedKwargs, typed_pos_args, typed_kwargs from . import ExtensionModule, ModuleInfo +if T.TYPE_CHECKING: + from . import ModuleState + from ..interpreter import Interpreter, kwargs as kwtypes + from ..interpreter.type_checking import SourcesVarargsType + + class CheckKw(kwtypes.StaticLibrary): + + compiler: Compiler + mmx: SourcesVarargsType + sse: SourcesVarargsType + sse2: SourcesVarargsType + sse3: SourcesVarargsType + ssse3: SourcesVarargsType + sse41: SourcesVarargsType + sse42: SourcesVarargsType + avx: SourcesVarargsType + avx2: SourcesVarargsType + neon: SourcesVarargsType + + +# FIXME add Altivec and AVX512. +ISETS = ( + 'mmx', + 'sse', + 'sse2', + 'sse3', + 'ssse3', + 'sse41', + 'sse42', + 'avx', + 'avx2', + 'neon', +) + + class SimdModule(ExtensionModule): INFO = ModuleInfo('SIMD', '0.42.0', unstable=True) - def __init__(self, interpreter): + def __init__(self, interpreter: Interpreter): super().__init__(interpreter) - # FIXME add Altivec and AVX512. - self.isets = ('mmx', - 'sse', - 'sse2', - 'sse3', - 'ssse3', - 'sse41', - 'sse42', - 'avx', - 'avx2', - 'neon', - ) self.methods.update({ 'check': self.check, }) - def check(self, state, args, kwargs): - result = [] - if len(args) != 1: - raise mesonlib.MesonException('Check requires one argument, a name prefix for checks.') - prefix = args[0] - if not isinstance(prefix, str): - raise mesonlib.MesonException('Argument must be a string.') - if 'compiler' not in kwargs: - raise mesonlib.MesonException('Must specify compiler keyword') + @typed_pos_args('simd.check', str) + @typed_kwargs('simd.check', + KwargInfo('compiler', Compiler, required=True), + *[BT_SOURCES_KW.evolve(name=iset, default=None) for iset in ISETS], + *[a for a in STATIC_LIB_KWS if a.name != 'sources'], + allow_unknown=True) # Because we also accept STATIC_LIB_KWS, but build targets have not been completely ported to typed_pos_args/typed_kwargs. + @permittedKwargs({'compiler', *ISETS, *build.known_stlib_kwargs}) # Also remove this, per above comment + def check(self, state: ModuleState, args: T.Tuple[str], kwargs: CheckKw) -> T.List[T.Union[T.List[build.StaticLibrary], build.ConfigurationData]]: + result: T.List[build.StaticLibrary] = [] + if 'sources' in kwargs: raise mesonlib.MesonException('SIMD module does not support the "sources" keyword') - basic_kwargs = {} - for key, value in kwargs.items(): - if key not in self.isets and key != 'compiler': - basic_kwargs[key] = value + + local_kwargs = set((*ISETS, 'compiler')) + static_lib_kwargs = T.cast('kwtypes.StaticLibrary', {k: v for k, v in kwargs.items() if k not in local_kwargs}) + + prefix = args[0] compiler = kwargs['compiler'] - if not isinstance(compiler, compilers.compilers.Compiler): - raise mesonlib.MesonException('Compiler argument must be a compiler object.') conf = build.ConfigurationData() - for iset in self.isets: - if iset not in kwargs: + + for iset in ISETS: + sources = kwargs[iset] + if sources is None: continue - iset_fname = kwargs[iset] # Might also be an array or Files. static_library will validate. - args = compiler.get_instruction_set_args(iset) - if args is None: - mlog.log('Compiler supports %s:' % iset, mlog.red('NO')) + + compile_args = compiler.get_instruction_set_args(iset) + if compile_args is None: + mlog.log(f'Compiler supports {iset}:', mlog.red('NO')) + continue + + if not compiler.has_multi_arguments(compile_args, state.environment)[0]: + mlog.log(f'Compiler supports {iset}:', mlog.red('NO')) continue - if args: - if not compiler.has_multi_arguments(args, state.environment)[0]: - mlog.log('Compiler supports %s:' % iset, mlog.red('NO')) - continue - mlog.log('Compiler supports %s:' % iset, mlog.green('YES')) - conf.values['HAVE_' + iset.upper()] = ('1', 'Compiler supports %s.' % iset) + mlog.log(f'Compiler supports {iset}:', mlog.green('YES')) + conf.values['HAVE_' + iset.upper()] = ('1', f'Compiler supports {iset}.') + libname = prefix + '_' + iset - lib_kwargs = {'sources': iset_fname, - } - lib_kwargs.update(basic_kwargs) + lib_kwargs = static_lib_kwargs.copy() + lib_kwargs['sources'] = sources + + # Add compile args we derived above to those the user provided us langarg_key = compiler.get_language() + '_args' old_lang_args = mesonlib.extract_as_list(lib_kwargs, langarg_key) - all_lang_args = old_lang_args + args + all_lang_args = old_lang_args + compile_args lib_kwargs[langarg_key] = all_lang_args - result.append(self.interpreter.func_static_lib(None, [libname], lib_kwargs)) + + lib = self.interpreter.build_target(state.current_node, (libname, []), lib_kwargs, build.StaticLibrary) + + result.append(lib) + return [result, conf] -def initialize(*args, **kwargs): - return SimdModule(*args, **kwargs) +def initialize(interp: Interpreter) -> SimdModule: + return SimdModule(interp) diff --git a/mesonbuild/modules/sourceset.py b/mesonbuild/modules/sourceset.py index c35416e..ae594b5 100644 --- a/mesonbuild/modules/sourceset.py +++ b/mesonbuild/modules/sourceset.py @@ -197,8 +197,6 @@ class SourceSetImpl(SourceSet, MutableModuleObject): raise InterpreterException('add_all called with both positional and keyword arguments') keys, dependencies = self.check_conditions(when) for s in if_true: - if not isinstance(s, SourceSetImpl): - raise InvalidCode('Arguments to \'add_all\' after the first must be source sets') s.frozen = True self.rules.append(SourceSetRule(keys, dependencies, [], [], if_true, [])) diff --git a/mesonbuild/modules/windows.py b/mesonbuild/modules/windows.py index dec6e2e..b7cdeb3 100644 --- a/mesonbuild/modules/windows.py +++ b/mesonbuild/modules/windows.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import enum import os @@ -85,7 +86,8 @@ class WindowsModule(ExtensionModule): if not rescomp or not rescomp.found(): comp = self.detect_compiler(state.environment.coredata.compilers[for_machine]) - if comp.id in {'msvc', 'clang-cl', 'intel-cl'}: + if comp.id in {'msvc', 'clang-cl', 'intel-cl'} or (comp.linker and comp.linker.id in {'link', 'lld-link'}): + # Microsoft compilers uses rc irrespective of the frontend rescomp = ExternalProgram('rc', silent=True) else: rescomp = ExternalProgram('windres', silent=True) @@ -95,6 +97,7 @@ class WindowsModule(ExtensionModule): for (arg, match, rc_type) in [ ('/?', '^.*Microsoft.*Resource Compiler.*$', ResourceCompilerType.rc), + ('/?', 'LLVM Resource Converter.*$', ResourceCompilerType.rc), ('--version', '^.*GNU windres.*$', ResourceCompilerType.windres), ('--version', '^.*Wine Resource Compiler.*$', ResourceCompilerType.wrc), ]: @@ -163,7 +166,7 @@ class WindowsModule(ExtensionModule): elif isinstance(src, build.CustomTargetIndex): FeatureNew.single_use('windows.compile_resource CustomTargetIndex in positional arguments', '0.61.0', state.subproject, location=state.current_node) - # This dance avoids a case where two indexs of the same + # This dance avoids a case where two indexes of the same # target are given as separate arguments. yield (f'{src.get_id()}_{src.target.get_outputs().index(src.output)}', f'windows_compile_resources_{src.get_filename()}', src) @@ -203,6 +206,7 @@ class WindowsModule(ExtensionModule): depfile=depfile, depend_files=wrc_depend_files, extra_depends=wrc_depends, + description='Compiling Windows resource {}', )) return ModuleReturnValue(res_targets, [res_targets]) diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index 53a4a89..0f63c9e 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -11,18 +11,23 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from dataclasses import dataclass +from __future__ import annotations +from dataclasses import dataclass, field import re import codecs -import types +import os import typing as T + from .mesonlib import MesonException from . import mlog if T.TYPE_CHECKING: + from typing_extensions import Literal + from .ast import AstVisitor + BaseNodeT = T.TypeVar('BaseNodeT', bound='BaseNode') + # This is the regex for the supported escape sequences of a regular string # literal, like 'abc\x00' ESCAPE_SEQUENCE_SINGLE_RE = re.compile(r''' @@ -34,25 +39,20 @@ ESCAPE_SEQUENCE_SINGLE_RE = re.compile(r''' | \\[\\'abfnrtv] # Single-character escapes )''', re.UNICODE | re.VERBOSE) -class MesonUnicodeDecodeError(MesonException): - def __init__(self, match: str) -> None: - super().__init__(match) - self.match = match - def decode_match(match: T.Match[str]) -> str: - try: - return codecs.decode(match.group(0).encode(), 'unicode_escape') - except UnicodeDecodeError: - raise MesonUnicodeDecodeError(match.group(0)) + return codecs.decode(match.group(0).encode(), 'unicode_escape') class ParseException(MesonException): + + ast: T.Optional[CodeBlockNode] = None + def __init__(self, text: str, line: str, lineno: int, colno: int) -> None: # Format as error message, followed by the line with the error, followed by a caret to show the error column. - super().__init__("{}\n{}\n{}".format(text, line, '{}^'.format(' ' * colno))) + super().__init__(mlog.code_line(text, line, colno)) self.lineno = lineno self.colno = colno -class BlockParseException(MesonException): +class BlockParseException(ParseException): def __init__( self, text: str, @@ -72,7 +72,7 @@ class BlockParseException(MesonException): # Followed by a caret to show the block start # Followed by underscores # Followed by a caret to show the block end. - super().__init__("{}\n{}\n{}".format(text, line, '{}^{}^'.format(' ' * start_colno, '_' * (colno - start_colno - 1)))) + MesonException.__init__(self, "{}\n{}\n{}".format(text, line, '{}^{}^'.format(' ' * start_colno, '_' * (colno - start_colno - 1)))) else: # If block start and end are on different lines, it is formatted as: # Error message @@ -81,7 +81,7 @@ class BlockParseException(MesonException): # Followed by a message saying where the block started. # Followed by the line of the block start. # Followed by a caret for the block start. - super().__init__("%s\n%s\n%s\nFor a block that started at %d,%d\n%s\n%s" % (text, line, '%s^' % (' ' * colno), start_lineno, start_colno, start_line, "%s^" % (' ' * start_colno))) + MesonException.__init__(self, "%s\n%s\n%s\nFor a block that started at %d,%d\n%s\n%s" % (text, line, '%s^' % (' ' * colno), start_lineno, start_colno, start_line, "%s^" % (' ' * start_colno))) self.lineno = lineno self.colno = colno @@ -111,14 +111,17 @@ class Lexer: 'endif', 'and', 'or', 'not', 'foreach', 'endforeach', 'in', 'continue', 'break'} self.future_keywords = {'return'} + self.in_unit_test = 'MESON_RUNNING_IN_PROJECT_TESTS' in os.environ + if self.in_unit_test: + self.keywords.update({'testcase', 'endtestcase'}) self.token_specification = [ # Need to be sorted longest to shortest. - ('ignore', re.compile(r'[ \t]')), + ('whitespace', re.compile(r'[ \t]+')), ('multiline_fstring', re.compile(r"f'''(.|\n)*?'''", re.M)), ('fstring', re.compile(r"f'([^'\\]|(\\.))*'")), ('id', re.compile('[_a-zA-Z][_0-9a-zA-Z]*')), ('number', re.compile(r'0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|0|[1-9]\d*')), - ('eol_cont', re.compile(r'\\\n')), + ('eol_cont', re.compile(r'\\[ \t]*(#.*)?\n')), ('eol', re.compile(r'\n')), ('multiline_string', re.compile(r"'''(.|\n)*?'''", re.M)), ('comment', re.compile(r'#.*')), @@ -162,7 +165,7 @@ class Lexer: col = 0 while loc < len(self.code): matched = False - value = None # type: T.Union[str, bool, int] + value: str = '' for (tid, reg) in self.token_specification: mo = reg.match(self.code, loc) if mo: @@ -174,10 +177,8 @@ class Lexer: loc = mo.end() span_end = loc bytespan = (span_start, span_end) - match_text = mo.group() - if tid in {'ignore', 'comment'}: - break - elif tid == 'lparen': + value = mo.group() + if tid == 'lparen': par_count += 1 elif tid == 'rparen': par_count -= 1 @@ -192,75 +193,61 @@ class Lexer: elif tid == 'dblquote': raise ParseException('Double quotes are not supported. Use single quotes.', self.getline(line_start), lineno, col) elif tid in {'string', 'fstring'}: - # Handle here and not on the regexp to give a better error message. - if match_text.find("\n") != -1: - msg = ParseException("Newline character in a string detected, use ''' (three single quotes) " - "for multiline strings instead.\n" - "This will become a hard error in a future Meson release.", - self.getline(line_start), lineno, col) - mlog.warning(msg, location=BaseNode(lineno, col, filename)) - value = match_text[2 if tid == 'fstring' else 1:-1] - try: - value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value) - except MesonUnicodeDecodeError as err: - raise MesonException(f"Failed to parse escape sequence: '{err.match}' in string:\n {match_text}") + if value.find("\n") != -1: + msg = ("Newline character in a string detected, use ''' (three single quotes) " + "for multiline strings instead.\n" + "This will become a hard error in a future Meson release.") + mlog.warning(mlog.code_line(msg, self.getline(line_start), col), location=BaseNode(lineno, col, filename)) + value = value[2 if tid == 'fstring' else 1:-1] elif tid in {'multiline_string', 'multiline_fstring'}: - # For multiline strings, parse out the value and pass - # through the normal string logic. - # For multiline format strings, we have to emit a - # different AST node so we can add a feature check, - # but otherwise, it follows the normal fstring logic. - if tid == 'multiline_string': - value = match_text[3:-3] - tid = 'string' - else: - value = match_text[4:-3] - lines = match_text.split('\n') + value = value[4 if tid == 'multiline_fstring' else 3:-3] + lines = value.split('\n') if len(lines) > 1: lineno += len(lines) - 1 line_start = mo.end() - len(lines[-1]) - elif tid == 'number': - value = int(match_text, base=0) elif tid == 'eol_cont': lineno += 1 line_start = loc - break + tid = 'whitespace' elif tid == 'eol': lineno += 1 line_start = loc if par_count > 0 or bracket_count > 0 or curl_count > 0: - break + tid = 'whitespace' elif tid == 'id': - if match_text in self.keywords: - tid = match_text + if value in self.keywords: + tid = value else: - if match_text in self.future_keywords: - mlog.warning(f"Identifier '{match_text}' will become a reserved keyword in a future release. Please rename it.", - location=types.SimpleNamespace(filename=filename, lineno=lineno)) - value = match_text + if value in self.future_keywords: + mlog.warning(f"Identifier '{value}' will become a reserved keyword in a future release. Please rename it.", + location=BaseNode(lineno, col, filename)) yield Token(tid, filename, curline_start, curline, col, bytespan, value) break if not matched: raise ParseException('lexer', self.getline(line_start), lineno, col) -@dataclass(eq=False) +@dataclass class BaseNode: lineno: int colno: int - filename: str - end_lineno: T.Optional[int] = None - end_colno: T.Optional[int] = None + filename: str = field(hash=False) + end_lineno: int = field(hash=False) + end_colno: int = field(hash=False) + whitespaces: T.Optional[WhitespaceNode] = field(hash=False) - def __post_init__(self) -> None: - if self.end_lineno is None: - self.end_lineno = self.lineno - if self.end_colno is None: - self.end_colno = self.colno + def __init__(self, lineno: int, colno: int, filename: str, + end_lineno: T.Optional[int] = None, end_colno: T.Optional[int] = None) -> None: + self.lineno = lineno + self.colno = colno + self.filename = filename + self.end_lineno = end_lineno if end_lineno is not None else lineno + self.end_colno = end_colno if end_colno is not None else colno + self.whitespaces = None # Attributes for the visitors - self.level = 0 # type: int - self.ast_id = '' # type: str - self.condition_level = 0 # type: int + self.level = 0 + self.ast_id = '' + self.condition_level = 0 def accept(self, visitor: 'AstVisitor') -> None: fname = 'visit_{}'.format(type(self).__name__) @@ -269,49 +256,79 @@ class BaseNode: if callable(func): func(self) + def append_whitespaces(self, token: Token) -> None: + if self.whitespaces is None: + self.whitespaces = WhitespaceNode(token) + else: + self.whitespaces.append(token) + + +@dataclass(unsafe_hash=True) +class WhitespaceNode(BaseNode): + + value: str + + def __init__(self, token: Token[str]): + super().__init__(token.lineno, token.colno, token.filename) + self.value = '' + self.append(token) + + def append(self, token: Token[str]) -> None: + self.value += token.value + +@dataclass(unsafe_hash=True) class ElementaryNode(T.Generic[TV_TokenTypes], BaseNode): + + value: TV_TokenTypes + bytespan: T.Tuple[int, int] = field(hash=False) + def __init__(self, token: Token[TV_TokenTypes]): super().__init__(token.lineno, token.colno, token.filename) - self.value = token.value # type: TV_TokenTypes - self.bytespan = token.bytespan # type: T.Tuple[int, int] + self.value = token.value + self.bytespan = token.bytespan class BooleanNode(ElementaryNode[bool]): - def __init__(self, token: Token[bool]): - super().__init__(token) - assert isinstance(self.value, bool) + pass class IdNode(ElementaryNode[str]): - def __init__(self, token: Token[str]): - super().__init__(token) - assert isinstance(self.value, str) - - def __str__(self) -> str: - return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) + pass +@dataclass(unsafe_hash=True) class NumberNode(ElementaryNode[int]): - def __init__(self, token: Token[int]): - super().__init__(token) - assert isinstance(self.value, int) -class StringNode(ElementaryNode[str]): + raw_value: str = field(hash=False) + def __init__(self, token: Token[str]): + BaseNode.__init__(self, token.lineno, token.colno, token.filename) + self.raw_value = token.value + self.value = int(token.value, base=0) + self.bytespan = token.bytespan + +class BaseStringNode(ElementaryNode[str]): + pass + +@dataclass(unsafe_hash=True) +class StringNode(BaseStringNode): + + raw_value: str = field(hash=False) + + def __init__(self, token: Token[str], escape: bool = True): super().__init__(token) - assert isinstance(self.value, str) + self.value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, token.value) if escape else token.value + self.raw_value = token.value + +class FormatStringNode(StringNode): + pass - def __str__(self) -> str: - return "String node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) +@dataclass(unsafe_hash=True) +class MultilineStringNode(BaseStringNode): -class FormatStringNode(ElementaryNode[str]): def __init__(self, token: Token[str]): super().__init__(token) - assert isinstance(self.value, str) + self.value = token.value - def __str__(self) -> str: - return f"Format string node: '{self.value}' ({self.lineno}, {self.colno})." - -class MultilineFormatStringNode(FormatStringNode): - def __str__(self) -> str: - return f"Multiline Format string node: '{self.value}' ({self.lineno}, {self.colno})." +class MultilineFormatStringNode(MultilineStringNode): + pass class ContinueNode(ElementaryNode): pass @@ -319,12 +336,23 @@ class ContinueNode(ElementaryNode): class BreakNode(ElementaryNode): pass +class SymbolNode(ElementaryNode[str]): + pass + +@dataclass(unsafe_hash=True) class ArgumentNode(BaseNode): + + arguments: T.List[BaseNode] = field(hash=False) + commas: T.List[SymbolNode] = field(hash=False) + columns: T.List[SymbolNode] = field(hash=False) + kwargs: T.Dict[BaseNode, BaseNode] = field(hash=False) + def __init__(self, token: Token[TV_TokenTypes]): super().__init__(token.lineno, token.colno, token.filename) - self.arguments = [] # type: T.List[BaseNode] - self.commas = [] # type: T.List[Token[TV_TokenTypes]] - self.kwargs = {} # type: T.Dict[BaseNode, BaseNode] + self.arguments = [] + self.commas = [] + self.columns = [] + self.kwargs = {} self.order_error = False def prepend(self, statement: BaseNode) -> None: @@ -360,132 +388,293 @@ class ArgumentNode(BaseNode): def __len__(self) -> int: return self.num_args() # Fixme +@dataclass(unsafe_hash=True) class ArrayNode(BaseNode): - def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): - super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) - self.args = args # type: ArgumentNode + lbracket: SymbolNode + args: ArgumentNode + rbracket: SymbolNode + + def __init__(self, lbracket: SymbolNode, args: ArgumentNode, rbracket: SymbolNode): + super().__init__(lbracket.lineno, lbracket.colno, args.filename, end_lineno=rbracket.lineno, end_colno=rbracket.colno+1) + self.lbracket = lbracket + self.args = args + self.rbracket = rbracket + +@dataclass(unsafe_hash=True) class DictNode(BaseNode): - def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): - super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) + + lcurl: SymbolNode + args: ArgumentNode + rcurl: SymbolNode + + def __init__(self, lcurl: SymbolNode, args: ArgumentNode, rcurl: SymbolNode): + super().__init__(lcurl.lineno, lcurl.colno, args.filename, end_lineno=rcurl.lineno, end_colno=rcurl.colno+1) + self.lcurl = lcurl self.args = args + self.rcurl = rcurl class EmptyNode(BaseNode): - def __init__(self, lineno: int, colno: int, filename: str): - super().__init__(lineno, colno, filename) - self.value = None + pass -class OrNode(BaseNode): - def __init__(self, left: BaseNode, right: BaseNode): - super().__init__(left.lineno, left.colno, left.filename) - self.left = left # type: BaseNode - self.right = right # type: BaseNode +@dataclass(unsafe_hash=True) +class BinaryOperatorNode(BaseNode): -class AndNode(BaseNode): - def __init__(self, left: BaseNode, right: BaseNode): - super().__init__(left.lineno, left.colno, left.filename) - self.left = left # type: BaseNode - self.right = right # type: BaseNode + left: BaseNode + operator: SymbolNode + right: BaseNode -class ComparisonNode(BaseNode): - def __init__(self, ctype: str, left: BaseNode, right: BaseNode): + def __init__(self, left: BaseNode, operator: SymbolNode, right: BaseNode): super().__init__(left.lineno, left.colno, left.filename) - self.left = left # type: BaseNode - self.right = right # type: BaseNode - self.ctype = ctype # type: str + self.left = left + self.operator = operator + self.right = right -class ArithmeticNode(BaseNode): - def __init__(self, operation: str, left: BaseNode, right: BaseNode): - super().__init__(left.lineno, left.colno, left.filename) - self.left = left # type: BaseNode - self.right = right # type: BaseNode - self.operation = operation # type: str +class OrNode(BinaryOperatorNode): + pass + +class AndNode(BinaryOperatorNode): + pass + +@dataclass(unsafe_hash=True) +class ComparisonNode(BinaryOperatorNode): + + ctype: COMPARISONS + + def __init__(self, ctype: COMPARISONS, left: BaseNode, operator: SymbolNode, right: BaseNode): + super().__init__(left, operator, right) + self.ctype = ctype + +@dataclass(unsafe_hash=True) +class ArithmeticNode(BinaryOperatorNode): + + # TODO: use a Literal for operation + operation: str -class NotNode(BaseNode): - def __init__(self, token: Token[TV_TokenTypes], value: BaseNode): + def __init__(self, operation: str, left: BaseNode, operator: SymbolNode, right: BaseNode): + super().__init__(left, operator, right) + self.operation = operation + +@dataclass(unsafe_hash=True) +class UnaryOperatorNode(BaseNode): + + operator: SymbolNode + value: BaseNode + + def __init__(self, token: Token[TV_TokenTypes], operator: SymbolNode, value: BaseNode): super().__init__(token.lineno, token.colno, token.filename) - self.value = value # type: BaseNode + self.operator = operator + self.value = value + +class NotNode(UnaryOperatorNode): + pass + +class UMinusNode(UnaryOperatorNode): + pass +@dataclass(unsafe_hash=True) class CodeBlockNode(BaseNode): + + pre_whitespaces: T.Optional[WhitespaceNode] = field(hash=False) + lines: T.List[BaseNode] = field(hash=False) + def __init__(self, token: Token[TV_TokenTypes]): super().__init__(token.lineno, token.colno, token.filename) - self.lines = [] # type: T.List[BaseNode] + self.pre_whitespaces = None + self.lines = [] + + def append_whitespaces(self, token: Token) -> None: + if self.lines: + self.lines[-1].append_whitespaces(token) + elif self.pre_whitespaces is None: + self.pre_whitespaces = WhitespaceNode(token) + else: + self.pre_whitespaces.append(token) +@dataclass(unsafe_hash=True) class IndexNode(BaseNode): - def __init__(self, iobject: BaseNode, index: BaseNode): + + iobject: BaseNode + lbracket: SymbolNode + index: BaseNode + rbracket: SymbolNode + + def __init__(self, iobject: BaseNode, lbracket: SymbolNode, index: BaseNode, rbracket: SymbolNode): super().__init__(iobject.lineno, iobject.colno, iobject.filename) - self.iobject = iobject # type: BaseNode - self.index = index # type: BaseNode + self.iobject = iobject + self.lbracket = lbracket + self.index = index + self.rbracket = rbracket +@dataclass(unsafe_hash=True) class MethodNode(BaseNode): - def __init__(self, filename: str, lineno: int, colno: int, source_object: BaseNode, name: str, args: ArgumentNode): - super().__init__(lineno, colno, filename) - self.source_object = source_object # type: BaseNode - self.name = name # type: str - assert isinstance(self.name, str) - self.args = args # type: ArgumentNode + source_object: BaseNode + dot: SymbolNode + name: IdNode + lpar: SymbolNode + args: ArgumentNode + rpar: SymbolNode + + def __init__(self, source_object: BaseNode, dot: SymbolNode, name: IdNode, lpar: SymbolNode, args: ArgumentNode, rpar: SymbolNode): + super().__init__(name.lineno, name.colno, name.filename, end_lineno=rpar.lineno, end_colno=rpar.colno+1) + self.source_object = source_object + self.dot = dot + self.name = name + self.lpar = lpar + self.args = args + self.rpar = rpar + +@dataclass(unsafe_hash=True) class FunctionNode(BaseNode): - def __init__(self, filename: str, lineno: int, colno: int, end_lineno: int, end_colno: int, func_name: str, args: ArgumentNode): - super().__init__(lineno, colno, filename, end_lineno=end_lineno, end_colno=end_colno) - self.func_name = func_name # type: str - assert isinstance(func_name, str) - self.args = args # type: ArgumentNode + func_name: IdNode + lpar: SymbolNode + args: ArgumentNode + rpar: SymbolNode + + def __init__(self, func_name: IdNode, lpar: SymbolNode, args: ArgumentNode, rpar: SymbolNode): + super().__init__(func_name.lineno, func_name.colno, func_name.filename, end_lineno=rpar.end_lineno, end_colno=rpar.end_colno+1) + self.func_name = func_name + self.lpar = lpar + self.args = args + self.rpar = rpar + +@dataclass(unsafe_hash=True) class AssignmentNode(BaseNode): - def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): - super().__init__(lineno, colno, filename) - self.var_name = var_name # type: str - assert isinstance(var_name, str) - self.value = value # type: BaseNode - -class PlusAssignmentNode(BaseNode): - def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): - super().__init__(lineno, colno, filename) - self.var_name = var_name # type: str - assert isinstance(var_name, str) - self.value = value # type: BaseNode + var_name: IdNode + operator: SymbolNode + value: BaseNode + + def __init__(self, var_name: IdNode, operator: SymbolNode, value: BaseNode): + super().__init__(var_name.lineno, var_name.colno, var_name.filename) + self.var_name = var_name + self.operator = operator + self.value = value + +class PlusAssignmentNode(AssignmentNode): + pass + +@dataclass(unsafe_hash=True) class ForeachClauseNode(BaseNode): - def __init__(self, token: Token, varnames: T.List[str], items: BaseNode, block: CodeBlockNode): - super().__init__(token.lineno, token.colno, token.filename) - self.varnames = varnames # type: T.List[str] - self.items = items # type: BaseNode - self.block = block # type: CodeBlockNode + foreach_: SymbolNode = field(hash=False) + varnames: T.List[IdNode] = field(hash=False) + commas: T.List[SymbolNode] = field(hash=False) + column: SymbolNode = field(hash=False) + items: BaseNode + block: CodeBlockNode + endforeach: SymbolNode = field(hash=False) + + def __init__(self, foreach_: SymbolNode, varnames: T.List[IdNode], commas: T.List[SymbolNode], column: SymbolNode, items: BaseNode, block: CodeBlockNode, endforeach: SymbolNode): + super().__init__(foreach_.lineno, foreach_.colno, foreach_.filename) + self.foreach_ = foreach_ + self.varnames = varnames + self.commas = commas + self.column = column + self.items = items + self.block = block + self.endforeach = endforeach + + +@dataclass(unsafe_hash=True) class IfNode(BaseNode): - def __init__(self, linenode: BaseNode, condition: BaseNode, block: CodeBlockNode): + + if_: SymbolNode + condition: BaseNode + block: CodeBlockNode + + def __init__(self, linenode: BaseNode, if_node: SymbolNode, condition: BaseNode, block: CodeBlockNode): super().__init__(linenode.lineno, linenode.colno, linenode.filename) - self.condition = condition # type: BaseNode - self.block = block # type: CodeBlockNode + self.if_ = if_node + self.condition = condition + self.block = block +@dataclass(unsafe_hash=True) +class ElseNode(BaseNode): + + else_: SymbolNode + block: CodeBlockNode + + def __init__(self, else_: SymbolNode, block: CodeBlockNode): + super().__init__(block.lineno, block.colno, block.filename) + self.else_ = else_ + self.block = block + +@dataclass(unsafe_hash=True) class IfClauseNode(BaseNode): + + ifs: T.List[IfNode] = field(hash=False) + elseblock: T.Union[EmptyNode, ElseNode] + endif: SymbolNode + def __init__(self, linenode: BaseNode): super().__init__(linenode.lineno, linenode.colno, linenode.filename) - self.ifs = [] # type: T.List[IfNode] - self.elseblock = None # type: T.Union[EmptyNode, CodeBlockNode] + self.ifs = [] + self.elseblock = EmptyNode(linenode.lineno, linenode.colno, linenode.filename) + self.endif = None + +@dataclass(unsafe_hash=True) +class TestCaseClauseNode(BaseNode): + + testcase: SymbolNode + condition: BaseNode + block: CodeBlockNode + endtestcase: SymbolNode -class UMinusNode(BaseNode): - def __init__(self, current_location: Token, value: BaseNode): - super().__init__(current_location.lineno, current_location.colno, current_location.filename) - self.value = value # type: BaseNode + def __init__(self, testcase: SymbolNode, condition: BaseNode, block: CodeBlockNode, endtestcase: SymbolNode): + super().__init__(condition.lineno, condition.colno, condition.filename) + self.testcase = testcase + self.condition = condition + self.block = block + self.endtestcase = endtestcase +@dataclass(unsafe_hash=True) class TernaryNode(BaseNode): - def __init__(self, condition: BaseNode, trueblock: BaseNode, falseblock: BaseNode): + + condition: BaseNode + questionmark: SymbolNode + trueblock: BaseNode + column: SymbolNode + falseblock: BaseNode + + def __init__(self, condition: BaseNode, questionmark: SymbolNode, trueblock: BaseNode, column: SymbolNode, falseblock: BaseNode): super().__init__(condition.lineno, condition.colno, condition.filename) - self.condition = condition # type: BaseNode - self.trueblock = trueblock # type: BaseNode - self.falseblock = falseblock # type: BaseNode - -comparison_map = {'equal': '==', - 'nequal': '!=', - 'lt': '<', - 'le': '<=', - 'gt': '>', - 'ge': '>=', - 'in': 'in', - 'notin': 'not in', - } + self.condition = condition + self.questionmark = questionmark + self.trueblock = trueblock + self.column = column + self.falseblock = falseblock + + +@dataclass(unsafe_hash=True) +class ParenthesizedNode(BaseNode): + + lpar: SymbolNode = field(hash=False) + inner: BaseNode + rpar: SymbolNode = field(hash=False) + + def __init__(self, lpar: SymbolNode, inner: BaseNode, rpar: SymbolNode): + super().__init__(lpar.lineno, lpar.colno, inner.filename, end_lineno=rpar.lineno, end_colno=rpar.colno+1) + self.lpar = lpar + self.inner = inner + self.rpar = rpar + + +if T.TYPE_CHECKING: + COMPARISONS = Literal['==', '!=', '<', '<=', '>=', '>', 'in', 'notin'] + +comparison_map: T.Mapping[str, COMPARISONS] = { + 'equal': '==', + 'nequal': '!=', + 'lt': '<', + 'le': '<=', + 'gt': '>', + 'ge': '>=', + 'in': 'in', + 'not in': 'notin', +} # Recursive descent parser for Meson's definition language. # Very basic apart from the fact that we have many precedence @@ -506,13 +695,31 @@ class Parser: def __init__(self, code: str, filename: str): self.lexer = Lexer(code) self.stream = self.lexer.lex(filename) - self.current = Token('eof', '', 0, 0, 0, (0, 0), None) # type: Token + self.current: Token = Token('eof', '', 0, 0, 0, (0, 0), None) + self.previous = self.current + self.current_ws: T.List[Token] = [] + self.getsym() self.in_ternary = False + def create_node(self, node_type: T.Type[BaseNodeT], *args: T.Any, **kwargs: T.Any) -> BaseNodeT: + node = node_type(*args, **kwargs) + for ws_token in self.current_ws: + node.append_whitespaces(ws_token) + self.current_ws = [] + return node + def getsym(self) -> None: + self.previous = self.current try: self.current = next(self.stream) + + while self.current.tid in {'eol', 'comment', 'whitespace'}: + self.current_ws.append(self.current) + if self.current.tid == 'eol': + break + self.current = next(self.stream) + except StopIteration: self.current = Token('eof', '', self.current.line_start, self.current.lineno, self.current.colno + self.current.bytespan[1] - self.current.bytespan[0], (0, 0), None) @@ -544,7 +751,11 @@ class Parser: def parse(self) -> CodeBlockNode: block = self.codeblock() - self.expect('eof') + try: + self.expect('eof') + except ParseException as e: + e.ast = block + raise return block def statement(self) -> BaseNode: @@ -553,55 +764,75 @@ class Parser: def e1(self) -> BaseNode: left = self.e2() if self.accept('plusassign'): + operator = self.create_node(SymbolNode, self.previous) value = self.e1() if not isinstance(left, IdNode): raise ParseException('Plusassignment target must be an id.', self.getline(), left.lineno, left.colno) assert isinstance(left.value, str) - return PlusAssignmentNode(left.filename, left.lineno, left.colno, left.value, value) + return self.create_node(PlusAssignmentNode, left, operator, value) elif self.accept('assign'): + operator = self.create_node(SymbolNode, self.previous) value = self.e1() if not isinstance(left, IdNode): raise ParseException('Assignment target must be an id.', self.getline(), left.lineno, left.colno) assert isinstance(left.value, str) - return AssignmentNode(left.filename, left.lineno, left.colno, left.value, value) + return self.create_node(AssignmentNode, left, operator, value) elif self.accept('questionmark'): if self.in_ternary: raise ParseException('Nested ternary operators are not allowed.', self.getline(), left.lineno, left.colno) + + qm_node = self.create_node(SymbolNode, self.previous) self.in_ternary = True trueblock = self.e1() self.expect('colon') + column_node = self.create_node(SymbolNode, self.previous) falseblock = self.e1() self.in_ternary = False - return TernaryNode(left, trueblock, falseblock) + return self.create_node(TernaryNode, left, qm_node, trueblock, column_node, falseblock) return left def e2(self) -> BaseNode: left = self.e3() while self.accept('or'): + operator = self.create_node(SymbolNode, self.previous) if isinstance(left, EmptyNode): raise ParseException('Invalid or clause.', self.getline(), left.lineno, left.colno) - left = OrNode(left, self.e3()) + left = self.create_node(OrNode, left, operator, self.e3()) return left def e3(self) -> BaseNode: left = self.e4() while self.accept('and'): + operator = self.create_node(SymbolNode, self.previous) if isinstance(left, EmptyNode): raise ParseException('Invalid and clause.', self.getline(), left.lineno, left.colno) - left = AndNode(left, self.e4()) + left = self.create_node(AndNode, left, operator, self.e4()) return left def e4(self) -> BaseNode: left = self.e5() for nodename, operator_type in comparison_map.items(): if self.accept(nodename): - return ComparisonNode(operator_type, left, self.e5()) - if self.accept('not') and self.accept('in'): - return ComparisonNode('notin', left, self.e5()) + operator = self.create_node(SymbolNode, self.previous) + return self.create_node(ComparisonNode, operator_type, left, operator, self.e5()) + if self.accept('not'): + ws = self.current_ws.copy() + not_token = self.previous + if self.accept('in'): + in_token = self.previous + self.current_ws = self.current_ws[len(ws):] # remove whitespaces between not and in + temp_node = EmptyNode(in_token.lineno, in_token.colno, in_token.filename) + for w in ws: + temp_node.append_whitespaces(w) + + not_token.bytespan = (not_token.bytespan[0], in_token.bytespan[1]) + not_token.value += temp_node.whitespaces.value + in_token.value + operator = self.create_node(SymbolNode, not_token) + return self.create_node(ComparisonNode, 'notin', left, operator, self.e5()) return left def e5(self) -> BaseNode: @@ -616,7 +847,8 @@ class Parser: while True: op = self.accept_any(tuple(op_map.keys())) if op: - left = ArithmeticNode(op_map[op], left, self.e5muldiv()) + operator = self.create_node(SymbolNode, self.previous) + left = self.create_node(ArithmeticNode, op_map[op], left, operator, self.e5muldiv()) else: break return left @@ -631,29 +863,34 @@ class Parser: while True: op = self.accept_any(tuple(op_map.keys())) if op: - left = ArithmeticNode(op_map[op], left, self.e6()) + operator = self.create_node(SymbolNode, self.previous) + left = self.create_node(ArithmeticNode, op_map[op], left, operator, self.e6()) else: break return left def e6(self) -> BaseNode: if self.accept('not'): - return NotNode(self.current, self.e7()) + operator = self.create_node(SymbolNode, self.previous) + return self.create_node(NotNode, self.current, operator, self.e7()) if self.accept('dash'): - return UMinusNode(self.current, self.e7()) + operator = self.create_node(SymbolNode, self.previous) + return self.create_node(UMinusNode, self.current, operator, self.e7()) return self.e7() def e7(self) -> BaseNode: left = self.e8() block_start = self.current if self.accept('lparen'): + lpar = self.create_node(SymbolNode, block_start) args = self.args() self.block_expect('rparen', block_start) + rpar = self.create_node(SymbolNode, self.previous) if not isinstance(left, IdNode): raise ParseException('Function call must be applied to plain id', self.getline(), left.lineno, left.colno) assert isinstance(left.value, str) - left = FunctionNode(left.filename, left.lineno, left.colno, self.current.lineno, self.current.colno, left.value, args) + left = self.create_node(FunctionNode, left, lpar, args, rpar) go_again = True while go_again: go_again = False @@ -668,17 +905,23 @@ class Parser: def e8(self) -> BaseNode: block_start = self.current if self.accept('lparen'): + lpar = self.create_node(SymbolNode, block_start) e = self.statement() self.block_expect('rparen', block_start) - return e + rpar = self.create_node(SymbolNode, self.previous) + return ParenthesizedNode(lpar, e, rpar) elif self.accept('lbracket'): + lbracket = self.create_node(SymbolNode, block_start) args = self.args() self.block_expect('rbracket', block_start) - return ArrayNode(args, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno) + rbracket = self.create_node(SymbolNode, self.previous) + return self.create_node(ArrayNode, lbracket, args, rbracket) elif self.accept('lcurl'): + lcurl = self.create_node(SymbolNode, block_start) key_values = self.key_values() self.block_expect('rcurl', block_start) - return DictNode(key_values, block_start.lineno, block_start.colno, self.current.lineno, self.current.colno) + rcurl = self.create_node(SymbolNode, self.previous) + return self.create_node(DictNode, lcurl, key_values, rcurl) else: return self.e9() @@ -686,33 +929,35 @@ class Parser: t = self.current if self.accept('true'): t.value = True - return BooleanNode(t) + return self.create_node(BooleanNode, t) if self.accept('false'): t.value = False - return BooleanNode(t) + return self.create_node(BooleanNode, t) if self.accept('id'): - return IdNode(t) + return self.create_node(IdNode, t) if self.accept('number'): - return NumberNode(t) + return self.create_node(NumberNode, t) if self.accept('string'): - return StringNode(t) + return self.create_node(StringNode, t) if self.accept('fstring'): - return FormatStringNode(t) + return self.create_node(FormatStringNode, t) + if self.accept('multiline_string'): + return self.create_node(MultilineStringNode, t) if self.accept('multiline_fstring'): - return MultilineFormatStringNode(t) + return self.create_node(MultilineFormatStringNode, t) return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) def key_values(self) -> ArgumentNode: - s = self.statement() # type: BaseNode - a = ArgumentNode(self.current) + s = self.statement() + a = self.create_node(ArgumentNode, self.current) while not isinstance(s, EmptyNode): if self.accept('colon'): + a.columns.append(self.create_node(SymbolNode, self.previous)) a.set_kwarg_no_check(s, self.statement()) - potential = self.current if not self.accept('comma'): return a - a.commas.append(potential) + a.commas.append(self.create_node(SymbolNode, self.previous)) else: raise ParseException('Only key:value pairs are valid in dict construction.', self.getline(), s.lineno, s.colno) @@ -720,23 +965,22 @@ class Parser: return a def args(self) -> ArgumentNode: - s = self.statement() # type: BaseNode - a = ArgumentNode(self.current) + s = self.statement() + a = self.create_node(ArgumentNode, self.current) while not isinstance(s, EmptyNode): - potential = self.current if self.accept('comma'): - a.commas.append(potential) + a.commas.append(self.create_node(SymbolNode, self.previous)) a.append(s) elif self.accept('colon'): + a.columns.append(self.create_node(SymbolNode, self.previous)) if not isinstance(s, IdNode): raise ParseException('Dictionary key must be a plain identifier.', self.getline(), s.lineno, s.colno) a.set_kwarg(s, self.statement()) - potential = self.current if not self.accept('comma'): return a - a.commas.append(potential) + a.commas.append(self.create_node(SymbolNode, self.previous)) else: a.append(s) return a @@ -744,65 +988,88 @@ class Parser: return a def method_call(self, source_object: BaseNode) -> MethodNode: + dot = self.create_node(SymbolNode, self.previous) methodname = self.e9() if not isinstance(methodname, IdNode): + if isinstance(source_object, NumberNode) and isinstance(methodname, NumberNode): + raise ParseException('meson does not support float numbers', + self.getline(), source_object.lineno, source_object.colno) raise ParseException('Method name must be plain id', self.getline(), self.current.lineno, self.current.colno) assert isinstance(methodname.value, str) self.expect('lparen') + lpar = self.create_node(SymbolNode, self.previous) args = self.args() + rpar = self.create_node(SymbolNode, self.current) self.expect('rparen') - method = MethodNode(methodname.filename, methodname.lineno, methodname.colno, source_object, methodname.value, args) + method = self.create_node(MethodNode, source_object, dot, methodname, lpar, args, rpar) if self.accept('dot'): return self.method_call(method) return method def index_call(self, source_object: BaseNode) -> IndexNode: + lbracket = self.create_node(SymbolNode, self.previous) index_statement = self.statement() self.expect('rbracket') - return IndexNode(source_object, index_statement) + rbracket = self.create_node(SymbolNode, self.previous) + return self.create_node(IndexNode, source_object, lbracket, index_statement, rbracket) def foreachblock(self) -> ForeachClauseNode: - t = self.current + foreach_ = self.create_node(SymbolNode, self.previous) self.expect('id') - assert isinstance(t.value, str) - varname = t - varnames = [t.value] # type: T.List[str] + assert isinstance(self.previous.value, str) + varnames = [self.create_node(IdNode, self.previous)] + commas = [] if self.accept('comma'): - t = self.current + commas.append(self.create_node(SymbolNode, self.previous)) self.expect('id') - assert isinstance(t.value, str) - varnames.append(t.value) + assert isinstance(self.previous.value, str) + varnames.append(self.create_node(IdNode, self.previous)) self.expect('colon') + column = self.create_node(SymbolNode, self.previous) items = self.statement() block = self.codeblock() - return ForeachClauseNode(varname, varnames, items, block) + endforeach = self.create_node(SymbolNode, self.current) + return self.create_node(ForeachClauseNode, foreach_, varnames, commas, column, items, block, endforeach) def ifblock(self) -> IfClauseNode: + if_node = self.create_node(SymbolNode, self.previous) condition = self.statement() - clause = IfClauseNode(condition) + clause = self.create_node(IfClauseNode, condition) self.expect('eol') block = self.codeblock() - clause.ifs.append(IfNode(clause, condition, block)) + clause.ifs.append(self.create_node(IfNode, clause, if_node, condition, block)) self.elseifblock(clause) clause.elseblock = self.elseblock() + clause.endif = self.create_node(SymbolNode, self.current) return clause def elseifblock(self, clause: IfClauseNode) -> None: while self.accept('elif'): + elif_ = self.create_node(SymbolNode, self.previous) s = self.statement() self.expect('eol') b = self.codeblock() - clause.ifs.append(IfNode(s, s, b)) + clause.ifs.append(self.create_node(IfNode, s, elif_, s, b)) - def elseblock(self) -> T.Union[CodeBlockNode, EmptyNode]: + def elseblock(self) -> T.Union[ElseNode, EmptyNode]: if self.accept('else'): + else_ = self.create_node(SymbolNode, self.previous) self.expect('eol') - return self.codeblock() + block = self.codeblock() + return ElseNode(else_, block) return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) + def testcaseblock(self) -> TestCaseClauseNode: + testcase = self.create_node(SymbolNode, self.previous) + condition = self.statement() + self.expect('eol') + block = self.codeblock() + endtestcase = SymbolNode(self.current) + return self.create_node(TestCaseClauseNode, testcase, condition, block, endtestcase) + def line(self) -> BaseNode: block_start = self.current if self.current == 'eol': @@ -816,17 +1083,39 @@ class Parser: self.block_expect('endforeach', block_start) return forblock if self.accept('continue'): - return ContinueNode(self.current) + return self.create_node(ContinueNode, self.current) if self.accept('break'): - return BreakNode(self.current) + return self.create_node(BreakNode, self.current) + if self.lexer.in_unit_test and self.accept('testcase'): + block = self.testcaseblock() + self.block_expect('endtestcase', block_start) + return block return self.statement() def codeblock(self) -> CodeBlockNode: - block = CodeBlockNode(self.current) + block = self.create_node(CodeBlockNode, self.current) cond = True - while cond: - curline = self.line() - if not isinstance(curline, EmptyNode): - block.lines.append(curline) - cond = self.accept('eol') + + try: + while cond: + for ws_token in self.current_ws: + block.append_whitespaces(ws_token) + self.current_ws = [] + + curline = self.line() + + if not isinstance(curline, EmptyNode): + block.lines.append(curline) + + cond = self.accept('eol') + + except ParseException as e: + e.ast = block + raise + + # Remaining whitespaces will not be catched since there are no more nodes + for ws_token in self.current_ws: + block.append_whitespaces(ws_token) + self.current_ws = [] + return block diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index 43594dd..f8e242a 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -11,25 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -import typing as T -import time -import sys, stat -import datetime -import os.path -import platform +import argparse, datetime, glob, json, os, platform, shutil, sys, tempfile, time import cProfile as profile -import argparse -import tempfile -import shutil -import glob - -from . import environment, interpreter, mesonlib -from . import build -from . import mlog, coredata -from . import mintro -from .mesonlib import MesonException, MachineChoice -from .dependencies import PkgConfigDependency +from pathlib import Path +import typing as T + +from . import build, coredata, environment, interpreter, mesonlib, mintro, mlog +from .mesonlib import MesonException git_ignore_file = '''# This file is autogenerated by Meson. If you change or delete it, it won't be recreated. * @@ -41,6 +31,8 @@ syntax: glob ''' +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: coredata.register_builtin_arguments(parser) parser.add_argument('--native-file', @@ -51,10 +43,6 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: default=[], action='append', help='File describing cross compilation environment.') - parser.add_argument('--vsenv', action='store_true', - help='Setup Visual Studio environment even when other compilers are found, ' + - 'abort if Visual Studio is not found. This option has no effect on other ' + - 'platforms than Windows. Defaults to True when using "vs" backend.') parser.add_argument('-v', '--version', action='version', version=coredata.version) parser.add_argument('--profile-self', action='store_true', dest='profile', @@ -69,15 +57,15 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: help='Wipe build directory and reconfigure using previous command line options. ' + 'Useful when build directory got corrupted, or when rebuilding with a ' + 'newer version of meson.') + parser.add_argument('--clearcache', action='store_true', default=False, + help='Clear cached state (e.g. found dependencies). Since 1.3.0.') parser.add_argument('builddir', nargs='?', default=None) parser.add_argument('sourcedir', nargs='?', default=None) class MesonApp: def __init__(self, options: argparse.Namespace) -> None: - (self.source_dir, self.build_dir) = self.validate_dirs(options.builddir, - options.sourcedir, - options.reconfigure, - options.wipe) + self.options = options + (self.source_dir, self.build_dir) = self.validate_dirs() if options.wipe: # Make a copy of the cmd line file to make sure we can always # restore that file if anything bad happens. For example if @@ -88,9 +76,9 @@ class MesonApp: try: restore.append((shutil.copy(filename, d), filename)) except FileNotFoundError: - raise MesonException( - 'Cannot find cmd_line.txt. This is probably because this ' - 'build directory was configured with a meson version < 0.49.0.') + # validate_dirs() already verified that build_dir has + # a partial build or is empty. + pass coredata.read_cmd_line_file(self.build_dir, options) @@ -110,16 +98,15 @@ class MesonApp: os.makedirs(os.path.dirname(f), exist_ok=True) shutil.move(b, f) - self.options = options - def has_build_file(self, dirname: str) -> bool: fname = os.path.join(dirname, environment.build_filename) return os.path.exists(fname) - def validate_core_dirs(self, dir1: str, dir2: str) -> T.Tuple[str, str]: + def validate_core_dirs(self, dir1: T.Optional[str], dir2: T.Optional[str]) -> T.Tuple[str, str]: + invalid_msg_prefix = f'Neither source directory {dir1!r} nor build directory {dir2!r}' if dir1 is None: if dir2 is None: - if not os.path.exists('meson.build') and os.path.exists('../meson.build'): + if not self.has_build_file('.') and self.has_build_file('..'): dir2 = '..' else: raise MesonException('Must specify at least one directory name.') @@ -128,14 +115,16 @@ class MesonApp: dir2 = os.getcwd() ndir1 = os.path.abspath(os.path.realpath(dir1)) ndir2 = os.path.abspath(os.path.realpath(dir2)) - if not os.path.exists(ndir1): - os.makedirs(ndir1) - if not os.path.exists(ndir2): - os.makedirs(ndir2) - if not stat.S_ISDIR(os.stat(ndir1).st_mode): - raise MesonException(f'{dir1} is not a directory') - if not stat.S_ISDIR(os.stat(ndir2).st_mode): - raise MesonException(f'{dir2} is not a directory') + if not os.path.exists(ndir1) and not os.path.exists(ndir2): + raise MesonException(f'{invalid_msg_prefix} exist.') + try: + os.makedirs(ndir1, exist_ok=True) + except FileExistsError as e: + raise MesonException(f'{dir1} is not a directory') from e + try: + os.makedirs(ndir2, exist_ok=True) + except FileExistsError as e: + raise MesonException(f'{dir2} is not a directory') from e if os.path.samefile(ndir1, ndir2): # Fallback to textual compare if undefined entries found has_undefined = any((s.st_ino == 0 and s.st_dev == 0) for s in (os.stat(ndir1), os.stat(ndir2))) @@ -147,45 +136,51 @@ class MesonApp: return ndir1, ndir2 if self.has_build_file(ndir2): return ndir2, ndir1 - raise MesonException(f'Neither directory contains a build file {environment.build_filename}.') + raise MesonException(f'{invalid_msg_prefix} contain a build file {environment.build_filename}.') def add_vcs_ignore_files(self, build_dir: str) -> None: - if os.listdir(build_dir): - return with open(os.path.join(build_dir, '.gitignore'), 'w', encoding='utf-8') as ofile: ofile.write(git_ignore_file) with open(os.path.join(build_dir, '.hgignore'), 'w', encoding='utf-8') as ofile: ofile.write(hg_ignore_file) - def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) -> T.Tuple[str, str]: - (src_dir, build_dir) = self.validate_core_dirs(dir1, dir2) - self.add_vcs_ignore_files(build_dir) - priv_dir = os.path.join(build_dir, 'meson-private/coredata.dat') - if os.path.exists(priv_dir): - if not reconfigure and not wipe: - print('Directory already configured.\n' - '\nJust run your build command (e.g. ninja) and Meson will regenerate as necessary.\n' - 'If ninja fails, run "ninja reconfigure" or "meson setup --reconfigure"\n' - 'to force Meson to regenerate.\n' - '\nIf build failures persist, run "meson setup --wipe" to rebuild from scratch\n' - 'using the same options as passed when configuring the build.' - '\nTo change option values, run "meson configure" instead.') - raise SystemExit - else: - has_cmd_line_file = os.path.exists(coredata.get_cmd_line_file(build_dir)) - if (wipe and not has_cmd_line_file) or (not wipe and reconfigure): - raise SystemExit(f'Directory does not contain a valid build tree:\n{build_dir}') + def validate_dirs(self) -> T.Tuple[str, str]: + (src_dir, build_dir) = self.validate_core_dirs(self.options.builddir, self.options.sourcedir) + if Path(build_dir) in Path(src_dir).parents: + raise MesonException(f'Build directory {build_dir} cannot be a parent of source directory {src_dir}') + if not os.listdir(build_dir): + self.add_vcs_ignore_files(build_dir) + return src_dir, build_dir + priv_dir = os.path.join(build_dir, 'meson-private') + has_valid_build = os.path.exists(os.path.join(priv_dir, 'coredata.dat')) + has_partial_build = os.path.isdir(priv_dir) + if has_valid_build: + if not self.options.reconfigure and not self.options.wipe: + print('Directory already configured.\n\n' + 'Just run your build command (e.g. ninja) and Meson will regenerate as necessary.\n' + 'Run "meson setup --reconfigure to force Meson to regenerate.\n\n' + 'If build failures persist, run "meson setup --wipe" to rebuild from scratch\n' + 'using the same options as passed when configuring the build.') + if self.options.cmd_line_options: + from . import mconf + raise SystemExit(mconf.run_impl(self.options, build_dir)) + raise SystemExit(0) + elif not has_partial_build and self.options.wipe: + raise MesonException(f'Directory is not empty and does not contain a previous build:\n{build_dir}') return src_dir, build_dir - def generate(self) -> None: + # See class Backend's 'generate' for comments on capture args and returned dictionary. + def generate(self, capture: bool = False, vslite_ctx: T.Optional[dict] = None) -> T.Optional[dict]: env = environment.Environment(self.source_dir, self.build_dir, self.options) mlog.initialize(env.get_log_dir(), self.options.fatal_warnings) if self.options.profile: mlog.set_timestamp_start(time.monotonic()) + if self.options.clearcache: + env.coredata.clear_cache() with mesonlib.BuildDirLock(self.build_dir): - self._generate(env) + return self._generate(env, capture, vslite_ctx) - def _generate(self, env: environment.Environment) -> None: + def _generate(self, env: environment.Environment, capture: bool, vslite_ctx: T.Optional[dict]) -> T.Optional[dict]: # Get all user defined options, including options that have been defined # during a previous invocation or using meson configure. user_defined_options = argparse.Namespace(**vars(self.options)) @@ -206,10 +201,11 @@ class MesonApp: b = build.Build(env) intr = interpreter.Interpreter(b, user_defined_options=user_defined_options) - if env.is_cross_build(): - logger_fun = mlog.log - else: - logger_fun = mlog.debug + # Super hack because mlog.log and mlog.debug have different signatures, + # and there is currently no way to annotate them correctly, unionize them, or + # even to write `T.Callable[[*mlog.TV_Loggable], None]` + logger_fun = T.cast('T.Callable[[mlog.TV_Loggable, mlog.TV_Loggable], None]', + (mlog.log if env.is_cross_build() else mlog.debug)) build_machine = intr.builtin['build_machine'] host_machine = intr.builtin['host_machine'] target_machine = intr.builtin['target_machine'] @@ -224,7 +220,7 @@ class MesonApp: logger_fun('Target machine cpu:', mlog.bold(target_machine.cpu_method([], {}))) try: if self.options.profile: - fname = os.path.join(self.build_dir, 'meson-private', 'profile-interpreter.log') + fname = os.path.join(self.build_dir, 'meson-logs', 'profile-interpreter.log') profile.runctx('intr.run()', globals(), locals(), filename=fname) else: intr.run() @@ -233,6 +229,7 @@ class MesonApp: raise cdf: T.Optional[str] = None + captured_compile_args: T.Optional[dict] = None try: dumpfile = os.path.join(env.get_scratch_dir(), 'build.dat') # We would like to write coredata as late as possible since we use the existence of @@ -242,13 +239,17 @@ class MesonApp: # sync with the time that gets applied to any files. Thus, we dump this file as late as # possible, but before build files, and if any error occurs, delete it. cdf = env.dump_coredata() + + self.finalize_postconf_hooks(b, intr) if self.options.profile: fname = f'profile-{intr.backend.name}-backend.log' - fname = os.path.join(self.build_dir, 'meson-private', fname) - profile.runctx('intr.backend.generate()', globals(), locals(), filename=fname) + fname = os.path.join(self.build_dir, 'meson-logs', fname) + profile.runctx('gen_result = intr.backend.generate(capture, vslite_ctx)', globals(), locals(), filename=fname) + captured_compile_args = locals()['gen_result'] + assert captured_compile_args is None or isinstance(captured_compile_args, dict) else: - intr.backend.generate() - self._finalize_devenv(b, intr) + captured_compile_args = intr.backend.generate(capture, vslite_ctx) + build.save(b, dumpfile) if env.first_invocation: # Use path resolved by coredata because they could have been @@ -261,7 +262,7 @@ class MesonApp: # Generate an IDE introspection file with the same syntax as the already existing API if self.options.profile: - fname = os.path.join(self.build_dir, 'meson-private', 'profile-introspector.log') + fname = os.path.join(self.build_dir, 'meson-logs', 'profile-introspector.log') profile.runctx('mintro.generate_introspection_file(b, intr.backend)', globals(), locals(), filename=fname) else: mintro.generate_introspection_file(b, intr.backend) @@ -281,6 +282,17 @@ class MesonApp: 'Please consider using `meson devenv` instead. See https://github.com/mesonbuild/meson/pull/9243 ' 'for details.') + if self.options.profile: + fname = os.path.join(self.build_dir, 'meson-logs', 'profile-startup-modules.json') + mods = set(sys.modules.keys()) + mesonmods = {mod for mod in mods if (mod+'.').startswith('mesonbuild.')} + stdmods = sorted(mods - mesonmods) + data = {'stdlib': {'modules': stdmods, 'count': len(stdmods)}, 'meson': {'modules': sorted(mesonmods), 'count': len(mesonmods)}} + with open(fname, 'w', encoding='utf-8') as f: + json.dump(data, f) + + mlog.log("meson setup completed") # Display timestamp + except Exception as e: mintro.write_meson_info_file(b, [e]) if cdf is not None: @@ -291,13 +303,46 @@ class MesonApp: os.unlink(cdf) raise - def _finalize_devenv(self, b: build.Build, intr: interpreter.Interpreter) -> None: + return captured_compile_args + + def finalize_postconf_hooks(self, b: build.Build, intr: interpreter.Interpreter) -> None: b.devenv.append(intr.backend.get_devenv()) - b.devenv.append(PkgConfigDependency.get_env(intr.environment, MachineChoice.HOST, uninstalled=True)) for mod in intr.modules.values(): - devenv = mod.get_devenv() - if devenv: - b.devenv.append(devenv) + mod.postconf_hook(b) + +def run_genvslite_setup(options: argparse.Namespace) -> None: + # With --genvslite, we essentially want to invoke multiple 'setup' iterations. I.e. - + # meson setup ... builddirprefix_debug + # meson setup ... builddirprefix_debugoptimized + # meson setup ... builddirprefix_release + # along with also setting up a new, thin/lite visual studio solution and projects with the multiple debug/opt/release configurations that + # invoke the appropriate 'meson compile ...' build commands upon the normal visual studio build/rebuild/clean actions, instead of using + # the native VS/msbuild system. + builddir_prefix = options.builddir + genvsliteval = options.cmd_line_options.pop(mesonlib.OptionKey('genvslite')) + # The command line may specify a '--backend' option, which doesn't make sense in conjunction with + # '--genvslite', where we always want to use a ninja back end - + k_backend = mesonlib.OptionKey('backend') + if k_backend in options.cmd_line_options.keys(): + if options.cmd_line_options[k_backend] != 'ninja': + raise MesonException('Explicitly specifying a backend option with \'genvslite\' is not necessary ' + '(the ninja backend is always used) but specifying a non-ninja backend ' + 'conflicts with a \'genvslite\' setup') + else: + options.cmd_line_options[k_backend] = 'ninja' + buildtypes_list = coredata.get_genvs_default_buildtype_list() + vslite_ctx = {} + + for buildtypestr in buildtypes_list: + options.builddir = f'{builddir_prefix}_{buildtypestr}' # E.g. builddir_release + options.cmd_line_options[mesonlib.OptionKey('buildtype')] = buildtypestr + app = MesonApp(options) + vslite_ctx[buildtypestr] = app.generate(capture=True) + #Now for generating the 'lite' solution and project files, which will use these builds we've just set up, above. + options.builddir = f'{builddir_prefix}_vs' + options.cmd_line_options[mesonlib.OptionKey('genvslite')] = genvsliteval + app = MesonApp(options) + app.generate(capture=False, vslite_ctx=vslite_ctx) def run(options: T.Union[argparse.Namespace, T.List[str]]) -> int: if not isinstance(options, argparse.Namespace): @@ -305,6 +350,11 @@ def run(options: T.Union[argparse.Namespace, T.List[str]]) -> int: add_arguments(parser) options = parser.parse_args(options) coredata.parse_cmd_line_options(options) - app = MesonApp(options) - app.generate() + + if mesonlib.OptionKey('genvslite') in options.cmd_line_options.keys(): + run_genvslite_setup(options) + else: + app = MesonApp(options) + app.generate() + return 0 diff --git a/mesonbuild/msubprojects.py b/mesonbuild/msubprojects.py index d6c182a..d8afd92 100755 --- a/mesonbuild/msubprojects.py +++ b/mesonbuild/msubprojects.py @@ -14,13 +14,16 @@ import tarfile import zipfile from . import mlog +from .ast import IntrospectionInterpreter from .mesonlib import quiet_git, GitException, Popen_safe, MesonException, windows_proof_rmtree -from .wrap.wrap import (Resolver, WrapException, ALL_TYPES, PackageDefinition, +from .wrap.wrap import (Resolver, WrapException, ALL_TYPES, parse_patch_url, update_wrap_file, get_releases) if T.TYPE_CHECKING: from typing_extensions import Protocol + from .wrap.wrap import PackageDefinition + SubParsers = argparse._SubParsersAction[argparse.ArgumentParser] class Arguments(Protocol): @@ -152,6 +155,11 @@ class Runner: try: wrapdb_version = self.wrap.get('wrapdb_version') branch, revision = wrapdb_version.split('-', 1) + except ValueError: + if not options.force: + self.log(' ->', mlog.red('Malformed wrapdb_version field, use --force to update anyway')) + return False + branch = revision = None except WrapException: # Fallback to parsing the patch URL to determine current version. # This won't work for projects that have upstream Meson support. @@ -160,7 +168,7 @@ class Runner: branch, revision = parse_patch_url(patch_url) except WrapException: if not options.force: - self.log(' ->', mlog.red('Could not determine current version, use --force to update any way')) + self.log(' ->', mlog.red('Could not determine current version, use --force to update anyway')) return False branch = revision = None @@ -186,7 +194,7 @@ class Runner: # cached. windows_proof_rmtree(self.repo_dir) try: - self.wrap_resolver.resolve(self.wrap.name, 'meson') + self.wrap_resolver.resolve(self.wrap.name) self.log(' -> New version extracted') return True except WrapException as e: @@ -207,13 +215,17 @@ class Runner: self.log(self.git_output(cmd)) def git_stash(self) -> None: - # That git command return 1 (failure) when there is something to stash. + # That git command return some output when there is something to stash. # We don't want to stash when there is nothing to stash because that would # print spurious "No local changes to save". - if not quiet_git(['diff', '--quiet', 'HEAD'], self.repo_dir)[0]: + if quiet_git(['status', '--porcelain', ':!/.meson-subproject-wrap-hash.txt'], self.repo_dir)[1].strip(): # Don't pipe stdout here because we want the user to see their changes have # been saved. - self.git_verbose(['stash']) + # Note: `--all` is used, and not `--include-untracked`, to prevent + # a potential error if `.meson-subproject-wrap-hash.txt` matches a + # gitignore pattern. + # We must add the dot in addition to the negation, because older versions of git have a bug. + self.git_verbose(['stash', 'push', '--all', ':!/.meson-subproject-wrap-hash.txt', '.']) def git_show(self) -> None: commit_message = self.git_output(['show', '--quiet', '--pretty=format:%h%n%d%n%s%n[%an]']) @@ -224,7 +236,9 @@ class Runner: try: self.git_output(['-c', 'rebase.autoStash=true', 'rebase', 'FETCH_HEAD']) except GitException as e: - self.log(' -> Could not rebase', mlog.bold(self.repo_dir), 'onto', mlog.bold(revision)) + self.git_output(['-c', 'rebase.autoStash=true', 'rebase', '--abort']) + self.log(' -> Could not rebase', mlog.bold(self.repo_dir), 'onto', mlog.bold(revision), + '-- aborted') self.log(mlog.red(e.output)) self.log(mlog.red(str(e))) return False @@ -246,9 +260,10 @@ class Runner: return True def git_checkout(self, revision: str, create: bool = False) -> bool: - cmd = ['checkout', '--ignore-other-worktrees', revision, '--'] + cmd = ['checkout', '--ignore-other-worktrees'] if create: - cmd.insert(1, '-b') + cmd.append('-b') + cmd += [revision, '--'] try: # Stash local changes, commits can always be found back in reflog, to # avoid any data lost by mistake. @@ -277,6 +292,19 @@ class Runner: success = self.git_rebase(revision) return success + def git_branch_has_upstream(self, urls: set) -> bool: + cmd = ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'] + ret, upstream = quiet_git(cmd, self.repo_dir) + if not ret: + return False + try: + remote = upstream.split('/', maxsplit=1)[0] + except IndexError: + return False + cmd = ['remote', 'get-url', remote] + ret, remote_url = quiet_git(cmd, self.repo_dir) + return remote_url.strip() in urls + def update_git(self) -> bool: options = T.cast('UpdateArguments', self.options) if not os.path.exists(os.path.join(self.repo_dir, '.git')): @@ -284,7 +312,7 @@ class Runner: # Delete existing directory and redownload windows_proof_rmtree(self.repo_dir) try: - self.wrap_resolver.resolve(self.wrap.name, 'meson') + self.wrap_resolver.resolve(self.wrap.name) self.update_git_done() return True except WrapException as e: @@ -368,12 +396,16 @@ class Runner: success = self.git_rebase(revision) else: # We are in another branch, either the user created their own branch and - # we should rebase it, or revision changed in the wrap file and we need - # to checkout the new branch. + # we should rebase it, or revision changed in the wrap file (we + # know this when the current branch has an upstream) and we need to + # checkout the new branch. if options.reset: success = self.git_checkout_and_reset(revision) else: - success = self.git_rebase(revision) + if self.git_branch_has_upstream({url, push_url}): + success = self.git_checkout_and_rebase(revision) + else: + success = self.git_rebase(revision) if success: self.update_git_done() return success @@ -456,7 +488,7 @@ class Runner: self.log(' -> Already downloaded') return True try: - self.wrap_resolver.resolve(self.wrap.name, 'meson') + self.wrap_resolver.resolve(self.wrap.name) self.log(' -> done') except WrapException as e: self.log(' ->', mlog.red(str(e))) @@ -606,7 +638,7 @@ def add_common_arguments(p: argparse.ArgumentParser) -> None: help='Path to source directory') p.add_argument('--types', default='', help=f'Comma-separated list of subproject types. Supported types are: {ALL_TYPES_STRING} (default: all)') - p.add_argument('--num-processes', default=None, type=int, + p.add_argument('-j', '--num-processes', default=None, type=int, help='How many parallel processes to use (Since 0.59.0).') p.add_argument('--allow-insecure', default=False, action='store_true', help='Allow insecure server connections.') @@ -625,6 +657,8 @@ def add_wrap_update_parser(subparsers: 'SubParsers') -> argparse.ArgumentParser: p.set_defaults(pre_func=Runner.pre_update_wrapdb) return p +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: subparsers = parser.add_subparsers(title='Commands', dest='command') subparsers.required = True @@ -680,15 +714,18 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: p.set_defaults(subprojects_func=Runner.packagefiles) def run(options: 'Arguments') -> int: - src_dir = os.path.relpath(os.path.realpath(options.sourcedir)) - if not os.path.isfile(os.path.join(src_dir, 'meson.build')): - mlog.error('Directory', mlog.bold(src_dir), 'does not seem to be a Meson source directory.') + source_dir = os.path.relpath(os.path.realpath(options.sourcedir)) + if not os.path.isfile(os.path.join(source_dir, 'meson.build')): + mlog.error('Directory', mlog.bold(source_dir), 'does not seem to be a Meson source directory.') return 1 - subprojects_dir = os.path.join(src_dir, 'subprojects') - if not os.path.isdir(subprojects_dir): - mlog.log('Directory', mlog.bold(src_dir), 'does not seem to have subprojects.') + with mlog.no_logging(): + intr = IntrospectionInterpreter(source_dir, '', 'none') + intr.load_root_meson_file() + subproject_dir = intr.extract_subproject_dir() or 'subprojects' + if not os.path.isdir(os.path.join(source_dir, subproject_dir)): + mlog.log('Directory', mlog.bold(source_dir), 'does not seem to have subprojects.') return 0 - r = Resolver(src_dir, 'subprojects', wrap_frontend=True, allow_insecure=options.allow_insecure) + r = Resolver(source_dir, subproject_dir, wrap_frontend=True, allow_insecure=options.allow_insecure, silent=True) if options.subprojects: wraps = [wrap for name, wrap in r.wraps.items() if name in options.subprojects] else: @@ -699,7 +736,8 @@ def run(options: 'Arguments') -> int: raise MesonException(f'Unknown subproject type {t!r}, supported types are: {ALL_TYPES_STRING}') tasks: T.List[T.Awaitable[bool]] = [] task_names: T.List[str] = [] - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) executor = ThreadPoolExecutor(options.num_processes) if types: wraps = [wrap for wrap in wraps if wrap.type in types] @@ -708,7 +746,7 @@ def run(options: 'Arguments') -> int: pre_func(options) logger = Logger(len(wraps)) for wrap in wraps: - dirname = Path(subprojects_dir, wrap.directory).as_posix() + dirname = Path(source_dir, subproject_dir, wrap.directory).as_posix() runner = Runner(logger, r, wrap, dirname, options) task = loop.run_in_executor(executor, runner.run) tasks.append(task) diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index bcf84d3..b7d2bc6 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -13,11 +13,13 @@ # limitations under the License. # A tool to run tests in many different ways. +from __future__ import annotations from pathlib import Path from collections import deque from contextlib import suppress from copy import deepcopy +from fnmatch import fnmatch import argparse import asyncio import datetime @@ -42,14 +44,23 @@ import xml.etree.ElementTree as et from . import build from . import environment from . import mlog -from .coredata import major_versions_differ, MesonVersionMismatchException +from .coredata import MesonVersionMismatchException, major_versions_differ from .coredata import version as coredata_version -from .mesonlib import (MesonException, OrderedSet, RealPathAction, +from .mesonlib import (MesonException, OptionKey, OrderedSet, RealPathAction, get_wine_shortpath, join_args, split_args, setup_vsenv) from .mintro import get_infodir, load_info_file from .programs import ExternalProgram from .backend.backends import TestProtocol, TestSerialisation +if T.TYPE_CHECKING: + TYPE_TAPResult = T.Union['TAPParser.Test', + 'TAPParser.Error', + 'TAPParser.Version', + 'TAPParser.Plan', + 'TAPParser.UnknownLine', + 'TAPParser.Bailout'] + + # GNU autotools interprets a return code of 77 from tests it executes to # mean that the test should be skipped. GNU_SKIP_RETURNCODE = 77 @@ -61,6 +72,26 @@ GNU_ERROR_RETURNCODE = 99 # Exit if 3 Ctrl-C's are received within one second MAX_CTRLC = 3 +# Define unencodable xml characters' regex for replacing them with their +# printable representation +UNENCODABLE_XML_UNICHRS: T.List[T.Tuple[int, int]] = [ + (0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84), + (0x86, 0x9F), (0xFDD0, 0xFDEF), (0xFFFE, 0xFFFF)] +# Not narrow build +if sys.maxunicode >= 0x10000: + UNENCODABLE_XML_UNICHRS.extend([ + (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), + (0x3FFFE, 0x3FFFF), (0x4FFFE, 0x4FFFF), + (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF), + (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), + (0x9FFFE, 0x9FFFF), (0xAFFFE, 0xAFFFF), + (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF), + (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), + (0xFFFFE, 0xFFFFF), (0x10FFFE, 0x10FFFF)]) +UNENCODABLE_XML_CHR_RANGES = [fr'{chr(low)}-{chr(high)}' for (low, high) in UNENCODABLE_XML_UNICHRS] +UNENCODABLE_XML_CHRS_RE = re.compile('([' + ''.join(UNENCODABLE_XML_CHR_RANGES) + '])') + + def is_windows() -> bool: platname = platform.system().lower() return platname == 'windows' @@ -93,6 +124,8 @@ def determine_worker_count() -> int: num_workers = 1 return num_workers +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument('--maxfail', default=0, type=int, help='Number of failing tests before aborting the ' @@ -110,9 +143,6 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument('--wrapper', default=None, dest='wrapper', type=split_args, help='wrapper to run tests with (e.g. Valgrind)') parser.add_argument('-C', dest='wd', action=RealPathAction, - # https://github.com/python/typeshed/issues/3107 - # https://github.com/python/mypy/issues/7177 - type=os.path.abspath, # type: ignore help='directory to cd into before running') parser.add_argument('--suite', default=[], dest='include_suites', action='append', metavar='SUITE', help='Only run tests belonging to the given suite.') @@ -126,7 +156,7 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: help="Run benchmarks instead of tests.") parser.add_argument('--logbase', default='testlog', help="Base name for log file.") - parser.add_argument('--num-processes', default=determine_worker_count(), type=int, + parser.add_argument('-j', '--num-processes', default=determine_worker_count(), type=int, help='How many parallel processes to use.') parser.add_argument('-v', '--verbose', default=False, action='store_true', help='Do not redirect stdout and stderr') @@ -175,7 +205,7 @@ def returncode_to_status(retcode: int) -> str: # functions here because the status returned by subprocess is munged. It # returns a negative value if the process was killed by a signal rather than # the raw status returned by `wait()`. Also, If a shell sits between Meson - # the the actual unit test that shell is likely to convert a termination due + # the actual unit test that shell is likely to convert a termination due # to a signal into an exit status of 128 plus the signal number. if retcode < 0: signum = -retcode @@ -271,13 +301,6 @@ class TestResult(enum.Enum): return str(self.colorize('>>> ')) -TYPE_TAPResult = T.Union['TAPParser.Test', - 'TAPParser.Error', - 'TAPParser.Version', - 'TAPParser.Plan', - 'TAPParser.UnknownLine', - 'TAPParser.Bailout'] - class TAPParser: class Plan(T.NamedTuple): num_tests: int @@ -408,7 +431,7 @@ class TAPParser: yield self.Error('more than one plan found') else: num_tests = int(m.group(1)) - skipped = (num_tests == 0) + skipped = num_tests == 0 if m.group(2): if m.group(2).upper().startswith('SKIP'): if num_tests > 0: @@ -497,12 +520,14 @@ class ConsoleLogger(TestLogger): RTRI = "\u25B6 " def __init__(self) -> None: - self.running_tests = OrderedSet() # type: OrderedSet['TestRun'] - self.progress_test = None # type: T.Optional['TestRun'] - self.progress_task = None # type: T.Optional[asyncio.Future] - self.max_left_width = 0 # type: int + self.running_tests: OrderedSet['TestRun'] = OrderedSet() + self.progress_test: T.Optional['TestRun'] = None + self.progress_task: T.Optional[asyncio.Future] = None + self.max_left_width = 0 self.stop = False - self.update = asyncio.Event() + # TODO: before 3.10 this cannot be created immediately, because + # it will create a new event loop + self.update: asyncio.Event self.should_erase_line = '' self.test_count = 0 self.started_tests = 0 @@ -572,7 +597,7 @@ class ConsoleLogger(TestLogger): def start(self, harness: 'TestHarness') -> None: async def report_progress() -> None: - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() next_update = 0.0 self.request_update() while not self.stop: @@ -600,6 +625,7 @@ class ConsoleLogger(TestLogger): self.emit_progress(harness) self.flush() + self.update = asyncio.Event() self.test_count = harness.test_count self.cols = max(self.cols, harness.max_left_width + 30) @@ -743,14 +769,16 @@ class TextLogfileBuilder(TestFileLogger): class JsonLogfileBuilder(TestFileLogger): def log(self, harness: 'TestHarness', result: 'TestRun') -> None: - jresult = {'name': result.name, - 'stdout': result.stdo, - 'result': result.res.value, - 'starttime': result.starttime, - 'duration': result.duration, - 'returncode': result.returncode, - 'env': result.env, - 'command': result.cmd} # type: T.Dict[str, T.Any] + jresult: T.Dict[str, T.Any] = { + 'name': result.name, + 'stdout': result.stdo, + 'result': result.res.value, + 'starttime': result.starttime, + 'duration': result.duration, + 'returncode': result.returncode, + 'env': result.env, + 'command': result.cmd, + } if result.stde: jresult['stderr'] = result.stde self.file.write(json.dumps(jresult) + '\n') @@ -777,7 +805,7 @@ class JunitBuilder(TestLogger): self.filename = filename self.root = et.Element( 'testsuites', tests='0', errors='0', failures='0') - self.suites = {} # type: T.Dict[str, et.Element] + self.suites: T.Dict[str, et.Element] = {} def log(self, harness: 'TestHarness', test: 'TestRun') -> None: """Log a single test case.""" @@ -840,10 +868,10 @@ class JunitBuilder(TestLogger): et.SubElement(testcase, 'system-out').text = subtest.explanation if test.stdo: out = et.SubElement(suite, 'system-out') - out.text = test.stdo.rstrip() + out.text = replace_unencodable_xml_chars(test.stdo.rstrip()) if test.stde: err = et.SubElement(suite, 'system-err') - err.text = test.stde.rstrip() + err.text = replace_unencodable_xml_chars(test.stde.rstrip()) else: if test.project not in self.suites: suite = self.suites[test.project] = et.Element( @@ -866,10 +894,10 @@ class JunitBuilder(TestLogger): suite.attrib['failures'] = str(int(suite.attrib['failures']) + 1) if test.stdo: out = et.SubElement(testcase, 'system-out') - out.text = test.stdo.rstrip() + out.text = replace_unencodable_xml_chars(test.stdo.rstrip()) if test.stde: err = et.SubElement(testcase, 'system-err') - err.text = test.stde.rstrip() + err.text = replace_unencodable_xml_chars(test.stde.rstrip()) async def finish(self, harness: 'TestHarness') -> None: """Calculate total test counts and write out the xml result.""" @@ -895,24 +923,24 @@ class TestRun: name: str, timeout: T.Optional[int], is_parallel: bool, verbose: bool): self.res = TestResult.PENDING self.test = test - self._num = None # type: T.Optional[int] + self._num: T.Optional[int] = None self.name = name self.timeout = timeout - self.results = [] # type: T.List[TAPParser.Test] - self.returncode = None # type: T.Optional[int] - self.starttime = None # type: T.Optional[float] - self.duration = None # type: T.Optional[float] + self.results: T.List[TAPParser.Test] = [] + self.returncode: T.Optional[int] = None + self.starttime: T.Optional[float] = None + self.duration: T.Optional[float] = None self.stdo = '' self.stde = '' self.additional_error = '' - self.cmd = None # type: T.Optional[T.List[str]] - self.env = test_env # type: T.Dict[str, str] + self.cmd: T.Optional[T.List[str]] = None + self.env = test_env self.should_fail = test.should_fail self.project = test.project_name - self.junit = None # type: T.Optional[et.ElementTree] + self.junit: T.Optional[et.ElementTree] = None self.is_parallel = is_parallel self.verbose = verbose - self.warnings = [] # type: T.List[str] + self.warnings: T.List[str] = [] def start(self, cmd: T.List[str]) -> None: self.res = TestResult.RUNNING @@ -1029,12 +1057,16 @@ class TestRunGTest(TestRunExitCode): filename = os.path.join(self.test.workdir, filename) try: - self.junit = et.parse(filename) + with open(filename, 'r', encoding='utf8', errors='replace') as f: + self.junit = et.parse(f) except FileNotFoundError: # This can happen if the test fails to run or complete for some # reason, like the rpath for libgtest isn't properly set. ExitCode # will handle the failure, don't generate a stacktrace. pass + except et.ParseError as e: + # ExitCode will handle the failure, don't generate a stacktrace. + mlog.error(f'Unable to parse {filename}: {e!s}') super().complete() @@ -1055,8 +1087,8 @@ class TestRunTAP(TestRun): async def parse(self, harness: 'TestHarness', lines: T.AsyncIterator[str]) -> None: res = None - warnings = [] # type: T.List[TAPParser.UnknownLine] - version: T.Optional[int] = None + warnings: T.List[TAPParser.UnknownLine] = [] + version = 12 async for i in TAPParser().parse_async(lines): if isinstance(i, TAPParser.Version): @@ -1075,9 +1107,6 @@ class TestRunTAP(TestRun): self.additional_error += 'TAP parsing error: ' + i.message res = TestResult.ERROR - if version is None: - self.warnings.append('Unknown TAP version. The first line MUST be `TAP version `. Assuming version 12.') - version = 12 if warnings: unknown = str(mlog.yellow('UNKNOWN')) width = len(str(max(i.lineno for i in warnings))) @@ -1140,6 +1169,13 @@ class TestRunRust(TestRun): TestRun.PROTOCOL_TO_CLASS[TestProtocol.RUST] = TestRunRust +# Check unencodable characters in xml output and replace them with +# their printable representation +def replace_unencodable_xml_chars(original_str: str) -> str: + # [1:-1] is needed for removing `'` characters from both start and end + # of the string + replacement_lambda = lambda illegal_chr: repr(illegal_chr.group())[1:-1] + return UNENCODABLE_XML_CHRS_RE.sub(replacement_lambda, original_str) def decode(stream: T.Union[None, bytes]) -> str: if stream is None: @@ -1229,13 +1265,14 @@ async def complete_all(futures: T.Iterable[asyncio.Future], # Python is silly and does not have a variant of asyncio.wait with an # absolute time as deadline. - deadline = None if timeout is None else asyncio.get_event_loop().time() + timeout + loop = asyncio.get_running_loop() + deadline = None if timeout is None else loop.time() + timeout while futures and (timeout is None or timeout > 0): done, futures = await asyncio.wait(futures, timeout=timeout, return_when=asyncio.FIRST_EXCEPTION) check_futures(done) if deadline: - timeout = deadline - asyncio.get_event_loop().time() + timeout = deadline - loop.time() check_futures(futures) @@ -1247,11 +1284,11 @@ class TestSubprocess: self._process = p self.stdout = stdout self.stderr = stderr - self.stdo_task = None # type: T.Optional[asyncio.Future[str]] - self.stde_task = None # type: T.Optional[asyncio.Future[str]] - self.postwait_fn = postwait_fn # type: T.Callable[[], None] - self.all_futures = [] # type: T.List[asyncio.Future] - self.queue = None # type: T.Optional[asyncio.Queue[T.Optional[str]]] + self.stdo_task: T.Optional[asyncio.Task[None]] = None + self.stde_task: T.Optional[asyncio.Task[None]] = None + self.postwait_fn = postwait_fn + self.all_futures: T.List[asyncio.Future] = [] + self.queue: T.Optional[asyncio.Queue[T.Optional[str]]] = None def stdout_lines(self) -> T.AsyncIterator[str]: self.queue = asyncio.Queue() @@ -1380,6 +1417,15 @@ class SingleTestRunner: if ('MALLOC_PERTURB_' not in env or not env['MALLOC_PERTURB_']) and not options.benchmark: env['MALLOC_PERTURB_'] = str(random.randint(1, 255)) + # Sanitizers do not default to aborting on error. This is counter to + # expectations when using -Db_sanitize and has led to confusion in the wild + # in CI. Set our own values of {ASAN,UBSAN}_OPTOINS to rectify this, but + # only if the user has not defined them. + if ('ASAN_OPTIONS' not in env or not env['ASAN_OPTIONS']): + env['ASAN_OPTIONS'] = 'halt_on_error=1:abort_on_error=1:print_summary=1' + if ('UBSAN_OPTIONS' not in env or not env['UBSAN_OPTIONS']): + env['UBSAN_OPTIONS'] = 'halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1' + if self.options.gdb or self.test.timeout is None or self.test.timeout <= 0: timeout = None elif self.options.timeout_multiplier is None: @@ -1443,7 +1489,7 @@ class SingleTestRunner: async def run(self, harness: 'TestHarness') -> TestRun: if self.cmd is None: - self.stdo = 'Not run because can not execute cross compiled binaries.' + self.stdo = 'Not run because cannot execute cross compiled binaries.' harness.log_start_test(self.runobj) self.runobj.complete_skip() else: @@ -1497,7 +1543,7 @@ class SingleTestRunner: if not self.options.split and not self.runobj.needs_parsing \ else asyncio.subprocess.PIPE - extra_cmd = [] # type: T.List[str] + extra_cmd: T.List[str] = [] if self.test.protocol is TestProtocol.GTEST: gtestname = self.test.name if self.test.workdir: @@ -1532,7 +1578,7 @@ class SingleTestRunner: class TestHarness: def __init__(self, options: argparse.Namespace): self.options = options - self.collected_failures = [] # type: T.List[TestRun] + self.collected_failures: T.List[TestRun] = [] self.fail_count = 0 self.expectedfail_count = 0 self.unexpectedpass_count = 0 @@ -1542,13 +1588,13 @@ class TestHarness: self.test_count = 0 self.name_max_len = 0 self.is_run = False - self.loggers = [] # type: T.List[TestLogger] + self.loggers: T.List[TestLogger] = [] self.console_logger = ConsoleLogger() self.loggers.append(self.console_logger) self.need_console = False - self.ninja = None # type: T.List[str] + self.ninja: T.List[str] = None - self.logfile_base = None # type: T.Optional[str] + self.logfile_base: T.Optional[str] = None if self.options.logbase and not self.options.gdb: namebase = None self.logfile_base = os.path.join(self.options.wd, 'meson-logs', self.options.logbase) @@ -1578,12 +1624,6 @@ class TestHarness: if self.options.no_rebuild: return - if not (Path(self.options.wd) / 'build.ninja').is_file(): - print('Only ninja backend is supported to rebuild tests before running them.') - # Disable, no point in trying to build anything later - self.options.no_rebuild = True - return - self.ninja = environment.detect_ninja() if not self.ninja: print("Can't find ninja, can't rebuild test.") @@ -1603,9 +1643,12 @@ class TestHarness: # happen before rebuild_deps(), because we need the correct list of # tests and their dependencies to compute if not self.options.no_rebuild: - ret = subprocess.run(self.ninja + ['build.ninja']).returncode - if ret != 0: - raise TestException(f'Could not configure {self.options.wd!r}') + teststdo = subprocess.run(self.ninja + ['-n', 'build.ninja'], capture_output=True).stdout + if b'ninja: no work to do.' not in teststdo and b'samu: nothing to do' not in teststdo: + stdo = sys.stderr if self.options.list else sys.stdout + ret = subprocess.run(self.ninja + ['build.ninja'], stdout=stdo.fileno()) + if ret.returncode != 0: + raise TestException(f'Could not configure {self.options.wd!r}') self.build_data = build.load(os.getcwd()) if not self.options.setup: @@ -1774,7 +1817,7 @@ class TestHarness: startdir = os.getcwd() try: os.chdir(self.options.wd) - runners = [] # type: T.List[SingleTestRunner] + runners: T.List[SingleTestRunner] = [] for i in range(self.options.repeat): runners.extend(self.get_test_runner(test) for test in tests) if i == 0: @@ -1857,21 +1900,52 @@ class TestHarness: run all tests with that name across all subprojects, which is identical to "meson test foo1" ''' + patterns: T.Dict[T.Tuple[str, str], bool] = {} for arg in self.options.args: + # Replace empty components by wildcards: + # '' -> '*:*' + # 'name' -> '*:name' + # ':name' -> '*:name' + # 'proj:' -> 'proj:*' if ':' in arg: subproj, name = arg.split(':', maxsplit=1) + if name == '': + name = '*' + if subproj == '': # in case arg was ':' + subproj = '*' else: - subproj, name = '', arg - for t in tests: - if subproj and t.project_name != subproj: - continue - if name and t.name != name: - continue - yield t + subproj, name = '*', arg + patterns[(subproj, name)] = False + + for t in tests: + # For each test, find the first matching pattern + # and mark it as used. yield the matching tests. + for subproj, name in list(patterns): + if fnmatch(t.project_name, subproj) and fnmatch(t.name, name): + patterns[(subproj, name)] = True + yield t + break + + for (subproj, name), was_used in patterns.items(): + if not was_used: + # For each unused pattern... + arg = f'{subproj}:{name}' + for t in tests: + # ... if it matches a test, then it wasn't used because another + # pattern matched the same test before. + # Report it as a warning. + if fnmatch(t.project_name, subproj) and fnmatch(t.name, name): + mlog.warning(f'{arg} test name is redundant and was not used') + break + else: + # If the pattern doesn't match any test, + # report it as an error. We don't want the `test` command to + # succeed on an invalid pattern. + raise MesonException(f'{arg} test name does not match any test') - def get_tests(self) -> T.List[TestSerialisation]: + def get_tests(self, errorfile: T.Optional[T.IO] = None) -> T.List[TestSerialisation]: if not self.tests: - print('No tests defined.') + print('No tests defined.', file=errorfile) return [] tests = [t for t in self.tests if self.test_suitable(t)] @@ -1879,7 +1953,7 @@ class TestHarness: tests = list(self.tests_from_args(tests)) if not tests: - print('No suitable tests defined.') + print('No suitable tests defined.', file=errorfile) return [] return tests @@ -1898,7 +1972,7 @@ class TestHarness: @staticmethod def get_wrapper(options: argparse.Namespace) -> T.List[str]: - wrap = [] # type: T.List[str] + wrap: T.List[str] = [] if options.gdb: wrap = [options.gdb_path, '--quiet'] if options.repeat > 1: @@ -1922,9 +1996,12 @@ class TestHarness: def run_tests(self, runners: T.List[SingleTestRunner]) -> None: try: self.open_logfiles() - # Replace with asyncio.run once we can require Python 3.7 - loop = asyncio.get_event_loop() - loop.run_until_complete(self._run_tests(runners)) + + # TODO: this is the default for python 3.8 + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + asyncio.run(self._run_tests(runners)) finally: self.close_logfiles() @@ -1938,10 +2015,11 @@ class TestHarness: async def _run_tests(self, runners: T.List[SingleTestRunner]) -> None: semaphore = asyncio.Semaphore(self.options.num_processes) - futures = deque() # type: T.Deque[asyncio.Future] - running_tests = {} # type: T.Dict[asyncio.Future, str] + futures: T.Deque[asyncio.Future] = deque() + running_tests: T.Dict[asyncio.Future, str] = {} interrupted = False - ctrlc_times = deque(maxlen=MAX_CTRLC) # type: T.Deque[float] + ctrlc_times: T.Deque[float] = deque(maxlen=MAX_CTRLC) + loop = asyncio.get_running_loop() async def run_test(test: SingleTestRunner) -> None: async with semaphore: @@ -1990,7 +2068,7 @@ class TestHarness: nonlocal interrupted if interrupted: return - ctrlc_times.append(asyncio.get_event_loop().time()) + ctrlc_times.append(loop.time()) if len(ctrlc_times) == MAX_CTRLC and ctrlc_times[-1] - ctrlc_times[0] < 1: self.flush_logfiles() mlog.warning('CTRL-C detected, exiting') @@ -2007,10 +2085,10 @@ class TestHarness: if sys.platform != 'win32': if os.getpgid(0) == os.getpid(): - asyncio.get_event_loop().add_signal_handler(signal.SIGINT, sigint_handler) + loop.add_signal_handler(signal.SIGINT, sigint_handler) else: - asyncio.get_event_loop().add_signal_handler(signal.SIGINT, sigterm_handler) - asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, sigterm_handler) + loop.add_signal_handler(signal.SIGINT, sigterm_handler) + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) try: for runner in runners: if not runner.is_parallel: @@ -2027,13 +2105,13 @@ class TestHarness: await complete_all(futures) finally: if sys.platform != 'win32': - asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) - asyncio.get_event_loop().remove_signal_handler(signal.SIGTERM) + loop.remove_signal_handler(signal.SIGINT) + loop.remove_signal_handler(signal.SIGTERM) for l in self.loggers: await l.finish(self) def list_tests(th: TestHarness) -> bool: - tests = th.get_tests() + tests = th.get_tests(errorfile=sys.stderr) for t in tests: print(th.get_pretty_suite(t)) return not tests @@ -2047,9 +2125,9 @@ def rebuild_deps(ninja: T.List[str], wd: str, tests: T.List[TestSerialisation]) assert len(ninja) > 0 - depends = set() # type: T.Set[str] - targets = set() # type: T.Set[str] - intro_targets = {} # type: T.Dict[str, T.List[str]] + depends: T.Set[str] = set() + targets: T.Set[str] = set() + intro_targets: T.Dict[str, T.List[str]] = {} for target in load_info_file(get_infodir(wd), kind='targets'): intro_targets[target['id']] = [ convert_path_to_target(f) @@ -2087,10 +2165,6 @@ def run(options: argparse.Namespace) -> int: if options.wrapper: check_bin = options.wrapper[0] - if sys.platform == 'win32': - loop = asyncio.ProactorEventLoop() - asyncio.set_event_loop(loop) - if check_bin is not None: exe = ExternalProgram(check_bin, silent=True) if not exe.found(): @@ -2098,7 +2172,18 @@ def run(options: argparse.Namespace) -> int: return 1 b = build.load(options.wd) - setup_vsenv(b.need_vsenv) + need_vsenv = T.cast('bool', b.environment.coredata.get_option(OptionKey('vsenv'))) + setup_vsenv(need_vsenv) + + if not options.no_rebuild: + backend = b.environment.coredata.get_option(OptionKey('backend')) + if backend == 'none': + # nothing to build... + options.no_rebuild = True + elif backend != 'ninja': + print('Only ninja backend is supported to rebuild tests before running them.') + # Disable, no point in trying to build anything later + options.no_rebuild = True with TestHarness(options) as th: try: diff --git a/mesonbuild/munstable_coredata.py b/mesonbuild/munstable_coredata.py index fa3b720..ba6ffb3 100644 --- a/mesonbuild/munstable_coredata.py +++ b/mesonbuild/munstable_coredata.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from . import coredata as cdata @@ -20,6 +21,8 @@ import os.path import pprint import textwrap +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser): parser.add_argument('--all', action='store_true', dest='all', default=False, help='Show data not used by current backend.') @@ -103,7 +106,7 @@ def run(options): print(' version: ' + repr(dep.get_version())) for for_machine in iter(MachineChoice): - items_list = list(sorted(v[for_machine].items())) + items_list = sorted(v[for_machine].items()) if items_list: print(f'Cached dependencies for {for_machine.get_lower_case_name()} machine') for dep_key, deps in items_list: diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 549d564..895cada 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import re import typing as T @@ -19,11 +20,16 @@ from . import coredata from . import mesonlib from . import mparser from . import mlog -from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo, permittedKwargs +from .interpreterbase import FeatureNew, FeatureDeprecated, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo +from .interpreter.type_checking import NoneType, in_set_validator + if T.TYPE_CHECKING: from .interpreterbase import TYPE_var, TYPE_kwargs from .interpreterbase import SubProject - from typing_extensions import TypedDict + from typing_extensions import TypedDict, Literal + + _DEPRECATED_ARGS = T.Union[bool, str, T.Dict[str, str], T.List[str]] + FuncOptionArgs = TypedDict('FuncOptionArgs', { 'type': str, 'description': str, @@ -32,16 +38,32 @@ if T.TYPE_CHECKING: 'value': object, 'min': T.Optional[int], 'max': T.Optional[int], - 'deprecated': T.Union[bool, str, T.Dict[str, str], T.List[str]], - }) - ParserArgs = TypedDict('ParserArgs', { - 'yield': bool, - 'choices': T.Optional[T.List[str]], - 'value': object, - 'min': T.Optional[int], - 'max': T.Optional[int], + 'deprecated': _DEPRECATED_ARGS, }) + class StringArgs(TypedDict): + value: str + + class BooleanArgs(TypedDict): + value: bool + + class ComboArgs(TypedDict): + value: str + choices: T.List[str] + + class IntegerArgs(TypedDict): + value: int + min: T.Optional[int] + max: T.Optional[int] + + class StringArrayArgs(TypedDict): + value: T.Optional[T.Union[str, T.List[str]]] + choices: T.List[str] + + class FeatureArgs(TypedDict): + value: Literal['enabled', 'disabled', 'auto'] + choices: T.List[str] + class OptionException(mesonlib.MesonException): pass @@ -54,13 +76,14 @@ class OptionInterpreter: def __init__(self, subproject: 'SubProject') -> None: self.options: 'coredata.MutableKeyedOptionDictType' = {} self.subproject = subproject - self.option_types = {'string': self.string_parser, - 'boolean': self.boolean_parser, - 'combo': self.combo_parser, - 'integer': self.integer_parser, - 'array': self.string_array_parser, - 'feature': self.feature_parser, - } + self.option_types: T.Dict[str, T.Callable[..., coredata.UserOption]] = { + 'string': self.string_parser, + 'boolean': self.boolean_parser, + 'combo': self.combo_parser, + 'integer': self.integer_parser, + 'array': self.string_array_parser, + 'feature': self.feature_parser, + } def process(self, option_file: str) -> None: try: @@ -90,7 +113,9 @@ class OptionInterpreter: def reduce_single(self, arg: T.Union[str, mparser.BaseNode]) -> 'TYPE_var': if isinstance(arg, str): return arg - elif isinstance(arg, (mparser.StringNode, mparser.BooleanNode, + if isinstance(arg, mparser.ParenthesizedNode): + return self.reduce_single(arg.inner) + elif isinstance(arg, (mparser.BaseStringNode, mparser.BooleanNode, mparser.NumberNode)): return arg.value elif isinstance(arg, mparser.ArrayNode): @@ -98,7 +123,7 @@ class OptionInterpreter: elif isinstance(arg, mparser.DictNode): d = {} for k, v in arg.args.kwargs.items(): - if not isinstance(k, mparser.StringNode): + if not isinstance(k, mparser.BaseStringNode): raise OptionException('Dictionary keys must be a string literal') d[k.value] = self.reduce_single(v) return d @@ -139,23 +164,31 @@ class OptionInterpreter: def evaluate_statement(self, node: mparser.BaseNode) -> None: if not isinstance(node, mparser.FunctionNode): raise OptionException('Option file may only contain option definitions') - func_name = node.func_name + func_name = node.func_name.value if func_name != 'option': raise OptionException('Only calls to option() are allowed in option files.') (posargs, kwargs) = self.reduce_arguments(node.args) self.func_option(posargs, kwargs) - @typed_kwargs('option', - KwargInfo('type', str, required=True), - KwargInfo('description', str, default=''), - KwargInfo('yield', bool, default=coredata.default_yielding, since='0.45.0'), - KwargInfo('choices', (ContainerTypeInfo(list, str), type(None))), - KwargInfo('value', object), - KwargInfo('min', (int, type(None))), - KwargInfo('max', (int, type(None))), - KwargInfo('deprecated', (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), - default=False, since='0.60.0') - ) + @typed_kwargs( + 'option', + KwargInfo( + 'type', + str, + required=True, + validator=in_set_validator({'string', 'boolean', 'integer', 'combo', 'array', 'feature'}) + ), + KwargInfo('description', str, default=''), + KwargInfo( + 'deprecated', + (bool, str, ContainerTypeInfo(dict, str), ContainerTypeInfo(list, str)), + default=False, + since='0.60.0', + since_values={str: '0.63.0'}, + ), + KwargInfo('yield', bool, default=coredata.DEFAULT_YIELDING, since='0.45.0'), + allow_unknown=True, + ) @typed_pos_args('option', str) def func_option(self, args: T.Tuple[str], kwargs: 'FuncOptionArgs') -> None: opt_name = args[0] @@ -166,60 +199,88 @@ class OptionInterpreter: raise OptionException('Option name %s is reserved.' % opt_name) opt_type = kwargs['type'] - parser = self.option_types.get(opt_type) - if not parser: - raise OptionException(f'Unknown type {opt_type}.') + parser = self.option_types[opt_type] description = kwargs['description'] or opt_name - # Only keep in kwargs arguments that are used by option type's parser - # because they use @permittedKwargs(). - known_parser_kwargs = {'value', 'choices', 'yield', 'min', 'max'} - parser_kwargs = {k: v for k, v in kwargs.items() if k in known_parser_kwargs and v is not None} - opt = parser(description, T.cast('ParserArgs', parser_kwargs)) - opt.deprecated = kwargs['deprecated'] - if isinstance(opt.deprecated, str): - FeatureNew.single_use('String value to "deprecated" keyword argument', '0.63.0', self.subproject) + # Drop the arguments we've already consumed + n_kwargs = {k: v for k, v in kwargs.items() + if k not in {'type', 'description', 'deprecated', 'yield'}} + + opt = parser(description, (kwargs['yield'], kwargs['deprecated']), n_kwargs) if key in self.options: mlog.deprecation(f'Option {opt_name} already exists.') self.options[key] = opt - @permittedKwargs({'value', 'yield'}) - def string_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', '') - return coredata.UserStringOption(description, value, kwargs['yield']) - - @permittedKwargs({'value', 'yield'}) - def boolean_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', True) - return coredata.UserBooleanOption(description, value, kwargs['yield']) - - @permittedKwargs({'value', 'yield', 'choices'}) - def combo_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - choices = kwargs.get('choices') - if not choices: - raise OptionException('Combo option missing "choices" keyword.') - value = kwargs.get('value', choices[0]) - return coredata.UserComboOption(description, choices, value, kwargs['yield']) - - @permittedKwargs({'value', 'min', 'max', 'yield'}) - def integer_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value') + @typed_kwargs( + 'string option', + KwargInfo('value', str, default=''), + ) + def string_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArgs) -> coredata.UserOption: + return coredata.UserStringOption(description, kwargs['value'], *args) + + @typed_kwargs( + 'boolean option', + KwargInfo( + 'value', + (bool, str), + default=True, + validator=lambda x: None if isinstance(x, bool) or x in {'true', 'false'} else 'boolean options must have boolean values', + deprecated_values={str: ('1.1.0', 'use a boolean, not a string')}, + ), + ) + def boolean_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: BooleanArgs) -> coredata.UserOption: + return coredata.UserBooleanOption(description, kwargs['value'], *args) + + @typed_kwargs( + 'combo option', + KwargInfo('value', (str, NoneType)), + KwargInfo('choices', ContainerTypeInfo(list, str, allow_empty=False), required=True), + ) + def combo_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: ComboArgs) -> coredata.UserOption: + choices = kwargs['choices'] + value = kwargs['value'] if value is None: - raise OptionException('Integer option must contain value argument.') - inttuple = (kwargs.get('min'), kwargs.get('max'), value) - return coredata.UserIntegerOption(description, inttuple, kwargs['yield']) - - @permittedKwargs({'value', 'yield', 'choices'}) - def string_array_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - choices = kwargs.get('choices', []) - value = kwargs.get('value', choices) - if not isinstance(value, list): - raise OptionException('Array choices must be passed as an array.') + value = kwargs['choices'][0] + return coredata.UserComboOption(description, choices, value, *args) + + @typed_kwargs( + 'integer option', + KwargInfo( + 'value', + (int, str), + default=True, + deprecated_values={str: ('1.1.0', 'use an integer, not a string')}, + convertor=int, + ), + KwargInfo('min', (int, NoneType)), + KwargInfo('max', (int, NoneType)), + ) + def integer_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: IntegerArgs) -> coredata.UserOption: + value = kwargs['value'] + inttuple = (kwargs['min'], kwargs['max'], value) + return coredata.UserIntegerOption(description, inttuple, *args) + + @typed_kwargs( + 'string array option', + KwargInfo('value', (ContainerTypeInfo(list, str), str, NoneType)), + KwargInfo('choices', ContainerTypeInfo(list, str), default=[]), + ) + def string_array_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> coredata.UserOption: + choices = kwargs['choices'] + value = kwargs['value'] if kwargs['value'] is not None else choices + if isinstance(value, str): + if value.startswith('['): + FeatureDeprecated('String value for array option', '1.3.0').use(self.subproject) + else: + raise mesonlib.MesonException('Value does not define an array: ' + value) return coredata.UserArrayOption(description, value, choices=choices, - yielding=kwargs['yield']) + yielding=args[0], + deprecated=args[1]) - @permittedKwargs({'value', 'yield'}) - def feature_parser(self, description: str, kwargs: 'ParserArgs') -> coredata.UserOption: - value = kwargs.get('value', 'auto') - return coredata.UserFeatureOption(description, value, kwargs['yield']) + @typed_kwargs( + 'feature option', + KwargInfo('value', str, default='auto', validator=in_set_validator({'auto', 'enabled', 'disabled'})), + ) + def feature_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: FeatureArgs) -> coredata.UserOption: + return coredata.UserFeatureOption(description, kwargs['value'], *args) diff --git a/mesonbuild/programs.py b/mesonbuild/programs.py index 1d616aa..beb5bc5 100644 --- a/mesonbuild/programs.py +++ b/mesonbuild/programs.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Representations and logic for External and Internal Programs.""" @@ -25,7 +26,7 @@ from pathlib import Path from . import mesonlib from . import mlog -from .mesonlib import MachineChoice +from .mesonlib import MachineChoice, OrderedSet if T.TYPE_CHECKING: from .environment import Environment @@ -102,13 +103,12 @@ class ExternalProgram(mesonlib.HoldableObject): def get_version(self, interpreter: T.Optional['Interpreter'] = None) -> str: if not self.cached_version: - from . import build raw_cmd = self.get_command() + ['--version'] if interpreter: - res = interpreter.run_command_impl(interpreter.current_node, (self, ['--version']), + res = interpreter.run_command_impl((self, ['--version']), {'capture': True, 'check': True, - 'env': build.EnvironmentVariables()}, + 'env': mesonlib.EnvironmentVariables()}, True) o, e = res.stdout, res.stderr else: @@ -353,16 +353,19 @@ class OverrideProgram(ExternalProgram): def find_external_program(env: 'Environment', for_machine: MachineChoice, name: str, display_name: str, default_names: T.List[str], allow_default_for_cross: bool = True) -> T.Generator['ExternalProgram', None, None]: - """Find an external program, chcking the cross file plus any default options.""" + """Find an external program, checking the cross file plus any default options.""" + potential_names = OrderedSet(default_names) + potential_names.add(name) # Lookup in cross or machine file. - potential_cmd = env.lookup_binary_entry(for_machine, name) - if potential_cmd is not None: - mlog.debug(f'{display_name} binary for {for_machine} specified from cross file, native file, ' - f'or env var as {potential_cmd}') - yield ExternalProgram.from_entry(name, potential_cmd) - # We never fallback if the user-specified option is no good, so - # stop returning options. - return + for potential_name in potential_names: + potential_cmd = env.lookup_binary_entry(for_machine, potential_name) + if potential_cmd is not None: + mlog.debug(f'{display_name} binary for {for_machine} specified from cross file, native file, ' + f'or env var as {potential_cmd}') + yield ExternalProgram.from_entry(potential_name, potential_cmd) + # We never fallback if the user-specified option is no good, so + # stop returning options. + return mlog.debug(f'{display_name} binary missing from cross or native file, or env var undefined.') # Fallback on hard-coded defaults, if a default binary is allowed for use # with cross targets, or if this is not a cross target diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index 8a1021c..88a99e5 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # Copyright 2016 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,10 +24,10 @@ from __future__ import annotations from .ast import IntrospectionInterpreter, BUILD_TARGET_FUNCTIONS, AstConditionLevel, AstIDGenerator, AstIndentationGenerator, AstPrinter -from mesonbuild.mesonlib import MesonException +from mesonbuild.mesonlib import MesonException, setup_vsenv from . import mlog, environment from functools import wraps -from .mparser import Token, ArrayNode, ArgumentNode, AssignmentNode, BooleanNode, ElementaryNode, IdNode, FunctionNode, StringNode +from .mparser import Token, ArrayNode, ArgumentNode, AssignmentNode, BaseStringNode, BooleanNode, ElementaryNode, IdNode, FunctionNode, StringNode, SymbolNode import json, os, re, sys import typing as T @@ -38,6 +37,8 @@ if T.TYPE_CHECKING: class RewriterException(MesonException): pass +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser, formatter=None): parser.add_argument('-s', '--sourcedir', type=str, default='.', metavar='SRCDIR', help='Path to source directory.') parser.add_argument('-V', '--verbose', action='store_true', default=False, help='Enable verbose output') @@ -45,7 +46,7 @@ def add_arguments(parser, formatter=None): subparsers = parser.add_subparsers(dest='type', title='Rewriter commands', description='Rewrite command to execute') # Target - tgt_parser = subparsers.add_parser('target', help='Modify a target', formatter_class=formatter) + tgt_parser = subparsers.add_parser('target', aliases=['tgt'], help='Modify a target', formatter_class=formatter) tgt_parser.add_argument('-s', '--subdir', default='', dest='subdir', help='Subdirectory of the new target (only for the "add_target" action)') tgt_parser.add_argument('--type', dest='tgt_type', choices=rewriter_keys['target']['target_type'][2], default='executable', help='Type of the target to add (only for the "add_target" action)') @@ -64,13 +65,13 @@ def add_arguments(parser, formatter=None): kw_parser.add_argument('kwargs', nargs='*', help='Pairs of keyword and value') # Default options - def_parser = subparsers.add_parser('default-options', help='Modify the project default options', formatter_class=formatter) + def_parser = subparsers.add_parser('default-options', aliases=['def'], help='Modify the project default options', formatter_class=formatter) def_parser.add_argument('operation', choices=rewriter_keys['default_options']['operation'][2], help='Action to execute') def_parser.add_argument('options', nargs='*', help='Key, value pairs of configuration option') # JSON file/command - cmd_parser = subparsers.add_parser('command', help='Execute a JSON array of commands', formatter_class=formatter) + cmd_parser = subparsers.add_parser('command', aliases=['cmd'], help='Execute a JSON array of commands', formatter_class=formatter) cmd_parser.add_argument('json', help='JSON string or file to execute') class RequiredKeys: @@ -104,6 +105,9 @@ class RequiredKeys: return wrapped +def _symbol(val: str) -> SymbolNode: + return SymbolNode(Token('', '', 0, 0, 0, (0, 0), val)) + class MTypeBase: def __init__(self, node: T.Optional[BaseNode] = None): if node is None: @@ -189,7 +193,7 @@ class MTypeList(MTypeBase): super().__init__(node) def _new_node(self): - return ArrayNode(ArgumentNode(Token('', '', 0, 0, 0, None, '')), 0, 0, 0, 0) + return ArrayNode(_symbol('['), ArgumentNode(Token('', '', 0, 0, 0, None, '')), _symbol(']')) def _new_element_node(self, value): # Overwrite in derived class @@ -267,12 +271,12 @@ class MTypeStrList(MTypeList): return StringNode(Token('', '', 0, 0, 0, None, str(value))) def _check_is_equal(self, node, value) -> bool: - if isinstance(node, StringNode): + if isinstance(node, BaseStringNode): return node.value == value return False def _check_regex_matches(self, node, regex: str) -> bool: - if isinstance(node, StringNode): + if isinstance(node, BaseStringNode): return re.match(regex, node.value) is not None return False @@ -292,7 +296,7 @@ class MTypeIDList(MTypeList): return False def _check_regex_matches(self, node, regex: str) -> bool: - if isinstance(node, StringNode): + if isinstance(node, BaseStringNode): return re.match(regex, node.value) is not None return False @@ -420,7 +424,7 @@ class Rewriter: if target in self.interpreter.assignments: node = self.interpreter.assignments[target] if isinstance(node, FunctionNode): - if node.func_name in {'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries'}: + if node.func_name.value in {'executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries'}: tgt = self.interpreter.assign_vals[target] return tgt @@ -440,7 +444,7 @@ class Rewriter: if dependency in self.interpreter.assignments: node = self.interpreter.assignments[dependency] if isinstance(node, FunctionNode): - if node.func_name == 'dependency': + if node.func_name.value == 'dependency': name = self.interpreter.flatten_args(node.args)[0] dep = check_list(name) @@ -569,31 +573,33 @@ class Rewriter: if key not in arg_node.kwargs: arg_node.kwargs[key] = None - modifyer = kwargs_def[key](arg_node.kwargs[key]) - if not modifyer.can_modify(): + modifier = kwargs_def[key](arg_node.kwargs[key]) + if not modifier.can_modify(): mlog.log(' -- Skipping', mlog.bold(key), 'because it is to complex to modify') # Apply the operation val_str = str(val) if cmd['operation'] == 'set': mlog.log(' -- Setting', mlog.bold(key), 'to', mlog.yellow(val_str)) - modifyer.set_value(val) + modifier.set_value(val) elif cmd['operation'] == 'add': mlog.log(' -- Adding', mlog.yellow(val_str), 'to', mlog.bold(key)) - modifyer.add_value(val) + modifier.add_value(val) elif cmd['operation'] == 'remove': mlog.log(' -- Removing', mlog.yellow(val_str), 'from', mlog.bold(key)) - modifyer.remove_value(val) + modifier.remove_value(val) elif cmd['operation'] == 'remove_regex': mlog.log(' -- Removing all values matching', mlog.yellow(val_str), 'from', mlog.bold(key)) - modifyer.remove_regex(val) + modifier.remove_regex(val) # Write back the result - arg_node.kwargs[key] = modifyer.get_node() + arg_node.kwargs[key] = modifier.get_node() num_changed += 1 # Convert the keys back to IdNode's arg_node.kwargs = {IdNode(Token('', '', 0, 0, 0, None, k)): v for k, v in arg_node.kwargs.items()} + for k, v in arg_node.kwargs.items(): + k.level = v.level if num_changed > 0 and node not in self.modified_nodes: self.modified_nodes += [node] @@ -628,7 +634,7 @@ class Rewriter: args = [] if isinstance(n, FunctionNode): args = list(n.args.arguments) - if n.func_name in BUILD_TARGET_FUNCTIONS: + if n.func_name.value in BUILD_TARGET_FUNCTIONS: args.pop(0) elif isinstance(n, ArrayNode): args = n.args.arguments @@ -650,7 +656,7 @@ class Rewriter: src_list = [] for i in target['sources']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): src_list += [j.value] # Generate the new String nodes @@ -684,7 +690,7 @@ class Rewriter: def find_node(src): for i in target['sources']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): if j.value == src: return i, j return None, None @@ -726,7 +732,7 @@ class Rewriter: node = tgt_function.args.kwargs[extra_files_key] except StopIteration: # Target has no extra_files kwarg, create one - node = ArrayNode(ArgumentNode(Token('', tgt_function.filename, 0, 0, 0, None, '[]')), tgt_function.end_lineno, tgt_function.end_colno, tgt_function.end_lineno, tgt_function.end_colno) + node = ArrayNode(_symbol('['), ArgumentNode(Token('', tgt_function.filename, 0, 0, 0, None, '[]')), _symbol(']')) tgt_function.args.kwargs[IdNode(Token('string', tgt_function.filename, 0, 0, 0, None, 'extra_files'))] = node mark_array = False if tgt_function not in self.modified_nodes: @@ -743,7 +749,7 @@ class Rewriter: extra_files_list = [] for i in target['extra_files']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): extra_files_list += [j.value] # Generate the new String nodes @@ -774,7 +780,7 @@ class Rewriter: def find_node(src): for i in target['extra_files']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): if j.value == src: return i, j return None, None @@ -810,17 +816,17 @@ class Rewriter: # Build src list src_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) - src_arr_node = ArrayNode(src_arg_node, 0, 0, 0, 0) + src_arr_node = ArrayNode(_symbol('['), src_arg_node, _symbol(']')) src_far_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) - src_fun_node = FunctionNode(filename, 0, 0, 0, 0, 'files', src_far_node) - src_ass_node = AssignmentNode(filename, 0, 0, source_id, src_fun_node) + src_fun_node = FunctionNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), 'files')), _symbol('('), src_far_node, _symbol(')')) + src_ass_node = AssignmentNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), source_id)), _symbol('='), src_fun_node) src_arg_node.arguments = [StringNode(Token('string', filename, 0, 0, 0, None, x)) for x in cmd['sources']] src_far_node.arguments = [src_arr_node] # Build target tgt_arg_node = ArgumentNode(Token('string', filename, 0, 0, 0, None, '')) - tgt_fun_node = FunctionNode(filename, 0, 0, 0, 0, cmd['target_type'], tgt_arg_node) - tgt_ass_node = AssignmentNode(filename, 0, 0, target_id, tgt_fun_node) + tgt_fun_node = FunctionNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), cmd['target_type'])), _symbol('('), tgt_arg_node, _symbol(')')) + tgt_ass_node = AssignmentNode(IdNode(Token('id', filename, 0, 0, 0, (0, 0), target_id)), _symbol('='), tgt_fun_node) tgt_arg_node.arguments = [ StringNode(Token('string', filename, 0, 0, 0, None, cmd['target'])), IdNode(Token('string', filename, 0, 0, 0, None, source_id)) @@ -843,12 +849,12 @@ class Rewriter: src_list = [] for i in target['sources']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): src_list += [j.value] extra_files_list = [] for i in target['extra_files']: for j in arg_list_from_node(i): - if isinstance(j, StringNode): + if isinstance(j, BaseStringNode): extra_files_list += [j.value] test_data = { 'name': target['name'], @@ -863,8 +869,8 @@ class Rewriter: alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] path_sorter = lambda key: ([(key.count('/') <= idx, alphanum_key(x)) for idx, x in enumerate(key.split('/'))]) - unknown = [x for x in i.arguments if not isinstance(x, StringNode)] - sources = [x for x in i.arguments if isinstance(x, StringNode)] + unknown = [x for x in i.arguments if not isinstance(x, BaseStringNode)] + sources = [x for x in i.arguments if isinstance(x, BaseStringNode)] sources = sorted(sources, key=lambda x: path_sorter(x.value)) i.arguments = unknown + sources @@ -884,7 +890,7 @@ class Rewriter: # Sort based on line and column in reversed order work_nodes = [{'node': x, 'action': 'modify'} for x in self.modified_nodes] work_nodes += [{'node': x, 'action': 'rm'} for x in self.to_remove_nodes] - work_nodes = list(sorted(work_nodes, key=lambda x: (x['node'].lineno, x['node'].colno), reverse=True)) + work_nodes = sorted(work_nodes, key=lambda x: (x['node'].lineno, x['node'].colno), reverse=True) work_nodes += [{'node': x, 'action': 'add'} for x in self.to_add_nodes] # Generating the new replacement string @@ -1041,6 +1047,7 @@ def run(options): mlog.set_quiet() try: + setup_vsenv() rewriter = Rewriter(options.sourcedir, skip_errors=options.skip) rewriter.analyze_meson() diff --git a/mesonbuild/scripts/clangformat.py b/mesonbuild/scripts/clangformat.py index f2f6a77..c66df16 100644 --- a/mesonbuild/scripts/clangformat.py +++ b/mesonbuild/scripts/clangformat.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import subprocess @@ -22,10 +23,9 @@ from ..mesonlib import version_compare from ..programs import ExternalProgram import typing as T -def run_clang_format(fname: Path, exelist: T.List[str], check: bool) -> subprocess.CompletedProcess: +def run_clang_format(fname: Path, exelist: T.List[str], check: bool, cformat_ver: T.Optional[str]) -> subprocess.CompletedProcess: clangformat_10 = False - if check: - cformat_ver = ExternalProgram('clang-format', exelist).get_version() + if check and cformat_ver: if version_compare(cformat_ver, '>=10'): clangformat_10 = True exelist = exelist + ['--dry-run', '--Werror'] @@ -57,4 +57,9 @@ def run(args: T.List[str]) -> int: print('Could not execute clang-format "%s"' % ' '.join(exelist)) return 1 - return run_tool('clang-format', srcdir, builddir, run_clang_format, exelist, options.check) + if options.check: + cformat_ver = ExternalProgram('clang-format', exelist, silent=True).get_version() + else: + cformat_ver = None + + return run_tool('clang-format', srcdir, builddir, run_clang_format, exelist, options.check, cformat_ver) diff --git a/mesonbuild/scripts/clangtidy.py b/mesonbuild/scripts/clangtidy.py index 7364e27..943bde5 100644 --- a/mesonbuild/scripts/clangtidy.py +++ b/mesonbuild/scripts/clangtidy.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import subprocess @@ -22,8 +23,12 @@ import typing as T def run_clang_tidy(fname: Path, builddir: Path) -> subprocess.CompletedProcess: return subprocess.run(['clang-tidy', '-p', str(builddir), str(fname)]) +def run_clang_tidy_fix(fname: Path, builddir: Path) -> subprocess.CompletedProcess: + return subprocess.run(['run-clang-tidy', '-fix', '-format', '-quiet', '-p', str(builddir), str(fname)]) + def run(args: T.List[str]) -> int: parser = argparse.ArgumentParser() + parser.add_argument('--fix', action='store_true') parser.add_argument('sourcedir') parser.add_argument('builddir') options = parser.parse_args(args) @@ -31,4 +36,5 @@ def run(args: T.List[str]) -> int: srcdir = Path(options.sourcedir) builddir = Path(options.builddir) - return run_tool('clang-tidy', srcdir, builddir, run_clang_tidy, builddir) + run_func = run_clang_tidy_fix if options.fix else run_clang_tidy + return run_tool('clang-tidy', srcdir, builddir, run_func, builddir) diff --git a/mesonbuild/scripts/cleantrees.py b/mesonbuild/scripts/cleantrees.py index 1a38753..3512f56 100644 --- a/mesonbuild/scripts/cleantrees.py +++ b/mesonbuild/scripts/cleantrees.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import sys diff --git a/mesonbuild/scripts/cmake_run_ctgt.py b/mesonbuild/scripts/cmake_run_ctgt.py index 33e07d6..df3f361 100755 --- a/mesonbuild/scripts/cmake_run_ctgt.py +++ b/mesonbuild/scripts/cmake_run_ctgt.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from __future__ import annotations import argparse import subprocess @@ -8,7 +9,7 @@ from pathlib import Path import typing as T def run(argsv: T.List[str]) -> int: - commands = [[]] # type: T.List[T.List[str]] + commands: T.List[T.List[str]] = [[]] SEPARATOR = ';;;' # Generate CMD parameters @@ -34,7 +35,7 @@ def run(argsv: T.List[str]) -> int: commands += [[]] continue - i = i.replace('"', '') # Remove lefover quotes + i = i.replace('"', '') # Remove leftover quotes commands[-1] += [i] # Execute diff --git a/mesonbuild/scripts/copy.py b/mesonbuild/scripts/copy.py index acef2a8..dba13a5 100644 --- a/mesonbuild/scripts/copy.py +++ b/mesonbuild/scripts/copy.py @@ -1,5 +1,6 @@ # SPDX-License-Identifer: Apache-2.0 # Copyright © 2021 Intel Corporation +from __future__ import annotations """Helper script to copy files at build time. diff --git a/mesonbuild/scripts/coverage.py b/mesonbuild/scripts/coverage.py index 6281486..4c0f81e 100644 --- a/mesonbuild/scripts/coverage.py +++ b/mesonbuild/scripts/coverage.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from mesonbuild import environment, mesonlib @@ -21,7 +22,7 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build outfiles = [] exitcode = 0 - (gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe) = environment.find_coverage_tools() + (gcovr_exe, gcovr_version, lcov_exe, lcov_version, genhtml_exe, llvm_cov_exe) = environment.find_coverage_tools() # load config files for tools if available in the source tree # - lcov requires manually specifying a per-project config @@ -34,6 +35,11 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build else: lcov_config = [] + if lcov_exe and mesonlib.version_compare(lcov_version, '>=2.0'): + lcov_exe_rc_branch_coverage = ['--rc', 'branch_coverage=1'] + else: + lcov_exe_rc_branch_coverage = ['--rc', 'lcov_branch_coverage=1'] + gcovr_config = ['-e', re.escape(subproject_root)] # gcovr >= 4.2 requires a different syntax for out of source builds @@ -89,6 +95,9 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build initial_tracefile = covinfo + '.initial' run_tracefile = covinfo + '.run' raw_tracefile = covinfo + '.raw' + lcov_subpoject_exclude = [] + if os.path.exists(subproject_root): + lcov_subpoject_exclude.append(os.path.join(subproject_root, '*')) if use_llvm_cov: # Create a shim to allow using llvm-cov as a gcov tool. if mesonlib.is_windows(): @@ -116,26 +125,26 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build '--capture', '--output-file', run_tracefile, '--no-checksum', - '--rc', 'lcov_branch_coverage=1'] + + *lcov_exe_rc_branch_coverage] + lcov_config + gcov_tool_args) # Join initial and test results. subprocess.check_call([lcov_exe, '-a', initial_tracefile, '-a', run_tracefile, - '--rc', 'lcov_branch_coverage=1', + *lcov_exe_rc_branch_coverage, '-o', raw_tracefile] + lcov_config) # Remove all directories outside the source_root from the covinfo subprocess.check_call([lcov_exe, '--extract', raw_tracefile, os.path.join(source_root, '*'), - '--rc', 'lcov_branch_coverage=1', + *lcov_exe_rc_branch_coverage, '--output-file', covinfo] + lcov_config) # Remove all directories inside subproject dir subprocess.check_call([lcov_exe, '--remove', covinfo, - os.path.join(subproject_root, '*'), - '--rc', 'lcov_branch_coverage=1', + *lcov_subpoject_exclude, + *lcov_exe_rc_branch_coverage, '--output-file', covinfo] + lcov_config) subprocess.check_call([genhtml_exe, '--prefix', build_root, @@ -156,7 +165,7 @@ def coverage(outputs: T.List[str], source_root: str, subproject_root: str, build '--html-details', '--print-summary', '-o', os.path.join(htmloutdir, 'index.html'), - ]) + ] + gcov_exe_args) outfiles.append(('Html', pathlib.Path(htmloutdir, 'index.html'))) elif outputs: print('lcov/genhtml or gcovr >= 3.3 needed to generate Html coverage report') diff --git a/mesonbuild/scripts/delwithsuffix.py b/mesonbuild/scripts/delwithsuffix.py index 873db0d..f58b19c 100644 --- a/mesonbuild/scripts/delwithsuffix.py +++ b/mesonbuild/scripts/delwithsuffix.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os, sys import typing as T diff --git a/mesonbuild/scripts/depfixer.py b/mesonbuild/scripts/depfixer.py index 8d9c90f..1039933 100644 --- a/mesonbuild/scripts/depfixer.py +++ b/mesonbuild/scripts/depfixer.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sys @@ -122,8 +123,8 @@ class Elf(DataSizes): def __init__(self, bfile: str, verbose: bool = True) -> None: self.bfile = bfile self.verbose = verbose - self.sections = [] # type: T.List[SectionHeader] - self.dynamic = [] # type: T.List[DynamicEntry] + self.sections: T.List[SectionHeader] = [] + self.dynamic: T.List[DynamicEntry] = [] self.open_bf(bfile) try: (self.ptrsize, self.is_le) = self.detect_elf_type() @@ -153,7 +154,7 @@ class Elf(DataSizes): def close_bf(self) -> None: if self.bf is not None: if self.bf_perms is not None: - os.fchmod(self.bf.fileno(), self.bf_perms) + os.chmod(self.bf.fileno(), self.bf_perms) self.bf_perms = None self.bf.close() self.bf = None @@ -328,7 +329,7 @@ class Elf(DataSizes): old_rpath = self.read_str() # Some rpath entries may come from multiple sources. # Only add each one once. - new_rpaths = OrderedSet() # type: OrderedSet[bytes] + new_rpaths: OrderedSet[bytes] = OrderedSet() if new_rpath: new_rpaths.update(new_rpath.split(b':')) if old_rpath: @@ -349,7 +350,7 @@ class Elf(DataSizes): sys.exit(msg) # The linker does read-only string deduplication. If there is a # string that shares a suffix with the rpath, they might get - # dedupped. This means changing the rpath string might break something + # deduped. This means changing the rpath string might break something # completely unrelated. This has already happened once with X.org. # Thus we want to keep this change as small as possible to minimize # the chance of obliterating other strings. It might still happen diff --git a/mesonbuild/scripts/dirchanger.py b/mesonbuild/scripts/dirchanger.py index 21632cd..60c4f12 100644 --- a/mesonbuild/scripts/dirchanger.py +++ b/mesonbuild/scripts/dirchanger.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations '''CD into dir given as first argument and execute the command given in the rest of the arguments.''' diff --git a/mesonbuild/scripts/env2mfile.py b/mesonbuild/scripts/env2mfile.py index 35049ae..8811b6c 100755 --- a/mesonbuild/scripts/env2mfile.py +++ b/mesonbuild/scripts/env2mfile.py @@ -31,6 +31,8 @@ def has_for_build() -> bool: return True return False +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: parser.add_argument('--debarch', default=None, help='The dpkg architecture to generate.') @@ -44,6 +46,10 @@ def add_arguments(parser: 'argparse.ArgumentParser') -> None: help='Generate a native compilation file.') parser.add_argument('--system', default=None, help='Define system for cross compilation.') + parser.add_argument('--subsystem', default=None, + help='Define subsystem for cross compilation.') + parser.add_argument('--kernel', default=None, + help='Define kernel for cross compilation.') parser.add_argument('--cpu', default=None, help='Define cpu for cross compilation.') parser.add_argument('--cpu-family', default=None, @@ -61,6 +67,8 @@ class MachineInfo: self.cmake: T.Dict[str, T.Union[str, T.List[str]]] = {} self.system: T.Optional[str] = None + self.subsystem: T.Optional[str] = None + self.kernel: T.Optional[str] = None self.cpu: T.Optional[str] = None self.cpu_family: T.Optional[str] = None self.endian: T.Optional[str] = None @@ -131,19 +139,27 @@ def get_args_from_envvars(infos: MachineInfo) -> None: if objcpp_link_args: infos.link_args['objcpp'] = objcpp_link_args -cpu_family_map = dict(mips64el="mips64", - i686='x86') -cpu_map = dict(armhf="arm7hlf", - mips64el="mips64", - powerpc64le="ppc64", - ) +deb_cpu_family_map = { + 'mips64el': 'mips64', + 'i686': 'x86', + 'powerpc64le': 'ppc64', +} + +deb_cpu_map = { + 'armhf': 'arm7hlf', + 'mips64el': 'mips64', + 'powerpc64le': 'ppc64', +} def deb_detect_cmake(infos: MachineInfo, data: T.Dict[str, str]) -> None: - system_name_map = dict(linux="Linux", kfreebsd="kFreeBSD", hurd="GNU") - system_processor_map = dict(arm='armv7l', mips64el='mips64', powerpc64le='ppc64le') + system_name_map = {'linux': 'Linux', 'kfreebsd': 'kFreeBSD', 'hurd': 'GNU'} + system_processor_map = {'arm': 'armv7l', 'mips64el': 'mips64', 'powerpc64le': 'ppc64le'} infos.cmake["CMAKE_C_COMPILER"] = infos.compilers['c'] - infos.cmake["CMAKE_CXX_COMPILER"] = infos.compilers['cpp'] + try: + infos.cmake["CMAKE_CXX_COMPILER"] = infos.compilers['cpp'] + except KeyError: + pass infos.cmake["CMAKE_SYSTEM_NAME"] = system_name_map[data['DEB_HOST_ARCH_OS']] infos.cmake["CMAKE_SYSTEM_PROCESSOR"] = system_processor_map.get(data['DEB_HOST_GNU_CPU'], data['DEB_HOST_GNU_CPU']) @@ -158,7 +174,7 @@ def deb_compiler_lookup(infos: MachineInfo, compilerstems: T.List[T.Tuple[str, s pass def detect_cross_debianlike(options: T.Any) -> MachineInfo: - if options.debarch is None: + if options.debarch == 'auto': cmd = ['dpkg-architecture'] else: cmd = ['dpkg-architecture', '-a' + options.debarch] @@ -173,10 +189,12 @@ def detect_cross_debianlike(options: T.Any) -> MachineInfo: data[k] = v host_arch = data['DEB_HOST_GNU_TYPE'] host_os = data['DEB_HOST_ARCH_OS'] - host_cpu_family = cpu_family_map.get(data['DEB_HOST_GNU_CPU'], - data['DEB_HOST_GNU_CPU']) - host_cpu = cpu_map.get(data['DEB_HOST_ARCH'], - data['DEB_HOST_ARCH']) + host_subsystem = host_os + host_kernel = 'linux' + host_cpu_family = deb_cpu_family_map.get(data['DEB_HOST_GNU_CPU'], + data['DEB_HOST_GNU_CPU']) + host_cpu = deb_cpu_map.get(data['DEB_HOST_ARCH'], + data['DEB_HOST_ARCH']) host_endian = data['DEB_HOST_ARCH_ENDIAN'] compilerstems = [('c', 'gcc'), @@ -197,7 +215,7 @@ def detect_cross_debianlike(options: T.Any) -> MachineInfo: except ValueError: pass try: - infos.binaries['pkgconfig'] = locate_path("%s-pkg-config" % host_arch) + infos.binaries['pkg-config'] = locate_path("%s-pkg-config" % host_arch) except ValueError: pass # pkg-config is optional try: @@ -205,6 +223,8 @@ def detect_cross_debianlike(options: T.Any) -> MachineInfo: except ValueError: pass infos.system = host_os + infos.subsystem = host_subsystem + infos.kernel = host_kernel infos.cpu_family = host_cpu_family infos.cpu = host_cpu infos.endian = host_endian @@ -252,11 +272,18 @@ def write_machine_file(infos: MachineInfo, ofilename: str, write_system_info: bo ofile.write(f"cpu_family = '{infos.cpu_family}'\n") ofile.write(f"endian = '{infos.endian}'\n") ofile.write(f"system = '{infos.system}'\n") + if infos.subsystem: + ofile.write(f"subsystem = '{infos.subsystem}'\n") + if infos.kernel: + ofile.write(f"kernel = '{infos.kernel}'\n") + os.replace(tmpfilename, ofilename) def detect_language_args_from_envvars(langname: str, envvar_suffix: str = '') -> T.Tuple[T.List[str], T.List[str]]: ldflags = tuple(shlex.split(os.environ.get('LDFLAGS' + envvar_suffix, ''))) - compile_args = shlex.split(os.environ.get(compilers.CFLAGS_MAPPING[langname] + envvar_suffix, '')) + compile_args = [] + if langname in compilers.CFLAGS_MAPPING: + compile_args = shlex.split(os.environ.get(compilers.CFLAGS_MAPPING[langname] + envvar_suffix, '')) if langname in compilers.LANGUAGES_USING_CPPFLAGS: cppflags = tuple(shlex.split(os.environ.get('CPPFLAGS' + envvar_suffix, ''))) lang_compile_args = list(cppflags) + compile_args @@ -271,7 +298,10 @@ def detect_compilers_from_envvars(envvar_suffix: str = '') -> MachineInfo: compilerstr = os.environ.get(envvarname + envvar_suffix) if not compilerstr: continue - compiler = shlex.split(compilerstr) + if os.path.exists(compilerstr): + compiler = [compilerstr] + else: + compiler = shlex.split(compilerstr) infos.compilers[langname] = compiler lang_compile_args, lang_link_args = detect_language_args_from_envvars(langname, envvar_suffix) if lang_compile_args: @@ -287,8 +317,16 @@ def detect_binaries_from_envvars(infos: MachineInfo, envvar_suffix: str = '') -> if binstr: infos.binaries[binname] = shlex.split(binstr) +def detect_properties_from_envvars(infos: MachineInfo, envvar_suffix: str = '') -> None: + var = os.environ.get('PKG_CONFIG_LIBDIR' + envvar_suffix) + if var is not None: + infos.properties['pkg_config_libdir'] = var + var = os.environ.get('PKG_CONFIG_SYSROOT_DIR' + envvar_suffix) + if var is not None: + infos.properties['sys_root'] = var + def detect_cross_system(infos: MachineInfo, options: T.Any) -> None: - for optname in ('system', 'cpu', 'cpu_family', 'endian'): + for optname in ('system', 'subsystem', 'kernel', 'cpu', 'cpu_family', 'endian'): v = getattr(options, optname) if not v: mlog.error(f'Cross property "{optname}" missing, set it with --{optname.replace("_", "-")}.') @@ -303,6 +341,8 @@ def detect_cross_env(options: T.Any) -> MachineInfo: print('Detecting cross environment via environment variables.') infos = detect_compilers_from_envvars() detect_cross_system(infos, options) + detect_binaries_from_envvars(infos) + detect_properties_from_envvars(infos) return infos def add_compiler_if_missing(infos: MachineInfo, langname: str, exe_names: T.List[str]) -> None: @@ -348,6 +388,7 @@ def detect_native_env(options: T.Any) -> MachineInfo: detect_missing_native_compilers(infos) detect_binaries_from_envvars(infos, esuffix) detect_missing_native_binaries(infos) + detect_properties_from_envvars(infos, esuffix) return infos def run(options: T.Any) -> None: diff --git a/mesonbuild/scripts/externalproject.py b/mesonbuild/scripts/externalproject.py index f15f26a..17c2251 100644 --- a/mesonbuild/scripts/externalproject.py +++ b/mesonbuild/scripts/externalproject.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import argparse diff --git a/mesonbuild/scripts/gettext.py b/mesonbuild/scripts/gettext.py index c31657a..4a6bb9c 100644 --- a/mesonbuild/scripts/gettext.py +++ b/mesonbuild/scripts/gettext.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import argparse diff --git a/mesonbuild/scripts/gtkdochelper.py b/mesonbuild/scripts/gtkdochelper.py index 260d658..ded952d 100644 --- a/mesonbuild/scripts/gtkdochelper.py +++ b/mesonbuild/scripts/gtkdochelper.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sys, os import subprocess diff --git a/mesonbuild/scripts/hotdochelper.py b/mesonbuild/scripts/hotdochelper.py index a96a34a..80365a0 100644 --- a/mesonbuild/scripts/hotdochelper.py +++ b/mesonbuild/scripts/hotdochelper.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil import subprocess @@ -13,14 +15,16 @@ parser.add_argument('--extra-extension-path', action="append", default=[]) parser.add_argument('--name') parser.add_argument('--builddir') parser.add_argument('--project-version') +parser.add_argument('--docdir') def run(argv: T.List[str]) -> int: options, args = parser.parse_known_args(argv) subenv = os.environ.copy() - for ext_path in options.extra_extension_path: - subenv['PYTHONPATH'] = subenv.get('PYTHONPATH', '') + ':' + ext_path + val = subenv.get('PYTHONPATH') + paths = [val] if val else [] + subenv['PYTHONPATH'] = os.pathsep.join(paths + options.extra_extension_path) res = subprocess.call(args, cwd=options.builddir, env=subenv) if res != 0: @@ -29,9 +33,7 @@ def run(argv: T.List[str]) -> int: if options.install: source_dir = os.path.join(options.builddir, options.install) destdir = os.environ.get('DESTDIR', '') - installdir = destdir_join(destdir, - os.path.join(os.environ['MESON_INSTALL_PREFIX'], - 'share/doc/', options.name, "html")) + installdir = destdir_join(destdir, options.docdir) shutil.rmtree(installdir, ignore_errors=True) shutil.copytree(source_dir, installdir) diff --git a/mesonbuild/scripts/itstool.py b/mesonbuild/scripts/itstool.py index b23ad8a..16af1c2 100644 --- a/mesonbuild/scripts/itstool.py +++ b/mesonbuild/scripts/itstool.py @@ -11,11 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import argparse import subprocess import tempfile +import shlex import shutil import typing as T @@ -55,7 +57,7 @@ def run_join(build_dir: str, itstool: str, its_files: T.List[str], mo_files: T.L shutil.copy(mo_file, tmp_mo_fname) locale_mo_files.append(tmp_mo_fname) - cmd = [itstool] + cmd = shlex.split(itstool) if its_files: for fname in its_files: cmd.extend(['-i', fname]) diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py index 3dd91c9..da89dd4 100644 --- a/mesonbuild/scripts/meson_exe.py +++ b/mesonbuild/scripts/meson_exe.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import sys @@ -71,7 +72,8 @@ def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[T.Dict[str, str] if p.returncode == 0xc0000135: # STATUS_DLL_NOT_FOUND on Windows indicating a common problem that is otherwise hard to diagnose - raise FileNotFoundError('due to missing DLLs') + strerror = 'Failed to run due to missing DLLs, with path: ' + child_env['PATH'] + raise FileNotFoundError(p.returncode, strerror, cmd_args) if p.returncode != 0: if exe.pickled: diff --git a/mesonbuild/scripts/msgfmthelper.py b/mesonbuild/scripts/msgfmthelper.py index b308f54..28bcc8b 100644 --- a/mesonbuild/scripts/msgfmthelper.py +++ b/mesonbuild/scripts/msgfmthelper.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import argparse import subprocess diff --git a/mesonbuild/scripts/pycompile.py b/mesonbuild/scripts/pycompile.py new file mode 100644 index 0000000..b236a1c --- /dev/null +++ b/mesonbuild/scripts/pycompile.py @@ -0,0 +1,65 @@ +# Copyright 2016 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ignore all lints for this file, since it is run by python2 as well + +# type: ignore +# pylint: disable=deprecated-module + +import json, os, subprocess, sys +from compileall import compile_file + +quiet = int(os.environ.get('MESON_INSTALL_QUIET', 0)) + +def compileall(files): + for f in files: + # f is prefixed by {py_xxxxlib}, both variants are 12 chars + # the key is the middle 10 chars of the prefix + key = f[1:11].upper() + f = f[12:] + + ddir = None + fullpath = os.environ['MESON_INSTALL_DESTDIR_'+key] + f + f = os.environ['MESON_INSTALL_'+key] + f + + if fullpath != f: + ddir = os.path.dirname(f) + + if os.path.isdir(fullpath): + for root, _, files in os.walk(fullpath): + if ddir is not None: + ddir = root.replace(fullpath, f, 1) + for dirf in files: + if dirf.endswith('.py'): + fullpath = os.path.join(root, dirf) + compile_file(fullpath, ddir, force=True, quiet=quiet) + else: + compile_file(fullpath, ddir, force=True, quiet=quiet) + +def run(manifest): + data_file = os.path.join(os.path.dirname(__file__), manifest) + with open(data_file, 'rb') as f: + dat = json.load(f) + compileall(dat) + +if __name__ == '__main__': + manifest = sys.argv[1] + run(manifest) + if len(sys.argv) > 2: + optlevel = int(sys.argv[2]) + # python2 only needs one or the other + if optlevel == 1 or (sys.version_info >= (3,) and optlevel > 0): + subprocess.check_call([sys.executable, '-O'] + sys.argv[:2]) + if optlevel == 2: + subprocess.check_call([sys.executable, '-OO'] + sys.argv[:2]) diff --git a/mesonbuild/scripts/python_info.py b/mesonbuild/scripts/python_info.py new file mode 100755 index 0000000..5b048ca --- /dev/null +++ b/mesonbuild/scripts/python_info.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +# ignore all lints for this file, since it is run by python2 as well + +# type: ignore +# pylint: disable=deprecated-module + +import sys + +# do not inject mesonbuild.scripts +# python -P would work too, but is exclusive to >=3.11 +if sys.path[0].endswith('scripts'): + del sys.path[0] + +import json, os, sysconfig + +def get_distutils_paths(scheme=None, prefix=None): + import distutils.dist + distribution = distutils.dist.Distribution() + install_cmd = distribution.get_command_obj('install') + if prefix is not None: + install_cmd.prefix = prefix + if scheme: + install_cmd.select_scheme(scheme) + install_cmd.finalize_options() + return { + 'data': install_cmd.install_data, + 'include': os.path.dirname(install_cmd.install_headers), + 'platlib': install_cmd.install_platlib, + 'purelib': install_cmd.install_purelib, + 'scripts': install_cmd.install_scripts, + } + +# On Debian derivatives, the Python interpreter shipped by the distribution uses +# a custom install scheme, deb_system, for the system install, and changes the +# default scheme to a custom one pointing to /usr/local and replacing +# site-packages with dist-packages. +# See https://github.com/mesonbuild/meson/issues/8739. +# +# We should be using sysconfig, but before 3.10.3, Debian only patches distutils. +# So we may end up falling back. + +def get_install_paths(): + if sys.version_info >= (3, 10): + scheme = sysconfig.get_default_scheme() + else: + scheme = sysconfig._get_default_scheme() + + if sys.version_info >= (3, 10, 3): + if 'deb_system' in sysconfig.get_scheme_names(): + scheme = 'deb_system' + else: + import distutils.command.install + if 'deb_system' in distutils.command.install.INSTALL_SCHEMES: + paths = get_distutils_paths(scheme='deb_system') + install_paths = get_distutils_paths(scheme='deb_system', prefix='') + return paths, install_paths + + paths = sysconfig.get_paths(scheme=scheme) + empty_vars = {'base': '', 'platbase': '', 'installed_base': ''} + install_paths = sysconfig.get_paths(scheme=scheme, vars=empty_vars) + return paths, install_paths + +paths, install_paths = get_install_paths() + +def links_against_libpython(): + # on versions supporting python-embed.pc, this is the non-embed lib + # + # PyPy is not yet up to 3.12 and work is still pending to export the + # relevant information (it doesn't automatically provide arbitrary + # Makefile vars) + if sys.version_info >= (3, 8) and not is_pypy: + variables = sysconfig.get_config_vars() + return bool(variables.get('LIBPYTHON', 'yes')) + else: + from distutils.core import Distribution, Extension + cmd = Distribution().get_command_obj('build_ext') + cmd.ensure_finalized() + return bool(cmd.get_libraries(Extension('dummy', []))) + +variables = sysconfig.get_config_vars() +variables.update({'base_prefix': getattr(sys, 'base_prefix', sys.prefix)}) + +is_pypy = '__pypy__' in sys.builtin_module_names + +if sys.version_info < (3, 0): + suffix = variables.get('SO') +elif sys.version_info < (3, 8, 7): + # https://bugs.python.org/issue?@action=redirect&bpo=39825 + from distutils.sysconfig import get_config_var + suffix = get_config_var('EXT_SUFFIX') +else: + suffix = variables.get('EXT_SUFFIX') + +limited_api_suffix = None +if sys.version_info >= (3, 2): + try: + from importlib.machinery import EXTENSION_SUFFIXES + limited_api_suffix = EXTENSION_SUFFIXES[1] + except Exception: + pass + +# pypy supports modules targetting the limited api but +# does not use a special suffix to distinguish them: +# https://doc.pypy.org/en/latest/cpython_differences.html#permitted-abi-tags-in-extensions +if is_pypy: + limited_api_suffix = suffix + +print(json.dumps({ + 'variables': variables, + 'paths': paths, + 'sysconfig_paths': sysconfig.get_paths(), + 'install_paths': install_paths, + 'version': sysconfig.get_python_version(), + 'platform': sysconfig.get_platform(), + 'is_pypy': is_pypy, + 'is_venv': sys.prefix != variables['base_prefix'], + 'link_libpython': links_against_libpython(), + 'suffix': suffix, + 'limited_api_suffix': limited_api_suffix, +})) diff --git a/mesonbuild/scripts/regen_checker.py b/mesonbuild/scripts/regen_checker.py index c96bdc1..f3a6f3c 100644 --- a/mesonbuild/scripts/regen_checker.py +++ b/mesonbuild/scripts/regen_checker.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sys, os import pickle, subprocess diff --git a/mesonbuild/scripts/run_tool.py b/mesonbuild/scripts/run_tool.py index 88376dd..adf767a 100644 --- a/mesonbuild/scripts/run_tool.py +++ b/mesonbuild/scripts/run_tool.py @@ -19,7 +19,7 @@ from pathlib import Path from concurrent.futures import ThreadPoolExecutor from ..compilers import lang_suffixes -from ..mesonlib import Popen_safe +from ..mesonlib import quiet_git import typing as T if T.TYPE_CHECKING: @@ -43,8 +43,8 @@ def run_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[..., subpro if patterns: globs = [srcdir.glob(p) for p in patterns] else: - p, o, _ = Popen_safe(['git', 'ls-files'], cwd=srcdir) - if p.returncode == 0: + r, o = quiet_git(['ls-files'], srcdir) + if r: globs = [[Path(srcdir, f) for f in o.splitlines()]] else: globs = [srcdir.glob('**/*')] diff --git a/mesonbuild/scripts/scanbuild.py b/mesonbuild/scripts/scanbuild.py index bb8e30c..be60024 100644 --- a/mesonbuild/scripts/scanbuild.py +++ b/mesonbuild/scripts/scanbuild.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import subprocess import shutil @@ -23,12 +24,12 @@ import typing as T from ast import literal_eval import os -def scanbuild(exelist: T.List[str], srcdir: Path, blddir: Path, privdir: Path, logdir: Path, args: T.List[str]) -> int: +def scanbuild(exelist: T.List[str], srcdir: Path, blddir: Path, privdir: Path, logdir: Path, subprojdir: Path, args: T.List[str]) -> int: # In case of problems leave the temp directory around # so it can be debugged. scandir = tempfile.mkdtemp(dir=str(privdir)) meson_cmd = exelist + args - build_cmd = exelist + ['-o', str(logdir)] + detect_ninja() + ['-C', scandir] + build_cmd = exelist + ['--exclude', str(subprojdir), '-o', str(logdir)] + detect_ninja() + ['-C', scandir] rc = subprocess.call(meson_cmd + [str(srcdir), scandir]) if rc != 0: return rc @@ -40,8 +41,9 @@ def scanbuild(exelist: T.List[str], srcdir: Path, blddir: Path, privdir: Path, l def run(args: T.List[str]) -> int: srcdir = Path(args[0]) bldpath = Path(args[1]) + subprojdir = srcdir / Path(args[2]) blddir = args[1] - meson_cmd = args[2:] + meson_cmd = args[3:] privdir = bldpath / 'meson-private' logdir = bldpath / 'meson-logs' / 'scanbuild' shutil.rmtree(str(logdir), ignore_errors=True) @@ -62,4 +64,4 @@ def run(args: T.List[str]) -> int: print('Could not execute scan-build "%s"' % ' '.join(exelist)) return 1 - return scanbuild(exelist, srcdir, bldpath, privdir, logdir, meson_cmd) + return scanbuild(exelist, srcdir, bldpath, privdir, logdir, subprojdir, meson_cmd) diff --git a/mesonbuild/scripts/symbolextractor.py b/mesonbuild/scripts/symbolextractor.py index 393e66f..08d839b 100644 --- a/mesonbuild/scripts/symbolextractor.py +++ b/mesonbuild/scripts/symbolextractor.py @@ -19,6 +19,7 @@ # This file is basically a reimplementation of # http://cgit.freedesktop.org/libreoffice/core/commit/?id=3213cd54b76bc80a6f0516aac75a48ff3b2ad67c +from __future__ import annotations import typing as T import os, sys diff --git a/mesonbuild/scripts/tags.py b/mesonbuild/scripts/tags.py index 8f1706d..c856807 100644 --- a/mesonbuild/scripts/tags.py +++ b/mesonbuild/scripts/tags.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import subprocess diff --git a/mesonbuild/scripts/test_loaded_modules.py b/mesonbuild/scripts/test_loaded_modules.py index b3547be..f824134 100644 --- a/mesonbuild/scripts/test_loaded_modules.py +++ b/mesonbuild/scripts/test_loaded_modules.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import sys import json import typing as T + from . import meson_exe # This script is used by run_unittests.py to verify we don't load too many diff --git a/mesonbuild/scripts/uninstall.py b/mesonbuild/scripts/uninstall.py index f08490f..8548766 100644 --- a/mesonbuild/scripts/uninstall.py +++ b/mesonbuild/scripts/uninstall.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import os import typing as T diff --git a/mesonbuild/scripts/vcstagger.py b/mesonbuild/scripts/vcstagger.py index 18cf5f7..c484ee1 100644 --- a/mesonbuild/scripts/vcstagger.py +++ b/mesonbuild/scripts/vcstagger.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sys, os, subprocess, re import typing as T @@ -26,7 +27,7 @@ def config_vcs_tag(infile: str, outfile: str, fallback: str, source_dir: str, re new_data = f.read().replace(replace_string, new_string) if os.path.exists(outfile): with open(outfile, encoding='utf-8') as f: - needs_update = (f.read() != new_data) + needs_update = f.read() != new_data else: needs_update = True if needs_update: diff --git a/mesonbuild/scripts/yasm.py b/mesonbuild/scripts/yasm.py index 730ff3e..1d25e6f 100644 --- a/mesonbuild/scripts/yasm.py +++ b/mesonbuild/scripts/yasm.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import subprocess import typing as T diff --git a/mesonbuild/templates/cpptemplates.py b/mesonbuild/templates/cpptemplates.py index 61c2737..d3d29d3 100644 --- a/mesonbuild/templates/cpptemplates.py +++ b/mesonbuild/templates/cpptemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileHeaderImpl hello_cpp_template = '''#include @@ -140,46 +141,13 @@ pkg_mod.generate( ''' -class CppProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.cpp' - open(source_name, 'w', encoding='utf-8').write(hello_cpp_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_cpp_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - test_exe_name = lowercase_token + '_test' - namespace = lowercase_token - lib_hpp_name = lowercase_token + '.hpp' - lib_cpp_name = lowercase_token + '.cpp' - test_cpp_name = lowercase_token + '_test.cpp' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'class_name': class_name, - 'namespace': namespace, - 'header_file': lib_hpp_name, - 'source_file': lib_cpp_name, - 'test_source_file': test_cpp_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_hpp_name, 'w', encoding='utf-8').write(lib_hpp_template.format(**kwargs)) - open(lib_cpp_name, 'w', encoding='utf-8').write(lib_cpp_template.format(**kwargs)) - open(test_cpp_name, 'w', encoding='utf-8').write(lib_cpp_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_cpp_meson_template.format(**kwargs)) +class CppProject(FileHeaderImpl): + + source_ext = 'cpp' + header_ext = 'hpp' + exe_template = hello_cpp_template + exe_meson_template = hello_cpp_meson_template + lib_template = lib_cpp_template + lib_header_template = lib_hpp_template + lib_test_template = lib_cpp_test_template + lib_meson_template = lib_cpp_meson_template diff --git a/mesonbuild/templates/cstemplates.py b/mesonbuild/templates/cstemplates.py index bad7984..d2d5ec9 100644 --- a/mesonbuild/templates/cstemplates.py +++ b/mesonbuild/templates/cstemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import ClassImpl hello_cs_template = '''using System; @@ -89,46 +90,11 @@ test('{test_name}', test_exe) ''' -class CSharpProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - source_name = uppercase_token[0] + lowercase_token[1:] + '.cs' - open(source_name, 'w', encoding='utf-8').write( - hello_cs_template.format(project_name=self.name, - class_name=class_name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_cs_meson_template.format(project_name=self.name, - exe_name=self.name, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - class_test = uppercase_token[0] + lowercase_token[1:] + '_test' - project_test = lowercase_token + '_test' - lib_cs_name = uppercase_token[0] + lowercase_token[1:] + '.cs' - test_cs_name = uppercase_token[0] + lowercase_token[1:] + '_test.cs' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'class_test': class_test, - 'class_name': class_name, - 'source_file': lib_cs_name, - 'test_source_file': test_cs_name, - 'test_exe_name': project_test, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_cs_name, 'w', encoding='utf-8').write(lib_cs_template.format(**kwargs)) - open(test_cs_name, 'w', encoding='utf-8').write(lib_cs_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_cs_meson_template.format(**kwargs)) +class CSharpProject(ClassImpl): + + source_ext = 'cs' + exe_template = hello_cs_template + exe_meson_template = hello_cs_meson_template + lib_template = lib_cs_template + lib_test_template = lib_cs_test_template + lib_meson_template = lib_cs_meson_template diff --git a/mesonbuild/templates/ctemplates.py b/mesonbuild/templates/ctemplates.py index 9b651bc..14eeaf7 100644 --- a/mesonbuild/templates/ctemplates.py +++ b/mesonbuild/templates/ctemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileHeaderImpl lib_h_template = '''#pragma once @@ -123,44 +124,13 @@ test('basic', exe) ''' -class CProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.c' - open(source_name, 'w', encoding='utf-8').write(hello_c_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_c_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_h_name = lowercase_token + '.h' - lib_c_name = lowercase_token + '.c' - test_c_name = lowercase_token + '_test.c' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'header_file': lib_h_name, - 'source_file': lib_c_name, - 'test_source_file': test_c_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs)) - open(lib_c_name, 'w', encoding='utf-8').write(lib_c_template.format(**kwargs)) - open(test_c_name, 'w', encoding='utf-8').write(lib_c_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_c_meson_template.format(**kwargs)) +class CProject(FileHeaderImpl): + + source_ext = 'c' + header_ext = 'h' + exe_template = hello_c_template + exe_meson_template = hello_c_meson_template + lib_template = lib_c_template + lib_header_template = lib_h_template + lib_test_template = lib_c_test_template + lib_meson_template = lib_c_meson_template diff --git a/mesonbuild/templates/cudatemplates.py b/mesonbuild/templates/cudatemplates.py index bbb1a3e..f59d79a 100644 --- a/mesonbuild/templates/cudatemplates.py +++ b/mesonbuild/templates/cudatemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileHeaderImpl hello_cuda_template = '''#include @@ -140,46 +141,13 @@ pkg_mod.generate( ''' -class CudaProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.cu' - open(source_name, 'w', encoding='utf-8').write(hello_cuda_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_cuda_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - test_exe_name = lowercase_token + '_test' - namespace = lowercase_token - lib_h_name = lowercase_token + '.h' - lib_cuda_name = lowercase_token + '.cu' - test_cuda_name = lowercase_token + '_test.cu' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'class_name': class_name, - 'namespace': namespace, - 'header_file': lib_h_name, - 'source_file': lib_cuda_name, - 'test_source_file': test_cuda_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs)) - open(lib_cuda_name, 'w', encoding='utf-8').write(lib_cuda_template.format(**kwargs)) - open(test_cuda_name, 'w', encoding='utf-8').write(lib_cuda_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_cuda_meson_template.format(**kwargs)) +class CudaProject(FileHeaderImpl): + + source_ext = 'cu' + header_ext = 'h' + exe_template = hello_cuda_template + exe_meson_template = hello_cuda_meson_template + lib_template = lib_cuda_template + lib_header_template = lib_h_template + lib_test_template = lib_cuda_test_template + lib_meson_template = lib_cuda_meson_template diff --git a/mesonbuild/templates/dlangtemplates.py b/mesonbuild/templates/dlangtemplates.py index 3d939d8..6a8a071 100644 --- a/mesonbuild/templates/dlangtemplates.py +++ b/mesonbuild/templates/dlangtemplates.py @@ -11,8 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileImpl + +import typing as T hello_d_template = '''module main; @@ -101,43 +104,16 @@ endif ''' -class DlangProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.d' - open(source_name, 'w', encoding='utf-8').write(hello_d_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_d_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_m_name = lowercase_token - lib_d_name = lowercase_token + '.d' - test_d_name = lowercase_token + '_test.d' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'module_file': lib_m_name, - 'source_file': lib_d_name, - 'test_source_file': test_d_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_d_name, 'w', encoding='utf-8').write(lib_d_template.format(**kwargs)) - open(test_d_name, 'w', encoding='utf-8').write(lib_d_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_d_meson_template.format(**kwargs)) +class DlangProject(FileImpl): + + source_ext = 'd' + exe_template = hello_d_template + exe_meson_template = hello_d_meson_template + lib_template = lib_d_template + lib_test_template = lib_d_test_template + lib_meson_template = lib_d_meson_template + + def lib_kwargs(self) -> T.Dict[str, str]: + kwargs = super().lib_kwargs() + kwargs['module_file'] = self.lowercase_token + return kwargs diff --git a/mesonbuild/templates/fortrantemplates.py b/mesonbuild/templates/fortrantemplates.py index 8fc1bca..8895e32 100644 --- a/mesonbuild/templates/fortrantemplates.py +++ b/mesonbuild/templates/fortrantemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileImpl lib_fortran_template = ''' ! This procedure will not be exported and is not @@ -100,41 +101,11 @@ test('basic', exe) ''' -class FortranProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.f90' - open(source_name, 'w', encoding='utf-8').write(hello_fortran_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_fortran_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_fortran_name = lowercase_token + '.f90' - test_fortran_name = lowercase_token + '_test.f90' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'source_file': lib_fortran_name, - 'test_source_file': test_fortran_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_fortran_name, 'w', encoding='utf-8').write(lib_fortran_template.format(**kwargs)) - open(test_fortran_name, 'w', encoding='utf-8').write(lib_fortran_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_fortran_meson_template.format(**kwargs)) +class FortranProject(FileImpl): + + source_ext = 'f90' + exe_template = hello_fortran_template + exe_meson_template = hello_fortran_meson_template + lib_template = lib_fortran_template + lib_meson_template = lib_fortran_meson_template + lib_test_template = lib_fortran_test_template diff --git a/mesonbuild/templates/javatemplates.py b/mesonbuild/templates/javatemplates.py index e432961..4163ffd 100644 --- a/mesonbuild/templates/javatemplates.py +++ b/mesonbuild/templates/javatemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import ClassImpl hello_java_template = ''' @@ -93,44 +94,11 @@ test('{test_name}', test_jar) ''' -class JavaProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - source_name = uppercase_token[0] + lowercase_token[1:] + '.java' - open(source_name, 'w', encoding='utf-8').write( - hello_java_template.format(project_name=self.name, - class_name=class_name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_java_meson_template.format(project_name=self.name, - exe_name=class_name, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - class_test = uppercase_token[0] + lowercase_token[1:] + '_test' - lib_java_name = uppercase_token[0] + lowercase_token[1:] + '.java' - test_java_name = uppercase_token[0] + lowercase_token[1:] + '_test.java' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'class_test': class_test, - 'class_name': class_name, - 'source_file': lib_java_name, - 'test_source_file': test_java_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_java_name, 'w', encoding='utf-8').write(lib_java_template.format(**kwargs)) - open(test_java_name, 'w', encoding='utf-8').write(lib_java_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_java_meson_template.format(**kwargs)) +class JavaProject(ClassImpl): + + source_ext = 'java' + exe_template = hello_java_template + exe_meson_template = hello_java_meson_template + lib_template = lib_java_template + lib_test_template = lib_java_test_template + lib_meson_template = lib_java_meson_template diff --git a/mesonbuild/templates/mesontemplates.py b/mesonbuild/templates/mesontemplates.py index 69939be..bc059fa 100644 --- a/mesonbuild/templates/mesontemplates.py +++ b/mesonbuild/templates/mesontemplates.py @@ -11,8 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations -import argparse +import typing as T + +if T.TYPE_CHECKING: + from ..minit import Arguments meson_executable_template = '''project('{project_name}', {language}, version : '{version}', @@ -35,7 +39,7 @@ jar('{executable}', ''' -def create_meson_build(options: argparse.Namespace) -> None: +def create_meson_build(options: Arguments) -> None: if options.type != 'executable': raise SystemExit('\nGenerating a meson.build file from existing sources is\n' 'supported only for project type "executable".\n' diff --git a/mesonbuild/templates/objcpptemplates.py b/mesonbuild/templates/objcpptemplates.py index 11a5091..a102165 100644 --- a/mesonbuild/templates/objcpptemplates.py +++ b/mesonbuild/templates/objcpptemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileHeaderImpl lib_h_template = '''#pragma once @@ -123,44 +124,13 @@ test('basic', exe) ''' -class ObjCppProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.mm' - open(source_name, 'w', encoding='utf-8').write(hello_objcpp_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_objcpp_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_h_name = lowercase_token + '.h' - lib_objcpp_name = lowercase_token + '.mm' - test_objcpp_name = lowercase_token + '_test.mm' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'header_file': lib_h_name, - 'source_file': lib_objcpp_name, - 'test_source_file': test_objcpp_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs)) - open(lib_objcpp_name, 'w', encoding='utf-8').write(lib_objcpp_template.format(**kwargs)) - open(test_objcpp_name, 'w', encoding='utf-8').write(lib_objcpp_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_objcpp_meson_template.format(**kwargs)) +class ObjCppProject(FileHeaderImpl): + + source_ext = 'mm' + header_ext = 'h' + exe_template = hello_objcpp_template + exe_meson_template = hello_objcpp_meson_template + lib_template = lib_objcpp_template + lib_header_template = lib_h_template + lib_test_template = lib_objcpp_test_template + lib_meson_template = lib_objcpp_meson_template diff --git a/mesonbuild/templates/objctemplates.py b/mesonbuild/templates/objctemplates.py index dac638d..4e31beb 100644 --- a/mesonbuild/templates/objctemplates.py +++ b/mesonbuild/templates/objctemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileHeaderImpl lib_h_template = '''#pragma once @@ -123,44 +124,13 @@ test('basic', exe) ''' -class ObjCProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.m' - open(source_name, 'w', encoding='utf-8').write(hello_objc_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_objc_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_h_name = lowercase_token + '.h' - lib_objc_name = lowercase_token + '.m' - test_objc_name = lowercase_token + '_test.m' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'header_file': lib_h_name, - 'source_file': lib_objc_name, - 'test_source_file': test_objc_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_h_name, 'w', encoding='utf-8').write(lib_h_template.format(**kwargs)) - open(lib_objc_name, 'w', encoding='utf-8').write(lib_objc_template.format(**kwargs)) - open(test_objc_name, 'w', encoding='utf-8').write(lib_objc_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_objc_meson_template.format(**kwargs)) +class ObjCProject(FileHeaderImpl): + + source_ext = 'm' + header_ext = 'h' + exe_template = hello_objc_template + exe_meson_template = hello_objc_meson_template + lib_template = lib_objc_template + lib_header_template = lib_h_template + lib_test_template = lib_objc_test_template + lib_meson_template = lib_objc_meson_template diff --git a/mesonbuild/templates/rusttemplates.py b/mesonbuild/templates/rusttemplates.py index 95a937c..26548b8 100644 --- a/mesonbuild/templates/rusttemplates.py +++ b/mesonbuild/templates/rusttemplates.py @@ -11,8 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +import typing as T + +from mesonbuild.templates.sampleimpl import FileImpl lib_rust_template = '''#![crate_name = "{crate_file}"] @@ -71,43 +74,16 @@ test('basic', exe) ''' -class RustProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.rs' - open(source_name, 'w', encoding='utf-8').write(hello_rust_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_rust_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - function_name = lowercase_token[0:3] + '_func' - test_exe_name = lowercase_token + '_test' - lib_crate_name = lowercase_token - lib_rs_name = lowercase_token + '.rs' - test_rs_name = lowercase_token + '_test.rs' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'function_name': function_name, - 'crate_file': lib_crate_name, - 'source_file': lib_rs_name, - 'test_source_file': test_rs_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_rs_name, 'w', encoding='utf-8').write(lib_rust_template.format(**kwargs)) - open(test_rs_name, 'w', encoding='utf-8').write(lib_rust_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_rust_meson_template.format(**kwargs)) +class RustProject(FileImpl): + + source_ext = 'rs' + exe_template = hello_rust_template + exe_meson_template = hello_rust_meson_template + lib_template = lib_rust_template + lib_test_template = lib_rust_test_template + lib_meson_template = lib_rust_meson_template + + def lib_kwargs(self) -> T.Dict[str, str]: + kwargs = super().lib_kwargs() + kwargs['crate_file'] = self.lowercase_token + return kwargs diff --git a/mesonbuild/templates/samplefactory.py b/mesonbuild/templates/samplefactory.py index 3678a02..8e200e2 100644 --- a/mesonbuild/templates/samplefactory.py +++ b/mesonbuild/templates/samplefactory.py @@ -11,6 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + +import typing as T + from mesonbuild.templates.valatemplates import ValaProject from mesonbuild.templates.fortrantemplates import FortranProject from mesonbuild.templates.objcpptemplates import ObjCppProject @@ -22,21 +26,26 @@ from mesonbuild.templates.objctemplates import ObjCProject from mesonbuild.templates.cpptemplates import CppProject from mesonbuild.templates.cstemplates import CSharpProject from mesonbuild.templates.ctemplates import CProject -from mesonbuild.templates.sampleimpl import SampleImpl - -import argparse - -def sameple_generator(options: argparse.Namespace) -> SampleImpl: - return { - 'c': CProject, - 'cpp': CppProject, - 'cs': CSharpProject, - 'cuda': CudaProject, - 'objc': ObjCProject, - 'objcpp': ObjCppProject, - 'java': JavaProject, - 'd': DlangProject, - 'rust': RustProject, - 'fortran': FortranProject, - 'vala': ValaProject - }[options.language](options) + +if T.TYPE_CHECKING: + from ..minit import Arguments + from .sampleimpl import ClassImpl, FileHeaderImpl, FileImpl, SampleImpl + + +_IMPL: T.Mapping[str, T.Union[T.Type[ClassImpl], T.Type[FileHeaderImpl], T.Type[FileImpl]]] = { + 'c': CProject, + 'cpp': CppProject, + 'cs': CSharpProject, + 'cuda': CudaProject, + 'objc': ObjCProject, + 'objcpp': ObjCppProject, + 'java': JavaProject, + 'd': DlangProject, + 'rust': RustProject, + 'fortran': FortranProject, + 'vala': ValaProject, +} + + +def sample_generator(options: Arguments) -> SampleImpl: + return _IMPL[options.language](options) diff --git a/mesonbuild/templates/sampleimpl.py b/mesonbuild/templates/sampleimpl.py index 2d1498b..e34cad7 100644 --- a/mesonbuild/templates/sampleimpl.py +++ b/mesonbuild/templates/sampleimpl.py @@ -11,11 +11,160 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations +import abc +import re +import typing as T + +if T.TYPE_CHECKING: + from ..minit import Arguments + + +class SampleImpl(metaclass=abc.ABCMeta): + + def __init__(self, args: Arguments): + self.name = args.name + self.version = args.version + self.lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) + self.uppercase_token = self.lowercase_token.upper() + self.capitalized_token = self.lowercase_token.capitalize() + + @abc.abstractmethod + def create_executable(self) -> None: + pass + + @abc.abstractmethod + def create_library(self) -> None: + pass + + @abc.abstractproperty + def exe_template(self) -> str: + pass + + @abc.abstractproperty + def exe_meson_template(self) -> str: + pass + + @abc.abstractproperty + def lib_template(self) -> str: + pass + + @abc.abstractproperty + def lib_test_template(self) -> str: + pass + + @abc.abstractproperty + def lib_meson_template(self) -> str: + pass + + @abc.abstractproperty + def source_ext(self) -> str: + pass + + +class ClassImpl(SampleImpl): + + """For Class based languages, like Java and C#""" -class SampleImpl: def create_executable(self) -> None: - raise NotImplementedError('Sample implementation for "executable" not implemented!') + source_name = f'{self.capitalized_token}.{self.source_ext}' + with open(source_name, 'w', encoding='utf-8') as f: + f.write(self.exe_template.format(project_name=self.name, + class_name=self.capitalized_token)) + with open('meson.build', 'w', encoding='utf-8') as f: + f.write(self.exe_meson_template.format(project_name=self.name, + exe_name=self.name, + source_name=source_name, + version=self.version)) + + def create_library(self) -> None: + lib_name = f'{self.capitalized_token}.{self.source_ext}' + test_name = f'{self.capitalized_token}_test.{self.source_ext}' + kwargs = {'utoken': self.uppercase_token, + 'ltoken': self.lowercase_token, + 'class_test': f'{self.capitalized_token}_test', + 'class_name': self.capitalized_token, + 'source_file': lib_name, + 'test_source_file': test_name, + 'test_exe_name': f'{self.lowercase_token}_test', + 'project_name': self.name, + 'lib_name': self.lowercase_token, + 'test_name': self.lowercase_token, + 'version': self.version, + } + with open(lib_name, 'w', encoding='utf-8') as f: + f.write(self.lib_template.format(**kwargs)) + with open(test_name, 'w', encoding='utf-8') as f: + f.write(self.lib_test_template.format(**kwargs)) + with open('meson.build', 'w', encoding='utf-8') as f: + f.write(self.lib_meson_template.format(**kwargs)) + + +class FileImpl(SampleImpl): + + """File based languages without headers""" + + def create_executable(self) -> None: + source_name = f'{self.lowercase_token}.{self.source_ext}' + with open(source_name, 'w', encoding='utf-8') as f: + f.write(self.exe_template.format(project_name=self.name)) + with open('meson.build', 'w', encoding='utf-8') as f: + f.write(self.exe_meson_template.format(project_name=self.name, + exe_name=self.name, + source_name=source_name, + version=self.version)) + + def lib_kwargs(self) -> T.Dict[str, str]: + """Get Language specific keyword arguments + + :return: A dictionary of key: values to fill in the templates + """ + return { + 'utoken': self.uppercase_token, + 'ltoken': self.lowercase_token, + 'header_dir': self.lowercase_token, + 'class_name': self.capitalized_token, + 'function_name': f'{self.lowercase_token[0:3]}_func', + 'namespace': self.lowercase_token, + 'source_file': f'{self.lowercase_token}.{self.source_ext}', + 'test_source_file': f'{self.lowercase_token}_test.{self.source_ext}', + 'test_exe_name': f'{self.lowercase_token}_test', + 'project_name': self.name, + 'lib_name': self.lowercase_token, + 'test_name': self.lowercase_token, + 'version': self.version, + } + + def create_library(self) -> None: + lib_name = f'{self.lowercase_token}.{self.source_ext}' + test_name = f'{self.lowercase_token}_test.{self.source_ext}' + kwargs = self.lib_kwargs() + with open(lib_name, 'w', encoding='utf-8') as f: + f.write(self.lib_template.format(**kwargs)) + with open(test_name, 'w', encoding='utf-8') as f: + f.write(self.lib_test_template.format(**kwargs)) + with open('meson.build', 'w', encoding='utf-8') as f: + f.write(self.lib_meson_template.format(**kwargs)) + + +class FileHeaderImpl(FileImpl): + + @abc.abstractproperty + def header_ext(self) -> str: + pass + + @abc.abstractproperty + def lib_header_template(self) -> str: + pass + + def lib_kwargs(self) -> T.Dict[str, str]: + kwargs = super().lib_kwargs() + kwargs['header_file'] = f'{self.lowercase_token}.{self.header_ext}' + return kwargs def create_library(self) -> None: - raise NotImplementedError('Sample implementation for "library" not implemented!') + super().create_library() + kwargs = self.lib_kwargs() + with open(kwargs['header_file'], 'w', encoding='utf-8') as f: + f.write(self.lib_header_template.format_map(kwargs)) diff --git a/mesonbuild/templates/valatemplates.py b/mesonbuild/templates/valatemplates.py index 1aeb46a..aa82de7 100644 --- a/mesonbuild/templates/valatemplates.py +++ b/mesonbuild/templates/valatemplates.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.templates.sampleimpl import SampleImpl -import re +from __future__ import annotations + +from mesonbuild.templates.sampleimpl import FileImpl hello_vala_template = '''void main (string[] args) {{ @@ -81,43 +82,11 @@ test('{test_name}', test_exe) ''' -class ValaProject(SampleImpl): - def __init__(self, options): - super().__init__() - self.name = options.name - self.version = options.version - - def create_executable(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - source_name = lowercase_token + '.vala' - open(source_name, 'w', encoding='utf-8').write(hello_vala_template.format(project_name=self.name)) - open('meson.build', 'w', encoding='utf-8').write( - hello_vala_meson_template.format(project_name=self.name, - exe_name=lowercase_token, - source_name=source_name, - version=self.version)) - - def create_library(self) -> None: - lowercase_token = re.sub(r'[^a-z0-9]', '_', self.name.lower()) - uppercase_token = lowercase_token.upper() - class_name = uppercase_token[0] + lowercase_token[1:] - test_exe_name = lowercase_token + '_test' - namespace = lowercase_token - lib_vala_name = lowercase_token + '.vala' - test_vala_name = lowercase_token + '_test.vala' - kwargs = {'utoken': uppercase_token, - 'ltoken': lowercase_token, - 'header_dir': lowercase_token, - 'class_name': class_name, - 'namespace': namespace, - 'source_file': lib_vala_name, - 'test_source_file': test_vala_name, - 'test_exe_name': test_exe_name, - 'project_name': self.name, - 'lib_name': lowercase_token, - 'test_name': lowercase_token, - 'version': self.version, - } - open(lib_vala_name, 'w', encoding='utf-8').write(lib_vala_template.format(**kwargs)) - open(test_vala_name, 'w', encoding='utf-8').write(lib_vala_test_template.format(**kwargs)) - open('meson.build', 'w', encoding='utf-8').write(lib_vala_meson_template.format(**kwargs)) +class ValaProject(FileImpl): + + source_ext = 'vala' + exe_template = hello_vala_template + exe_meson_template = hello_vala_meson_template + lib_template = lib_vala_template + lib_test_template = lib_vala_test_template + lib_meson_template = lib_vala_meson_template diff --git a/mesonbuild/utils/core.py b/mesonbuild/utils/core.py index 5450cdc..6e2ec6a 100644 --- a/mesonbuild/utils/core.py +++ b/mesonbuild/utils/core.py @@ -27,19 +27,14 @@ import abc import typing as T if T.TYPE_CHECKING: + from hashlib import _Hash from typing_extensions import Literal from ..mparser import BaseNode - from . import programs + from .. import programs + EnvironOrDict = T.Union[T.Dict[str, str], os._Environ[str]] -__all__ = [ - 'MesonException', - 'MesonBugException', - 'HoldableObject', - 'EnvInitValueType', - 'EnvironmentVariables', - 'ExecutableSerialisation', -] + EnvInitValueType = T.Dict[str, T.Union[str, T.List[str]]] class MesonException(Exception): @@ -73,12 +68,10 @@ class HoldableObject(metaclass=abc.ABCMeta): ''' Dummy base class for all objects that can be held by an interpreter.baseobjects.ObjectHolder ''' -EnvInitValueType = T.Dict[str, T.Union[str, T.List[str]]] - class EnvironmentVariables(HoldableObject): def __init__(self, values: T.Optional[EnvInitValueType] = None, init_method: Literal['set', 'prepend', 'append'] = 'set', separator: str = os.pathsep) -> None: - self.envvars: T.List[T.Tuple[T.Callable[[T.Dict[str, str], str, T.List[str], str], str], str, T.List[str], str]] = [] + self.envvars: T.List[T.Tuple[T.Callable[[T.Dict[str, str], str, T.List[str], str, T.Optional[str]], str], str, T.List[str], str]] = [] # The set of all env vars we have operations for. Only used for self.has_name() self.varnames: T.Set[str] = set() @@ -92,7 +85,7 @@ class EnvironmentVariables(HoldableObject): repr_str = "<{0}: {1}>" return repr_str.format(self.__class__.__name__, self.envvars) - def hash(self, hasher: T.Any): + def hash(self, hasher: _Hash) -> None: myenv = self.get_env({}) for key in sorted(myenv.keys()): hasher.update(bytes(key, encoding='utf-8')) @@ -106,6 +99,11 @@ class EnvironmentVariables(HoldableObject): def get_names(self) -> T.Set[str]: return self.varnames + def merge(self, other: EnvironmentVariables) -> None: + for method, name, values, separator in other.envvars: + self.varnames.add(name) + self.envvars.append((method, name, values, separator)) + def set(self, name: str, values: T.List[str], separator: str = os.pathsep) -> None: self.varnames.add(name) self.envvars.append((self._set, name, values, separator)) @@ -132,10 +130,10 @@ class EnvironmentVariables(HoldableObject): curr = env.get(name, default_value) return separator.join(values if curr is None else values + [curr]) - def get_env(self, full_env: T.MutableMapping[str, str], dump: bool = False) -> T.Dict[str, str]: + def get_env(self, full_env: EnvironOrDict, default_fmt: T.Optional[str] = None) -> T.Dict[str, str]: env = full_env.copy() for method, name, values, separator in self.envvars: - default_value = f'${name}' if dump else None + default_value = default_fmt.format(name) if default_fmt else None env[name] = method(env, name, values, separator, default_value) return env @@ -143,19 +141,19 @@ class EnvironmentVariables(HoldableObject): @dataclass(eq=False) class ExecutableSerialisation: - # XXX: should capture and feed default to False, instead of None? - cmd_args: T.List[str] env: T.Optional[EnvironmentVariables] = None exe_wrapper: T.Optional['programs.ExternalProgram'] = None workdir: T.Optional[str] = None extra_paths: T.Optional[T.List] = None - capture: T.Optional[bool] = None - feed: T.Optional[bool] = None + capture: T.Optional[str] = None + feed: T.Optional[str] = None tag: T.Optional[str] = None verbose: bool = False + installdir_map: T.Optional[T.Dict[str, str]] = None def __post_init__(self) -> None: self.pickled = False self.skip_if_destdir = False self.subproject = '' + self.dry_run = False diff --git a/mesonbuild/utils/platform.py b/mesonbuild/utils/platform.py index f0676a6..4a3927d 100644 --- a/mesonbuild/utils/platform.py +++ b/mesonbuild/utils/platform.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """base classes providing no-op functionality..""" diff --git a/mesonbuild/utils/posix.py b/mesonbuild/utils/posix.py index 67f9a44..cd05d27 100644 --- a/mesonbuild/utils/posix.py +++ b/mesonbuild/utils/posix.py @@ -13,13 +13,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Posix specific implementations of mesonlib functionality.""" import fcntl import typing as T -from .universal import MesonException +from .core import MesonException from .platform import BuildDirLock as BuildDirLockBase __all__ = ['BuildDirLock'] @@ -33,6 +34,9 @@ class BuildDirLock(BuildDirLockBase): except (BlockingIOError, PermissionError): self.lockfile.close() raise MesonException('Some other Meson process is already using this build directory. Exiting.') + except OSError as e: + self.lockfile.close() + raise MesonException(f'Failed to lock the build directory: {e.strerror}') def __exit__(self, *args: T.Any) -> None: fcntl.flock(self.lockfile, fcntl.LOCK_UN) diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py index a78ca7f..2619462 100644 --- a/mesonbuild/utils/universal.py +++ b/mesonbuild/utils/universal.py @@ -29,20 +29,34 @@ from itertools import tee from tempfile import TemporaryDirectory, NamedTemporaryFile import typing as T import textwrap -import copy import pickle import errno +import json from mesonbuild import mlog from .core import MesonException, HoldableObject if T.TYPE_CHECKING: - from typing_extensions import Literal + from typing_extensions import Literal, Protocol from .._typing import ImmutableListProtocol from ..build import ConfigurationData - from ..coredata import KeyedOptionDictType, UserOption + from ..coredata import StrOrBytesPath + from ..environment import Environment from ..compilers.compilers import Compiler + from ..interpreterbase.baseobjects import SubProject + + class _EnvPickleLoadable(Protocol): + + environment: Environment + + class _VerPickleLoadable(Protocol): + + version: str + + # A generic type for pickle_load. This allows any type that has either a + # .version or a .environment to be passed. + _PL = T.TypeVar('_PL', bound=T.Union[_EnvPickleLoadable, _VerPickleLoadable]) FileOrString = T.Union['File', str] @@ -64,7 +78,6 @@ __all__ = [ 'GitException', 'OptionKey', 'dump_conf_header', - 'OptionOverrideProxy', 'OptionType', 'OrderedSet', 'PerMachine', @@ -82,6 +95,13 @@ __all__ = [ 'default_libdir', 'default_libexecdir', 'default_prefix', + 'default_datadir', + 'default_includedir', + 'default_infodir', + 'default_localedir', + 'default_mandir', + 'default_sbindir', + 'default_sysconfdir', 'detect_subprojects', 'detect_vcs', 'do_conf_file', @@ -94,7 +114,6 @@ __all__ = [ 'generate_list', 'get_compiler_for_source', 'get_filenames_templates_dict', - 'get_library_dirs', 'get_variable_regex', 'get_wine_shortpath', 'git', @@ -124,6 +143,7 @@ __all__ = [ 'path_is_in_root', 'pickle_load', 'Popen_safe', + 'Popen_safe_logged', 'quiet_git', 'quote_arg', 'relative_to_if_possible', @@ -151,13 +171,13 @@ __all__ = [ # TODO: this is such a hack, this really should be either in coredata or in the # interpreter # {subproject: project_meson_version} -project_meson_versions = collections.defaultdict(str) # type: T.DefaultDict[str, str] +project_meson_versions: T.DefaultDict[str, str] = collections.defaultdict(str) from glob import glob -if os.path.basename(sys.executable) == 'meson.exe': - # In Windows and using the MSI installed executable. +if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + # using a PyInstaller bundle, e.g. the MSI installed executable python_command = [sys.executable, 'runpython'] else: python_command = [sys.executable] @@ -173,14 +193,15 @@ class GitException(MesonException): self.output = output.strip() if output else '' GIT = shutil.which('git') -def git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False, **kwargs: T.Any) -> T.Tuple[subprocess.Popen, str, str]: - cmd = [GIT] + cmd +def git(cmd: T.List[str], workingdir: StrOrBytesPath, check: bool = False, **kwargs: T.Any) -> T.Tuple[subprocess.Popen[str], str, str]: + assert GIT is not None, 'Callers should make sure it exists' + cmd = [GIT, *cmd] p, o, e = Popen_safe(cmd, cwd=workingdir, **kwargs) if check and p.returncode != 0: raise GitException('Git command failed: ' + str(cmd), e) return p, o, e -def quiet_git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False) -> T.Tuple[bool, str]: +def quiet_git(cmd: T.List[str], workingdir: StrOrBytesPath, check: bool = False) -> T.Tuple[bool, str]: if not GIT: m = 'Git program not found.' if check: @@ -191,7 +212,7 @@ def quiet_git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], ch return False, e return True, o -def verbose_git(cmd: T.List[str], workingdir: T.Union[str, bytes, os.PathLike], check: bool = False) -> bool: +def verbose_git(cmd: T.List[str], workingdir: StrOrBytesPath, check: bool = False) -> bool: if not GIT: m = 'Git program not found.' if check: @@ -249,7 +270,7 @@ def check_direntry_issues(direntry_array: T.Union[T.Iterable[T.Union[str, bytes] You are using {e!r} which is not a Unicode-compatible locale but you are trying to access a file system entry called {de!r} which is not pure ASCII. This may cause problems. - '''), file=sys.stderr) + ''')) class SecondLevelHolder(HoldableObject, metaclass=abc.ABCMeta): ''' A second level object holder. The primary purpose @@ -465,6 +486,9 @@ class MachineChoice(enum.IntEnum): BUILD = 0 HOST = 1 + def __str__(self) -> str: + return f'{self.get_lower_case_name()} machine' + def get_lower_case_name(self) -> str: return PerMachine('build', 'host')[self] @@ -493,13 +517,17 @@ class PerMachine(T.Generic[_T]): machines, we can elaborate the original and then redefault them and thus avoid repeating the elaboration explicitly. """ - unfreeze = PerMachineDefaultable() # type: PerMachineDefaultable[T.Optional[_T]] + unfreeze: PerMachineDefaultable[T.Optional[_T]] = PerMachineDefaultable() unfreeze.build = self.build unfreeze.host = self.host if unfreeze.host == unfreeze.build: unfreeze.host = None return unfreeze + def assign(self, build: _T, host: _T) -> None: + self.build = build + self.host = host + def __repr__(self) -> str: return f'PerMachine({self.build!r}, {self.host!r})' @@ -522,7 +550,7 @@ class PerThreeMachine(PerMachine[_T]): machines, we can elaborate the original and then redefault them and thus avoid repeating the elaboration explicitly. """ - unfreeze = PerThreeMachineDefaultable() # type: PerThreeMachineDefaultable[T.Optional[_T]] + unfreeze: PerThreeMachineDefaultable[T.Optional[_T]] = PerThreeMachineDefaultable() unfreeze.build = self.build unfreeze.host = self.host unfreeze.target = self.target @@ -574,7 +602,7 @@ class PerMachineDefaultable(PerMachine[T.Optional[_T]]): return m.default_missing() -class PerThreeMachineDefaultable(PerMachineDefaultable, PerThreeMachine[T.Optional[_T]]): +class PerThreeMachineDefaultable(PerMachineDefaultable[T.Optional[_T]], PerThreeMachine[T.Optional[_T]]): """Extends `PerThreeMachine` with the ability to default from `None`s. """ def __init__(self) -> None: @@ -681,15 +709,23 @@ def darwin_get_object_archs(objpath: str) -> 'ImmutableListProtocol[str]': mlog.debug(f'lipo {objpath}: {stderr}') return None stdo = stdo.rsplit(': ', 1)[1] + # Convert from lipo-style archs to meson-style CPUs - stdo = stdo.replace('i386', 'x86') - stdo = stdo.replace('arm64', 'aarch64') - stdo = stdo.replace('ppc7400', 'ppc') - stdo = stdo.replace('ppc970', 'ppc') + map_arch = { + 'i386': 'x86', + 'arm64': 'aarch64', + 'arm64e': 'aarch64', + 'ppc7400': 'ppc', + 'ppc970': 'ppc', + } + lipo_archs = stdo.split() + meson_archs = [map_arch.get(lipo_arch, lipo_arch) for lipo_arch in lipo_archs] + # Add generic name for armv7 and armv7s if 'armv7' in stdo: - stdo += ' arm' - return stdo.split() + meson_archs.append('arm') + + return meson_archs def windows_detect_native_arch() -> str: """ @@ -730,10 +766,38 @@ def windows_detect_native_arch() -> str: def detect_vcs(source_dir: T.Union[str, Path]) -> T.Optional[T.Dict[str, str]]: vcs_systems = [ - dict(name = 'git', cmd = 'git', repo_dir = '.git', get_rev = 'git describe --dirty=+', rev_regex = '(.*)', dep = '.git/logs/HEAD'), - dict(name = 'mercurial', cmd = 'hg', repo_dir = '.hg', get_rev = 'hg id -i', rev_regex = '(.*)', dep = '.hg/dirstate'), - dict(name = 'subversion', cmd = 'svn', repo_dir = '.svn', get_rev = 'svn info', rev_regex = 'Revision: (.*)', dep = '.svn/wc.db'), - dict(name = 'bazaar', cmd = 'bzr', repo_dir = '.bzr', get_rev = 'bzr revno', rev_regex = '(.*)', dep = '.bzr'), + { + 'name': 'git', + 'cmd': 'git', + 'repo_dir': '.git', + 'get_rev': 'git describe --dirty=+ --always', + 'rev_regex': '(.*)', + 'dep': '.git/logs/HEAD' + }, + { + 'name': 'mercurial', + 'cmd': 'hg', + 'repo_dir': '.hg', + 'get_rev': 'hg id -i', + 'rev_regex': '(.*)', + 'dep': '.hg/dirstate' + }, + { + 'name': 'subversion', + 'cmd': 'svn', + 'repo_dir': '.svn', + 'get_rev': 'svn info', + 'rev_regex': 'Revision: (.*)', + 'dep': '.svn/wc.db' + }, + { + 'name': 'bazaar', + 'cmd': 'bzr', + 'repo_dir': '.bzr', + 'get_rev': 'bzr revno', + 'rev_regex': '(.*)', + 'dep': '.bzr' + }, ] if isinstance(source_dir, str): source_dir = Path(source_dir) @@ -864,8 +928,8 @@ def version_compare(vstr1: str, vstr2: str) -> bool: def version_compare_many(vstr1: str, conditions: T.Union[str, T.Iterable[str]]) -> T.Tuple[bool, T.List[str], T.List[str]]: if isinstance(conditions, str): conditions = [conditions] - found = [] - not_found = [] + found: T.List[str] = [] + not_found: T.List[str] = [] for req in conditions: if not version_compare(vstr1, req): not_found.append(req) @@ -978,58 +1042,60 @@ def default_libdir() -> str: def default_libexecdir() -> str: + if is_haiku(): + return 'lib' # There is no way to auto-detect this, so it must be set at build time return 'libexec' def default_prefix() -> str: - return 'c:/' if is_windows() else '/usr/local' + if is_windows(): + return 'c:/' + if is_haiku(): + return '/boot/system/non-packaged' + return '/usr/local' -def get_library_dirs() -> T.List[str]: - if is_windows(): - return ['C:/mingw/lib'] # TODO: get programmatically - if is_osx(): - return ['/usr/lib'] # TODO: get programmatically - # The following is probably Debian/Ubuntu specific. - # /usr/local/lib is first because it contains stuff - # installed by the sysadmin and is probably more up-to-date - # than /usr/lib. If you feel that this search order is - # problematic, please raise the issue on the mailing list. - unixdirs = ['/usr/local/lib', '/usr/lib', '/lib'] - - if is_freebsd(): - return unixdirs - # FIXME: this needs to be further genericized for aarch64 etc. - machine = platform.machine() - if machine in {'i386', 'i486', 'i586', 'i686'}: - plat = 'i386' - elif machine.startswith('arm'): - plat = 'arm' - else: - plat = '' +def default_datadir() -> str: + if is_haiku(): + return 'data' + return 'share' + + +def default_includedir() -> str: + if is_haiku(): + return 'develop/headers' + return 'include' + - # Solaris puts 32-bit libraries in the main /lib & /usr/lib directories - # and 64-bit libraries in platform specific subdirectories. - if is_sunos(): - if machine == 'i86pc': - plat = 'amd64' - elif machine.startswith('sun4'): - plat = 'sparcv9' +def default_infodir() -> str: + if is_haiku(): + return 'documentation/info' + return 'share/info' - usr_platdir = Path('/usr/lib/') / plat - if usr_platdir.is_dir(): - unixdirs += [str(x) for x in (usr_platdir).iterdir() if x.is_dir()] - if os.path.exists('/usr/lib64'): - unixdirs.append('/usr/lib64') - lib_platdir = Path('/lib/') / plat - if lib_platdir.is_dir(): - unixdirs += [str(x) for x in (lib_platdir).iterdir() if x.is_dir()] - if os.path.exists('/lib64'): - unixdirs.append('/lib64') +def default_localedir() -> str: + if is_haiku(): + return 'data/locale' + return 'share/locale' - return unixdirs + +def default_mandir() -> str: + if is_haiku(): + return 'documentation/man' + return 'share/man' + + +def default_sbindir() -> str: + if is_haiku(): + return 'bin' + return 'sbin' + + +def default_sysconfdir() -> str: + if is_haiku(): + return 'settings' + return 'etc' def has_path_sep(name: str, sep: str = '/\\') -> bool: @@ -1072,7 +1138,7 @@ if is_windows(): return result def split_args(cmd: str) -> T.List[str]: - result = [] + result: T.List[str] = [] arg = '' num_backslashes = 0 num_quotes = 0 @@ -1120,7 +1186,7 @@ def join_args(args: T.Iterable[str]) -> str: def do_replacement(regex: T.Pattern[str], line: str, variable_format: Literal['meson', 'cmake', 'cmake@'], confdata: T.Union[T.Dict[str, T.Tuple[str, T.Optional[str]]], 'ConfigurationData']) -> T.Tuple[str, T.Set[str]]: - missing_variables = set() # type: T.Set[str] + missing_variables: T.Set[str] = set() if variable_format == 'cmake': start_tag = '${' backslash_tag = '\\${' @@ -1144,6 +1210,8 @@ def do_replacement(regex: T.Pattern[str], line: str, var, _ = confdata.get(varname) if isinstance(var, str): var_str = var + elif isinstance(var, bool): + var_str = str(int(var)) elif isinstance(var, int): var_str = str(var) else: @@ -1156,42 +1224,62 @@ def do_replacement(regex: T.Pattern[str], line: str, return re.sub(regex, variable_replace, line), missing_variables def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData', - variable_format: Literal['meson', 'cmake', 'cmake@']) -> str: + variable_format: Literal['meson', 'cmake', 'cmake@'], subproject: T.Optional[SubProject] = None) -> str: + cmake_bool_define = False + if variable_format != "meson": + cmake_bool_define = "cmakedefine01" in line + def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str: arr = line.split() - define_value = [] + + if cmake_bool_define: + (v, desc) = confdata.get(arr[1]) + return str(int(bool(v))) + + define_value: T.List[str] = [] for token in arr[2:]: try: - (v, desc) = confdata.get(token) + v, _ = confdata.get(token) define_value += [str(v)] except KeyError: define_value += [token] return ' '.join(define_value) arr = line.split() - if variable_format == 'meson' and len(arr) != 2: - raise MesonException('#mesondefine does not contain exactly two tokens: %s' % line.strip()) + if len(arr) != 2: + if variable_format == 'meson': + raise MesonException('#mesondefine does not contain exactly two tokens: %s' % line.strip()) + elif subproject is not None: + from ..interpreterbase.decorators import FeatureNew + FeatureNew.single_use('cmakedefine without exactly two tokens', '0.54.1', subproject) varname = arr[1] try: - (v, desc) = confdata.get(varname) + v, _ = confdata.get(varname) except KeyError: - return '/* #undef %s */\n' % varname - if isinstance(v, bool): - if v: - return '#define %s\n' % varname + if cmake_bool_define: + return '#define %s 0\n' % varname else: - return '#undef %s\n' % varname - elif isinstance(v, int): - return '#define %s %d\n' % (varname, v) - elif isinstance(v, str): + return '/* #undef %s */\n' % varname + + if isinstance(v, str) or variable_format != "meson": if variable_format == 'meson': result = v else: + if not cmake_bool_define and not v: + return '/* #undef %s */\n' % varname + result = get_cmake_define(line, confdata) - result = f'#define {varname} {result}\n' - (result, missing_variable) = do_replacement(regex, result, variable_format, confdata) + result = f'#define {varname} {result}'.strip() + '\n' + result, _ = do_replacement(regex, result, variable_format, confdata) return result + elif isinstance(v, bool): + if v: + return '#define %s\n' % varname + else: + return '#undef %s\n' % varname + elif isinstance(v, int): + return '#define %s %d\n' % (varname, v) else: raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname) @@ -1204,9 +1292,9 @@ def get_variable_regex(variable_format: Literal['meson', 'cmake', 'cmake@'] = 'm regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}') return regex -def do_conf_str(src: str, data: list, confdata: 'ConfigurationData', +def do_conf_str(src: str, data: T.List[str], confdata: 'ConfigurationData', variable_format: Literal['meson', 'cmake', 'cmake@'], - encoding: str = 'utf-8') -> T.Tuple[T.List[str], T.Set[str], bool]: + subproject: T.Optional[SubProject] = None) -> T.Tuple[T.List[str], T.Set[str], bool]: def line_is_valid(line: str, variable_format: str) -> bool: if variable_format == 'meson': if '#cmakedefine' in line: @@ -1222,15 +1310,15 @@ def do_conf_str(src: str, data: list, confdata: 'ConfigurationData', if variable_format != 'meson': search_token = '#cmakedefine' - result = [] - missing_variables = set() + result: T.List[str] = [] + missing_variables: T.Set[str] = set() # Detect when the configuration data is empty and no tokens were found # during substitution so we can warn the user to use the `copy:` kwarg. confdata_useless = not confdata.keys() for line in data: - if line.startswith(search_token): + if line.lstrip().startswith(search_token): confdata_useless = False - line = do_define(regex, line, confdata, variable_format) + line = do_define(regex, line, confdata, variable_format, subproject) else: if not line_is_valid(line, variable_format): raise MesonException(f'Format error in {src}: saw "{line.strip()}" when format set to "{variable_format}"') @@ -1244,14 +1332,14 @@ def do_conf_str(src: str, data: list, confdata: 'ConfigurationData', def do_conf_file(src: str, dst: str, confdata: 'ConfigurationData', variable_format: Literal['meson', 'cmake', 'cmake@'], - encoding: str = 'utf-8') -> T.Tuple[T.Set[str], bool]: + encoding: str = 'utf-8', subproject: T.Optional[SubProject] = None) -> T.Tuple[T.Set[str], bool]: try: with open(src, encoding=encoding, newline='') as f: data = f.readlines() except Exception as e: raise MesonException(f'Could not read input file {src}: {e!s}') - (result, missing_variables, confdata_useless) = do_conf_str(src, data, confdata, variable_format, encoding) + (result, missing_variables, confdata_useless) = do_conf_str(src, data, confdata, variable_format, subproject) dst_tmp = dst + '~' try: with open(dst_tmp, 'w', encoding=encoding, newline='') as f: @@ -1267,7 +1355,7 @@ CONF_C_PRELUDE = '''/* * Do not edit, your changes will be lost. */ -#pragma once +{} ''' @@ -1276,34 +1364,52 @@ CONF_NASM_PRELUDE = '''; Autogenerated by the Meson build system. ''' -def dump_conf_header(ofilename: str, cdata: 'ConfigurationData', output_format: T.Literal['c', 'nasm']) -> None: +def _dump_c_header(ofile: T.TextIO, + cdata: ConfigurationData, + output_format: Literal['c', 'nasm'], + macro_name: T.Optional[str]) -> None: + format_desc: T.Callable[[str], str] if output_format == 'c': - prelude = CONF_C_PRELUDE + if macro_name: + prelude = CONF_C_PRELUDE.format('#ifndef {0}\n#define {0}'.format(macro_name)) + else: + prelude = CONF_C_PRELUDE.format('#pragma once') prefix = '#' - else: + format_desc = lambda desc: f'/* {desc} */\n' + else: # nasm prelude = CONF_NASM_PRELUDE prefix = '%' + format_desc = lambda desc: '; ' + '\n; '.join(desc.splitlines()) + '\n' + + ofile.write(prelude) + for k in sorted(cdata.keys()): + (v, desc) = cdata.get(k) + if desc: + ofile.write(format_desc(desc)) + if isinstance(v, bool): + if v: + ofile.write(f'{prefix}define {k}\n\n') + else: + ofile.write(f'{prefix}undef {k}\n\n') + elif isinstance(v, (int, str)): + ofile.write(f'{prefix}define {k} {v}\n\n') + else: + raise MesonException('Unknown data type in configuration file entry: ' + k) + if output_format == 'c' and macro_name: + ofile.write('#endif\n') + +def dump_conf_header(ofilename: str, cdata: ConfigurationData, + output_format: Literal['c', 'nasm', 'json'], + macro_name: T.Optional[str]) -> None: ofilename_tmp = ofilename + '~' with open(ofilename_tmp, 'w', encoding='utf-8') as ofile: - ofile.write(prelude) - for k in sorted(cdata.keys()): - (v, desc) = cdata.get(k) - if desc: - if output_format == 'c': - ofile.write('/* %s */\n' % desc) - elif output_format == 'nasm': - for line in desc.split('\n'): - ofile.write('; %s\n' % line) - if isinstance(v, bool): - if v: - ofile.write(f'{prefix}define {k}\n\n') - else: - ofile.write(f'{prefix}undef {k}\n\n') - elif isinstance(v, (int, str)): - ofile.write(f'{prefix}define {k} {v}\n\n') - else: - raise MesonException('Unknown data type in configuration file entry: ' + k) + if output_format == 'json': + data = {k: v[0] for k, v in cdata.values.items()} + json.dump(data, ofile, sort_keys=True) + else: # c, nasm + _dump_c_header(ofile, cdata, output_format, macro_name) + replace_if_different(ofilename, ofilename_tmp) @@ -1331,7 +1437,7 @@ def listify(item: T.Any, flatten: bool = True) -> T.List[T.Any]: ''' if not isinstance(item, list): return [item] - result = [] # type: T.List[T.Any] + result: T.List[T.Any] = [] for i in item: if flatten and isinstance(i, list): result += listify(i, flatten=True) @@ -1372,7 +1478,7 @@ def stringlistify(item: T.Union[T.Any, T.Sequence[T.Any]]) -> T.List[str]: def expand_arguments(args: T.Iterable[str]) -> T.Optional[T.List[str]]: - expended_args = [] # type: T.List[str] + expended_args: T.List[str] = [] for arg in args: if not arg.startswith('@'): expended_args.append(arg) @@ -1441,7 +1547,7 @@ def Popen_safe_legacy(args: T.List[str], write: T.Optional[str] = None, **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]: p = subprocess.Popen(args, universal_newlines=False, close_fds=False, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs) - input_ = None # type: T.Optional[bytes] + input_: T.Optional[bytes] = None if write is not None: input_ = write.encode('utf-8') o, e = p.communicate(input_) @@ -1458,6 +1564,27 @@ def Popen_safe_legacy(args: T.List[str], write: T.Optional[str] = None, return p, o, e +def Popen_safe_logged(args: T.List[str], msg: str = 'Called', **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]: + ''' + Wrapper around Popen_safe that assumes standard piped o/e and logs this to the meson log. + ''' + try: + p, o, e = Popen_safe(args, **kwargs) + except Exception as excp: + mlog.debug('-----------') + mlog.debug(f'{msg}: `{join_args(args)}` -> {excp}') + raise + + rc, out, err = p.returncode, o.strip(), e.strip() + mlog.debug('-----------') + mlog.debug(f'{msg}: `{join_args(args)}` -> {rc}') + if out: + mlog.debug(f'stdout:\n{out}\n-----------') + if err: + mlog.debug(f'stderr:\n{err}\n-----------') + return p, o, e + + def iter_regexin_iter(regexiter: T.Iterable[str], initer: T.Iterable[str]) -> T.Optional[str]: ''' Takes each regular expression in @regexiter and tries to search for it in @@ -1476,8 +1603,8 @@ def iter_regexin_iter(regexiter: T.Iterable[str], initer: T.Iterable[str]) -> T. def _substitute_values_check_errors(command: T.List[str], values: T.Dict[str, T.Union[str, T.List[str]]]) -> None: # Error checking - inregex = ['@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@'] # type: T.List[str] - outregex = ['@OUTPUT([0-9]+)?@', '@OUTDIR@'] # type: T.List[str] + inregex: T.List[str] = ['@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@'] + outregex: T.List[str] = ['@OUTPUT([0-9]+)?@', '@OUTDIR@'] if '@INPUT@' not in values: # Error out if any input-derived templates are present in the command match = iter_regexin_iter(inregex, command) @@ -1541,7 +1668,7 @@ def substitute_values(command: T.List[str], values: T.Dict[str, T.Union[str, T.L _substitute_values_check_errors(command, values) # Substitution - outcmd = [] # type: T.List[str] + outcmd: T.List[str] = [] rx_keys = [re.escape(key) for key in values if key not in ('@INPUT@', '@OUTPUT@')] value_rx = re.compile('|'.join(rx_keys)) if rx_keys else None for vv in command: @@ -1607,7 +1734,7 @@ def get_filenames_templates_dict(inputs: T.List[str], outputs: T.List[str]) -> T @OUTPUT0@, @OUTPUT1@, ... one for each output file ''' - values = {} # type: T.Dict[str, T.Union[str, T.List[str]]] + values: T.Dict[str, T.Union[str, T.List[str]]] = {} # Gather values derived from the input if inputs: # We want to substitute all the inputs. @@ -1759,8 +1886,8 @@ class OrderedSet(T.MutableSet[_T]): def __repr__(self) -> str: # Don't print 'OrderedSet("")' for an empty set. if self.__container: - return 'OrderedSet("{}")'.format( - '", "'.join(repr(e) for e in self.__container.keys())) + return 'OrderedSet([{}])'.format( + ', '.join(repr(e) for e in self.__container.keys())) return 'OrderedSet()' def __reversed__(self) -> T.Iterator[_T]: @@ -1839,17 +1966,20 @@ class ProgressBarFallback: # lgtm [py/iter-returns-non-self] __iter__ method' warning. ''' def __init__(self, iterable: T.Optional[T.Iterable[str]] = None, total: T.Optional[int] = None, - bar_type: T.Optional[str] = None, desc: T.Optional[str] = None): + bar_type: T.Optional[str] = None, desc: T.Optional[str] = None, + disable: T.Optional[bool] = None): if iterable is not None: self.iterable = iter(iterable) return self.total = total self.done = 0 self.printed_dots = 0 - if self.total and bar_type == 'download': - print('Download size:', self.total) - if desc: - print(f'{desc}: ', end='') + self.disable = not mlog.colorize_console() if disable is None else disable + if not self.disable: + if self.total and bar_type == 'download': + print('Download size:', self.total) + if desc: + print(f'{desc}: ', end='') # Pretend to be an iterator when called as one and don't print any # progress @@ -1860,8 +1990,9 @@ class ProgressBarFallback: # lgtm [py/iter-returns-non-self] return next(self.iterable) def print_dot(self) -> None: - print('.', end='') - sys.stdout.flush() + if not self.disable: + print('.', end='') + sys.stdout.flush() self.printed_dots += 1 def update(self, progress: int) -> None: @@ -1875,21 +2006,29 @@ class ProgressBarFallback: # lgtm [py/iter-returns-non-self] self.print_dot() def close(self) -> None: - print('') + if not self.disable: + print() try: from tqdm import tqdm except ImportError: # ideally we would use a typing.Protocol here, but it's part of typing_extensions until 3.8 - ProgressBar = ProgressBarFallback # type: T.Union[T.Type[ProgressBarFallback], T.Type[ProgressBarTqdm]] + ProgressBar: T.Union[T.Type[ProgressBarFallback], T.Type[ProgressBarTqdm]] = ProgressBarFallback else: class ProgressBarTqdm(tqdm): def __init__(self, *args: T.Any, bar_type: T.Optional[str] = None, **kwargs: T.Any) -> None: if bar_type == 'download': - kwargs.update({'unit': 'bytes', 'leave': True}) + kwargs.update({'unit': 'B', + 'unit_scale': True, + 'unit_divisor': 1024, + 'leave': True, + 'bar_format': '{l_bar}{bar}| {n_fmt}/{total_fmt} {rate_fmt} eta {remaining}', + }) + else: - kwargs.update({'leave': False}) - kwargs['ncols'] = 100 + kwargs.update({'leave': False, + 'bar_format': '{l_bar}{bar}| {n_fmt}/{total_fmt} eta {remaining}', + }) super().__init__(*args, **kwargs) ProgressBar = ProgressBarTqdm @@ -1901,7 +2040,7 @@ class RealPathAction(argparse.Action): super().__init__(option_strings, dest, nargs=None, default=default, **kwargs) def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: T.Union[str, T.Sequence[T.Any], None], option_string: str = None) -> None: + values: T.Union[str, T.Sequence[T.Any], None], option_string: T.Optional[str] = None) -> None: assert isinstance(values, str) setattr(namespace, self.dest, os.path.abspath(os.path.realpath(values))) @@ -1935,9 +2074,9 @@ def get_wine_shortpath(winecmd: T.List[str], wine_paths: T.List[str], return wine_path # Check paths that can be reduced by making them relative to workdir. - rel_paths = [] + rel_paths: T.List[str] = [] if workdir: - abs_paths = [] + abs_paths: T.List[str] = [] for p in wine_paths: try: rel = Path(p).relative_to(workdir) @@ -1974,7 +2113,7 @@ def get_wine_shortpath(winecmd: T.List[str], wine_paths: T.List[str], def run_once(func: T.Callable[..., _T]) -> T.Callable[..., _T]: - ret = [] # type: T.List[_T] + ret: T.List[_T] = [] @wraps(func) def wrapper(*args: T.Any, **kwargs: T.Any) -> _T: @@ -1996,52 +2135,6 @@ def generate_list(func: T.Callable[..., T.Generator[_T, None, None]]) -> T.Calla return wrapper -class OptionOverrideProxy(collections.abc.Mapping): - '''Mimic an option list but transparently override selected option - values. - ''' - - # TODO: the typing here could be made more explicit using a TypeDict from - # python 3.8 or typing_extensions - - def __init__(self, overrides: T.Dict['OptionKey', T.Any], options: 'KeyedOptionDictType', - subproject: T.Optional[str] = None): - self.overrides = overrides - self.options = options - self.subproject = subproject - - def __getitem__(self, key: 'OptionKey') -> 'UserOption': - # FIXME: This is fundamentally the same algorithm than interpreter.get_option_internal(). - # We should try to share the code somehow. - key = key.evolve(subproject=self.subproject) - if not key.is_project(): - opt = self.options.get(key) - if opt is None or opt.yielding: - opt = self.options[key.as_root()] - else: - opt = self.options[key] - if opt.yielding: - opt = self.options.get(key.as_root(), opt) - override_value = self.overrides.get(key.as_root()) - if override_value is not None: - opt = copy.copy(opt) - opt.set_value(override_value) - return opt - - def __iter__(self) -> T.Iterator['OptionKey']: - return iter(self.options) - - def __len__(self) -> int: - return len(self.options) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OptionOverrideProxy): - return NotImplemented - t1 = (self.overrides, self.subproject, self.options) - t2 = (other.overrides, other.subproject, other.options) - return t1 == t2 - - class OptionType(enum.IntEnum): """Enum used to specify what kind of argument a thing is.""" @@ -2062,6 +2155,7 @@ _BUILTIN_NAMES = { 'includedir', 'infodir', 'libdir', + 'licensedir', 'libexecdir', 'localedir', 'localstatedir', @@ -2075,6 +2169,7 @@ _BUILTIN_NAMES = { 'debug', 'default_library', 'errorlogs', + 'genvslite', 'install_umask', 'layout', 'optimization', @@ -2089,6 +2184,7 @@ _BUILTIN_NAMES = { 'force_fallback_for', 'pkg_config_path', 'cmake_prefix_path', + 'vsenv', } @@ -2239,7 +2335,7 @@ class OptionKey: def evolve(self, name: T.Optional[str] = None, subproject: T.Optional[str] = None, machine: T.Optional[MachineChoice] = None, lang: T.Optional[str] = '', module: T.Optional[str] = '') -> 'OptionKey': - """Create a new copy of this key, but with alterted members. + """Create a new copy of this key, but with altered members. For example: >>> a = OptionKey('foo', '', MachineChoice.Host) @@ -2262,11 +2358,11 @@ class OptionKey: return self.evolve(subproject='') def as_build(self) -> 'OptionKey': - """Convenience method for key.evolve(machine=MachinceChoice.BUILD).""" + """Convenience method for key.evolve(machine=MachineChoice.BUILD).""" return self.evolve(machine=MachineChoice.BUILD) def as_host(self) -> 'OptionKey': - """Convenience method for key.evolve(machine=MachinceChoice.HOST).""" + """Convenience method for key.evolve(machine=MachineChoice.HOST).""" return self.evolve(machine=MachineChoice.HOST) def is_backend(self) -> bool: @@ -2289,29 +2385,37 @@ class OptionKey: """Convenience method to check if this is a base option.""" return self.type is OptionType.BASE -def pickle_load(filename: str, object_name: str, object_type: T.Type) -> T.Any: - load_fail_msg = f'{object_name} file {filename!r} is corrupted. Try with a fresh build tree.' + +def pickle_load(filename: str, object_name: str, object_type: T.Type[_PL], suggest_reconfigure: bool = True) -> _PL: + load_fail_msg = f'{object_name} file {filename!r} is corrupted.' + extra_msg = ' Consider reconfiguring the directory with "meson setup --reconfigure".' if suggest_reconfigure else '' try: with open(filename, 'rb') as f: obj = pickle.load(f) except (pickle.UnpicklingError, EOFError): - raise MesonException(load_fail_msg) + raise MesonException(load_fail_msg + extra_msg) except (TypeError, ModuleNotFoundError, AttributeError): - build_dir = os.path.dirname(os.path.dirname(filename)) raise MesonException( f"{object_name} file {filename!r} references functions or classes that don't " "exist. This probably means that it was generated with an old " - "version of meson. Try running from the source directory " - f'meson setup {build_dir} --wipe') + "version of meson." + extra_msg) + if not isinstance(obj, object_type): - raise MesonException(load_fail_msg) + raise MesonException(load_fail_msg + extra_msg) + + # Because these Protocols are not available at runtime (and cannot be made + # available at runtime until we drop support for Python < 3.8), we have to + # do a bit of hackery so that mypy understands what's going on here + version: str + if hasattr(obj, 'version'): + version = T.cast('_VerPickleLoadable', obj).version + else: + version = T.cast('_EnvPickleLoadable', obj).environment.coredata.version + from ..coredata import version as coredata_version from ..coredata import major_versions_differ, MesonVersionMismatchException - version = getattr(obj, 'version', None) - if version is None: - version = obj.environment.coredata.version if major_versions_differ(version, coredata_version): - raise MesonVersionMismatchException(version, coredata_version) + raise MesonVersionMismatchException(version, coredata_version, extra_msg) return obj diff --git a/mesonbuild/utils/vsenv.py b/mesonbuild/utils/vsenv.py index 47055a0..5a02379 100644 --- a/mesonbuild/utils/vsenv.py +++ b/mesonbuild/utils/vsenv.py @@ -1,12 +1,16 @@ +from __future__ import annotations + import os import subprocess import json import pathlib import shutil import tempfile +import locale from .. import mlog -from .universal import MesonException, is_windows, windows_detect_native_arch +from .core import MesonException +from .universal import is_windows, windows_detect_native_arch __all__ = [ @@ -68,7 +72,7 @@ def _setup_vsenv(force: bool) -> bool: ) bat_info = json.loads(bat_json) if not bat_info: - # VS installer instelled but not VS itself maybe? + # VS installer installed but not VS itself maybe? raise MesonException('Could not parse vswhere.exe output') bat_root = pathlib.Path(bat_info[0]['installationPath']) if windows_detect_native_arch() == 'arm64': @@ -90,7 +94,8 @@ def _setup_vsenv(force: bool) -> bool: bat_file.write(bat_contents) bat_file.flush() bat_file.close() - bat_output = subprocess.check_output(bat_file.name, universal_newlines=True) + bat_output = subprocess.check_output(bat_file.name, universal_newlines=True, + encoding=locale.getpreferredencoding(False)) os.unlink(bat_file.name) bat_lines = bat_output.split('\n') bat_separator_seen = False diff --git a/mesonbuild/utils/win32.py b/mesonbuild/utils/win32.py index bc0caec..18ee0d0 100644 --- a/mesonbuild/utils/win32.py +++ b/mesonbuild/utils/win32.py @@ -13,13 +13,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Windows specific implementations of mesonlib functionality.""" import msvcrt import typing as T -from .universal import MesonException +from .core import MesonException from .platform import BuildDirLock as BuildDirLockBase __all__ = ['BuildDirLock'] diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index d949f43..a5b4dc8 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .. import mlog import contextlib @@ -33,7 +34,7 @@ import json from base64 import b64encode from netrc import netrc -from pathlib import Path +from pathlib import Path, PurePath from . import WrapMode from .. import coredata @@ -44,6 +45,9 @@ from .. import mesonlib if T.TYPE_CHECKING: import http.client + from typing_extensions import Literal + + Method = Literal['meson', 'cmake', 'cargo'] try: # Importing is just done to check if SSL exists, so all warnings @@ -53,8 +57,7 @@ try: except ImportError: has_ssl = False -REQ_TIMEOUT = 600.0 -SSL_WARNING_PRINTED = False +REQ_TIMEOUT = 30.0 WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com' ALL_TYPES = ['file', 'git', 'hg', 'svn'] @@ -95,10 +98,7 @@ def open_wrapdburl(urlstring: str, allow_insecure: bool = False, have_opt: bool raise WrapException(f'SSL module not available in {sys.executable}: Cannot contact the WrapDB.{insecure_msg}') else: # following code is only for those without Python SSL - global SSL_WARNING_PRINTED # pylint: disable=global-statement - if not SSL_WARNING_PRINTED: - mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.') - SSL_WARNING_PRINTED = True + mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.', once=True) # If we got this far, allow_insecure was manually passed nossl_url = url._replace(scheme='http') @@ -148,11 +148,11 @@ class PackageDefinition: def __init__(self, fname: str, subproject: str = ''): self.filename = fname self.subproject = SubProject(subproject) - self.type = None # type: T.Optional[str] - self.values = {} # type: T.Dict[str, str] - self.provided_deps = {} # type: T.Dict[str, T.Optional[str]] - self.provided_programs = [] # type: T.List[str] - self.diff_files = [] # type: T.List[Path] + self.type: T.Optional[str] = None + self.values: T.Dict[str, str] = {} + self.provided_deps: T.Dict[str, T.Optional[str]] = {} + self.provided_programs: T.List[str] = [] + self.diff_files: T.List[Path] = [] self.basename = os.path.basename(fname) self.has_wrap = self.basename.endswith('.wrap') self.name = self.basename[:-5] if self.has_wrap else self.basename @@ -227,6 +227,8 @@ class PackageDefinition: self.diff_files.append(path) def parse_provide_section(self, config: configparser.ConfigParser) -> None: + if config.has_section('provides'): + raise WrapException('Unexpected "[provides]" section, did you mean "[provide]"?') if config.has_section('provide'): for k, v in config['provide'].items(): if k == 'dependency_names': @@ -286,14 +288,15 @@ class Resolver: wrap_mode: WrapMode = WrapMode.default wrap_frontend: bool = False allow_insecure: bool = False + silent: bool = False def __post_init__(self) -> None: self.subdir_root = os.path.join(self.source_dir, self.subdir) - self.cachedir = os.path.join(self.subdir_root, 'packagecache') - self.wraps = {} # type: T.Dict[str, PackageDefinition] + self.cachedir = os.environ.get('MESON_PACKAGE_CACHE_DIR') or os.path.join(self.subdir_root, 'packagecache') + self.wraps: T.Dict[str, PackageDefinition] = {} self.netrc: T.Optional[netrc] = None - self.provided_deps = {} # type: T.Dict[str, PackageDefinition] - self.provided_programs = {} # type: T.Dict[str, PackageDefinition] + self.provided_deps: T.Dict[str, PackageDefinition] = {} + self.provided_programs: T.Dict[str, PackageDefinition] = {} self.wrapdb: T.Dict[str, T.Any] = {} self.wrapdb_provided_deps: T.Dict[str, str] = {} self.wrapdb_provided_programs: T.Dict[str, str] = {} @@ -406,7 +409,7 @@ class Resolver: return wrap_name return None - def resolve(self, packagename: str, method: str) -> str: + def resolve(self, packagename: str, force_method: T.Optional[Method] = None) -> T.Tuple[str, Method]: self.packagename = packagename self.directory = packagename self.wrap = self.wraps.get(packagename) @@ -433,27 +436,39 @@ class Resolver: # Write a dummy wrap file in main project that redirect to the # wrap we picked. with open(main_fname, 'w', encoding='utf-8') as f: - f.write(textwrap.dedent('''\ + f.write(textwrap.dedent(f'''\ [wrap-redirect] - filename = {} - '''.format(os.path.relpath(self.wrap.filename, self.subdir_root)))) + filename = {PurePath(os.path.relpath(self.wrap.filename, self.subdir_root)).as_posix()} + ''')) else: # No wrap file, it's a dummy package definition for an existing # directory. Use the source code in place. self.dirname = self.wrap.filename rel_path = os.path.relpath(self.dirname, self.source_dir) - if method == 'meson': - buildfile = os.path.join(self.dirname, 'meson.build') - elif method == 'cmake': - buildfile = os.path.join(self.dirname, 'CMakeLists.txt') - else: - raise WrapException('Only the methods "meson" and "cmake" are supported') + # Map each supported method to a file that must exist at the root of source tree. + methods_map: T.Dict[Method, str] = { + 'meson': 'meson.build', + 'cmake': 'CMakeLists.txt', + 'cargo': 'Cargo.toml', + } + + # Check if this wrap forces a specific method, use meson otherwise. + method = T.cast('T.Optional[Method]', self.wrap.values.get('method', force_method)) + if method and method not in methods_map: + allowed_methods = ', '.join(methods_map.keys()) + raise WrapException(f'Wrap method {method!r} is not supported, must be one of: {allowed_methods}') + if force_method and method != force_method: + raise WrapException(f'Wrap method is {method!r} but we are trying to configure it with {force_method}') + method = method or 'meson' + + def has_buildfile() -> bool: + return os.path.exists(os.path.join(self.dirname, methods_map[method])) # The directory is there and has meson.build? Great, use it. - if os.path.exists(buildfile): + if has_buildfile(): self.validate() - return rel_path + return rel_path, method # Check if the subproject is a git submodule self.resolve_git_submodule() @@ -462,7 +477,17 @@ class Resolver: if not os.path.isdir(self.dirname): raise WrapException('Path already exists but is not a directory') else: - if self.wrap.type == 'file': + # Check first if we have the extracted directory in our cache. This can + # happen for example when MESON_PACKAGE_CACHE_DIR=/usr/share/cargo/registry + # on distros that ships Rust source code. + # TODO: We don't currently clone git repositories into the cache + # directory, but we should to avoid cloning multiple times the same + # repository. In that case, we could do something smarter than + # copy_tree() here. + cached_directory = os.path.join(self.cachedir, self.directory) + if os.path.isdir(cached_directory): + self.copy_tree(cached_directory, self.dirname) + elif self.wrap.type == 'file': self.get_file() else: self.check_can_download() @@ -481,16 +506,14 @@ class Resolver: windows_proof_rmtree(self.dirname) raise - # A meson.build or CMakeLists.txt file is required in the directory - if not os.path.exists(buildfile): - raise WrapException(f'Subproject exists but has no {os.path.basename(buildfile)} file') + if not has_buildfile(): + raise WrapException(f'Subproject exists but has no {methods_map[method]} file.') # At this point, the subproject has been successfully resolved for the # first time so save off the hash of the entire wrap file for future # reference. self.wrap.update_hash_cache(self.dirname) - - return rel_path + return rel_path, method def check_can_download(self) -> None: # Don't download subproject data based on wrap file if requested. @@ -547,7 +570,10 @@ class Resolver: if 'lead_directory_missing' in self.wrap.values: os.mkdir(self.dirname) extract_dir = self.dirname - shutil.unpack_archive(path, extract_dir) + try: + shutil.unpack_archive(path, extract_dir) + except OSError as e: + raise WrapException(f'failed to unpack archive with error: {str(e)}') from e def get_git(self) -> None: if not GIT: @@ -555,7 +581,7 @@ class Resolver: revno = self.wrap.get('revision') checkout_cmd = ['-c', 'advice.detachedHead=false', 'checkout', revno, '--'] is_shallow = False - depth_option = [] # type: T.List[str] + depth_option: T.List[str] = [] if self.wrap.values.get('depth', '') != '': is_shallow = True depth_option = ['--depth', self.wrap.values.get('depth')] @@ -568,12 +594,6 @@ class Resolver: revno = self.wrap.get('revision') verbose_git(['fetch', *depth_option, 'origin', revno], self.dirname, check=True) verbose_git(checkout_cmd, self.dirname, check=True) - if self.wrap.values.get('clone-recursive', '').lower() == 'true': - verbose_git(['submodule', 'update', '--init', '--checkout', - '--recursive', *depth_option], self.dirname, check=True) - push_url = self.wrap.values.get('push-url') - if push_url: - verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True) else: if not is_shallow: verbose_git(['clone', self.wrap.get('url'), self.directory], self.subdir_root, check=True) @@ -587,12 +607,12 @@ class Resolver: args += ['--branch', revno] args += [self.wrap.get('url'), self.directory] verbose_git(args, self.subdir_root, check=True) - if self.wrap.values.get('clone-recursive', '').lower() == 'true': - verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option], - self.dirname, check=True) - push_url = self.wrap.values.get('push-url') - if push_url: - verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True) + if self.wrap.values.get('clone-recursive', '').lower() == 'true': + verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option], + self.dirname, check=True) + push_url = self.wrap.values.get('push-url') + if push_url: + verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True) def validate(self) -> None: # This check is only for subprojects with wraps. @@ -679,7 +699,7 @@ class Resolver: except urllib.error.URLError as e: mlog.log(str(e)) raise WrapException(f'could not get {urlstring} is the internet available?') - with contextlib.closing(resp) as resp: + with contextlib.closing(resp) as resp, tmpfile as tmpfile: try: dlsize = int(resp.info()['Content-Length']) except TypeError: @@ -696,7 +716,8 @@ class Resolver: return hashvalue, tmpfile.name sys.stdout.flush() progress_bar = ProgressBar(bar_type='download', total=dlsize, - desc='Downloading') + desc='Downloading', + disable=(self.silent or None)) while True: block = resp.read(blocksize) if block == b'': @@ -796,14 +817,17 @@ class Resolver: if not path.exists(): raise WrapException(f'Diff file "{path}" does not exist') relpath = os.path.relpath(str(path), self.dirname) - if PATCH: - cmd = [PATCH, '-f', '-p1', '-i', relpath] - elif GIT: - # If the `patch` command is not available, fall back to `git - # apply`. The `--work-tree` is necessary in case we're inside a + if GIT: + # Git is more likely to be available on Windows and more likely + # to apply correctly assuming patches are often generated by git. + # See https://github.com/mesonbuild/meson/issues/12092. + # The `--work-tree` is necessary in case we're inside a # Git repository: by default, Git will try to apply the patch to # the repository root. cmd = [GIT, '--work-tree', '.', 'apply', '-p1', relpath] + elif PATCH: + # Always pass a POSIX path to patch, because on Windows it's MSYS + cmd = [PATCH, '-f', '-p1', '-i', str(Path(relpath).as_posix())] else: raise WrapException('Missing "patch" or "git" commands to apply diff files') diff --git a/mesonbuild/wrap/wraptool.py b/mesonbuild/wrap/wraptool.py index bcf0e67..c4ff9db 100644 --- a/mesonbuild/wrap/wraptool.py +++ b/mesonbuild/wrap/wraptool.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import sys, os import configparser @@ -27,6 +28,8 @@ from .. import mesonlib, msubprojects if T.TYPE_CHECKING: import argparse +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: subparsers = parser.add_subparsers(title='Commands', dest='command') subparsers.required = True diff --git a/packaging/createmsi.py b/packaging/createmsi.py index fe49b7b..fef2b38 100755 --- a/packaging/createmsi.py +++ b/packaging/createmsi.py @@ -31,7 +31,7 @@ sys.path.append(os.getcwd()) from mesonbuild import coredata # Elementtree does not support CDATA. So hack it. -WINVER_CHECK = ' 602)]]>' +WINVER_CHECK = 'Installed OR (VersionNT64 > 602)>' def gen_guid(): ''' @@ -102,7 +102,7 @@ class PackageGenerator: 'Title': 'Meson', 'Description': 'Meson executables', 'Level': '1', - 'Absent': 'disallow', + 'AllowAbsent': 'no', }, self.staging_dirs[1]: { 'Id': 'NinjaProgram', @@ -160,49 +160,44 @@ class PackageGenerator: ''' Generate package files for MSI installer package ''' - self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'}) - product = ET.SubElement(self.root, 'Product', { + self.root = ET.Element('Wix', { + 'xmlns': 'http://wixtoolset.org/schemas/v4/wxs', + 'xmlns:ui': 'http://wixtoolset.org/schemas/v4/wxs/ui' + }) + + package = ET.SubElement(self.root, 'Package', { 'Name': self.product_name, 'Manufacturer': 'The Meson Development Team', - 'Id': self.guid, 'UpgradeCode': self.update_guid, 'Language': '1033', 'Codepage': '1252', 'Version': self.version, }) - package = ET.SubElement(product, 'Package', { - 'Id': '*', + ET.SubElement(package, 'SummaryInformation', { 'Keywords': 'Installer', 'Description': f'Meson {self.version} installer', - 'Comments': 'Meson is a high performance build system', 'Manufacturer': 'The Meson Development Team', - 'InstallerVersion': '500', - 'Languages': '1033', - 'Compressed': 'yes', - 'SummaryCodepage': '1252', }) - condition = ET.SubElement(product, 'Condition', {'Message': 'This application is only supported on Windows 10 or higher.'}) + ET.SubElement(package, + 'Launch', + {'Message': 'This application is only supported on Windows 10 or higher.', + 'Condition': 'X'*len(WINVER_CHECK)}) - condition.text = 'X'*len(WINVER_CHECK) - ET.SubElement(product, 'MajorUpgrade', - {'DowngradeErrorMessage': 'A newer version of Meson is already installed.'}) + ET.SubElement(package, 'MajorUpgrade', + {'DowngradeErrorMessage': + 'A newer version of Meson is already installed.'}) - package.set('Platform', 'x64') - ET.SubElement(product, 'Media', { + ET.SubElement(package, 'Media', { 'Id': '1', 'Cabinet': 'meson.cab', 'EmbedCab': 'yes', }) - targetdir = ET.SubElement(product, 'Directory', { - 'Id': 'TARGETDIR', - 'Name': 'SourceDir', + targetdir = ET.SubElement(package, 'StandardDirectory', { + 'Id': 'ProgramFiles64Folder', }) - progfiledir = ET.SubElement(targetdir, 'Directory', { - 'Id': self.progfile_dir, - }) - installdir = ET.SubElement(progfiledir, 'Directory', { + installdir = ET.SubElement(targetdir, 'Directory', { 'Id': 'INSTALLDIR', 'Name': 'Meson', }) @@ -213,16 +208,12 @@ class PackageGenerator: 'Language': '0', }) - ET.SubElement(product, 'Property', { - 'Id': 'WIXUI_INSTALLDIR', - 'Value': 'INSTALLDIR', - }) - ET.SubElement(product, 'UIRef', { + ET.SubElement(package, 'ui:WixUI', { 'Id': 'WixUI_FeatureTree', }) for s_d in self.staging_dirs: assert os.path.isdir(s_d) - top_feature = ET.SubElement(product, 'Feature', { + top_feature = ET.SubElement(package, 'Feature', { 'Id': 'Complete', 'Title': 'Meson ' + self.version, 'Description': 'The complete package', @@ -246,7 +237,7 @@ class PackageGenerator: }) ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'}) ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True) - # ElementTree can not do prettyprinting so do it manually + # ElementTree cannot do pretty-printing, so do it manually import xml.dom.minidom doc = xml.dom.minidom.parse(self.main_xml) with open(self.main_xml, 'w') as open_file: @@ -277,10 +268,10 @@ class PackageGenerator: component_id = f'ApplicationFiles{self.component_num}' comp_xml_node = ET.SubElement(parent_xml_node, 'Component', { 'Id': component_id, + 'Bitness': 'always64', 'Guid': gen_guid(), }) self.feature_components[staging_dir].append(component_id) - comp_xml_node.set('Win64', 'yes') if self.component_num == 0: ET.SubElement(comp_xml_node, 'Environment', { 'Id': 'Environment', @@ -300,7 +291,7 @@ class PackageGenerator: }) for dirname in cur_node.dirs: - dir_id = os.path.join(current_dir, dirname).replace('\\', '_').replace('/', '_') + dir_id = os.path.join(current_dir, dirname).replace('\\', '_').replace('/', '_').replace('-', '_') dir_node = ET.SubElement(parent_xml_node, 'Directory', { 'Id': dir_id, 'Name': dirname, @@ -311,23 +302,40 @@ class PackageGenerator: ''' Generate the Meson build MSI package. ''' - wixdir = 'c:\\Program Files\\Wix Toolset v3.11\\bin' - if not os.path.isdir(wixdir): - wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin' - if not os.path.isdir(wixdir): - print("ERROR: This script requires WIX") - sys.exit(1) - subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml]) - subprocess.check_call([os.path.join(wixdir, 'light'), - '-ext', 'WixUIExtension', - '-cultures:en-us', - '-dWixUILicenseRtf=packaging\\License.rtf', - '-out', self.final_output, - self.main_o]) + subprocess.check_call(['wix', + 'build', + '-bindvariable', 'WixUILicenseRtf=packaging\\License.rtf', + '-ext', 'WixToolset.UI.wixext', + '-culture', 'en-us', + '-arch', 'x64', + '-o', + self.final_output, + self.main_xml, + ]) + + +def install_wix(): + subprocess.check_call(['dotnet', + 'nuget', + 'add', + 'source', + 'https://api.nuget.org/v3/index.json']) + subprocess.check_call(['dotnet', + 'tool', + 'install', + '--global', + 'wix']) + subprocess.check_call(['wix', + 'extension', + 'add', + 'WixToolset.UI.wixext', + ]) if __name__ == '__main__': if not os.path.exists('meson.py'): sys.exit(print('Run me in the top level source dir.')) + if not shutil.which('wix'): + install_wix() subprocess.check_call(['pip', 'install', '--upgrade', 'pyinstaller']) p = PackageGenerator() diff --git a/packaging/createpkg.py b/packaging/createpkg.py index 70da656..fd022d9 100755 --- a/packaging/createpkg.py +++ b/packaging/createpkg.py @@ -16,6 +16,7 @@ import subprocess import shutil, sys, os +from glob import glob import xml.etree.ElementTree as ET @@ -41,7 +42,10 @@ class PkgGenerator: if os.path.exists(self.pkg_dir): shutil.rmtree(self.pkg_dir) os.mkdir(self.pkg_dir) - pyinstaller_bin = '/Users/jpakkane/Library/Python/3.8/bin/pyinstaller' + pyinstaller_bin = glob('/Users/jpakkane/Library/Python/*/bin/pyinstaller') + if len(pyinstaller_bin) != 1: + sys.exit('Could not determine unique installer.') + pyinstaller_bin = pyinstaller_bin[0] pyinst_cmd = [pyinstaller_bin, '--clean', '--additional-hooks-dir=packaging', @@ -85,7 +89,7 @@ class PkgGenerator: ET.SubElement(root, 'pkg-ref', {'id': self.identifier}) ET.SubElement(root, 'options', {'customize': 'never', 'require-scripts': 'false', - 'hostArhcitectures': 'x86_64,arm64'}) + 'hostArchitectures': 'x86_64,arm64'}) choices_outline = ET.SubElement(root, 'choices-outline') line = ET.SubElement(choices_outline, 'line', {'choice': 'default'}) ET.SubElement(line, 'line', {'choice': self.identifier}) @@ -96,7 +100,7 @@ class PkgGenerator: 'version': '0', # self.version, 'onConclusion': 'none'}).text = self.pkgname ET.ElementTree(root).write(self.distribution_file, encoding='utf-8', xml_declaration=True) - # ElementTree can not do prettyprinting so do it manually + # ElementTree cannot do pretty-printing, so do it manually import xml.dom.minidom doc = xml.dom.minidom.parse(self.distribution_file) with open(self.distribution_file, 'w') as open_file: diff --git a/packaging/hook-mesonbuild.py b/packaging/hook-mesonbuild.py index b076c50..d6b06cd 100644 --- a/packaging/hook-mesonbuild.py +++ b/packaging/hook-mesonbuild.py @@ -21,11 +21,15 @@ def get_all_modules_from_dir(dirname): modules = ['mesonbuild.' + modname + '.' + x for x in modules if not x.startswith('_')] return modules -datas += collect_data_files('mesonbuild.scripts') +datas += collect_data_files('mesonbuild.scripts', include_py_files=True, excludes=['**/__pycache__']) datas += collect_data_files('mesonbuild.cmake.data') datas += collect_data_files('mesonbuild.dependencies.data') +# lazy-loaded +hiddenimports += get_all_modules_from_dir('mesonbuild/dependencies') +# imported by meson.build files hiddenimports += get_all_modules_from_dir('mesonbuild/modules') +# executed when named on CLI hiddenimports += get_all_modules_from_dir('mesonbuild/scripts') # Python packagers want to be minimal and only copy the things diff --git a/run_format_tests.py b/run_format_tests.py old mode 100644 new mode 100755 index 1f41f3d..ca3e715 --- a/run_format_tests.py +++ b/run_format_tests.py @@ -63,6 +63,7 @@ def check_format() -> None: 'work area', '.eggs', '_cache', # e.g. .mypy_cache 'venv', # virtualenvs have DOS line endings + '120 rewrite', # we explicitly test for tab in meson.build file } for (root, _, filenames) in os.walk('.'): if any([x in root for x in skip_dirs]): @@ -74,9 +75,17 @@ def check_format() -> None: continue check_file(root / file) +def check_symlinks(): + for f in Path('test cases').glob('**/*'): + if f.is_symlink(): + if 'boost symlinks' in str(f): + continue + raise SystemExit(f'Test data dir contains symlink: {f}.') + if __name__ == '__main__': script_dir = os.path.split(__file__)[0] if script_dir != '': os.chdir(script_dir) check_format() + check_symlinks() diff --git a/run_meson_command_tests.py b/run_meson_command_tests.py index e044af5..093d6ea 100755 --- a/run_meson_command_tests.py +++ b/run_meson_command_tests.py @@ -84,7 +84,7 @@ class CommandTests(unittest.TestCase): os.chdir(str(self.orig_dir)) super().tearDown() - def _run(self, command, workdir=None): + def _run(self, command, workdir=None, env=None): ''' Run a command while printing the stdout, and also return a copy of it ''' @@ -92,7 +92,7 @@ class CommandTests(unittest.TestCase): # between CI issue and test bug in that case. Set timeout and fail loud # instead. p = subprocess.run(command, stdout=subprocess.PIPE, - env=os.environ.copy(), text=True, + env=env, text=True, cwd=workdir, timeout=60 * 5) print(p.stdout) if p.returncode != 0: @@ -135,8 +135,7 @@ class CommandTests(unittest.TestCase): (bindir / 'python3').symlink_to(python_command[0]) os.environ['PATH'] = str(bindir) + os.pathsep + os.environ['PATH'] # use our overridden PATH-compatible python - path_resolved_meson_command = resolved_meson_command.copy() - path_resolved_meson_command[0] = str(bindir / 'python3') + path_resolved_meson_command = [str(bindir / 'meson')] # See if it works! meson_py = 'meson' meson_setup = [meson_py, 'setup'] @@ -210,6 +209,21 @@ class CommandTests(unittest.TestCase): self._run([script.as_posix(), source, '--outfile', target, '--interpreter', python_command[0]]) self._run([target.as_posix(), '--help']) + def test_meson_runpython(self): + meson_command = str(self.src_root / 'meson.py') + script_file = str(self.src_root / 'foo.py') + test_command = 'import sys; print(sys.argv[1])' + env = os.environ.copy() + del env['MESON_COMMAND_TESTS'] + with open(script_file, 'w') as f: + f.write('#!/usr/bin/env python3\n\n') + f.write(f'{test_command}\n') + + for cmd in [['-c', test_command, 'fake argument'], [script_file, 'fake argument']]: + pyout = self._run(python_command + cmd) + mesonout = self._run(python_command + [meson_command, 'runpython'] + cmd, env=env) + self.assertEqual(pyout, mesonout) + if __name__ == '__main__': print('Meson build system', meson_version, 'Command Tests') diff --git a/run_mypy.py b/run_mypy.py index 3daf7c7..16ff318 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -12,6 +12,7 @@ from mesonbuild.mesonlib import version_compare modules = [ # fully typed submodules # 'mesonbuild/ast/', + 'mesonbuild/cargo/', 'mesonbuild/cmake/', 'mesonbuild/compilers/', 'mesonbuild/dependencies/', @@ -19,6 +20,7 @@ modules = [ 'mesonbuild/interpreterbase/', 'mesonbuild/linkers/', 'mesonbuild/scripts/', + 'mesonbuild/templates/', 'mesonbuild/wrap/', # specific files @@ -33,13 +35,17 @@ modules = [ 'mesonbuild/interpreter/type_checking.py', 'mesonbuild/mcompile.py', 'mesonbuild/mdevenv.py', + 'mesonbuild/utils/core.py', 'mesonbuild/utils/platform.py', 'mesonbuild/utils/universal.py', + 'mesonbuild/mconf.py', + 'mesonbuild/mdist.py', 'mesonbuild/minit.py', 'mesonbuild/minstall.py', 'mesonbuild/mintro.py', 'mesonbuild/mlog.py', 'mesonbuild/msubprojects.py', + 'mesonbuild/modules/__init__.py', 'mesonbuild/modules/external_project.py', 'mesonbuild/modules/fs.py', 'mesonbuild/modules/gnome.py', @@ -50,7 +56,11 @@ modules = [ 'mesonbuild/modules/modtest.py', 'mesonbuild/modules/pkgconfig.py', 'mesonbuild/modules/qt.py', + 'mesonbuild/modules/qt4.py', + 'mesonbuild/modules/qt5.py', + 'mesonbuild/modules/qt6.py', 'mesonbuild/modules/rust.py', + 'mesonbuild/modules/simd.py', 'mesonbuild/modules/sourceset.py', 'mesonbuild/modules/wayland.py', 'mesonbuild/modules/windows.py', @@ -88,15 +98,16 @@ def main() -> int: check_mypy() root = Path(__file__).absolute().parent - args = [] # type: T.List[str] parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('files', nargs='*') + parser.add_argument('--mypy', help='path to mypy executable') parser.add_argument('-q', '--quiet', action='store_true', help='do not print informational messages') parser.add_argument('-p', '--pretty', action='store_true', help='pretty print mypy errors') parser.add_argument('-C', '--clear', action='store_true', help='clear the terminal before running mypy') + parser.add_argument('--allver', action='store_true', help='Check all supported versions of python') - opts = parser.parse_args() + opts, args = parser.parse_known_args() if opts.pretty: args.append('--pretty') @@ -117,13 +128,18 @@ def main() -> int: to_check.extend(modules) if to_check: + command = [opts.mypy] if opts.mypy else [sys.executable, '-m', 'mypy'] if not opts.quiet: print('Running mypy (this can take some time) ...') - p = subprocess.run( - [sys.executable, '-m', 'mypy', '--python-version', '3.10'] + args + to_check, - cwd=root, - ) - return p.returncode + retcode = subprocess.run(command + args + to_check, cwd=root).returncode + if opts.allver and retcode == 0: + for minor in range(7, sys.version_info[1]): + if not opts.quiet: + print(f'Checking mypy with python version: 3.{minor}') + p = subprocess.run(command + args + to_check + [f'--python-version=3.{minor}'], cwd=root) + if p.returncode != 0: + retcode = p.returncode + return retcode else: if not opts.quiet: print('nothing to do...') diff --git a/run_project_tests.py b/run_project_tests.py index ce913f9..39bf162 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -40,6 +40,7 @@ import time import typing as T import xml.etree.ElementTree as ET import collections +import importlib.util from mesonbuild import build from mesonbuild import environment @@ -53,17 +54,18 @@ from mesonbuild.mesonlib import MachineChoice, Popen_safe, TemporaryDirectoryWin from mesonbuild.mlog import blue, bold, cyan, green, red, yellow, normal_green from mesonbuild.coredata import backendlist, version as meson_version from mesonbuild.modules.python import PythonExternalProgram -from run_tests import get_fake_options, run_configure, get_meson_script -from run_tests import get_backend_commands, get_backend_args_for_dir, Backend -from run_tests import ensure_backend_detects_changes -from run_tests import guess_backend +from run_tests import ( + get_fake_options, run_configure, get_meson_script, get_backend_commands, + get_backend_args_for_dir, Backend, ensure_backend_detects_changes, + guess_backend, handle_meson_skip_test, +) + if T.TYPE_CHECKING: from types import FrameType from mesonbuild.environment import Environment from mesonbuild._typing import Protocol from concurrent.futures import Future - from mesonbuild.modules.python import PythonIntrospectionDict class CompilerArgumentType(Protocol): cross_file: str @@ -132,11 +134,11 @@ class InstalledFile: self.path = raw['file'] self.typ = raw['type'] self.platform = raw.get('platform', None) - self.language = raw.get('language', 'c') # type: str + self.language = raw.get('language', 'c') - version = raw.get('version', '') # type: str + version = raw.get('version', '') if version: - self.version = version.split('.') # type: T.List[str] + self.version = version.split('.') else: # split on '' will return [''], we want an empty list though self.version = [] @@ -149,12 +151,12 @@ class InstalledFile: canonical_compiler = 'msvc' python_suffix = python.info['suffix'] - + python_limited_suffix = python.info['limited_api_suffix'] has_pdb = False if self.language in {'c', 'cpp'}: has_pdb = canonical_compiler == 'msvc' elif self.language == 'd': - # dmd's optlink does not genearte pdb iles + # dmd's optlink does not generate pdb files has_pdb = env.coredata.compilers.host['d'].linker.id in {'link', 'lld-link'} # Abort if the platform does not match @@ -168,7 +170,7 @@ class InstalledFile: return None # Handle the different types - if self.typ in {'py_implib', 'python_lib', 'python_file'}: + if self.typ in {'py_implib', 'py_limited_implib', 'python_lib', 'python_limited_lib', 'python_file', 'python_bytecode'}: val = p.as_posix() val = val.replace('@PYTHON_PLATLIB@', python.platlib) val = val.replace('@PYTHON_PURELIB@', python.purelib) @@ -177,6 +179,8 @@ class InstalledFile: return p if self.typ == 'python_lib': return p.with_suffix(python_suffix) + if self.typ == 'python_limited_lib': + return p.with_suffix(python_limited_suffix) if self.typ == 'py_implib': p = p.with_suffix(python_suffix) if env.machines.host.is_windows() and canonical_compiler == 'msvc': @@ -185,7 +189,17 @@ class InstalledFile: return p.with_suffix('.dll.a') else: return None - elif self.typ in {'file', 'dir'}: + if self.typ == 'py_limited_implib': + p = p.with_suffix(python_limited_suffix) + if env.machines.host.is_windows() and canonical_compiler == 'msvc': + return p.with_suffix('.lib') + elif env.machines.host.is_windows() or env.machines.host.is_cygwin(): + return p.with_suffix('.dll.a') + else: + return None + if self.typ == 'python_bytecode': + return p.parent / importlib.util.cache_from_source(p.name) + elif self.typ in {'file', 'dir', 'link'}: return p elif self.typ == 'shared_lib': if env.machines.host.is_windows() or env.machines.host.is_cygwin(): @@ -213,7 +227,9 @@ class InstalledFile: suffix = '{}.{}'.format(suffix, '.'.join(self.version)) return p.with_suffix(suffix) elif self.typ == 'exe': - if env.machines.host.is_windows() or env.machines.host.is_cygwin(): + if 'mwcc' in canonical_compiler: + return p.with_suffix('.nef') + elif env.machines.host.is_windows() or env.machines.host.is_cygwin(): return p.with_suffix('.exe') elif self.typ == 'pdb': if self.version: @@ -247,21 +263,27 @@ class InstalledFile: if not abs_p.is_dir(): raise RuntimeError(f'{p} is not a directory') return [x.relative_to(installdir) for x in abs_p.rglob('*') if x.is_file() or x.is_symlink()] + elif self.typ == 'link': + abs_p = installdir / p + if not abs_p.is_symlink(): + raise RuntimeError(f'{p} is not a symlink') + return [p] else: return [p] @functools.total_ordering class TestDef: - def __init__(self, path: Path, name: T.Optional[str], args: T.List[str], skip: bool = False): + def __init__(self, path: Path, name: T.Optional[str], args: T.List[str], skip: bool = False, skip_category: bool = False): self.category = path.parts[1] self.path = path self.name = name self.args = args self.skip = skip self.env = os.environ.copy() - self.installed_files = [] # type: T.List[InstalledFile] - self.do_not_set_opts = [] # type: T.List[str] - self.stdout = [] # type: T.List[T.Dict[str, str]] + self.installed_files: T.List[InstalledFile] = [] + self.do_not_set_opts: T.List[str] = [] + self.stdout: T.List[T.Dict[str, str]] = [] + self.skip_category = skip_category self.skip_expected = False # Always print a stack trace for Meson exceptions @@ -286,9 +308,11 @@ class TestDef: return (s_id, self.path, self.name or '') < (o_id, other.path, other.name or '') return NotImplemented +failing_testcases: T.List[str] = [] failing_logs: T.List[str] = [] print_debug = 'MESON_PRINT_TEST_OUTPUT' in os.environ under_ci = 'CI' in os.environ +ci_is_github = 'GITHUB_ACTIONS' in os.environ raw_ci_jobname = os.environ.get('MESON_CI_JOBNAME', None) ci_jobname = raw_ci_jobname if raw_ci_jobname != 'thirdparty' else None do_debug = under_ci or print_debug @@ -375,7 +399,7 @@ def platform_fix_name(fname: str, canonical_compiler: str, env: environment.Envi def validate_install(test: TestDef, installdir: Path, env: environment.Environment) -> str: ret_msg = '' - expected_raw = [] # type: T.List[Path] + expected_raw: T.List[Path] = [] for i in test.installed_files: try: expected_raw += i.get_paths(host_c_compiler, env, installdir) @@ -420,13 +444,18 @@ def log_text_file(logfile: T.TextIO, testdir: Path, result: TestResult) -> None: def _run_ci_include(args: T.List[str]) -> str: + header = f'Included file {args[0]}:' + footer = '' + if ci_is_github: + header = f'::group::==== {header} ====' + footer = '::endgroup::' if not args: return 'At least one parameter required' try: data = Path(args[0]).read_text(errors='ignore', encoding='utf-8') - return 'Included file {}:\n{}\n'.format(args[0], data) + return f'{header}\n{data}\n{footer}\n' except Exception: - return 'Failed to open {}'.format(args[0]) + return 'Failed to open {}\n'.format(args[0]) ci_commands = { 'ci_include': _run_ci_include @@ -440,7 +469,7 @@ def run_ci_commands(raw_log: str) -> T.List[str]: cmd = shlex.split(l[11:]) if not cmd or cmd[0] not in ci_commands: continue - res += ['CI COMMAND {}:\n{}\n'.format(cmd[0], ci_commands[cmd[0]](cmd[1:]))] + res += ['CI COMMAND {}:\n{}'.format(cmd[0], ci_commands[cmd[0]](cmd[1:]))] return res class OutputMatch: @@ -532,7 +561,7 @@ def validate_output(test: TestDef, stdo: str, stde: str) -> str: # coded to run as a batch process. def clear_internal_caches() -> None: import mesonbuild.interpreterbase - from mesonbuild.dependencies import CMakeDependency + from mesonbuild.dependencies.cmake import CMakeDependency from mesonbuild.mesonlib import PerMachine mesonbuild.interpreterbase.FeatureNew.feature_registry = {} CMakeDependency.class_cmakeinfo = PerMachine(None, None) @@ -544,11 +573,11 @@ def run_test_inprocess(testdir: str) -> T.Tuple[int, str, str, str]: sys.stderr = mystderr = StringIO() old_cwd = os.getcwd() os.chdir(testdir) - test_log_fname = Path('meson-logs', 'testlog.txt') + test_log_fname = os.path.join('meson-logs', 'testlog.txt') try: returncode_test = mtest.run_with_args(['--no-rebuild']) - if test_log_fname.exists(): - test_log = test_log_fname.open(encoding='utf-8', errors='ignore').read() + if os.path.exists(test_log_fname): + test_log = _run_ci_include([test_log_fname]) else: test_log = '' returncode_benchmark = mtest.run_with_args(['--no-rebuild', '--benchmark', '--logbase', 'benchmarklog']) @@ -654,16 +683,18 @@ def _run_test(test: TestDef, gen_args.extend(['--native-file', nativefile.as_posix()]) if crossfile.exists(): gen_args.extend(['--cross-file', crossfile.as_posix()]) - (returncode, stdo, stde) = run_configure(gen_args, env=test.env, catch_exception=True) - try: - logfile = Path(test_build_dir, 'meson-logs', 'meson-log.txt') - with logfile.open(errors='ignore', encoding='utf-8') as fid: - mesonlog = fid.read() - except Exception: + inprocess, res = run_configure(gen_args, env=test.env, catch_exception=True) + returncode, stdo, stde = res + cmd = '(inprocess) $ ' if inprocess else '$ ' + cmd += mesonlib.join_args(gen_args) + logfile = os.path.join(test_build_dir, 'meson-logs', 'meson-log.txt') + if os.path.exists(logfile): + mesonlog = '\n'.join((cmd, _run_ci_include([logfile]))) + else: mesonlog = no_meson_log_msg cicmds = run_ci_commands(mesonlog) testresult = TestResult(cicmds) - testresult.add_step(BuildStep.configure, stdo, stde, mesonlog, time.time() - gen_start) + testresult.add_step(BuildStep.configure, '\n'.join((cmd, stdo)), stde, mesonlog, time.time() - gen_start) output_msg = validate_output(test, stdo, stde) testresult.mlog += output_msg if output_msg: @@ -687,8 +718,8 @@ def _run_test(test: TestDef, # Build with subprocess def build_step() -> None: build_start = time.time() - pc, o, e = Popen_safe(compile_commands + dir_args, cwd=test_build_dir) - testresult.add_step(BuildStep.build, o, e, '', time.time() - build_start) + pc, o, _ = Popen_safe(compile_commands + dir_args, cwd=test_build_dir, stderr=subprocess.STDOUT) + testresult.add_step(BuildStep.build, o, '', '', time.time() - build_start) if should_fail == 'build': if pc.returncode != 0: raise testresult @@ -770,7 +801,7 @@ def _skip_keys(test_def: T.Dict) -> T.Tuple[bool, bool]: # Test is expected to skip if os matches if 'skip_on_os' in test_def: - mesonenv = environment.Environment(None, None, get_fake_options('/')) + mesonenv = environment.Environment('', '', get_fake_options('/')) for skip_os in test_def['skip_on_os']: if skip_os.startswith('!'): if mesonenv.machines.host.system != skip_os[1:]: @@ -789,7 +820,7 @@ def _skip_keys(test_def: T.Dict) -> T.Tuple[bool, bool]: return (skip, skip_expected) -def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: +def load_test_json(t: TestDef, stdout_mandatory: bool, skip_category: bool = False) -> T.List[TestDef]: all_tests: T.List[TestDef] = [] test_def = {} test_def_file = t.path / 'test.json' @@ -797,7 +828,7 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: test_def = json.loads(test_def_file.read_text(encoding='utf-8')) # Handle additional environment variables - env = {} # type: T.Dict[str, str] + env: T.Dict[str, str] = {} if 'env' in test_def: assert isinstance(test_def['env'], dict) env = test_def['env'] @@ -807,7 +838,7 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: env[key] = val # Handle installed files - installed = [] # type: T.List[InstalledFile] + installed: T.List[InstalledFile] = [] if 'installed' in test_def: installed = [InstalledFile(x) for x in test_def['installed']] @@ -817,7 +848,7 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: raise RuntimeError(f"{test_def_file} must contain a non-empty stdout key") # Handle the do_not_set_opts list - do_not_set_opts = test_def.get('do_not_set_opts', []) # type: T.List[str] + do_not_set_opts: T.List[str] = test_def.get('do_not_set_opts', []) (t.skip, t.skip_expected) = _skip_keys(test_def) @@ -841,12 +872,12 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: new_opt_list: T.List[T.List[T.Tuple[str, str, bool, bool]]] # 'matrix; entry is present, so build multiple tests from matrix definition - opt_list = [] # type: T.List[T.List[T.Tuple[str, str, bool, bool]]] + opt_list: T.List[T.List[T.Tuple[str, str, bool, bool]]] = [] matrix = test_def['matrix'] assert "options" in matrix for key, val in matrix["options"].items(): assert isinstance(val, list) - tmp_opts = [] # type: T.List[T.Tuple[str, str, bool, bool]] + tmp_opts: T.List[T.Tuple[str, str, bool, bool]] = [] for i in val: assert isinstance(i, dict) assert "val" in i @@ -899,7 +930,7 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: opts = [f'-D{x[0]}={x[1]}' for x in i if x[1] is not None] skip = any([x[2] for x in i]) skip_expected = any([x[3] for x in i]) - test = TestDef(t.path, name, opts, skip or t.skip) + test = TestDef(t.path, name, opts, skip or t.skip, skip_category) test.env.update(env) test.installed_files = installed test.do_not_set_opts = do_not_set_opts @@ -910,16 +941,18 @@ def load_test_json(t: TestDef, stdout_mandatory: bool) -> T.List[TestDef]: return all_tests -def gather_tests(testdir: Path, stdout_mandatory: bool, only: T.List[str]) -> T.List[TestDef]: +def gather_tests(testdir: Path, stdout_mandatory: bool, only: T.List[str], skip_category: bool) -> T.List[TestDef]: all_tests: T.List[TestDef] = [] for t in testdir.iterdir(): # Filter non-tests files (dot files, etc) if not t.is_dir() or t.name.startswith('.'): continue + if t.name in {'18 includedirxyz'}: + continue if only and not any(t.name.startswith(prefix) for prefix in only): continue - test_def = TestDef(t, None, []) - all_tests.extend(load_test_json(test_def, stdout_mandatory)) + test_def = TestDef(t, None, [], skip_category=skip_category) + all_tests.extend(load_test_json(test_def, stdout_mandatory, skip_category)) return sorted(all_tests) @@ -943,7 +976,7 @@ def have_d_compiler() -> bool: def have_objc_compiler(use_tmp: bool) -> bool: with TemporaryDirectoryWinProof(prefix='b ', dir=None if use_tmp else '.') as build_dir: - env = environment.Environment(None, build_dir, get_fake_options('/')) + env = environment.Environment('', build_dir, get_fake_options('/')) try: objc_comp = detect_objc_compiler(env, MachineChoice.HOST) except mesonlib.MesonException: @@ -959,7 +992,7 @@ def have_objc_compiler(use_tmp: bool) -> bool: def have_objcpp_compiler(use_tmp: bool) -> bool: with TemporaryDirectoryWinProof(prefix='b ', dir=None if use_tmp else '.') as build_dir: - env = environment.Environment(None, build_dir, get_fake_options('/')) + env = environment.Environment('', build_dir, get_fake_options('/')) try: objcpp_comp = detect_objcpp_compiler(env, MachineChoice.HOST) except mesonlib.MesonException: @@ -1103,7 +1136,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List TestCategory('swift', 'swift', backend not in (Backend.ninja, Backend.xcode) or not shutil.which('swiftc')), # CUDA tests on Windows: use Ninja backend: python run_project_tests.py --only cuda --backend ninja TestCategory('cuda', 'cuda', backend not in (Backend.ninja, Backend.xcode) or not shutil.which('nvcc')), - TestCategory('python3', 'python3', backend is not Backend.ninja), + TestCategory('python3', 'python3', backend is not Backend.ninja or 'python3' not in sys.executable), TestCategory('python', 'python'), TestCategory('fpga', 'fpga', shutil.which('yosys') is None), TestCategory('frameworks', 'frameworks'), @@ -1120,7 +1153,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List assert key in categories, f'key `{key}` is not a recognized category' all_tests = [t for t in all_tests if t.category in only.keys()] - gathered_tests = [(t.category, gather_tests(Path('test cases', t.subdir), t.stdout_mandatory, only[t.category]), t.skip) for t in all_tests] + gathered_tests = [(t.category, gather_tests(Path('test cases', t.subdir), t.stdout_mandatory, only[t.category], t.skip), t.skip) for t in all_tests] return gathered_tests def run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], @@ -1185,12 +1218,6 @@ class LogRunFuture: RunFutureUnion = T.Union[TestRunFuture, LogRunFuture] -def test_emits_skip_msg(line: str) -> bool: - for prefix in {'Problem encountered', 'Assert failed', 'Failed to configure the CMake subproject'}: - if f'{prefix}: MESON_SKIP_TEST' in line: - return True - return False - def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], log_name_base: str, failfast: bool, @@ -1303,15 +1330,7 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], skip_as_expected = True else: # skipped due to test outputting 'MESON_SKIP_TEST' - for l in result.stdo.splitlines(): - if test_emits_skip_msg(l): - is_skipped = True - offset = l.index('MESON_SKIP_TEST') + 16 - skip_reason = l[offset:].strip() - break - else: - is_skipped = False - skip_reason = '' + is_skipped, skip_reason = handle_meson_skip_test(result.stdo) if not skip_dont_care(t): skip_as_expected = (is_skipped == t.skip_expected) else: @@ -1322,7 +1341,8 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], if is_skipped and skip_as_expected: f.update_log(TestStatus.SKIP) - safe_print(bold('Reason:'), skip_reason) + if not t.skip_category: + safe_print(bold('Reason:'), skip_reason) current_test = ET.SubElement(current_suite, 'testcase', {'name': testname, 'classname': t.category}) ET.SubElement(current_test, 'skipped', {}) continue @@ -1330,7 +1350,7 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], if not skip_as_expected: failing_tests += 1 if is_skipped: - skip_msg = 'Test asked to be skipped, but was not expected to' + skip_msg = f'Test asked to be skipped ({skip_reason}), but was not expected to' status = TestStatus.UNEXSKIP else: skip_msg = 'Test ran, but was expected to be skipped' @@ -1338,6 +1358,7 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], result.msg = f"{skip_msg} for MESON_CI_JOBNAME '{ci_jobname}'" f.update_log(status) + safe_print(bold('Reason:'), result.msg) current_test = ET.SubElement(current_suite, 'testcase', {'name': testname, 'classname': t.category}) ET.SubElement(current_test, 'failure', {'message': result.msg}) continue @@ -1356,7 +1377,11 @@ def _run_tests(all_tests: T.List[T.Tuple[str, T.List[TestDef], bool]], left_w = max(3, left_w) right_w = cols - left_w - name_len - 2 right_w = max(3, right_w) + failing_testcases.append(name_str) failing_logs.append(f'\n\x1b[31m{"="*left_w}\x1b[0m {name_str} \x1b[31m{"="*right_w}\x1b[0m\n') + _during = bold('Failed during:') + _reason = bold('Reason:') + failing_logs.append(f'{_during} {result.step.name}\n{_reason} {result.msg}\n') if result.step == BuildStep.configure and result.mlog != no_meson_log_msg: # For configure failures, instead of printing stdout, # print the meson log if available since it's a superset @@ -1447,7 +1472,7 @@ def detect_system_compiler(options: 'CompilerArgumentType') -> None: if options.native_file: fake_opts.native_file = [options.native_file] - env = environment.Environment(None, None, fake_opts) + env = environment.Environment('', '', fake_opts) print_compilers(env, MachineChoice.HOST) if options.cross_file: @@ -1536,6 +1561,14 @@ def print_tool_versions() -> None: print('{0:<{2}}: {1}'.format(tool.tool, get_version(tool), max_width)) print() +tmpdir = list(Path('.').glob('**/*install functions and follow symlinks')) +print(tmpdir) +assert(len(tmpdir) == 1) +symlink_test_dir = tmpdir[0] +symlink_file1 = symlink_test_dir / 'foo/link1' +symlink_file2 = symlink_test_dir / 'foo/link2.h' +del tmpdir + def clear_transitive_files() -> None: a = Path('test cases/common') for d in a.glob('*subproject subdir/subprojects/subsubsub*'): @@ -1543,6 +1576,18 @@ def clear_transitive_files() -> None: mesonlib.windows_proof_rmtree(str(d)) else: mesonlib.windows_proof_rm(str(d)) + try: + symlink_file1.unlink() + except FileNotFoundError: + pass + try: + symlink_file2.unlink() + except FileNotFoundError: + pass + +def setup_symlinks() -> None: + symlink_file1.symlink_to('file1') + symlink_file2.symlink_to('file1') if __name__ == '__main__': if under_ci and not raw_ci_jobname: @@ -1586,9 +1631,10 @@ if __name__ == '__main__': options.extra_args += ['--native-file', options.native_file] clear_transitive_files() + setup_symlinks() print('Meson build system', meson_version, 'Project Tests') - print('Using python', sys.version.split('\n')[0]) + print('Using python', sys.version.split('\n')[0], f'({sys.executable!r})') if 'VSCMD_VER' in os.environ: print('VSCMD version', os.environ['VSCMD_VER']) setup_commands(options.backend) @@ -1611,10 +1657,6 @@ if __name__ == '__main__': (passing_tests, failing_tests, skipped_tests) = res except StopException: pass - print() - print('Total passed tests: ', green(str(passing_tests))) - print('Total failed tests: ', red(str(failing_tests))) - print('Total skipped tests:', yellow(str(skipped_tests))) if failing_tests > 0: print('\nMesonlogs of failing tests\n') for l in failing_logs: @@ -1622,6 +1664,14 @@ if __name__ == '__main__': print(l, '\n') except UnicodeError: print(l.encode('ascii', errors='replace').decode(), '\n') + print() + print('Total passed tests: ', green(str(passing_tests))) + print('Total failed tests: ', red(str(failing_tests))) + print('Total skipped tests:', yellow(str(skipped_tests))) + if failing_tests > 0: + print('\nAll failures:') + for c in failing_testcases: + print(f' -> {c}') for name, dirs, _ in all_tests: dir_names = list({x.path.name for x in dirs}) for k, g in itertools.groupby(dir_names, key=lambda x: x.split()[0]): diff --git a/run_single_test.py b/run_single_test.py index eb9c5bb..5cd4f5e 100755 --- a/run_single_test.py +++ b/run_single_test.py @@ -13,7 +13,8 @@ import pathlib import typing as T from mesonbuild import mlog -from run_project_tests import TestDef, load_test_json, run_test, BuildStep, test_emits_skip_msg +from run_tests import handle_meson_skip_test +from run_project_tests import TestDef, load_test_json, run_test, BuildStep from run_project_tests import setup_commands, detect_system_compiler, print_tool_versions if T.TYPE_CHECKING: @@ -27,6 +28,7 @@ if T.TYPE_CHECKING: subtests: T.List[int] backend: str extra_args: T.List[str] + quick: bool def main() -> None: @@ -39,11 +41,13 @@ def main() -> None: parser.add_argument('--cross-file', action='store', help='File describing cross compilation environment.') parser.add_argument('--native-file', action='store', help='File describing native compilation environment.') parser.add_argument('--use-tmpdir', action='store_true', help='Use tmp directory for temporary files.') + parser.add_argument('--quick', action='store_true', help='Skip some compiler and tool checking') args = T.cast('ArgumentType', parser.parse_args()) setup_commands(args.backend) - detect_system_compiler(args) - print_tool_versions() + if not args.quick: + detect_system_compiler(args) + print_tool_versions() test = TestDef(args.case, args.case.stem, []) tests = load_test_json(test, False) @@ -66,15 +70,7 @@ def main() -> None: is_skipped = True skip_reason = 'not run because preconditions were not met' else: - for l in result.stdo.splitlines(): - if test_emits_skip_msg(l): - is_skipped = True - offset = l.index('MESON_SKIP_TEST') + 16 - skip_reason = l[offset:].strip() - break - else: - is_skipped = False - skip_reason = '' + is_skipped, skip_reason = handle_meson_skip_test(result.stdo) if is_skipped: msg = mlog.yellow('SKIP:') diff --git a/run_tests.py b/run_tests.py index b03cbac..a959d6a 100755 --- a/run_tests.py +++ b/run_tests.py @@ -36,7 +36,7 @@ import typing as T from mesonbuild.compilers.c import CCompiler from mesonbuild.compilers.detect import detect_c_compiler -from mesonbuild import dependencies +from mesonbuild.dependencies.pkgconfig import PkgConfigInterface from mesonbuild import mesonlib from mesonbuild import mesonmain from mesonbuild import mtest @@ -161,6 +161,8 @@ def get_fake_env(sdir='', bdir=None, prefix='', opts=None): env = Environment(sdir, bdir, opts) env.coredata.options[OptionKey('args', lang='c')] = FakeCompilerOptions() env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library + # Invalidate cache when using a different Environment object. + clear_meson_configure_class_caches() return env def get_convincing_fake_env_and_cc(bdir, prefix): @@ -187,6 +189,15 @@ if mesonlib.is_windows() or mesonlib.is_cygwin(): else: exe_suffix = '' +def handle_meson_skip_test(out: str) -> T.Tuple[bool, str]: + for line in out.splitlines(): + for prefix in {'Problem encountered', 'Assert failed', 'Failed to configure the CMake subproject'}: + if f'{prefix}: MESON_SKIP_TEST' in line: + offset = line.index('MESON_SKIP_TEST') + 16 + reason = line[offset:].strip() + return (True, reason) + return (False, '') + def get_meson_script() -> str: ''' Guess the meson that corresponds to the `mesonbuild` that has been imported @@ -297,14 +308,13 @@ def run_mtest_inprocess(commandlist: T.List[str]) -> T.Tuple[int, str, str]: out = StringIO() with mock.patch.object(sys, 'stdout', out), mock.patch.object(sys, 'stderr', out): returncode = mtest.run_with_args(commandlist) - return returncode, stdout.getvalue() + return returncode, out.getvalue() def clear_meson_configure_class_caches() -> None: - CCompiler.find_library_cache = {} - CCompiler.find_framework_cache = {} - dependencies.PkgConfigDependency.pkgbin_cache = {} - dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None) - mesonlib.project_meson_versions = collections.defaultdict(str) + CCompiler.find_library_cache.clear() + CCompiler.find_framework_cache.clear() + PkgConfigInterface.class_impl.assign(False, False) + mesonlib.project_meson_versions.clear() def run_configure_inprocess(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: stderr = StringIO() @@ -327,11 +337,11 @@ def run_configure_external(full_command: T.List[str], env: T.Optional[T.Dict[str pc, o, e = mesonlib.Popen_safe(full_command, env=env) return pc.returncode, o, e -def run_configure(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[int, str, str]: +def run_configure(commandlist: T.List[str], env: T.Optional[T.Dict[str, str]] = None, catch_exception: bool = False) -> T.Tuple[bool, T.Tuple[int, str, str]]: global meson_exe if meson_exe: - return run_configure_external(meson_exe + commandlist, env=env) - return run_configure_inprocess(commandlist, env=env, catch_exception=catch_exception) + return (False, run_configure_external(meson_exe + commandlist, env=env)) + return (True, run_configure_inprocess(commandlist, env=env, catch_exception=catch_exception)) def print_system_info(): print(mlog.bold('System information.')) @@ -343,6 +353,10 @@ def print_system_info(): print('') print(flush=True) +def subprocess_call(cmd, **kwargs): + print(f'$ {mesonlib.join_args(cmd)}') + return subprocess.call(cmd, **kwargs) + def main(): print_system_info() parser = argparse.ArgumentParser() @@ -354,7 +368,7 @@ def main(): parser.add_argument('--no-unittests', action='store_true', default=False) (options, _) = parser.parse_known_args() returncode = 0 - backend, _ = guess_backend(options.backend, shutil.which('msbuild')) + _, backend_flags = guess_backend(options.backend, shutil.which('msbuild')) no_unittests = options.no_unittests # Running on a developer machine? Be nice! if not mesonlib.is_windows() and not mesonlib.is_haiku() and 'CI' not in os.environ: @@ -380,7 +394,7 @@ def main(): cmd = mesonlib.python_command + ['run_meson_command_tests.py', '-v'] if options.failfast: cmd += ['--failfast'] - returncode += subprocess.call(cmd, env=env) + returncode += subprocess_call(cmd, env=env) if options.failfast and returncode != 0: return returncode if no_unittests: @@ -390,14 +404,14 @@ def main(): else: print(mlog.bold('Running unittests.')) print(flush=True) - cmd = mesonlib.python_command + ['run_unittests.py', '--backend=' + backend.name, '-v'] + cmd = mesonlib.python_command + ['run_unittests.py', '-v'] + backend_flags if options.failfast: cmd += ['--failfast'] - returncode += subprocess.call(cmd, env=env) + returncode += subprocess_call(cmd, env=env) if options.failfast and returncode != 0: return returncode cmd = mesonlib.python_command + ['run_project_tests.py'] + sys.argv[1:] - returncode += subprocess.call(cmd, env=env) + returncode += subprocess_call(cmd, env=env) else: cross_test_args = mesonlib.python_command + ['run_cross_test.py'] for cf in options.cross: @@ -408,7 +422,7 @@ def main(): cmd += ['--failfast'] if options.cross_only: cmd += ['--cross-only'] - returncode += subprocess.call(cmd, env=env) + returncode += subprocess_call(cmd, env=env) if options.failfast and returncode != 0: return returncode return returncode diff --git a/run_unittests.py b/run_unittests.py index ddcde76..7a2502a 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -36,6 +36,7 @@ from mesonbuild.mesonlib import python_command, setup_vsenv import mesonbuild.modules.pkgconfig from unittests.allplatformstests import AllPlatformTests +from unittests.cargotests import CargoVersionTest, CargoCfgTest from unittests.darwintests import DarwinTests from unittests.failuretests import FailureTests from unittests.linuxcrosstests import LinuxCrossArmTests, LinuxCrossMingwTests @@ -135,7 +136,7 @@ def main(): # Let there be colors! if 'CI' in os.environ: pytest_args += ['--color=yes'] - pytest_args += ['./run_unittests.py'] + pytest_args += ['unittests'] pytest_args += convert_args(sys.argv[1:]) # Always disable pytest-cov because we use a custom setup try: diff --git a/setup.cfg b/setup.cfg index 2d82fbe..2f2962e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,13 +5,15 @@ description = A high performance build system author = Jussi Pakkanen author_email = jpakkane@gmail.com url = https://mesonbuild.com +project_urls = + Source = https://github.com/mesonbuild/meson keywords = meson mesonbuild build system cmake license = Apache License, Version 2.0 -license_file = COPYING +license_files = COPYING classifiers = Development Status :: 5 - Production/Stable Environment :: Console @@ -28,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development :: Build Tools long_description = Meson is a cross-platform build system designed to be both as fast and as user friendly as possible. It supports many languages and compilers, including GCC, Clang, PGI, Intel, and Visual Studio. Its build definitions are written in a simple non-Turing complete DSL. @@ -60,4 +63,4 @@ include = mesonbuild, mesonbuild.* [tool:pytest] python_classes = python_files = - run_unittests.py + unittests/*tests.py diff --git a/test cases/cmake/26 cmake package prefix dir/cmakePackagePrefixDirConfig.cmake.in b/test cases/cmake/26 cmake package prefix dir/cmakePackagePrefixDirConfig.cmake.in new file mode 100644 index 0000000..a18cb7d --- /dev/null +++ b/test cases/cmake/26 cmake package prefix dir/cmakePackagePrefixDirConfig.cmake.in @@ -0,0 +1 @@ +@PACKAGE_INIT@ diff --git a/test cases/cmake/26 cmake package prefix dir/meson.build b/test cases/cmake/26 cmake package prefix dir/meson.build new file mode 100644 index 0000000..851371b --- /dev/null +++ b/test cases/cmake/26 cmake package prefix dir/meson.build @@ -0,0 +1,19 @@ +project('cmakePackagePrefixDir', 'c', version: '1.0.0') + +cmake = import('cmake') + +cmake.configure_package_config_file( + name: 'cmakePackagePrefixDir', + input: 'cmakePackagePrefixDirConfig.cmake.in', + configuration: configuration_data(), +) + +# NOTE: can't use fs.read because cmakePackagePrefixDirConfig.cmake is in build_dir +python = find_program('python3') +lines = run_command(python, '-c', + '[print(line, end="") for line in open("@0@")]'.format(meson.current_build_dir() / 'cmakePackagePrefixDirConfig.cmake'), check : true, +).stdout().split('\n') + +message(lines) + +assert(lines[5] == 'get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)') diff --git a/test cases/cmake/26 cmake package prefix dir/test.json b/test cases/cmake/26 cmake package prefix dir/test.json new file mode 100644 index 0000000..d6a9505 --- /dev/null +++ b/test cases/cmake/26 cmake package prefix dir/test.json @@ -0,0 +1,5 @@ +{ + "installed": [ + {"type": "file", "file": "usr/lib/cmake/cmakePackagePrefixDir/cmakePackagePrefixDirConfig.cmake"} + ] +} diff --git a/test cases/cmake/27 dependency fallback/main.cpp b/test cases/cmake/27 dependency fallback/main.cpp new file mode 100644 index 0000000..9507961 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/main.cpp @@ -0,0 +1,10 @@ +#include +#include + +using namespace std; + +int main(void) { + cmModClass obj("Hello"); + cout << obj.getStr() << endl; + return 0; +} diff --git a/test cases/cmake/27 dependency fallback/meson.build b/test cases/cmake/27 dependency fallback/meson.build new file mode 100644 index 0000000..871d70c --- /dev/null +++ b/test cases/cmake/27 dependency fallback/meson.build @@ -0,0 +1,30 @@ +project('cmakeSubTest', ['c', 'cpp']) + +# Fallback to a CMake subproject +sub_dep = dependency('cmModLib++') +exe1 = executable('main', ['main.cpp'], dependencies: [sub_dep]) +test('test1', exe1) + +# Subproject contains both meson.build and CMakeLists.txt. It should default +# to meson but wrap force cmake. +subproject('force_cmake') + +testcase expect_error('Wrap method \'notfound\' is not supported, must be one of: meson, cmake, cargo') + subproject('broken_method') +endtestcase + +# With method=meson we can't use cmake.subproject() +cmake = import('cmake') +testcase expect_error('Wrap method is \'meson\' but we are trying to configure it with cmake') + cmake.subproject('meson_method') +endtestcase + +# cmake.subproject() force cmake method even if meson.build exists. +testcase expect_error('Subproject exists but has no CMakeLists.txt file.') + cmake.subproject('meson_subp') +endtestcase + +# Without specifying the method it defaults to meson even if CMakeLists.txt exists. +testcase expect_error('Subproject exists but has no meson.build file.') + subproject('cmake_subp') +endtestcase diff --git a/test cases/cmake/27 dependency fallback/subprojects/broken_method.wrap b/test cases/cmake/27 dependency fallback/subprojects/broken_method.wrap new file mode 100644 index 0000000..ce0690a --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/broken_method.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method=notfound diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmMod.wrap b/test cases/cmake/27 dependency fallback/subprojects/cmMod.wrap new file mode 100644 index 0000000..9e6d855 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmMod.wrap @@ -0,0 +1,5 @@ +[wrap-file] +method = cmake + +[provide] +cmModLib++ = cmModLib___dep diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/27 dependency fallback/subprojects/cmMod/CMakeLists.txt new file mode 100644 index 0000000..d08e55c --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmMod/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.5) + +project(cmMod) +set(CMAKE_CXX_STANDARD 14) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +add_definitions("-DDO_NOTHING_JUST_A_FLAG=1") + +add_library(cmModLib++ SHARED cmMod.cpp) +target_compile_definitions(cmModLib++ PRIVATE MESON_MAGIC_FLAG=21) +target_compile_definitions(cmModLib++ INTERFACE MESON_MAGIC_FLAG=42) + +# Test PCH support +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") + target_precompile_headers(cmModLib++ PRIVATE "cpp_pch.hpp") +endif() + +include(GenerateExportHeader) +generate_export_header(cmModLib++) diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.cpp b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.cpp new file mode 100644 index 0000000..f4cbea0 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.cpp @@ -0,0 +1,15 @@ +#include "cmMod.hpp" + +using namespace std; + +#if MESON_MAGIC_FLAG != 21 +#error "Invalid MESON_MAGIC_FLAG (private)" +#endif + +cmModClass::cmModClass(string foo) { + str = foo + " World"; +} + +string cmModClass::getStr() const { + return str; +} diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.hpp b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.hpp new file mode 100644 index 0000000..4445e1f --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cmMod.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "cmmodlib++_export.h" +#include + +#if MESON_MAGIC_FLAG != 42 && MESON_MAGIC_FLAG != 21 +#error "Invalid MESON_MAGIC_FLAG" +#endif + +class CMMODLIB___EXPORT cmModClass { +private: + std::string str; + +public: + cmModClass(std::string foo); + + std::string getStr() const; +}; diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmMod/cpp_pch.hpp b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cpp_pch.hpp new file mode 100644 index 0000000..aa7ceb3 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmMod/cpp_pch.hpp @@ -0,0 +1,2 @@ +#include +#include diff --git a/test cases/cmake/27 dependency fallback/subprojects/cmake_subp/CMakeLists.txt b/test cases/cmake/27 dependency fallback/subprojects/cmake_subp/CMakeLists.txt new file mode 100644 index 0000000..6443fca --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/cmake_subp/CMakeLists.txt @@ -0,0 +1,2 @@ +cmake_minimum_required(VERSION 3.5) +project(cmModDummy) diff --git a/test cases/cmake/27 dependency fallback/subprojects/force_cmake.wrap b/test cases/cmake/27 dependency fallback/subprojects/force_cmake.wrap new file mode 100644 index 0000000..b24754e --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/force_cmake.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method=cmake diff --git a/test cases/cmake/27 dependency fallback/subprojects/force_cmake/CMakeLists.txt b/test cases/cmake/27 dependency fallback/subprojects/force_cmake/CMakeLists.txt new file mode 100644 index 0000000..497beb9 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/force_cmake/CMakeLists.txt @@ -0,0 +1,2 @@ +cmake_minimum_required(VERSION 3.5) +project(cmModBoth) diff --git a/test cases/cmake/27 dependency fallback/subprojects/force_cmake/meson.build b/test cases/cmake/27 dependency fallback/subprojects/force_cmake/meson.build new file mode 100644 index 0000000..9264974 --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/force_cmake/meson.build @@ -0,0 +1,4 @@ +project('both methods') + +# Ensure the meson method is not used. +notfound() diff --git a/test cases/cmake/27 dependency fallback/subprojects/meson_method.wrap b/test cases/cmake/27 dependency fallback/subprojects/meson_method.wrap new file mode 100644 index 0000000..e52701e --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/meson_method.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method=meson diff --git a/test cases/cmake/27 dependency fallback/subprojects/meson_subp/meson.build b/test cases/cmake/27 dependency fallback/subprojects/meson_subp/meson.build new file mode 100644 index 0000000..e4746ce --- /dev/null +++ b/test cases/cmake/27 dependency fallback/subprojects/meson_subp/meson.build @@ -0,0 +1 @@ +project('dummy') diff --git a/test cases/cmake/9 disabled subproject/meson.build b/test cases/cmake/9 disabled subproject/meson.build index c153fa3..b22f959 100644 --- a/test cases/cmake/9 disabled subproject/meson.build +++ b/test cases/cmake/9 disabled subproject/meson.build @@ -2,5 +2,5 @@ project('cmakeSubTest', ['c', 'cpp']) cm = import('cmake') -sub_pro = cm.subproject('nothinig', required: false) +sub_pro = cm.subproject('nothing', required: false) assert(not sub_pro.found(), 'subproject found() reports wrong value') diff --git a/test cases/common/100 postconf with args/postconf.py b/test cases/common/100 postconf with args/postconf.py index cef7f79..af6abe4 100644 --- a/test cases/common/100 postconf with args/postconf.py +++ b/test cases/common/100 postconf with args/postconf.py @@ -12,7 +12,7 @@ template = '''#pragma once input_file = os.path.join(os.environ['MESON_SOURCE_ROOT'], 'raw.dat') output_file = os.path.join(os.environ['MESON_BUILD_ROOT'], 'generated.h') -with open(input_file) as f: +with open(input_file, encoding='utf-8') as f: data = f.readline().strip() -with open(output_file, 'w') as f: +with open(output_file, 'w', encoding='utf-8') as f: f.write(template.format(data, sys.argv[1], sys.argv[2])) diff --git a/test cases/common/102 extract same name/meson.build b/test cases/common/102 extract same name/meson.build index 08daa5b..86e68b9 100644 --- a/test cases/common/102 extract same name/meson.build +++ b/test cases/common/102 extract same name/meson.build @@ -9,7 +9,7 @@ if meson.backend() == 'xcode' # # No-one has reverse engineered the naming scheme so we would access them. # IF you feel up to the challenge, patches welcome. - error('MESON_SKIP_TEST, Xcode can not extract objs when they would have the same filename.') + error('MESON_SKIP_TEST, Xcode cannot extract objs when they would have the same filename.') endif lib = library('somelib', ['lib.c', 'src/lib.c']) diff --git a/test cases/common/105 generatorcustom/gen.c b/test cases/common/105 generatorcustom/gen.c new file mode 100644 index 0000000..964ae7e --- /dev/null +++ b/test cases/common/105 generatorcustom/gen.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Copyright © 2023 Intel Corporation */ + +#include +#include + +int main(int argc, const char ** argv) { + if (argc != 3) { + fprintf(stderr, "%s %i %s\n", "Got incorrect number of arguments, got ", argc - 1, ", but expected 2"); + exit(1); + } + + FILE * input, * output; + + if ((input = fopen(argv[1], "rb")) == NULL) { + exit(1); + } + if ((output = fopen(argv[2], "wb")) == NULL) { + exit(1); + } + + fprintf(output, "#pragma once\n"); + fprintf(output, "#define "); + + int bytes_copied = 0; + int c; + while((c = fgetc(input)) != EOF) { + if(fputc(c, output) == EOF) { + fprintf(stderr, "Writing to output file failed.\n"); + return 1; + } + if(++bytes_copied > 10000) { + fprintf(stderr, "File copy stuck in an eternal loop!\n"); + return 1; + } + } + fputc('\n', output); + + fclose(input); + fclose(output); + + return 0; +} diff --git a/test cases/common/105 generatorcustom/host.c b/test cases/common/105 generatorcustom/host.c new file mode 100644 index 0000000..1ddfa88 --- /dev/null +++ b/test cases/common/105 generatorcustom/host.c @@ -0,0 +1,9 @@ +#include "res1-cpp.h" + +int main(void) { + #ifdef res1 + return 0; + #else + return 1; + #endif +} diff --git a/test cases/common/105 generatorcustom/meson.build b/test cases/common/105 generatorcustom/meson.build index 2128d21..dab55de 100644 --- a/test cases/common/105 generatorcustom/meson.build +++ b/test cases/common/105 generatorcustom/meson.build @@ -26,3 +26,19 @@ allinone = custom_target('alltogether', proggie = executable('proggie', 'main.c', allinone) test('proggie', proggie) + +# specifically testing that cross binaries are run with an exe_wrapper +if meson.can_run_host_binaries() + gen_tool = executable('generator', 'gen.c', native : false) + + c_gen = generator( + gen_tool, + output : '@BASENAME@-cpp.h', + arguments : ['@INPUT@', '@OUTPUT@'] + ) + + hs2 = c_gen.process('res1.txt') + + host_exe = executable('host_test', 'host.c', hs2, native : false) + test('compiled generator', host_exe) +endif diff --git a/test cases/common/117 shared module/meson.build b/test cases/common/117 shared module/meson.build index 936c839..94d17a7 100644 --- a/test cases/common/117 shared module/meson.build +++ b/test cases/common/117 shared module/meson.build @@ -36,5 +36,6 @@ test('import test 2', e, args : m2) # Shared module that does not export any symbols shared_module('nosyms', 'nosyms.c', + override_options: ['werror=false'], install : true, install_dir : join_paths(get_option('libdir'), 'modules')) diff --git a/test cases/common/12 data/meson.build b/test cases/common/12 data/meson.build index d318633..aa02131 100644 --- a/test cases/common/12 data/meson.build +++ b/test cases/common/12 data/meson.build @@ -22,3 +22,7 @@ install_data(sources : ['vanishing/to_be_renamed_2.txt', 'to_be_renamed_3.txt'], install_dir : 'share/renamed', rename : ['renamed 2.txt', 'renamed 3.txt']) install_data(sources : 'to_be_renamed_4.txt', rename : 'some/nested/path.txt') + +install_data('subdir/data.txt', preserve_path : true) + +subproject('moredata') diff --git a/test cases/unit/99 custom target name/file.txt.in b/test cases/common/12 data/subdir/data.txt similarity index 100% rename from test cases/unit/99 custom target name/file.txt.in rename to test cases/common/12 data/subdir/data.txt diff --git a/test cases/common/12 data/subprojects/moredata/data.txt b/test cases/common/12 data/subprojects/moredata/data.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test cases/common/12 data/subprojects/moredata/data.txt @@ -0,0 +1 @@ + diff --git a/test cases/common/12 data/subprojects/moredata/meson.build b/test cases/common/12 data/subprojects/moredata/meson.build new file mode 100644 index 0000000..6234e26 --- /dev/null +++ b/test cases/common/12 data/subprojects/moredata/meson.build @@ -0,0 +1,3 @@ +project('moredata') + +install_data('data.txt') diff --git a/test cases/common/12 data/test.json b/test cases/common/12 data/test.json index f392e9a..c5fef01 100644 --- a/test cases/common/12 data/test.json +++ b/test cases/common/12 data/test.json @@ -10,6 +10,8 @@ {"type": "file", "file": "usr/share/renamed/renamed 2.txt"}, {"type": "file", "file": "usr/share/renamed/renamed 3.txt"}, {"type": "file", "file": "etc/etcfile.dat"}, - {"type": "file", "file": "usr/bin/runscript.sh"} + {"type": "file", "file": "usr/bin/runscript.sh"}, + {"type": "file", "file": "usr/share/moredata/data.txt"}, + {"type": "file", "file": "usr/share/data install test/subdir/data.txt"} ] } diff --git a/test cases/common/13 pch/meson.build b/test cases/common/13 pch/meson.build index 4bb3e1f..d832919 100644 --- a/test cases/common/13 pch/meson.build +++ b/test cases/common/13 pch/meson.build @@ -13,7 +13,11 @@ subdir('cpp') subdir('generated') subdir('userDefined') subdir('withIncludeDirectories') -subdir('withIncludeFile') +if meson.backend() == 'xcode' + warning('Xcode backend does not support forced includes. Skipping "withIncludeFile" which requires this.') +else + subdir('withIncludeFile') +endif if meson.backend() == 'xcode' warning('Xcode backend only supports one precompiled header per target. Skipping "mixed" which has various precompiled headers.') diff --git a/test cases/common/130 include order/inc3/meson.build b/test cases/common/130 include order/inc3/meson.build new file mode 100644 index 0000000..3c100c4 --- /dev/null +++ b/test cases/common/130 include order/inc3/meson.build @@ -0,0 +1,2 @@ +configure_file(output: 'prefer-build-dir-over-src-dir.h', + configuration: configuration_data()) diff --git a/test cases/common/130 include order/inc3/prefer-build-dir-over-src-dir.h b/test cases/common/130 include order/inc3/prefer-build-dir-over-src-dir.h new file mode 100644 index 0000000..0b07943 --- /dev/null +++ b/test cases/common/130 include order/inc3/prefer-build-dir-over-src-dir.h @@ -0,0 +1 @@ +#error "inc3/prefer-build-dir-over-src-dir.h included" diff --git a/test cases/common/130 include order/meson.build b/test cases/common/130 include order/meson.build index 9f275b8..8e05866 100644 --- a/test cases/common/130 include order/meson.build +++ b/test cases/common/130 include order/meson.build @@ -12,6 +12,8 @@ project('include order', 'c') # Custom target dir with a built header subdir('ctsub') +# Configures a header file +subdir('inc3') # Defines an internal dep subdir('sub1') # Defines a per-target include path @@ -32,5 +34,5 @@ test('eh', e) test('oh', f) # Test that the order in include_directories() is maintained -incs = include_directories('inc1', 'inc2') +incs = include_directories('inc1', 'inc2', 'inc3') executable('ordertest', 'ordertest.c', include_directories: incs) diff --git a/test cases/common/130 include order/ordertest.c b/test cases/common/130 include order/ordertest.c index 775e34f..6784af7 100644 --- a/test cases/common/130 include order/ordertest.c +++ b/test cases/common/130 include order/ordertest.c @@ -1,4 +1,5 @@ #include "hdr.h" +#include "prefer-build-dir-over-src-dir.h" #if !defined(SOME_DEFINE) || SOME_DEFINE != 42 #error "Should have picked up hdr.h from inc1/hdr.h" diff --git a/test cases/common/132 get define/meson.build b/test cases/common/132 get define/meson.build index 02e5a0c..66ac3f9 100644 --- a/test cases/common/132 get define/meson.build +++ b/test cases/common/132 get define/meson.build @@ -2,49 +2,50 @@ project('get define', 'c', 'cpp') host_system = host_machine.system() +system_define_map = { + 'linux' : ['__linux__', '1'], + 'darwin' : ['__APPLE__', '1'], + 'windows' : ['_WIN32', '1'], + 'cygwin' : ['__CYGWIN__', '1'], + 'haiku' : ['__HAIKU__', '1'], + 'dragonfly' : ['__DragonFly__', '1'], + 'netbsd' : ['__NetBSD__', '1'], + 'openbsd' : ['__OpenBSD__', '1'], + 'gnu' : ['__GNU__', '1'], + 'sunos' : ['__sun__', '1'], + + # The __FreeBSD__ define will be equal to the major version of the release + # (ex, in FreeBSD 11.x, __FreeBSD__ == 11). To make the test robust when + # being run on various versions of FreeBSD, just test that the define is + # set. + 'freebsd' : ['__FreeBSD__'], +} + foreach lang : ['c', 'cpp'] cc = meson.get_compiler(lang) - if host_system == 'linux' - d = cc.get_define('__linux__') - assert(d == '1', '__linux__ value is @0@ instead of 1'.format(d)) - elif host_system == 'darwin' - d = cc.get_define('__APPLE__') - assert(d == '1', '__APPLE__ value is @0@ instead of 1'.format(d)) - elif host_system == 'windows' - d = cc.get_define('_WIN32') - assert(d == '1', '_WIN32 value is @0@ instead of 1'.format(d)) - elif host_system == 'cygwin' - d = cc.get_define('__CYGWIN__') - assert(d == '1', '__CYGWIN__ value is @0@ instead of 1'.format(d)) - elif host_system == 'haiku' - d = cc.get_define('__HAIKU__') - assert(d == '1', '__HAIKU__ value is @0@ instead of 1'.format(d)) - elif host_system == 'freebsd' - # the __FreeBSD__ define will be equal to the major version of the release - # (ex, in FreeBSD 11.x, __FreeBSD__ == 11). To make the test robust when - # being run on various versions of FreeBSD, just test that the define is - # set. - d = cc.get_define('__FreeBSD__') - assert(d != '', '__FreeBSD__ value is unset') - elif host_system == 'dragonfly' - d = cc.get_define('__DragonFly__') - assert(d == '1', '__DragonFly__ value is @0@ instead of 1'.format(d)) - elif host_system == 'netbsd' - d = cc.get_define('__NetBSD__') - assert(d == '1', '__NetBSD__ value is @0@ instead of 1'.format(d)) - elif host_system == 'openbsd' - d = cc.get_define('__OpenBSD__') - assert(d == '1', '__OpenBSD__ value is @0@ instead of 1'.format(d)) - elif host_system == 'gnu' - d = cc.get_define('__GNU__') - assert(d == '1', '__GNU__ value is @0@ instead of 1'.format(d)) - elif host_system == 'sunos' - d = cc.get_define('__sun__') - assert(d == '1', '__sun__ value is @0@ instead of 1'.format(d)) - else + + if not system_define_map.has_key(host_system) error('Please report a bug and help us improve support for this platform') endif + system_define = system_define_map.get(host_system) + + def_name = system_define[0] + def_val = cc.get_define(system_define[0]) + def_exist = cc.has_define(system_define[0]) + + assert((def_val != '') == def_exist, + 'The has_define and get_define results for @0@ disagree with each other'.format(def_name)) + + if system_define.length() == 2 + assert(def_val == system_define[1], + '@0@ value is @1@ instead of @2@'.format(def_name, def_val, system_define[1])) + elif system_define.length() == 1 + assert(def_val != '', '@0@ value is unset'.format(def_name)) + else + error('Invalid number of items in system_define array, this is a bug in the test!') + endif + if cc.find_library('z', required : false).found() # When a C file containing #include is pre-processed and foo.h is # found in the compiler's default search path, GCC inserts an extra comment @@ -63,8 +64,16 @@ foreach lang : ['c', 'cpp'] endif # Check that an undefined value is empty. - have = cc.get_define('MESON_FAIL_VALUE') - assert(have == '', 'MESON_FAIL_VALUE value is "@0@" instead of ""'.format(have)) + have_val = cc.get_define('MESON_FAIL_VALUE') + have = cc.has_define('MESON_FAIL_VALUE') + assert(have_val == '', 'MESON_FAIL_VALUE value is "@0@" instead of ""'.format(have_val)) + assert(not have, 'MESON_FAIL_VALUE was found even though it should not have been') + + # Check that an empty define is reported as existing. + have_val = cc.get_define('MESON_EMPTY_VALUE', prefix: ['#define MESON_EMPTY_VALUE']) + have = cc.has_define('MESON_EMPTY_VALUE', prefix: ['#define MESON_EMPTY_VALUE']) + assert(have_val == '', 'MESON_EMPTY_VALUE value is "@0@" instead of ""'.format(have_val)) + assert(have, 'MESON_EMPTY_VALUE was not found even though it should have been') # Check if prefix array works properly and has the expected order have = cc.get_define('MESON_FAIL_VALUE', prefix: ['#define MESON_FAIL_VALUE 1', '#undef MESON_FAIL_VALUE']) diff --git a/test cases/common/132 get define/test.json b/test cases/common/132 get define/test.json new file mode 100644 index 0000000..f783084 --- /dev/null +++ b/test cases/common/132 get define/test.json @@ -0,0 +1,10 @@ +{ + "matrix": { + "options": { + "c_std": [ + { "val": "none" }, + { "val": "c11" } + ] + } + } +} diff --git a/test cases/common/14 configure file/meson.build b/test cases/common/14 configure file/meson.build index 569dd09..90a468f 100644 --- a/test cases/common/14 configure file/meson.build +++ b/test cases/common/14 configure file/meson.build @@ -161,7 +161,7 @@ cfile = configure_file(input : 'config.h.in', install_dir : false, configuration : conf) -# test intsall_dir with install: false +# test install_dir with install: false cfile = configure_file(input : 'config.h.in', output : 'do_not_get_installed_in_install_dir.h', install : false, diff --git a/test cases/common/147 simd/simd_mmx.c b/test cases/common/147 simd/simd_mmx.c index 7605442..443deaf 100644 --- a/test cases/common/147 simd/simd_mmx.c +++ b/test cases/common/147 simd/simd_mmx.c @@ -45,14 +45,16 @@ void increment_mmx(float arr[4]) { * enough to fit in int16; */ int i; + /* This is unused due to below comment about GCC 8. __m64 packed = _mm_set_pi16(arr[3], arr[2], arr[1], arr[0]); __m64 incr = _mm_set1_pi16(1); __m64 result = _mm_add_pi16(packed, incr); - /* Should be + int64_t unpacker = (int64_t)(result); + */ + /* The above should be * int64_t unpacker = _m_to_int64(result); * but it does not exist on 32 bit platforms for some reason. */ - int64_t unpacker = (int64_t)(result); _mm_empty(); for(i=0; i<4; i++) { /* This fails on GCC 8 when optimizations are enabled. diff --git a/test cases/common/148 shared module resolving symbol in executable/prog.c b/test cases/common/148 shared module resolving symbol in executable/prog.c index b2abcdb..55ffee0 100644 --- a/test cases/common/148 shared module resolving symbol in executable/prog.c +++ b/test cases/common/148 shared module resolving symbol in executable/prog.c @@ -30,7 +30,7 @@ int main(int argc, char **argv) int expected, actual; fptr importedfunc; - if (argc=0) {}; // noop + (void)argc; // noop #ifdef _WIN32 HMODULE h = LoadLibraryA(argv[1]); diff --git a/test cases/common/151 duplicate source names/meson.build b/test cases/common/151 duplicate source names/meson.build index 635aa8c..74fc0ff 100644 --- a/test cases/common/151 duplicate source names/meson.build +++ b/test cases/common/151 duplicate source names/meson.build @@ -9,7 +9,7 @@ if meson.backend() == 'xcode' # # No-one has reverse engineered the naming scheme so we would access them. # IF you feel up to the challenge, patches welcome. - error('MESON_SKIP_TEST, Xcode can not extract objs when they would have the same filename.') + error('MESON_SKIP_TEST, Xcode cannot extract objs when they would have the same filename.') endif sources = [] diff --git a/test cases/common/158 disabler/meson.build b/test cases/common/158 disabler/meson.build index d132e2b..65ca5fd 100644 --- a/test cases/common/158 disabler/meson.build +++ b/test cases/common/158 disabler/meson.build @@ -97,7 +97,7 @@ assert(if_is_not_disabled, 'Disabler in is_variable should not skip blocks') get_d = get_variable('d6') assert(is_disabler(get_d), 'get_variable should yield a disabler') -get_fallback_d = get_variable('nonexistant', disabler()) +get_fallback_d = get_variable('nonexistent', disabler()) assert(is_disabler(get_fallback_d), 'get_variable fallback should yield a disabler') var_true = true diff --git a/test cases/common/178 bothlibraries/meson.build b/test cases/common/178 bothlibraries/meson.build index bb3a2bc..62f2061 100644 --- a/test cases/common/178 bothlibraries/meson.build +++ b/test cases/common/178 bothlibraries/meson.build @@ -54,9 +54,18 @@ test('runtest-both-2', exe_both2) # both_libraries the static has no sources and thus no compilers, resulting in # the executable linking using the C compiler. # https://github.com/Netflix/vmaf/issues/1107 -libccpp = both_libraries('ccpp', 'foo.cpp', 'libfile.c') +libccpp = both_libraries('ccpp', 'foo.cpp', 'libfile.c', + cpp_args : ['-std=c++11'], + c_static_args : ['-DSTATIC_COMPILATION'], + cpp_static_args : ['-DSTATIC_COMPILATION'], +) exe = executable('prog-ccpp', 'main2.c', link_with: libccpp.get_static_lib(), c_args : ['-DSTATIC_COMPILATION'], ) test('runtest-ccpp', exe) + +exe = executable('prog-ccpp-shared', 'main2.c', + link_with: libccpp.get_shared_lib(), +) +test('runtest-ccpp-shared', exe) diff --git a/test cases/common/18 includedir/meson.build b/test cases/common/18 includedir/meson.build index 17eec0e..3180587 100644 --- a/test cases/common/18 includedir/meson.build +++ b/test cases/common/18 includedir/meson.build @@ -2,3 +2,31 @@ project('include dir test', 'c') inc = include_directories('include') subdir('src') + +errormsg = '''Tried to form an absolute path to a dir in the source tree. +You should not do that but use relative paths instead, for +directories that are part of your project. + +To get include path to any directory relative to the current dir do + +incdir = include_directories(dirname) + +After this incdir will contain both the current source dir as well as the +corresponding build dir. It can then be used in any subdirectory and +Meson will take care of all the busywork to make paths work. + +Dirname can even be '.' to mark the current directory. Though you should +remember that the current source and build directories are always +put in the include directories by default so you only need to do +include_directories('.') if you intend to use the result in a +different subdirectory. + +Note that this error message can also be triggered by +external dependencies being installed within your source +tree - it's not recommended to do this. +''' +testcase expect_error(errormsg) + include_directories(meson.current_source_dir() / 'include') +endtestcase +# Test for issue #12217 +include_directories(meson.current_source_dir() + 'xyz') diff --git a/test cases/common/18 includedirxyz/do_not_delete b/test cases/common/18 includedirxyz/do_not_delete new file mode 100644 index 0000000..8bd0f88 --- /dev/null +++ b/test cases/common/18 includedirxyz/do_not_delete @@ -0,0 +1 @@ +This file is to ensure this directory exists diff --git a/test cases/common/182 find override/meson.build b/test cases/common/182 find override/meson.build index 8dcbac7..edb1687 100644 --- a/test cases/common/182 find override/meson.build +++ b/test cases/common/182 find override/meson.build @@ -18,8 +18,14 @@ assert(tool.found()) assert(tool.full_path() != '') assert(tool.full_path() == tool.path()) -# six_meson_exe is an overritten project executable +# six_meson_exe is an overridden project executable six_prog = find_program('six_meson_exe') assert(six_prog.found()) assert(six_prog.full_path() != '') assert(six_prog.full_path() == six_prog.path()) + +# We have prog-version.py in current directory, but it's version 1.0. +# This needs to use fallback for "prog-version" name which will be version 2.0. +prog = find_program('prog-version.py', 'prog-version', version: '>= 2.0') +assert(prog.found()) +assert(prog.version() == '2.0') diff --git a/test cases/common/182 find override/prog-version.py b/test cases/common/182 find override/prog-version.py new file mode 100755 index 0000000..c00dd99 --- /dev/null +++ b/test cases/common/182 find override/prog-version.py @@ -0,0 +1,3 @@ +#! /usr/bin/env python3 + +print('1.0') diff --git a/test cases/common/182 find override/subprojects/sub2.wrap b/test cases/common/182 find override/subprojects/sub2.wrap new file mode 100644 index 0000000..035629f --- /dev/null +++ b/test cases/common/182 find override/subprojects/sub2.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = sub2 + +[provide] +program_names = prog-version diff --git a/test cases/common/182 find override/subprojects/sub2/meson.build b/test cases/common/182 find override/subprojects/sub2/meson.build new file mode 100644 index 0000000..f542073 --- /dev/null +++ b/test cases/common/182 find override/subprojects/sub2/meson.build @@ -0,0 +1,4 @@ +project('sub2') + +prog = find_program('prog-version.py') +meson.override_find_program('prog-version', prog) diff --git a/test cases/common/182 find override/subprojects/sub2/prog-version.py b/test cases/common/182 find override/subprojects/sub2/prog-version.py new file mode 100755 index 0000000..78401bb --- /dev/null +++ b/test cases/common/182 find override/subprojects/sub2/prog-version.py @@ -0,0 +1,3 @@ +#! /usr/bin/env python3 + +print('2.0') diff --git a/test cases/common/189 check header/meson.build b/test cases/common/189 check header/meson.build index 98b395d..1d3eb45 100644 --- a/test cases/common/189 check header/meson.build +++ b/test cases/common/189 check header/meson.build @@ -44,5 +44,5 @@ foreach comp : [meson.get_compiler('c'), meson.get_compiler('cpp')] # This header exists in the source and the builddir, but we still must not # find it since we are looking in the system directories. assert(not comp.check_header(non_existent_header, prefix : fallback), - 'Found non-existent header.') + 'Found nonexistent header.') endforeach diff --git a/test cases/common/192 feature option/meson.build b/test cases/common/192 feature option/meson.build index b5e26fa..a2ebe32 100644 --- a/test cases/common/192 feature option/meson.build +++ b/test cases/common/192 feature option/meson.build @@ -15,8 +15,13 @@ assert(not required_opt.disabled(), 'Should be enabled option') assert(not required_opt.auto(), 'Should be enabled option') assert(required_opt.allowed(), 'Should be enabled option') assert(required_opt.require(true, error_message: 'xyz').enabled(), 'Should be enabled option') +assert(required_opt.enable_if(true, error_message: 'xyz').enabled(), 'Should be enabled option') +assert(required_opt.enable_if(false, error_message: 'xyz').enabled(), 'Should be enabled option') +assert(required_opt.disable_if(false, error_message: 'xyz').enabled(), 'Should be enabled option') assert(required_opt.disable_auto_if(true).enabled(), 'Should be enabled option') assert(required_opt.disable_auto_if(false).enabled(), 'Should be enabled option') +assert(required_opt.enable_auto_if(true).enabled(), 'Should be enabled option') +assert(required_opt.enable_auto_if(false).enabled(), 'Should be enabled option') assert(not optional_opt.enabled(), 'Should be auto option') assert(not optional_opt.disabled(), 'Should be auto option') @@ -24,8 +29,14 @@ assert(optional_opt.auto(), 'Should be auto option') assert(optional_opt.allowed(), 'Should be auto option') assert(optional_opt.require(true).auto(), 'Should be auto option') assert(optional_opt.require(false, error_message: 'xyz').disabled(), 'Should be disabled auto option') +assert(optional_opt.enable_if(true).enabled(), 'Should be enabled option') +assert(optional_opt.enable_if(false).auto(), 'Should be auto option') +assert(optional_opt.disable_if(true).disabled(), 'Should be disabled auto option') +assert(optional_opt.disable_if(false).auto(), 'Should be auto option') assert(optional_opt.disable_auto_if(true).disabled(), 'Should be disabled auto option') assert(optional_opt.disable_auto_if(false).auto(), 'Should be auto option') +assert(optional_opt.enable_auto_if(true).enabled(), 'Should be disabled auto option') +assert(optional_opt.enable_auto_if(false).auto(), 'Should be auto option') assert(not disabled_opt.enabled(), 'Should be disabled option') assert(disabled_opt.disabled(), 'Should be disabled option') @@ -33,8 +44,13 @@ assert(not disabled_opt.auto(), 'Should be disabled option') assert(not disabled_opt.allowed(), 'Should be disabled option') assert(disabled_opt.require(true).disabled(), 'Should be disabled option') assert(disabled_opt.require(false, error_message: 'xyz').disabled(), 'Should be disabled option') +assert(disabled_opt.enable_if(false).disabled(), 'Should be disabled option') +assert(disabled_opt.disable_if(true).disabled(), 'Should be disabled option') +assert(disabled_opt.disable_if(false).disabled(), 'Should be disabled option') assert(disabled_opt.disable_auto_if(true).disabled(), 'Should be disabled option') assert(disabled_opt.disable_auto_if(false).disabled(), 'Should be disabled option') +assert(disabled_opt.enable_auto_if(true).disabled(), 'Should be disabled option') +assert(disabled_opt.enable_auto_if(false).disabled(), 'Should be disabled option') dep = dependency('threads', required : required_opt) assert(dep.found(), 'Should find required "threads" dep') diff --git a/test cases/common/195 generator in subdir/com/mesonbuild/meson.build b/test cases/common/195 generator in subdir/com/mesonbuild/meson.build index 4808743..6469a2e 100644 --- a/test cases/common/195 generator in subdir/com/mesonbuild/meson.build +++ b/test cases/common/195 generator in subdir/com/mesonbuild/meson.build @@ -1,4 +1,4 @@ -gprog = find_program('genprog.py') +gprog = find_program('tooldir/genprog.py') gen = generator(gprog, \ output : ['@BASENAME@.c', '@BASENAME@.h'], diff --git a/test cases/common/195 generator in subdir/com/mesonbuild/genprog.py b/test cases/common/195 generator in subdir/com/mesonbuild/tooldir/genprog.py similarity index 100% rename from test cases/common/195 generator in subdir/com/mesonbuild/genprog.py rename to test cases/common/195 generator in subdir/com/mesonbuild/tooldir/genprog.py diff --git a/test cases/common/196 subproject with features/meson.build b/test cases/common/196 subproject with features/meson.build index 5bdfefb..130c69a 100644 --- a/test cases/common/196 subproject with features/meson.build +++ b/test cases/common/196 subproject with features/meson.build @@ -10,7 +10,7 @@ disabled_subproj = subproject('disabled_sub', required: get_option('disabled-sub assert(disabled_subproj.found() == false, 'Disabled subproject should be NOT found') disabled_dep = dependency('', fallback: ['disabled_sub', 'libSub'], required: false) -assert(disabled_dep.found() == false, 'Subprojetc was disabled, it should never be built.') +assert(disabled_dep.found() == false, 'Subproject was disabled, it should never be built.') nothing = executable('nothing', 'nothing.c', dependencies: [disabled_dep]) subproj_with_missing_dep = subproject('auto_sub_with_missing_dep', required: get_option('auto-sub-with-missing-dep')) diff --git a/test cases/common/197 function attributes/meson.build b/test cases/common/197 function attributes/meson.build index 4d43ecd..8ef6b74 100644 --- a/test cases/common/197 function attributes/meson.build +++ b/test cases/common/197 function attributes/meson.build @@ -23,7 +23,8 @@ if c.get_id() == 'pgi' error('MESON_SKIP_TEST: PGI supports its own set of features, will need a separate list for PGI to test it.') endif -expected_result = not ['msvc', 'clang-cl', 'intel-cl'].contains(c.get_id()) +expected = {} +expected_default = not ['msvc', 'clang-cl', 'intel-cl'].contains(c.get_id()) # Q: Why is ifunc not in this list or any of the below lists? # A: It's too damn hard to figure out if you actually support it, since it @@ -53,10 +54,27 @@ attributes = [ 'sentinel', 'unused', 'used', + 'vector_size', 'warn_unused_result', 'weak', + 'dllexport', + 'dllimport', ] +expected += { + 'dllexport': ['windows', 'cygwin'].contains(host_machine.system()), + 'dllimport': ['windows', 'cygwin'].contains(host_machine.system()), +} + +if c.get_id() == 'gcc' and ['windows', 'cygwin'].contains(host_machine.system()) + expected += { + 'visibility': false, + 'visibility:hidden': false, + 'visibility:internal': false, + 'visibility:protected': false, + } +endif + if c.get_id() != 'intel' # not supported by icc as of 19.0.0 attributes += 'weakref' @@ -66,6 +84,10 @@ endif if host_machine.system() != 'darwin' attributes += 'alias' attributes += 'visibility' + attributes += 'visibility:default' + attributes += 'visibility:hidden' + attributes += 'visibility:internal' + attributes += 'visibility:protected' attributes += 'alloc_size' endif @@ -93,24 +115,18 @@ endif if get_option('mode') == 'single' foreach a : attributes x = c.has_function_attribute(a) + expected_result = expected.get(a, expected_default) assert(x == expected_result, '@0@: @1@'.format(c.get_id(), a)) x = cpp.has_function_attribute(a) assert(x == expected_result, '@0@: @1@'.format(cpp.get_id(), a)) endforeach - - win_expect = ['windows', 'cygwin'].contains(host_machine.system()) - foreach a : ['dllexport', 'dllimport'] - assert(c.has_function_attribute(a) == win_expect, - '@0@: @1@'.format(c.get_id(), a)) - assert(cpp.has_function_attribute(a) == win_expect, - '@0@: @1@'.format(cpp.get_id(), a)) - endforeach else - if not ['msvc', 'clang-cl', 'intel-cl'].contains(c.get_id()) - multi_expected = attributes - else - multi_expected = [] - endif + multi_expected = [] + foreach a : attributes + if expected.get(a, expected_default) + multi_expected += a + endif + endforeach multi_check = c.get_supported_function_attributes(attributes) assert(multi_check == multi_expected, 'get_supported_function_arguments works (C)') diff --git a/test cases/common/20 global arg/prog.c b/test cases/common/20 global arg/prog.c index 2a71236..c70a669 100644 --- a/test cases/common/20 global arg/prog.c +++ b/test cases/common/20 global arg/prog.c @@ -11,7 +11,7 @@ #endif #if !defined(GLOBAL_HOST) && !defined(GLOBAL_BUILD) - #error "Neither global_host nor glogal_build is set." + #error "Neither global_host nor global_build is set." #endif #if defined(GLOBAL_HOST) && defined(GLOBAL_BUILD) diff --git a/test cases/common/208 link custom/meson.build b/test cases/common/208 link custom/meson.build index 4d4f655..41ce78c 100644 --- a/test cases/common/208 link custom/meson.build +++ b/test cases/common/208 link custom/meson.build @@ -83,4 +83,11 @@ exe1 = executable('exe1', 'custom_target.c', link_with: lib1) test('custom_target_1', exe1) exe1_2 = executable('exe1_2', 'custom_target.c', link_with: lib2) -test('custom_target_2', exe2) \ No newline at end of file +test('custom_target_2', exe2) + +# Link with custom target containing a SONAME +dummy3 = shared_library('dummy3', 'dummy.c', version: '1.0') +t = custom_target(input: dummy, output: 'libcustom@PLAINNAME@', command: [custom_prog, '@INPUT@', '@OUTPUT@']) +lib3 = static_library('lib3', 'outerlib.c', link_with: t) +exe3 = executable('exe3', 'custom_target.c', link_with: lib3) +test('custom_target_3', exe3) diff --git a/test cases/common/216 custom target input extracted objects/meson.build b/test cases/common/216 custom target input extracted objects/meson.build index 654b76d..136e154 100644 --- a/test cases/common/216 custom target input extracted objects/meson.build +++ b/test cases/common/216 custom target input extracted objects/meson.build @@ -1,7 +1,7 @@ project('custom target input extracted objects', 'c') if meson.backend() == 'xcode' - error('MESON_SKIP_TEST: sometimes Xcode puts object files in weird paths and we can not extract them.') + error('MESON_SKIP_TEST: sometimes Xcode puts object files in weird paths and we cannot extract them.') endif diff --git a/test cases/common/220 fs module/meson.build b/test cases/common/220 fs module/meson.build index d38c872..8001963 100644 --- a/test cases/common/220 fs module/meson.build +++ b/test cases/common/220 fs module/meson.build @@ -1,4 +1,4 @@ -project('fs module test') +project('fs module test', 'c') is_windows = build_machine.system() == 'windows' @@ -39,7 +39,9 @@ assert(fs.expanduser('~/foo').endswith('foo'), 'expanduser with tail failed') # -- as_posix assert(fs.as_posix('/') == '/', 'as_posix idempotent') assert(fs.as_posix('\\') == '/', 'as_posix simple') -assert(fs.as_posix('\\\\') == '/', 'as_posix simple') +# Python 3.12 changed how these paths are handled, so deal with both. +drivepath = fs.as_posix('\\\\') +assert(drivepath == '/' or drivepath == '//', 'as_posix simple') assert(fs.as_posix('foo\\bar/baz') == 'foo/bar/baz', 'as_posix mixed slash') # -- is_absolute @@ -120,7 +122,7 @@ assert(not fs.is_samepath(f1, 'subdir/subdirfile.txt'), 'is_samepath known bad c assert(not fs.is_samepath('not-a-path', f2), 'is_samepath should not error if path(s) do not exist') f = files('meson.build', 'subdir/../meson.build') -assert(fs.is_samepath(f[0], f[1]), 'is_samepath not detercting same files') +assert(fs.is_samepath(f[0], f[1]), 'is_samepath not detecting same files') if not is_windows and build_machine.system() != 'cygwin' assert(fs.is_samepath(symlink, 'meson.build'), 'symlink is_samepath fail') @@ -139,6 +141,33 @@ assert(fs.name('foo/bar/baz.dll.a') == 'baz.dll.a', 'failed to get basename with assert(fs.stem('foo/bar/baz.dll') == 'baz', 'failed to get stem with suffix') assert(fs.stem('foo/bar/baz.dll.a') == 'baz.dll', 'failed to get stem with compound suffix') +# relative_to +if build_machine.system() == 'windows' + # strings + assert(fs.relative_to('c:\\prefix\\lib\\foo', 'c:\\prefix') == 'lib\\foo') + assert(fs.relative_to('c:\\prefix\\lib', 'c:\\prefix\\bin') == '..\\lib') + assert(fs.relative_to('c:\\proj1\\foo', 'd:\\proj1\\bar') == 'c:\\proj1\\foo') + assert(fs.relative_to('prefix\\lib\\foo', 'prefix') == 'lib\\foo') + assert(fs.relative_to('prefix\\lib', 'prefix\\bin') == '..\\lib') + assert(fs.relative_to('proj1\\foo', 'proj1\\bar') == '..\\foo') + assert(fs.relative_to('subdir/subdirfile.txt', meson.current_source_dir()) == 'subdir\\subdirfile.txt') + assert(fs.relative_to(files('meson.build'), files('subdir/meson.build')) == '..\\..\\meson.build') + assert(fs.relative_to(files('meson.build'), 'subdir/meson.build') == '..\\..\\meson.build') +else + # strings + assert(fs.relative_to('/prefix/lib/foo', '/prefix') == 'lib/foo') + assert(fs.relative_to('/prefix/lib', '/prefix/bin') == '../lib') + assert(fs.relative_to('prefix/lib/foo', 'prefix') == 'lib/foo') + assert(fs.relative_to('prefix/lib', 'prefix/bin') == '../lib') + assert(fs.relative_to('subdir/subdirfile.txt', meson.current_source_dir()) == 'subdir/subdirfile.txt') + assert(fs.relative_to(files('meson.build'), files('subdir/meson.build')) == '../../meson.build') + assert(fs.relative_to(files('meson.build'), 'subdir/meson.build') == '../../meson.build') +endif + subdir('subdir') subproject('subbie') + +testcase expect_error('File notfound does not exist.') + fs.read('notfound') +endtestcase diff --git a/test cases/common/220 fs module/subdir/btgt.c b/test cases/common/220 fs module/subdir/btgt.c new file mode 100644 index 0000000..8479e67 --- /dev/null +++ b/test cases/common/220 fs module/subdir/btgt.c @@ -0,0 +1,5 @@ +int +main(void) +{ + return 0; +} diff --git a/test cases/common/220 fs module/subdir/meson.build b/test cases/common/220 fs module/subdir/meson.build index 0cd2475..6e2c8be 100644 --- a/test cases/common/220 fs module/subdir/meson.build +++ b/test cases/common/220 fs module/subdir/meson.build @@ -4,3 +4,33 @@ assert(fs.is_samepath(meson.project_source_root(), '..'), 'is_samepath not detec assert(fs.is_samepath(meson.project_build_root(), meson.current_build_dir() / '..'), 'is_samepath not detecting same directory') assert(fs.is_samepath(subdirfiles[0], 'subdirfile.txt'), 'is_samepath not detecting same directory when using File and str') + +# More relative_to to test subdir/builddir components + +build_to_src = fs.relative_to(meson.current_source_dir(), meson.current_build_dir()) +src_to_build = fs.relative_to(meson.current_build_dir(), meson.current_source_dir()) + +btgt = executable('btgt', 'btgt.c') +ctgt = fs.copyfile('subdirfile.txt') + +if build_machine.system() == 'windows' + # Test that CustomTarget works + assert(fs.relative_to('subdirfile.txt', ctgt) == '..\\@0@\\subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(ctgt, 'subdirfile.txt') == '..\\@0@\\subdirfile.txt'.format(src_to_build)) + # Test that CustomTargetIndex works + assert(fs.relative_to('subdirfile.txt', ctgt[0]) == '..\\@0@\\subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(ctgt[0], 'subdirfile.txt') == '..\\@0@\\subdirfile.txt'.format(src_to_build)) + # Test that BuildTarget works + assert(fs.relative_to('subdirfile.txt', btgt) == '..\\@0@\\subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(btgt, 'subdirfile.txt') == '..\\@0@\\@1@'.format(src_to_build, fs.name(btgt.full_path()))) +else + # Test that CustomTarget works + assert(fs.relative_to('subdirfile.txt', ctgt) == '../@0@/subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(ctgt, 'subdirfile.txt') == '../@0@/subdirfile.txt'.format(src_to_build)) + # Test that CustomTargetIndex works + assert(fs.relative_to('subdirfile.txt', ctgt[0]) == '../@0@/subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(ctgt[0], 'subdirfile.txt') == '../@0@/subdirfile.txt'.format(src_to_build)) + # Test that BuildTarget works + assert(fs.relative_to('subdirfile.txt', btgt) == '../@0@/subdirfile.txt'.format(build_to_src)) + assert(fs.relative_to(btgt, 'subdirfile.txt') == '../@0@/@1@'.format(src_to_build, fs.name(btgt.full_path()))) +endif diff --git a/test cases/common/222 native prop/meson.build b/test cases/common/222 native prop/meson.build index 8752371..88e32ec 100644 --- a/test cases/common/222 native prop/meson.build +++ b/test cases/common/222 native prop/meson.build @@ -8,16 +8,16 @@ x = meson.get_external_property('astring', native: true) assert(x=='mystring', 'did not get native property with native:true and non-cross build.') x = meson.get_external_property('astring', 'fallback', native: false) -assert(x==ref, 'did not get get native property with native:false and non-cross build.') +assert(x==ref, 'did not get native property with native:false and non-cross build.') -x = meson.get_external_property('notexist', 'fallback') +x = meson.get_external_property('nonexistent', 'fallback') assert(x=='fallback', 'fallback did not work') -x = meson.get_external_property('notexist', 'fallback', native: true) +x = meson.get_external_property('nonexistent', 'fallback', native: true) assert(x=='fallback', 'fallback native:true did not work') -x = meson.get_external_property('notexist', 'fallback', native: false) +x = meson.get_external_property('nonexistent', 'fallback', native: false) assert(x=='fallback', 'fallback native:false did not work') diff --git a/test cases/common/223 persubproject options/subprojects/sub2/meson.build b/test cases/common/223 persubproject options/subprojects/sub2/meson.build index cf1435a..8862220 100644 --- a/test cases/common/223 persubproject options/subprojects/sub2/meson.build +++ b/test cases/common/223 persubproject options/subprojects/sub2/meson.build @@ -14,3 +14,7 @@ shared_library('lib1', 'foo.c') # Parent project is c++11 but this one uses c++14 to build. libcpp14 = library('lib2', 'foo.cpp') meson.override_dependency('libcpp14', declare_dependency(link_with: libcpp14)) + +# Compiler checks should be using c++14 as well +cxx = meson.get_compiler('cpp') +assert(cxx.compiles(files('foo.cpp'))) diff --git a/test cases/common/227 very long commmand line/codegen.py b/test cases/common/227 very long command line/codegen.py similarity index 100% rename from test cases/common/227 very long commmand line/codegen.py rename to test cases/common/227 very long command line/codegen.py diff --git a/test cases/failing/128 subproject object as a dependency/main.c b/test cases/common/227 very long command line/main.c similarity index 100% rename from test cases/failing/128 subproject object as a dependency/main.c rename to test cases/common/227 very long command line/main.c diff --git a/test cases/common/227 very long commmand line/meson.build b/test cases/common/227 very long command line/meson.build similarity index 99% rename from test cases/common/227 very long commmand line/meson.build rename to test cases/common/227 very long command line/meson.build index f8df311..e4661b0 100644 --- a/test cases/common/227 very long commmand line/meson.build +++ b/test cases/common/227 very long command line/meson.build @@ -8,7 +8,7 @@ if build_machine.system() == 'windows' limit = 32767 # NOTE: filename limit is 260 characters unless # 1. Python >= 3.6 is being used - # 2. Windows 10 registry has been edited to enable long pathnaems + # 2. Windows 10 registry has been edited to enable long pathnames # ninja backend uses absolute filenames, so we ensure they don't exceed 260. elif build_machine.system() == 'cygwin' # cygwin-to-win32: see above diff --git a/test cases/common/227 very long commmand line/name_gen.py b/test cases/common/227 very long command line/name_gen.py similarity index 100% rename from test cases/common/227 very long commmand line/name_gen.py rename to test cases/common/227 very long command line/name_gen.py diff --git a/test cases/failing/99 no lang/main.c b/test cases/common/235 invalid standard overridden to valid/main.c similarity index 100% rename from test cases/failing/99 no lang/main.c rename to test cases/common/235 invalid standard overridden to valid/main.c diff --git a/test cases/common/235 invalid standard overriden to valid/meson.build b/test cases/common/235 invalid standard overridden to valid/meson.build similarity index 100% rename from test cases/common/235 invalid standard overriden to valid/meson.build rename to test cases/common/235 invalid standard overridden to valid/meson.build diff --git a/test cases/common/235 invalid standard overriden to valid/test.json b/test cases/common/235 invalid standard overridden to valid/test.json similarity index 100% rename from test cases/common/235 invalid standard overriden to valid/test.json rename to test cases/common/235 invalid standard overridden to valid/test.json diff --git a/test cases/common/239 includedir violation/subprojects/sub/include/placeholder.h b/test cases/common/239 includedir violation/subprojects/sub/include/placeholder.h index 196f917..22653bf 100644 --- a/test cases/common/239 includedir violation/subprojects/sub/include/placeholder.h +++ b/test cases/common/239 includedir violation/subprojects/sub/include/placeholder.h @@ -1,3 +1,3 @@ #pragma once -// Git can not handle empty directories, so there must be something here. +// Git cannot handle empty directories, so there must be something here. diff --git a/test cases/common/245 custom target index source/main.c b/test cases/common/245 custom target index source/main.c index 48af141..248ab20 100644 --- a/test cases/common/245 custom target index source/main.c +++ b/test cases/common/245 custom target index source/main.c @@ -1,8 +1,10 @@ #include #include "gen.h" -int main(int argc) +int main(int argc, char **argv) { + (void)argv; + assert(argc == 3); return genfunc(); } diff --git a/test cases/common/247 deprecated option/meson_options.txt b/test cases/common/247 deprecated option/meson_options.txt index 9665501..88cd8aa 100644 --- a/test cases/common/247 deprecated option/meson_options.txt +++ b/test cases/common/247 deprecated option/meson_options.txt @@ -16,7 +16,7 @@ option('o5', type: 'boolean', deprecated: {'enabled': 'true', 'disabled': 'false # A boolean option has been replaced by a feature with another name, old true/false values # are accepted by the new option for backward compatibility. -option('o6', type: 'boolean', value: 'true', deprecated: 'o7') +option('o6', type: 'boolean', value: true, deprecated: 'o7') option('o7', type: 'feature', value: 'enabled', deprecated: {'true': 'enabled', 'false': 'disabled'}) # A project option is replaced by a module option diff --git a/test cases/common/252 install data structured/meson.build b/test cases/common/252 install data structured/meson.build index 9d29794..4834747 100644 --- a/test cases/common/252 install data structured/meson.build +++ b/test cases/common/252 install data structured/meson.build @@ -1,4 +1,4 @@ -project('install structured data') +project('install structured data', default_options: ['python.bytecompile=-1']) install_data( 'dir1/file1', diff --git a/test cases/common/254 long output/meson.build b/test cases/common/254 long output/meson.build index 6d8d62b..145dcb4 100644 --- a/test cases/common/254 long output/meson.build +++ b/test cases/common/254 long output/meson.build @@ -1,5 +1,5 @@ project('long-stderr', 'c') dumper = executable('dumper', 'dumper.c') -test('dump-test', dumper) -test('dump-test-TAP', dumper, protocol : 'tap') +test('dump-test', dumper, timeout: 60) +test('dump-test-TAP', dumper, protocol : 'tap', timeout: 60) diff --git a/test cases/common/257 generated header dep/meson.build b/test cases/common/257 generated header dep/meson.build index 195d082..82db3b2 100644 --- a/test cases/common/257 generated header dep/meson.build +++ b/test cases/common/257 generated header dep/meson.build @@ -8,7 +8,7 @@ project('generated header dep', 'c') # dependency on the header file. This happened in GLib: # https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2917. -python = import('python').find_installation() +python = find_program('python3') header = custom_target( output: 'foo.h', capture: true, diff --git a/test cases/common/259 preprocess/bar.c b/test cases/common/259 preprocess/bar.c index 43737b9..0d961e6 100644 --- a/test cases/common/259 preprocess/bar.c +++ b/test cases/common/259 preprocess/bar.c @@ -1,3 +1,3 @@ -int bar(void) { - return BAR; +int @BAR@(void) { + return BAR + PLOP; } diff --git a/test cases/common/259 preprocess/math.c b/test cases/common/259 preprocess/math.c new file mode 100644 index 0000000..37d3cc6 --- /dev/null +++ b/test cases/common/259 preprocess/math.c @@ -0,0 +1,3 @@ +// Verify we preprocess as C language, otherwise including math.h would fail. +// See https://github.com/mesonbuild/meson/issues/11940. +#include diff --git a/test cases/common/259 preprocess/meson.build b/test cases/common/259 preprocess/meson.build index 4824598..10e8b09 100644 --- a/test cases/common/259 preprocess/meson.build +++ b/test cases/common/259 preprocess/meson.build @@ -4,7 +4,18 @@ cc = meson.get_compiler('c') add_project_arguments(['-DFOO=0', '-DBAR=0'], language: 'c') -pp_files = cc.preprocess('foo.c', 'bar.c', output: '@PLAINNAME@') +fs = import('fs') +bar_content = fs.read('bar.c') +bar_x = custom_target( + input: 'bar.c', + output: 'bar.x', + command: ['python3', '-c', '''import sys;print(sys.argv[1].replace('@BAR@', 'bar'))''', bar_content], + capture: true, +) + +dep = declare_dependency(compile_args: '-DPLOP=0') + +pp_files = cc.preprocess('foo.c', bar_x, 'math.c', output: '@PLAINNAME@.c', dependencies: dep) foreach f : pp_files message(f.full_path()) diff --git a/test cases/common/26 find program/meson.build b/test cases/common/26 find program/meson.build index 5d38d0b..3c4d15c 100644 --- a/test cases/common/26 find program/meson.build +++ b/test cases/common/26 find program/meson.build @@ -35,5 +35,5 @@ assert(prog.found(), 'Program version should match') prog = find_program('test_subdir.py', required : false) assert(not prog.found(), 'Program should not be found') -prog = find_program('test_subdir.py', dirs : ['/donotexist', meson.current_source_dir() / 'scripts']) +prog = find_program('test_subdir.py', dirs : ['/nonexistent', meson.current_source_dir() / 'scripts']) assert(prog.found(), 'Program should be found') diff --git a/test cases/common/260 declare_dependency objects/bar.c b/test cases/common/260 declare_dependency objects/bar.c new file mode 100644 index 0000000..f3ca85c --- /dev/null +++ b/test cases/common/260 declare_dependency objects/bar.c @@ -0,0 +1 @@ +void bar(void) {} diff --git a/test cases/common/260 declare_dependency objects/foo.c b/test cases/common/260 declare_dependency objects/foo.c new file mode 100644 index 0000000..9a39cb9 --- /dev/null +++ b/test cases/common/260 declare_dependency objects/foo.c @@ -0,0 +1,3 @@ +extern void bar(void); + +void foo(void) { bar(); } diff --git a/test cases/common/260 declare_dependency objects/meson.build b/test cases/common/260 declare_dependency objects/meson.build new file mode 100644 index 0000000..a9a0c7b --- /dev/null +++ b/test cases/common/260 declare_dependency objects/meson.build @@ -0,0 +1,23 @@ +# Test that declare_dependency(objects: ...) fixes issues with duplicated +# objects in the final link line, thanks to deduplication of dependencies. +# The commented declare_dependency() invocation using link_whole instead +# fails thusly: +# +# ar csrDT libbar.a libfoo.a.p/foo.c.o libbar.a.p/bar.c.o +# ar csrDT libfoo.a libfoo.a.p/foo.c.o +# cc -o prog prog.p/prog.c.o -Wl,--as-needed -Wl,--no-undefined -Wl,--whole-archive -Wl,--start-group libfoo.a libbar.a -Wl,--end-group -Wl,--no-whole-archive +# /usr/bin/ld: libfoo.a.p/foo.c.o: in function `foo': +# ../foo.c:3: multiple definition of `foo'; libfoo.a.p/foo.c.o:../foo.c:3: first defined here + +project('Transitive declare_dependency(objects)', 'c') + +libfoo = static_library('foo', 'foo.c') +#foo = declare_dependency(link_whole: libfoo) +foo = declare_dependency(objects: libfoo.extract_all_objects(recursive: true)) + +libbar = static_library('bar', 'bar.c', dependencies: foo) + +#bar = declare_dependency(link_whole: libbar, dependencies: foo) +bar = declare_dependency(objects: libbar.extract_all_objects(recursive: true), dependencies: foo) + +executable('prog', sources: files('prog.c'), dependencies: [foo, bar]) diff --git a/test cases/common/260 declare_dependency objects/prog.c b/test cases/common/260 declare_dependency objects/prog.c new file mode 100644 index 0000000..9311679 --- /dev/null +++ b/test cases/common/260 declare_dependency objects/prog.c @@ -0,0 +1,3 @@ +extern void foo(void); + +int main(void) { foo(); } diff --git a/test cases/common/261 testcase clause/meson.build b/test cases/common/261 testcase clause/meson.build new file mode 100644 index 0000000..834865f --- /dev/null +++ b/test cases/common/261 testcase clause/meson.build @@ -0,0 +1,37 @@ +project('testcase clause') + +# To make sure unreachable code is not executed. +unreachable = true + +# Verify assertion exception gets catched and dropped. +testcase expect_error('Assert failed: false') + assert(false) + unreachable = false +endtestcase +assert(unreachable) + +# The inner testcase raises an exception because it did not receive the expected +# error message. The outer testcase catches the inner testcase exception and +# drop it. +testcase expect_error('Expecting error \'something\' but got \'Assert failed: false\'') + testcase expect_error('something') + assert(false) + unreachable = false + endtestcase + unreachable = false +endtestcase +assert(unreachable) + +# The inner testcase raises an exception because it did not receive an +# exception. The outer testcase catches the inner testcase exception and +# drop it. +testcase expect_error('Expecting an error but code block succeeded') + testcase expect_error('something') + reached = true + endtestcase + unreachable = false +endtestcase +assert(reached) +assert(unreachable) + +message('all good') diff --git a/test cases/common/261 testcase clause/test.json b/test cases/common/261 testcase clause/test.json new file mode 100644 index 0000000..650ae9f --- /dev/null +++ b/test cases/common/261 testcase clause/test.json @@ -0,0 +1,9 @@ +{ + "stdout": [ + { + "line": ".*all good", + "match": "re", + "count": 1 + } + ] +} diff --git a/test cases/common/262 generator chain/data.txt b/test cases/common/262 generator chain/data.txt new file mode 100644 index 0000000..a972fee --- /dev/null +++ b/test cases/common/262 generator chain/data.txt @@ -0,0 +1 @@ +stage1 diff --git a/test cases/common/262 generator chain/meson.build b/test cases/common/262 generator chain/meson.build new file mode 100644 index 0000000..5c571d4 --- /dev/null +++ b/test cases/common/262 generator chain/meson.build @@ -0,0 +1,19 @@ +project('Generator Chain', 'c') + +stage1_exe = find_program('stage1.py') + +stage2_exe = find_program('stage2.py') + +stage1_gen = generator(stage1_exe, + output : '@PLAINNAME@.inter', + arguments : ['@INPUT@', '@OUTPUT@']) + +stage2_gen = generator(stage2_exe, + output : '@PLAINNAME@.c', + arguments : ['@INPUT@', '@OUTPUT@']) + +out = stage2_gen.process(stage1_gen.process('data.txt')) + +hello = executable('hello', out) + +test('basic', hello) diff --git a/test cases/common/262 generator chain/stage1.py b/test cases/common/262 generator chain/stage1.py new file mode 100644 index 0000000..73e02fc --- /dev/null +++ b/test cases/common/262 generator chain/stage1.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +assert(Path(sys.argv[1]).read_text() == 'stage1\n') +Path(sys.argv[2]).write_text('stage2\n') diff --git a/test cases/common/262 generator chain/stage2.py b/test cases/common/262 generator chain/stage2.py new file mode 100644 index 0000000..7f82592 --- /dev/null +++ b/test cases/common/262 generator chain/stage2.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +assert(Path(sys.argv[1]).read_text() == 'stage2\n') +Path(sys.argv[2]).write_text('int main(void){}\n') diff --git a/test cases/common/263 internal dependency includes in checks/include/test_262_header.h b/test cases/common/263 internal dependency includes in checks/include/test_262_header.h new file mode 100644 index 0000000..5a2ca62 --- /dev/null +++ b/test cases/common/263 internal dependency includes in checks/include/test_262_header.h @@ -0,0 +1 @@ +int foo(void); diff --git a/test cases/common/263 internal dependency includes in checks/meson.build b/test cases/common/263 internal dependency includes in checks/meson.build new file mode 100644 index 0000000..c8b5277 --- /dev/null +++ b/test cases/common/263 internal dependency includes in checks/meson.build @@ -0,0 +1,7 @@ +project('test 262', 'c') + +cc = meson.get_compiler('c') + +internal_dep = declare_dependency(include_directories: 'include') + +assert(cc.has_header_symbol('test_262_header.h', 'foo', dependencies: internal_dep)) diff --git a/test cases/common/264 required keyword in has functions/meson.build b/test cases/common/264 required keyword in has functions/meson.build new file mode 100644 index 0000000..afd07ea --- /dev/null +++ b/test cases/common/264 required keyword in has functions/meson.build @@ -0,0 +1,67 @@ +project('required keyword in has functions', 'c') + +cc = meson.get_compiler('c') +opt = get_option('opt') + +cc.has_function('printf', prefix : '#include', required : true) +cc.has_type('time_t', prefix : '#include', required : true) +cc.has_member('struct tm', 'tm_sec', prefix : '#include', required : true) +cc.has_members('struct tm', ['tm_sec', 'tm_min'], prefix : '#include', required : true) +cc.has_header('time.h', required : true) +cc.has_header_symbol('time.h', 'time', required : true) + +assert(not cc.has_function('printf', prefix : '#include', required : opt)) +assert(not cc.has_type('time_t', prefix : '#include', required : opt)) +assert(not cc.has_member('struct tm', 'tm_sec', prefix : '#include', required : opt)) +assert(not cc.has_members('struct tm', ['tm_sec', 'tm_min'], prefix : '#include', required : opt)) +assert(not cc.has_header('time.h', required : opt)) +assert(not cc.has_header_symbol('time.h', 'time', required : opt)) + +# compiler.has_argument +if cc.get_id() == 'msvc' + is_arg = '/O2' +else + is_arg = '-O2' +endif +cc.has_argument(is_arg, required: true) +assert(not cc.has_argument(is_arg, required: opt)) + +# compiler.has_multi_arguments +if cc.get_id() == 'gcc' + pre_arg = '-Wformat' + arg = '-Werror=format-security' + cc.has_multi_arguments([pre_arg, arg], required: true) + assert(not cc.has_multi_arguments(pre_arg, arg, required: opt)) +endif + +# compiler.has_link_argument +if cc.get_argument_syntax() == 'msvc' + is_arg = '/OPT:REF' +else + is_arg = '-Wl,-L/tmp' +endif +cc.has_link_argument(is_arg, required: true) +assert(not cc.has_link_argument(is_arg, required: opt)) + +# compiler.has_function_attribute +if not ['pgi', 'msvc', 'clang-cl', 'intel-cl'].contains(cc.get_id()) + a = 'aligned' + cc.has_function_attribute(a, required: true) + assert(not cc.has_function_attribute(a, required: opt)) +endif + +testcase expect_error('''compiler.has_function keyword argument 'required' was of type str but should have been one of: bool, UserFeatureOption''') + cc.has_function('printf', required : 'not a bool') +endtestcase + +testcase expect_error('''C function 'asdfkawlegsdiovapfjhkr' not usable''') + cc.has_function('asdfkawlegsdiovapfjhkr', required : true) +endtestcase + +testcase expect_error('''C header 'asdfkawlegsdiovapfjhkr.h' not found''') + cc.has_header('asdfkawlegsdiovapfjhkr.h', required : true) +endtestcase + +testcase expect_error('''C symbol time_not_found not found in header time.h''') + cc.has_header_symbol('time.h', 'time_not_found', required : true) +endtestcase diff --git a/test cases/common/264 required keyword in has functions/meson_options.txt b/test cases/common/264 required keyword in has functions/meson_options.txt new file mode 100644 index 0000000..53175af --- /dev/null +++ b/test cases/common/264 required keyword in has functions/meson_options.txt @@ -0,0 +1 @@ +option('opt', type: 'feature', value: 'disabled') diff --git a/test cases/common/265 default_options dict/lib.c b/test cases/common/265 default_options dict/lib.c new file mode 100644 index 0000000..3ef78ee --- /dev/null +++ b/test cases/common/265 default_options dict/lib.c @@ -0,0 +1 @@ +#warning Make sure this is not fatal diff --git a/test cases/common/265 default_options dict/meson.build b/test cases/common/265 default_options dict/meson.build new file mode 100644 index 0000000..06edd7b --- /dev/null +++ b/test cases/common/265 default_options dict/meson.build @@ -0,0 +1,22 @@ +project('test default options', 'c', + default_options: { + 'bool': true, + 'int': 42, + 'str': 'foo', + 'array': ['foo'], + 'werror': true, + }, +) + +assert(get_option('bool') == true) +assert(get_option('int') == 42) +assert(get_option('str') == 'foo') +assert(get_option('array') == ['foo']) +assert(get_option('werror') == true) + +cc = meson.get_compiler('c') + +# MSVC does not support #warning +if cc.get_id() != 'msvc' + static_library('foo', 'lib.c', override_options: {'werror': false}) +endif diff --git a/test cases/common/265 default_options dict/meson_options.txt b/test cases/common/265 default_options dict/meson_options.txt new file mode 100644 index 0000000..1b0b0e1 --- /dev/null +++ b/test cases/common/265 default_options dict/meson_options.txt @@ -0,0 +1,4 @@ +option('bool', type: 'boolean', value: false) +option('int', type: 'integer', value: 0) +option('str', type: 'string', value: 'bar') +option('array', type: 'array', value: ['bar']) diff --git a/test cases/common/266 format string/meson.build b/test cases/common/266 format string/meson.build new file mode 100644 index 0000000..200cfac --- /dev/null +++ b/test cases/common/266 format string/meson.build @@ -0,0 +1,20 @@ +project('test format string') + +# Test all supported types in message(), format(), f-string. +foreach t : [get_option('opt'), 42, true, false, 'str', ['list'], {'dict': 'value'}] + message(t, '@0@'.format(t), f'@t@', [t], {'key': t}) +endforeach + +# Deprecated but should work with str.format(). +env = environment() +message('@0@'.format(env)) + +# Should fail with f-string and message() +error_msg = 'Value other than strings, integers, bools, options, dictionaries and lists thereof.' +testcase expect_error('message(): ' + error_msg) + message(env) +endtestcase + +testcase expect_error('f-string: ' + error_msg) + message(f'@env@') +endtestcase diff --git a/test cases/common/266 format string/meson_options.txt b/test cases/common/266 format string/meson_options.txt new file mode 100644 index 0000000..2e39176 --- /dev/null +++ b/test cases/common/266 format string/meson_options.txt @@ -0,0 +1 @@ +option('opt', type: 'feature') diff --git a/test cases/common/266 format string/test.json b/test cases/common/266 format string/test.json new file mode 100644 index 0000000..2369864 --- /dev/null +++ b/test cases/common/266 format string/test.json @@ -0,0 +1,28 @@ +{ + "stdout": [ + { + "line": "Message: auto auto auto [auto] {'key' : auto}" + }, + { + "line": "Message: 42 42 42 [42] {'key' : 42}" + }, + { + "line": "Message: true true true [true] {'key' : true}" + }, + { + "line": "Message: false false false [false] {'key' : false}" + }, + { + "line": "Message: str str str ['str'] {'key' : 'str'}" + }, + { + "line": "Message: ['list'] ['list'] ['list'] [['list']] {'key' : ['list']}" + }, + { + "line": "Message: {'dict' : 'value'} {'dict' : 'value'} {'dict' : 'value'} [{'dict' : 'value'}] {'key' : {'dict' : 'value'}}" + }, + { + "line": "Message: " + } + ] + } diff --git a/test cases/common/267 default_options in find_program/meson.build b/test cases/common/267 default_options in find_program/meson.build new file mode 100644 index 0000000..99d8425 --- /dev/null +++ b/test cases/common/267 default_options in find_program/meson.build @@ -0,0 +1,5 @@ +project('test default_options in find_program') + +dummy_exe = find_program('dummy', default_options: ['subproject_option=true']) + +test('test_dummy', dummy_exe) diff --git a/test cases/common/267 default_options in find_program/subprojects/dummy.wrap b/test cases/common/267 default_options in find_program/subprojects/dummy.wrap new file mode 100644 index 0000000..0c03eec --- /dev/null +++ b/test cases/common/267 default_options in find_program/subprojects/dummy.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = dummy + +[provide] +program_names = dummy \ No newline at end of file diff --git a/test cases/common/267 default_options in find_program/subprojects/dummy/dummy.c b/test cases/common/267 default_options in find_program/subprojects/dummy/dummy.c new file mode 100644 index 0000000..a51103c --- /dev/null +++ b/test cases/common/267 default_options in find_program/subprojects/dummy/dummy.c @@ -0,0 +1,3 @@ +int main(void) { + return 0; +} \ No newline at end of file diff --git a/test cases/common/267 default_options in find_program/subprojects/dummy/meson.build b/test cases/common/267 default_options in find_program/subprojects/dummy/meson.build new file mode 100644 index 0000000..b644130 --- /dev/null +++ b/test cases/common/267 default_options in find_program/subprojects/dummy/meson.build @@ -0,0 +1,6 @@ +project('dummy', 'c') + +if get_option('subproject_option') + dummy_exe = executable('dummy', 'dummy.c') + meson.override_find_program('dummy', dummy_exe) +endif diff --git a/test cases/common/267 default_options in find_program/subprojects/dummy/meson_options.txt b/test cases/common/267 default_options in find_program/subprojects/dummy/meson_options.txt new file mode 100644 index 0000000..c91a24a --- /dev/null +++ b/test cases/common/267 default_options in find_program/subprojects/dummy/meson_options.txt @@ -0,0 +1 @@ +option('subproject_option', type: 'boolean', value: false) diff --git a/test cases/common/268 install functions and follow symlinks/foo/file1 b/test cases/common/268 install functions and follow symlinks/foo/file1 new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test cases/common/268 install functions and follow symlinks/foo/file1 @@ -0,0 +1 @@ +test diff --git a/test cases/common/268 install functions and follow symlinks/meson.build b/test cases/common/268 install functions and follow symlinks/meson.build new file mode 100644 index 0000000..327c021 --- /dev/null +++ b/test cases/common/268 install functions and follow symlinks/meson.build @@ -0,0 +1,38 @@ +project('install_data following symlinks') + +install_data( + 'foo/link1', + install_dir: get_option('datadir') / 'followed', + follow_symlinks: true, +) + +install_headers( + 'foo/link2.h', + follow_symlinks: true, + subdir: 'followed' +) + +install_data( + 'foo/link1', + install_dir: get_option('datadir'), + follow_symlinks: false, +) + +install_headers( + 'foo/link2.h', + follow_symlinks: false, +) + +install_subdir( + 'foo', + install_dir: get_option('datadir') / 'subdir', + strip_directory: true, + follow_symlinks: false, +) + +install_subdir( + 'foo', + install_dir: get_option('datadir') / 'subdir_followed', + strip_directory: true, + follow_symlinks: true, +) diff --git a/test cases/common/268 install functions and follow symlinks/test.json b/test cases/common/268 install functions and follow symlinks/test.json new file mode 100644 index 0000000..6a39517 --- /dev/null +++ b/test cases/common/268 install functions and follow symlinks/test.json @@ -0,0 +1,14 @@ +{ + "installed": [ + {"type": "link", "file": "usr/share/link1"}, + {"type": "link", "file": "usr/include/link2.h"}, + {"type": "file", "file": "usr/share/followed/link1"}, + {"type": "file", "file": "usr/include/followed/link2.h"}, + {"type": "link", "file": "usr/share/subdir/link1"}, + {"type": "link", "file": "usr/share/subdir/link2.h"}, + {"type": "file", "file": "usr/share/subdir/file1"}, + {"type": "file", "file": "usr/share/subdir_followed/link1"}, + {"type": "file", "file": "usr/share/subdir_followed/link2.h"}, + {"type": "file", "file": "usr/share/subdir_followed/file1"} + ] +} diff --git a/test cases/common/269 configure file output format/compare.py b/test cases/common/269 configure file output format/compare.py new file mode 100644 index 0000000..5188b02 --- /dev/null +++ b/test cases/common/269 configure file output format/compare.py @@ -0,0 +1,5 @@ +import sys + +with open(sys.argv[1], 'r', encoding='utf-8') as f, open(sys.argv[2], 'r', encoding='utf-8') as g: + if f.read() != g.read(): + sys.exit('contents are not equal') diff --git a/test cases/common/269 configure file output format/expected/config.h b/test cases/common/269 configure file output format/expected/config.h new file mode 100644 index 0000000..33cfd89 --- /dev/null +++ b/test cases/common/269 configure file output format/expected/config.h @@ -0,0 +1,21 @@ +/* + * Autogenerated by the Meson build system. + * Do not edit, your changes will be lost. + */ + +#pragma once + +#define bool + +#undef false + +/* ultimate question of life, the universe, and everything */ +#define int 42 + +/* This is +a multiline +description */ +#define str "hello world!" + +#define unquoted float + diff --git a/test cases/common/269 configure file output format/expected/config.json b/test cases/common/269 configure file output format/expected/config.json new file mode 100644 index 0000000..47d7832 --- /dev/null +++ b/test cases/common/269 configure file output format/expected/config.json @@ -0,0 +1 @@ +{"bool": true, "false": false, "int": 42, "str": "\"hello world!\"", "unquoted": "float"} \ No newline at end of file diff --git a/test cases/common/269 configure file output format/expected/config.mg b/test cases/common/269 configure file output format/expected/config.mg new file mode 100644 index 0000000..ccdb4e7 --- /dev/null +++ b/test cases/common/269 configure file output format/expected/config.mg @@ -0,0 +1,23 @@ +/* + * Autogenerated by the Meson build system. + * Do not edit, your changes will be lost. + */ + +#ifndef CONFIG_MAGNESIUM_H +#define CONFIG_MAGNESIUM_H + +#define bool + +#undef false + +/* ultimate question of life, the universe, and everything */ +#define int 42 + +/* This is +a multiline +description */ +#define str "hello world!" + +#define unquoted float + +#endif diff --git a/test cases/common/269 configure file output format/expected/config.nasm b/test cases/common/269 configure file output format/expected/config.nasm new file mode 100644 index 0000000..63c5c22 --- /dev/null +++ b/test cases/common/269 configure file output format/expected/config.nasm @@ -0,0 +1,17 @@ +; Autogenerated by the Meson build system. +; Do not edit, your changes will be lost. + +%define bool + +%undef false + +; ultimate question of life, the universe, and everything +%define int 42 + +; This is +; a multiline +; description +%define str "hello world!" + +%define unquoted float + diff --git a/test cases/common/269 configure file output format/meson.build b/test cases/common/269 configure file output format/meson.build new file mode 100644 index 0000000..17650e3 --- /dev/null +++ b/test cases/common/269 configure file output format/meson.build @@ -0,0 +1,47 @@ +project('configure file output format') + +data = configuration_data() +data.set_quoted('str', 'hello world!', description: '''This is +a multiline +description''') +data.set('unquoted', 'float') +data.set('int', 42, description: 'ultimate question of life, the universe, and everything') +data.set('bool', true) +data.set('false', false) + +config_h = configure_file( + configuration: data, + output_format: 'c', + output: 'config.h' +) + +config_nasm = configure_file( + configuration: data, + output_format: 'nasm', + output: 'config.nasm' +) + +config_json = configure_file( + configuration: data, + output_format: 'json', + output: 'config.json' +) + +config_mg = configure_file( + configuration: data, + macro_name: 'CONFIG_MAGNESIUM_H', + output_format: 'c', + output: 'config_mg.h' +) + +py = find_program('python3') +compare_py = files('compare.py') +expected_config_h = files('expected/config.h') +expected_config_nasm = files('expected/config.nasm') +expected_config_json = files('expected/config.json') +expected_config_mg = files('expected/config.mg') + +test('c_output', py, args: [compare_py, expected_config_h, config_h]) +test('nasm_output', py, args: [compare_py, expected_config_nasm, config_nasm]) +test('json_output', py, args: [compare_py, expected_config_json, config_json]) +test('c_mg_output', py, args: [compare_py, expected_config_mg, config_mg]) diff --git a/test cases/common/27 multiline string/meson.build b/test cases/common/27 multiline string/meson.build index a87d29a..39d2438 100644 --- a/test cases/common/27 multiline string/meson.build +++ b/test cases/common/27 multiline string/meson.build @@ -34,4 +34,4 @@ int main(void) { return 0; }''' -assert(cc.compiles(prog), 'multline test compile failed') +assert(cc.compiles(prog), 'multiline test compile failed') diff --git a/test cases/common/270 int_to_str_fill/meson.build b/test cases/common/270 int_to_str_fill/meson.build new file mode 100644 index 0000000..a549d94 --- /dev/null +++ b/test cases/common/270 int_to_str_fill/meson.build @@ -0,0 +1,9 @@ +project('test_fill_in_int_to_string') + +n = 4 +assert(n.to_string() == '4') +assert(n.to_string(fill: 3) == '004') +assert(n.to_string(fill: -3) == '4') + +n = -4 +assert(n.to_string(fill: 3) == '-04') diff --git a/test cases/common/271 env in generator.process/generate_main.py b/test cases/common/271 env in generator.process/generate_main.py new file mode 100644 index 0000000..993c5ac --- /dev/null +++ b/test cases/common/271 env in generator.process/generate_main.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import os +import sys + +ENV_VAR_VALUE = os.environ.get('ENV_VAR_VALUE') +assert ENV_VAR_VALUE is not None + +with open(sys.argv[1], 'r') as infile, \ + open(sys.argv[2], 'w') as outfile: + + outfile.write(infile.read().replace('ENV_VAR_VALUE', ENV_VAR_VALUE)) diff --git a/test cases/common/271 env in generator.process/main.template b/test cases/common/271 env in generator.process/main.template new file mode 100644 index 0000000..3c3340e --- /dev/null +++ b/test cases/common/271 env in generator.process/main.template @@ -0,0 +1,3 @@ +int main(void) { + return ENV_VAR_VALUE; +} \ No newline at end of file diff --git a/test cases/common/271 env in generator.process/meson.build b/test cases/common/271 env in generator.process/meson.build new file mode 100644 index 0000000..5d74040 --- /dev/null +++ b/test cases/common/271 env in generator.process/meson.build @@ -0,0 +1,21 @@ +project('test_env_in_generator_process', 'c') + +generate_main_py = find_program('generate_main.py') + +main_generator = generator(generate_main_py, + arguments: ['@INPUT@', '@OUTPUT@'], + output: '@BASENAME@' + '.c' +) + +main_template = files('main.template') + +# With explicit values +my_executable = executable('myexecutable', main_generator.process(main_template, env: {'ENV_VAR_VALUE': '0'})) +test('explicit_value', my_executable) + +# With env object +env = environment() +env.set('ENV_VAR_VALUE', '0') + +my_executable2 = executable('myexecutable2', main_generator.process(main_template, env: env)) +test('env_object', my_executable2) diff --git a/test cases/common/272 unity/meson.build b/test cases/common/272 unity/meson.build new file mode 100644 index 0000000..9acb9b8 --- /dev/null +++ b/test cases/common/272 unity/meson.build @@ -0,0 +1,13 @@ +project('unity', 'c', + default_options : [ + 'unity_size=2']) + +if get_option('unity') != 'on' + error('MESON_SKIP_TEST: unity builds not enabled') +endif + +slib_notinstalled = static_library('slib_notinstalled', + # test depends on the number of files being divisible by unity_size + ['slib1.c', 'slib2.c']) + +slib_installed = static_library('slib_installed', ['slib1.c', 'slib2.c'], link_with : slib_notinstalled, install : true) diff --git a/test cases/common/272 unity/slib.c b/test cases/common/272 unity/slib.c new file mode 100644 index 0000000..d705336 --- /dev/null +++ b/test cases/common/272 unity/slib.c @@ -0,0 +1,6 @@ +int func1(void); +int func2(void); + +int static_lib_func(void) { + return func1() + func2(); +} diff --git a/test cases/common/272 unity/slib1.c b/test cases/common/272 unity/slib1.c new file mode 100644 index 0000000..35304ee --- /dev/null +++ b/test cases/common/272 unity/slib1.c @@ -0,0 +1,3 @@ +int func1(void) { + return 1; +} diff --git a/test cases/common/272 unity/slib2.c b/test cases/common/272 unity/slib2.c new file mode 100644 index 0000000..5badf23 --- /dev/null +++ b/test cases/common/272 unity/slib2.c @@ -0,0 +1,3 @@ +int func2(void) { + return 2; +} diff --git a/test cases/common/272 unity/test.json b/test cases/common/272 unity/test.json new file mode 100644 index 0000000..0a10fdd --- /dev/null +++ b/test cases/common/272 unity/test.json @@ -0,0 +1,5 @@ +{ + "installed": [ + {"type": "file", "file": "usr/lib/libslib_installed.a"} + ] +} diff --git a/test cases/unit/98 install all targets/subdir/foo3-devel.h b/test cases/common/28 try compile/foo.h.in similarity index 100% rename from test cases/unit/98 install all targets/subdir/foo3-devel.h rename to test cases/common/28 try compile/foo.h.in diff --git a/test cases/common/28 try compile/meson.build b/test cases/common/28 try compile/meson.build index cb41e1d..9f457c5 100644 --- a/test cases/common/28 try compile/meson.build +++ b/test cases/common/28 try compile/meson.build @@ -8,20 +8,63 @@ breakcode = '''#include void func(void) { printf("This won't work.\n"); } ''' -foreach compiler : [meson.get_compiler('c'), meson.get_compiler('cpp')] - if compiler.compiles(code, name : 'should succeed') == false +warncode = '''#warning This is a warning +int main(void) { return 0; } +''' + +configure_file( + input: 'foo.h.in', + output: 'foo.h', + configuration: {}, +) + +header_code = '#include "foo.h"' + +foreach lang : ['c', 'cpp'] + compiler = meson.get_compiler(lang) + + assert(not compiler.compiles(header_code, name: 'Should not include . by default')) + assert(compiler.compiles(header_code, name: 'Should include builddir', + include_directories: include_directories('.'), + )) + + if compiler.compiles(code, name : 'code should succeed') == false error('Compiler ' + compiler.get_id() + ' is fail.') endif - if compiler.compiles(files('valid.c'), name : 'should succeed') == false + if compiler.compiles(files('valid.c'), name : 'file should succeed') == false error('Compiler ' + compiler.get_id() + ' is fail.') endif - if compiler.compiles(breakcode, name : 'should fail') + copied = configure_file(input: 'valid.c', output: lang + '-valid-copy.c', copy: true) + if compiler.compiles(copied, name : 'built file should succeed') == false + error('Compiler ' + compiler.get_id() + ' is fail.') + endif + + if compiler.compiles(breakcode, name : 'code should fail') error('Compiler ' + compiler.get_id() + ' returned true on broken code.') endif - if compiler.compiles(files('invalid.c'), name : 'should fail') + if compiler.compiles(files('invalid.c'), name : 'file should fail') error('Compiler ' + compiler.get_id() + ' returned true on broken code.') endif + + # MSVC does not support #warning instruction + if compiler.get_id() != 'msvc' + # First check that all tests pass without werror, then check they fail with it. + foreach with_werror : [false, true] + expect_success = not with_werror + assert(compiler.compiles(warncode, + name: f'code with warning compiles with werror=@with_werror@', + werror: with_werror) == expect_success) + assert(compiler.links(warncode, + name: f'code with warning links with werror=@with_werror@', + werror: with_werror) == expect_success) + if meson.can_run_host_binaries() + assert((compiler.run(warncode, + name: f'code with warning runs with werror=@with_werror@', + werror: with_werror).returncode() == 0) == expect_success) + endif + endforeach + endif endforeach diff --git a/test cases/common/3 static/lib3.c b/test cases/common/3 static/lib3.c new file mode 100644 index 0000000..f834cf8 --- /dev/null +++ b/test cases/common/3 static/lib3.c @@ -0,0 +1,11 @@ +int func3(const int x) { + return x + 1; +} + +#ifndef WORK +# error "did not get static only C args" +#endif + +#ifdef BREAK +# error "got shared only C args, but shouldn't have" +#endif diff --git a/test cases/common/3 static/meson.build b/test cases/common/3 static/meson.build index 04ff2f6..1127ecb 100644 --- a/test cases/common/3 static/meson.build +++ b/test cases/common/3 static/meson.build @@ -1,4 +1,4 @@ -project('static library test', 'c') +project('static library test', 'c', default_options : ['default_library=static']) lib = static_library('mylib', get_option('source'), link_args : '-THISMUSTNOBEUSED') # Static linker needs to ignore all link args. @@ -12,3 +12,8 @@ endif assert(has_not_changed, 'Static library has changed.') assert(not is_disabler(lib), 'Static library is a disabler.') + +if get_option('default_library') == 'static' + library('lib2', 'lib3.c', c_static_args : ['-DWORK'], c_shared_args : ['-DBREAK']) +endif +build_target('lib4', 'lib3.c', c_static_args : ['-DWORK'], target_type : 'static_library') diff --git a/test cases/common/32 has header/meson.build b/test cases/common/32 has header/meson.build index 8096763..e6f6efb 100644 --- a/test cases/common/32 has header/meson.build +++ b/test cases/common/32 has header/meson.build @@ -49,6 +49,6 @@ foreach fallback : fallbacks # This header exists in the source and the builddir, but we still must not # find it since we are looking in the system directories. assert(not comp.has_header(non_existent_header, prefix : fallback), - 'Found non-existent header.') + 'Found nonexistent header.') endforeach endforeach diff --git a/test cases/common/33 run program/meson.build b/test cases/common/33 run program/meson.build index aa0a1d6..2257d93 100644 --- a/test cases/common/33 run program/meson.build +++ b/test cases/common/33 run program/meson.build @@ -1,4 +1,4 @@ -project('run command', version : run_command('get-version.py', check : true).stdout().strip()) +project('run command', version : run_command('get-version.py', check : true).stdout().strip(), meson_version: '>=0.1.0') if build_machine.system() == 'windows' c = run_command('cmd', '/c', 'echo', 'hello', check: false) diff --git a/test cases/common/33 run program/test.json b/test cases/common/33 run program/test.json new file mode 100644 index 0000000..da8f128 --- /dev/null +++ b/test cases/common/33 run program/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "line": "test cases/common/33 run program/meson.build:1: WARNING: Project targets '>=0.1.0' but uses feature introduced in '0.47.0': check arg in run_command.", + "comment": "This triggers on line 1 -- the line with the project() function" + } + ] +} diff --git a/test cases/common/35 string operations/meson.build b/test cases/common/35 string operations/meson.build index 116fe0b..27cc0d8 100644 --- a/test cases/common/35 string operations/meson.build +++ b/test cases/common/35 string operations/meson.build @@ -125,3 +125,15 @@ assert(mysubstring.substring(10, -25) == '', 'substring is broken') assert(mysubstring.substring(-4, 2) == '', 'substring is broken') assert(mysubstring.substring(10, 9) == '', 'substring is broken') assert(mysubstring.substring(8, 10) == 'z', 'substring is broken') + +# str.splitlines() +assert('foo\nbar\nbaz'.splitlines() == ['foo', 'bar', 'baz'], 'splitlines is broken') +assert(''.splitlines() == [], 'splitlines with empty string is broken') +assert('foo\rbar\nbaz\n'.splitlines() == ['foo', 'bar', 'baz'], 'splitlines trailing newline is broken') +assert('hello\r\nworld'.splitlines() == ['hello', 'world']) +assert( + ' leading ws\nand trailing\t'.splitlines() == [' leading ws', 'and trailing\t'], + 'splitlines leading/trailing whitespace is broken', +) +assert('\n\r\n\r'.splitlines() == ['', '', ''], 'splitlines is broken') +assert('foo'.splitlines() == ['foo'], 'splitlines is broken') diff --git a/test cases/common/36 has function/meson.build b/test cases/common/36 has function/meson.build index a3f0a3c..bb3e869 100644 --- a/test cases/common/36 has function/meson.build +++ b/test cases/common/36 has function/meson.build @@ -37,11 +37,11 @@ foreach cc : compilers if cc.has_function('hfkerhisadf', prefix : '#include', args : unit_test_args) - error('Found non-existent function "hfkerhisadf".') + error('Found nonexistent function "hfkerhisadf".') endif if cc.has_function('hfkerhisadf', args : unit_test_args) - error('Found non-existent function "hfkerhisadf".') + error('Found nonexistent function "hfkerhisadf".') endif # With glibc (before 2.32, see below) on Linux, lchmod is a stub that will diff --git a/test cases/common/4 shared/libfile2.c b/test cases/common/4 shared/libfile2.c new file mode 100644 index 0000000..fee1d1e --- /dev/null +++ b/test cases/common/4 shared/libfile2.c @@ -0,0 +1,22 @@ +#if defined _WIN32 || defined __CYGWIN__ + #define DLL_PUBLIC __declspec(dllexport) +#else + #if defined __GNUC__ + #define DLL_PUBLIC __attribute__ ((visibility("default"))) + #else + #pragma message ("Compiler does not support symbol visibility.") + #define DLL_PUBLIC + #endif +#endif + +#ifndef WORK +# error "Did not get shared only arguments" +#endif + +#ifdef BREAK +# error "got static only C args, but shouldn't have" +#endif + +int DLL_PUBLIC libfunc(void) { + return 3; +} diff --git a/test cases/common/4 shared/meson.build b/test cases/common/4 shared/meson.build index 1c88bc5..7f79ad6 100644 --- a/test cases/common/4 shared/meson.build +++ b/test cases/common/4 shared/meson.build @@ -1,4 +1,4 @@ -project('shared library test', 'c') +project('shared library test', 'c', default_options : ['default_library=shared']) lib = shared_library('mylib', 'libfile.c') build_target('mylib2', 'libfile.c', target_type: 'shared_library') @@ -11,3 +11,8 @@ endif assert(has_not_changed, 'Shared library has changed.') assert(not is_disabler(lib), 'Shared library is a disabler.') + +if get_option('default_library') == 'shared' + library('mylib5', 'libfile2.c', c_shared_args : ['-DWORK']) +endif +build_target('mylib4', 'libfile2.c', target_type: 'shared_library', c_shared_args : ['-DWORK'], c_static_args : ['-DBREAK']) diff --git a/test cases/common/40 options/meson.build b/test cases/common/40 options/meson.build index 2eccef7..de4a7d5 100644 --- a/test cases/common/40 options/meson.build +++ b/test cases/common/40 options/meson.build @@ -1,4 +1,4 @@ -project('options', 'c') +project('options', 'c', meson_version : '>= 1.0.0') if get_option('testoption') != 'optval' error('Incorrect value to test option') @@ -43,3 +43,14 @@ if get_option('CASESENSITIVE') != 'ALL CAPS' endif assert(get_option('wrap_mode') == 'default', 'Wrap mode option is broken.') +assert(get_option('boolean_string') == false) +assert(get_option('boolean_string2') == true) +assert(get_option('integer_string') == 42) + +testcase expect_error('Invalid option name \'..invalid\'') + get_option('..invalid') +endtestcase + +testcase expect_error('Invalid option name \'this.is.also.invalid\'') + get_option('this.is.also.invalid') +endtestcase diff --git a/test cases/common/40 options/meson_options.txt b/test cases/common/40 options/meson_options.txt index 8067eae..ad48141 100644 --- a/test cases/common/40 options/meson_options.txt +++ b/test cases/common/40 options/meson_options.txt @@ -5,5 +5,9 @@ option('array_opt', type : 'array', choices : ['one', 'two', 'three'], value : [ option('free_array_opt', type : 'array') option('integer_opt', type : 'integer', min : 0, max : -(-5), value : 3) option('neg' + '_' + 'int' + '_' + 'opt', type : 'integer', min : -5, max : 5, value : -3) -option('CaseSenSiTivE', type : 'string', value: 'Some CAPS', description : 'An option with mixed capitaliziation') +option('CaseSenSiTivE', type : 'string', value: 'Some CAPS', description : 'An option with mixed capitalization') option('CASESENSITIVE', type : 'string', value: 'ALL CAPS', description : 'An option with all caps') + +option('boolean_string', type : 'boolean', value : 'false') +option('boolean_string2', type : 'boolean', value : 'true') +option('integer_string', type : 'integer', value : '42') diff --git a/test cases/common/40 options/test.json b/test cases/common/40 options/test.json new file mode 100644 index 0000000..c7c7f00 --- /dev/null +++ b/test cases/common/40 options/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": " * 1.1.0: {'\"boolean option\" keyword argument \"value\" of type str', '\"integer option\" keyword argument \"value\" of type str'}" + } + ] +} diff --git a/test cases/common/42 subproject/meson.build b/test cases/common/42 subproject/meson.build index 7f322bc..c2c0122 100644 --- a/test cases/common/42 subproject/meson.build +++ b/test cases/common/42 subproject/meson.build @@ -1,6 +1,8 @@ project('subproj user', 'c', version : '2.3.4', - license : 'mylicense') + license : 'mylicense', + license_files: 'mylicense.txt', +) assert(meson.project_name() == 'subproj user', 'Incorrect project name') @@ -24,5 +26,5 @@ meson.install_dependency_manifest('share/sublib/sublib.depmf') unknown_var = sub.get_variable('does-not-exist', []) if unknown_var != [] - error ('unexpetced fallback value for subproject.get_variable()') + error ('unexpected fallback value for subproject.get_variable()') endif diff --git a/test cases/unit/98 install all targets/subdir/foo2.in b/test cases/common/42 subproject/mylicense.txt similarity index 100% rename from test cases/unit/98 install all targets/subdir/foo2.in rename to test cases/common/42 subproject/mylicense.txt diff --git a/test cases/common/42 subproject/subprojects/sublib/meson.build b/test cases/common/42 subproject/subprojects/sublib/meson.build index 3a620fe..0c40241 100644 --- a/test cases/common/42 subproject/subprojects/sublib/meson.build +++ b/test cases/common/42 subproject/subprojects/sublib/meson.build @@ -1,6 +1,8 @@ project('subproject', 'c', version : '1.0.0', - license : ['sublicense1', 'sublicense2']) + license : ['sublicense1', 'sublicense2'], + license_files: ['sublicense1.txt', 'sublicense2.txt'], +) if not meson.is_subproject() error('Claimed to be master project even though we are a subproject.') diff --git a/test cases/unit/98 install all targets/subdir/bar2-devel.h b/test cases/common/42 subproject/subprojects/sublib/sublicense1.txt similarity index 100% rename from test cases/unit/98 install all targets/subdir/bar2-devel.h rename to test cases/common/42 subproject/subprojects/sublib/sublicense1.txt diff --git a/test cases/unit/98 install all targets/foo1-devel.h b/test cases/common/42 subproject/subprojects/sublib/sublicense2.txt similarity index 100% rename from test cases/unit/98 install all targets/foo1-devel.h rename to test cases/common/42 subproject/subprojects/sublib/sublicense2.txt diff --git a/test cases/common/42 subproject/test.json b/test cases/common/42 subproject/test.json index a56106f..949cb79 100644 --- a/test cases/common/42 subproject/test.json +++ b/test cases/common/42 subproject/test.json @@ -2,6 +2,9 @@ "installed": [ {"type": "exe", "file": "usr/bin/user"}, {"type": "pdb", "file": "usr/bin/user"}, - {"type": "file", "file": "usr/share/sublib/sublib.depmf"} + {"type": "file", "file": "usr/share/sublib/sublib.depmf"}, + {"type": "file", "file": "usr/share/sublib/mylicense.txt"}, + {"type": "file", "file": "usr/share/sublib/subprojects/sublib/sublicense1.txt"}, + {"type": "file", "file": "usr/share/sublib/subprojects/sublib/sublicense2.txt"} ] } diff --git a/test cases/common/44 pkgconfig-gen/dependencies/meson.build b/test cases/common/44 pkgconfig-gen/dependencies/meson.build index 6e27ae8..8243f6f 100644 --- a/test cases/common/44 pkgconfig-gen/dependencies/meson.build +++ b/test cases/common/44 pkgconfig-gen/dependencies/meson.build @@ -1,5 +1,9 @@ project('pkgconfig-gen-dependencies', 'c', version: '1.0') +if find_program('pkg-config').version() == '2.0.1' + error('MESON_SKIP_TEST: cannot test uninstalled.pc due to https://github.com/pkgconf/pkgconf/issues/310#issuecomment-1677844842') +endif + pkgg = import('pkgconfig') # libmain internally use libinternal and expose libexpose in its API @@ -60,3 +64,11 @@ pkgg.generate(main_lib2, libraries : internal_lib, filebase : 'pub-lib-order', ) + +# This will be built against both simple7.pc and simple7-uninstalled.pc, check +# that include directories works in both cases. +simple7 = dependency('simple7') +exe = executable('test2', 'test2.c', + dependencies: simple7, +) +test('Test2', exe) diff --git a/test cases/common/44 pkgconfig-gen/dependencies/test2.c b/test cases/common/44 pkgconfig-gen/dependencies/test2.c new file mode 100644 index 0000000..83979d5 --- /dev/null +++ b/test cases/common/44 pkgconfig-gen/dependencies/test2.c @@ -0,0 +1,8 @@ +#include +#include + +int main(void) { + if (INC1 + INC2 != 3) + return 1; + return 0; +} diff --git a/test cases/common/44 pkgconfig-gen/inc1/inc1.h b/test cases/common/44 pkgconfig-gen/inc1/inc1.h new file mode 100644 index 0000000..04ff366 --- /dev/null +++ b/test cases/common/44 pkgconfig-gen/inc1/inc1.h @@ -0,0 +1 @@ +#define INC1 1 diff --git a/test cases/common/44 pkgconfig-gen/inc2/inc2.h b/test cases/common/44 pkgconfig-gen/inc2/inc2.h new file mode 100644 index 0000000..b4b5ae1 --- /dev/null +++ b/test cases/common/44 pkgconfig-gen/inc2/inc2.h @@ -0,0 +1 @@ +#define INC2 2 diff --git a/test cases/common/44 pkgconfig-gen/meson.build b/test cases/common/44 pkgconfig-gen/meson.build index 12a110e..fd6371e 100644 --- a/test cases/common/44 pkgconfig-gen/meson.build +++ b/test cases/common/44 pkgconfig-gen/meson.build @@ -8,7 +8,7 @@ if not cc.find_library('z', required: false).found() endif # First check we have pkg-config >= 0.29 -pkgconfig = find_program('pkg-config', required: false) +pkgconfig = find_program('pkg-config', native: true, required: false) if not pkgconfig.found() error('MESON_SKIP_TEST: pkg-config not found') endif @@ -18,7 +18,7 @@ if v.version_compare('<0.29') error('MESON_SKIP_TEST: pkg-config version \'' + v + '\' too old') endif -python = import('python').find_installation() +python = find_program('python3') fs = import('fs') pkgg = import('pkgconfig') @@ -176,3 +176,20 @@ pkgg.generate( description : 'Check that variables can be single string', variables: 'foo=bar', ) + +# without a mainlib, name/description are mandatory +testcase expect_error('pkgconfig.generate: if a library is not passed as a positional argument, the \'name\' keyword argument is required.') + pkgg.generate(description: 'empty data') +endtestcase + +testcase expect_error('pkgconfig.generate: if a library is not passed as a positional argument, the \'description\' keyword argument is required.') + pkgg.generate(name: 'foobar') +endtestcase + +# Make sure the -uninstalled.pc file contains both include directories. +# See dependencies/test2.c that gets built against both simple7.pc and +# simple7-uninstalled.pc. +simple7 = library('simple7', include_directories: 'inc1') +dep = declare_dependency(include_directories: 'inc2') +install_headers('inc1/inc1.h', 'inc2/inc2.h') +pkgg.generate(simple7, libraries: dep) diff --git a/test cases/common/44 pkgconfig-gen/test.json b/test cases/common/44 pkgconfig-gen/test.json index 4d9f4c8..01786d4 100644 --- a/test cases/common/44 pkgconfig-gen/test.json +++ b/test cases/common/44 pkgconfig-gen/test.json @@ -1,6 +1,8 @@ { "installed": [ {"type": "file", "file": "usr/include/simple.h"}, + {"type": "file", "file": "usr/include/inc1.h"}, + {"type": "file", "file": "usr/include/inc2.h"}, {"type": "file", "file": "usr/lib/libstat2.a"}, {"type": "file", "file": "usr/lib/pkgconfig/simple.pc"}, {"type": "file", "file": "usr/lib/pkgconfig/libanswer.pc"}, @@ -13,13 +15,14 @@ {"type": "file", "file": "usr/lib/pkgconfig/simple3.pc"}, {"type": "file", "file": "usr/lib/pkgconfig/simple5.pc"}, {"type": "file", "file": "usr/lib/pkgconfig/simple6.pc"}, + {"type": "file", "file": "usr/lib/pkgconfig/simple7.pc"}, {"type": "file", "file": "usr/lib/pkgconfig/ct.pc"}, {"type": "file", "file": "usr/lib/pkgconfig/ct0.pc"}, {"type": "file", "file": "usr/share/pkgconfig/libhello_nolib.pc"} ], "stdout": [ { - "line": "test cases/common/44 pkgconfig-gen/meson.build:164: WARNING: Project targets '>=0.60.0' but uses feature introduced in '0.62.0': pkgconfig.generate implicit variable for builtin directories." + "line": "test cases/common/44 pkgconfig-gen/meson.build:160: WARNING: Project targets '>=0.60.0' but uses feature introduced in '0.62.0': pkgconfig.generate implicit variable for builtin directories." }, { "line": "test cases/common/44 pkgconfig-gen/meson.build:170: WARNING: Project targets '>=0.60.0' but uses feature introduced in '0.62.0': pkgconfig.generate implicit variable for builtin directories.", diff --git a/test cases/common/51 run target/helloprinter.c b/test cases/common/51 run target/helloprinter.c index 4a6e0ac..dbbe82f 100644 --- a/test cases/common/51 run target/helloprinter.c +++ b/test cases/common/51 run target/helloprinter.c @@ -2,7 +2,7 @@ int main(int argc, char **argv) { if(argc != 2) { - printf("I can not haz argument.\n"); + printf("I cannot haz argument.\n"); return 1; } else { printf("I can haz argument: %s\n", argv[1]); diff --git a/test cases/common/53 install script/meson.build b/test cases/common/53 install script/meson.build index 24d5dc8..03f8b2c 100644 --- a/test cases/common/53 install script/meson.build +++ b/test cases/common/53 install script/meson.build @@ -1,6 +1,6 @@ project('custom install script', 'c') -meson.add_install_script('myinstall.py', 'diiba/daaba', 'file.dat') +meson.add_install_script('myinstall.py', 'diiba/daaba', 'file.dat', dry_run: true) meson.add_install_script('myinstall.py', 'this/should', 'also-work.dat') subdir('src') diff --git a/test cases/common/53 install script/myinstall.py b/test cases/common/53 install script/myinstall.py index a573342..5e5a929 100755 --- a/test cases/common/53 install script/myinstall.py +++ b/test cases/common/53 install script/myinstall.py @@ -5,6 +5,7 @@ import os import shutil prefix = os.environ['MESON_INSTALL_DESTDIR_PREFIX'] +dry_run = bool(os.environ.get('MESON_INSTALL_DRY_RUN')) def main() -> None: @@ -16,15 +17,24 @@ def main() -> None: dirname = os.path.join(prefix, args.dirname) if not os.path.exists(dirname): - os.makedirs(dirname) + if dry_run: + print(f"DRYRUN: Creating directory {dirname}") + else: + os.makedirs(dirname) if args.mode == 'create': for name in args.files: - with open(os.path.join(dirname, name), 'w') as f: - f.write('') + if dry_run: + print(f'DRYRUN: Writing file {name}') + else: + with open(os.path.join(dirname, name), 'w') as f: + f.write('') else: for name in args.files: - shutil.copy(name, dirname) + if dry_run: + print(f"DRYRUN: Copying file {name} to {dirname}") + else: + shutil.copy(name, dirname) if __name__ == "__main__": diff --git a/test cases/common/56 array methods/meson.build b/test cases/common/56 array methods/meson.build index d323db8..e9e4969 100644 --- a/test cases/common/56 array methods/meson.build +++ b/test cases/common/56 array methods/meson.build @@ -18,7 +18,7 @@ if not file_list.contains(file_a[0]) endif if file_list.contains(file_c[0]) - error('Contains with ObjectHolder lists found non existent object') + error('Contains with ObjectHolder lists found nonexistent object') endif if empty.contains('abc') diff --git a/test cases/common/59 install subdir/meson.build b/test cases/common/59 install subdir/meson.build index 13d41be..fb2034b 100644 --- a/test cases/common/59 install subdir/meson.build +++ b/test cases/common/59 install subdir/meson.build @@ -7,6 +7,12 @@ install_subdir('sub2', exclude_directories : ['excluded'], install_dir : 'share') +# More exclusions +install_subdir('sub3', + exclude_files : ['data/excluded.txt'], + exclude_directories : ['data/excluded'], + install_dir : 'share') + subdir('subdir') # A subdir with write perms only for the owner # and read-list perms for owner and group diff --git a/test cases/common/59 install subdir/sub3/data/data.txt b/test cases/common/59 install subdir/sub3/data/data.txt new file mode 100644 index 0000000..1269488 --- /dev/null +++ b/test cases/common/59 install subdir/sub3/data/data.txt @@ -0,0 +1 @@ +data diff --git a/test cases/common/59 install subdir/sub3/data/excluded.txt b/test cases/common/59 install subdir/sub3/data/excluded.txt new file mode 100644 index 0000000..bbde3dc --- /dev/null +++ b/test cases/common/59 install subdir/sub3/data/excluded.txt @@ -0,0 +1 @@ +excluded diff --git a/test cases/common/59 install subdir/sub3/data/excluded/excluded.txt b/test cases/common/59 install subdir/sub3/data/excluded/excluded.txt new file mode 100644 index 0000000..bbde3dc --- /dev/null +++ b/test cases/common/59 install subdir/sub3/data/excluded/excluded.txt @@ -0,0 +1 @@ +excluded diff --git a/test cases/common/59 install subdir/test.json b/test cases/common/59 install subdir/test.json index 0dd885c..aa8e27a 100644 --- a/test cases/common/59 install subdir/test.json +++ b/test cases/common/59 install subdir/test.json @@ -12,6 +12,7 @@ {"type": "file", "file": "usr/share/sub1/sub2/data2.dat"}, {"type": "file", "file": "usr/share/sub2/one.dat"}, {"type": "file", "file": "usr/share/sub2/dircheck/excluded-three.dat"}, + {"type": "file", "file": "usr/share/sub3/data/data.txt"}, {"type": "dir", "file": "usr/share/new_directory"} ] } diff --git a/test cases/common/94 threads/meson.build b/test cases/common/94 threads/meson.build index 1fbb15a..6a939ff 100644 --- a/test cases/common/94 threads/meson.build +++ b/test cases/common/94 threads/meson.build @@ -1,6 +1,11 @@ project('threads', 'cpp', 'c', default_options : ['cpp_std=c++11']) +cc = meson.get_compiler('c') +if cc.has_function_attribute('unused') + add_project_arguments('-DHAVE_UNUSED', language: 'c') +endif + threaddep = dependency('threads') test('cppthreadtest', diff --git a/test cases/common/94 threads/threadprog.c b/test cases/common/94 threads/threadprog.c index 7bfb7c4..19f35e9 100644 --- a/test cases/common/94 threads/threadprog.c +++ b/test cases/common/94 threads/threadprog.c @@ -3,7 +3,7 @@ #include #include -DWORD WINAPI thread_func(void) { +DWORD WINAPI thread_func(void *arg) { printf("Printing from a thread.\n"); return 0; } @@ -22,7 +22,13 @@ int main(void) { #include #include -void* main_func(void) { +#ifdef HAVE_UNUSED + #define UNUSED_ATTR __attribute__((unused)) +#else + #define UNUSED_ATTR +#endif + +void* main_func(void UNUSED_ATTR *arg) { printf("Printing from a thread.\n"); return NULL; } diff --git a/test cases/common/98 subproject subdir/meson.build b/test cases/common/98 subproject subdir/meson.build index 3053b3b..ef053d8 100644 --- a/test cases/common/98 subproject subdir/meson.build +++ b/test cases/common/98 subproject subdir/meson.build @@ -27,7 +27,7 @@ d = dependency('sub-notfound', fallback : 'sub_novar', required : false) assert(not d.found(), 'Dependency should be not-found') # Verify that implicit fallback works because subprojects/sub_implicit directory exists -d = dependency('sub_implicit', default_options: 'opt=overriden') +d = dependency('sub_implicit', default_options: 'opt=overridden') assert(d.found(), 'Should implicitly fallback') # Verify that implicit fallback works because sub_implicit.wrap has diff --git a/test cases/common/98 subproject subdir/subprojects/sub_implicit/meson.build b/test cases/common/98 subproject subdir/subprojects/sub_implicit/meson.build index 9f43604..8002e9b 100644 --- a/test cases/common/98 subproject subdir/subprojects/sub_implicit/meson.build +++ b/test cases/common/98 subproject subdir/subprojects/sub_implicit/meson.build @@ -10,4 +10,4 @@ sub_implicit_provide2_dep = dep # This one is not overridden but the wrap file tells the variable name to use. glib_dep = dep -assert(get_option('opt') == 'overriden') \ No newline at end of file +assert(get_option('opt') == 'overridden') diff --git a/test cases/common/99 postconf/postconf.py b/test cases/common/99 postconf/postconf.py index 950c706..8cf576c 100644 --- a/test cases/common/99 postconf/postconf.py +++ b/test cases/common/99 postconf/postconf.py @@ -10,7 +10,7 @@ template = '''#pragma once input_file = os.path.join(os.environ['MESON_SOURCE_ROOT'], 'raw.dat') output_file = os.path.join(os.environ['MESON_BUILD_ROOT'], 'generated.h') -with open(input_file) as f: +with open(input_file, encoding='utf-8') as f: data = f.readline().strip() -with open(output_file, 'w') as f: +with open(output_file, 'w', encoding='utf-8') as f: f.write(template.format(data)) diff --git a/test cases/cython/1 basic/meson.build b/test cases/cython/1 basic/meson.build index 8c24e23..81a24f5 100644 --- a/test cases/cython/1 basic/meson.build +++ b/test cases/cython/1 basic/meson.build @@ -1,11 +1,14 @@ project( 'basic cython project', ['cython', 'c'], - default_options : ['warning_level=3'] + default_options : ['warning_level=3', 'buildtype=release'], ) -py_mod = import('python') -py3 = py_mod.find_installation() +if meson.backend() != 'ninja' + error('MESON_SKIP_TEST: Ninja backend required') +endif + +py3 = import('python').find_installation() py3_dep = py3.dependency(required : false) if not py3_dep.found() error('MESON_SKIP_TEST: Python library not found.') diff --git a/test cases/cython/2 generated sources/includestuff.pyx b/test cases/cython/2 generated sources/includestuff.pyx new file mode 100644 index 0000000..83f881b --- /dev/null +++ b/test cases/cython/2 generated sources/includestuff.pyx @@ -0,0 +1 @@ +include "stuff.pxi" diff --git a/test cases/cython/2 generated sources/meson.build b/test cases/cython/2 generated sources/meson.build index cfe6260..d438d80 100644 --- a/test cases/cython/2 generated sources/meson.build +++ b/test cases/cython/2 generated sources/meson.build @@ -1,10 +1,15 @@ project( 'generated cython sources', - ['cython'], + ['cython', 'c'], + default_options : ['buildtype=release'], ) -py_mod = import('python') -py3 = py_mod.find_installation('python3') +if meson.backend() != 'ninja' + error('MESON_SKIP_TEST: Ninja backend required') +endif + +fs = import('fs') +py3 = import('python').find_installation('python3') py3_dep = py3.dependency(required : false) if not py3_dep.found() error('MESON_SKIP_TEST: Python library not found.') @@ -17,7 +22,7 @@ ct = custom_target( command : [py3, '@INPUT@', '@OUTPUT@'], ) -ct_ext = py3.extension_module('ct', ct, dependencies : py3_dep) +ct_ext = py3.extension_module('ct', ct) test( 'custom target', @@ -34,7 +39,7 @@ cti = custom_target( command : [py3, '@INPUT@', '@OUTPUT@'], ) -cti_ext = py3.extension_module('cti', cti[0], dependencies : py3_dep) +cti_ext = py3.extension_module('cti', cti[0]) cf = configure_file( input : 'configure.pyx.in', @@ -42,7 +47,7 @@ cf = configure_file( copy : true, ) -cf_ext = py3.extension_module('cf', cf, dependencies : py3_dep) +cf_ext = py3.extension_module('cf', cf) test( 'configure file', @@ -60,7 +65,6 @@ gen = generator( g_ext = py3.extension_module( 'g', gen.process('g.in'), - dependencies : py3_dep, ) test( @@ -70,6 +74,30 @@ test( env : ['PYTHONPATH=' + meson.current_build_dir()] ) +stuff_pxi = fs.copyfile( + 'stuff.pxi.in', + 'stuff.pxi' +) + +# Need to copy the cython source to the build directory +# since meson can only generate the .pxi there +includestuff_pyx = fs.copyfile( + 'includestuff.pyx' +) + +stuff_pxi_dep = declare_dependency(sources: stuff_pxi) + +includestuff_ext = py3.extension_module( + 'includestuff', + includestuff_pyx, + dependencies: stuff_pxi_dep +) + +simpleinclude_ext = py3.extension_module( + 'simpleinclude', + 'simpleinclude.pyx', +) + subdir('libdir') test( diff --git a/test cases/cython/2 generated sources/simpleinclude.pyx b/test cases/cython/2 generated sources/simpleinclude.pyx new file mode 100644 index 0000000..c110f75 --- /dev/null +++ b/test cases/cython/2 generated sources/simpleinclude.pyx @@ -0,0 +1 @@ +include "simplestuff.pxi" diff --git a/test cases/cython/2 generated sources/simplestuff.pxi b/test cases/cython/2 generated sources/simplestuff.pxi new file mode 100644 index 0000000..2645216 --- /dev/null +++ b/test cases/cython/2 generated sources/simplestuff.pxi @@ -0,0 +1,2 @@ +def func2(): + print("Hello world") diff --git a/test cases/cython/2 generated sources/stuff.pxi.in b/test cases/cython/2 generated sources/stuff.pxi.in new file mode 100644 index 0000000..6417cbc --- /dev/null +++ b/test cases/cython/2 generated sources/stuff.pxi.in @@ -0,0 +1,2 @@ +def func(): + print("Hello world") diff --git a/test cases/cython/3 cython_args/meson.build b/test cases/cython/3 cython_args/meson.build index e41d1b7..45cbbbb 100644 --- a/test cases/cython/3 cython_args/meson.build +++ b/test cases/cython/3 cython_args/meson.build @@ -1,7 +1,14 @@ -project('cython_args', ['cython', 'c']) +project('cython_args', ['cython', 'c'], + # Needed because Windows Python builds are release-only and tend to be + # unhappy with a debug build type. + default_options : ['buildtype=release'] +) + +if meson.backend() != 'ninja' + error('MESON_SKIP_TEST: Ninja backend required') +endif -pymod = import('python') -python = pymod.find_installation('python3') +python = import('python').find_installation('python3') python_dep = python.dependency() if not python_dep.found() error('MESON_SKIP_TEST: Python library not found.') @@ -12,9 +19,9 @@ mod = python.extension_module( files('cythonargs.pyx'), cython_args: [ '--compile-time-env', - 'VALUE=1' + 'VALUE=1', + '-3', ], - dependencies: [python_dep] ) test( diff --git a/test cases/d/11 dub/meson.build b/test cases/d/11 dub/meson.build index d852ca0..3bb3d56 100644 --- a/test cases/d/11 dub/meson.build +++ b/test cases/d/11 dub/meson.build @@ -1,5 +1,7 @@ project('dub-example', 'd') +error('MESON_SKIP_TEST: Dub support is broken at the moment (#11773)') + dub_exe = find_program('dub', required : false) if not dub_exe.found() error('MESON_SKIP_TEST: Dub not found') diff --git a/test cases/d/13 declare dep/meson.build b/test cases/d/13 declare dep/meson.build index eef9816..2293934 100644 --- a/test cases/d/13 declare dep/meson.build +++ b/test cases/d/13 declare dep/meson.build @@ -3,7 +3,7 @@ project('meson-d-sample', 'd', ) my_dep = declare_dependency( - d_module_versions: ['TestVersion', 1], + d_module_versions: ['TestVersion'], d_import_dirs: include_directories('views'), ) diff --git a/test cases/d/14 dub with deps/meson.build b/test cases/d/14 dub with deps/meson.build index 08f3080..c8e472b 100644 --- a/test cases/d/14 dub with deps/meson.build +++ b/test cases/d/14 dub with deps/meson.build @@ -1,5 +1,7 @@ project('dub-with-deps-example', ['d']) +error('MESON_SKIP_TEST: Dub support is broken at the moment (#11773)') + dub_exe = find_program('dub', required : false) if not dub_exe.found() error('MESON_SKIP_TEST: Dub not found') diff --git a/test cases/d/3 shared library/meson.build b/test cases/d/3 shared library/meson.build index fa41779..fc7a0c4 100644 --- a/test cases/d/3 shared library/meson.build +++ b/test cases/d/3 shared library/meson.build @@ -3,7 +3,7 @@ project('D Shared Library', 'd') dc = meson.get_compiler('d') if dc.get_id() == 'gcc' if dc.version().version_compare('< 8') - error('MESON_SKIP_TEST: GDC < 8.0 can not build shared libraries') + error('MESON_SKIP_TEST: GDC < 8.0 cannot build shared libraries') endif endif diff --git a/test cases/d/4 library versions/meson.build b/test cases/d/4 library versions/meson.build index c745b92..1baebab 100644 --- a/test cases/d/4 library versions/meson.build +++ b/test cases/d/4 library versions/meson.build @@ -3,7 +3,7 @@ project('D library versions', 'd') dc = meson.get_compiler('d') if dc.get_id() == 'gcc' if dc.version().version_compare('< 8') - error('MESON_SKIP_TEST: GDC < 8.0 can not build shared libraries') + error('MESON_SKIP_TEST: GDC < 8.0 cannot build shared libraries') endif endif diff --git a/test cases/d/7 multilib/meson.build b/test cases/d/7 multilib/meson.build index 1879c08..0afd266 100644 --- a/test cases/d/7 multilib/meson.build +++ b/test cases/d/7 multilib/meson.build @@ -3,7 +3,7 @@ project('D Multiple Versioned Shared Libraries', 'd') dc = meson.get_compiler('d') if dc.get_id() == 'gcc' if dc.version().version_compare('< 8') - error('MESON_SKIP_TEST: GDC < 8.0 can not build shared libraries') + error('MESON_SKIP_TEST: GDC < 8.0 cannot build shared libraries') endif endif diff --git a/test cases/d/9 features/meson.build b/test cases/d/9 features/meson.build index 06f0341..50059f1 100644 --- a/test cases/d/9 features/meson.build +++ b/test cases/d/9 features/meson.build @@ -1,5 +1,16 @@ project('D Features', 'd', default_options : ['debug=false']) +dc = meson.get_compiler('d') + +# GDC 13 hard errors if options are given number values. +# https://github.com/mesonbuild/meson/pull/11996 + +if dc.get_id() == 'gcc' and dc.version().version_compare('>=13') + number_options_supported = false +else + number_options_supported = true +endif + # ONLY FOR BACKWARDS COMPATIBILITY. # DO NOT DO THIS IN NEW CODE! # USE include_directories() INSTEAD OF BUILDING @@ -46,12 +57,13 @@ e_test = executable('dapp_test', test('dapp_test', e_test) # test version level -e_version_int = executable('dapp_version_int', - test_src, - d_import_dirs: [data_dir], - d_module_versions: ['With_VersionInteger', 3], -) -test('dapp_version_int_t', e_version_int, args: ['debug']) +if number_options_supported + e_version_int = executable('dapp_version_int', + test_src, + d_import_dirs: [data_dir], + d_module_versions: ['With_VersionInteger', 3], + ) + test('dapp_version_int_t', e_version_int, args: ['debug']) # test version level failure e_version_int_fail = executable('dapp_version_int_fail', @@ -60,6 +72,7 @@ e_version_int_fail = executable('dapp_version_int_fail', d_module_versions: ['With_VersionInteger', 2], ) test('dapp_version_int_t_fail', e_version_int_fail, args: ['debug'], should_fail: true) +endif # test debug conditions: disabled e_no_debug = executable('dapp_no_debug', @@ -69,23 +82,34 @@ e_no_debug = executable('dapp_no_debug', ) test('dapp_no_debug_t_fail', e_no_debug, args: ['debug'], should_fail: true) -# test debug conditions: enabled -e_debug = executable('dapp_debug', - test_src, - d_import_dirs: [data_dir], - d_module_versions: ['With_Debug'], - d_debug: 1, -) -test('dapp_debug_t', e_debug, args: ['debug']) +if number_options_supported + # test debug conditions: enabled + e_debug = executable('dapp_debug', + test_src, + d_import_dirs: [data_dir], + d_module_versions: ['With_Debug'], + d_debug: 1, + ) + test('dapp_debug_t', e_debug, args: ['debug']) -# test debug conditions: integer -e_debug_int = executable('dapp_debug_int', - test_src, - d_import_dirs: [data_dir], - d_module_versions: ['With_DebugInteger'], - d_debug: 3, -) -test('dapp_debug_int_t', e_debug_int, args: ['debug']) + # test debug conditions: integer + e_debug_int = executable('dapp_debug_int', + test_src, + d_import_dirs: [data_dir], + d_module_versions: ['With_DebugInteger'], + d_debug: 3, + ) + test('dapp_debug_int_t', e_debug_int, args: ['debug']) + + # test with all debug conditions at once, and with redundant values + e_debug_all = executable('dapp_debug_all', + test_src, + d_import_dirs: [data_dir], + d_module_versions: ['With_DebugAll'], + d_debug: ['4', 'DebugIdentifier', 2, 'DebugIdentifierUnused'], + ) + test('dapp_debug_all_t', e_debug_all, args: ['debug']) +endif # test debug conditions: identifier e_debug_ident = executable('dapp_debug_ident', @@ -95,12 +119,3 @@ e_debug_ident = executable('dapp_debug_ident', d_debug: 'DebugIdentifier', ) test('dapp_debug_ident_t', e_debug_ident, args: ['debug']) - -# test with all debug conditions at once, and with redundant values -e_debug_all = executable('dapp_debug_all', - test_src, - d_import_dirs: [data_dir], - d_module_versions: ['With_DebugAll'], - d_debug: ['4', 'DebugIdentifier', 2, 'DebugIdentifierUnused'], -) -test('dapp_debug_all_t', e_debug_all, args: ['debug']) diff --git a/test cases/failing/10 out of bounds/test.json b/test cases/failing/10 out of bounds/test.json index e27d990..98ea37b 100644 --- a/test cases/failing/10 out of bounds/test.json +++ b/test cases/failing/10 out of bounds/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/10 out of bounds/meson.build:4:0: ERROR: Index 0 out of bounds of array of size 0." + "line": "test cases/failing/10 out of bounds/meson.build:4:6: ERROR: Index 0 out of bounds of array of size 0." } ] } diff --git a/test cases/failing/102 bool in combo/meson.build b/test cases/failing/100 bool in combo/meson.build similarity index 100% rename from test cases/failing/102 bool in combo/meson.build rename to test cases/failing/100 bool in combo/meson.build diff --git a/test cases/failing/102 bool in combo/meson_options.txt b/test cases/failing/100 bool in combo/meson_options.txt similarity index 100% rename from test cases/failing/102 bool in combo/meson_options.txt rename to test cases/failing/100 bool in combo/meson_options.txt diff --git a/test cases/failing/102 bool in combo/nativefile.ini b/test cases/failing/100 bool in combo/nativefile.ini similarity index 100% rename from test cases/failing/102 bool in combo/nativefile.ini rename to test cases/failing/100 bool in combo/nativefile.ini diff --git a/test cases/failing/102 bool in combo/test.json b/test cases/failing/100 bool in combo/test.json similarity index 73% rename from test cases/failing/102 bool in combo/test.json rename to test cases/failing/100 bool in combo/test.json index c0041af..357d7a8 100644 --- a/test cases/failing/102 bool in combo/test.json +++ b/test cases/failing/100 bool in combo/test.json @@ -1,5 +1,5 @@ { "stdout": [ - { "line": "test cases/failing/102 bool in combo/meson.build:1:0: ERROR: Value \"True\" (of type \"boolean\") for combo option \"opt\" is not one of the choices. Possible choices are (as string): \"true\", \"false\"." } + { "line": "test cases/failing/100 bool in combo/meson.build:1:0: ERROR: Value \"True\" (of type \"boolean\") for combo option \"opt\" is not one of the choices. Possible choices are (as string): \"true\", \"false\"." } ] } diff --git a/test cases/failing/100 no glib-compile-resources/test.json b/test cases/failing/100 no glib-compile-resources/test.json deleted file mode 100644 index ad9ba29..0000000 --- a/test cases/failing/100 no glib-compile-resources/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/100 no glib-compile-resources/meson.build:8:0: ERROR: Program 'glib-compile-resources' not found or not executable" - } - ] -} diff --git a/test cases/failing/103 compiler no lang/meson.build b/test cases/failing/101 compiler no lang/meson.build similarity index 100% rename from test cases/failing/103 compiler no lang/meson.build rename to test cases/failing/101 compiler no lang/meson.build diff --git a/test cases/failing/103 compiler no lang/test.json b/test cases/failing/101 compiler no lang/test.json similarity index 66% rename from test cases/failing/103 compiler no lang/test.json rename to test cases/failing/101 compiler no lang/test.json index 123dcb9..364efa9 100644 --- a/test cases/failing/103 compiler no lang/test.json +++ b/test cases/failing/101 compiler no lang/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/103 compiler no lang/meson.build:2:6: ERROR: Tried to access compiler for language \"c\", not specified for host machine." + "line": "test cases/failing/101 compiler no lang/meson.build:2:6: ERROR: Tried to access compiler for language \"c\", not specified for host machine." } ] } diff --git a/test cases/failing/101 number in combo/test.json b/test cases/failing/101 number in combo/test.json deleted file mode 100644 index 4c30f98..0000000 --- a/test cases/failing/101 number in combo/test.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "stdout": [ - { "line": "test cases/failing/101 number in combo/meson.build:1:0: ERROR: Value \"1\" (of type \"number\") for combo option \"Optimization level\" is not one of the choices. Possible choices are (as string): \"plain\", \"0\", \"g\", \"1\", \"2\", \"3\", \"s\"." } - ] -} diff --git a/test cases/failing/104 no fallback/meson.build b/test cases/failing/102 no fallback/meson.build similarity index 100% rename from test cases/failing/104 no fallback/meson.build rename to test cases/failing/102 no fallback/meson.build diff --git a/test cases/failing/104 no fallback/subprojects/foob/meson.build b/test cases/failing/102 no fallback/subprojects/foob/meson.build similarity index 100% rename from test cases/failing/104 no fallback/subprojects/foob/meson.build rename to test cases/failing/102 no fallback/subprojects/foob/meson.build diff --git a/test cases/failing/102 no fallback/test.json b/test cases/failing/102 no fallback/test.json new file mode 100644 index 0000000..5fbffe3 --- /dev/null +++ b/test cases/failing/102 no fallback/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "match": "re", + "line": ".*/meson\\.build:2:11: ERROR: (Pkg-config binary for machine MachineChoice\\.HOST not found\\. Giving up\\.|Dependency \"foob\" not found, tried .*)" + } + ] +} diff --git a/test cases/failing/105 feature require/meson.build b/test cases/failing/103 feature require/meson.build similarity index 100% rename from test cases/failing/105 feature require/meson.build rename to test cases/failing/103 feature require/meson.build diff --git a/test cases/failing/106 feature require.bis/meson_options.txt b/test cases/failing/103 feature require/meson_options.txt similarity index 100% rename from test cases/failing/106 feature require.bis/meson_options.txt rename to test cases/failing/103 feature require/meson_options.txt diff --git a/test cases/failing/103 feature require/test.json b/test cases/failing/103 feature require/test.json new file mode 100644 index 0000000..2da5494 --- /dev/null +++ b/test cases/failing/103 feature require/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "match": "re", + "line": ".*/meson\\.build:2:31: ERROR: Feature reqfeature cannot be enabled: frobnicator not available" + } + ] +} diff --git a/test cases/failing/106 feature require.bis/meson.build b/test cases/failing/104 feature require.bis/meson.build similarity index 100% rename from test cases/failing/106 feature require.bis/meson.build rename to test cases/failing/104 feature require.bis/meson.build diff --git a/test cases/failing/105 feature require/meson_options.txt b/test cases/failing/104 feature require.bis/meson_options.txt similarity index 100% rename from test cases/failing/105 feature require/meson_options.txt rename to test cases/failing/104 feature require.bis/meson_options.txt diff --git a/test cases/failing/104 feature require.bis/test.json b/test cases/failing/104 feature require.bis/test.json new file mode 100644 index 0000000..ed488af --- /dev/null +++ b/test cases/failing/104 feature require.bis/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "match": "re", + "line": ".*/meson\\.build:2:31: ERROR: Feature reqfeature cannot be enabled" + } + ] +} diff --git a/test cases/failing/104 no fallback/test.json b/test cases/failing/104 no fallback/test.json deleted file mode 100644 index e034061..0000000 --- a/test cases/failing/104 no fallback/test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "stdout": [ - { - "match": "re", - "line": ".*/meson\\.build:2:0: ERROR: (Pkg-config binary for machine MachineChoice\\.HOST not found\\. Giving up\\.|Dependency \"foob\" not found, tried .*)" - } - ] -} diff --git a/test cases/failing/105 feature require/test.json b/test cases/failing/105 feature require/test.json deleted file mode 100644 index 7c4640d..0000000 --- a/test cases/failing/105 feature require/test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "stdout": [ - { - "match": "re", - "line": ".*/meson\\.build:2:0: ERROR: Feature reqfeature cannot be enabled: frobnicator not available" - } - ] -} diff --git a/test cases/failing/107 no build get_external_property/meson.build b/test cases/failing/105 no build get_external_property/meson.build similarity index 100% rename from test cases/failing/107 no build get_external_property/meson.build rename to test cases/failing/105 no build get_external_property/meson.build diff --git a/test cases/failing/105 no build get_external_property/test.json b/test cases/failing/105 no build get_external_property/test.json new file mode 100644 index 0000000..e8b282c --- /dev/null +++ b/test cases/failing/105 no build get_external_property/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/105 no build get_external_property/meson.build:3:14: ERROR: Unknown property for build machine: nonexisting" + } + ] +} diff --git a/test cases/failing/108 enter subdir twice/meson.build b/test cases/failing/106 enter subdir twice/meson.build similarity index 100% rename from test cases/failing/108 enter subdir twice/meson.build rename to test cases/failing/106 enter subdir twice/meson.build diff --git a/test cases/failing/108 enter subdir twice/sub/meson.build b/test cases/failing/106 enter subdir twice/sub/meson.build similarity index 100% rename from test cases/failing/108 enter subdir twice/sub/meson.build rename to test cases/failing/106 enter subdir twice/sub/meson.build diff --git a/test cases/failing/108 enter subdir twice/test.json b/test cases/failing/106 enter subdir twice/test.json similarity index 64% rename from test cases/failing/108 enter subdir twice/test.json rename to test cases/failing/106 enter subdir twice/test.json index 0a8e127..9a8b11d 100644 --- a/test cases/failing/108 enter subdir twice/test.json +++ b/test cases/failing/106 enter subdir twice/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/108 enter subdir twice/meson.build:3:0: ERROR: Tried to enter directory \"sub\", which has already been visited." + "line": "test cases/failing/106 enter subdir twice/meson.build:3:0: ERROR: Tried to enter directory \"sub\", which has already been visited." } ] } diff --git a/test cases/failing/106 feature require.bis/test.json b/test cases/failing/106 feature require.bis/test.json deleted file mode 100644 index 2583990..0000000 --- a/test cases/failing/106 feature require.bis/test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "stdout": [ - { - "match": "re", - "line": ".*/meson\\.build:2:0: ERROR: Feature reqfeature cannot be enabled" - } - ] -} diff --git a/test cases/failing/109 invalid fstring/109 invalid fstring/meson.build b/test cases/failing/107 invalid fstring/109 invalid fstring/meson.build similarity index 100% rename from test cases/failing/109 invalid fstring/109 invalid fstring/meson.build rename to test cases/failing/107 invalid fstring/109 invalid fstring/meson.build diff --git a/test cases/failing/109 invalid fstring/109 invalid fstring/test.json b/test cases/failing/107 invalid fstring/109 invalid fstring/test.json similarity index 100% rename from test cases/failing/109 invalid fstring/109 invalid fstring/test.json rename to test cases/failing/107 invalid fstring/109 invalid fstring/test.json diff --git a/test cases/failing/109 invalid fstring/meson.build b/test cases/failing/107 invalid fstring/meson.build similarity index 100% rename from test cases/failing/109 invalid fstring/meson.build rename to test cases/failing/107 invalid fstring/meson.build diff --git a/test cases/failing/107 invalid fstring/test.json b/test cases/failing/107 invalid fstring/test.json new file mode 100644 index 0000000..ccd82b5 --- /dev/null +++ b/test cases/failing/107 invalid fstring/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/107 invalid fstring/meson.build:3:4: ERROR: Identifier \"foo\" does not name a variable." + } + ] +} diff --git a/test cases/failing/107 no build get_external_property/test.json b/test cases/failing/107 no build get_external_property/test.json deleted file mode 100644 index b95427e..0000000 --- a/test cases/failing/107 no build get_external_property/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/107 no build get_external_property/meson.build:3:0: ERROR: Unknown property for build machine: nonexisting" - } - ] -} diff --git a/test cases/failing/110 compiler argument checking/meson.build b/test cases/failing/108 compiler argument checking/meson.build similarity index 100% rename from test cases/failing/110 compiler argument checking/meson.build rename to test cases/failing/108 compiler argument checking/meson.build diff --git a/test cases/failing/108 compiler argument checking/test.json b/test cases/failing/108 compiler argument checking/test.json new file mode 100644 index 0000000..e9e27af --- /dev/null +++ b/test cases/failing/108 compiler argument checking/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/108 compiler argument checking/meson.build:4:25: ERROR: Compiler for C does not support \"-meson-goober-arg-for-testing\"" + } + ] +} diff --git a/test cases/failing/111 empty fallback/meson.build b/test cases/failing/109 empty fallback/meson.build similarity index 100% rename from test cases/failing/111 empty fallback/meson.build rename to test cases/failing/109 empty fallback/meson.build diff --git a/test cases/failing/111 empty fallback/subprojects/foo/meson.build b/test cases/failing/109 empty fallback/subprojects/foo/meson.build similarity index 100% rename from test cases/failing/111 empty fallback/subprojects/foo/meson.build rename to test cases/failing/109 empty fallback/subprojects/foo/meson.build diff --git a/test cases/failing/109 empty fallback/test.json b/test cases/failing/109 empty fallback/test.json new file mode 100644 index 0000000..46a4ec5 --- /dev/null +++ b/test cases/failing/109 empty fallback/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "match": "re", + "line": "test cases/failing/109 empty fallback/meson.build:6:0: ERROR: Dependency \"foo\" not found.*" + } + ] +} diff --git a/test cases/failing/109 invalid fstring/test.json b/test cases/failing/109 invalid fstring/test.json deleted file mode 100644 index a095d62..0000000 --- a/test cases/failing/109 invalid fstring/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/109 invalid fstring/meson.build:3:0: ERROR: Identifier \"foo\" does not name a variable." - } - ] -} diff --git a/test cases/failing/11 object arithmetic/test.json b/test cases/failing/11 object arithmetic/test.json index 822e504..12386d9 100644 --- a/test cases/failing/11 object arithmetic/test.json +++ b/test cases/failing/11 object arithmetic/test.json @@ -2,7 +2,7 @@ "stdout": [ { "match": "re", - "line": "test cases/failing/11 object arithmetic/meson\\.build:3:0: ERROR: The `\\+` operator of str does not accept objects of type MesonMain .*" + "line": "test cases/failing/11 object arithmetic/meson\\.build:3:12: ERROR: The `\\+` operator of str does not accept objects of type MesonMain .*" } ] } diff --git a/test cases/failing/112 cmake executable dependency/meson.build b/test cases/failing/110 cmake executable dependency/meson.build similarity index 100% rename from test cases/failing/112 cmake executable dependency/meson.build rename to test cases/failing/110 cmake executable dependency/meson.build diff --git a/test cases/failing/112 cmake executable dependency/subprojects/cmlib/CMakeLists.txt b/test cases/failing/110 cmake executable dependency/subprojects/cmlib/CMakeLists.txt similarity index 100% rename from test cases/failing/112 cmake executable dependency/subprojects/cmlib/CMakeLists.txt rename to test cases/failing/110 cmake executable dependency/subprojects/cmlib/CMakeLists.txt diff --git a/test cases/failing/96 no native compiler/main.c b/test cases/failing/110 cmake executable dependency/subprojects/cmlib/main.c similarity index 100% rename from test cases/failing/96 no native compiler/main.c rename to test cases/failing/110 cmake executable dependency/subprojects/cmlib/main.c diff --git a/test cases/failing/110 cmake executable dependency/test.json b/test cases/failing/110 cmake executable dependency/test.json new file mode 100644 index 0000000..80d01fe --- /dev/null +++ b/test cases/failing/110 cmake executable dependency/test.json @@ -0,0 +1,10 @@ +{ + "stdout": [ + { + "line": "test cases/failing/110 cmake executable dependency/meson.build:9:14: ERROR: main is an executable and does not support the dependency() method. Use target() instead." + } + ], + "tools": { + "cmake": ">=3.14" + } +} diff --git a/test cases/failing/110 compiler argument checking/test.json b/test cases/failing/110 compiler argument checking/test.json deleted file mode 100644 index 3de6acb..0000000 --- a/test cases/failing/110 compiler argument checking/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/110 compiler argument checking/meson.build:4:0: ERROR: Compiler for C does not support \"-meson-goober-arg-for-testing\"" - } - ] -} diff --git a/test cases/failing/113 allow_fallback with fallback/meson.build b/test cases/failing/111 allow_fallback with fallback/meson.build similarity index 100% rename from test cases/failing/113 allow_fallback with fallback/meson.build rename to test cases/failing/111 allow_fallback with fallback/meson.build diff --git a/test cases/failing/113 allow_fallback with fallback/test.json b/test cases/failing/111 allow_fallback with fallback/test.json similarity index 70% rename from test cases/failing/113 allow_fallback with fallback/test.json rename to test cases/failing/111 allow_fallback with fallback/test.json index 58ed475..92aa19a 100644 --- a/test cases/failing/113 allow_fallback with fallback/test.json +++ b/test cases/failing/111 allow_fallback with fallback/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/113 allow_fallback with fallback/meson.build:3:0: ERROR: \"fallback\" and \"allow_fallback\" arguments are mutually exclusive" + "line": "test cases/failing/111 allow_fallback with fallback/meson.build:3:0: ERROR: \"fallback\" and \"allow_fallback\" arguments are mutually exclusive" } ] } diff --git a/test cases/failing/111 empty fallback/test.json b/test cases/failing/111 empty fallback/test.json deleted file mode 100644 index 02db40b..0000000 --- a/test cases/failing/111 empty fallback/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/111 empty fallback/meson.build:6:0: ERROR: Dependency \"foo\" not found, tried pkgconfig and cmake" - } - ] -} diff --git a/test cases/failing/112 cmake executable dependency/test.json b/test cases/failing/112 cmake executable dependency/test.json deleted file mode 100644 index c1ccf6c..0000000 --- a/test cases/failing/112 cmake executable dependency/test.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/112 cmake executable dependency/meson.build:9:0: ERROR: main is an executable and does not support the dependency() method. Use target() instead." - } - ], - "tools": { - "cmake": ">=3.14" - } -} diff --git a/test cases/failing/114 nonsensical bindgen/meson.build b/test cases/failing/112 nonsensical bindgen/meson.build similarity index 100% rename from test cases/failing/114 nonsensical bindgen/meson.build rename to test cases/failing/112 nonsensical bindgen/meson.build diff --git a/test cases/failing/114 nonsensical bindgen/src/header.h b/test cases/failing/112 nonsensical bindgen/src/header.h similarity index 100% rename from test cases/failing/114 nonsensical bindgen/src/header.h rename to test cases/failing/112 nonsensical bindgen/src/header.h diff --git a/test cases/failing/114 nonsensical bindgen/src/source.c b/test cases/failing/112 nonsensical bindgen/src/source.c similarity index 100% rename from test cases/failing/114 nonsensical bindgen/src/source.c rename to test cases/failing/112 nonsensical bindgen/src/source.c diff --git a/test cases/failing/114 nonsensical bindgen/test.json b/test cases/failing/112 nonsensical bindgen/test.json similarity index 69% rename from test cases/failing/114 nonsensical bindgen/test.json rename to test cases/failing/112 nonsensical bindgen/test.json index bc85311..a2b4b71 100644 --- a/test cases/failing/114 nonsensical bindgen/test.json +++ b/test cases/failing/112 nonsensical bindgen/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/114 nonsensical bindgen/meson.build:17:24: ERROR: bindgen source file must be a C header, not an object or build target" + "line": "test cases/failing/112 nonsensical bindgen/meson.build:17:24: ERROR: bindgen source file must be a C header, not an object or build target" } ] } diff --git a/test cases/failing/115 run_target in test/meson.build b/test cases/failing/113 run_target in test/meson.build similarity index 50% rename from test cases/failing/115 run_target in test/meson.build rename to test cases/failing/113 run_target in test/meson.build index 1d448ec..117009e 100644 --- a/test cases/failing/115 run_target in test/meson.build +++ b/test cases/failing/113 run_target in test/meson.build @@ -1,7 +1,7 @@ project('trivial test', 'c') -py_inst = import('python').find_installation() +python = find_program('python3') exe = executable('trivialprog', 'trivial.c') -runt = run_target('invalid', command: [py_inst, '--version']) +runt = run_target('invalid', command: [python, '--version']) test('runtest', exe, args: runt) diff --git a/test cases/failing/115 run_target in test/test.json b/test cases/failing/113 run_target in test/test.json similarity index 77% rename from test cases/failing/115 run_target in test/test.json rename to test cases/failing/113 run_target in test/test.json index 6eddb7c..0e1ded4 100644 --- a/test cases/failing/115 run_target in test/test.json +++ b/test cases/failing/113 run_target in test/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/115 run_target in test/meson.build:7:0: ERROR: test keyword argument 'args' was of type array[RunTarget] but should have been array[str | File | BuildTarget | CustomTarget | CustomTargetIndex]" + "line": "test cases/failing/113 run_target in test/meson.build:7:0: ERROR: test keyword argument 'args' was of type array[RunTarget] but should have been array[str | File | BuildTarget | CustomTarget | CustomTargetIndex]" } ] } diff --git a/test cases/failing/115 run_target in test/trivial.c b/test cases/failing/113 run_target in test/trivial.c similarity index 100% rename from test cases/failing/115 run_target in test/trivial.c rename to test cases/failing/113 run_target in test/trivial.c diff --git a/test cases/failing/116 run_target in add_install_script/meson.build b/test cases/failing/114 run_target in add_install_script/meson.build similarity index 50% rename from test cases/failing/116 run_target in add_install_script/meson.build rename to test cases/failing/114 run_target in add_install_script/meson.build index f8ae3a1..d3634bf 100644 --- a/test cases/failing/116 run_target in add_install_script/meson.build +++ b/test cases/failing/114 run_target in add_install_script/meson.build @@ -1,7 +1,7 @@ project('trivial test', 'c') -py_inst = import('python').find_installation() +python = find_program('python3') exe = executable('trivialprog', 'trivial.c') -runt = run_target('invalid', command: [py_inst, '--version']) +runt = run_target('invalid', command: [python, '--version']) meson.add_install_script(exe, runt) diff --git a/test cases/failing/116 run_target in add_install_script/test.json b/test cases/failing/114 run_target in add_install_script/test.json similarity index 81% rename from test cases/failing/116 run_target in add_install_script/test.json rename to test cases/failing/114 run_target in add_install_script/test.json index 03027e4..0b28f9b 100644 --- a/test cases/failing/116 run_target in add_install_script/test.json +++ b/test cases/failing/114 run_target in add_install_script/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/116 run_target in add_install_script/meson.build:7:6: ERROR: meson.add_install_script argument 2 was of type \"RunTarget\" but should have been one of: \"str\", \"File\", \"BuildTarget\", \"CustomTarget\", \"CustomTargetIndex\", \"ExternalProgram\"" + "line": "test cases/failing/114 run_target in add_install_script/meson.build:7:6: ERROR: meson.add_install_script argument 2 was of type \"RunTarget\" but should have been one of: \"str\", \"File\", \"BuildTarget\", \"CustomTarget\", \"CustomTargetIndex\", \"ExternalProgram\"" } ] } diff --git a/test cases/failing/116 run_target in add_install_script/trivial.c b/test cases/failing/114 run_target in add_install_script/trivial.c similarity index 100% rename from test cases/failing/116 run_target in add_install_script/trivial.c rename to test cases/failing/114 run_target in add_install_script/trivial.c diff --git a/test cases/failing/117 pathsep in install_symlink/meson.build b/test cases/failing/115 pathsep in install_symlink/meson.build similarity index 100% rename from test cases/failing/117 pathsep in install_symlink/meson.build rename to test cases/failing/115 pathsep in install_symlink/meson.build diff --git a/test cases/failing/117 pathsep in install_symlink/test.json b/test cases/failing/115 pathsep in install_symlink/test.json similarity index 74% rename from test cases/failing/117 pathsep in install_symlink/test.json rename to test cases/failing/115 pathsep in install_symlink/test.json index aa35619..fee661f 100644 --- a/test cases/failing/117 pathsep in install_symlink/test.json +++ b/test cases/failing/115 pathsep in install_symlink/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/117 pathsep in install_symlink/meson.build:3:0: ERROR: Link name is \"foo/bar\", but link names cannot contain path separators. The dir part should be in install_dir." + "line": "test cases/failing/115 pathsep in install_symlink/meson.build:3:0: ERROR: Link name is \"foo/bar\", but link names cannot contain path separators. The dir part should be in install_dir." } ] } diff --git a/test cases/failing/118 subproject version conflict/meson.build b/test cases/failing/116 subproject version conflict/meson.build similarity index 100% rename from test cases/failing/118 subproject version conflict/meson.build rename to test cases/failing/116 subproject version conflict/meson.build diff --git a/test cases/failing/118 subproject version conflict/subprojects/A/meson.build b/test cases/failing/116 subproject version conflict/subprojects/A/meson.build similarity index 100% rename from test cases/failing/118 subproject version conflict/subprojects/A/meson.build rename to test cases/failing/116 subproject version conflict/subprojects/A/meson.build diff --git a/test cases/failing/118 subproject version conflict/subprojects/B/meson.build b/test cases/failing/116 subproject version conflict/subprojects/B/meson.build similarity index 100% rename from test cases/failing/118 subproject version conflict/subprojects/B/meson.build rename to test cases/failing/116 subproject version conflict/subprojects/B/meson.build diff --git a/test cases/failing/116 subproject version conflict/test.json b/test cases/failing/116 subproject version conflict/test.json new file mode 100644 index 0000000..c827744 --- /dev/null +++ b/test cases/failing/116 subproject version conflict/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/116 subproject version conflict/meson.build:4:8: ERROR: Subproject B version is 100 but ['1'] required." + } + ] +} diff --git a/test cases/failing/120 structured_sources conflicts/main.rs b/test cases/failing/117 structured source empty string/main.rs similarity index 100% rename from test cases/failing/120 structured_sources conflicts/main.rs rename to test cases/failing/117 structured source empty string/main.rs diff --git a/test cases/failing/119 structured source empty string/meson.build b/test cases/failing/117 structured source empty string/meson.build similarity index 100% rename from test cases/failing/119 structured source empty string/meson.build rename to test cases/failing/117 structured source empty string/meson.build diff --git a/test cases/failing/117 structured source empty string/test.json b/test cases/failing/117 structured source empty string/test.json new file mode 100644 index 0000000..7c90b03 --- /dev/null +++ b/test cases/failing/117 structured source empty string/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/117 structured source empty string/meson.build:9:2: ERROR: structured_sources: keys to dictionary argument may not be an empty string." + } + ] +} diff --git a/test cases/failing/119 structured source empty string/main.rs b/test cases/failing/118 structured_sources conflicts/main.rs similarity index 100% rename from test cases/failing/119 structured source empty string/main.rs rename to test cases/failing/118 structured_sources conflicts/main.rs diff --git a/test cases/failing/120 structured_sources conflicts/meson.build b/test cases/failing/118 structured_sources conflicts/meson.build similarity index 100% rename from test cases/failing/120 structured_sources conflicts/meson.build rename to test cases/failing/118 structured_sources conflicts/meson.build diff --git a/test cases/failing/120 structured_sources conflicts/test.json b/test cases/failing/118 structured_sources conflicts/test.json similarity index 67% rename from test cases/failing/120 structured_sources conflicts/test.json rename to test cases/failing/118 structured_sources conflicts/test.json index 2f3d1ef..55b1228 100644 --- a/test cases/failing/120 structured_sources conflicts/test.json +++ b/test cases/failing/118 structured_sources conflicts/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/120 structured_sources conflicts/meson.build:7:0: ERROR: Conflicting sources in structured sources: main.rs" + "line": "test cases/failing/118 structured_sources conflicts/meson.build:7:0: ERROR: Conflicting sources in structured sources: main.rs" } ] } diff --git a/test cases/failing/118 subproject version conflict/test.json b/test cases/failing/118 subproject version conflict/test.json deleted file mode 100644 index a31511c..0000000 --- a/test cases/failing/118 subproject version conflict/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/118 subproject version conflict/meson.build:4:0: ERROR: Subproject B version is 100 but ['1'] required." - } - ] -} diff --git a/test cases/failing/121 missing compiler/meson.build b/test cases/failing/119 missing compiler/meson.build similarity index 100% rename from test cases/failing/121 missing compiler/meson.build rename to test cases/failing/119 missing compiler/meson.build diff --git a/test cases/failing/121 missing compiler/subprojects/sub/main.c b/test cases/failing/119 missing compiler/subprojects/sub/main.c similarity index 100% rename from test cases/failing/121 missing compiler/subprojects/sub/main.c rename to test cases/failing/119 missing compiler/subprojects/sub/main.c diff --git a/test cases/failing/121 missing compiler/subprojects/sub/meson.build b/test cases/failing/119 missing compiler/subprojects/sub/meson.build similarity index 100% rename from test cases/failing/121 missing compiler/subprojects/sub/meson.build rename to test cases/failing/119 missing compiler/subprojects/sub/meson.build diff --git a/test cases/failing/121 missing compiler/test.json b/test cases/failing/119 missing compiler/test.json similarity index 68% rename from test cases/failing/121 missing compiler/test.json rename to test cases/failing/119 missing compiler/test.json index 442e331..c5f3fb1 100644 --- a/test cases/failing/121 missing compiler/test.json +++ b/test cases/failing/119 missing compiler/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/121 missing compiler/subprojects/sub/meson.build:4:0: ERROR: No host machine compiler for 'subprojects/sub/main.c'" + "line": "test cases/failing/119 missing compiler/subprojects/sub/meson.build:4:0: ERROR: No host machine compiler for 'subprojects/sub/main.c'" } ] } diff --git a/test cases/failing/119 structured source empty string/test.json b/test cases/failing/119 structured source empty string/test.json deleted file mode 100644 index 3d41fc2..0000000 --- a/test cases/failing/119 structured source empty string/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/119 structured source empty string/meson.build:7:0: ERROR: structured_sources: keys to dictionary argument may not be an empty string." - } - ] -} diff --git a/test cases/failing/12 string arithmetic/test.json b/test cases/failing/12 string arithmetic/test.json index 96595c8..10c5e74 100644 --- a/test cases/failing/12 string arithmetic/test.json +++ b/test cases/failing/12 string arithmetic/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/12 string arithmetic/meson.build:3:0: ERROR: The `+` operator of str does not accept objects of type int (3)" + "line": "test cases/failing/12 string arithmetic/meson.build:3:12: ERROR: The `+` operator of str does not accept objects of type int (3)" } ] } diff --git a/test cases/failing/122 cmake subproject error/meson.build b/test cases/failing/120 cmake subproject error/meson.build similarity index 100% rename from test cases/failing/122 cmake subproject error/meson.build rename to test cases/failing/120 cmake subproject error/meson.build diff --git a/test cases/failing/122 cmake subproject error/subprojects/cmlib/CMakeLists.txt b/test cases/failing/120 cmake subproject error/subprojects/cmlib/CMakeLists.txt similarity index 100% rename from test cases/failing/122 cmake subproject error/subprojects/cmlib/CMakeLists.txt rename to test cases/failing/120 cmake subproject error/subprojects/cmlib/CMakeLists.txt diff --git a/test cases/failing/120 cmake subproject error/test.json b/test cases/failing/120 cmake subproject error/test.json new file mode 100644 index 0000000..d88e708 --- /dev/null +++ b/test cases/failing/120 cmake subproject error/test.json @@ -0,0 +1,10 @@ +{ + "stdout": [ + { + "line": "test cases/failing/120 cmake subproject error/meson.build:8:14: ERROR: Failed to configure the CMake subproject: Fancy error message" + } + ], + "tools": { + "cmake": ">=3.14" + } +} diff --git a/test cases/failing/123 pkgconfig not relocatable outside prefix/meson.build b/test cases/failing/121 pkgconfig not relocatable outside prefix/meson.build similarity index 100% rename from test cases/failing/123 pkgconfig not relocatable outside prefix/meson.build rename to test cases/failing/121 pkgconfig not relocatable outside prefix/meson.build diff --git a/test cases/failing/123 pkgconfig not relocatable outside prefix/test.json b/test cases/failing/121 pkgconfig not relocatable outside prefix/test.json similarity index 78% rename from test cases/failing/123 pkgconfig not relocatable outside prefix/test.json rename to test cases/failing/121 pkgconfig not relocatable outside prefix/test.json index 2ca1320..c0dfb87 100644 --- a/test cases/failing/123 pkgconfig not relocatable outside prefix/test.json +++ b/test cases/failing/121 pkgconfig not relocatable outside prefix/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/123 pkgconfig not relocatable outside prefix/meson\\.build:17:5: ERROR: Pkgconfig prefix cannot be outside of the prefix when pkgconfig\\.relocatable=true. Pkgconfig prefix is (C:)?/opt/lib/pkgconfig.", + "line": "test cases/failing/121 pkgconfig not relocatable outside prefix/meson\\.build:17:5: ERROR: Pkgconfig prefix cannot be outside of the prefix when pkgconfig\\.relocatable=true. Pkgconfig prefix is (C:)?/opt/lib/pkgconfig.", "match": "re" } ] diff --git a/test cases/failing/122 cmake subproject error/test.json b/test cases/failing/122 cmake subproject error/test.json deleted file mode 100644 index 1201da2..0000000 --- a/test cases/failing/122 cmake subproject error/test.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/122 cmake subproject error/meson.build:8:0: ERROR: Failed to configure the CMake subproject: Fancy error message" - } - ], - "tools": { - "cmake": ">=3.14" - } -} diff --git a/test cases/failing/124 subproject sandbox violation/meson.build b/test cases/failing/122 subproject sandbox violation/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/meson.build rename to test cases/failing/122 subproject sandbox violation/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/meson_options.txt b/test cases/failing/122 subproject sandbox violation/meson_options.txt similarity index 100% rename from test cases/failing/124 subproject sandbox violation/meson_options.txt rename to test cases/failing/122 subproject sandbox violation/meson_options.txt diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj3/file.txt b/test cases/failing/122 subproject sandbox violation/subprojects/subproj1/file.txt similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj3/file.txt rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj1/file.txt diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj1/meson.build b/test cases/failing/122 subproject sandbox violation/subprojects/subproj1/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj1/meson.build rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj1/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj1/nested/meson.build b/test cases/failing/122 subproject sandbox violation/subprojects/subproj1/nested/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj1/nested/meson.build rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj1/nested/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj2/file.txt b/test cases/failing/122 subproject sandbox violation/subprojects/subproj2/file.txt similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj2/file.txt rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj2/file.txt diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj2/meson.build b/test cases/failing/122 subproject sandbox violation/subprojects/subproj2/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj2/meson.build rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj2/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj2/nested/meson.build b/test cases/failing/122 subproject sandbox violation/subprojects/subproj2/nested/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj2/nested/meson.build rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj2/nested/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj1/file.txt b/test cases/failing/122 subproject sandbox violation/subprojects/subproj3/file.txt similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj1/file.txt rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj3/file.txt diff --git a/test cases/failing/124 subproject sandbox violation/subprojects/subproj3/meson.build b/test cases/failing/122 subproject sandbox violation/subprojects/subproj3/meson.build similarity index 100% rename from test cases/failing/124 subproject sandbox violation/subprojects/subproj3/meson.build rename to test cases/failing/122 subproject sandbox violation/subprojects/subproj3/meson.build diff --git a/test cases/failing/124 subproject sandbox violation/test.json b/test cases/failing/122 subproject sandbox violation/test.json similarity index 82% rename from test cases/failing/124 subproject sandbox violation/test.json rename to test cases/failing/122 subproject sandbox violation/test.json index ca3d142..4bd4028 100644 --- a/test cases/failing/124 subproject sandbox violation/test.json +++ b/test cases/failing/122 subproject sandbox violation/test.json @@ -10,7 +10,7 @@ }, "stdout": [ { - "line": "test cases/failing/124 subproject sandbox violation/meson.build:24:0: ERROR: Sandbox violation: Tried to grab file file.txt from a nested subproject." + "line": "test cases/failing/122 subproject sandbox violation/meson.build:24:0: ERROR: Sandbox violation: Tried to grab file file.txt from a nested subproject." } ] } diff --git a/test cases/failing/126 targets before add_project_dependency/inc/lib.h b/test cases/failing/123 override and add_project_dependency/inc/lib.h similarity index 100% rename from test cases/failing/126 targets before add_project_dependency/inc/lib.h rename to test cases/failing/123 override and add_project_dependency/inc/lib.h diff --git a/test cases/failing/126 targets before add_project_dependency/lib.c b/test cases/failing/123 override and add_project_dependency/lib.c similarity index 100% rename from test cases/failing/126 targets before add_project_dependency/lib.c rename to test cases/failing/123 override and add_project_dependency/lib.c diff --git a/test cases/failing/125 override and add_project_dependency/meson.build b/test cases/failing/123 override and add_project_dependency/meson.build similarity index 100% rename from test cases/failing/125 override and add_project_dependency/meson.build rename to test cases/failing/123 override and add_project_dependency/meson.build diff --git a/test cases/failing/125 override and add_project_dependency/subprojects/a/meson.build b/test cases/failing/123 override and add_project_dependency/subprojects/a/meson.build similarity index 100% rename from test cases/failing/125 override and add_project_dependency/subprojects/a/meson.build rename to test cases/failing/123 override and add_project_dependency/subprojects/a/meson.build diff --git a/test cases/failing/125 override and add_project_dependency/subprojects/a/prog.c b/test cases/failing/123 override and add_project_dependency/subprojects/a/prog.c similarity index 100% rename from test cases/failing/125 override and add_project_dependency/subprojects/a/prog.c rename to test cases/failing/123 override and add_project_dependency/subprojects/a/prog.c diff --git a/test cases/failing/125 override and add_project_dependency/test.json b/test cases/failing/123 override and add_project_dependency/test.json similarity index 66% rename from test cases/failing/125 override and add_project_dependency/test.json rename to test cases/failing/123 override and add_project_dependency/test.json index df749ec..e0a4433 100644 --- a/test cases/failing/125 override and add_project_dependency/test.json +++ b/test cases/failing/123 override and add_project_dependency/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/125 override and add_project_dependency/subprojects/a/meson.build:6:0: ERROR: Dependencies must be external dependencies" + "line": "test cases/failing/123 override and add_project_dependency/subprojects/a/meson.build:6:0: ERROR: Dependencies must be external dependencies" } ] } diff --git a/test cases/failing/125 override and add_project_dependency/inc/lib.h b/test cases/failing/124 targets before add_project_dependency/inc/lib.h similarity index 100% rename from test cases/failing/125 override and add_project_dependency/inc/lib.h rename to test cases/failing/124 targets before add_project_dependency/inc/lib.h diff --git a/test cases/failing/125 override and add_project_dependency/lib.c b/test cases/failing/124 targets before add_project_dependency/lib.c similarity index 100% rename from test cases/failing/125 override and add_project_dependency/lib.c rename to test cases/failing/124 targets before add_project_dependency/lib.c diff --git a/test cases/failing/126 targets before add_project_dependency/meson.build b/test cases/failing/124 targets before add_project_dependency/meson.build similarity index 100% rename from test cases/failing/126 targets before add_project_dependency/meson.build rename to test cases/failing/124 targets before add_project_dependency/meson.build diff --git a/test cases/failing/126 targets before add_project_dependency/test.json b/test cases/failing/124 targets before add_project_dependency/test.json similarity index 70% rename from test cases/failing/126 targets before add_project_dependency/test.json rename to test cases/failing/124 targets before add_project_dependency/test.json index d467914..d856ba4 100644 --- a/test cases/failing/126 targets before add_project_dependency/test.json +++ b/test cases/failing/124 targets before add_project_dependency/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/126 targets before add_project_dependency/meson.build:5:0: ERROR: Tried to use 'add_project_dependencies' after a build target has been declared." + "line": "test cases/failing/124 targets before add_project_dependency/meson.build:5:0: ERROR: Tried to use 'add_project_dependencies' after a build target has been declared." } ] } diff --git a/test cases/failing/127 extract from unity/meson.build b/test cases/failing/125 extract from unity/meson.build similarity index 100% rename from test cases/failing/127 extract from unity/meson.build rename to test cases/failing/125 extract from unity/meson.build diff --git a/test cases/failing/127 extract from unity/src1.c b/test cases/failing/125 extract from unity/src1.c similarity index 100% rename from test cases/failing/127 extract from unity/src1.c rename to test cases/failing/125 extract from unity/src1.c diff --git a/test cases/failing/127 extract from unity/src2.c b/test cases/failing/125 extract from unity/src2.c similarity index 100% rename from test cases/failing/127 extract from unity/src2.c rename to test cases/failing/125 extract from unity/src2.c diff --git a/test cases/failing/125 extract from unity/test.json b/test cases/failing/125 extract from unity/test.json new file mode 100644 index 0000000..2f6468d --- /dev/null +++ b/test cases/failing/125 extract from unity/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/125 extract from unity/meson.build:4:37: ERROR: Single object files cannot be extracted in Unity builds. You can only extract all the object files for each compiler at once." + } + ] +} diff --git a/test cases/common/227 very long commmand line/main.c b/test cases/failing/126 subproject object as a dependency/main.c similarity index 100% rename from test cases/common/227 very long commmand line/main.c rename to test cases/failing/126 subproject object as a dependency/main.c diff --git a/test cases/failing/128 subproject object as a dependency/meson.build b/test cases/failing/126 subproject object as a dependency/meson.build similarity index 100% rename from test cases/failing/128 subproject object as a dependency/meson.build rename to test cases/failing/126 subproject object as a dependency/meson.build diff --git a/test cases/failing/128 subproject object as a dependency/subprojects/sub/meson.build b/test cases/failing/126 subproject object as a dependency/subprojects/sub/meson.build similarity index 100% rename from test cases/failing/128 subproject object as a dependency/subprojects/sub/meson.build rename to test cases/failing/126 subproject object as a dependency/subprojects/sub/meson.build diff --git a/test cases/failing/128 subproject object as a dependency/test.json b/test cases/failing/126 subproject object as a dependency/test.json similarity index 64% rename from test cases/failing/128 subproject object as a dependency/test.json rename to test cases/failing/126 subproject object as a dependency/test.json index b04bf37..4bf7f5b 100644 --- a/test cases/failing/128 subproject object as a dependency/test.json +++ b/test cases/failing/126 subproject object as a dependency/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/128 subproject object as a dependency/meson.build:3:0: ERROR: Tried to use subproject object as a dependency." + "line": "test cases/failing/126 subproject object as a dependency/meson.build:3:0: ERROR: Tried to use subproject object as a dependency." } ] } diff --git a/test cases/failing/127 extract from unity/test.json b/test cases/failing/127 extract from unity/test.json deleted file mode 100644 index e27599d..0000000 --- a/test cases/failing/127 extract from unity/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/127 extract from unity/meson.build:4:0: ERROR: Single object files can not be extracted in Unity builds. You can only extract all the object files for each compiler at once." - } - ] -} diff --git a/test cases/unit/96 compiler.links file arg/test.c b/test cases/failing/127 generator host binary/exe.c similarity index 100% rename from test cases/unit/96 compiler.links file arg/test.c rename to test cases/failing/127 generator host binary/exe.c diff --git a/test cases/failing/127 generator host binary/lib.in b/test cases/failing/127 generator host binary/lib.in new file mode 100644 index 0000000..d0b6ab7 --- /dev/null +++ b/test cases/failing/127 generator host binary/lib.in @@ -0,0 +1 @@ +int foo(void) { return 7; } diff --git a/test cases/failing/127 generator host binary/meson.build b/test cases/failing/127 generator host binary/meson.build new file mode 100644 index 0000000..e769338 --- /dev/null +++ b/test cases/failing/127 generator host binary/meson.build @@ -0,0 +1,14 @@ +project('generator host binary no exe_wrapper') + +if meson.can_run_host_binaries() + error('MESON_SKIP_TEST: test requires that build machine cannot run host binaries') +endif + +add_languages('c', native : false) + +exe = executable('exe', 'exe.c', native : false) + +gen = generator(exe, output : '@BASENAME@.c', arguments : ['@INPUT@', '@OUTPUT@']) +foo = gen.process('lib.in') + +library('foo', foo) diff --git a/test cases/failing/127 generator host binary/test.json b/test cases/failing/127 generator host binary/test.json new file mode 100644 index 0000000..7e354d6 --- /dev/null +++ b/test cases/failing/127 generator host binary/test.json @@ -0,0 +1,5 @@ +{ + "stdout": [ + { "line": "ERROR: An exe_wrapper is needed but was not found. Please define one in cross file and check the command and/or add it to PATH." } + ] +} diff --git a/test cases/failing/128 invalid ast/meson.build b/test cases/failing/128 invalid ast/meson.build new file mode 100644 index 0000000..06011c2 --- /dev/null +++ b/test cases/failing/128 invalid ast/meson.build @@ -0,0 +1,3 @@ +project('invalid ast crash', meson_version: '0.1.0') + += >%@ diff --git a/test cases/failing/128 invalid ast/test.json b/test cases/failing/128 invalid ast/test.json new file mode 100644 index 0000000..e5c3873 --- /dev/null +++ b/test cases/failing/128 invalid ast/test.json @@ -0,0 +1,9 @@ +{ + "stdout": [ + { + "match": "re", + "line": "test cases/failing/128 invalid ast/meson.build:1:44: ERROR: Meson version is [0-9.]+(\\.rc[0-9]+)? but project requires 0.1.0" + } + ] +} + diff --git a/test cases/failing/129 invalid project function/meson.build b/test cases/failing/129 invalid project function/meson.build new file mode 100644 index 0000000..0032c9c --- /dev/null +++ b/test cases/failing/129 invalid project function/meson.build @@ -0,0 +1 @@ +project('invalid project function with bad kwargs', meson_version: '0.1.0', unknown_kwarg: 'val') diff --git a/test cases/failing/129 invalid project function/test.json b/test cases/failing/129 invalid project function/test.json new file mode 100644 index 0000000..b28266c --- /dev/null +++ b/test cases/failing/129 invalid project function/test.json @@ -0,0 +1,9 @@ +{ + "stdout": [ + { + "match": "re", + "line": "test cases/failing/129 invalid project function/meson.build:1:67: ERROR: Meson version is [0-9.]+(\\.rc[0-9]+)? but project requires 0.1.0" + } + ] +} + diff --git a/test cases/failing/13 array arithmetic/test.json b/test cases/failing/13 array arithmetic/test.json index 8904775..9300829 100644 --- a/test cases/failing/13 array arithmetic/test.json +++ b/test cases/failing/13 array arithmetic/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/13 array arithmetic/meson.build:3:0: ERROR: Object <[ArrayHolder] holds [list]: ['a', 'b']> of type array does not support the `*` operator." + "line": "test cases/failing/13 array arithmetic/meson.build:3:19: ERROR: Object <[ArrayHolder] holds [list]: ['a', 'b']> of type array does not support the `*` operator." } ] } diff --git a/test cases/failing/16 extract from subproject/test.json b/test cases/failing/16 extract from subproject/test.json index 2e32904..1616033 100644 --- a/test cases/failing/16 extract from subproject/test.json +++ b/test cases/failing/16 extract from subproject/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/16 extract from subproject/meson.build:6:0: ERROR: Tried to extract objects from a different subproject." + "line": "test cases/failing/16 extract from subproject/meson.build:7:32: ERROR: Tried to extract objects from a different subproject." } ] } diff --git a/test cases/failing/20 version/test.json b/test cases/failing/20 version/test.json index f330624..565fbf2 100644 --- a/test cases/failing/20 version/test.json +++ b/test cases/failing/20 version/test.json @@ -2,7 +2,7 @@ "stdout": [ { "match": "re", - "line": "test cases/failing/20 version/meson\\.build:1:0: ERROR: Meson version is .* but project requires >100\\.0\\.0" + "line": "test cases/failing/20 version/meson\\.build:1:44: ERROR: Meson version is .* but project requires >100\\.0\\.0" } ] } diff --git a/test cases/failing/21 subver/test.json b/test cases/failing/21 subver/test.json index a197b36..694b777 100644 --- a/test cases/failing/21 subver/test.json +++ b/test cases/failing/21 subver/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/21 subver/meson.build:3:0: ERROR: Subproject foo version is 1.0.0 but ['>1.0.0'] required." + "line": "test cases/failing/21 subver/meson.build:3:4: ERROR: Subproject foo version is 1.0.0 but ['>1.0.0'] required." } ] } diff --git a/test cases/failing/28 no crossprop/test.json b/test cases/failing/28 no crossprop/test.json index 6fb9dce..78be0b7 100644 --- a/test cases/failing/28 no crossprop/test.json +++ b/test cases/failing/28 no crossprop/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/28 no crossprop/meson.build:3:0: ERROR: Unknown property for host machine: nonexisting" + "line": "test cases/failing/28 no crossprop/meson.build:3:14: ERROR: Unknown property for host machine: nonexisting" } ] } diff --git a/test cases/failing/3 missing subdir/test.json b/test cases/failing/3 missing subdir/test.json index 562de25..e70f6de 100644 --- a/test cases/failing/3 missing subdir/test.json +++ b/test cases/failing/3 missing subdir/test.json @@ -3,7 +3,7 @@ { "comment": "'missing/meson.build' gets transformed with os.path.sep separators", "match": "re", - "line": "test cases/failing/3 missing subdir/meson\\.build:3:0: ERROR: Non\\-existent build file 'missing[\\\\/]meson\\.build'" + "line": "test cases/failing/3 missing subdir/meson\\.build:3:0: ERROR: Nonexistent build file 'missing[\\\\/]meson\\.build'" } ] } diff --git a/test cases/failing/30 invalid man extension/test.json b/test cases/failing/30 invalid man extension/test.json index 3e5f45d..6ab6843 100644 --- a/test cases/failing/30 invalid man extension/test.json +++ b/test cases/failing/30 invalid man extension/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/30 invalid man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 9" + "line": "test cases/failing/30 invalid man extension/meson.build:2:5: ERROR: Man file must have a file extension of a number between 1 and 9" } ] } diff --git a/test cases/failing/31 no man extension/test.json b/test cases/failing/31 no man extension/test.json index 0972da1..3082f48 100644 --- a/test cases/failing/31 no man extension/test.json +++ b/test cases/failing/31 no man extension/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/31 no man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 9" + "line": "test cases/failing/31 no man extension/meson.build:2:5: ERROR: Man file must have a file extension of a number between 1 and 9" } ] } diff --git a/test cases/failing/32 exe static shared/test.json b/test cases/failing/32 exe static shared/test.json index 51d3804..0b859e3 100644 --- a/test cases/failing/32 exe static shared/test.json +++ b/test cases/failing/32 exe static shared/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/32 exe static shared/meson.build:9:0: ERROR: Can't link non-PIC static library 'stat' into shared library 'shr2'. Use the 'pic' option to static_library to build with PIC." + "line": "test cases/failing/32 exe static shared/meson.build:9:9: ERROR: Can't link non-PIC static library 'stat' into shared library 'shr2'. Use the 'pic' option to static_library to build with PIC." } ] } diff --git a/test cases/failing/34 dependency not-required then required/test.json b/test cases/failing/34 dependency not-required then required/test.json index 3cf35f5..7dd8519 100644 --- a/test cases/failing/34 dependency not-required then required/test.json +++ b/test cases/failing/34 dependency not-required then required/test.json @@ -2,7 +2,7 @@ "stdout": [ { "match": "re", - "line": ".*/meson\\.build:4:0: ERROR: (Pkg-config binary for machine MachineChoice\\.HOST not found\\. Giving up\\.|Dependency \"foo\\-bar\\-xyz\\-12\\.3\" not found, tried .*)" + "line": ".*/meson\\.build:4:10: ERROR: (Pkg-config binary for machine MachineChoice\\.HOST not found\\. Giving up\\.|Dependency \"foo\\-bar\\-xyz\\-12\\.3\" not found, tried .*)" } ] } diff --git a/test cases/failing/39 kwarg assign/test.json b/test cases/failing/39 kwarg assign/test.json index 8fd9d0f..e12f2dc 100644 --- a/test cases/failing/39 kwarg assign/test.json +++ b/test cases/failing/39 kwarg assign/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/39 kwarg assign/meson.build:3:0: ERROR: Tried to assign values inside an argument list." + "line": "test cases/failing/39 kwarg assign/meson.build:3:30: ERROR: Tried to assign values inside an argument list." } ] } diff --git a/test cases/failing/4 missing meson.build/test.json b/test cases/failing/4 missing meson.build/test.json index 3857090..e111087 100644 --- a/test cases/failing/4 missing meson.build/test.json +++ b/test cases/failing/4 missing meson.build/test.json @@ -3,7 +3,7 @@ { "match": "re", "comment": "'subdir/meson.build' gets transformed with os.path.sep separators", - "line": "test cases/failing/4 missing meson\\.build/meson\\.build:3:0: ERROR: Non\\-existent build file 'subdir[\\\\/]meson\\.build'" + "line": "test cases/failing/4 missing meson\\.build/meson\\.build:3:0: ERROR: Nonexistent build file 'subdir[\\\\/]meson\\.build'" } ] } diff --git a/test cases/failing/49 executable comparison/test.json b/test cases/failing/49 executable comparison/test.json index a37002e..76c310b 100644 --- a/test cases/failing/49 executable comparison/test.json +++ b/test cases/failing/49 executable comparison/test.json @@ -2,7 +2,7 @@ "stdout": [ { "match": "re", - "line": "test cases/failing/49 executable comparison/meson.build:6:0: ERROR: Object of type Executable does not support the `<` operator." + "line": "test cases/failing/49 executable comparison/meson.build:6:14: ERROR: Object of type Executable does not support the `<` operator." } ] } diff --git a/test cases/failing/50 inconsistent comparison/test.json b/test cases/failing/50 inconsistent comparison/test.json index 171bfa6..fa71eef 100644 --- a/test cases/failing/50 inconsistent comparison/test.json +++ b/test cases/failing/50 inconsistent comparison/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/50 inconsistent comparison/meson.build:5:0: ERROR: Object <[ArrayHolder] holds [list]: []> of type array does not support the `<` operator." + "line": "test cases/failing/50 inconsistent comparison/meson.build:5:12: ERROR: Object <[ArrayHolder] holds [list]: []> of type array does not support the `<` operator." } ] } diff --git a/test cases/failing/55 or on new line/meson.build b/test cases/failing/53 or on new line/meson.build similarity index 100% rename from test cases/failing/55 or on new line/meson.build rename to test cases/failing/53 or on new line/meson.build diff --git a/test cases/failing/55 or on new line/meson_options.txt b/test cases/failing/53 or on new line/meson_options.txt similarity index 100% rename from test cases/failing/55 or on new line/meson_options.txt rename to test cases/failing/53 or on new line/meson_options.txt diff --git a/test cases/failing/55 or on new line/test.json b/test cases/failing/53 or on new line/test.json similarity index 50% rename from test cases/failing/55 or on new line/test.json rename to test cases/failing/53 or on new line/test.json index f1b8a67..3390025 100644 --- a/test cases/failing/55 or on new line/test.json +++ b/test cases/failing/53 or on new line/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/55 or on new line/meson.build:4:8: ERROR: Invalid or clause." + "line": "test cases/failing/53 or on new line/meson.build:4:8: ERROR: Invalid or clause." } ] } diff --git a/test cases/failing/53 wrong shared crate type/meson.build b/test cases/failing/53 wrong shared crate type/meson.build deleted file mode 100644 index 90020fa..0000000 --- a/test cases/failing/53 wrong shared crate type/meson.build +++ /dev/null @@ -1,7 +0,0 @@ -project('test') - -if not add_languages('rust', required: false) - error('MESON_SKIP_TEST test requires rust compiler') -endif - -shared_library('mytest', 'foo.rs', rust_crate_type : 'staticlib') diff --git a/test cases/failing/53 wrong shared crate type/test.json b/test cases/failing/53 wrong shared crate type/test.json deleted file mode 100644 index 5faaece..0000000 --- a/test cases/failing/53 wrong shared crate type/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/53 wrong shared crate type/meson.build:7:0: ERROR: Crate type \"staticlib\" invalid for dynamic libraries; must be \"dylib\", \"cdylib\", or \"proc-macro\"" - } - ] -} diff --git a/test cases/failing/56 link with executable/meson.build b/test cases/failing/54 link with executable/meson.build similarity index 100% rename from test cases/failing/56 link with executable/meson.build rename to test cases/failing/54 link with executable/meson.build diff --git a/test cases/failing/56 link with executable/module.c b/test cases/failing/54 link with executable/module.c similarity index 100% rename from test cases/failing/56 link with executable/module.c rename to test cases/failing/54 link with executable/module.c diff --git a/test cases/failing/56 link with executable/prog.c b/test cases/failing/54 link with executable/prog.c similarity index 100% rename from test cases/failing/56 link with executable/prog.c rename to test cases/failing/54 link with executable/prog.c diff --git a/test cases/failing/54 link with executable/test.json b/test cases/failing/54 link with executable/test.json new file mode 100644 index 0000000..2b51bdd --- /dev/null +++ b/test cases/failing/54 link with executable/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/54 link with executable/meson.build:4:4: ERROR: Link target 'prog' is not linkable." + } + ] +} diff --git a/test cases/failing/54 wrong static crate type/meson.build b/test cases/failing/54 wrong static crate type/meson.build deleted file mode 100644 index 179d7cd..0000000 --- a/test cases/failing/54 wrong static crate type/meson.build +++ /dev/null @@ -1,7 +0,0 @@ -project('test') - -if not add_languages('rust', required: false) - error('MESON_SKIP_TEST test requires rust compiler') -endif - -static_library('mytest', 'foo.rs', rust_crate_type : 'cdylib') diff --git a/test cases/failing/54 wrong static crate type/test.json b/test cases/failing/54 wrong static crate type/test.json deleted file mode 100644 index 83ae5e1..0000000 --- a/test cases/failing/54 wrong static crate type/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/54 wrong static crate type/meson.build:7:0: ERROR: Crate type \"cdylib\" invalid for static libraries; must be \"rlib\" or \"staticlib\"" - } - ] -} diff --git a/test cases/failing/57 assign custom target index/meson.build b/test cases/failing/55 assign custom target index/meson.build similarity index 100% rename from test cases/failing/57 assign custom target index/meson.build rename to test cases/failing/55 assign custom target index/meson.build diff --git a/test cases/failing/57 assign custom target index/test.json b/test cases/failing/55 assign custom target index/test.json similarity index 59% rename from test cases/failing/57 assign custom target index/test.json rename to test cases/failing/55 assign custom target index/test.json index 392137a..39983fb 100644 --- a/test cases/failing/57 assign custom target index/test.json +++ b/test cases/failing/55 assign custom target index/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/57 assign custom target index/meson.build:24:0: ERROR: Assignment target must be an id." + "line": "test cases/failing/55 assign custom target index/meson.build:24:0: ERROR: Assignment target must be an id." } ] } diff --git a/test cases/failing/58 getoption prefix/meson.build b/test cases/failing/56 getoption prefix/meson.build similarity index 100% rename from test cases/failing/58 getoption prefix/meson.build rename to test cases/failing/56 getoption prefix/meson.build diff --git a/test cases/failing/58 getoption prefix/subprojects/abc/meson.build b/test cases/failing/56 getoption prefix/subprojects/abc/meson.build similarity index 100% rename from test cases/failing/58 getoption prefix/subprojects/abc/meson.build rename to test cases/failing/56 getoption prefix/subprojects/abc/meson.build diff --git a/test cases/failing/58 getoption prefix/subprojects/abc/meson_options.txt b/test cases/failing/56 getoption prefix/subprojects/abc/meson_options.txt similarity index 100% rename from test cases/failing/58 getoption prefix/subprojects/abc/meson_options.txt rename to test cases/failing/56 getoption prefix/subprojects/abc/meson_options.txt diff --git a/test cases/failing/58 getoption prefix/test.json b/test cases/failing/56 getoption prefix/test.json similarity index 72% rename from test cases/failing/58 getoption prefix/test.json rename to test cases/failing/56 getoption prefix/test.json index 630dcd9..c52dffc 100644 --- a/test cases/failing/58 getoption prefix/test.json +++ b/test cases/failing/56 getoption prefix/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/58 getoption prefix/meson.build:5:0: ERROR: Having a colon in option name is forbidden, projects are not allowed to directly access options of other subprojects." + "line": "test cases/failing/56 getoption prefix/meson.build:5:0: ERROR: Having a colon in option name is forbidden, projects are not allowed to directly access options of other subprojects." } ] } diff --git a/test cases/failing/56 link with executable/test.json b/test cases/failing/56 link with executable/test.json deleted file mode 100644 index 2288783..0000000 --- a/test cases/failing/56 link with executable/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/56 link with executable/meson.build:4:0: ERROR: Link target 'prog' is not linkable." - } - ] -} diff --git a/test cases/failing/59 bad option argument/meson.build b/test cases/failing/57 bad option argument/meson.build similarity index 100% rename from test cases/failing/59 bad option argument/meson.build rename to test cases/failing/57 bad option argument/meson.build diff --git a/test cases/failing/57 bad option argument/meson_options.txt b/test cases/failing/57 bad option argument/meson_options.txt new file mode 100644 index 0000000..0e0372b --- /dev/null +++ b/test cases/failing/57 bad option argument/meson_options.txt @@ -0,0 +1 @@ +option('name', type : 'string', value_ : 'foo') diff --git a/test cases/failing/57 bad option argument/test.json b/test cases/failing/57 bad option argument/test.json new file mode 100644 index 0000000..622acdf --- /dev/null +++ b/test cases/failing/57 bad option argument/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/57 bad option argument/meson_options.txt:1:0: ERROR: string option got unknown keyword arguments \"value_\"" + } + ] +} diff --git a/test cases/failing/60 subproj filegrab/meson.build b/test cases/failing/58 subproj filegrab/meson.build similarity index 100% rename from test cases/failing/60 subproj filegrab/meson.build rename to test cases/failing/58 subproj filegrab/meson.build diff --git a/test cases/failing/63 string as link target/prog.c b/test cases/failing/58 subproj filegrab/prog.c similarity index 100% rename from test cases/failing/63 string as link target/prog.c rename to test cases/failing/58 subproj filegrab/prog.c diff --git a/test cases/failing/60 subproj filegrab/subprojects/a/meson.build b/test cases/failing/58 subproj filegrab/subprojects/a/meson.build similarity index 100% rename from test cases/failing/60 subproj filegrab/subprojects/a/meson.build rename to test cases/failing/58 subproj filegrab/subprojects/a/meson.build diff --git a/test cases/failing/60 subproj filegrab/test.json b/test cases/failing/58 subproj filegrab/test.json similarity index 68% rename from test cases/failing/60 subproj filegrab/test.json rename to test cases/failing/58 subproj filegrab/test.json index 04a6dbb..950e109 100644 --- a/test cases/failing/60 subproj filegrab/test.json +++ b/test cases/failing/58 subproj filegrab/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/60 subproj filegrab/subprojects/a/meson.build:3:0: ERROR: Sandbox violation: Tried to grab file prog.c outside current (sub)project." + "line": "test cases/failing/58 subproj filegrab/subprojects/a/meson.build:3:0: ERROR: Sandbox violation: Tried to grab file prog.c outside current (sub)project." } ] } diff --git a/test cases/failing/59 bad option argument/meson_options.txt b/test cases/failing/59 bad option argument/meson_options.txt deleted file mode 100644 index de1fff6..0000000 --- a/test cases/failing/59 bad option argument/meson_options.txt +++ /dev/null @@ -1 +0,0 @@ -option('name', type : 'string', vaule : 'foo') diff --git a/test cases/failing/59 bad option argument/test.json b/test cases/failing/59 bad option argument/test.json deleted file mode 100644 index 3c5df1b..0000000 --- a/test cases/failing/59 bad option argument/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/59 bad option argument/meson_options.txt:1:0: ERROR: option got unknown keyword arguments \"vaule\"" - } - ] -} diff --git a/test cases/failing/61 grab subproj/meson.build b/test cases/failing/59 grab subproj/meson.build similarity index 100% rename from test cases/failing/61 grab subproj/meson.build rename to test cases/failing/59 grab subproj/meson.build diff --git a/test cases/failing/61 grab subproj/subprojects/foo/meson.build b/test cases/failing/59 grab subproj/subprojects/foo/meson.build similarity index 100% rename from test cases/failing/61 grab subproj/subprojects/foo/meson.build rename to test cases/failing/59 grab subproj/subprojects/foo/meson.build diff --git a/test cases/failing/61 grab subproj/subprojects/foo/sub.c b/test cases/failing/59 grab subproj/subprojects/foo/sub.c similarity index 100% rename from test cases/failing/61 grab subproj/subprojects/foo/sub.c rename to test cases/failing/59 grab subproj/subprojects/foo/sub.c diff --git a/test cases/failing/61 grab subproj/test.json b/test cases/failing/59 grab subproj/test.json similarity index 64% rename from test cases/failing/61 grab subproj/test.json rename to test cases/failing/59 grab subproj/test.json index 873ec6c..7c90202 100644 --- a/test cases/failing/61 grab subproj/test.json +++ b/test cases/failing/59 grab subproj/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/61 grab subproj/meson.build:7:0: ERROR: Sandbox violation: Tried to grab file sub.c from a nested subproject." + "line": "test cases/failing/59 grab subproj/meson.build:7:0: ERROR: Sandbox violation: Tried to grab file sub.c from a nested subproject." } ] } diff --git a/test cases/failing/6 missing incdir/test.json b/test cases/failing/6 missing incdir/test.json index 172d8a9..600a90b 100644 --- a/test cases/failing/6 missing incdir/test.json +++ b/test cases/failing/6 missing incdir/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/6 missing incdir/meson.build:3:0: ERROR: Include dir nosuchdir does not exist." + "line": "test cases/failing/6 missing incdir/meson.build:3:6: ERROR: Include dir nosuchdir does not exist." } ] } diff --git a/test cases/failing/62 grab sibling/meson.build b/test cases/failing/60 grab sibling/meson.build similarity index 100% rename from test cases/failing/62 grab sibling/meson.build rename to test cases/failing/60 grab sibling/meson.build diff --git a/test cases/failing/62 grab sibling/subprojects/a/meson.build b/test cases/failing/60 grab sibling/subprojects/a/meson.build similarity index 100% rename from test cases/failing/62 grab sibling/subprojects/a/meson.build rename to test cases/failing/60 grab sibling/subprojects/a/meson.build diff --git a/test cases/failing/62 grab sibling/subprojects/b/meson.build b/test cases/failing/60 grab sibling/subprojects/b/meson.build similarity index 100% rename from test cases/failing/62 grab sibling/subprojects/b/meson.build rename to test cases/failing/60 grab sibling/subprojects/b/meson.build diff --git a/test cases/failing/62 grab sibling/subprojects/b/sneaky.c b/test cases/failing/60 grab sibling/subprojects/b/sneaky.c similarity index 100% rename from test cases/failing/62 grab sibling/subprojects/b/sneaky.c rename to test cases/failing/60 grab sibling/subprojects/b/sneaky.c diff --git a/test cases/failing/62 grab sibling/test.json b/test cases/failing/60 grab sibling/test.json similarity index 68% rename from test cases/failing/62 grab sibling/test.json rename to test cases/failing/60 grab sibling/test.json index 9e7c4bb..8f42bad 100644 --- a/test cases/failing/62 grab sibling/test.json +++ b/test cases/failing/60 grab sibling/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/62 grab sibling/subprojects/a/meson.build:3:0: ERROR: Sandbox violation: Tried to grab file sneaky.c outside current (sub)project." + "line": "test cases/failing/60 grab sibling/subprojects/a/meson.build:3:0: ERROR: Sandbox violation: Tried to grab file sneaky.c outside current (sub)project." } ] } diff --git a/test cases/failing/63 string as link target/meson.build b/test cases/failing/61 string as link target/meson.build similarity index 100% rename from test cases/failing/63 string as link target/meson.build rename to test cases/failing/61 string as link target/meson.build diff --git a/test cases/failing/60 subproj filegrab/prog.c b/test cases/failing/61 string as link target/prog.c similarity index 100% rename from test cases/failing/60 subproj filegrab/prog.c rename to test cases/failing/61 string as link target/prog.c diff --git a/test cases/failing/63 string as link target/test.json b/test cases/failing/61 string as link target/test.json similarity index 53% rename from test cases/failing/63 string as link target/test.json rename to test cases/failing/61 string as link target/test.json index a531d64..b7f6bd7 100644 --- a/test cases/failing/63 string as link target/test.json +++ b/test cases/failing/61 string as link target/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/63 string as link target/meson.build:2:0: ERROR: '' is not a target." + "line": "test cases/failing/61 string as link target/meson.build:2:0: ERROR: '' is not a target." } ] } diff --git a/test cases/failing/64 dependency not-found and required/meson.build b/test cases/failing/62 dependency not-found and required/meson.build similarity index 100% rename from test cases/failing/64 dependency not-found and required/meson.build rename to test cases/failing/62 dependency not-found and required/meson.build diff --git a/test cases/failing/62 dependency not-found and required/test.json b/test cases/failing/62 dependency not-found and required/test.json new file mode 100644 index 0000000..69a67ff --- /dev/null +++ b/test cases/failing/62 dependency not-found and required/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/62 dependency not-found and required/meson.build:2:6: ERROR: Dependency is required but has no candidates." + } + ] +} diff --git a/test cases/failing/65 subproj different versions/main.c b/test cases/failing/63 subproj different versions/main.c similarity index 100% rename from test cases/failing/65 subproj different versions/main.c rename to test cases/failing/63 subproj different versions/main.c diff --git a/test cases/failing/65 subproj different versions/meson.build b/test cases/failing/63 subproj different versions/meson.build similarity index 100% rename from test cases/failing/65 subproj different versions/meson.build rename to test cases/failing/63 subproj different versions/meson.build diff --git a/test cases/failing/65 subproj different versions/subprojects/a/a.c b/test cases/failing/63 subproj different versions/subprojects/a/a.c similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/a/a.c rename to test cases/failing/63 subproj different versions/subprojects/a/a.c diff --git a/test cases/failing/65 subproj different versions/subprojects/a/a.h b/test cases/failing/63 subproj different versions/subprojects/a/a.h similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/a/a.h rename to test cases/failing/63 subproj different versions/subprojects/a/a.h diff --git a/test cases/failing/65 subproj different versions/subprojects/a/meson.build b/test cases/failing/63 subproj different versions/subprojects/a/meson.build similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/a/meson.build rename to test cases/failing/63 subproj different versions/subprojects/a/meson.build diff --git a/test cases/failing/65 subproj different versions/subprojects/b/b.c b/test cases/failing/63 subproj different versions/subprojects/b/b.c similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/b/b.c rename to test cases/failing/63 subproj different versions/subprojects/b/b.c diff --git a/test cases/failing/65 subproj different versions/subprojects/b/b.h b/test cases/failing/63 subproj different versions/subprojects/b/b.h similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/b/b.h rename to test cases/failing/63 subproj different versions/subprojects/b/b.h diff --git a/test cases/failing/65 subproj different versions/subprojects/b/meson.build b/test cases/failing/63 subproj different versions/subprojects/b/meson.build similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/b/meson.build rename to test cases/failing/63 subproj different versions/subprojects/b/meson.build diff --git a/test cases/failing/65 subproj different versions/subprojects/c/c.h b/test cases/failing/63 subproj different versions/subprojects/c/c.h similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/c/c.h rename to test cases/failing/63 subproj different versions/subprojects/c/c.h diff --git a/test cases/failing/65 subproj different versions/subprojects/c/meson.build b/test cases/failing/63 subproj different versions/subprojects/c/meson.build similarity index 100% rename from test cases/failing/65 subproj different versions/subprojects/c/meson.build rename to test cases/failing/63 subproj different versions/subprojects/c/meson.build diff --git a/test cases/failing/63 subproj different versions/test.json b/test cases/failing/63 subproj different versions/test.json new file mode 100644 index 0000000..f2535fa --- /dev/null +++ b/test cases/failing/63 subproj different versions/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/63 subproj different versions/subprojects/b/meson.build:3:8: ERROR: Dependency 'c' is required but not found." + } + ] +} diff --git a/test cases/failing/64 dependency not-found and required/test.json b/test cases/failing/64 dependency not-found and required/test.json deleted file mode 100644 index 84d14b4..0000000 --- a/test cases/failing/64 dependency not-found and required/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/64 dependency not-found and required/meson.build:2:0: ERROR: Dependency is required but has no candidates." - } - ] -} diff --git a/test cases/failing/66 wrong boost module/meson.build b/test cases/failing/64 wrong boost module/meson.build similarity index 100% rename from test cases/failing/66 wrong boost module/meson.build rename to test cases/failing/64 wrong boost module/meson.build diff --git a/test cases/failing/64 wrong boost module/test.json b/test cases/failing/64 wrong boost module/test.json new file mode 100644 index 0000000..78e2069 --- /dev/null +++ b/test cases/failing/64 wrong boost module/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/64 wrong boost module/meson.build:9:10: ERROR: Dependency \"boost\" not found, tried system" + } + ] +} diff --git a/test cases/failing/67 install_data rename bad size/file1.txt b/test cases/failing/65 install_data rename bad size/file1.txt similarity index 100% rename from test cases/failing/67 install_data rename bad size/file1.txt rename to test cases/failing/65 install_data rename bad size/file1.txt diff --git a/test cases/failing/67 install_data rename bad size/file2.txt b/test cases/failing/65 install_data rename bad size/file2.txt similarity index 100% rename from test cases/failing/67 install_data rename bad size/file2.txt rename to test cases/failing/65 install_data rename bad size/file2.txt diff --git a/test cases/failing/67 install_data rename bad size/meson.build b/test cases/failing/65 install_data rename bad size/meson.build similarity index 100% rename from test cases/failing/67 install_data rename bad size/meson.build rename to test cases/failing/65 install_data rename bad size/meson.build diff --git a/test cases/failing/67 install_data rename bad size/test.json b/test cases/failing/65 install_data rename bad size/test.json similarity index 75% rename from test cases/failing/67 install_data rename bad size/test.json rename to test cases/failing/65 install_data rename bad size/test.json index af1f0d9..efc6290 100644 --- a/test cases/failing/67 install_data rename bad size/test.json +++ b/test cases/failing/65 install_data rename bad size/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/67 install_data rename bad size/meson.build:3:0: ERROR: \"rename\" and \"sources\" argument lists must be the same length if \"rename\" is given. Rename has 1 elements and sources has 2." + "line": "test cases/failing/65 install_data rename bad size/meson.build:3:0: ERROR: \"rename\" and \"sources\" argument lists must be the same length if \"rename\" is given. Rename has 1 elements and sources has 2." } ] } diff --git a/test cases/failing/65 subproj different versions/test.json b/test cases/failing/65 subproj different versions/test.json deleted file mode 100644 index 2f9f70d..0000000 --- a/test cases/failing/65 subproj different versions/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/65 subproj different versions/subprojects/b/meson.build:3:0: ERROR: Dependency 'c' is required but not found." - } - ] -} diff --git a/test cases/failing/68 skip only subdir/meson.build b/test cases/failing/66 skip only subdir/meson.build similarity index 100% rename from test cases/failing/68 skip only subdir/meson.build rename to test cases/failing/66 skip only subdir/meson.build diff --git a/test cases/failing/68 skip only subdir/subdir/meson.build b/test cases/failing/66 skip only subdir/subdir/meson.build similarity index 100% rename from test cases/failing/68 skip only subdir/subdir/meson.build rename to test cases/failing/66 skip only subdir/subdir/meson.build diff --git a/test cases/failing/68 skip only subdir/test.json b/test cases/failing/66 skip only subdir/test.json similarity index 55% rename from test cases/failing/68 skip only subdir/test.json rename to test cases/failing/66 skip only subdir/test.json index 4558847..de21e00 100644 --- a/test cases/failing/68 skip only subdir/test.json +++ b/test cases/failing/66 skip only subdir/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/68 skip only subdir/meson.build:8:0: ERROR: File main.cpp does not exist." + "line": "test cases/failing/66 skip only subdir/meson.build:8:0: ERROR: File main.cpp does not exist." } ] } diff --git a/test cases/failing/66 wrong boost module/test.json b/test cases/failing/66 wrong boost module/test.json deleted file mode 100644 index c65a78c..0000000 --- a/test cases/failing/66 wrong boost module/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/66 wrong boost module/meson.build:9:0: ERROR: Dependency \"boost\" not found, tried system" - } - ] -} diff --git a/test cases/failing/69 dual override/meson.build b/test cases/failing/67 dual override/meson.build similarity index 100% rename from test cases/failing/69 dual override/meson.build rename to test cases/failing/67 dual override/meson.build diff --git a/test cases/failing/69 dual override/overrides.py b/test cases/failing/67 dual override/overrides.py similarity index 100% rename from test cases/failing/69 dual override/overrides.py rename to test cases/failing/67 dual override/overrides.py diff --git a/test cases/failing/69 dual override/test.json b/test cases/failing/67 dual override/test.json similarity index 65% rename from test cases/failing/69 dual override/test.json rename to test cases/failing/67 dual override/test.json index 784d6b2..b50f3ac 100644 --- a/test cases/failing/69 dual override/test.json +++ b/test cases/failing/67 dual override/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/69 dual override/meson.build:5:6: ERROR: Tried to override executable \"override\" which has already been overridden." + "line": "test cases/failing/67 dual override/meson.build:5:6: ERROR: Tried to override executable \"override\" which has already been overridden." } ] } diff --git a/test cases/failing/70 override used/meson.build b/test cases/failing/68 override used/meson.build similarity index 75% rename from test cases/failing/70 override used/meson.build rename to test cases/failing/68 override used/meson.build index 128108e..582fc1b 100644 --- a/test cases/failing/70 override used/meson.build +++ b/test cases/failing/68 override used/meson.build @@ -1,4 +1,4 @@ -project('overridde an already found exe') +project('override an already found exe') old = find_program('something.py') replacement = find_program('other.py') diff --git a/test cases/failing/70 override used/other.py b/test cases/failing/68 override used/other.py similarity index 100% rename from test cases/failing/70 override used/other.py rename to test cases/failing/68 override used/other.py diff --git a/test cases/failing/70 override used/something.py b/test cases/failing/68 override used/something.py similarity index 100% rename from test cases/failing/70 override used/something.py rename to test cases/failing/68 override used/something.py diff --git a/test cases/failing/70 override used/test.json b/test cases/failing/68 override used/test.json similarity index 67% rename from test cases/failing/70 override used/test.json rename to test cases/failing/68 override used/test.json index adb60aa..f11aac6 100644 --- a/test cases/failing/70 override used/test.json +++ b/test cases/failing/68 override used/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/70 override used/meson.build:5:6: ERROR: Tried to override finding of executable \"something.py\" which has already been found." + "line": "test cases/failing/68 override used/meson.build:5:6: ERROR: Tried to override finding of executable \"something.py\" which has already been found." } ] } diff --git a/test cases/failing/71 run_command unclean exit/meson.build b/test cases/failing/69 run_command unclean exit/meson.build similarity index 100% rename from test cases/failing/71 run_command unclean exit/meson.build rename to test cases/failing/69 run_command unclean exit/meson.build diff --git a/test cases/failing/71 run_command unclean exit/returncode.py b/test cases/failing/69 run_command unclean exit/returncode.py similarity index 100% rename from test cases/failing/71 run_command unclean exit/returncode.py rename to test cases/failing/69 run_command unclean exit/returncode.py diff --git a/test cases/failing/69 run_command unclean exit/test.json b/test cases/failing/69 run_command unclean exit/test.json new file mode 100644 index 0000000..96c2141 --- /dev/null +++ b/test cases/failing/69 run_command unclean exit/test.json @@ -0,0 +1,8 @@ +{ + "stdout": [ + { + "match": "re", + "line": "test cases/failing/69 run_command unclean exit/meson\\.build:4:0: ERROR: Command `.*['\"].*[\\\\/]test cases[\\\\/]failing[\\\\/]69 run_command unclean exit[\\\\/]\\.[\\\\/]returncode\\.py['\"] 1` failed with status 1\\." + } + ] +} diff --git a/test cases/failing/72 int literal leading zero/meson.build b/test cases/failing/70 int literal leading zero/meson.build similarity index 100% rename from test cases/failing/72 int literal leading zero/meson.build rename to test cases/failing/70 int literal leading zero/meson.build diff --git a/test cases/failing/72 int literal leading zero/test.json b/test cases/failing/70 int literal leading zero/test.json similarity index 69% rename from test cases/failing/72 int literal leading zero/test.json rename to test cases/failing/70 int literal leading zero/test.json index 556e492..fafbf0d 100644 --- a/test cases/failing/72 int literal leading zero/test.json +++ b/test cases/failing/70 int literal leading zero/test.json @@ -2,7 +2,7 @@ "stdout": [ { "comment": "this error message is not very informative", - "line": "test cases/failing/72 int literal leading zero/meson.build:5:13: ERROR: Expecting eof got number." + "line": "test cases/failing/70 int literal leading zero/meson.build:5:13: ERROR: Expecting eof got number." } ] } diff --git a/test cases/failing/87 invalid configure file/input b/test cases/failing/71 configuration immutable/input similarity index 100% rename from test cases/failing/87 invalid configure file/input rename to test cases/failing/71 configuration immutable/input diff --git a/test cases/failing/73 configuration immutable/meson.build b/test cases/failing/71 configuration immutable/meson.build similarity index 100% rename from test cases/failing/73 configuration immutable/meson.build rename to test cases/failing/71 configuration immutable/meson.build diff --git a/test cases/failing/73 configuration immutable/test.json b/test cases/failing/71 configuration immutable/test.json similarity index 65% rename from test cases/failing/73 configuration immutable/test.json rename to test cases/failing/71 configuration immutable/test.json index 32d9427..af1a65d 100644 --- a/test cases/failing/73 configuration immutable/test.json +++ b/test cases/failing/71 configuration immutable/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/73 configuration immutable/meson.build:12:16: ERROR: Can not set values on configuration object that has been used." + "line": "test cases/failing/71 configuration immutable/meson.build:12:16: ERROR: Can not set values on configuration object that has been used." } ] } diff --git a/test cases/failing/71 run_command unclean exit/test.json b/test cases/failing/71 run_command unclean exit/test.json deleted file mode 100644 index e9bfb94..0000000 --- a/test cases/failing/71 run_command unclean exit/test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "stdout": [ - { - "match": "re", - "line": "test cases/failing/71 run_command unclean exit/meson\\.build:4:0: ERROR: Command \".*[\\\\/]test cases[\\\\/]failing[\\\\/]71 run_command unclean exit[\\\\/]\\.[\\\\/]returncode\\.py 1\" failed with status 1\\." - } - ] -} diff --git a/test cases/failing/74 link with shared module on osx/meson.build b/test cases/failing/72 link with shared module on osx/meson.build similarity index 100% rename from test cases/failing/74 link with shared module on osx/meson.build rename to test cases/failing/72 link with shared module on osx/meson.build diff --git a/test cases/failing/74 link with shared module on osx/module.c b/test cases/failing/72 link with shared module on osx/module.c similarity index 100% rename from test cases/failing/74 link with shared module on osx/module.c rename to test cases/failing/72 link with shared module on osx/module.c diff --git a/test cases/failing/74 link with shared module on osx/prog.c b/test cases/failing/72 link with shared module on osx/prog.c similarity index 100% rename from test cases/failing/74 link with shared module on osx/prog.c rename to test cases/failing/72 link with shared module on osx/prog.c diff --git a/test cases/failing/72 link with shared module on osx/test.json b/test cases/failing/72 link with shared module on osx/test.json new file mode 100644 index 0000000..1a2d78c --- /dev/null +++ b/test cases/failing/72 link with shared module on osx/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/72 link with shared module on osx/meson.build:8:4: ERROR: target prog links against shared module mymodule. This is not permitted on OSX" + } + ] +} diff --git a/test cases/failing/75 non ascii in ascii encoded configure file/config9.h.in b/test cases/failing/73 non-ascii in ascii encoded configure file/config9.h.in similarity index 100% rename from test cases/failing/75 non ascii in ascii encoded configure file/config9.h.in rename to test cases/failing/73 non-ascii in ascii encoded configure file/config9.h.in diff --git a/test cases/failing/75 non ascii in ascii encoded configure file/meson.build b/test cases/failing/73 non-ascii in ascii encoded configure file/meson.build similarity index 62% rename from test cases/failing/75 non ascii in ascii encoded configure file/meson.build rename to test cases/failing/73 non-ascii in ascii encoded configure file/meson.build index 26da80e..eadb627 100644 --- a/test cases/failing/75 non ascii in ascii encoded configure file/meson.build +++ b/test cases/failing/73 non-ascii in ascii encoded configure file/meson.build @@ -1,5 +1,5 @@ -project('non acsii to ascii encoding') -# Writing a non ASCII character with a ASCII encoding should fail +project('non-ascii to ascii encoding') +# Writing a non-ASCII character with a ASCII encoding should fail conf9 = configuration_data() conf9.set('var', 'д') configure_file( diff --git a/test cases/failing/75 non ascii in ascii encoded configure file/test.json b/test cases/failing/73 non-ascii in ascii encoded configure file/test.json similarity index 79% rename from test cases/failing/75 non ascii in ascii encoded configure file/test.json rename to test cases/failing/73 non-ascii in ascii encoded configure file/test.json index 27cb0ab..63f5bef 100644 --- a/test cases/failing/75 non ascii in ascii encoded configure file/test.json +++ b/test cases/failing/73 non-ascii in ascii encoded configure file/test.json @@ -2,7 +2,7 @@ "stdout": [ { "match": "re", - "line": "test cases/failing/75 non ascii in ascii encoded configure file/meson\\.build:5:0: ERROR: Could not write output file .*[\\\\/]config9\\.h: 'ascii' codec can't encode character '\\\\u0434' in position 17: ordinal not in range\\(128\\)" + "line": "test cases/failing/73 non-ascii in ascii encoded configure file/meson\\.build:5:0: ERROR: Could not write output file .*[\\\\/]config9\\.h: 'ascii' codec can't encode character '\\\\u0434' in position 17: ordinal not in range\\(128\\)" } ] } diff --git a/test cases/failing/74 link with shared module on osx/test.json b/test cases/failing/74 link with shared module on osx/test.json deleted file mode 100644 index 9ca1b9d..0000000 --- a/test cases/failing/74 link with shared module on osx/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/74 link with shared module on osx/meson.build:8:0: ERROR: target prog links against shared module mymodule. This is not permitted on OSX" - } - ] -} diff --git a/test cases/failing/76 subproj dependency not-found and required/meson.build b/test cases/failing/74 subproj dependency not-found and required/meson.build similarity index 100% rename from test cases/failing/76 subproj dependency not-found and required/meson.build rename to test cases/failing/74 subproj dependency not-found and required/meson.build diff --git a/test cases/failing/74 subproj dependency not-found and required/test.json b/test cases/failing/74 subproj dependency not-found and required/test.json new file mode 100644 index 0000000..997bc56 --- /dev/null +++ b/test cases/failing/74 subproj dependency not-found and required/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/74 subproj dependency not-found and required/meson.build:2:10: ERROR: Neither a subproject directory nor a missing.wrap file was found." + } + ] +} diff --git a/test cases/failing/77 unfound run/meson.build b/test cases/failing/75 unfound run/meson.build similarity index 100% rename from test cases/failing/77 unfound run/meson.build rename to test cases/failing/75 unfound run/meson.build diff --git a/test cases/failing/77 unfound run/test.json b/test cases/failing/75 unfound run/test.json similarity index 60% rename from test cases/failing/77 unfound run/test.json rename to test cases/failing/75 unfound run/test.json index 99464bd..855c2a4 100644 --- a/test cases/failing/77 unfound run/test.json +++ b/test cases/failing/75 unfound run/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/77 unfound run/meson.build:4:0: ERROR: Tried to use non-existing executable 'nonexisting_prog'" + "line": "test cases/failing/75 unfound run/meson.build:4:0: ERROR: Tried to use non-existing executable 'nonexisting_prog'" } ] } diff --git a/test cases/failing/78 framework dependency with version/meson.build b/test cases/failing/76 framework dependency with version/meson.build similarity index 80% rename from test cases/failing/78 framework dependency with version/meson.build rename to test cases/failing/76 framework dependency with version/meson.build index b7e04ba..ee315eb 100644 --- a/test cases/failing/78 framework dependency with version/meson.build +++ b/test cases/failing/76 framework dependency with version/meson.build @@ -5,4 +5,4 @@ if host_machine.system() != 'darwin' endif # do individual frameworks have a meaningful version to test? And multiple frameworks might be listed... -dep = dependency('appleframeworks', modules: 'foundation', version: '>0') +dep = dependency('appleframeworks', modules: 'Foundation', version: '>0') diff --git a/test cases/failing/76 framework dependency with version/test.json b/test cases/failing/76 framework dependency with version/test.json new file mode 100644 index 0000000..07b4a6e --- /dev/null +++ b/test cases/failing/76 framework dependency with version/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/76 framework dependency with version/meson.build:8:6: ERROR: Dependency lookup for appleframeworks with method 'framework' failed: Unknown version, but need ['>0']." + } + ] +} diff --git a/test cases/failing/76 subproj dependency not-found and required/test.json b/test cases/failing/76 subproj dependency not-found and required/test.json deleted file mode 100644 index 3b98436..0000000 --- a/test cases/failing/76 subproj dependency not-found and required/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/76 subproj dependency not-found and required/meson.build:2:0: ERROR: Neither a subproject directory nor a missing.wrap file was found." - } - ] -} diff --git a/test cases/failing/79 override exe config/foo.c b/test cases/failing/77 override exe config/foo.c similarity index 100% rename from test cases/failing/79 override exe config/foo.c rename to test cases/failing/77 override exe config/foo.c diff --git a/test cases/failing/79 override exe config/meson.build b/test cases/failing/77 override exe config/meson.build similarity index 100% rename from test cases/failing/79 override exe config/meson.build rename to test cases/failing/77 override exe config/meson.build diff --git a/test cases/failing/79 override exe config/test.json b/test cases/failing/77 override exe config/test.json similarity index 72% rename from test cases/failing/79 override exe config/test.json rename to test cases/failing/77 override exe config/test.json index 1a671a3..a0dd7ae 100644 --- a/test cases/failing/79 override exe config/test.json +++ b/test cases/failing/77 override exe config/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/79 override exe config/meson.build:6:0: ERROR: Program 'bar' was overridden with the compiled executable 'foo' and therefore cannot be used during configuration" + "line": "test cases/failing/77 override exe config/meson.build:6:0: ERROR: Program 'bar' was overridden with the compiled executable 'foo' and therefore cannot be used during configuration" } ] } diff --git a/test cases/failing/78 framework dependency with version/test.json b/test cases/failing/78 framework dependency with version/test.json deleted file mode 100644 index d43a498..0000000 --- a/test cases/failing/78 framework dependency with version/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/78 framework dependency with version/meson.build:8:0: ERROR: Dependency lookup for appleframeworks with method 'framework' failed: Unknown version, but need ['>0']." - } - ] -} diff --git a/test cases/failing/80 gl dependency with version/meson.build b/test cases/failing/78 gl dependency with version/meson.build similarity index 100% rename from test cases/failing/80 gl dependency with version/meson.build rename to test cases/failing/78 gl dependency with version/meson.build diff --git a/test cases/failing/78 gl dependency with version/test.json b/test cases/failing/78 gl dependency with version/test.json new file mode 100644 index 0000000..fbd9ff9 --- /dev/null +++ b/test cases/failing/78 gl dependency with version/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/78 gl dependency with version/meson.build:9:6: ERROR: Dependency lookup for gl with method 'system' failed: Unknown version, but need ['>0']." + } + ] +} diff --git a/test cases/failing/81 threads dependency with version/meson.build b/test cases/failing/79 threads dependency with version/meson.build similarity index 100% rename from test cases/failing/81 threads dependency with version/meson.build rename to test cases/failing/79 threads dependency with version/meson.build diff --git a/test cases/failing/79 threads dependency with version/test.json b/test cases/failing/79 threads dependency with version/test.json new file mode 100644 index 0000000..890695b --- /dev/null +++ b/test cases/failing/79 threads dependency with version/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/79 threads dependency with version/meson.build:3:6: ERROR: Dependency lookup for threads with method 'system' failed: Unknown version, but need ['>0']." + } + ] +} diff --git a/test cases/failing/8 recursive/test.json b/test cases/failing/8 recursive/test.json index b4c964c..8878c52 100644 --- a/test cases/failing/8 recursive/test.json +++ b/test cases/failing/8 recursive/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/8 recursive/subprojects/b/meson.build:3:0: ERROR: Recursive include of subprojects: a => b => a." + "line": "test cases/failing/8 recursive/subprojects/b/meson.build:3:4: ERROR: Recursive include of subprojects: a => b => a." } ] } diff --git a/test cases/failing/80 gl dependency with version/test.json b/test cases/failing/80 gl dependency with version/test.json deleted file mode 100644 index 3d39bc3..0000000 --- a/test cases/failing/80 gl dependency with version/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/80 gl dependency with version/meson.build:9:0: ERROR: Dependency lookup for gl with method 'system' failed: Unknown version, but need ['>0']." - } - ] -} diff --git a/test cases/failing/82 gtest dependency with version/meson.build b/test cases/failing/80 gtest dependency with version/meson.build similarity index 100% rename from test cases/failing/82 gtest dependency with version/meson.build rename to test cases/failing/80 gtest dependency with version/meson.build diff --git a/test cases/failing/80 gtest dependency with version/test.json b/test cases/failing/80 gtest dependency with version/test.json new file mode 100644 index 0000000..3f9934d --- /dev/null +++ b/test cases/failing/80 gtest dependency with version/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/80 gtest dependency with version/meson.build:8:6: ERROR: Dependency 'gtest' is required but not found." + } + ] +} diff --git a/test cases/failing/83 dub libray/meson.build b/test cases/failing/81 dub library/meson.build similarity index 100% rename from test cases/failing/83 dub libray/meson.build rename to test cases/failing/81 dub library/meson.build diff --git a/test cases/failing/81 dub library/test.json b/test cases/failing/81 dub library/test.json new file mode 100644 index 0000000..2db91ee --- /dev/null +++ b/test cases/failing/81 dub library/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/81 dub library/meson.build:11:0: ERROR: Dependency \"dubtestproject\" not found" + } + ] +} diff --git a/test cases/failing/81 threads dependency with version/test.json b/test cases/failing/81 threads dependency with version/test.json deleted file mode 100644 index 308ac66..0000000 --- a/test cases/failing/81 threads dependency with version/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/81 threads dependency with version/meson.build:3:0: ERROR: Dependency lookup for threads with method 'system' failed: Unknown version, but need ['>0']." - } - ] -} diff --git a/test cases/failing/84 dub executable/meson.build b/test cases/failing/82 dub executable/meson.build similarity index 100% rename from test cases/failing/84 dub executable/meson.build rename to test cases/failing/82 dub executable/meson.build diff --git a/test cases/failing/84 dub executable/test.json b/test cases/failing/82 dub executable/test.json similarity index 59% rename from test cases/failing/84 dub executable/test.json rename to test cases/failing/82 dub executable/test.json index 6dfff62..8ae46b8 100644 --- a/test cases/failing/84 dub executable/test.json +++ b/test cases/failing/82 dub executable/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/84 dub executable/meson.build:11:0: ERROR: Dependency \"dubtestproject:test1\" not found" + "line": "test cases/failing/82 dub executable/meson.build:11:0: ERROR: Dependency \"dubtestproject:test1\" not found" } ] } diff --git a/test cases/failing/82 gtest dependency with version/test.json b/test cases/failing/82 gtest dependency with version/test.json deleted file mode 100644 index 7cf397b..0000000 --- a/test cases/failing/82 gtest dependency with version/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/82 gtest dependency with version/meson.build:8:0: ERROR: Dependency 'gtest' is required but not found." - } - ] -} diff --git a/test cases/failing/85 dub compiler/meson.build b/test cases/failing/83 dub compiler/meson.build similarity index 100% rename from test cases/failing/85 dub compiler/meson.build rename to test cases/failing/83 dub compiler/meson.build diff --git a/test cases/failing/85 dub compiler/test.json b/test cases/failing/83 dub compiler/test.json similarity index 81% rename from test cases/failing/85 dub compiler/test.json rename to test cases/failing/83 dub compiler/test.json index 50ee39b..07241d2 100644 --- a/test cases/failing/85 dub compiler/test.json +++ b/test cases/failing/83 dub compiler/test.json @@ -13,7 +13,7 @@ }, "stdout": [ { - "line": "test cases/failing/85 dub compiler/meson.build:17:0: ERROR: Dependency \"dubtestproject:test2\" not found" + "line": "test cases/failing/83 dub compiler/meson.build:17:0: ERROR: Dependency \"dubtestproject:test2\" not found" } ] } diff --git a/test cases/failing/83 dub libray/test.json b/test cases/failing/83 dub libray/test.json deleted file mode 100644 index 4dcbbed..0000000 --- a/test cases/failing/83 dub libray/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/83 dub libray/meson.build:11:0: ERROR: Dependency \"dubtestproject\" not found" - } - ] -} diff --git a/test cases/failing/86 subproj not-found dep/meson.build b/test cases/failing/84 subproj not-found dep/meson.build similarity index 100% rename from test cases/failing/86 subproj not-found dep/meson.build rename to test cases/failing/84 subproj not-found dep/meson.build diff --git a/test cases/failing/86 subproj not-found dep/subprojects/somesubproj/meson.build b/test cases/failing/84 subproj not-found dep/subprojects/somesubproj/meson.build similarity index 100% rename from test cases/failing/86 subproj not-found dep/subprojects/somesubproj/meson.build rename to test cases/failing/84 subproj not-found dep/subprojects/somesubproj/meson.build diff --git a/test cases/failing/84 subproj not-found dep/test.json b/test cases/failing/84 subproj not-found dep/test.json new file mode 100644 index 0000000..bea1ed0 --- /dev/null +++ b/test cases/failing/84 subproj not-found dep/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/84 subproj not-found dep/meson.build:2:10: ERROR: Dependency '(anonymous)' is required but not found." + } + ] +} diff --git a/test cases/failing/73 configuration immutable/input b/test cases/failing/85 invalid configure file/input similarity index 100% rename from test cases/failing/73 configuration immutable/input rename to test cases/failing/85 invalid configure file/input diff --git a/test cases/failing/87 invalid configure file/meson.build b/test cases/failing/85 invalid configure file/meson.build similarity index 100% rename from test cases/failing/87 invalid configure file/meson.build rename to test cases/failing/85 invalid configure file/meson.build diff --git a/test cases/failing/87 invalid configure file/test.json b/test cases/failing/85 invalid configure file/test.json similarity index 67% rename from test cases/failing/87 invalid configure file/test.json rename to test cases/failing/85 invalid configure file/test.json index d8ea73d..1cee208 100644 --- a/test cases/failing/87 invalid configure file/test.json +++ b/test cases/failing/85 invalid configure file/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/87 invalid configure file/meson.build:3:0: ERROR: \"install_dir\" must be specified when \"install\" in a configure_file is true" + "line": "test cases/failing/85 invalid configure file/meson.build:3:0: ERROR: \"install_dir\" must be specified when \"install\" in a configure_file is true" } ] } diff --git a/test cases/failing/88 kwarg dupe/meson.build b/test cases/failing/86 kwarg dupe/meson.build similarity index 100% rename from test cases/failing/88 kwarg dupe/meson.build rename to test cases/failing/86 kwarg dupe/meson.build diff --git a/test cases/failing/88 kwarg dupe/prog.c b/test cases/failing/86 kwarg dupe/prog.c similarity index 100% rename from test cases/failing/88 kwarg dupe/prog.c rename to test cases/failing/86 kwarg dupe/prog.c diff --git a/test cases/failing/88 kwarg dupe/test.json b/test cases/failing/86 kwarg dupe/test.json similarity index 65% rename from test cases/failing/88 kwarg dupe/test.json rename to test cases/failing/86 kwarg dupe/test.json index cfd68eb..229debb 100644 --- a/test cases/failing/88 kwarg dupe/test.json +++ b/test cases/failing/86 kwarg dupe/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/88 kwarg dupe/meson.build:5:0: ERROR: Entry \"install\" defined both as a keyword argument and in a \"kwarg\" entry." + "line": "test cases/failing/86 kwarg dupe/meson.build:6:2: ERROR: Entry \"install\" defined both as a keyword argument and in a \"kwarg\" entry." } ] } diff --git a/test cases/failing/86 subproj not-found dep/test.json b/test cases/failing/86 subproj not-found dep/test.json deleted file mode 100644 index b662643..0000000 --- a/test cases/failing/86 subproj not-found dep/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/86 subproj not-found dep/meson.build:2:0: ERROR: Dependency '(anonymous)' is required but not found." - } - ] -} diff --git a/test cases/failing/89 missing pch file/meson.build b/test cases/failing/87 missing pch file/meson.build similarity index 100% rename from test cases/failing/89 missing pch file/meson.build rename to test cases/failing/87 missing pch file/meson.build diff --git a/test cases/failing/89 missing pch file/prog.c b/test cases/failing/87 missing pch file/prog.c similarity index 100% rename from test cases/failing/89 missing pch file/prog.c rename to test cases/failing/87 missing pch file/prog.c diff --git a/test cases/failing/89 missing pch file/test.json b/test cases/failing/87 missing pch file/test.json similarity index 55% rename from test cases/failing/89 missing pch file/test.json rename to test cases/failing/87 missing pch file/test.json index 638d2e7..a54acb7 100644 --- a/test cases/failing/89 missing pch file/test.json +++ b/test cases/failing/87 missing pch file/test.json @@ -2,7 +2,7 @@ "stdout": [ { "comment": "literal 'pch/prog.h' from meson.build appears in output, irrespective of os.path.sep", - "line": "test cases/failing/89 missing pch file/meson.build:2:0: ERROR: File pch/prog.h does not exist." + "line": "test cases/failing/87 missing pch file/meson.build:2:6: ERROR: File pch/prog.h does not exist." } ] } diff --git a/test cases/failing/90 pch source different folder/include/pch.h b/test cases/failing/88 pch source different folder/include/pch.h similarity index 100% rename from test cases/failing/90 pch source different folder/include/pch.h rename to test cases/failing/88 pch source different folder/include/pch.h diff --git a/test cases/failing/90 pch source different folder/meson.build b/test cases/failing/88 pch source different folder/meson.build similarity index 100% rename from test cases/failing/90 pch source different folder/meson.build rename to test cases/failing/88 pch source different folder/meson.build diff --git a/test cases/failing/90 pch source different folder/prog.c b/test cases/failing/88 pch source different folder/prog.c similarity index 100% rename from test cases/failing/90 pch source different folder/prog.c rename to test cases/failing/88 pch source different folder/prog.c diff --git a/test cases/failing/90 pch source different folder/src/pch.c b/test cases/failing/88 pch source different folder/src/pch.c similarity index 100% rename from test cases/failing/90 pch source different folder/src/pch.c rename to test cases/failing/88 pch source different folder/src/pch.c diff --git a/test cases/failing/88 pch source different folder/test.json b/test cases/failing/88 pch source different folder/test.json new file mode 100644 index 0000000..60fa4e0 --- /dev/null +++ b/test cases/failing/88 pch source different folder/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/88 pch source different folder/meson.build:4:6: ERROR: PCH files must be stored in the same folder." + } + ] +} diff --git a/test cases/failing/91 unknown config tool/meson.build b/test cases/failing/89 unknown config tool/meson.build similarity index 100% rename from test cases/failing/91 unknown config tool/meson.build rename to test cases/failing/89 unknown config tool/meson.build diff --git a/test cases/failing/91 unknown config tool/test.json b/test cases/failing/89 unknown config tool/test.json similarity index 60% rename from test cases/failing/91 unknown config tool/test.json rename to test cases/failing/89 unknown config tool/test.json index e225167..f5c0d96 100644 --- a/test cases/failing/91 unknown config tool/test.json +++ b/test cases/failing/89 unknown config tool/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/91 unknown config tool/meson.build:2:0: ERROR: Dependency \"no-such-config-tool\" not found" + "line": "test cases/failing/89 unknown config tool/meson.build:2:0: ERROR: Dependency \"no-such-config-tool\" not found" } ] } diff --git a/test cases/failing/92 custom target install data/Info.plist.cpp b/test cases/failing/90 custom target install data/Info.plist.cpp similarity index 100% rename from test cases/failing/92 custom target install data/Info.plist.cpp rename to test cases/failing/90 custom target install data/Info.plist.cpp diff --git a/test cases/failing/92 custom target install data/meson.build b/test cases/failing/90 custom target install data/meson.build similarity index 100% rename from test cases/failing/92 custom target install data/meson.build rename to test cases/failing/90 custom target install data/meson.build diff --git a/test cases/failing/92 custom target install data/preproc.py b/test cases/failing/90 custom target install data/preproc.py similarity index 100% rename from test cases/failing/92 custom target install data/preproc.py rename to test cases/failing/90 custom target install data/preproc.py diff --git a/test cases/failing/92 custom target install data/test.json b/test cases/failing/90 custom target install data/test.json similarity index 71% rename from test cases/failing/92 custom target install data/test.json rename to test cases/failing/90 custom target install data/test.json index 46ab013..d495163 100644 --- a/test cases/failing/92 custom target install data/test.json +++ b/test cases/failing/90 custom target install data/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/92 custom target install data/meson.build:11:0: ERROR: install_data argument 1 was of type \"CustomTarget\" but should have been one of: \"str\", \"File\"" + "line": "test cases/failing/90 custom target install data/meson.build:11:0: ERROR: install_data argument 1 was of type \"CustomTarget\" but should have been one of: \"str\", \"File\"" } ] } diff --git a/test cases/failing/90 pch source different folder/test.json b/test cases/failing/90 pch source different folder/test.json deleted file mode 100644 index cbbac3d..0000000 --- a/test cases/failing/90 pch source different folder/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/90 pch source different folder/meson.build:4:0: ERROR: PCH files must be stored in the same folder." - } - ] -} diff --git a/test cases/failing/93 add dict non string key/meson.build b/test cases/failing/91 add dict non string key/meson.build similarity index 100% rename from test cases/failing/93 add dict non string key/meson.build rename to test cases/failing/91 add dict non string key/meson.build diff --git a/test cases/failing/91 add dict non string key/test.json b/test cases/failing/91 add dict non string key/test.json new file mode 100644 index 0000000..db506b2 --- /dev/null +++ b/test cases/failing/91 add dict non string key/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/91 add dict non string key/meson.build:9:9: ERROR: Key must be a string" + } + ] +} diff --git a/test cases/failing/94 add dict duplicate keys/meson.build b/test cases/failing/92 add dict duplicate keys/meson.build similarity index 100% rename from test cases/failing/94 add dict duplicate keys/meson.build rename to test cases/failing/92 add dict duplicate keys/meson.build diff --git a/test cases/failing/92 add dict duplicate keys/test.json b/test cases/failing/92 add dict duplicate keys/test.json new file mode 100644 index 0000000..b428a53 --- /dev/null +++ b/test cases/failing/92 add dict duplicate keys/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/92 add dict duplicate keys/meson.build:9:27: ERROR: Duplicate dictionary key: myKey" + } + ] +} diff --git a/test cases/failing/93 add dict non string key/test.json b/test cases/failing/93 add dict non string key/test.json deleted file mode 100644 index 3ef2dde..0000000 --- a/test cases/failing/93 add dict non string key/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/93 add dict non string key/meson.build:9:0: ERROR: Key must be a string" - } - ] -} diff --git a/test cases/failing/95 no host get_external_property/meson.build b/test cases/failing/93 no host get_external_property/meson.build similarity index 100% rename from test cases/failing/95 no host get_external_property/meson.build rename to test cases/failing/93 no host get_external_property/meson.build diff --git a/test cases/failing/93 no host get_external_property/test.json b/test cases/failing/93 no host get_external_property/test.json new file mode 100644 index 0000000..b26f3c9 --- /dev/null +++ b/test cases/failing/93 no host get_external_property/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/93 no host get_external_property/meson.build:3:14: ERROR: Unknown property for host machine: nonexisting" + } + ] +} diff --git a/test cases/failing/94 add dict duplicate keys/test.json b/test cases/failing/94 add dict duplicate keys/test.json deleted file mode 100644 index 5b2d7d6..0000000 --- a/test cases/failing/94 add dict duplicate keys/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/94 add dict duplicate keys/meson.build:9:0: ERROR: Duplicate dictionary key: myKey" - } - ] -} diff --git a/test cases/failing/112 cmake executable dependency/subprojects/cmlib/main.c b/test cases/failing/94 no native compiler/main.c similarity index 100% rename from test cases/failing/112 cmake executable dependency/subprojects/cmlib/main.c rename to test cases/failing/94 no native compiler/main.c diff --git a/test cases/failing/96 no native compiler/meson.build b/test cases/failing/94 no native compiler/meson.build similarity index 100% rename from test cases/failing/96 no native compiler/meson.build rename to test cases/failing/94 no native compiler/meson.build diff --git a/test cases/failing/96 no native compiler/test.json b/test cases/failing/94 no native compiler/test.json similarity index 58% rename from test cases/failing/96 no native compiler/test.json rename to test cases/failing/94 no native compiler/test.json index 0107727..7181c6b 100644 --- a/test cases/failing/96 no native compiler/test.json +++ b/test cases/failing/94 no native compiler/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/96 no native compiler/meson.build:12:0: ERROR: No host machine compiler for \"main.c\"" + "line": "test cases/failing/94 no native compiler/meson.build:12:0: ERROR: No host machine compiler for \"main.c\"" } ] } diff --git a/test cases/failing/95 no host get_external_property/test.json b/test cases/failing/95 no host get_external_property/test.json deleted file mode 100644 index 0ef6dd2..0000000 --- a/test cases/failing/95 no host get_external_property/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/95 no host get_external_property/meson.build:3:0: ERROR: Unknown property for host machine: nonexisting" - } - ] -} diff --git a/test cases/failing/97 subdir parse error/meson.build b/test cases/failing/95 subdir parse error/meson.build similarity index 100% rename from test cases/failing/97 subdir parse error/meson.build rename to test cases/failing/95 subdir parse error/meson.build diff --git a/test cases/failing/97 subdir parse error/subdir/meson.build b/test cases/failing/95 subdir parse error/subdir/meson.build similarity index 100% rename from test cases/failing/97 subdir parse error/subdir/meson.build rename to test cases/failing/95 subdir parse error/subdir/meson.build diff --git a/test cases/failing/97 subdir parse error/test.json b/test cases/failing/95 subdir parse error/test.json similarity index 59% rename from test cases/failing/97 subdir parse error/test.json rename to test cases/failing/95 subdir parse error/test.json index 414789e..c94ed58 100644 --- a/test cases/failing/97 subdir parse error/test.json +++ b/test cases/failing/95 subdir parse error/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/97 subdir parse error/subdir/meson.build:1:0: ERROR: Plusassignment target must be an id." + "line": "test cases/failing/95 subdir parse error/subdir/meson.build:1:0: ERROR: Plusassignment target must be an id." } ] } diff --git a/test cases/failing/98 invalid option file/meson.build b/test cases/failing/96 invalid option file/meson.build similarity index 100% rename from test cases/failing/98 invalid option file/meson.build rename to test cases/failing/96 invalid option file/meson.build diff --git a/test cases/failing/98 invalid option file/meson_options.txt b/test cases/failing/96 invalid option file/meson_options.txt similarity index 100% rename from test cases/failing/98 invalid option file/meson_options.txt rename to test cases/failing/96 invalid option file/meson_options.txt diff --git a/test cases/failing/98 invalid option file/test.json b/test cases/failing/96 invalid option file/test.json similarity index 50% rename from test cases/failing/98 invalid option file/test.json rename to test cases/failing/96 invalid option file/test.json index 6ab5393..5f1a89f 100644 --- a/test cases/failing/98 invalid option file/test.json +++ b/test cases/failing/96 invalid option file/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/98 invalid option file/meson_options.txt:1:0: ERROR: lexer" + "line": "test cases/failing/96 invalid option file/meson_options.txt:1:0: ERROR: lexer" } ] } diff --git a/test cases/common/235 invalid standard overriden to valid/main.c b/test cases/failing/97 no lang/main.c similarity index 100% rename from test cases/common/235 invalid standard overriden to valid/main.c rename to test cases/failing/97 no lang/main.c diff --git a/test cases/failing/99 no lang/meson.build b/test cases/failing/97 no lang/meson.build similarity index 100% rename from test cases/failing/99 no lang/meson.build rename to test cases/failing/97 no lang/meson.build diff --git a/test cases/failing/99 no lang/test.json b/test cases/failing/97 no lang/test.json similarity index 54% rename from test cases/failing/99 no lang/test.json rename to test cases/failing/97 no lang/test.json index 48c6dd7..a2af0a1 100644 --- a/test cases/failing/99 no lang/test.json +++ b/test cases/failing/97 no lang/test.json @@ -1,7 +1,7 @@ { "stdout": [ { - "line": "test cases/failing/99 no lang/meson.build:2:0: ERROR: No host machine compiler for 'main.c'" + "line": "test cases/failing/97 no lang/meson.build:2:0: ERROR: No host machine compiler for 'main.c'" } ] } diff --git a/test cases/failing/100 no glib-compile-resources/meson.build b/test cases/failing/98 no glib-compile-resources/meson.build similarity index 100% rename from test cases/failing/100 no glib-compile-resources/meson.build rename to test cases/failing/98 no glib-compile-resources/meson.build diff --git a/test cases/failing/98 no glib-compile-resources/test.json b/test cases/failing/98 no glib-compile-resources/test.json new file mode 100644 index 0000000..d5edd76 --- /dev/null +++ b/test cases/failing/98 no glib-compile-resources/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/98 no glib-compile-resources/meson.build:8:12: ERROR: Program 'glib-compile-resources' not found or not executable" + } + ] +} diff --git a/test cases/failing/100 no glib-compile-resources/trivial.gresource.xml b/test cases/failing/98 no glib-compile-resources/trivial.gresource.xml similarity index 100% rename from test cases/failing/100 no glib-compile-resources/trivial.gresource.xml rename to test cases/failing/98 no glib-compile-resources/trivial.gresource.xml diff --git a/test cases/failing/101 number in combo/meson.build b/test cases/failing/99 number in combo/meson.build similarity index 100% rename from test cases/failing/101 number in combo/meson.build rename to test cases/failing/99 number in combo/meson.build diff --git a/test cases/failing/101 number in combo/nativefile.ini b/test cases/failing/99 number in combo/nativefile.ini similarity index 100% rename from test cases/failing/101 number in combo/nativefile.ini rename to test cases/failing/99 number in combo/nativefile.ini diff --git a/test cases/failing/99 number in combo/test.json b/test cases/failing/99 number in combo/test.json new file mode 100644 index 0000000..f59812e --- /dev/null +++ b/test cases/failing/99 number in combo/test.json @@ -0,0 +1,5 @@ +{ + "stdout": [ + { "line": "test cases/failing/99 number in combo/meson.build:1:0: ERROR: Value \"1\" (of type \"number\") for combo option \"Optimization level\" is not one of the choices. Possible choices are (as string): \"plain\", \"0\", \"g\", \"1\", \"2\", \"3\", \"s\"." } + ] +} diff --git a/test cases/unit/98 install all targets/foo.in b/test cases/fortran/22 extract_objects/bar.f90 similarity index 100% rename from test cases/unit/98 install all targets/foo.in rename to test cases/fortran/22 extract_objects/bar.f90 diff --git a/test cases/unit/98 install all targets/bar-notag.txt b/test cases/fortran/22 extract_objects/foo1.f90 similarity index 100% rename from test cases/unit/98 install all targets/bar-notag.txt rename to test cases/fortran/22 extract_objects/foo1.f90 diff --git a/test cases/unit/98 install all targets/bar-devel.h b/test cases/fortran/22 extract_objects/foo2.f90 similarity index 100% rename from test cases/unit/98 install all targets/bar-devel.h rename to test cases/fortran/22 extract_objects/foo2.f90 diff --git a/test cases/fortran/22 extract_objects/meson.build b/test cases/fortran/22 extract_objects/meson.build new file mode 100644 index 0000000..9ca325e --- /dev/null +++ b/test cases/fortran/22 extract_objects/meson.build @@ -0,0 +1,17 @@ +project('test_project', 'fortran') + +if get_option('unity') == 'on' + error('MESON_SKIP_TEST: extract_objects does not work in unity builds') +endif + +libfoo = static_library( + 'foo', + sources : ['foo1.f90', 'foo2.f90']) + +foo1_object = libfoo.extract_objects('foo1.f90') + +libfinal = library( + 'final', + sources : 'bar.f90', + objects : foo1_object, +) diff --git a/test cases/fortran/23 preprocess/main.f90 b/test cases/fortran/23 preprocess/main.f90 new file mode 100644 index 0000000..7cbc11c --- /dev/null +++ b/test cases/fortran/23 preprocess/main.f90 @@ -0,0 +1,4 @@ +#define MYDEF program +MYDEF foo + write (*,*) 'Hello, world!' +end MYDEF foo diff --git a/test cases/fortran/23 preprocess/meson.build b/test cases/fortran/23 preprocess/meson.build new file mode 100644 index 0000000..b776940 --- /dev/null +++ b/test cases/fortran/23 preprocess/meson.build @@ -0,0 +1,7 @@ +project('preprocess', 'fortran') + +fc = meson.get_compiler('fortran') + +pp_files = fc.preprocess('main.f90', output: '@PLAINNAME@') + +library('foo', pp_files) diff --git a/test cases/frameworks/1 boost/meson.build b/test cases/frameworks/1 boost/meson.build index 821bb62..2e22cbf 100644 --- a/test cases/frameworks/1 boost/meson.build +++ b/test cases/frameworks/1 boost/meson.build @@ -17,7 +17,7 @@ linkdep = dependency('boost', static: s, modules : ['thread', 'system', 'dat testdep = dependency('boost', static: s, modules : ['unit_test_framework']) nomoddep = dependency('boost', static: s) extralibdep = dependency('boost', static: s, modules : ['thread', 'system', 'date_time', 'log_setup', 'log', 'filesystem', 'regex']) -notfound = dependency('boost', static: s, modules : ['this_should_not_exist_on_any_systen'], required: false) +notfound = dependency('boost', static: s, modules : ['this_should_not_exist_on_any_system'], required: false) assert(not notfound.found()) diff --git a/test cases/frameworks/11 gir subproject/test.json b/test cases/frameworks/11 gir subproject/test.json index 7fbce30..aed0a1c 100644 --- a/test cases/frameworks/11 gir subproject/test.json +++ b/test cases/frameworks/11 gir subproject/test.json @@ -9,5 +9,5 @@ {"type": "expr", "file": "usr/lib/?libgirlib.so"}, {"type": "file", "platform": "cygwin", "file": "usr/lib/libgirsubproject.dll.a"} ], - "skip_on_jobname": ["azure", "cygwin", "macos", "msys2"] + "skip_on_jobname": ["azure", "cygwin", "macos", "msys2", "pypy"] } diff --git a/test cases/frameworks/14 doxygen/doc/Doxyfile.in b/test cases/frameworks/14 doxygen/doc/Doxyfile.in index 69fb4aa..d1ad3a1 100644 --- a/test cases/frameworks/14 doxygen/doc/Doxyfile.in +++ b/test cases/frameworks/14 doxygen/doc/Doxyfile.in @@ -58,7 +58,7 @@ PROJECT_LOGO = # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. -OUTPUT_DIRECTORY = doc +OUTPUT_DIRECTORY = "@TOP_BUILDDIR@/doc" # If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- # directories (in 2 levels) under the output directory of each output format and diff --git a/test cases/frameworks/14 doxygen/doc/meson.build b/test cases/frameworks/14 doxygen/doc/meson.build index bde2d7c..626b8ce 100644 --- a/test cases/frameworks/14 doxygen/doc/meson.build +++ b/test cases/frameworks/14 doxygen/doc/meson.build @@ -1,5 +1,5 @@ -cdata.set('TOP_SRCDIR', meson.source_root()) -cdata.set('TOP_BUILDDIR', meson.build_root()) +cdata.set('TOP_SRCDIR', meson.project_source_root()) +cdata.set('TOP_BUILDDIR', meson.project_build_root()) doxyfile = configure_file(input: 'Doxyfile.in', output: 'Doxyfile', diff --git a/test cases/frameworks/14 doxygen/meson.build b/test cases/frameworks/14 doxygen/meson.build index 517cef2..9a323fa 100644 --- a/test cases/frameworks/14 doxygen/meson.build +++ b/test cases/frameworks/14 doxygen/meson.build @@ -1,4 +1,4 @@ -project('doxygen test', 'cpp', version : '0.1.0') +project('doxygen test', 'cpp', version : '0.1.0', meson_version: '>=0.56') spede_inc = include_directories('include') diff --git a/test cases/frameworks/15 llvm/meson.build b/test cases/frameworks/15 llvm/meson.build index 3855fae..bb945cc 100644 --- a/test cases/frameworks/15 llvm/meson.build +++ b/test cases/frameworks/15 llvm/meson.build @@ -2,50 +2,120 @@ project('llvmtest', ['c', 'cpp'], default_options : ['c_std=c99']) method = get_option('method') static = get_option('link-static') -d = dependency('llvm', required : false, method : method, static : static) + +d = dependency('llvm', required : false, static : static) if not d.found() error('MESON_SKIP_TEST llvm not found.') endif -d = dependency('llvm', modules : 'not-found', required : false, static : static, method : method) -assert(d.found() == false, 'not-found llvm module found') +if method != 'config-tool' and d.version().startswith('17.0') and host_machine.system() == 'windows' + # https://github.com/llvm/llvm-project/commit/e7fc7540daa9333f0be4f380fc9c619236d17f57#r130257253 + error('MESON_SKIP_TEST broken llvm cmake files on MSYS2') +endif -d = dependency('llvm', version : '<0.1', required : false, static : static, method : method) -assert(d.found() == false, 'ancient llvm module found') +modules_to_find = [ + 'bitwriter', 'asmprinter', 'executionengine', 'mcjit', 'target', + 'nativecodegen', 'amdgpu', 'engine' +] -d = dependency('llvm', optional_modules : 'not-found', required : false, static : static, method : method) -assert(d.found() == true, 'optional module stopped llvm from being found.') +if method == 'combination' + if not d.version().version_compare(static ? '>0.1' : '>=7.0') + error('MESON_SKIP_TEST: llvm version is too low') + endif + llvm_ct_dep = dependency( + 'llvm', + modules : modules_to_find, + required : false, + static : static, + method : 'config-tool', + ) -# Check we can apply a version constraint -d = dependency('llvm', version : ['< 500', '>=@0@'.format(d.version())], required: false, static : static, method : method) -assert(d.found() == true, 'Cannot set version constraints') + if static and d.version().startswith('16.0') and d.version()[5].to_int() <= 6 + message('Skipping modules with cmake due to an LLVM bug, see https://github.com/mesonbuild/meson/issues/11642') + llvm_cm_dep = dependency( + 'llvm', + required : false, + static : static, + method : 'cmake', + ) + else + llvm_cm_dep = dependency( + 'llvm', + modules : modules_to_find, + required : false, + static : static, + method : 'cmake', + ) + endif -dep_tinfo = dependency('tinfo', required : false) -if not dep_tinfo.found() - cpp = meson.get_compiler('cpp') - dep_tinfo = cpp.find_library('tinfo', required: false) -endif + assert(llvm_ct_dep.found() and llvm_cm_dep.found(), 'config-tool and cmake both need to be found') + cm_version_major = llvm_cm_dep.version().split('.')[0].to_int() + cm_version_minor = llvm_cm_dep.version().split('.')[1].to_int() + ct_version_major = llvm_ct_dep.version().split('.')[0].to_int() + ct_version_minor = llvm_ct_dep.version().split('.')[1].to_int() + assert(cm_version_major == ct_version_major, 'config-tool and cmake returns different major versions') + assert(cm_version_minor == ct_version_minor, 'config-tool and cmake returns different minor versions') +else + if not static and method == 'cmake' + d = dependency('llvm', version : '>=7.0', required : false, static : static) + if not d.found() + error('MESON_SKIP_TEST llvm version is too low for cmake dynamic link.') + endif + endif -llvm_dep = dependency( - 'llvm', - modules : ['bitwriter', 'asmprinter', 'executionengine', 'target', - 'mcjit', 'nativecodegen', 'amdgpu'], - required : false, - static : static, - method : method, -) - -if not llvm_dep.found() - error('MESON_SKIP_TEST required llvm modules not found.') -endif + d = dependency('llvm', modules : 'not-found', required : false, static : static, method : method) + assert(d.found() == false, 'not-found llvm module found') -executable( - 'sum', - 'sum.c', - dependencies : [ - llvm_dep, dep_tinfo, - # zlib will be statically linked on windows - dependency('zlib', required : host_machine.system() != 'windows'), - meson.get_compiler('c').find_library('dl', required : false), - ] - ) + d = dependency('llvm', version : '<0.1', required : false, static : static, method : method) + assert(d.found() == false, 'ancient llvm module found') + + d = dependency('llvm', optional_modules : 'not-found', required : false, static : static, method : method) + assert(d.found() == true, 'optional module stopped llvm from being found.') + + # Check we can apply a version constraint + d = dependency('llvm', version : ['< 500', '>=@0@'.format(d.version())], required: false, static : static, method : method) + assert(d.found() == true, 'Cannot set version constraints') + + # Check if we have to get pseudo components + d = dependency('llvm', modules: ['all-targets','native','engine'], required: false, static : static, method : method) + assert(d.found() == true, 'Cannot find pseudo components') + + dep_tinfo = dependency('tinfo', required : false) + if not dep_tinfo.found() + cpp = meson.get_compiler('cpp') + dep_tinfo = cpp.find_library('tinfo', required: false) + endif + + if static and method == 'cmake' and d.version().startswith('16.0') + message('Skipping modules with cmake due to an LLVM bug, see https://github.com/mesonbuild/meson/issues/11642') + llvm_dep = dependency( + 'llvm', + required : false, + static : static, + method : method, + ) + else + llvm_dep = dependency( + 'llvm', + modules : modules_to_find, + required : false, + static : static, + method : method, + ) + endif + + if not llvm_dep.found() + error('MESON_SKIP_TEST required llvm modules not found.') + endif + + executable( + 'sum', + 'sum.c', + dependencies : [ + llvm_dep, dep_tinfo, + # zlib will be statically linked on windows + dependency('zlib', required : host_machine.system() != 'windows'), + meson.get_compiler('c').find_library('dl', required : false), + ] + ) +endif diff --git a/test cases/frameworks/15 llvm/meson_options.txt b/test cases/frameworks/15 llvm/meson_options.txt index de3d172..8730c48 100644 --- a/test cases/frameworks/15 llvm/meson_options.txt +++ b/test cases/frameworks/15 llvm/meson_options.txt @@ -1,7 +1,7 @@ option( 'method', type : 'combo', - choices : ['config-tool', 'cmake'] + choices : ['config-tool', 'cmake', 'combination'] ) option( 'link-static', diff --git a/test cases/frameworks/15 llvm/test.json b/test cases/frameworks/15 llvm/test.json index e70edd5..f9d7305 100644 --- a/test cases/frameworks/15 llvm/test.json +++ b/test cases/frameworks/15 llvm/test.json @@ -2,17 +2,18 @@ "matrix": { "options": { "method": [ - { "val": "config-tool", "skip_on_jobname": ["msys2-gcc"]}, - { "val": "cmake", "skip_on_jobname": ["msys2-gcc"] } + { "val": "config-tool", "skip_on_jobname": ["msys2-gcc"] }, + { "val": "cmake", "skip_on_jobname": ["msys2"] }, + { "val": "combination", "skip_on_jobname": ["msys2"] } ], "link-static": [ { "val": true, "skip_on_jobname": ["opensuse"] }, { "val": false } ] - }, - "exclude": [ - { "method": "cmake", "link-static": false } - ] + } }, - "skip_on_jobname": ["azure", "cygwin"] + "skip_on_jobname": ["azure", "cygwin"], + "tools": { + "cmake": ">=3.11" + } } diff --git a/test cases/frameworks/16 sdl2/meson_options.txt b/test cases/frameworks/16 sdl2/meson_options.txt index 176af17..039a3f5 100644 --- a/test cases/frameworks/16 sdl2/meson_options.txt +++ b/test cases/frameworks/16 sdl2/meson_options.txt @@ -1,6 +1,6 @@ option( 'method', type : 'combo', - choices : ['auto', 'pkg-config', 'config-tool', 'sdlconfig', 'extraframework'], + choices : ['auto', 'pkg-config', 'config-tool', 'sdlconfig', 'extraframework', 'cmake'], value : 'auto', ) diff --git a/test cases/frameworks/16 sdl2/test.json b/test cases/frameworks/16 sdl2/test.json index 57a3f21..ee4112a 100644 --- a/test cases/frameworks/16 sdl2/test.json +++ b/test cases/frameworks/16 sdl2/test.json @@ -6,7 +6,8 @@ { "val": "pkg-config" }, { "val": "config-tool" }, { "val": "sdlconfig" }, - { "val": "extraframework", "skip_on_os": ["!darwin"], "skip_on_jobname": ["macos"] } + { "val": "extraframework", "skip_on_os": ["!darwin"], "skip_on_jobname": ["macos"] }, + { "val": "cmake", "skip_on_jobname": ["bionic"] } ] } }, diff --git a/test cases/frameworks/21 libwmf/meson.build b/test cases/frameworks/21 libwmf/meson.build index 9dbab6a..5892932 100644 --- a/test cases/frameworks/21 libwmf/meson.build +++ b/test cases/frameworks/21 libwmf/meson.build @@ -10,7 +10,7 @@ libwmf_ver = libwmf_dep.version() assert(libwmf_ver.split('.').length() > 1, 'libwmf version is "@0@"'.format(libwmf_ver)) message('libwmf version is "@0@"'.format(libwmf_ver)) # Workaround for Debian bug 912563 where libwmf-devel returns cflags -# that do not not have Freetype include paths but their headers +# that do not have Freetype include paths but their headers # use them unconditionally. ft_dep = dependency('freetype2') e = executable('libwmf_prog', 'libwmf_prog.c', dependencies : [libwmf_dep, ft_dep]) diff --git a/test cases/frameworks/25 hdf5/main.cpp b/test cases/frameworks/25 hdf5/main.cpp index 477e76b..9c7f7bf 100644 --- a/test cases/frameworks/25 hdf5/main.cpp +++ b/test cases/frameworks/25 hdf5/main.cpp @@ -1,29 +1,19 @@ #include -#include "hdf5.h" +#include "H5Cpp.h" int main(void) { -herr_t ier; unsigned maj, min, rel; -ier = H5open(); -if (ier) { - std::cerr << "Unable to initialize HDF5: " << ier << std::endl; +try { + H5::H5Library::open(); + H5::H5Library::getLibVersion(maj, min, rel); + std::cout << "C++ HDF5 version " << maj << "." << min << "." << rel << std::endl; + H5::H5Library::close(); + return EXIT_SUCCESS; +} catch (H5::LibraryIException &e) { + std::cerr << "Exception caught from HDF5: " << e.getDetailMsg() << std::endl; return EXIT_FAILURE; } - -ier = H5get_libversion(&maj, &min, &rel); -if (ier) { - std::cerr << "HDF5 did not initialize!" << std::endl; - return EXIT_FAILURE; -} -std::cout << "C++ HDF5 version " << maj << "." << min << "." << rel << std::endl; - -ier = H5close(); -if (ier) { - std::cerr << "Unable to close HDF5: " << ier << std::endl; - return EXIT_FAILURE; -} -return EXIT_SUCCESS; } diff --git a/test cases/frameworks/25 hdf5/meson.build b/test cases/frameworks/25 hdf5/meson.build index 0df2ffd..b9f5784 100644 --- a/test cases/frameworks/25 hdf5/meson.build +++ b/test cases/frameworks/25 hdf5/meson.build @@ -29,7 +29,7 @@ if test_fortran # Search paths don't work correctly here and -lgfortran doesn't work test_fortran = false elif host_machine.system() == 'windows' and cpp.get_id() != 'gcc' and fc.get_id() == 'gcc' - # mixing gfotran with non-gcc doesn't work on windows + # mixing gfortran with non-gcc doesn't work on windows test_fortran = false endif diff --git a/test cases/frameworks/27 gpgme/test.json b/test cases/frameworks/27 gpgme/test.json index 6ace9de..59eb0e2 100644 --- a/test cases/frameworks/27 gpgme/test.json +++ b/test cases/frameworks/27 gpgme/test.json @@ -1,3 +1,3 @@ { - "skip_on_jobname": ["azure", "cygwin", "macos", "msys2"] + "skip_on_jobname": ["azure", "cygwin", "macos", "msys2", "linux-arch", "ubuntu"] } diff --git a/test cases/frameworks/36 gtkdoc cpp/foo-docs.xml b/test cases/frameworks/36 gtkdoc cpp/foo-docs.xml new file mode 100644 index 0000000..85c673c --- /dev/null +++ b/test cases/frameworks/36 gtkdoc cpp/foo-docs.xml @@ -0,0 +1,16 @@ + + + +]> + + + Foo Reference Manual + + + + GLib Core Application Support + + + diff --git a/test cases/frameworks/36 gtkdoc cpp/foo.cpp b/test cases/frameworks/36 gtkdoc cpp/foo.cpp new file mode 100644 index 0000000..15fa269 --- /dev/null +++ b/test cases/frameworks/36 gtkdoc cpp/foo.cpp @@ -0,0 +1,5 @@ +#include "foo.h" + +int foo_do_something(void) { + return 42; +} diff --git a/test cases/frameworks/36 gtkdoc cpp/foo.h b/test cases/frameworks/36 gtkdoc cpp/foo.h new file mode 100644 index 0000000..cac03d3 --- /dev/null +++ b/test cases/frameworks/36 gtkdoc cpp/foo.h @@ -0,0 +1 @@ +int foo_do_something(void); diff --git a/test cases/frameworks/36 gtkdoc cpp/meson.build b/test cases/frameworks/36 gtkdoc cpp/meson.build new file mode 100644 index 0000000..747eae5 --- /dev/null +++ b/test cases/frameworks/36 gtkdoc cpp/meson.build @@ -0,0 +1,13 @@ +project('gnome module without C', 'cpp') + +gtkdoc = find_program('gtkdoc-scan', required: false) +if not gtkdoc.found() + error('MESON_SKIP_TEST gtkdoc not found.') +endif + +gnome = import('gnome') + +lib = library('foo++', 'foo.cpp') +gnome.gtkdoc('foo', + src_dir: '.', + main_xml : 'foo-docs.xml',) diff --git a/test cases/frameworks/36 gtkdoc cpp/test.json b/test cases/frameworks/36 gtkdoc cpp/test.json new file mode 100644 index 0000000..b2d9bc8 --- /dev/null +++ b/test cases/frameworks/36 gtkdoc cpp/test.json @@ -0,0 +1,17 @@ +{ + "installed": [ + {"type": "file", "file": "usr/share/gtk-doc/html/foo/up-insensitive.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/home.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/foo.html"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/foo-foo.html"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/style.css"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/index.html"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/foo.devhelp2"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/left.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/left-insensitive.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/right-insensitive.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/up.png"}, + {"type": "file", "file": "usr/share/gtk-doc/html/foo/right.png"} + ], + "skip_on_jobname": ["azure", "msys2"] +} diff --git a/test cases/frameworks/37 gir cpp/foo.cpp b/test cases/frameworks/37 gir cpp/foo.cpp new file mode 100644 index 0000000..15fa269 --- /dev/null +++ b/test cases/frameworks/37 gir cpp/foo.cpp @@ -0,0 +1,5 @@ +#include "foo.h" + +int foo_do_something(void) { + return 42; +} diff --git a/test cases/frameworks/37 gir cpp/foo.h b/test cases/frameworks/37 gir cpp/foo.h new file mode 100644 index 0000000..cac03d3 --- /dev/null +++ b/test cases/frameworks/37 gir cpp/foo.h @@ -0,0 +1 @@ +int foo_do_something(void); diff --git a/test cases/frameworks/37 gir cpp/meson.build b/test cases/frameworks/37 gir cpp/meson.build new file mode 100644 index 0000000..0d228b0 --- /dev/null +++ b/test cases/frameworks/37 gir cpp/meson.build @@ -0,0 +1,22 @@ +project('gnome module without C', 'cpp') + +gi = dependency('gobject-introspection-1.0', required: false) +if not gi.found() + error('MESON_SKIP_TEST gobject-introspection not found.') +endif + +if host_machine.system() == 'cygwin' + # FIXME: g-ir-scanner seems broken on cygwin: + # ERROR: can't resolve libraries to shared libraries: foo++ + error('MESON_SKIP_TEST g-ir-scanner is broken on cygwin.') +endif + +gnome = import('gnome') + +lib = library('foo++', 'foo.cpp') +gnome.generate_gir( + lib, + sources: ['foo.cpp', 'foo.h'], + namespace: 'foo', + nsversion: meson.project_version(), +) diff --git a/test cases/frameworks/37 gir cpp/test.json b/test cases/frameworks/37 gir cpp/test.json new file mode 100644 index 0000000..3641d75 --- /dev/null +++ b/test cases/frameworks/37 gir cpp/test.json @@ -0,0 +1,3 @@ +{ + "skip_on_jobname": ["azure", "macos", "msys2", "cygwin"] +} diff --git a/test cases/frameworks/4 qt/manualinclude.cpp b/test cases/frameworks/4 qt/manualinclude.cpp index 60b94e5..ef151a4 100644 --- a/test cases/frameworks/4 qt/manualinclude.cpp +++ b/test cases/frameworks/4 qt/manualinclude.cpp @@ -1,4 +1,5 @@ #include"manualinclude.h" +#include #include #include diff --git a/test cases/frameworks/4 qt/meson.build b/test cases/frameworks/4 qt/meson.build index 735b3f9..f8ba175 100644 --- a/test cases/frameworks/4 qt/meson.build +++ b/test cases/frameworks/4 qt/meson.build @@ -2,6 +2,9 @@ project('qt4, qt5, and qt6 build test', 'cpp', # Qt6 requires C++ 17 support default_options : ['cpp_std=c++17']) +# Visit the subdir before entering the loop +subdir('mocdep') + qt5_modules = ['Widgets'] qt6_modules = ['Widgets'] foreach qt : ['qt4', 'qt5', 'qt6'] @@ -47,6 +50,9 @@ foreach qt : ['qt4', 'qt5', 'qt6'] qtmodule = import(qt) assert(qtmodule.has_tools()) + # Test that fetching a variable works and yields a non-empty value + assert(qtdep.get_variable('prefix', configtool: 'QT_INSTALL_PREFIX') != '') + # The following has two resource files because having two in one target # requires you to do it properly or you get linker symbol clashes. @@ -65,7 +71,7 @@ foreach qt : ['qt4', 'qt5', 'qt6'] extra_cpp_args = [] if meson.is_unity() extra_cpp_args += '-DUNITY_BUILD' - prep_rcc = qtmodule.preprocess(qt + '_unity_ressource', qresources : ['stuff.qrc', 'stuff2.qrc'], method : get_option('method')) + prep_rcc = qtmodule.preprocess(qt + '_unity_resource', qresources : ['stuff.qrc', 'stuff2.qrc'], method : get_option('method')) else prep_rcc = qtmodule.preprocess(qresources : ['stuff.qrc', 'stuff2.qrc'], method : get_option('method')) endif @@ -114,17 +120,21 @@ foreach qt : ['qt4', 'qt5', 'qt6'] moc_extra_arguments : ['-DMOC_EXTRA_FLAG'], # This is just a random macro to test `extra_arguments` moc_sources : 'manualinclude.cpp', moc_headers : 'manualinclude.h', - method : get_option('method')) + method : get_option('method'), + dependencies: mocdep, + ) manpreprocessed = qtmodule.compile_moc( extra_args : ['-DMOC_EXTRA_FLAG'], # This is just a random macro to test `extra_arguments` sources : 'manualinclude.cpp', headers : 'manualinclude.h', - method : get_option('method')) + method : get_option('method'), + dependencies: mocdep, + ) qtmaninclude = executable(qt + 'maninclude', sources : ['manualinclude.cpp', manpreprocessed], - dependencies : qtcore) + dependencies : [qtcore, mocdep]) test(qt + 'maninclude', qtmaninclude) @@ -152,7 +162,8 @@ foreach qt : ['qt4', 'qt5', 'qt6'] endif # Check we can apply a version constraint - dependency(qt, modules: qt_modules, version: '>=@0@'.format(qtdep.version()), method : get_option('method')) + accept_versions = ['>=@0@'.format(qtdep.version()), '<@0@'.format(qtdep.version()[0].to_int() + 1)] + dependency(qt, modules: qt_modules, version: accept_versions, method : get_option('method')) endif endforeach diff --git a/test cases/frameworks/4 qt/mocdep/meson.build b/test cases/frameworks/4 qt/mocdep/meson.build new file mode 100644 index 0000000..174b426 --- /dev/null +++ b/test cases/frameworks/4 qt/mocdep/meson.build @@ -0,0 +1,3 @@ +mocdep = declare_dependency( + include_directories: include_directories('.'), +) diff --git a/test cases/frameworks/4 qt/mocdep/mocdep.h b/test cases/frameworks/4 qt/mocdep/mocdep.h new file mode 100644 index 0000000..357a52c --- /dev/null +++ b/test cases/frameworks/4 qt/mocdep/mocdep.h @@ -0,0 +1 @@ +#define MOC_DEP 1 diff --git a/test cases/frameworks/4 qt/subfolder/meson.build b/test cases/frameworks/4 qt/subfolder/meson.build index f1b84e6..652d147 100644 --- a/test cases/frameworks/4 qt/subfolder/meson.build +++ b/test cases/frameworks/4 qt/subfolder/meson.build @@ -23,7 +23,7 @@ rc_file = configure_file( extra_cpp_args = [] if meson.is_unity() extra_cpp_args += '-DUNITY_BUILD' - qresources = qtmodule.preprocess(qt + '_subfolder_unity_ressource',qresources : ['resources/stuff3.qrc', rc_file]) + qresources = qtmodule.preprocess(qt + '_subfolder_unity_resource',qresources : ['resources/stuff3.qrc', rc_file]) else qresources = qtmodule.preprocess(qresources : ['resources/stuff3.qrc', rc_file]) endif diff --git a/test cases/frameworks/5 protocol buffers/meson.build b/test cases/frameworks/5 protocol buffers/meson.build index 046847a..9a4154b 100644 --- a/test cases/frameworks/5 protocol buffers/meson.build +++ b/test cases/frameworks/5 protocol buffers/meson.build @@ -1,4 +1,4 @@ -project('protocol buffer test', 'cpp', default_options: ['cpp_std=c++11']) +project('protocol buffer test', 'cpp', default_options: ['cpp_std=c++14']) protoc = find_program('protoc', required : false) dep = dependency('protobuf', required : false) diff --git a/test cases/frameworks/7 gnome/gir/copy.py b/test cases/frameworks/7 gnome/gir/copy.py index fa70145..e92deaf 100755 --- a/test cases/frameworks/7 gnome/gir/copy.py +++ b/test cases/frameworks/7 gnome/gir/copy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 -# Copyright © 2021 Intel Corproation +# Copyright © 2021 Intel Corporation import argparse import shutil diff --git a/test cases/frameworks/7 gnome/gir/dep1/dep1.h b/test cases/frameworks/7 gnome/gir/dep1/dep1.h index 92fc44c..5e6591b 100644 --- a/test cases/frameworks/7 gnome/gir/dep1/dep1.h +++ b/test cases/frameworks/7 gnome/gir/dep1/dep1.h @@ -1,8 +1,8 @@ #ifndef MESON_DEP1_H #define MESON_DEP1_H -#if !defined (MESON_TEST) -#error "MESON_TEST not defined." +#if !defined (MESON_TEST_1) +#error "MESON_TEST_1 not defined." #endif #include diff --git a/test cases/frameworks/7 gnome/gir/dep1/dep2/dep2.h b/test cases/frameworks/7 gnome/gir/dep1/dep2/dep2.h index 0afea90..128b243 100644 --- a/test cases/frameworks/7 gnome/gir/dep1/dep2/dep2.h +++ b/test cases/frameworks/7 gnome/gir/dep1/dep2/dep2.h @@ -1,8 +1,8 @@ #ifndef MESON_DEP2_H #define MESON_DEP2_H -#if !defined (MESON_TEST) -#error "MESON_TEST not defined." +#if !defined (MESON_TEST_1) +#error "MESON_TEST_1 not defined." #endif #include diff --git a/test cases/frameworks/7 gnome/gir/dep1/dep3/dep3.h b/test cases/frameworks/7 gnome/gir/dep1/dep3/dep3.h index 9883d76..4392bad 100644 --- a/test cases/frameworks/7 gnome/gir/dep1/dep3/dep3.h +++ b/test cases/frameworks/7 gnome/gir/dep1/dep3/dep3.h @@ -1,8 +1,8 @@ #ifndef MESON_DEP3_H #define MESON_DEP3_H -#if !defined (MESON_TEST) -#error "MESON_TEST not defined." +#if !defined (MESON_TEST_1) +#error "MESON_TEST_1 not defined." #endif #include diff --git a/test cases/frameworks/7 gnome/gir/meson-sample.h b/test cases/frameworks/7 gnome/gir/meson-sample.h index 04e79b8..7468130 100644 --- a/test cases/frameworks/7 gnome/gir/meson-sample.h +++ b/test cases/frameworks/7 gnome/gir/meson-sample.h @@ -1,8 +1,12 @@ #ifndef MESON_SAMPLE_H #define MESON_SAMPLE_H -#if !defined (MESON_TEST) -#error "MESON_TEST not defined." +#if !defined (MESON_TEST_1) +#error "MESON_TEST_1 not defined." +#endif + +#if !defined (MESON_TEST_2) +#error "MESON_TEST_2 not defined." #endif #include diff --git a/test cases/frameworks/7 gnome/gir/meson-sample2.h b/test cases/frameworks/7 gnome/gir/meson-sample2.h index d39084e..f3bcd39 100644 --- a/test cases/frameworks/7 gnome/gir/meson-sample2.h +++ b/test cases/frameworks/7 gnome/gir/meson-sample2.h @@ -1,8 +1,8 @@ #ifndef MESON_SAMPLE2_H #define MESON_SAMPLE2_H -#if !defined (MESON_TEST) -#error "MESON_TEST not defined." +#if !defined (MESON_TEST_1) +#error "MESON_TEST_1 not defined." #endif #include diff --git a/test cases/frameworks/7 gnome/gir/meson.build b/test cases/frameworks/7 gnome/gir/meson.build index fbff206..d2ceaee 100644 --- a/test cases/frameworks/7 gnome/gir/meson.build +++ b/test cases/frameworks/7 gnome/gir/meson.build @@ -4,7 +4,7 @@ libsources = ['meson-sample.c', 'meson-sample.h'] lib2sources = ['meson-sample2.c', 'meson-sample2.h'] gen_source = custom_target( - 'meson_smaple3.h', + 'meson_sample3.h', input : 'meson-sample.h', output : 'meson-sample3.h', command : [find_program('copy.py'), '@INPUT@', '@OUTPUT@'], @@ -14,6 +14,7 @@ gen_source = custom_target( girlib = shared_library( 'gir_lib', sources : libsources, + c_args: '-DMESON_TEST_2', dependencies : [gobj, dep1_dep], install : true ) @@ -28,6 +29,7 @@ girlib2 = shared_library( girexe = executable( 'girprog', sources : 'prog.c', + c_args: '-DMESON_TEST_2', dependencies : [glib, gobj, gir, dep1_dep], link_with : girlib ) @@ -37,6 +39,7 @@ fake_dep = dependency('no-way-this-exists', required: false) gnome.generate_gir( girlib, girlib2, sources : [libsources, lib2sources, gen_source], + env : {'CPPFLAGS': '-DMESON_TEST_2'}, nsversion : '1.0', namespace : 'Meson', symbol_prefix : 'meson', diff --git a/test cases/frameworks/7 gnome/meson.build b/test cases/frameworks/7 gnome/meson.build index 5f438cb..4d54e77 100644 --- a/test cases/frameworks/7 gnome/meson.build +++ b/test cases/frameworks/7 gnome/meson.build @@ -23,7 +23,7 @@ endif cc = meson.get_compiler('c') -add_global_arguments('-DMESON_TEST', language : 'c') +add_global_arguments('-DMESON_TEST_1', language : 'c') if cc.get_id() == 'intel' # Ignore invalid GCC pragma warnings from glib # https://bugzilla.gnome.org/show_bug.cgi?id=776562 @@ -51,6 +51,15 @@ gobj = dependency('gobject-2.0') gir = dependency('gobject-introspection-1.0') gmod = dependency('gmodule-2.0') +# GLib >= 2.76 removed slice allocator which causes a leak in g-i to now be +# visible to asan. The leak should be fixed in g-i >= 1.76.2: +# https://gitlab.gnome.org/GNOME/gobject-introspection/-/merge_requests/411 +if get_option('b_sanitize') != 'none' and \ + gir.version().version_compare('<=1.76.1') and \ + glib.version().version_compare('>=2.76') + error('MESON_SKIP_TEST gobject-introspection >=1.76.2 is required with address sanitizer.') +endif + # Test that static deps don't error out when static libraries aren't found glib_static = dependency('glib-2.0', static : true) diff --git a/test cases/frameworks/7 gnome/mkenums/meson.build b/test cases/frameworks/7 gnome/mkenums/meson.build index 4cf4dcf..2886be9 100644 --- a/test cases/frameworks/7 gnome/mkenums/meson.build +++ b/test cases/frameworks/7 gnome/mkenums/meson.build @@ -162,3 +162,13 @@ main = configure_file( enumexe6 = executable('enumprog6', main, enums_c2, enums_h6, dependencies : gobj) test('enum test 4', enumexe6) + +# Test with headers coming from other directories +# https://github.com/mesonbuild/meson/pull/10855 +subdir('subdir') +enums7 = gnome.mkenums_simple('enums7', sources: ['meson-sample.h', h2, h3]) +main = configure_file( + input : 'main.c', + output : 'mai7.c', + configuration : {'ENUM_FILE': 'enums7.h'}) +test('enums7 test', executable('enumprog7', main, enums7, dependencies : gobj)) diff --git a/test cases/frameworks/7 gnome/mkenums/subdir/h2.h.in b/test cases/frameworks/7 gnome/mkenums/subdir/h2.h.in new file mode 100644 index 0000000..7b40c4b --- /dev/null +++ b/test cases/frameworks/7 gnome/mkenums/subdir/h2.h.in @@ -0,0 +1,5 @@ +#pragma once + +typedef enum { + MESON_SUBDIR_FOO, +} MesonSubdir; diff --git a/test cases/frameworks/7 gnome/mkenums/subdir/h3.h b/test cases/frameworks/7 gnome/mkenums/subdir/h3.h new file mode 100644 index 0000000..9049d5c --- /dev/null +++ b/test cases/frameworks/7 gnome/mkenums/subdir/h3.h @@ -0,0 +1,5 @@ +#pragma once + +typedef enum { + MESON_SUBDIR2_FOO, +} MesonSubdir2; diff --git a/test cases/frameworks/7 gnome/mkenums/subdir/meson.build b/test cases/frameworks/7 gnome/mkenums/subdir/meson.build new file mode 100644 index 0000000..0b03846 --- /dev/null +++ b/test cases/frameworks/7 gnome/mkenums/subdir/meson.build @@ -0,0 +1,2 @@ +h2 = configure_file(input: 'h2.h.in', output: 'h2.h', copy: true) +h3 = files('h3.h') diff --git a/test cases/frameworks/7 gnome/test.json b/test cases/frameworks/7 gnome/test.json index 28ce9fe..f75ba13 100644 --- a/test cases/frameworks/7 gnome/test.json +++ b/test cases/frameworks/7 gnome/test.json @@ -36,5 +36,5 @@ {"type": "file", "file": "usr/include/simple-resources.h"}, {"type": "file", "file": "usr/include/generated-gdbus.h"} ], - "skip_on_jobname": ["azure", "cygwin", "macos", "msys2"] + "skip_on_jobname": ["azure", "cygwin", "macos", "msys2", "pypy"] } diff --git a/test cases/java/3 args/meson.build b/test cases/java/3 args/meson.build index d13573f..9dfb427 100644 --- a/test cases/java/3 args/meson.build +++ b/test cases/java/3 args/meson.build @@ -1,8 +1,8 @@ project('simplejava', 'java') -add_project_arguments('-target', '1.7', language : 'java') +add_project_arguments('-target', '1.8', language : 'java') javaprog = jar('myprog', 'com/mesonbuild/Simple.java', main_class : 'com.mesonbuild.Simple', - java_args : ['-source', '1.7']) + java_args : ['-source', '1.8']) test('mytest', javaprog) diff --git a/test cases/java/8 codegen custom target/com/mesonbuild/meson.build b/test cases/java/8 codegen custom target/com/mesonbuild/meson.build index 0309941..5188f0a 100644 --- a/test cases/java/8 codegen custom target/com/mesonbuild/meson.build +++ b/test cases/java/8 codegen custom target/com/mesonbuild/meson.build @@ -1,4 +1,4 @@ -python = import('python').find_installation('python3') +python = find_program('python3') config_file = custom_target('confgen', input : 'Config.java.in', diff --git a/test cases/linuxlike/1 pkg-config/meson.build b/test cases/linuxlike/1 pkg-config/meson.build index ca48e9b..b09630a 100644 --- a/test cases/linuxlike/1 pkg-config/meson.build +++ b/test cases/linuxlike/1 pkg-config/meson.build @@ -22,6 +22,13 @@ assert(dep.get_pkgconfig_variable('nonexisting') == '', 'Value of unknown variab assert(dep.get_pkgconfig_variable('nonexisting', default: 'foo') == 'foo', 'Value of unknown variable is not defaulted.') # pkg-config is able to replace variables assert(dep.get_pkgconfig_variable('prefix', define_variable: ['prefix', '/tmp']) == '/tmp', 'prefix variable has not been replaced.') +assert(dep.get_variable('prefix', pkgconfig_define: ['prefix', '/tmp']) == '/tmp', 'prefix variable has not been replaced.') +# pkg-config can replace multiple variables at once +assert(dep.get_variable('prefix', pkgconfig_define: ['prefix', '/tmp', 'libdir', '/bad/libdir']) == '/tmp', 'first variable has not been replaced.') +assert(dep.get_variable('prefix', pkgconfig_define: ['libdir', '/bad/libdir', 'prefix', '/tmp']) == '/tmp', 'second variable has not been replaced.') +assert(dep.get_pkgconfig_variable('prefix', define_variable: ['prefix', '/tmp', 'libdir', '/bad/libdir']) == '/tmp', 'first variable has not been replaced.') +assert(dep.get_pkgconfig_variable('prefix', define_variable: ['libdir', '/bad/libdir', 'prefix', '/tmp']) == '/tmp', 'second variable has not been replaced.') + # Test that dependencies of dependencies work. dep2 = declare_dependency(dependencies : dep) diff --git a/test cases/linuxlike/11 runpath rpath ldlibrarypath/main.c b/test cases/linuxlike/11 runpath rpath ldlibrarypath/main.c index 5009531..b5e7b30 100644 --- a/test cases/linuxlike/11 runpath rpath ldlibrarypath/main.c +++ b/test cases/linuxlike/11 runpath rpath ldlibrarypath/main.c @@ -2,7 +2,7 @@ int some_symbol (void); -int main () { +int main (void) { int ret = some_symbol (); if (ret == 1) return 0; diff --git a/test cases/linuxlike/11 runpath rpath ldlibrarypath/meson.build b/test cases/linuxlike/11 runpath rpath ldlibrarypath/meson.build index a3103ac..b49da66 100644 --- a/test cases/linuxlike/11 runpath rpath ldlibrarypath/meson.build +++ b/test cases/linuxlike/11 runpath rpath ldlibrarypath/meson.build @@ -1,7 +1,5 @@ project('runpath rpath ldlibrarypath', 'c') -error('MESON_SKIP_TEST test disabled due to bug #1635.') - libsrc = files('lib.c') subdir('lib1') diff --git a/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake b/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake index 4b3f814..2d8318f 100644 --- a/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake +++ b/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake @@ -5,7 +5,7 @@ if(ZLIB_FOUND OR ZLIB_Found) set(cmMesonTestF1_LIBRARIES general ${ZLIB_LIBRARY}) set(cmMesonTestF1_INCLUDE_DIRS ${ZLIB_INCLUDE_DIR}) - add_library(CMMesonTESTf1::evil_non_standard_trget UNKNOWN IMPORTED) + add_library(CMMesonTESTf1::evil_non_standard_target UNKNOWN IMPORTED) else() set(cmMesonTestF1_FOUND OFF) endif() diff --git a/test cases/linuxlike/13 cmake dependency/meson.build b/test cases/linuxlike/13 cmake dependency/meson.build index 193ad18..f612e1d 100644 --- a/test cases/linuxlike/13 cmake dependency/meson.build +++ b/test cases/linuxlike/13 cmake dependency/meson.build @@ -2,13 +2,20 @@ # due to use of setup_env.json project('external CMake dependency', ['c', 'cpp']) -if not find_program('cmake', required: false).found() +cmake = find_program('cmake', required: false) +if not cmake.found() error('MESON_SKIP_TEST cmake binary not available.') endif # Zlib is probably on all dev machines. dep = dependency('ZLIB', version : '>=1.2', method : 'cmake') + +if '#define' in dep.version() and cmake.version().version_compare('< 3.27.4') + # ZLIB 1.3 version is broken with those cmake versions + error('MESON_SKIP_TEST known bug in cmake (https://gitlab.kitware.com/cmake/cmake/-/issues/25200)') +endif + exe = executable('zlibprog', 'prog-checkver.c', dependencies : dep, c_args : '-DFOUND_ZLIB="' + dep.version() + '"') diff --git a/test cases/linuxlike/13 cmake dependency/test.json b/test cases/linuxlike/13 cmake dependency/test.json index 1505986..484ce20 100644 --- a/test cases/linuxlike/13 cmake dependency/test.json +++ b/test cases/linuxlike/13 cmake dependency/test.json @@ -8,7 +8,7 @@ "line": "WARNING: Could not find and exact match for the CMake dependency cmMesonTestF1." }, { - "line": " ['CMMesonTESTf1::evil_non_standard_trget']" + "line": " ['CMMesonTESTf1::evil_non_standard_target']" } ] } diff --git a/test cases/linuxlike/3 linker script/meson.build b/test cases/linuxlike/3 linker script/meson.build index 5901bf7..6608587 100644 --- a/test cases/linuxlike/3 linker script/meson.build +++ b/test cases/linuxlike/3 linker script/meson.build @@ -22,7 +22,7 @@ m = configure_file( output : 'bob-conf.map', configuration : conf, ) -vflag = '-Wl,--version-script,@0@'.format(m) +vflag = '-Wl,--version-script,@0@'.format(meson.current_build_dir() / 'bob-conf.map') l = shared_library('bob-conf', 'bob.c', link_args : vflag, link_depends : m) e = executable('prog-conf', 'prog.c', link_with : l) @@ -43,14 +43,6 @@ l = shared_library('bob-ct', ['bob.c', m], link_args : vflag, link_depends : m) e = executable('prog-ct', 'prog.c', link_with : l) test('core', e) -# File -mapfile = files('bob.map') -vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), mapfile[0]) - -l = shared_library('bob-files', 'bob.c', link_args : vflag, link_depends : mapfile) -e = executable('prog-files', 'prog.c', link_with : l) -test('core', e) - subdir('sub') # With map file in subdir diff --git a/test cases/linuxlike/5 dependency versions/meson.build b/test cases/linuxlike/5 dependency versions/meson.build index 164e679..4038ee9 100644 --- a/test cases/linuxlike/5 dependency versions/meson.build +++ b/test cases/linuxlike/5 dependency versions/meson.build @@ -82,7 +82,7 @@ assert(fakezlib_dep.type_name() == 'internal', 'fakezlib_dep should be of type " # Verify that once we got a system dependency, we won't fallback if a newer # version is requested. d = dependency('zlib', version: '>= 999', - fallback : ['donotexist', 'fakezlib_dep'], + fallback : ['nonexistent', 'fakezlib_dep'], required: false) assert(not d.found(), 'version should not match and it should not fallback') diff --git a/test cases/nasm/2 asm language/meson.build b/test cases/nasm/2 asm language/meson.build index 21787ff..d025d43 100644 --- a/test cases/nasm/2 asm language/meson.build +++ b/test cases/nasm/2 asm language/meson.build @@ -5,6 +5,16 @@ if not host_machine.cpu_family().startswith('x86') error('MESON_SKIP_TEST: nasm only supported for x86 and x86_64') endif +if host_machine.system() == 'windows' + error('MESON_SKIP_TEST: this test asm is not made for Windows') +elif host_machine.system() == 'sunos' + error('MESON_SKIP_TEST: this test asm is not made for Solaris or illumos') +endif + +if meson.backend().startswith('vs') + error('MESON_SKIP_TEST: VS backend does not recognise NASM yet') +endif + if not add_languages('nasm', required: false) nasm = find_program('nasm', 'yasm', required: false) assert(not nasm.found()) @@ -43,3 +53,15 @@ exe = executable('hello', 'hello.asm', link_args: link_args, ) test('hello', exe) + +#Test whether pthread dependency gets filtered out +threads = dependency('threads') + +exe2 = executable('hello_w_threads', 'hello.asm', + config_file, + nasm_args: '-DFOO', + link_args: link_args, + dependencies: [threads] +) + +test('hello_w_threads', exe2) diff --git a/test cases/nasm/3 nasm only/dummy.asm b/test cases/nasm/3 nasm only/dummy.asm new file mode 100644 index 0000000..92c86b0 --- /dev/null +++ b/test cases/nasm/3 nasm only/dummy.asm @@ -0,0 +1,4 @@ +global dummy +section .rdata align=16 +dummy: + dd 0x00010203 diff --git a/test cases/nasm/3 nasm only/dummy.def b/test cases/nasm/3 nasm only/dummy.def new file mode 100644 index 0000000..8f8eb99 --- /dev/null +++ b/test cases/nasm/3 nasm only/dummy.def @@ -0,0 +1,2 @@ +EXPORTS + dummy diff --git a/test cases/nasm/3 nasm only/meson.build b/test cases/nasm/3 nasm only/meson.build new file mode 100644 index 0000000..9777291 --- /dev/null +++ b/test cases/nasm/3 nasm only/meson.build @@ -0,0 +1,17 @@ +project('nasm only') + +if not add_languages('nasm', required: false) + error('MESON_SKIP_TEST: nasm not found') +endif + +if meson.backend().startswith('vs') + error('MESON_SKIP_TEST: VS backend does not recognise NASM yet') +endif + +sources = files('dummy.asm') + +dummy = library( + 'dummy', + sources, + vs_module_defs: 'dummy.def', +) diff --git a/test cases/native/2 global arg/prog.c b/test cases/native/2 global arg/prog.c index 2a71236..c70a669 100644 --- a/test cases/native/2 global arg/prog.c +++ b/test cases/native/2 global arg/prog.c @@ -11,7 +11,7 @@ #endif #if !defined(GLOBAL_HOST) && !defined(GLOBAL_BUILD) - #error "Neither global_host nor glogal_build is set." + #error "Neither global_host nor global_build is set." #endif #if defined(GLOBAL_HOST) && defined(GLOBAL_BUILD) diff --git a/test cases/objc/2 nsstring/meson.build b/test cases/objc/2 nsstring/meson.build index 94d2cf1..2c483d5 100644 --- a/test cases/objc/2 nsstring/meson.build +++ b/test cases/objc/2 nsstring/meson.build @@ -1,7 +1,7 @@ project('nsstring', 'objc') if host_machine.system() == 'darwin' - dep = dependency('appleframeworks', modules : 'foundation') + dep = dependency('appleframeworks', modules : 'Foundation') elif host_machine.system() == 'cygwin' error('MESON_SKIP_TEST GNUstep is not packaged for Cygwin.') else diff --git a/test cases/osx/5 extra frameworks/meson.build b/test cases/osx/5 extra frameworks/meson.build index 0bd2c17..f6c01e6 100644 --- a/test cases/osx/5 extra frameworks/meson.build +++ b/test cases/osx/5 extra frameworks/meson.build @@ -1,10 +1,14 @@ project('xcode extra framework test', 'c') -dep_libs = dependency('OpenGL', method : 'extraframework') -assert(dep_libs.type_name() == 'extraframeworks', 'type_name is ' + dep_libs.type_name()) +opengl_dep = dependency('OpenGL', method : 'extraframework') +assert(opengl_dep.type_name() == 'extraframeworks', 'type_name is ' + opengl_dep.type_name()) dep_main = dependency('Foundation') assert(dep_main.type_name() == 'extraframeworks', 'type_name is ' + dep_main.type_name()) -stlib = static_library('stat', 'stat.c', install : true, dependencies: dep_libs) +# https://github.com/mesonbuild/meson/issues/10002 +ldap_dep = dependency('ldap', method : 'extraframework') +assert(ldap_dep.type_name() == 'extraframeworks', 'type_name is ' + ldap_dep.type_name()) + +stlib = static_library('stat', 'stat.c', install : true, dependencies: [opengl_dep, ldap_dep]) exe = executable('prog', 'prog.c', install : true, dependencies: dep_main) diff --git a/test cases/osx/5 extra frameworks/stat.c b/test cases/osx/5 extra frameworks/stat.c index 4825cef..79a3974 100644 --- a/test cases/osx/5 extra frameworks/stat.c +++ b/test cases/osx/5 extra frameworks/stat.c @@ -1 +1,4 @@ +// https://github.com/mesonbuild/meson/issues/10002 +#include + int func(void) { return 933; } diff --git a/test cases/osx/6 multiframework/meson.build b/test cases/osx/6 multiframework/meson.build index 2884624..57e5d61 100644 --- a/test cases/osx/6 multiframework/meson.build +++ b/test cases/osx/6 multiframework/meson.build @@ -4,7 +4,7 @@ project('multiframework', 'objc') # that causes a build failure when defining two modules. The # arguments for the latter module overwrote the arguments for # the first one rather than adding to them. -cocoa_dep = dependency('appleframeworks', modules : ['AppKit', 'foundation']) +cocoa_dep = dependency('appleframeworks', modules : ['AppKit', 'Foundation']) executable('deptester', 'main.m', diff --git a/test cases/osx/9 global variable ar/libfile.c b/test cases/osx/9 global variable ar/libfile.c new file mode 100644 index 0000000..b258d7b --- /dev/null +++ b/test cases/osx/9 global variable ar/libfile.c @@ -0,0 +1,9 @@ +// Source: https://lists.gnu.org/archive/html/libtool/2002-07/msg00025.html + +#include + +extern int l2; +void l1(void) +{ + printf("l1 %d\n", l2); +} diff --git a/test cases/osx/9 global variable ar/libfile2.c b/test cases/osx/9 global variable ar/libfile2.c new file mode 100644 index 0000000..1499c4d --- /dev/null +++ b/test cases/osx/9 global variable ar/libfile2.c @@ -0,0 +1,7 @@ +// Source: https://lists.gnu.org/archive/html/libtool/2002-07/msg00025.html + +int l2; +void l2_func(void) +{ + l2 = 77; +} diff --git a/test cases/osx/9 global variable ar/meson.build b/test cases/osx/9 global variable ar/meson.build new file mode 100644 index 0000000..313dd1b --- /dev/null +++ b/test cases/osx/9 global variable ar/meson.build @@ -0,0 +1,6 @@ +# Source: https://lists.gnu.org/archive/html/libtool/2002-07/msg00025.html + +project('global variable test', 'c') + +lib = static_library('mylib', 'libfile.c', 'libfile2.c') +test('global variable', executable('prog', 'prog.c', link_with: lib)) diff --git a/test cases/osx/9 global variable ar/nativefile.ini b/test cases/osx/9 global variable ar/nativefile.ini new file mode 100644 index 0000000..4fb5e7f --- /dev/null +++ b/test cases/osx/9 global variable ar/nativefile.ini @@ -0,0 +1,2 @@ +[binaries] +ar = 'ar' diff --git a/test cases/osx/9 global variable ar/prog.c b/test cases/osx/9 global variable ar/prog.c new file mode 100644 index 0000000..4665016 --- /dev/null +++ b/test cases/osx/9 global variable ar/prog.c @@ -0,0 +1,7 @@ +// Source: https://lists.gnu.org/archive/html/libtool/2002-07/msg00025.html + +extern void l1(void); +int main(void) +{ + l1(); +} diff --git a/test cases/python/1 basic/meson.build b/test cases/python/1 basic/meson.build index 2e543dd..481a881 100644 --- a/test cases/python/1 basic/meson.build +++ b/test cases/python/1 basic/meson.build @@ -8,6 +8,9 @@ if py_version.version_compare('< 3.2') error('MESON_SKIP_TEST python 3 required for tests') endif +py_full_version = py.version() +message(f'Using python version: @py_full_version@') + py_purelib = py.get_path('purelib') if not (py_purelib.endswith('site-packages') or py_purelib.endswith('dist-packages')) error('Python3 purelib path seems invalid? ' + py_purelib) diff --git a/test cases/python/10 extmodule limited api disabled/meson.build b/test cases/python/10 extmodule limited api disabled/meson.build new file mode 100644 index 0000000..42cd618 --- /dev/null +++ b/test cases/python/10 extmodule limited api disabled/meson.build @@ -0,0 +1,10 @@ +project('Python limited api disabled', 'c', + default_options : ['buildtype=release', 'werror=true', 'python.allow_limited_api=false']) + +py_mod = import('python') +py = py_mod.find_installation() + +module = py.extension_module('my_module', + 'module.c', + limited_api: '3.7', +) diff --git a/test cases/python/10 extmodule limited api disabled/module.c b/test cases/python/10 extmodule limited api disabled/module.c new file mode 100644 index 0000000..a5d3a87 --- /dev/null +++ b/test cases/python/10 extmodule limited api disabled/module.c @@ -0,0 +1,17 @@ +#include + +#if defined(Py_LIMITED_API) +#error "Py_LIMITED_API's definition by Meson should have been disabled." +#endif + +static struct PyModuleDef my_module = { + PyModuleDef_HEAD_INIT, + "my_module", + NULL, + -1, + NULL +}; + +PyMODINIT_FUNC PyInit_my_module(void) { + return PyModule_Create(&my_module); +} diff --git a/test cases/python/2 extmodule/blaster.py.in b/test cases/python/2 extmodule/blaster.py.in index b690b40..c93026f 100755 --- a/test cases/python/2 extmodule/blaster.py.in +++ b/test cases/python/2 extmodule/blaster.py.in @@ -8,4 +8,4 @@ if not isinstance(result, int): raise SystemExit('Returned result not an integer.') if result != 1: - raise SystemExit(f'Returned result {result} is not 1.') + raise SystemExit('Returned result {} is not 1.'.format(result)) diff --git a/test cases/python/2 extmodule/ext/meson.build b/test cases/python/2 extmodule/ext/meson.build index 14fa94a..0fba9f5 100644 --- a/test cases/python/2 extmodule/ext/meson.build +++ b/test cases/python/2 extmodule/ext/meson.build @@ -4,6 +4,12 @@ pylib = py.extension_module('tachyon', install: true, ) +pylib2 = py2.extension_module('tachyon', + 'tachyon_module.c', + c_args: '-DMESON_MODULENAME="tachyon"', + install: true, +) + subdir('nested') subdir('wrongdir') pypathdir = meson.current_build_dir() diff --git a/test cases/python/2 extmodule/ext/nested/meson.build b/test cases/python/2 extmodule/ext/nested/meson.build index 34c1192..4a7ec76 100644 --- a/test cases/python/2 extmodule/ext/nested/meson.build +++ b/test cases/python/2 extmodule/ext/nested/meson.build @@ -13,3 +13,20 @@ py.install_sources( pure: false, subdir: 'nested', ) + + +py2.extension_module('tachyon', + '../tachyon_module.c', + c_args: '-DMESON_MODULENAME="nested.tachyon"', + install: true, + subdir: 'nested' +) +py2.install_sources( + configure_file( + input: '../../blaster.py.in', + output: 'blaster.py', + configuration: {'tachyon_module': 'nested.tachyon'} + ), + pure: false, + subdir: 'nested', +) diff --git a/test cases/python/2 extmodule/ext/tachyon_module.c b/test cases/python/2 extmodule/ext/tachyon_module.c index a5d7cdc..68eda53 100644 --- a/test cases/python/2 extmodule/ext/tachyon_module.c +++ b/test cases/python/2 extmodule/ext/tachyon_module.c @@ -1,5 +1,5 @@ /* - Copyright 2016 The Meson development team + Copyright 2018 The Meson development team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,7 +27,11 @@ static PyObject* phaserize(PyObject *self, PyObject *args) { return NULL; result = strcmp(message, "shoot") ? 0 : 1; +#if PY_VERSION_HEX < 0x03000000 + return PyInt_FromLong(result); +#else return PyLong_FromLong(result); +#endif } static PyMethodDef TachyonMethods[] = { @@ -36,9 +40,14 @@ static PyMethodDef TachyonMethods[] = { {NULL, NULL, 0, NULL} }; +#if PY_VERSION_HEX < 0x03000000 +PyMODINIT_FUNC inittachyon(void) { + Py_InitModule("tachyon", TachyonMethods); +} +#else static struct PyModuleDef tachyonmodule = { PyModuleDef_HEAD_INIT, - MESON_MODULENAME, + "tachyon", NULL, -1, TachyonMethods @@ -47,3 +56,4 @@ static struct PyModuleDef tachyonmodule = { PyMODINIT_FUNC PyInit_tachyon(void) { return PyModule_Create(&tachyonmodule); } +#endif diff --git a/test cases/python/2 extmodule/ext/wrongdir/meson.build b/test cases/python/2 extmodule/ext/wrongdir/meson.build index 5074701..79b13eb 100644 --- a/test cases/python/2 extmodule/ext/wrongdir/meson.build +++ b/test cases/python/2 extmodule/ext/wrongdir/meson.build @@ -4,3 +4,9 @@ py.extension_module('tachyon', install: true, install_dir: get_option('libdir') ) +py2.extension_module('tachyon', + '../tachyon_module.c', + c_args: '-DMESON_MODULENAME="tachyon"', + install: true, + install_dir: get_option('libdir') +) diff --git a/test cases/python/2 extmodule/meson.build b/test cases/python/2 extmodule/meson.build index 239492c..65f9177 100644 --- a/test cases/python/2 extmodule/meson.build +++ b/test cases/python/2 extmodule/meson.build @@ -1,5 +1,5 @@ project('Python extension module', 'c', - default_options : ['buildtype=release']) + default_options : ['buildtype=release', 'werror=true', 'python.bytecompile=-1']) # Because Windows Python ships only with optimized libs, # we must build this project the same way. @@ -10,6 +10,7 @@ endif py_mod = import('python') py = py_mod.find_installation() +py2 = py_mod.find_installation('python2', required: get_option('python2'), disabler: true) py_dep = py.dependency(required: false) if not py_dep.found() @@ -31,6 +32,12 @@ test('extmod', py.install_sources(blaster, pure: false) py.install_sources(blaster, subdir: 'pure') +install_subdir('subinst', install_dir: py.get_install_dir(pure: false)) + +py2.install_sources(blaster, pure: false) +py2.install_sources(blaster, subdir: 'pure') +install_subdir('subinst', install_dir: py2.get_install_dir(pure: false)) + py3_pkg_dep = dependency('python3', method: 'pkg-config', required : false) if py3_pkg_dep.found() diff --git a/test cases/python/2 extmodule/meson_options.txt b/test cases/python/2 extmodule/meson_options.txt new file mode 100644 index 0000000..76d3b67 --- /dev/null +++ b/test cases/python/2 extmodule/meson_options.txt @@ -0,0 +1 @@ +option('python2', type: 'feature', value: 'disabled') diff --git a/test cases/python/2 extmodule/subinst/printer.py b/test cases/python/2 extmodule/subinst/printer.py new file mode 100755 index 0000000..8bc528d --- /dev/null +++ b/test cases/python/2 extmodule/subinst/printer.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +print('subinst') diff --git a/test cases/python/2 extmodule/subinst/submod/printer.py b/test cases/python/2 extmodule/subinst/submod/printer.py new file mode 100755 index 0000000..2a4a61b --- /dev/null +++ b/test cases/python/2 extmodule/subinst/submod/printer.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +print('subinst.submod') diff --git a/test cases/python/2 extmodule/test.json b/test cases/python/2 extmodule/test.json index 6bd1195..fcb3975 100644 --- a/test cases/python/2 extmodule/test.json +++ b/test cases/python/2 extmodule/test.json @@ -7,6 +7,8 @@ { "type": "python_file", "file": "usr/@PYTHON_PLATLIB@/nested/blaster.py" }, { "type": "python_lib", "file": "usr/@PYTHON_PLATLIB@/nested/tachyon" }, { "type": "py_implib", "file": "usr/@PYTHON_PLATLIB@/nested/tachyon" }, + { "type": "python_file", "file": "usr/@PYTHON_PLATLIB@/subinst/printer.py" }, + { "type": "python_file", "file": "usr/@PYTHON_PLATLIB@/subinst/submod/printer.py" }, { "type": "python_lib", "file": "usr/lib/tachyon" }, { "type": "py_implib", "file": "usr/lib/tachyon" } ] diff --git a/test cases/python/3 cython/libdir/meson.build b/test cases/python/3 cython/libdir/meson.build index 2b6ebc7..d148b00 100644 --- a/test cases/python/3 cython/libdir/meson.build +++ b/test cases/python/3 cython/libdir/meson.build @@ -1,11 +1,11 @@ pyx_c = custom_target('storer_pyx', output : 'storer_pyx.c', input : 'storer.pyx', - command : [cython, '@INPUT@', '-o', '@OUTPUT@'], + command : [cython, '@INPUT@', '-o', '@OUTPUT@', '-3'], ) slib = py3.extension_module('storer', 'storer.c', pyx_c, - dependencies : py3_dep) +) pydir = meson.current_build_dir() diff --git a/test cases/python/3 cython/meson.build b/test cases/python/3 cython/meson.build index 5fc07a8..8ff8d51 100644 --- a/test cases/python/3 cython/meson.build +++ b/test cases/python/3 cython/meson.build @@ -1,6 +1,6 @@ project('cython', 'c', - default_options : ['warning_level=3']) - + default_options : ['warning_level=3', 'buildtype=release'] +) if meson.backend() != 'ninja' error('MESON_SKIP_TEST: Ninja backend required') endif @@ -10,8 +10,7 @@ if not cython.found() error('MESON_SKIP_TEST: Cython3 not found.') endif -py_mod = import('python') -py3 = py_mod.find_installation() +py3 = import('python').find_installation(pure: false) py3_dep = py3.dependency(required: false) if not py3_dep.found() error('MESON_SKIP_TEST: Python library not found.') diff --git a/test cases/python/4 custom target depends extmodule/blaster.py b/test cases/python/4 custom target depends extmodule/blaster.py index 61b11f9..65b6493 100644 --- a/test cases/python/4 custom target depends extmodule/blaster.py +++ b/test cases/python/4 custom target depends extmodule/blaster.py @@ -10,6 +10,9 @@ filedir = Path(os.path.dirname(__file__)).resolve() if list(filedir.glob('ext/*tachyon*')): sys.path.insert(0, (filedir / 'ext').as_posix()) +if hasattr(os, 'add_dll_directory'): + os.add_dll_directory(filedir / 'ext' / 'lib') + import tachyon parser = argparse.ArgumentParser() diff --git a/test cases/python/5 modules kwarg/meson.build b/test cases/python/5 modules kwarg/meson.build index 9751ada..41a9a4f 100644 --- a/test cases/python/5 modules kwarg/meson.build +++ b/test cases/python/5 modules kwarg/meson.build @@ -1,7 +1,7 @@ project('python kwarg') py = import('python') -prog_python = py.find_installation('python3', modules : ['distutils']) +prog_python = py.find_installation('python3', modules : ['os', 'sys', 're']) assert(prog_python.found() == true, 'python not found when should be') prog_python = py.find_installation('python3', modules : ['thisbetternotexistmod'], required : false) assert(prog_python.found() == false, 'python not found but reported as found') diff --git a/test cases/python/6 failing subproject/subprojects/bar/meson.build b/test cases/python/6 failing subproject/subprojects/bar/meson.build index 21431ca..a5534ce 100644 --- a/test cases/python/6 failing subproject/subprojects/bar/meson.build +++ b/test cases/python/6 failing subproject/subprojects/bar/meson.build @@ -1,4 +1,4 @@ project('bar', 'cpp') python = import('python').find_installation('python3') -dependency('nonexistant-dependency') +dependency('nonexistent-dependency') diff --git a/test cases/python/7 install path/meson.build b/test cases/python/7 install path/meson.build index 1075c1b..2cac652 100644 --- a/test cases/python/7 install path/meson.build +++ b/test cases/python/7 install path/meson.build @@ -1,7 +1,8 @@ project('install path', default_options: [ + 'python.bytecompile=-1', 'python.purelibdir=/pure', - 'python.platlibdir=/plat' + 'python.platlibdir=/plat', ] ) @@ -17,4 +18,8 @@ py_plat.install_sources('test.py', pure: true, subdir: 'kwrevert') install_data('test.py', install_dir: py_plat.get_install_dir() / 'kw/data') install_data('test.py', install_dir: py_plat.get_install_dir(pure: true) / 'kwrevert/data') +if get_option('backend') == 'none' + subdir('target') +endif + subdir('structured') diff --git a/test cases/python/7 install path/target/meson.build b/test cases/python/7 install path/target/meson.build new file mode 100644 index 0000000..178ae02 --- /dev/null +++ b/test cases/python/7 install path/target/meson.build @@ -0,0 +1,3 @@ +testcase expect_error('Install-only backend cannot generate target rules, try using `--backend=ninja`.') + import('fs').copyfile('../test.py') +endtestcase diff --git a/test cases/python/9 extmodule limited api/limited.c b/test cases/python/9 extmodule limited api/limited.c new file mode 100644 index 0000000..0d1c718 --- /dev/null +++ b/test cases/python/9 extmodule limited api/limited.c @@ -0,0 +1,19 @@ +#include + +#ifndef Py_LIMITED_API +#error Py_LIMITED_API must be defined. +#elif Py_LIMITED_API != 0x03070000 +#error Wrong value for Py_LIMITED_API +#endif + +static struct PyModuleDef limited_module = { + PyModuleDef_HEAD_INIT, + "limited_api_test", + NULL, + -1, + NULL +}; + +PyMODINIT_FUNC PyInit_limited(void) { + return PyModule_Create(&limited_module); +} diff --git a/test cases/python/9 extmodule limited api/meson.build b/test cases/python/9 extmodule limited api/meson.build new file mode 100644 index 0000000..68afc96 --- /dev/null +++ b/test cases/python/9 extmodule limited api/meson.build @@ -0,0 +1,16 @@ +project('Python limited api', 'c', + default_options : ['buildtype=release', 'werror=true']) + +py_mod = import('python') +py = py_mod.find_installation() + +ext_mod_limited = py.extension_module('limited', + 'limited.c', + limited_api: '3.7', + install: true, +) + +ext_mod = py.extension_module('not_limited', + 'not_limited.c', + install: true, +) diff --git a/test cases/python/9 extmodule limited api/not_limited.c b/test cases/python/9 extmodule limited api/not_limited.c new file mode 100644 index 0000000..105dbb8 --- /dev/null +++ b/test cases/python/9 extmodule limited api/not_limited.c @@ -0,0 +1,59 @@ +#include +#include + +#ifdef Py_LIMITED_API +#error Py_LIMITED_API must not be defined. +#endif + +/* This function explicitly calls functions whose declaration is elided when + * Py_LIMITED_API is defined. This is to test that the linker is actually + * linking to the right version of the library on Windows. */ +static PyObject *meth_not_limited(PyObject *self, PyObject *args) +{ + PyObject *list; + Py_ssize_t size; + + if (!PyArg_ParseTuple(args, "o", & list)) + return NULL; + + if (!PyList_Check(list)) { + PyErr_Format(PyExc_TypeError, "expected 'list'"); + return NULL; + } + + /* PyList_GET_SIZE and PyList_GET_ITEM are only available if Py_LIMITED_API + * is not defined. It seems likely that they will remain excluded from the + * limited API as their checked counterparts (PyList_GetSize and + * PyList_GetItem) are made available in that mode instead. */ + size = PyList_GET_SIZE(list); + for(Py_ssize_t i = 0; i < size; ++i) { + PyObject *element = PyList_GET_ITEM(list, i); + if (element == NULL) { + return NULL; + } + + if(PyObject_Print(element, stdout, Py_PRINT_RAW) == -1) { + return NULL; + } + } + + Py_RETURN_NONE; +} + +static struct PyMethodDef not_limited_methods[] = { + { "not_limited", meth_not_limited, METH_VARARGS, + "Calls functions whose declaration is elided by Py_LIMITED_API" }, + { NULL, NULL, 0, NULL } +}; + +static struct PyModuleDef not_limited_module = { + PyModuleDef_HEAD_INIT, + "not_limited_api_test", + NULL, + -1, + not_limited_methods +}; + +PyMODINIT_FUNC PyInit_not_limited(void) { + return PyModule_Create(¬_limited_module); +} diff --git a/test cases/python/9 extmodule limited api/test.json b/test cases/python/9 extmodule limited api/test.json new file mode 100644 index 0000000..06a1706 --- /dev/null +++ b/test cases/python/9 extmodule limited api/test.json @@ -0,0 +1,8 @@ +{ + "installed": [ + {"type": "python_limited_lib", "file": "usr/@PYTHON_PLATLIB@/limited"}, + {"type": "py_limited_implib", "file": "usr/@PYTHON_PLATLIB@/limited"}, + {"type": "python_lib", "file": "usr/@PYTHON_PLATLIB@/not_limited"}, + {"type": "py_implib", "file": "usr/@PYTHON_PLATLIB@/not_limited"} + ] +} diff --git a/test cases/python3/4 custom target depends extmodule/blaster.py b/test cases/python3/4 custom target depends extmodule/blaster.py index d2c93ad..9cce645 100644 --- a/test cases/python3/4 custom target depends extmodule/blaster.py +++ b/test cases/python3/4 custom target depends extmodule/blaster.py @@ -10,6 +10,9 @@ filedir = Path(os.path.dirname(__file__)).resolve() if list(filedir.glob('ext/*tachyon.*')): sys.path.insert(0, (filedir / 'ext').as_posix()) +if hasattr(os, 'add_dll_directory'): + os.add_dll_directory(filedir / 'ext' / 'lib') + import tachyon parser = argparse.ArgumentParser() diff --git a/test cases/rust/1 basic/meson.build b/test cases/rust/1 basic/meson.build index 63ad375..3ba9877 100644 --- a/test cases/rust/1 basic/meson.build +++ b/test cases/rust/1 basic/meson.build @@ -1,4 +1,4 @@ -project('rustprog', 'rust') +project('rustprog', 'rust', default_options : ['b_ndebug=true']) e = executable('rust-program', 'prog.rs', rust_args : ['-C', 'lto'], # Just a test @@ -7,3 +7,14 @@ e = executable('rust-program', 'prog.rs', test('rusttest', e) subdir('subdir') + +# this should fail due to debug_assert +test( + 'debug_assert_on', + executable( + 'rust-program2', + 'prog.rs', + override_options : ['b_ndebug=false'], + ), + should_fail : true, +) diff --git a/test cases/rust/1 basic/prog.rs b/test cases/rust/1 basic/prog.rs index f1b3d30..69d2935 100644 --- a/test cases/rust/1 basic/prog.rs +++ b/test cases/rust/1 basic/prog.rs @@ -1,4 +1,5 @@ fn main() { let foo = "rust compiler is working"; + debug_assert!(false, "debug_asserts on!"); println!("{}", foo); } diff --git a/test cases/rust/12 bindgen/dependencies/meson.build b/test cases/rust/12 bindgen/dependencies/meson.build index 37e5a42..998bd32 100644 --- a/test cases/rust/12 bindgen/dependencies/meson.build +++ b/test cases/rust/12 bindgen/dependencies/meson.build @@ -12,6 +12,8 @@ external_dep_rs = rust.bindgen( external_dep = static_library( 'external_dep', [external_dep_rs], + # for generated code, do not lint + rust_args: ['-A', 'warnings'], dependencies : dep_zlib.partial_dependency(links : true), ) diff --git a/test cases/rust/12 bindgen/meson.build b/test cases/rust/12 bindgen/meson.build index c05cc06..e36e9e2 100644 --- a/test cases/rust/12 bindgen/meson.build +++ b/test cases/rust/12 bindgen/meson.build @@ -8,6 +8,17 @@ if not prog_bindgen.found() error('MESON_SKIP_TEST bindgen not found') endif +cc_id = meson.get_compiler('c').get_id() +compiler_specific_args = [] +if cc_id == 'gcc' + compiler_specific_args = ['-mtls-dialect=gnu2'] +elif cc_id == 'msvc' + compiler_specific_args = ['/fp:fast'] +endif + +add_project_arguments(['-DPROJECT_ARG', compiler_specific_args], language : 'c') +add_global_arguments(['-DGLOBAL_ARG', compiler_specific_args], language : 'c') + # This seems to happen on windows when libclang.dll is not in path or is not # valid. We must try to process a header file for this to work. # @@ -81,3 +92,18 @@ test('generated header', rust_bin2) subdir('sub') subdir('dependencies') + +gp = rust.bindgen( + input : 'src/global-project.h', + output : 'global-project.rs', +) + +gp_lib = static_library('gp_lib', 'src/global.c') + +gp_exe = executable( + 'gp_exe', + structured_sources(['src/global.rs', gp]), + link_with : gp_lib, +) + +test('global and project arguments', gp_exe) diff --git a/test cases/rust/12 bindgen/src/global-project.h b/test cases/rust/12 bindgen/src/global-project.h new file mode 100644 index 0000000..6084e8e --- /dev/null +++ b/test cases/rust/12 bindgen/src/global-project.h @@ -0,0 +1,10 @@ +#ifndef GLOBAL_ARG +char * success(void); +#endif +#ifndef PROJECT_ARG +char * success(void); +#endif +#ifndef CMD_ARG +char * success(void); +#endif +int success(void); diff --git a/test cases/rust/12 bindgen/src/global.c b/test cases/rust/12 bindgen/src/global.c new file mode 100644 index 0000000..10f6676 --- /dev/null +++ b/test cases/rust/12 bindgen/src/global.c @@ -0,0 +1,5 @@ +#include "src/global-project.h" + +int success(void) { + return 0; +} diff --git a/test cases/rust/12 bindgen/src/global.rs b/test cases/rust/12 bindgen/src/global.rs new file mode 100644 index 0000000..4b70b1e --- /dev/null +++ b/test cases/rust/12 bindgen/src/global.rs @@ -0,0 +1,14 @@ +// SPDX-license-identifer: Apache-2.0 +// Copyright © 2023 Intel Corporation + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +include!("global-project.rs"); + +fn main() { + unsafe { + std::process::exit(success()); + }; +} diff --git a/test cases/rust/12 bindgen/test.json b/test cases/rust/12 bindgen/test.json index 11cefc3..d45543b 100644 --- a/test cases/rust/12 bindgen/test.json +++ b/test cases/rust/12 bindgen/test.json @@ -1,7 +1,10 @@ { + "env": { + "CFLAGS": "-DCMD_ARG" + }, "stdout": [ { - "line": "test cases/rust/12 bindgen/meson.build:30: WARNING: Project targets '>= 0.63' but uses feature introduced in '1.0.0': include_directories kwarg of type string. Use include_directories('include') instead" + "line": "test cases/rust/12 bindgen/meson.build:38: WARNING: Project targets '>= 0.63' but uses feature introduced in '1.0.0': \"rust.bindgen\" keyword argument \"include_directories\" of type array[str]." } ] } diff --git a/test cases/rust/13 external c dependencies/test.json b/test cases/rust/13 external c dependencies/test.json index 423581f..abd996f 100644 --- a/test cases/rust/13 external c dependencies/test.json +++ b/test cases/rust/13 external c dependencies/test.json @@ -12,7 +12,8 @@ ] }, "exclude": [ - { "static": true, "method": "pkg-config" } + { "static": true, "method": "pkg-config" }, + { "static": true, "method": "cmake" } ] } } diff --git a/test cases/rust/15 polyglot sharedlib/adder.c b/test cases/rust/15 polyglot sharedlib/adder.c index 66613ed..1b5faa6 100644 --- a/test cases/rust/15 polyglot sharedlib/adder.c +++ b/test cases/rust/15 polyglot sharedlib/adder.c @@ -11,7 +11,13 @@ adder* adder_create(int number) { return a; } -// adder_add is implemented in the Rust file. +// adder_add_r is implemented in the Rust file. +int adder_add_r(adder *a, int number); + +int adder_add(adder *a, int number) +{ + return adder_add_r(a, number); +} void adder_destroy(adder *a) { free(a); diff --git a/test cases/rust/15 polyglot sharedlib/adder.rs b/test cases/rust/15 polyglot sharedlib/adder.rs index 9095350..ec4d1cc 100644 --- a/test cases/rust/15 polyglot sharedlib/adder.rs +++ b/test cases/rust/15 polyglot sharedlib/adder.rs @@ -3,7 +3,14 @@ pub struct Adder { pub number: i32 } +extern "C" { + pub fn zero() -> i32; + pub fn zero_static() -> i32; +} + #[no_mangle] -pub extern fn adder_add(a: &Adder, number: i32) -> i32 { - return a.number + number; +pub extern fn adder_add_r(a: &Adder, number: i32) -> i32 { + unsafe { + return a.number + number + zero() + zero_static(); + } } diff --git a/test cases/rust/15 polyglot sharedlib/meson.build b/test cases/rust/15 polyglot sharedlib/meson.build index 13fc8fd..fc3d53b 100644 --- a/test cases/rust/15 polyglot sharedlib/meson.build +++ b/test cases/rust/15 polyglot sharedlib/meson.build @@ -1,20 +1,15 @@ project('adder', 'c', 'rust', version: '1.0.0') -if build_machine.system() != 'linux' - error('MESON_SKIP_TEST, this test only works on Linux. Patches welcome.') -endif +subdir('zero') -thread_dep = dependency('threads') -dl_dep = meson.get_compiler('c').find_library('dl', required: false) -m_dep = meson.get_compiler('c').find_library('m', required: false) - -rl = static_library('radder', 'adder.rs', rust_crate_type: 'staticlib') +rl = shared_library('radder', 'adder.rs', + rust_abi: 'c', + link_with: [zero_shared, zero_static]) l = shared_library('adder', 'adder.c', - c_args: '-DBUILDING_ADDER', - link_with: rl, - version: '1.0.0', - soversion: '1', - link_args: '-Wl,-u,adder_add', # Ensure that Rust code is not removed as unused. - dependencies: [thread_dep, dl_dep, m_dep]) + c_args: '-DBUILDING_ADDER', + link_with: rl, + version: '1.0.0', + soversion: '1', +) test('adder', executable('addertest', 'addertest.c', link_with: l)) diff --git a/test cases/rust/15 polyglot sharedlib/zero/meson.build b/test cases/rust/15 polyglot sharedlib/zero/meson.build new file mode 100644 index 0000000..ec7ecf7 --- /dev/null +++ b/test cases/rust/15 polyglot sharedlib/zero/meson.build @@ -0,0 +1,6 @@ +# They both have the same name, this tests we use +verbatim to distinguish them +# using their filename. It also ensures we pass the importlib on Windows. +# Those libs are in a subdir as regression test: +# https://github.com/mesonbuild/meson/issues/12484 +zero_shared = shared_library('zero', 'zero.c') +zero_static = static_library('zero', 'zero_static.c') diff --git a/test cases/rust/15 polyglot sharedlib/zero/zero.c b/test cases/rust/15 polyglot sharedlib/zero/zero.c new file mode 100644 index 0000000..02672f3 --- /dev/null +++ b/test cases/rust/15 polyglot sharedlib/zero/zero.c @@ -0,0 +1,11 @@ +#if defined _WIN32 || defined __CYGWIN__ +#define EXPORT __declspec(dllexport) +#else +#define EXPORT +#endif + +EXPORT int zero(void); + +int zero(void) { + return 0; +} diff --git a/test cases/rust/15 polyglot sharedlib/zero/zero_static.c b/test cases/rust/15 polyglot sharedlib/zero/zero_static.c new file mode 100644 index 0000000..7f14fb4 --- /dev/null +++ b/test cases/rust/15 polyglot sharedlib/zero/zero_static.c @@ -0,0 +1,6 @@ +int zero_static(void); + +int zero_static(void) +{ + return 0; +} diff --git a/test cases/rust/16 internal c dependencies/meson.build b/test cases/rust/16 internal c dependencies/meson.build index c7476d7..f459828 100644 --- a/test cases/rust/16 internal c dependencies/meson.build +++ b/test cases/rust/16 internal c dependencies/meson.build @@ -3,12 +3,16 @@ project('internal dependencies', 'c', 'rust') test_prog = find_program('test.py') static = static_library('static', 'lib.c', c_args : '-DMODE="static"') -exe = executable('static', 'main.rs', link_with : static) +# Add some -I compiler arguments to make sure they're not passed to the Rust +# compiler when handling the dependency. +static_dep = declare_dependency(link_with : static, compile_args : ['-Idoesnotexist']) +exe = executable('static', 'main.rs', dependencies : static_dep) test('static linkage', test_prog, args : [exe, 'This is a static C library']) # Shared linkage with rust doesn't work on macOS with meson, yet if host_machine.system() != 'darwin' shared = shared_library('shared', 'lib.c', c_args : '-DMODE="shared"') - exe = executable('shared', 'main.rs', link_with : shared) + shared_dep = declare_dependency(link_with : shared, compile_args : ['-Idoesnotexist']) + exe = executable('shared', 'main.rs', dependencies : shared_dep) test('shared linkage', test_prog, args : [exe, 'This is a shared C library']) endif diff --git a/test cases/rust/17 staticlib link staticlib/branch.rs b/test cases/rust/17 staticlib link staticlib/branch.rs index 29e1cd0..ea97647 100644 --- a/test cases/rust/17 staticlib link staticlib/branch.rs +++ b/test cases/rust/17 staticlib link staticlib/branch.rs @@ -1,4 +1,4 @@ #[no_mangle] pub extern "C" fn what_have_we_here() -> i32 { - leaf::HOW_MANY * leaf::HOW_MANY + myleaf::HOW_MANY * myleaf::HOW_MANY } diff --git a/test cases/rust/17 staticlib link staticlib/meson.build b/test cases/rust/17 staticlib link staticlib/meson.build index 68d08f3..4a01c05 100644 --- a/test cases/rust/17 staticlib link staticlib/meson.build +++ b/test cases/rust/17 staticlib link staticlib/meson.build @@ -3,6 +3,6 @@ project('staticlib link staticlib', 'c', 'rust') leaf = static_library('leaf', 'leaf.rs', rust_crate_type : 'rlib') # Even though leaf is linked using link_with, this gets implicitly promoted to link_whole because # it is an internal Rust project. -branch = static_library('branch', 'branch.rs', link_with: leaf, rust_crate_type : 'staticlib', install : true) +branch = static_library('branch', 'branch.rs', link_with: leaf, rust_crate_type : 'staticlib', rust_dependency_map : { 'leaf' : 'myleaf' }, install : true) e = executable('prog', 'prog.c', link_with : branch, install : true) test('linktest', e) diff --git a/test cases/rust/18 proc-macro/meson.build b/test cases/rust/18 proc-macro/meson.build index 01c4cbe..c5f0dfc 100644 --- a/test cases/rust/18 proc-macro/meson.build +++ b/test cases/rust/18 proc-macro/meson.build @@ -13,7 +13,21 @@ pm = shared_library( main = executable( 'main', 'use.rs', - link_with : pm + link_with : pm, + rust_args : ['-C', 'panic=abort'], ) test('main_test', main) + +rust = import('rust') + +pm = rust.proc_macro('proc_macro_examples2', 'proc.rs') + +main = executable( + 'main2', + 'use.rs', + link_with : pm, + rust_dependency_map : {'proc_macro_examples2' : 'proc_macro_examples'} +) + +test('main_test2', main) diff --git a/test cases/rust/19 structured sources/meson.build b/test cases/rust/19 structured sources/meson.build index d84e83f..d5b3909 100644 --- a/test cases/rust/19 structured sources/meson.build +++ b/test cases/rust/19 structured sources/meson.build @@ -27,7 +27,7 @@ target = executable( ), ) -# Should not be coppied +# Should not be copied executable( 'no_copy_target', structured_sources( diff --git a/test cases/rust/2 sharedlib/meson.build b/test cases/rust/2 sharedlib/meson.build index 02b8cf7..295fa04 100644 --- a/test cases/rust/2 sharedlib/meson.build +++ b/test cases/rust/2 sharedlib/meson.build @@ -1,10 +1,11 @@ -project('rust shared library', 'rust') +project('rust shared library', 'rust', 'c') if host_machine.system() == 'darwin' error('MESON_SKIP_TEST: does not work right on macos, please fix!') endif -l = shared_library('stuff', 'stuff.rs', install : true) +s = static_library('static', 'value.c') +l = shared_library('stuff', 'stuff.rs', link_whole : s, install : true) e = executable('prog', 'prog.rs', link_with : l, install : true) if build_machine.system() == 'windows' diff --git a/test cases/rust/2 sharedlib/stuff.rs b/test cases/rust/2 sharedlib/stuff.rs index 8cabc62..e7c0521 100644 --- a/test cases/rust/2 sharedlib/stuff.rs +++ b/test cases/rust/2 sharedlib/stuff.rs @@ -1,3 +1,11 @@ #![crate_name = "stuff"] -pub fn explore() -> &'static str { "librarystring" } +extern "C" { + fn c_value() -> i32; +} + +pub fn explore() -> String { + unsafe { + format!("library{}string", c_value()) + } +} diff --git a/test cases/rust/2 sharedlib/value.c b/test cases/rust/2 sharedlib/value.c new file mode 100644 index 0000000..d17b6de --- /dev/null +++ b/test cases/rust/2 sharedlib/value.c @@ -0,0 +1,3 @@ +int c_value(void) { + return 7; +} diff --git a/test cases/rust/20 rust and cpp/lib.cpp b/test cases/rust/20 rust and cpp/lib.cpp new file mode 100644 index 0000000..b08f870 --- /dev/null +++ b/test cases/rust/20 rust and cpp/lib.cpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2023 Intel Corporation + +#include "lib.hpp" + +#include + +namespace { + +uint64_t priv_length(const std::string & str) { + return str.length(); +} + +} + +extern "C" uint64_t lib_length(const char * str) { + return priv_length(str); +} diff --git a/test cases/rust/20 rust and cpp/lib.hpp b/test cases/rust/20 rust and cpp/lib.hpp new file mode 100644 index 0000000..63093c4 --- /dev/null +++ b/test cases/rust/20 rust and cpp/lib.hpp @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2023 Intel Corporation + +#include +#include + +extern "C" uint64_t lib_length(const char * str); + diff --git a/test cases/rust/20 rust and cpp/main.rs b/test cases/rust/20 rust and cpp/main.rs new file mode 100644 index 0000000..b048cac --- /dev/null +++ b/test cases/rust/20 rust and cpp/main.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2023 Intel Corporation + +use std::ffi::CString; +use std::os::raw::c_char; + +extern "C" { + fn lib_length(s: *const c_char) -> u64; +} + +fn main() { + let len: u64; + unsafe { + let c_str = CString::new("Hello, world!").unwrap(); + len = lib_length(c_str.as_ptr()); + } + + std::process::exit(if len == 13 { 0 } else { 1 }) +} diff --git a/test cases/rust/20 rust and cpp/meson.build b/test cases/rust/20 rust and cpp/meson.build new file mode 100644 index 0000000..c301012 --- /dev/null +++ b/test cases/rust/20 rust and cpp/meson.build @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2023 Intel Corporation + +project( + 'Rust and C++', + 'rust', 'cpp', + default_options : ['cpp_std=c++14'], + meson_version : '>= 1.2.0', +) + +cpplib = static_library('cpp', 'lib.cpp') +exe = executable('main', 'main.rs', link_with : cpplib) + +test('main', exe) diff --git a/test cases/rust/20 rust and cpp/test.json b/test cases/rust/20 rust and cpp/test.json new file mode 100644 index 0000000..c072a6c --- /dev/null +++ b/test cases/rust/20 rust and cpp/test.json @@ -0,0 +1,15 @@ +{ + "matrix": { + "options": { + "b_vscrt": [ + { "val": "none" }, + { "val": "mdd" }, + { "val": "md" }, + { "val": "mtd" }, + { "val": "mt" }, + { "val": "from_buildtype" }, + { "val": "static_from_buildtype" } + ] + } + } +} diff --git a/test cases/rust/21 transitive dependencies/app.rs b/test cases/rust/21 transitive dependencies/app.rs new file mode 100644 index 0000000..a9802b3 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/app.rs @@ -0,0 +1,9 @@ +extern "C" { + fn static2() -> i32; +} + +fn main() { + unsafe { + static2(); + } +} diff --git a/test cases/rust/21 transitive dependencies/diamond/func.c b/test cases/rust/21 transitive dependencies/diamond/func.c new file mode 100644 index 0000000..c07ab72 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/func.c @@ -0,0 +1,4 @@ +int c_func(void); +int c_func(void) { + return 123; +} diff --git a/test cases/rust/21 transitive dependencies/diamond/main.c b/test cases/rust/21 transitive dependencies/diamond/main.c new file mode 100644 index 0000000..c633e9a --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/main.c @@ -0,0 +1,5 @@ +int r3(void); + +int main_func(void) { + return r3() == 246 ? 0 : 1; +} diff --git a/test cases/rust/21 transitive dependencies/diamond/meson.build b/test cases/rust/21 transitive dependencies/diamond/meson.build new file mode 100644 index 0000000..dc48d45 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/meson.build @@ -0,0 +1,25 @@ +# Regression test for a diamond dependency graph: +# ┌►R1┐ +# main-►R3─┤ ├─►C1 +# └►R2┘ +# Both libr1.rlib and libr2.rlib used to contain func.c.o. That was causing +# libr3.rlib to have duplicated func.c.o and then libmain.so failed to link: +# multiple definition of `c_func'. + +libc1 = static_library('c1', 'func.c') +libr1 = static_library('r1', 'r1.rs', link_with: libc1) +libr2 = static_library('r2', 'r2.rs', link_with: libc1) +libr3 = static_library('r3', 'r3.rs', + link_with: [libr1, libr2], + rust_abi: 'c', +) +shared_library('main', 'main.c', link_whole: [libr3]) + +# Same dependency graph, but r3 is now installed. Since c1, r1 and r2 are +# not installed, r3 must contain them. +libr3 = static_library('r3-installed', 'r3.rs', + link_with: [libr1, libr2], + rust_abi: 'c', + install: true, +) +shared_library('main-installed', 'main.c', link_with: [libr3]) diff --git a/test cases/rust/21 transitive dependencies/diamond/r1.rs b/test cases/rust/21 transitive dependencies/diamond/r1.rs new file mode 100644 index 0000000..7afb711 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/r1.rs @@ -0,0 +1,9 @@ +extern "C" { + fn c_func() -> i32; +} + +pub fn r1() -> i32 { + unsafe { + c_func() + } +} diff --git a/test cases/rust/21 transitive dependencies/diamond/r2.rs b/test cases/rust/21 transitive dependencies/diamond/r2.rs new file mode 100644 index 0000000..ee73ee2 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/r2.rs @@ -0,0 +1,9 @@ +extern "C" { + fn c_func() -> i32; +} + +pub fn r2() -> i32 { + unsafe { + c_func() + } +} diff --git a/test cases/rust/21 transitive dependencies/diamond/r3.rs b/test cases/rust/21 transitive dependencies/diamond/r3.rs new file mode 100644 index 0000000..9794b7e --- /dev/null +++ b/test cases/rust/21 transitive dependencies/diamond/r3.rs @@ -0,0 +1,4 @@ +#[no_mangle] +pub fn r3() -> i32 { + r1::r1() + r2::r2() +} diff --git a/test cases/rust/21 transitive dependencies/foo.c b/test cases/rust/21 transitive dependencies/foo.c new file mode 100644 index 0000000..e40878a --- /dev/null +++ b/test cases/rust/21 transitive dependencies/foo.c @@ -0,0 +1,8 @@ +#include + +uint32_t foo_rs(void); + +int main(void) +{ + return foo_rs() == 42 ? 0 : 1; +} diff --git a/test cases/rust/21 transitive dependencies/foo.rs b/test cases/rust/21 transitive dependencies/foo.rs new file mode 100644 index 0000000..8e38638 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/foo.rs @@ -0,0 +1,9 @@ +extern crate pm; +use pm::make_answer; + +make_answer!(); + +#[no_mangle] +pub fn foo_rs() -> u32 { + answer() +} diff --git a/test cases/rust/21 transitive dependencies/liba/lib.rs b/test cases/rust/21 transitive dependencies/liba/lib.rs new file mode 100644 index 0000000..0fc8ce5 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/liba/lib.rs @@ -0,0 +1,3 @@ +pub fn foo() -> i32 { + 123 +} diff --git a/test cases/rust/21 transitive dependencies/liba/meson.build b/test cases/rust/21 transitive dependencies/liba/meson.build new file mode 100644 index 0000000..b32822d --- /dev/null +++ b/test cases/rust/21 transitive dependencies/liba/meson.build @@ -0,0 +1,5 @@ +liba = static_library('liba', 'lib.rs', + rust_crate_type : 'rlib', +) + +liba_dep = declare_dependency(link_with : liba) diff --git a/test cases/rust/21 transitive dependencies/libb/lib.rs b/test cases/rust/21 transitive dependencies/libb/lib.rs new file mode 100644 index 0000000..e97463b --- /dev/null +++ b/test cases/rust/21 transitive dependencies/libb/lib.rs @@ -0,0 +1,3 @@ +pub fn bar() -> i32 { + 2 * liba::foo() +} diff --git a/test cases/rust/21 transitive dependencies/libb/meson.build b/test cases/rust/21 transitive dependencies/libb/meson.build new file mode 100644 index 0000000..67947e7 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/libb/meson.build @@ -0,0 +1,6 @@ +libb = static_library('libb', 'lib.rs', + rust_crate_type : 'rlib', + dependencies : [liba_dep], +) + +libb_dep = declare_dependency(link_with : libb) diff --git a/test cases/rust/21 transitive dependencies/main.rs b/test cases/rust/21 transitive dependencies/main.rs new file mode 100644 index 0000000..4b24845 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("{}", libb::bar()); +} diff --git a/test cases/rust/21 transitive dependencies/meson.build b/test cases/rust/21 transitive dependencies/meson.build new file mode 100644 index 0000000..37687fd --- /dev/null +++ b/test cases/rust/21 transitive dependencies/meson.build @@ -0,0 +1,37 @@ +project('transitive dependencies', 'rust', 'c', + version : '1.0.0', + meson_version : '>= 1.0.0', + default_options : ['rust_std=2018'], +) + +subdir('liba') +subdir('libb') + +main = executable('main', 'main.rs', + dependencies : [libb_dep], +) + +# Since foo-rs is a static library, its dependencies are normally added to +# footest link command. However, since pm is a proc-macro, footest should not +# link with it. In native build this is an harmless overlinking, but in cross +# building foo and pm are for different arch and it would fail to link. +rust = import('rust') +pm = rust.proc_macro('pm', 'proc.rs') +foo = static_library('foo-rs', 'foo.rs', + rust_abi: 'c', + link_with: pm, +) +exe = executable('footest', 'foo.c', + link_with: foo, +) +test('footest', exe) + +subdir('diamond') + +# The ninja rule for libstatic2.a does not depend on libstatic1.a because it +# only need static2.c.o to create the archive. That means that the ninja rule +# for app must depend on both, otherwise libstatic1.a won't be built and linking +# will fail. +static1 = static_library('static1', 'static1.c', build_by_default: false) +static2 = static_library('static2', 'static2.c', link_with: static1) +exe = executable('app', 'app.rs', link_with: static2) diff --git a/test cases/rust/21 transitive dependencies/proc.rs b/test cases/rust/21 transitive dependencies/proc.rs new file mode 100644 index 0000000..53935e4 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/proc.rs @@ -0,0 +1,7 @@ +extern crate proc_macro; +use proc_macro::TokenStream; + +#[proc_macro] +pub fn make_answer(_item: TokenStream) -> TokenStream { + "fn answer() -> u32 { 42 }".parse().unwrap() +} diff --git a/test cases/rust/21 transitive dependencies/static1.c b/test cases/rust/21 transitive dependencies/static1.c new file mode 100644 index 0000000..104618a --- /dev/null +++ b/test cases/rust/21 transitive dependencies/static1.c @@ -0,0 +1,5 @@ +int static1(void); + +int static1(void){ + return 1; +} diff --git a/test cases/rust/21 transitive dependencies/static2.c b/test cases/rust/21 transitive dependencies/static2.c new file mode 100644 index 0000000..6877c04 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/static2.c @@ -0,0 +1,7 @@ +int static1(void); +int static2(void); + +int static2(void) +{ + return 1 + static1(); +} diff --git a/test cases/rust/21 transitive dependencies/test.json b/test cases/rust/21 transitive dependencies/test.json new file mode 100644 index 0000000..0d98c23 --- /dev/null +++ b/test cases/rust/21 transitive dependencies/test.json @@ -0,0 +1,5 @@ +{ + "installed": [ + {"type": "file", "file": "usr/lib/libr3-installed.a"} + ] +} diff --git a/test cases/rust/22 cargo subproject/main.c b/test cases/rust/22 cargo subproject/main.c new file mode 100644 index 0000000..5daec64 --- /dev/null +++ b/test cases/rust/22 cargo subproject/main.c @@ -0,0 +1,5 @@ +int rust_func(void); + +int main(int argc, char *argv[]) { + return rust_func(); +} diff --git a/test cases/rust/22 cargo subproject/meson.build b/test cases/rust/22 cargo subproject/meson.build new file mode 100644 index 0000000..420e6e3 --- /dev/null +++ b/test cases/rust/22 cargo subproject/meson.build @@ -0,0 +1,7 @@ +project('cargo subproject', 'c') + +foo_dep = dependency('foo-rs') +exe = executable('app', 'main.c', + dependencies: foo_dep, +) +test('cargo-test', exe) diff --git a/test cases/rust/22 cargo subproject/subprojects/bar-rs.wrap b/test cases/rust/22 cargo subproject/subprojects/bar-rs.wrap new file mode 100644 index 0000000..99686e9 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/bar-rs.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method = cargo diff --git a/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml new file mode 100644 index 0000000..232b4d7 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/bar-rs/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "bar" +version = "0.1" diff --git a/test cases/rust/22 cargo subproject/subprojects/bar-rs/src/lib.rs b/test cases/rust/22 cargo subproject/subprojects/bar-rs/src/lib.rs new file mode 100644 index 0000000..5b64db8 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/bar-rs/src/lib.rs @@ -0,0 +1 @@ +pub const VALUE: i32 = 0; diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-rs.wrap b/test cases/rust/22 cargo subproject/subprojects/foo-rs.wrap new file mode 100644 index 0000000..99686e9 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/foo-rs.wrap @@ -0,0 +1,2 @@ +[wrap-file] +method = cargo diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml b/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml new file mode 100644 index 0000000..214c327 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/foo-rs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "foo" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +mybar = { version = "0.1", package = "bar" } diff --git a/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs b/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs new file mode 100644 index 0000000..732d7d2 --- /dev/null +++ b/test cases/rust/22 cargo subproject/subprojects/foo-rs/src/lib.rs @@ -0,0 +1,4 @@ +#[no_mangle] +pub extern "C" fn rust_func() -> i32 { + mybar::VALUE +} diff --git a/test cases/rust/3 staticlib/meson.build b/test cases/rust/3 staticlib/meson.build index 6769564..cf8e103 100644 --- a/test cases/rust/3 staticlib/meson.build +++ b/test cases/rust/3 staticlib/meson.build @@ -1,5 +1,7 @@ -project('rust static library', 'rust') +project('rust static library', 'rust', 'c') -l = static_library('stuff', 'stuff.rs', install : true) +o = static_library('other', 'other.rs') +v = static_library('value', 'value.c') +l = static_library('stuff', 'stuff.rs', link_whole : [o, v], install : true) e = executable('prog', 'prog.rs', link_with : l, install : true) test('linktest', e) diff --git a/test cases/rust/3 staticlib/other.rs b/test cases/rust/3 staticlib/other.rs new file mode 100644 index 0000000..037be33 --- /dev/null +++ b/test cases/rust/3 staticlib/other.rs @@ -0,0 +1,5 @@ +pub fn explore( + value: i32, +) -> String { + format!("library{}string", value) +} diff --git a/test cases/rust/3 staticlib/prog.rs b/test cases/rust/3 staticlib/prog.rs index fbf3181..eee2653 100644 --- a/test cases/rust/3 staticlib/prog.rs +++ b/test cases/rust/3 staticlib/prog.rs @@ -1,3 +1,5 @@ extern crate stuff; -fn main() { println!("printing: {}", stuff::explore()); } +fn main() { + println!("printing: {}", stuff::explore()); +} diff --git a/test cases/rust/3 staticlib/stuff.rs b/test cases/rust/3 staticlib/stuff.rs index 8cabc62..7cfcdff 100644 --- a/test cases/rust/3 staticlib/stuff.rs +++ b/test cases/rust/3 staticlib/stuff.rs @@ -1,3 +1,14 @@ #![crate_name = "stuff"] -pub fn explore() -> &'static str { "librarystring" } +extern crate other; + +extern "C" { + fn c_explore_value() -> i32; +} + +pub fn explore( +) -> String { + unsafe { + other::explore(c_explore_value()) + } +} diff --git a/test cases/rust/3 staticlib/value.c b/test cases/rust/3 staticlib/value.c new file mode 100644 index 0000000..b71c8060 --- /dev/null +++ b/test cases/rust/3 staticlib/value.c @@ -0,0 +1,5 @@ +int +c_explore_value (void) +{ + return 42; +} diff --git a/test cases/rust/4 polyglot/meson.build b/test cases/rust/4 polyglot/meson.build index 3601d5e..b2fd8f9 100644 --- a/test cases/rust/4 polyglot/meson.build +++ b/test cases/rust/4 polyglot/meson.build @@ -4,6 +4,52 @@ if host_machine.system() == 'darwin' error('MESON_SKIP_TEST: does not work right on macos, please fix!') endif -l = shared_library('stuff', 'stuff.rs', rust_crate_type: 'cdylib', install : true) -e = executable('prog', 'prog.c', link_with : l, install : true) -test('polyglottest', e) +cc = meson.get_compiler('c') + +# Test all combinations of crate and target types. +# - 'clib' gets translated to `rust_abi: 'c'` instead. +# - '' gets translated to no kwargs. +allowed_map = { + 'static_library': ['rlib', 'staticlib', 'lib', 'clib', ''], + 'shared_library': ['dylib', 'cdylib', 'lib', 'proc-macro', 'clib', ''], + 'both_libraries': ['lib', 'clib', ''], +} +foreach crate_type : ['lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro', 'clib', '', 'invalid'] + foreach target_type, allowed : allowed_map + name = f'stuff_@crate_type@_@target_type@'.underscorify() + src = crate_type == 'proc-macro' ? 'proc.rs' : 'stuff.rs' + if crate_type not in allowed + # Note: in the both_libraries() case it is possible that the static part + # is still being built because the shared part raised an error but we + # don't rollback correctly. + testcase expect_error('(Crate type .* invalid for .*)|(.*must be one of.*not invalid)', how: 're') + build_target(name, src, + target_type: target_type, + rust_crate_type: crate_type, + install: true) + endtestcase + continue + endif + rust_kwargs = {} + if crate_type == 'clib' + rust_kwargs = {'rust_abi': 'c'} + elif crate_type != '' + rust_kwargs = {'rust_crate_type': crate_type} + endif + l = build_target(name, src, + target_type: target_type, + kwargs: rust_kwargs, + install: true) + if crate_type in ['cdylib', 'staticlib', 'clib'] + e = executable(f'prog-@name@', 'prog.c', + link_with: l, + rust_dependency_map: {name: 'stuff'}, + install: true) + test(f'polyglottest-@name@', e) + else + testcase expect_error('Try to link Rust ABI library .*', how: 're') + executable(f'prog-@name@', 'prog.c', link_with: l) + endtestcase + endif + endforeach +endforeach diff --git a/test cases/rust/4 polyglot/proc.rs b/test cases/rust/4 polyglot/proc.rs new file mode 100644 index 0000000..53935e4 --- /dev/null +++ b/test cases/rust/4 polyglot/proc.rs @@ -0,0 +1,7 @@ +extern crate proc_macro; +use proc_macro::TokenStream; + +#[proc_macro] +pub fn make_answer(_item: TokenStream) -> TokenStream { + "fn answer() -> u32 { 42 }".parse().unwrap() +} diff --git a/test cases/rust/4 polyglot/stuff.rs b/test cases/rust/4 polyglot/stuff.rs index ecf623c..30c3a36 100644 --- a/test cases/rust/4 polyglot/stuff.rs +++ b/test cases/rust/4 polyglot/stuff.rs @@ -1,5 +1,3 @@ -#![crate_name = "stuff"] - #[no_mangle] pub extern fn f() { println!("Hello from Rust!"); diff --git a/test cases/rust/4 polyglot/test.json b/test cases/rust/4 polyglot/test.json index a8837a1..d963ad8 100644 --- a/test cases/rust/4 polyglot/test.json +++ b/test cases/rust/4 polyglot/test.json @@ -1,10 +1,92 @@ { "installed": [ - {"type": "exe", "file": "usr/bin/prog"}, - {"type": "pdb", "file": "usr/bin/prog"}, - {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff.so"}, - {"type": "file", "platform": "msvc", "file": "usr/bin/stuff.dll"}, - {"type": "file", "platform": "msvc", "file": "usr/lib/stuff.dll.lib"}, - {"type": "pdb", "file": "usr/bin/stuff"} + {"type": "exe", "file": "usr/bin/prog-stuff_clib_both_libraries"}, + {"type": "pdb", "file": "usr/bin/prog-stuff_clib_both_libraries"}, + {"type": "pdb", "file": "usr/bin/stuff_clib_both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_clib_both_libraries.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_clib_both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_clib_both_libraries.dll.lib"}, + {"type": "file", "file": "usr/lib/libstuff_clib_both_libraries.a"}, + + {"type": "exe", "file": "usr/bin/prog-stuff_clib_shared_library"}, + {"type": "pdb", "file": "usr/bin/prog-stuff_clib_shared_library"}, + {"type": "pdb", "file": "usr/bin/stuff_clib_shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_clib_shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_clib_shared_library.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_clib_shared_library.dll.lib"}, + + {"type": "exe", "file": "usr/bin/prog-stuff_clib_static_library"}, + {"type": "pdb", "file": "usr/bin/prog-stuff_clib_static_library"}, + {"type": "file", "file": "usr/lib/libstuff_clib_static_library.a"}, + + {"type": "exe", "file": "usr/bin/prog-stuff_cdylib_shared_library"}, + {"type": "pdb", "file": "usr/bin/prog-stuff_cdylib_shared_library"}, + {"type": "pdb", "file": "usr/bin/stuff_cdylib_shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_cdylib_shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_cdylib_shared_library.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_cdylib_shared_library.dll.lib"}, + + {"type": "exe", "file": "usr/bin/prog-stuff_staticlib_static_library"}, + {"type": "pdb", "file": "usr/bin/prog-stuff_staticlib_static_library"}, + {"type": "file", "file": "usr/lib/libstuff_staticlib_static_library.a"}, + + {"type": "pdb", "file": "usr/bin/stuff__both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff__both_libraries.so"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff__both_libraries.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/libstuff__both_libraries.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff__both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff__both_libraries.dll.lib"}, + + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff__static_library.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/libstuff__static_library.rlib"}, + + {"type": "pdb", "file": "usr/bin/stuff_proc_macro_shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_proc_macro_shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_proc_macro_shared_library.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_proc_macro_shared_library.dll.lib"}, + + {"type": "pdb", "file": "usr/bin/stuff_lib_both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_lib_both_libraries.rlib"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_lib_both_libraries.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_lib_both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/libstuff_lib_both_libraries.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_lib_both_libraries.dll.lib"}, + + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_rlib_static_library.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/libstuff_rlib_static_library.rlib"}, + + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_lib_static_library.rlib"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/libstuff_lib_static_library.rlib"}, + + {"type": "pdb", "file": "usr/bin/stuff__shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff__shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff__shared_library.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff__shared_library.dll.lib"}, + + {"type": "pdb", "file": "usr/bin/stuff_lib_shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_lib_shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_lib_shared_library.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_lib_shared_library.dll.lib"}, + + {"type": "pdb", "file": "usr/bin/stuff_dylib_shared_library.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_dylib_shared_library.so"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_dylib_shared_library.dll.lib"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_dylib_shared_library.dll"}, + + {"type": "pdb", "file": "usr/bin/stuff_cdylib_both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_cdylib_both_libraries.so"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_cdylib_both_libraries.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_cdylib_both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_cdylib_both_libraries.dll.lib"}, + + {"type": "pdb", "file": "usr/bin/stuff_proc_macro_both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_proc_macro_both_libraries.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_proc_macro_both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_proc_macro_both_libraries.dll.lib"}, + + {"type": "pdb", "file": "usr/bin/stuff_dylib_both_libraries.pdb"}, + {"type": "file", "platform": "gcc", "file": "usr/lib/libstuff_dylib_both_libraries.so"}, + {"type": "file", "platform": "msvc", "file": "usr/bin/stuff_dylib_both_libraries.dll"}, + {"type": "file", "platform": "msvc", "file": "usr/lib/stuff_dylib_both_libraries.dll.lib"} ] } diff --git a/test cases/rust/5 polyglot static/clib.c b/test cases/rust/5 polyglot static/clib.c new file mode 100644 index 0000000..84749de --- /dev/null +++ b/test cases/rust/5 polyglot static/clib.c @@ -0,0 +1,14 @@ +#include +#include + +int32_t hello_from_rust(const int32_t a, const int32_t b); + +static void hello_from_c(void) { + printf("Hello from C!\n"); +} + +void hello_from_both(void) { + hello_from_c(); + if (hello_from_rust(2, 3) == 5) + printf("Hello from Rust!\n"); +} diff --git a/test cases/rust/5 polyglot static/meson.build b/test cases/rust/5 polyglot static/meson.build index b2a44da..54f383c 100644 --- a/test cases/rust/5 polyglot static/meson.build +++ b/test cases/rust/5 polyglot static/meson.build @@ -1,17 +1,26 @@ project('static rust and c polyglot executable', 'c', 'rust') -deps = [ - meson.get_compiler('c').find_library('dl', required: false), - meson.get_compiler('c').find_library('m', required: false), - dependency('threads'), -] +r = static_library('stuff', 'stuff.rs', rust_crate_type : 'staticlib') -extra_winlibs = meson.get_compiler('c').get_id() in ['msvc', 'clang-cl'] ? ['userenv.lib', 'ws2_32.lib', 'bcrypt.lib'] : [] +# clib is installed static library and stuff is not installed. That means that +# to be usable clib must link_whole stuff. Meson automatically promote to link_whole, +# as it would do with C libraries, but then cannot extract objects from stuff and +# thus should error out. +# FIXME: We should support this use-case in the future. +testcase expect_error('Cannot link_whole a custom or Rust target \'stuff\' into a static library \'clib\'. Instead, pass individual object files with the "objects:" keyword argument if possible. Meson had to promote link to link_whole because \'clib\' is installed but not \'stuff\', and thus has to include objects from \'stuff\' to be usable.') + l = static_library('clib', 'clib.c', link_with : r, install : true) +endtestcase + +l = static_library('clib', 'clib.c', link_with : r) -l = static_library('stuff', 'stuff.rs', rust_crate_type : 'staticlib', install : true) e = executable('prog', 'prog.c', - dependencies: deps, link_with : l, - link_args: extra_winlibs, install : true) test('polyglottest', e) + +# Create a version that has overflow-checks on, then run a test to ensure that +# the overflow-checks is larger than the other version by some ammount +r2 = static_library('stuff2', 'stuff.rs', rust_crate_type : 'staticlib', rust_args : ['-C', 'overflow-checks=on']) +l2 = static_library('clib2', 'clib.c') +e2 = executable('prog2', 'prog.c', link_with : [r2, l2]) +test('overflow-checks', find_program('overflow_size_checks.py'), args : [e, e2]) diff --git a/test cases/rust/5 polyglot static/overflow_size_checks.py b/test cases/rust/5 polyglot static/overflow_size_checks.py new file mode 100755 index 0000000..9a6a64a --- /dev/null +++ b/test cases/rust/5 polyglot static/overflow_size_checks.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2023 Intel Corporation + +from __future__ import annotations +import argparse +import os +import typing as T + +if T.TYPE_CHECKING: + class Arguments(T.Protocol): + checks_off: str + checks_on: str + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('checks_off') + parser.add_argument('checks_on') + args: Arguments = parser.parse_args() + + off = os.stat(args.checks_off).st_size + on = os.stat(args.checks_on).st_size + + assert on > off, f'Expected binary built with overflow-checks to be bigger, but it was smaller. with: "{on}"B, without: "{off}"B' + + +if __name__ == "__main__": + main() diff --git a/test cases/rust/5 polyglot static/prog.c b/test cases/rust/5 polyglot static/prog.c index dbbd880..0a8e0d1 100644 --- a/test cases/rust/5 polyglot static/prog.c +++ b/test cases/rust/5 polyglot static/prog.c @@ -1,8 +1,7 @@ #include -void f(); +void hello_from_both(); int main(void) { - printf("Hello from C!\n"); - f(); + hello_from_both(); } diff --git a/test cases/rust/5 polyglot static/stuff.rs b/test cases/rust/5 polyglot static/stuff.rs index ecf623c..c312441 100644 --- a/test cases/rust/5 polyglot static/stuff.rs +++ b/test cases/rust/5 polyglot static/stuff.rs @@ -1,6 +1,4 @@ -#![crate_name = "stuff"] - #[no_mangle] -pub extern fn f() { - println!("Hello from Rust!"); +pub extern "C" fn hello_from_rust(a: i32, b: i32) -> i32 { + a + b } diff --git a/test cases/rust/5 polyglot static/test.json b/test cases/rust/5 polyglot static/test.json index 1d4eff4..135300d 100644 --- a/test cases/rust/5 polyglot static/test.json +++ b/test cases/rust/5 polyglot static/test.json @@ -1,7 +1,6 @@ { "installed": [ {"type": "exe", "file": "usr/bin/prog"}, - {"type": "pdb", "file": "usr/bin/prog"}, - {"type": "file", "file": "usr/lib/libstuff.a"} + {"type": "pdb", "file": "usr/bin/prog"} ] } diff --git a/test cases/rust/9 unit tests/helper.rs b/test cases/rust/9 unit tests/helper.rs new file mode 100644 index 0000000..afe3233 --- /dev/null +++ b/test cases/rust/9 unit tests/helper.rs @@ -0,0 +1,3 @@ +pub fn subtract(a: i32, b: i32) -> i32 { + a - b +} diff --git a/test cases/rust/9 unit tests/meson.build b/test cases/rust/9 unit tests/meson.build index b649abb..b444271 100644 --- a/test cases/rust/9 unit tests/meson.build +++ b/test cases/rust/9 unit tests/meson.build @@ -31,13 +31,17 @@ test( suite : ['foo'], ) -exe = executable('rust_exe', ['test2.rs', 'test.rs']) +exe = executable('rust_exe', ['test2.rs', 'test.rs'], build_by_default : false) rust = import('unstable-rust') rust.test('rust_test_from_exe', exe, should_fail : true) -lib = static_library('rust_static', ['test.rs']) +lib = static_library('rust_static', ['test.rs'], build_by_default : false, rust_crate_type : 'lib') rust.test('rust_test_from_static', lib, args: ['--skip', 'test_add_intentional_fail']) -lib = shared_library('rust_shared', ['test.rs']) +lib = shared_library('rust_shared', ['test.rs'], build_by_default : false) rust.test('rust_test_from_shared', lib, args: ['--skip', 'test_add_intentional_fail']) + +helper = static_library('helper', 'helper.rs') +lib = static_library('rust_link_with', 'test3.rs', build_by_default : false) +rust.test('rust_test_link_with', lib, link_with : helper, rust_args : ['--cfg', 'broken="false"']) diff --git a/test cases/rust/9 unit tests/test2.rs b/test cases/rust/9 unit tests/test2.rs index 9623c7c..e068605 100644 --- a/test cases/rust/9 unit tests/test2.rs +++ b/test cases/rust/9 unit tests/test2.rs @@ -3,8 +3,8 @@ use std::env; fn main() { let args: Vec = env::args().collect(); - let first = args[1].parse::().expect("Invliad value for first argument."); - let second = args[2].parse::().expect("Invliad value for second argument."); + let first = args[1].parse::().expect("Invalid value for first argument."); + let second = args[2].parse::().expect("Invalid value for second argument."); let new = test::add(first, second); println!("New value: {}", new); diff --git a/test cases/rust/9 unit tests/test3.rs b/test cases/rust/9 unit tests/test3.rs new file mode 100644 index 0000000..6d538a0 --- /dev/null +++ b/test cases/rust/9 unit tests/test3.rs @@ -0,0 +1,23 @@ +pub fn add(a: i32, b: i32) -> i32 { + a + b +} + +#[cfg(test)] +mod tests { + extern crate helper; + + use super::*; + + // This is an intentinally broken test that should be turned off by extra rust arguments + #[cfg(not(broken = "false"))] + #[test] + fn test_broken() { + assert_eq!(0, 5); + } + + #[test] + fn test_add_sub() { + let x = helper::subtract(6, 5); + assert_eq!(add(x, 5), 6); + } +} diff --git a/test cases/unit/98 install all targets/bar-custom.txt b/test cases/unit/100 custom target name/file.txt.in similarity index 100% rename from test cases/unit/98 install all targets/bar-custom.txt rename to test cases/unit/100 custom target name/file.txt.in diff --git a/test cases/unit/99 custom target name/meson.build b/test cases/unit/100 custom target name/meson.build similarity index 84% rename from test cases/unit/99 custom target name/meson.build rename to test cases/unit/100 custom target name/meson.build index 8d148a8..8287614 100644 --- a/test cases/unit/99 custom target name/meson.build +++ b/test cases/unit/100 custom target name/meson.build @@ -1,6 +1,6 @@ project('custom target name', 'c') -python = import('python').find_installation() +python = find_program('python3') # Name argument is optional and should default to 'file.txt' custom_target( diff --git a/test cases/unit/99 custom target name/subdir/meson.build b/test cases/unit/100 custom target name/subdir/meson.build similarity index 100% rename from test cases/unit/99 custom target name/subdir/meson.build rename to test cases/unit/100 custom target name/subdir/meson.build diff --git a/test cases/unit/100 relative find program/foo.py b/test cases/unit/101 relative find program/foo.py similarity index 100% rename from test cases/unit/100 relative find program/foo.py rename to test cases/unit/101 relative find program/foo.py diff --git a/test cases/unit/100 relative find program/meson.build b/test cases/unit/101 relative find program/meson.build similarity index 100% rename from test cases/unit/100 relative find program/meson.build rename to test cases/unit/101 relative find program/meson.build diff --git a/test cases/unit/100 relative find program/subdir/meson.build b/test cases/unit/101 relative find program/subdir/meson.build similarity index 100% rename from test cases/unit/100 relative find program/subdir/meson.build rename to test cases/unit/101 relative find program/subdir/meson.build diff --git a/test cases/unit/101 rlib linkage/lib2.rs b/test cases/unit/102 rlib linkage/lib2.rs similarity index 100% rename from test cases/unit/101 rlib linkage/lib2.rs rename to test cases/unit/102 rlib linkage/lib2.rs diff --git a/test cases/unit/101 rlib linkage/main.rs b/test cases/unit/102 rlib linkage/main.rs similarity index 100% rename from test cases/unit/101 rlib linkage/main.rs rename to test cases/unit/102 rlib linkage/main.rs diff --git a/test cases/unit/101 rlib linkage/meson.build b/test cases/unit/102 rlib linkage/meson.build similarity index 100% rename from test cases/unit/101 rlib linkage/meson.build rename to test cases/unit/102 rlib linkage/meson.build diff --git a/test cases/unit/102 python without pkgconfig/meson.build b/test cases/unit/103 python without pkgconfig/meson.build similarity index 100% rename from test cases/unit/102 python without pkgconfig/meson.build rename to test cases/unit/103 python without pkgconfig/meson.build diff --git a/test cases/unit/103 strip/lib.c b/test cases/unit/104 strip/lib.c similarity index 100% rename from test cases/unit/103 strip/lib.c rename to test cases/unit/104 strip/lib.c diff --git a/test cases/unit/103 strip/meson.build b/test cases/unit/104 strip/meson.build similarity index 100% rename from test cases/unit/103 strip/meson.build rename to test cases/unit/104 strip/meson.build diff --git a/test cases/unit/104 debug function/meson.build b/test cases/unit/105 debug function/meson.build similarity index 100% rename from test cases/unit/104 debug function/meson.build rename to test cases/unit/105 debug function/meson.build diff --git a/test cases/unit/105 pkgconfig relocatable with absolute path/meson.build b/test cases/unit/106 pkgconfig relocatable with absolute path/meson.build similarity index 100% rename from test cases/unit/105 pkgconfig relocatable with absolute path/meson.build rename to test cases/unit/106 pkgconfig relocatable with absolute path/meson.build diff --git a/test cases/unit/106 subproject symlink/cp.py b/test cases/unit/107 subproject symlink/cp.py similarity index 100% rename from test cases/unit/106 subproject symlink/cp.py rename to test cases/unit/107 subproject symlink/cp.py diff --git a/test cases/unit/106 subproject symlink/main.c b/test cases/unit/107 subproject symlink/main.c similarity index 100% rename from test cases/unit/106 subproject symlink/main.c rename to test cases/unit/107 subproject symlink/main.c diff --git a/test cases/unit/106 subproject symlink/meson.build b/test cases/unit/107 subproject symlink/meson.build similarity index 100% rename from test cases/unit/106 subproject symlink/meson.build rename to test cases/unit/107 subproject symlink/meson.build diff --git a/test cases/unit/106 subproject symlink/symlinked_subproject/datadir/datafile b/test cases/unit/107 subproject symlink/symlinked_subproject/datadir/datafile similarity index 100% rename from test cases/unit/106 subproject symlink/symlinked_subproject/datadir/datafile rename to test cases/unit/107 subproject symlink/symlinked_subproject/datadir/datafile diff --git a/test cases/unit/106 subproject symlink/symlinked_subproject/datadir/meson.build b/test cases/unit/107 subproject symlink/symlinked_subproject/datadir/meson.build similarity index 100% rename from test cases/unit/106 subproject symlink/symlinked_subproject/datadir/meson.build rename to test cases/unit/107 subproject symlink/symlinked_subproject/datadir/meson.build diff --git a/test cases/unit/106 subproject symlink/symlinked_subproject/meson.build b/test cases/unit/107 subproject symlink/symlinked_subproject/meson.build similarity index 100% rename from test cases/unit/106 subproject symlink/symlinked_subproject/meson.build rename to test cases/unit/107 subproject symlink/symlinked_subproject/meson.build diff --git a/test cases/unit/106 subproject symlink/symlinked_subproject/src.c b/test cases/unit/107 subproject symlink/symlinked_subproject/src.c similarity index 100% rename from test cases/unit/106 subproject symlink/symlinked_subproject/src.c rename to test cases/unit/107 subproject symlink/symlinked_subproject/src.c diff --git a/test cases/unit/108 configure same noop/meson_options.txt b/test cases/unit/108 configure same noop/meson_options.txt deleted file mode 100644 index c406af2..0000000 --- a/test cases/unit/108 configure same noop/meson_options.txt +++ /dev/null @@ -1,5 +0,0 @@ -option( - 'opt', - type : 'string', - value: '', -) diff --git a/test cases/unit/107 new subproject on reconfigure/meson.build b/test cases/unit/108 new subproject on reconfigure/meson.build similarity index 100% rename from test cases/unit/107 new subproject on reconfigure/meson.build rename to test cases/unit/108 new subproject on reconfigure/meson.build diff --git a/test cases/unit/107 new subproject on reconfigure/meson_options.txt b/test cases/unit/108 new subproject on reconfigure/meson_options.txt similarity index 100% rename from test cases/unit/107 new subproject on reconfigure/meson_options.txt rename to test cases/unit/108 new subproject on reconfigure/meson_options.txt diff --git a/test cases/unit/107 new subproject on reconfigure/subprojects/foo/foo.c b/test cases/unit/108 new subproject on reconfigure/subprojects/foo/foo.c similarity index 100% rename from test cases/unit/107 new subproject on reconfigure/subprojects/foo/foo.c rename to test cases/unit/108 new subproject on reconfigure/subprojects/foo/foo.c diff --git a/test cases/unit/107 new subproject on reconfigure/subprojects/foo/meson.build b/test cases/unit/108 new subproject on reconfigure/subprojects/foo/meson.build similarity index 100% rename from test cases/unit/107 new subproject on reconfigure/subprojects/foo/meson.build rename to test cases/unit/108 new subproject on reconfigure/subprojects/foo/meson.build diff --git a/test cases/unit/108 configure same noop/meson.build b/test cases/unit/109 configure same noop/meson.build similarity index 100% rename from test cases/unit/108 configure same noop/meson.build rename to test cases/unit/109 configure same noop/meson.build diff --git a/test cases/unit/109 configure same noop/meson_options.txt b/test cases/unit/109 configure same noop/meson_options.txt new file mode 100644 index 0000000..57b4084 --- /dev/null +++ b/test cases/unit/109 configure same noop/meson_options.txt @@ -0,0 +1,6 @@ +option('string', type : 'string', value: '') +option('boolean', type : 'boolean', value: false) +option('combo', type : 'combo', choices : ['one', 'two', 'three'], value: 'one') +option('integer', type : 'integer', value: 5) +option('array', type : 'array', value: ['one', 'two']) +option('feature', type : 'feature', value : 'enabled') diff --git a/test cases/unit/109 freeze/freeze.c b/test cases/unit/110 freeze/freeze.c similarity index 100% rename from test cases/unit/109 freeze/freeze.c rename to test cases/unit/110 freeze/freeze.c diff --git a/test cases/unit/109 freeze/meson.build b/test cases/unit/110 freeze/meson.build similarity index 100% rename from test cases/unit/109 freeze/meson.build rename to test cases/unit/110 freeze/meson.build diff --git a/test cases/unit/111 replace unencodable xml chars/meson.build b/test cases/unit/111 replace unencodable xml chars/meson.build new file mode 100644 index 0000000..73485b0 --- /dev/null +++ b/test cases/unit/111 replace unencodable xml chars/meson.build @@ -0,0 +1,4 @@ +project('replace unencodable xml chars') + +test_script = find_program('script.py') +test('main', test_script, verbose: true) diff --git a/test cases/unit/111 replace unencodable xml chars/script.py b/test cases/unit/111 replace unencodable xml chars/script.py new file mode 100644 index 0000000..2f2d4d6 --- /dev/null +++ b/test cases/unit/111 replace unencodable xml chars/script.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +import sys + +# Print base string(\nHello Meson\n) to see valid chars are not replaced +print('\n\x48\x65\x6c\x6c\x6f\x20\x4d\x65\x73\x6f\x6e\n') +# Print invalid input from all known unencodable chars +print( + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11' + '\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f') + +# Cover for potential encoding issues +try: + print( + '\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f' + '\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e' + '\x9f\ufdd0\ufdd1\ufdd2\ufdd3\ufdd4\ufdd5\ufdd6\ufdd7\ufdd8' + '\ufdd9\ufdda\ufddb\ufddc\ufddd\ufdde\ufddf\ufde0\ufde1' + '\ufde2\ufde3\ufde4\ufde5\ufde6\ufde7\ufde8\ufde9\ufdea' + '\ufdeb\ufdec\ufded\ufdee\ufdef\ufffe\uffff') +except: + pass + +# Cover for potential encoding issues +try: + if sys.maxunicode >= 0x10000: + print( + '\U0001fffe\U0001ffff\U0002fffe\U0002ffff' + '\U0003fffe\U0003ffff\U0004fffe\U0004ffff' + '\U0005fffe\U0005ffff\U0006fffe\U0006ffff' + '\U0007fffe\U0007ffff\U0008fffe\U0008ffff' + '\U0009fffe\U0009ffff\U000afffe\U000affff' + '\U000bfffe\U000bffff\U000cfffe\U000cffff' + '\U000dfffe\U000dffff\U000efffe\U000effff' + '\U000ffffe\U000fffff\U0010fffe\U0010ffff') +except: + pass diff --git a/test cases/unit/110 classpath/com/mesonbuild/Simple.java b/test cases/unit/112 classpath/com/mesonbuild/Simple.java similarity index 100% rename from test cases/unit/110 classpath/com/mesonbuild/Simple.java rename to test cases/unit/112 classpath/com/mesonbuild/Simple.java diff --git a/test cases/unit/110 classpath/meson.build b/test cases/unit/112 classpath/meson.build similarity index 100% rename from test cases/unit/110 classpath/meson.build rename to test cases/unit/112 classpath/meson.build diff --git a/test cases/unit/113 list build options/meson.build b/test cases/unit/113 list build options/meson.build new file mode 100644 index 0000000..2d634d3 --- /dev/null +++ b/test cases/unit/113 list build options/meson.build @@ -0,0 +1,6 @@ +project('feature user option', 'c') + +feature_opts = get_option('auto_features') +optional_opt = get_option('optional') + +message('Build options:', meson.build_options()) diff --git a/test cases/unit/113 list build options/meson_options.txt b/test cases/unit/113 list build options/meson_options.txt new file mode 100644 index 0000000..d84f22a --- /dev/null +++ b/test cases/unit/113 list build options/meson_options.txt @@ -0,0 +1 @@ +option('optional', type : 'feature', value : 'auto', description : 'An optional feature') diff --git a/test cases/unit/114 complex link cases/main.c b/test cases/unit/114 complex link cases/main.c new file mode 100644 index 0000000..739b413 --- /dev/null +++ b/test cases/unit/114 complex link cases/main.c @@ -0,0 +1,6 @@ +int s3(void); + +int main(int argc, char *argv[]) +{ + return s3(); +} diff --git a/test cases/unit/114 complex link cases/meson.build b/test cases/unit/114 complex link cases/meson.build new file mode 100644 index 0000000..3b4b898 --- /dev/null +++ b/test cases/unit/114 complex link cases/meson.build @@ -0,0 +1,111 @@ +project('complex link cases', 'c') + +cc = meson.get_compiler('c') + +# In all tests, e1 uses s3 which uses s2 which uses s1. + +# Executable links with s3 and s1 but not s2 because it is included in s3. +s1 = static_library('t1-s1', 's1.c') +s2 = static_library('t1-s2', 's2.c', link_with: s1) +s3 = static_library('t1-s3', 's3.c', link_whole: s2) +e = executable('t1-e1', 'main.c', link_with: s3) + +# s3 is installed but not s1 so it has to include s1 too. +# Executable links only s3 because it contains s1 and s2. +s1 = static_library('t2-s1', 's1.c') +s2 = static_library('t2-s2', 's2.c', link_with: s1) +s3 = static_library('t2-s3', 's3.c', link_whole: s2, install: true) +e = executable('t2-e1', 'main.c', link_with: s3) + +# Executable needs to link with s3 only +s1 = static_library('t3-s1', 's1.c') +s2 = static_library('t3-s2', 's2.c', link_with: s1) +s3 = shared_library('t3-s3', 's3.c', link_with: s2) +e = executable('t3-e1', 'main.c', link_with: s3) + +# Executable needs to link with s3 and s2 +s1 = static_library('t4-s1', 's1.c') +s2 = shared_library('t4-s2', 's2.c', link_with: s1) +s3 = static_library('t4-s3', 's3.c', link_with: s2) +e = executable('t4-e1', 'main.c', link_with: s3) + +# Executable needs to link with s3 and s1 +s1 = shared_library('t5-s1', 's1.c') +s2 = static_library('t5-s2', 's2.c', link_with: s1) +s3 = static_library('t5-s3', 's3.c', link_with: s2, install: true) +e = executable('t5-e1', 'main.c', link_with: s3) + +# Executable needs to link with s3 and s2 +s1 = static_library('t6-s1', 's1.c') +s2 = static_library('t6-s2', 's2.c', link_with: s1, install: true) +s3 = static_library('t6-s3', 's3.c', link_with: s2, install: true) +e = executable('t6-e1', 'main.c', link_with: s3) + +# Regression test: s1 gets promoted to link_whole and that used to make all other +# libraries in the list (s2) to be ignored. +# Executable only needs to link with s3. +# See https://github.com/mesonbuild/meson/issues/11956. +s1 = static_library('t7-s1', 's1.c') +s2 = static_library('t7-s2', 's2.c') +s3 = static_library('t7-s3', 's3.c', link_with: [s1, s2], install: true) +e = executable('t7-e1', 'main.c', link_with: s3) + +# Regression test: s3 should come last in the linker command. This seems to be +# required for at least backward compatibility reasons: +# https://github.com/mesonbuild/meson/pull/11957#issuecomment-1629243208 +s1 = static_library('t8-s1', 's1.c') +s2 = static_library('t8-s2', 's2.c') +s3 = static_library('t8-s3', 's3.c') +e = executable('t8-e1', 'main.c', + link_with: [s1, s2], + dependencies: declare_dependency(link_with: s3), +) + +if cc.get_argument_syntax() == 'gcc' + # s1 is an internal static library, using custom target. + s1_o = custom_target( + input: 's1.c', + output: 's1.c.o', + command: [cc.cmd_array(), '-c', '-o', '@OUTPUT@', '@INPUT@'] + ) + s1 = custom_target( + output: 'libt9-s1.a', + command: ['ar', 'rcs', '@OUTPUT@', s1_o], + ) + + # Executable needs to link with s1, s2 and s3. + s2 = static_library('t9-s2', 's2.c', link_with: s1) + s3 = static_library('t9-s3', 's3.c', link_with: s2) + e = executable('t9-e1', 'main.c', link_with: s3) + + # s2 cannot be installed because s1 is not being installed and Meson cannot + # extract object files from the custom target. + testcase expect_error('Cannot link_whole a custom or Rust target \'libt9-s1.a\' into a static library \'t10-s2\'. Instead, pass individual object files with the "objects:" keyword argument if possible. Meson had to promote link to link_whole because \'t10-s2\' is installed but not \'libt9-s1.a\', and thus has to include objects from \'libt9-s1.a\' to be usable.') + s2 = static_library('t10-s2', 's2.c', link_with: s1, install: true) + endtestcase + + # s3 cannot be installed because s1 is not being installed and Meson cannot + # extract object files from the custom target. + testcase expect_error('Cannot link_whole a custom or Rust target \'libt9-s1.a\' into a static library \'t11-s3\'. Instead, pass individual object files with the "objects:" keyword argument if possible. Meson had to promote link to link_whole because \'t11-s3\' is installed but not \'libt9-s1.a\', and thus has to include objects from \'libt9-s1.a\' to be usable.') + s2 = static_library('t11-s2', 's2.c', link_with: s1) + s3 = static_library('t11-s3', 's3.c', link_with: s2, install: true) + endtestcase + + # s1 is an installed static library, using custom target. + s1 = custom_target( + output: 'libt12-s1.a', + command: ['ar', 'rcs', '@OUTPUT@', s1_o], + install: true, + install_dir: get_option('libdir'), + ) + + # Executable needs to link with s1, s2 and s3. + s2 = static_library('t12-s2', 's2.c', link_with: s1, install: true) + s3 = static_library('t12-s3', 's3.c', link_with: s2) + e = executable('t12-e1', 'main.c', link_with: s3) + + # Executable links with s3 and s1 but not s2 because it is included in s3. + s2 = static_library('t13-s2', 's2.c', link_with: s1) + s3 = static_library('t13-s3', 's3.c', link_with: s2, install: true) + e = executable('t13-e1', 'main.c', link_with: s3) +endif diff --git a/test cases/unit/114 complex link cases/s1.c b/test cases/unit/114 complex link cases/s1.c new file mode 100644 index 0000000..68bba49 --- /dev/null +++ b/test cases/unit/114 complex link cases/s1.c @@ -0,0 +1,3 @@ +int s1(void) { + return 1; +} diff --git a/test cases/unit/114 complex link cases/s2.c b/test cases/unit/114 complex link cases/s2.c new file mode 100644 index 0000000..835870c --- /dev/null +++ b/test cases/unit/114 complex link cases/s2.c @@ -0,0 +1,5 @@ +int s1(void); + +int s2(void) { + return s1() + 1; +} diff --git a/test cases/unit/114 complex link cases/s3.c b/test cases/unit/114 complex link cases/s3.c new file mode 100644 index 0000000..08e0620 --- /dev/null +++ b/test cases/unit/114 complex link cases/s3.c @@ -0,0 +1,5 @@ +int s2(void); + +int s3(void) { + return s2() + 1; +} diff --git a/test cases/unit/115 c cpp stds/meson.build b/test cases/unit/115 c cpp stds/meson.build new file mode 100644 index 0000000..0b15efc --- /dev/null +++ b/test cases/unit/115 c cpp stds/meson.build @@ -0,0 +1,6 @@ +project('c cpp stds', 'c', 'cpp', + default_options: [ + 'c_std=gnu89,c89', + 'cpp_std=gnu++98,vc++11', + ], +) diff --git a/test cases/unit/116 empty project/expected_mods.json b/test cases/unit/116 empty project/expected_mods.json new file mode 100644 index 0000000..7463bcb --- /dev/null +++ b/test cases/unit/116 empty project/expected_mods.json @@ -0,0 +1,242 @@ +{ + "stdlib": { + "modules": [ + "__future__", + "__main__", + "_abc", + "_ast", + "_bisect", + "_blake2", + "_bz2", + "_codecs", + "_collections", + "_collections_abc", + "_compat_pickle", + "_compression", + "_datetime", + "_frozen_importlib", + "_frozen_importlib_external", + "_functools", + "_hashlib", + "_imp", + "_io", + "_json", + "_locale", + "_lsprof", + "_lzma", + "_opcode", + "_operator", + "_pickle", + "_posixsubprocess", + "_random", + "_sha512", + "_signal", + "_sitebuiltins", + "_socket", + "_sre", + "_ssl", + "_stat", + "_string", + "_struct", + "_thread", + "_uuid", + "_warnings", + "_weakref", + "_weakrefset", + "abc", + "argparse", + "array", + "ast", + "base64", + "binascii", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "codecs", + "collections", + "collections.abc", + "configparser", + "contextlib", + "copy", + "copyreg", + "dataclasses", + "datetime", + "dis", + "email", + "email._encoded_words", + "email._parseaddr", + "email._policybase", + "email.base64mime", + "email.charset", + "email.encoders", + "email.errors", + "email.feedparser", + "email.header", + "email.iterators", + "email.message", + "email.parser", + "email.quoprimime", + "email.utils", + "encodings", + "encodings.aliases", + "encodings.utf_8", + "enum", + "errno", + "fcntl", + "fnmatch", + "functools", + "genericpath", + "gettext", + "glob", + "hashlib", + "http", + "http.client", + "importlib", + "importlib._bootstrap", + "importlib._bootstrap_external", + "importlib.machinery", + "inspect", + "io", + "itertools", + "json", + "json.decoder", + "json.encoder", + "json.scanner", + "keyword", + "linecache", + "locale", + "lzma", + "marshal", + "math", + "netrc", + "ntpath", + "opcode", + "operator", + "os", + "os.path", + "pathlib", + "pickle", + "platform", + "posix", + "posixpath", + "profile", + "quopri", + "random", + "re", + "reprlib", + "select", + "selectors", + "shlex", + "shutil", + "signal", + "site", + "socket", + "sre_compile", + "sre_constants", + "sre_parse", + "ssl", + "stat", + "string", + "struct", + "subprocess", + "sys", + "tempfile", + "textwrap", + "threading", + "time", + "token", + "tokenize", + "types", + "typing", + "typing.io", + "typing.re", + "urllib", + "urllib.error", + "urllib.parse", + "urllib.request", + "urllib.response", + "uu", + "uuid", + "warnings", + "weakref", + "zipimport", + "zlib" + ], + "count": 162 + }, + "meson": { + "modules": [ + "mesonbuild", + "mesonbuild._pathlib", + "mesonbuild.arglist", + "mesonbuild.ast", + "mesonbuild.ast.interpreter", + "mesonbuild.ast.introspection", + "mesonbuild.ast.postprocess", + "mesonbuild.ast.printer", + "mesonbuild.ast.visitor", + "mesonbuild.backend", + "mesonbuild.backend.backends", + "mesonbuild.backend.ninjabackend", + "mesonbuild.build", + "mesonbuild.compilers", + "mesonbuild.compilers.compilers", + "mesonbuild.compilers.detect", + "mesonbuild.coredata", + "mesonbuild.dependencies", + "mesonbuild.dependencies.base", + "mesonbuild.dependencies.detect", + "mesonbuild.depfile", + "mesonbuild.envconfig", + "mesonbuild.environment", + "mesonbuild.interpreter", + "mesonbuild.interpreter.compiler", + "mesonbuild.interpreter.dependencyfallbacks", + "mesonbuild.interpreter.interpreter", + "mesonbuild.interpreter.interpreterobjects", + "mesonbuild.interpreter.mesonmain", + "mesonbuild.interpreter.primitives", + "mesonbuild.interpreter.primitives.array", + "mesonbuild.interpreter.primitives.boolean", + "mesonbuild.interpreter.primitives.dict", + "mesonbuild.interpreter.primitives.integer", + "mesonbuild.interpreter.primitives.range", + "mesonbuild.interpreter.primitives.string", + "mesonbuild.interpreter.type_checking", + "mesonbuild.interpreterbase", + "mesonbuild.interpreterbase._unholder", + "mesonbuild.interpreterbase.baseobjects", + "mesonbuild.interpreterbase.decorators", + "mesonbuild.interpreterbase.disabler", + "mesonbuild.interpreterbase.exceptions", + "mesonbuild.interpreterbase.helpers", + "mesonbuild.interpreterbase.interpreterbase", + "mesonbuild.interpreterbase.operator", + "mesonbuild.linkers", + "mesonbuild.linkers.base", + "mesonbuild.linkers.detect", + "mesonbuild.mesonlib", + "mesonbuild.mesonmain", + "mesonbuild.mintro", + "mesonbuild.mlog", + "mesonbuild.modules", + "mesonbuild.mparser", + "mesonbuild.msetup", + "mesonbuild.optinterpreter", + "mesonbuild.programs", + "mesonbuild.scripts", + "mesonbuild.scripts.meson_exe", + "mesonbuild.utils", + "mesonbuild.utils.core", + "mesonbuild.utils.platform", + "mesonbuild.utils.posix", + "mesonbuild.utils.universal", + "mesonbuild.utils.vsenv", + "mesonbuild.wrap", + "mesonbuild.wrap.wrap" + ], + "count": 68 + } +} diff --git a/test cases/unit/116 empty project/meson.build b/test cases/unit/116 empty project/meson.build new file mode 100644 index 0000000..b92b9b4 --- /dev/null +++ b/test cases/unit/116 empty project/meson.build @@ -0,0 +1 @@ +project('empty project') diff --git a/test cases/unit/117 genvslite/main.cpp b/test cases/unit/117 genvslite/main.cpp new file mode 100644 index 0000000..ca250bd --- /dev/null +++ b/test cases/unit/117 genvslite/main.cpp @@ -0,0 +1,10 @@ +#include + +int main() { +#ifdef NDEBUG + printf("Non-debug\n"); +#else + printf("Debug\n"); +#endif + return 0; +} diff --git a/test cases/unit/117 genvslite/meson.build b/test cases/unit/117 genvslite/meson.build new file mode 100644 index 0000000..3445d7f --- /dev/null +++ b/test cases/unit/117 genvslite/meson.build @@ -0,0 +1,5 @@ +project('genvslite', 'cpp', + default_options : ['b_ndebug=if-release'] + ) + +exe = executable('genvslite', 'main.cpp') diff --git a/test cases/unit/118 meson package cache dir/cache_dir/bar/meson.build b/test cases/unit/118 meson package cache dir/cache_dir/bar/meson.build new file mode 100644 index 0000000..dca36f6 --- /dev/null +++ b/test cases/unit/118 meson package cache dir/cache_dir/bar/meson.build @@ -0,0 +1 @@ +project('bar') diff --git a/test cases/unit/118 meson package cache dir/cache_dir/foo.zip b/test cases/unit/118 meson package cache dir/cache_dir/foo.zip new file mode 100644 index 0000000..91bc36a Binary files /dev/null and b/test cases/unit/118 meson package cache dir/cache_dir/foo.zip differ diff --git a/test cases/unit/118 meson package cache dir/meson.build b/test cases/unit/118 meson package cache dir/meson.build new file mode 100644 index 0000000..2057bba --- /dev/null +++ b/test cases/unit/118 meson package cache dir/meson.build @@ -0,0 +1,4 @@ +project('meson package cache dir') + +subproject('foo') +subproject('bar') diff --git a/test cases/unit/118 meson package cache dir/subprojects/bar.wrap b/test cases/unit/118 meson package cache dir/subprojects/bar.wrap new file mode 100644 index 0000000..3ec5834 --- /dev/null +++ b/test cases/unit/118 meson package cache dir/subprojects/bar.wrap @@ -0,0 +1,3 @@ +[wrap-file] +directory = bar + diff --git a/test cases/unit/118 meson package cache dir/subprojects/foo.wrap b/test cases/unit/118 meson package cache dir/subprojects/foo.wrap new file mode 100644 index 0000000..b7dd41d --- /dev/null +++ b/test cases/unit/118 meson package cache dir/subprojects/foo.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = foo +source_url = http://server.invalid/foo.zip +source_filename = foo.zip +source_hash = c5dd7e8fca93045f736c83700686722b0fbc20b7dc4597b295060684c5b05b72 diff --git a/test cases/unit/119 openssl cmake bug/meson.build b/test cases/unit/119 openssl cmake bug/meson.build new file mode 100644 index 0000000..d08a8ef --- /dev/null +++ b/test cases/unit/119 openssl cmake bug/meson.build @@ -0,0 +1,5 @@ +project('bug', 'cpp') + +# When cmake is not available, +# this triggers the bug described in #12098 +openssl_dep = dependency('openssl') diff --git a/test cases/unit/119 openssl cmake bug/nativefile.ini b/test cases/unit/119 openssl cmake bug/nativefile.ini new file mode 100644 index 0000000..dd6b0ff --- /dev/null +++ b/test cases/unit/119 openssl cmake bug/nativefile.ini @@ -0,0 +1,7 @@ +[binaries] + +cmake = '/path/to/nothing' + +[built-in options] + +pkg_config_path = '' \ No newline at end of file diff --git a/test cases/unit/120 rewrite/meson.build b/test cases/unit/120 rewrite/meson.build new file mode 100644 index 0000000..7d0330b --- /dev/null +++ b/test cases/unit/120 rewrite/meson.build @@ -0,0 +1,189 @@ +# This file should expose all possible meson syntaxes + # and ensure the AstInterpreter and RawPrinter are able + + # to parse and write a file identical to the original. + + project ( # project comment 1 + # project comment 2 + 'rewrite' , # argument comment + # project comment 3 + 'cpp', + 'c', + default_options: [ + 'unity=on', + 'unity_size=50', # number of cpp / unity. default is 4... + 'warning_level=2', # eqv to /W3 + 'werror=true', # treat warnings as errors + 'b_ndebug=if-release', # disable assert in Release + 'cpp_eh=a', # /EHa exception handling + 'cpp_std=c++17', + 'cpp_winlibs=' + ','.join([ # array comment + # in array + # comment + 'kernel32.lib', + 'user32.lib', + 'gdi32.lib', + 'winspool.lib', + 'comdlg32.lib', + 'advapi32.lib', + 'shell32.lib' + # before comma comment + , + # after comma comment + 'ole32.lib', + 'oleaut32.lib', + 'uuid.lib', + 'odbc32.lib', + 'odbccp32.lib', + 'Delayimp.lib', # For delay loaded dll + 'OLDNAMES.lib', + 'dbghelp.lib', + 'psapi.lib', + ]), + ], + meson_version: '>=1.2', + version: '1.0.0', + ) # project comment 4 + +cppcoro_dep = dependency('andreasbuhr-cppcoro-cppcoro') +cppcoro = declare_dependency( + dependencies: [cppcoro_dep.partial_dependency( + includes: true, + link_args: true, + links: true, + sources: true, + )], + # '/await:strict' allows to use rather than with C++17. + # We can remove '/await:strict' once we update to C++20. + compile_args: ['/await:strict'], + # includes:true doesn't work for now in partial_dependency() + # This line could be removed once https://github.com/mesonbuild/meson/pull/10122 is released. + include_directories: cppcoro_dep.get_variable('includedir1'), +) + + +if get_option('unicode') #if comment +#if comment 2 + mfc=cpp_compiler.find_library(get_option('debug')?'mfc140ud':'mfc140u') + # if comment 3 +else#elsecommentnowhitespaces + # else comment 1 + mfc = cpp_compiler.find_library( get_option( 'debug' ) ? 'mfc140d' : 'mfc140') +# else comment 2 +endif #endif comment + + +assert(1 in [1, 2], '''1 should be in [1, 2]''') +assert(3 not in [1, 2], '''3 shouldn't be in [1, 2]''') +assert(not (3 in [1, 2]), '''3 shouldn't be in [1, 2]''') + +assert('b' in ['a', 'b'], ''''b' should be in ['a', 'b']''') +assert('c' not in ['a', 'b'], ''''c' shouldn't be in ['a', 'b']''') + +assert(exe1 in [exe1, exe2], ''''exe1 should be in [exe1, exe2]''') +assert(exe3 not in [exe1, exe2], ''''exe3 shouldn't be in [exe1, exe2]''') + +assert('a' in {'a': 'b'}, '''1 should be in {'a': 'b'}''') +assert('b'not in{'a':'b'}, '''1 should be in {'a': 'b'}''') + +assert('a'in'abc') +assert('b' not in 'def') + + +w = 'world' +d = {'a': 1, 'b': 0b10101010, 'c': 'pi', 'd': '''a +b +c''', 'e': f'hello @w@', 'f': f'''triple + formatted + string # this is not a comment + hello @w@ +''', 'g': [1, 2, 3], + + 'h' # comment a + : # comment b +0xDEADBEEF # comment c +, # comment d +'hh': 0xfeedc0de, # lowercase hexa +'hhh': 0XaBcD0123, # mixed case hexa +'oo': 0O123456, # upper O octa +'bb': 0B1111, # upper B binary +'i': {'aa': 11, # this is a comment + 'bb': 22}, # a comment inside a dict +'o': 0o754, +'m': -12, # minus number +'eq': 1 + 3 - 3 % 4 + -( 7 * 8 ), +} # end of dict comment + +hw = d['e'] +one = d['g'][0] + w += '!' + + +components = { + 'foo': ['foo.c'], + 'bar': ['bar.c'], + 'baz': ['baz.c'], # this line is indented with a tab! +} + +# compute a configuration based on system dependencies, custom logic +conf = configuration_data() +conf.set('USE_FOO', 1) + +# Determine the sources to compile +sources_to_compile = [] +foreach name, sources : components + if conf.get('USE_@0@'.format(name.to_upper()), 0) == 1 + sources_to_compile += sources + endif +endforeach + + +items = ['a', 'continue', 'b', 'break', 'c'] +result = [] +foreach i : items + if i == 'continue' + continue + elif i == 'break' + break + endif + result += i +endforeach +# result is ['a', 'b'] + + + +if a and b + # do something +endif +if c or d + # do something +endif +if not e + # do something +endif +if not (f or g) + # do something +endif + +single_quote = 'contains a \' character' +string_escapes = '\\\'\a\b\f\n\r\t\v\046\x26\u2D4d\U00002d4d\N{GREEK CAPITAL LETTER DELTA}' +no_string_escapes = '''\\\'\a\b\f\n\r\t\v\046\x26\u2D4d\U00002d4d\N{GREEK CAPITAL LETTER DELTA}''' + +# FIXME: is it supposed to work? (cont_eol inside string) +# cont_string = 'blablabla\ +# blablabla' + +# cont_eol with whitespace and comments after +if a \ # comment in cont 1 + and b \ # comment in cont 2 + or c # comment in cont 3 + message('ok') +endif + +if a \ + or b + debug('help!') +endif + + +# End of file comment with no linebreak \ No newline at end of file diff --git a/test cases/unit/121 executable suffix/main.c b/test cases/unit/121 executable suffix/main.c new file mode 100644 index 0000000..78f2de1 --- /dev/null +++ b/test cases/unit/121 executable suffix/main.c @@ -0,0 +1 @@ +int main(void) { return 0; } diff --git a/test cases/unit/121 executable suffix/meson.build b/test cases/unit/121 executable suffix/meson.build new file mode 100644 index 0000000..8f95226 --- /dev/null +++ b/test cases/unit/121 executable suffix/meson.build @@ -0,0 +1,3 @@ +project('exectuable suffix', 'c') +foo = executable('foo', 'main.c') +foo_bin = executable('foo', 'main.c', name_suffix: 'bin') diff --git a/test cases/unit/15 prebuilt object/meson.build b/test cases/unit/15 prebuilt object/meson.build index b542d1c..81aa2ae 100644 --- a/test cases/unit/15 prebuilt object/meson.build +++ b/test cases/unit/15 prebuilt object/meson.build @@ -35,6 +35,12 @@ e += executable('exe5', 'main.c', ct[0]) sl2 = static_library('lib6', sources: ct) e += executable('exe6', sources: 'main.c', objects: sl2.extract_all_objects(recursive: true)) +e += executable('exe7', sources: 'main.c', objects: ct) +e += executable('exe8', sources: 'main.c', objects: ct[0]) + +sl3 = static_library('lib9', objects: ct) +e += executable('exe9', sources: 'main.c', objects: sl2.extract_all_objects(recursive: true)) + foreach i : e test(i.name(), i) endforeach diff --git a/test cases/unit/33 cross file overrides always args/ubuntu-armhf-overrides.txt b/test cases/unit/33 cross file overrides always args/ubuntu-armhf-overrides.txt index a00a7d1..d687b29 100644 --- a/test cases/unit/33 cross file overrides always args/ubuntu-armhf-overrides.txt +++ b/test cases/unit/33 cross file overrides always args/ubuntu-armhf-overrides.txt @@ -6,7 +6,7 @@ cpp = '/usr/bin/arm-linux-gnueabihf-g++' rust = ['rustc', '--target', 'arm-unknown-linux-gnueabihf', '-C', 'linker=/usr/bin/arm-linux-gnueabihf-gcc-7'] ar = '/usr/arm-linux-gnueabihf/bin/ar' strip = '/usr/arm-linux-gnueabihf/bin/strip' -pkgconfig = '/usr/bin/arm-linux-gnueabihf-pkg-config' +pkg-config = '/usr/bin/arm-linux-gnueabihf-pkg-config' [properties] root = '/usr/arm-linux-gnueabihf' diff --git a/test cases/unit/36 exe_wrapper behaviour/broken-cross.txt b/test cases/unit/36 exe_wrapper behaviour/broken-cross.txt index a5a3931..3615f92 100644 --- a/test cases/unit/36 exe_wrapper behaviour/broken-cross.txt +++ b/test cases/unit/36 exe_wrapper behaviour/broken-cross.txt @@ -3,7 +3,7 @@ c = '/usr/bin/x86_64-w64-mingw32-gcc' cpp = '/usr/bin/x86_64-w64-mingw32-g++' ar = '/usr/bin/x86_64-w64-mingw32-ar' strip = '/usr/bin/x86_64-w64-mingw32-strip' -pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' +pkg-config = '/usr/bin/x86_64-w64-mingw32-pkg-config' windres = '/usr/bin/x86_64-w64-mingw32-windres' exe_wrapper = 'broken' diff --git a/test cases/unit/91 install skip subprojects/foo/foofile b/test cases/unit/39 external, internal library rpath/built library/foo.py similarity index 100% rename from test cases/unit/91 install skip subprojects/foo/foofile rename to test cases/unit/39 external, internal library rpath/built library/foo.py diff --git a/test cases/unit/39 external, internal library rpath/built library/meson.build b/test cases/unit/39 external, internal library rpath/built library/meson.build index 07fe7bb..6399cdc 100644 --- a/test cases/unit/39 external, internal library rpath/built library/meson.build +++ b/test cases/unit/39 external, internal library rpath/built library/meson.build @@ -2,6 +2,8 @@ project('built library', 'c') cc = meson.get_compiler('c') +import('python').find_installation().install_sources('foo.py') + if host_machine.system() != 'cygwin' # bar_in_system has undefined symbols, but still must be found bar_system_dep = cc.find_library('bar_in_system') diff --git a/test cases/unit/63 cmake parser/meson.build b/test cases/unit/63 cmake parser/meson.build index 472561d..d2a80c9 100644 --- a/test cases/unit/63 cmake parser/meson.build +++ b/test cases/unit/63 cmake parser/meson.build @@ -9,11 +9,11 @@ assert(dep.get_variable(cmake : 'VAR_WITH_SPACES') == 'With Spaces', 'set() with assert(dep.get_variable(cmake : 'VAR_WITHOUT_SPACES_PS') == 'NoSpaces', 'set(PARENT_SCOPE) without spaces incorrect') assert(dep.get_variable(cmake : 'VAR_WITH_SPACES_PS') == 'With Spaces', 'set(PARENT_SCOPE) with spaces incorrect') -assert(dep.get_variable(cmake : 'VAR_THAT_IS_UNSET', default_value : 'sentinal') == 'sentinal', 'set() to unset is incorrect') +assert(dep.get_variable(cmake : 'VAR_THAT_IS_UNSET', default_value : 'sentinel') == 'sentinel', 'set() to unset is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_NS') == 'foo', 'set(CACHED) without spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_WS') == 'foo bar', 'set(CACHED STRING) with spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_NS') == 'foo;bar', 'set(CACHED STRING) without spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_WS') == 'foo;foo bar;bar', 'set(CACHED STRING[]) with spaces is incorrect') # We don't support this, so it should be unset. -assert(dep.get_variable(cmake : 'ENV{var}', default_value : 'sentinal') == 'sentinal', 'set(ENV) should be ignored') +assert(dep.get_variable(cmake : 'ENV{var}', default_value : 'sentinel') == 'sentinel', 'set(ENV) should be ignored') diff --git a/test cases/unit/64 alias target/meson.build b/test cases/unit/64 alias target/meson.build index bcd4005..197897b 100644 --- a/test cases/unit/64 alias target/meson.build +++ b/test cases/unit/64 alias target/meson.build @@ -1,6 +1,6 @@ project('alias target', 'c') -python3 = import('python').find_installation() +python3 = find_program('python3') exe_target = executable('prog', 'main.c', build_by_default : false) diff --git a/test cases/unit/68 clang-tidy/cttest_fixed.cpp b/test cases/unit/68 clang-tidy/cttest_fixed.cpp new file mode 100644 index 0000000..5b422f6 --- /dev/null +++ b/test cases/unit/68 clang-tidy/cttest_fixed.cpp @@ -0,0 +1,7 @@ +#include + +int main(int, char**) { + bool intbool = true; + printf("Intbool is %d\n", (int)intbool); + return 0; +} diff --git a/test cases/unit/70 cross test passed/meson.build b/test cases/unit/70 cross test passed/meson.build index 4deb74b..3a09a77 100644 --- a/test cases/unit/70 cross test passed/meson.build +++ b/test cases/unit/70 cross test passed/meson.build @@ -6,7 +6,7 @@ project( e = executable('exec', 'src/main.c') -py = import('python').find_installation() +py = find_program('python3') test('root', e) test('main', py, args : [meson.current_source_dir() / 'script.py', e]) diff --git a/test cases/unit/71 summary/meson.build b/test cases/unit/71 summary/meson.build index ce97fb3..76fc545 100644 --- a/test cases/unit/71 summary/meson.build +++ b/test cases/unit/71 summary/meson.build @@ -11,7 +11,7 @@ summary({'Some boolean': false, 'enabled_opt': get_option('enabled_opt'), }, section: 'Configuration') summary({'missing prog': find_program('xyzzy', required: false), - 'existing prog': import('python').find_installation(), + 'existing prog': find_program(get_option('python')).full_path(), 'missing dep': dependency('', required: false), 'external dep': dependency('zlib', required: false), 'internal dep': declare_dependency(), diff --git a/test cases/unit/71 summary/meson_options.txt b/test cases/unit/71 summary/meson_options.txt index 281c3b6..cf3f32c 100644 --- a/test cases/unit/71 summary/meson_options.txt +++ b/test cases/unit/71 summary/meson_options.txt @@ -1 +1,2 @@ option('enabled_opt', type: 'feature', value: 'auto') +option('python', type: 'string') diff --git a/test cases/unit/73 dep files/meson.build b/test cases/unit/73 dep files/meson.build index 4829f56..af7f6e4 100644 --- a/test cases/unit/73 dep files/meson.build +++ b/test cases/unit/73 dep files/meson.build @@ -1,6 +1,6 @@ project('test', 'c') -python = import('python').find_installation() +python = find_program('python3') lib = library('foo', 'foo.c') diff --git a/test cases/unit/77 nostdlib/subprojects/mylibc/libc.c b/test cases/unit/77 nostdlib/subprojects/mylibc/libc.c index 67261cb..fb9a9c2 100644 --- a/test cases/unit/77 nostdlib/subprojects/mylibc/libc.c +++ b/test cases/unit/77 nostdlib/subprojects/mylibc/libc.c @@ -1,5 +1,5 @@ /* Do not use this as the basis of your own libc. - * The code is probably unoptimal or wonky, as I + * The code is probably suboptimal or wonky, as I * had no prior experience with this, but instead * just fiddled with the code until it worked. */ diff --git a/test cases/unit/87 run native test/meson.build b/test cases/unit/87 run native test/meson.build index 3bf419c..e706dd7 100644 --- a/test cases/unit/87 run native test/meson.build +++ b/test cases/unit/87 run native test/meson.build @@ -1,6 +1,6 @@ project('run native test', ['c']) -executable('terget_exe', 'main.c') +executable('target_exe', 'main.c') native_exe = executable('native_exe', 'main.c', native: true) test('native_exe', native_exe, args: ['native_test_has_run.stamp']) diff --git a/test cases/unit/90 devenv/meson.build b/test cases/unit/90 devenv/meson.build index 3b0bb6a..316b20c 100644 --- a/test cases/unit/90 devenv/meson.build +++ b/test cases/unit/90 devenv/meson.build @@ -15,3 +15,11 @@ meson.add_devenv(env) # This exe links on a library built in another directory. On Windows this means # PATH must contain builddir/subprojects/sub to be able to run it. executable('app', 'main.c', dependencies: foo_dep, install: true) + +env = environment({'TEST_C': ['/prefix']}, method: 'prepend') +meson.add_devenv(env) +env = environment({'TEST_C': ['/suffix']}, method: 'append') +meson.add_devenv(env) + +# Reproducer for https://github.com/mesonbuild/meson/issues/12032 +pkgconf = import('pkgconfig') diff --git a/test cases/unit/90 devenv/test-devenv.py b/test cases/unit/90 devenv/test-devenv.py index 75497ff..07bcf61 100755 --- a/test cases/unit/90 devenv/test-devenv.py +++ b/test cases/unit/90 devenv/test-devenv.py @@ -6,3 +6,4 @@ assert os.environ['MESON_DEVENV'] == '1' assert os.environ['MESON_PROJECT_NAME'] == 'devenv' assert os.environ['TEST_A'] == '1' assert os.environ['TEST_B'] == '0+1+2+3+4' +assert os.environ['TEST_C'] == os.pathsep.join(['/prefix', '/suffix']) diff --git a/test cases/unit/91 install skip subprojects/foo.c b/test cases/unit/92 install skip subprojects/foo.c similarity index 100% rename from test cases/unit/91 install skip subprojects/foo.c rename to test cases/unit/92 install skip subprojects/foo.c diff --git a/test cases/unit/91 install skip subprojects/foo.dat b/test cases/unit/92 install skip subprojects/foo.dat similarity index 100% rename from test cases/unit/91 install skip subprojects/foo.dat rename to test cases/unit/92 install skip subprojects/foo.dat diff --git a/test cases/unit/91 install skip subprojects/foo.h b/test cases/unit/92 install skip subprojects/foo.h similarity index 100% rename from test cases/unit/91 install skip subprojects/foo.h rename to test cases/unit/92 install skip subprojects/foo.h diff --git a/test cases/failing/54 wrong static crate type/foo.rs b/test cases/unit/92 install skip subprojects/foo/foofile similarity index 100% rename from test cases/failing/54 wrong static crate type/foo.rs rename to test cases/unit/92 install skip subprojects/foo/foofile diff --git a/test cases/unit/91 install skip subprojects/meson.build b/test cases/unit/92 install skip subprojects/meson.build similarity index 100% rename from test cases/unit/91 install skip subprojects/meson.build rename to test cases/unit/92 install skip subprojects/meson.build diff --git a/test cases/unit/91 install skip subprojects/subprojects/bar/bar.c b/test cases/unit/92 install skip subprojects/subprojects/bar/bar.c similarity index 100% rename from test cases/unit/91 install skip subprojects/subprojects/bar/bar.c rename to test cases/unit/92 install skip subprojects/subprojects/bar/bar.c diff --git a/test cases/unit/91 install skip subprojects/subprojects/bar/bar.dat b/test cases/unit/92 install skip subprojects/subprojects/bar/bar.dat similarity index 100% rename from test cases/unit/91 install skip subprojects/subprojects/bar/bar.dat rename to test cases/unit/92 install skip subprojects/subprojects/bar/bar.dat diff --git a/test cases/unit/91 install skip subprojects/subprojects/bar/bar.h b/test cases/unit/92 install skip subprojects/subprojects/bar/bar.h similarity index 100% rename from test cases/unit/91 install skip subprojects/subprojects/bar/bar.h rename to test cases/unit/92 install skip subprojects/subprojects/bar/bar.h diff --git a/test cases/unit/91 install skip subprojects/subprojects/bar/bar/barfile b/test cases/unit/92 install skip subprojects/subprojects/bar/bar/barfile similarity index 100% rename from test cases/unit/91 install skip subprojects/subprojects/bar/bar/barfile rename to test cases/unit/92 install skip subprojects/subprojects/bar/bar/barfile diff --git a/test cases/unit/91 install skip subprojects/subprojects/bar/meson.build b/test cases/unit/92 install skip subprojects/subprojects/bar/meson.build similarity index 100% rename from test cases/unit/91 install skip subprojects/subprojects/bar/meson.build rename to test cases/unit/92 install skip subprojects/subprojects/bar/meson.build diff --git a/test cases/unit/92 new subproject in configured project/meson.build b/test cases/unit/93 new subproject in configured project/meson.build similarity index 100% rename from test cases/unit/92 new subproject in configured project/meson.build rename to test cases/unit/93 new subproject in configured project/meson.build diff --git a/test cases/unit/92 new subproject in configured project/meson_options.txt b/test cases/unit/93 new subproject in configured project/meson_options.txt similarity index 100% rename from test cases/unit/92 new subproject in configured project/meson_options.txt rename to test cases/unit/93 new subproject in configured project/meson_options.txt diff --git a/test cases/unit/92 new subproject in configured project/subprojects/sub/foo.c b/test cases/unit/93 new subproject in configured project/subprojects/sub/foo.c similarity index 100% rename from test cases/unit/92 new subproject in configured project/subprojects/sub/foo.c rename to test cases/unit/93 new subproject in configured project/subprojects/sub/foo.c diff --git a/test cases/unit/92 new subproject in configured project/subprojects/sub/meson.build b/test cases/unit/93 new subproject in configured project/subprojects/sub/meson.build similarity index 100% rename from test cases/unit/92 new subproject in configured project/subprojects/sub/meson.build rename to test cases/unit/93 new subproject in configured project/subprojects/sub/meson.build diff --git a/test cases/unit/93 clangformat/.clang-format b/test cases/unit/94 clangformat/.clang-format similarity index 100% rename from test cases/unit/93 clangformat/.clang-format rename to test cases/unit/94 clangformat/.clang-format diff --git a/test cases/unit/93 clangformat/.clang-format-ignore b/test cases/unit/94 clangformat/.clang-format-ignore similarity index 100% rename from test cases/unit/93 clangformat/.clang-format-ignore rename to test cases/unit/94 clangformat/.clang-format-ignore diff --git a/test cases/unit/93 clangformat/.clang-format-include b/test cases/unit/94 clangformat/.clang-format-include similarity index 100% rename from test cases/unit/93 clangformat/.clang-format-include rename to test cases/unit/94 clangformat/.clang-format-include diff --git a/test cases/unit/93 clangformat/meson.build b/test cases/unit/94 clangformat/meson.build similarity index 100% rename from test cases/unit/93 clangformat/meson.build rename to test cases/unit/94 clangformat/meson.build diff --git a/test cases/unit/93 clangformat/src/badformat.cpp b/test cases/unit/94 clangformat/not-included/badformat.cpp similarity index 100% rename from test cases/unit/93 clangformat/src/badformat.cpp rename to test cases/unit/94 clangformat/not-included/badformat.cpp diff --git a/test cases/unit/93 clangformat/src/badformat.c b/test cases/unit/94 clangformat/src/badformat.c similarity index 100% rename from test cases/unit/93 clangformat/src/badformat.c rename to test cases/unit/94 clangformat/src/badformat.c diff --git a/test cases/unit/93 clangformat/not-included/badformat.cpp b/test cases/unit/94 clangformat/src/badformat.cpp similarity index 100% rename from test cases/unit/93 clangformat/not-included/badformat.cpp rename to test cases/unit/94 clangformat/src/badformat.cpp diff --git a/test cases/unit/94 custominc/easytogrepfor/genh.py b/test cases/unit/95 custominc/easytogrepfor/genh.py similarity index 100% rename from test cases/unit/94 custominc/easytogrepfor/genh.py rename to test cases/unit/95 custominc/easytogrepfor/genh.py diff --git a/test cases/unit/94 custominc/easytogrepfor/meson.build b/test cases/unit/95 custominc/easytogrepfor/meson.build similarity index 100% rename from test cases/unit/94 custominc/easytogrepfor/meson.build rename to test cases/unit/95 custominc/easytogrepfor/meson.build diff --git a/test cases/unit/94 custominc/helper.c b/test cases/unit/95 custominc/helper.c similarity index 100% rename from test cases/unit/94 custominc/helper.c rename to test cases/unit/95 custominc/helper.c diff --git a/test cases/unit/94 custominc/meson.build b/test cases/unit/95 custominc/meson.build similarity index 100% rename from test cases/unit/94 custominc/meson.build rename to test cases/unit/95 custominc/meson.build diff --git a/test cases/unit/94 custominc/prog.c b/test cases/unit/95 custominc/prog.c similarity index 100% rename from test cases/unit/94 custominc/prog.c rename to test cases/unit/95 custominc/prog.c diff --git a/test cases/unit/94 custominc/prog2.c b/test cases/unit/95 custominc/prog2.c similarity index 100% rename from test cases/unit/94 custominc/prog2.c rename to test cases/unit/95 custominc/prog2.c diff --git a/test cases/unit/95 implicit force fallback/meson.build b/test cases/unit/96 implicit force fallback/meson.build similarity index 100% rename from test cases/unit/95 implicit force fallback/meson.build rename to test cases/unit/96 implicit force fallback/meson.build diff --git a/test cases/unit/95 implicit force fallback/subprojects/something/meson.build b/test cases/unit/96 implicit force fallback/subprojects/something/meson.build similarity index 100% rename from test cases/unit/95 implicit force fallback/subprojects/something/meson.build rename to test cases/unit/96 implicit force fallback/subprojects/something/meson.build diff --git a/test cases/unit/96 compiler.links file arg/meson.build b/test cases/unit/97 compiler.links file arg/meson.build similarity index 100% rename from test cases/unit/96 compiler.links file arg/meson.build rename to test cases/unit/97 compiler.links file arg/meson.build diff --git a/test cases/unit/97 compiler.links file arg/test.c b/test cases/unit/97 compiler.links file arg/test.c new file mode 100644 index 0000000..78f2de1 --- /dev/null +++ b/test cases/unit/97 compiler.links file arg/test.c @@ -0,0 +1 @@ +int main(void) { return 0; } diff --git a/test cases/unit/97 link full name/libtestprovider/meson.build b/test cases/unit/98 link full name/libtestprovider/meson.build similarity index 100% rename from test cases/unit/97 link full name/libtestprovider/meson.build rename to test cases/unit/98 link full name/libtestprovider/meson.build diff --git a/test cases/unit/97 link full name/libtestprovider/provider.c b/test cases/unit/98 link full name/libtestprovider/provider.c similarity index 100% rename from test cases/unit/97 link full name/libtestprovider/provider.c rename to test cases/unit/98 link full name/libtestprovider/provider.c diff --git a/test cases/unit/97 link full name/proguser/meson.build b/test cases/unit/98 link full name/proguser/meson.build similarity index 100% rename from test cases/unit/97 link full name/proguser/meson.build rename to test cases/unit/98 link full name/proguser/meson.build diff --git a/test cases/unit/97 link full name/proguser/receiver.c b/test cases/unit/98 link full name/proguser/receiver.c similarity index 84% rename from test cases/unit/97 link full name/proguser/receiver.c rename to test cases/unit/98 link full name/proguser/receiver.c index 65e9d8e..d661ae4 100644 --- a/test cases/unit/97 link full name/proguser/receiver.c +++ b/test cases/unit/98 link full name/proguser/receiver.c @@ -6,7 +6,7 @@ int __attribute__((weak)) get_checked(void) { #define CHECK_VALUE (100) #define TEST_SUCCESS (0) -#define TEST_FAILTURE (-1) +#define TEST_FAILURE (-1) int main(void) { if (get_checked() == CHECK_VALUE) { @@ -14,5 +14,5 @@ int main(void) { return TEST_SUCCESS; } fprintf(stdout,"bad\n"); - return TEST_FAILTURE; + return TEST_FAILURE; } diff --git a/test cases/failing/53 wrong shared crate type/foo.rs b/test cases/unit/99 install all targets/bar-custom.txt similarity index 100% rename from test cases/failing/53 wrong shared crate type/foo.rs rename to test cases/unit/99 install all targets/bar-custom.txt diff --git a/test cases/unit/99 install all targets/bar-devel.h b/test cases/unit/99 install all targets/bar-devel.h new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/99 install all targets/bar-notag.txt b/test cases/unit/99 install all targets/bar-notag.txt new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/98 install all targets/custom_files/data.txt b/test cases/unit/99 install all targets/custom_files/data.txt similarity index 100% rename from test cases/unit/98 install all targets/custom_files/data.txt rename to test cases/unit/99 install all targets/custom_files/data.txt diff --git a/test cases/unit/99 install all targets/excludes/excluded.txt b/test cases/unit/99 install all targets/excludes/excluded.txt new file mode 100644 index 0000000..59b0644 --- /dev/null +++ b/test cases/unit/99 install all targets/excludes/excluded.txt @@ -0,0 +1 @@ +Excluded diff --git a/test cases/unit/99 install all targets/excludes/excluded/placeholder.txt b/test cases/unit/99 install all targets/excludes/excluded/placeholder.txt new file mode 100644 index 0000000..3b94f91 --- /dev/null +++ b/test cases/unit/99 install all targets/excludes/excluded/placeholder.txt @@ -0,0 +1 @@ +Placeholder diff --git a/test cases/unit/99 install all targets/excludes/installed.txt b/test cases/unit/99 install all targets/excludes/installed.txt new file mode 100644 index 0000000..8437692 --- /dev/null +++ b/test cases/unit/99 install all targets/excludes/installed.txt @@ -0,0 +1 @@ +Installed diff --git a/test cases/unit/99 install all targets/foo.in b/test cases/unit/99 install all targets/foo.in new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/99 install all targets/foo1-devel.h b/test cases/unit/99 install all targets/foo1-devel.h new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/98 install all targets/subdir/lib.c b/test cases/unit/99 install all targets/lib.c similarity index 100% rename from test cases/unit/98 install all targets/subdir/lib.c rename to test cases/unit/99 install all targets/lib.c diff --git a/test cases/unit/98 install all targets/subdir/main.c b/test cases/unit/99 install all targets/main.c similarity index 100% rename from test cases/unit/98 install all targets/subdir/main.c rename to test cases/unit/99 install all targets/main.c diff --git a/test cases/unit/98 install all targets/meson.build b/test cases/unit/99 install all targets/meson.build similarity index 93% rename from test cases/unit/98 install all targets/meson.build rename to test cases/unit/99 install all targets/meson.build index 3065b5f..c5f33a0 100644 --- a/test cases/unit/98 install all targets/meson.build +++ b/test cases/unit/99 install all targets/meson.build @@ -1,5 +1,7 @@ project('install tag', 'c') +subproject('subproject') + subdir('subdir') # Those files should not be tagged @@ -94,6 +96,12 @@ install_subdir('custom_files', install_dir: get_option('datadir'), install_tag: 'custom', ) +install_subdir('excludes', + install_dir: get_option('datadir'), + install_tag: 'custom', + exclude_directories: 'excluded', + exclude_files: 'excluded.txt', +) # First is custom, 2nd is devel, 3rd has no tag custom_target('ct3', diff --git a/test cases/unit/98 install all targets/subdir/script.py b/test cases/unit/99 install all targets/script.py similarity index 100% rename from test cases/unit/98 install all targets/subdir/script.py rename to test cases/unit/99 install all targets/script.py diff --git a/test cases/unit/99 install all targets/subdir/bar2-devel.h b/test cases/unit/99 install all targets/subdir/bar2-devel.h new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/99 install all targets/subdir/foo2.in b/test cases/unit/99 install all targets/subdir/foo2.in new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/99 install all targets/subdir/foo3-devel.h b/test cases/unit/99 install all targets/subdir/foo3-devel.h new file mode 100644 index 0000000..e69de29 diff --git a/test cases/unit/98 install all targets/lib.c b/test cases/unit/99 install all targets/subdir/lib.c similarity index 100% rename from test cases/unit/98 install all targets/lib.c rename to test cases/unit/99 install all targets/subdir/lib.c diff --git a/test cases/unit/98 install all targets/main.c b/test cases/unit/99 install all targets/subdir/main.c similarity index 100% rename from test cases/unit/98 install all targets/main.c rename to test cases/unit/99 install all targets/subdir/main.c diff --git a/test cases/unit/98 install all targets/subdir/meson.build b/test cases/unit/99 install all targets/subdir/meson.build similarity index 100% rename from test cases/unit/98 install all targets/subdir/meson.build rename to test cases/unit/99 install all targets/subdir/meson.build diff --git a/test cases/unit/98 install all targets/script.py b/test cases/unit/99 install all targets/subdir/script.py similarity index 100% rename from test cases/unit/98 install all targets/script.py rename to test cases/unit/99 install all targets/subdir/script.py diff --git a/test cases/unit/99 install all targets/subprojects/subproject/aaa.txt b/test cases/unit/99 install all targets/subprojects/subproject/aaa.txt new file mode 100644 index 0000000..43d5a8e --- /dev/null +++ b/test cases/unit/99 install all targets/subprojects/subproject/aaa.txt @@ -0,0 +1 @@ +AAA diff --git a/test cases/unit/99 install all targets/subprojects/subproject/bbb.txt b/test cases/unit/99 install all targets/subprojects/subproject/bbb.txt new file mode 100644 index 0000000..ba62923 --- /dev/null +++ b/test cases/unit/99 install all targets/subprojects/subproject/bbb.txt @@ -0,0 +1 @@ +BBB diff --git a/test cases/unit/99 install all targets/subprojects/subproject/meson.build b/test cases/unit/99 install all targets/subprojects/subproject/meson.build new file mode 100644 index 0000000..ff474ac --- /dev/null +++ b/test cases/unit/99 install all targets/subprojects/subproject/meson.build @@ -0,0 +1,5 @@ +project('subproject') + +install_data('aaa.txt') + +install_data('bbb.txt', install_tag: 'data') diff --git a/manual tests/7 vala composite widgets/meson.build b/test cases/vala/27 file as command line argument/meson.build similarity index 100% rename from manual tests/7 vala composite widgets/meson.build rename to test cases/vala/27 file as command line argument/meson.build diff --git a/manual tests/7 vala composite widgets/my-resources.xml b/test cases/vala/27 file as command line argument/my-resources.xml similarity index 100% rename from manual tests/7 vala composite widgets/my-resources.xml rename to test cases/vala/27 file as command line argument/my-resources.xml diff --git a/manual tests/7 vala composite widgets/mywidget.ui b/test cases/vala/27 file as command line argument/mywidget.ui similarity index 100% rename from manual tests/7 vala composite widgets/mywidget.ui rename to test cases/vala/27 file as command line argument/mywidget.ui diff --git a/manual tests/7 vala composite widgets/mywidget.vala b/test cases/vala/27 file as command line argument/mywidget.vala similarity index 100% rename from manual tests/7 vala composite widgets/mywidget.vala rename to test cases/vala/27 file as command line argument/mywidget.vala diff --git a/test cases/vala/5 target glib/GLib.Thread.vala b/test cases/vala/5 target glib/GLib.Thread.vala index 7018821..fc82919 100644 --- a/test cases/vala/5 target glib/GLib.Thread.vala +++ b/test cases/vala/5 target glib/GLib.Thread.vala @@ -1,4 +1,4 @@ -extern int get_ret_code (); +extern void * get_ret_code (); public class MyThread : Object { public int x_times { get; private set; } diff --git a/test cases/vala/5 target glib/retcode.c b/test cases/vala/5 target glib/retcode.c index abca9bf..df44de5 100644 --- a/test cases/vala/5 target glib/retcode.c +++ b/test cases/vala/5 target glib/retcode.c @@ -1,5 +1,5 @@ -int +void * get_ret_code (void) { - return 42; + return (void *) (int) 42; } diff --git a/test cases/vala/7 shared library/lib/meson.build b/test cases/vala/7 shared library/lib/meson.build index edeeb96..bbd3862 100644 --- a/test cases/vala/7 shared library/lib/meson.build +++ b/test cases/vala/7 shared library/lib/meson.build @@ -33,3 +33,8 @@ shared_library('installed_vala_onlyvapi', 'mylib.vala', dependencies : valadeps, install : true, install_dir : [false, false, join_paths(get_option('datadir'), 'vala', 'vapi')]) + +# Regression test: Vala libraries were broken when also installing python modules. +# https://gitlab.gnome.org/GNOME/gitg/-/issues/412 +python = import('python').find_installation() +python.install_sources('source.py') diff --git a/test cases/vala/7 shared library/lib/source.py b/test cases/vala/7 shared library/lib/source.py new file mode 100644 index 0000000..e69de29 diff --git a/test cases/vala/7 shared library/test.json b/test cases/vala/7 shared library/test.json index eee3c3d..08bd707 100644 --- a/test cases/vala/7 shared library/test.json +++ b/test cases/vala/7 shared library/test.json @@ -9,6 +9,8 @@ {"type": "file", "file": "usr/include/installed_vala_onlyh.h"}, {"type": "file", "file": "usr/share/vala/vapi/installed_vala_all.vapi"}, {"type": "file", "file": "usr/share/vala-1.0/vapi/installed_vala_all_nolib.vapi"}, - {"type": "file", "file": "usr/share/vala/vapi/installed_vala_onlyvapi.vapi"} + {"type": "file", "file": "usr/share/vala/vapi/installed_vala_onlyvapi.vapi"}, + {"type": "python_file", "file": "usr/@PYTHON_PURELIB@/source.py"}, + {"type": "python_bytecode", "file": "usr/@PYTHON_PURELIB@/source.py"} ] } diff --git a/test cases/warning/9 meson.options/meson.build b/test cases/warning/9 meson.options/meson.build new file mode 100644 index 0000000..59c3872 --- /dev/null +++ b/test cases/warning/9 meson.options/meson.build @@ -0,0 +1,3 @@ +project('options too old', meson_version : '>= 0.63') + +subproject('no-warn') diff --git a/test cases/warning/9 meson.options/meson.options b/test cases/warning/9 meson.options/meson.options new file mode 100644 index 0000000..b84ee83 --- /dev/null +++ b/test cases/warning/9 meson.options/meson.options @@ -0,0 +1 @@ +option('foo', type : 'string') diff --git a/test cases/warning/9 meson.options/subprojects/no-warn/meson.build b/test cases/warning/9 meson.options/subprojects/no-warn/meson.build new file mode 100644 index 0000000..f86fbf7 --- /dev/null +++ b/test cases/warning/9 meson.options/subprojects/no-warn/meson.build @@ -0,0 +1 @@ +project('has both no warn', meson_version : '>= 0.63') diff --git a/test cases/warning/9 meson.options/subprojects/no-warn/meson.options b/test cases/warning/9 meson.options/subprojects/no-warn/meson.options new file mode 100644 index 0000000..b84ee83 --- /dev/null +++ b/test cases/warning/9 meson.options/subprojects/no-warn/meson.options @@ -0,0 +1 @@ +option('foo', type : 'string') diff --git a/test cases/warning/9 meson.options/subprojects/no-warn/meson_options.txt b/test cases/warning/9 meson.options/subprojects/no-warn/meson_options.txt new file mode 100644 index 0000000..b84ee83 --- /dev/null +++ b/test cases/warning/9 meson.options/subprojects/no-warn/meson_options.txt @@ -0,0 +1 @@ +option('foo', type : 'string') diff --git a/test cases/warning/9 meson.options/test.json b/test cases/warning/9 meson.options/test.json new file mode 100644 index 0000000..f711924 --- /dev/null +++ b/test cases/warning/9 meson.options/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "WARNING: Project targets '>= 0.63' but uses feature introduced in '1.1': meson.options file. Use meson_options.txt instead" + } + ] +} diff --git a/test cases/windows/10 vs module defs generated custom target/subdir/meson.build b/test cases/windows/10 vs module defs generated custom target/subdir/meson.build index c4ae33a..0277692 100644 --- a/test cases/windows/10 vs module defs generated custom target/subdir/meson.build +++ b/test cases/windows/10 vs module defs generated custom target/subdir/meson.build @@ -5,3 +5,5 @@ def_file = custom_target('gen_def', output: 'somedll.def') shlib = shared_library('somedll', 'somedll.c', vs_module_defs: def_file) + +shared_library('somedll2', 'somedll.c', vs_module_defs: def_file[0]) diff --git a/test cases/windows/13 test argument extra paths/test/meson.build b/test cases/windows/13 test argument extra paths/test/meson.build index 2e608be..4daccae 100644 --- a/test cases/windows/13 test argument extra paths/test/meson.build +++ b/test cases/windows/13 test argument extra paths/test/meson.build @@ -1,3 +1,3 @@ -python3 = import('python').find_installation('') +python3 = find_program('python3') test('run_exe', python3, args: [files('test_run_exe.py')[0], barexe]) diff --git a/test cases/windows/9 vs module defs generated/exe.def b/test cases/windows/9 vs module defs generated/exe.def new file mode 100644 index 0000000..9031a84 --- /dev/null +++ b/test cases/windows/9 vs module defs generated/exe.def @@ -0,0 +1,2 @@ +EXPORTS + exefunc diff --git a/test cases/windows/9 vs module defs generated/meson.build b/test cases/windows/9 vs module defs generated/meson.build index 7728ca7..fd06442 100644 --- a/test cases/windows/9 vs module defs generated/meson.build +++ b/test cases/windows/9 vs module defs generated/meson.build @@ -1,5 +1,5 @@ project('generated_dll_module_defs', 'c') subdir('subdir') -exe = executable('prog', 'prog.c', link_with : shlib) +exe = executable('prog', 'prog.c', link_with : shlib, vs_module_defs : 'exe.def') test('runtest', exe) diff --git a/test cases/windows/9 vs module defs generated/prog.c b/test cases/windows/9 vs module defs generated/prog.c index 066ac22..4466a8c 100644 --- a/test cases/windows/9 vs module defs generated/prog.c +++ b/test cases/windows/9 vs module defs generated/prog.c @@ -1,5 +1,9 @@ int somedllfunc(void); +int exefunc(void) { + return 42; +} + int main(void) { - return somedllfunc() == 42 ? 0 : 1; + return somedllfunc() == exefunc() ? 0 : 1; } diff --git a/tools/boost_names.py b/tools/boost_names.py index b716ccb..f27d524 100755 --- a/tools/boost_names.py +++ b/tools/boost_names.py @@ -277,12 +277,12 @@ def main() -> int: ''')) for mod in modules: - desc_excaped = re.sub(r"'", "\\'", mod.desc) + desc_escaped = re.sub(r"'", "\\'", mod.desc) print(textwrap.indent(textwrap.dedent(f"""\ '{mod.key}': BoostModule( name='{mod.name}', key='{mod.key}', - desc='{desc_excaped}', + desc='{desc_escaped}', libs={[x.name for x in mod.libs]}, ),\ """), ' ')) diff --git a/tools/cmake2meson.py b/tools/cmake2meson.py index a12d9cf..7889cd8 100755 --- a/tools/cmake2meson.py +++ b/tools/cmake2meson.py @@ -119,7 +119,7 @@ class Parser: return Statement(cur.value, args) def arguments(self) -> T.List[T.Union[Token, T.Any]]: - args = [] # type: T.List[T.Union[Token, T.Any]] + args: T.List[T.Union[Token, T.Any]] = [] if self.accept('lparen'): args.append(self.arguments()) self.expect('rparen') @@ -159,7 +159,7 @@ class Converter: self.cmake_root = Path(cmake_root).expanduser() self.indent_unit = ' ' self.indent_level = 0 - self.options = [] # type: T.List[tuple] + self.options: T.List[T.Tuple[str, str, T.Optional[str]]] = [] def convert_args(self, args: T.List[Token], as_array: bool = True) -> str: res = [] diff --git a/tools/dircondenser.py b/tools/dircondenser.py index fa299e9..1b262f9 100755 --- a/tools/dircondenser.py +++ b/tools/dircondenser.py @@ -49,6 +49,8 @@ def get_entries() -> T.List[T.Tuple[int, str]]: numstr = int(number) except ValueError: raise SystemExit(f'Dir name {e} does not start with a number.') + if 'includedirxyz' in e: + continue entries.append((numstr, rest)) entries.sort() return entries diff --git a/unittests/__init__.py b/unittests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 8b78590..86a6b61 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -41,9 +41,10 @@ from mesonbuild.mesonlib import ( BuildDirLock, MachineChoice, is_windows, is_osx, is_cygwin, is_dragonflybsd, is_sunos, windows_proof_rmtree, python_command, version_compare, split_args, quote_arg, relpath, is_linux, git, search_version, do_conf_file, do_conf_str, default_prefix, - MesonException, EnvironmentException, OptionKey, ExecutableSerialisation, EnvironmentVariables, + MesonException, EnvironmentException, OptionKey, windows_proof_rm ) +from mesonbuild.programs import ExternalProgram from mesonbuild.compilers.mixins.clang import ClangCompiler from mesonbuild.compilers.mixins.gnu import GnuCompiler @@ -54,9 +55,11 @@ from mesonbuild.compilers import ( detect_static_linker, detect_c_compiler, compiler_from_language, detect_compiler_for ) +from mesonbuild.linkers import linkers -from mesonbuild.dependencies import PkgConfigDependency +from mesonbuild.dependencies.pkgconfig import PkgConfigDependency from mesonbuild.build import Target, ConfigurationData, Executable, SharedLibrary, StaticLibrary +from mesonbuild import mtest import mesonbuild.modules.pkgconfig from mesonbuild.scripts import destdir_join @@ -150,20 +153,33 @@ class AllPlatformTests(BasePlatformTests): (result, missing_variables, confdata_useless) = do_conf_str('configuration_file', in_data, confdata, variable_format = vformat) return '\n'.join(result) - def check_formats(confdata, result): + def check_meson_format(confdata, result): self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result) + + def check_cmake_format_simple(confdata, result): + self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), result) + + def check_cmake_formats_full(confdata, result): self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result) self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result) + def check_formats(confdata, result): + check_meson_format(confdata, result) + check_cmake_formats_full(confdata, result) + confdata = ConfigurationData() # Key error as they do not exists check_formats(confdata, '/* #undef VAR */\n') # Check boolean confdata.values = {'VAR': (False, 'description')} - check_formats(confdata, '#undef VAR\n') + check_meson_format(confdata, '#undef VAR\n') + check_cmake_formats_full(confdata, '/* #undef VAR */\n') + confdata.values = {'VAR': (True, 'description')} - check_formats(confdata, '#define VAR\n') + check_meson_format(confdata, '#define VAR\n') + check_cmake_format_simple(confdata, '#define VAR\n') + check_cmake_formats_full(confdata, '#define VAR 1\n') # Check string confdata.values = {'VAR': ('value', 'description')} @@ -173,6 +189,25 @@ class AllPlatformTests(BasePlatformTests): confdata.values = {'VAR': (10, 'description')} check_formats(confdata, '#define VAR 10\n') + # Checking if cmakedefine behaves as it does with cmake + confdata.values = {'VAR': ("var", 'description')} + self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '#define VAR var\n') + + confdata.values = {'VAR': (True, 'description')} + self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 1\n') + + confdata.values = {'VAR': (0, 'description')} + self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') + confdata.values = {'VAR': (False, 'description')} + self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') + + confdata.values = {} + self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n') + self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '/* #undef VAR */\n') + + confdata.values = {'VAR': (5, 'description')} + self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), '#define VAR\n') + # Check multiple string with cmake formats confdata.values = {'VAR': ('value', 'description')} self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n') @@ -353,13 +388,13 @@ class AllPlatformTests(BasePlatformTests): static_linker = detect_static_linker(env, cc) if is_windows(): raise SkipTest('https://github.com/mesonbuild/meson/issues/1526') - if not isinstance(static_linker, mesonbuild.linkers.ArLinker): + if not isinstance(static_linker, linkers.ArLinker): raise SkipTest('static linker is not `ar`') # Configure self.init(testdir) # Get name of static library targets = self.introspect('--targets') - self.assertEqual(len(targets), 1) + self.assertGreaterEqual(len(targets), 1) libname = targets[0]['filename'][0] # Build and get contents of static library self.build() @@ -396,6 +431,62 @@ class AllPlatformTests(BasePlatformTests): self.assertTrue(compdb[3]['file'].endswith("libfile4.c")) # FIXME: We don't have access to the linker command + def test_replace_unencodable_xml_chars(self): + ''' + Test that unencodable xml chars are replaced with their + printable representation + https://github.com/mesonbuild/meson/issues/9894 + ''' + # Create base string(\nHello Meson\n) to see valid chars are not replaced + base_string_invalid = '\n\x48\x65\x6c\x6c\x6f\x20\x4d\x65\x73\x6f\x6e\n' + base_string_valid = '\nHello Meson\n' + # Create invalid input from all known unencodable chars + invalid_string = ( + '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11' + '\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f' + '\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f' + '\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e' + '\x9f\ufdd0\ufdd1\ufdd2\ufdd3\ufdd4\ufdd5\ufdd6\ufdd7\ufdd8' + '\ufdd9\ufdda\ufddb\ufddc\ufddd\ufdde\ufddf\ufde0\ufde1' + '\ufde2\ufde3\ufde4\ufde5\ufde6\ufde7\ufde8\ufde9\ufdea' + '\ufdeb\ufdec\ufded\ufdee\ufdef\ufffe\uffff') + if sys.maxunicode >= 0x10000: + invalid_string = invalid_string + ( + '\U0001fffe\U0001ffff\U0002fffe\U0002ffff' + '\U0003fffe\U0003ffff\U0004fffe\U0004ffff' + '\U0005fffe\U0005ffff\U0006fffe\U0006ffff' + '\U0007fffe\U0007ffff\U0008fffe\U0008ffff' + '\U0009fffe\U0009ffff\U000afffe\U000affff' + '\U000bfffe\U000bffff\U000cfffe\U000cffff' + '\U000dfffe\U000dffff\U000efffe\U000effff' + '\U000ffffe\U000fffff\U0010fffe\U0010ffff') + + valid_string = base_string_valid + repr(invalid_string)[1:-1] + base_string_valid + invalid_string = base_string_invalid + invalid_string + base_string_invalid + fixed_string = mtest.replace_unencodable_xml_chars(invalid_string) + self.assertEqual(fixed_string, valid_string) + + def test_replace_unencodable_xml_chars_unit(self): + ''' + Test that unencodable xml chars are replaced with their + printable representation + https://github.com/mesonbuild/meson/issues/9894 + ''' + if not shutil.which('xmllint'): + raise SkipTest('xmllint not installed') + testdir = os.path.join(self.unit_test_dir, '111 replace unencodable xml chars') + self.init(testdir) + tests_command_output = self.run_tests() + junit_xml_logs = Path(self.logdir, 'testlog.junit.xml') + subprocess.run(['xmllint', junit_xml_logs], check=True) + # Ensure command output and JSON / text logs are not mangled. + raw_output_sample = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b' + assert raw_output_sample in tests_command_output + text_log = Path(self.logdir, 'testlog.txt').read_text() + assert raw_output_sample in text_log + json_log = json.loads(Path(self.logdir, 'testlog.json').read_bytes()) + assert raw_output_sample in json_log['stdout'] + def test_run_target_files_path(self): ''' Test that run_targets are run from the correct directory @@ -440,14 +531,15 @@ class AllPlatformTests(BasePlatformTests): self.init(testdir) intro = self.introspect('--installed') expected = { + 'nested_elided/sub': 'share', + 'new_directory': 'share/new_directory', + 'sub/sub1': 'share/sub1', + 'sub1': 'share/sub1', 'sub2': 'share/sub2', + 'sub3': '/usr/share/sub3', + 'sub_elided': 'share', 'subdir/sub1': 'share/sub1', 'subdir/sub_elided': 'share', - 'sub1': 'share/sub1', - 'sub/sub1': 'share/sub1', - 'sub_elided': 'share', - 'nested_elided/sub': 'share', - 'new_directory': 'share/new_directory', } self.assertEqual(len(intro), len(expected)) @@ -542,7 +634,7 @@ class AllPlatformTests(BasePlatformTests): self.run_tests() def test_implicit_forcefallback(self): - testdir = os.path.join(self.unit_test_dir, '95 implicit force fallback') + testdir = os.path.join(self.unit_test_dir, '96 implicit force fallback') with self.assertRaises(subprocess.CalledProcessError): self.init(testdir) self.init(testdir, extra_args=['--wrap-mode=forcefallback']) @@ -678,7 +770,7 @@ class AllPlatformTests(BasePlatformTests): with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f: exclude_suites_log = f.read() self.assertNotIn('buggy', exclude_suites_log) - # --suite overrides add_test_setup(xclude_suites) + # --suite overrides add_test_setup(exclude_suites) self._run(self.mtest_command + ['--setup=good', '--suite', 'buggy']) with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f: include_suites_log = f.read() @@ -816,6 +908,30 @@ class AllPlatformTests(BasePlatformTests): o = self._run(self.mtest_command + ['--list', '--no-rebuild']) self.assertNotIn('Regenerating build files.', o) + def test_unexisting_test_name(self): + testdir = os.path.join(self.unit_test_dir, '4 suite selection') + self.init(testdir) + self.build() + + self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['notatest']) + + def test_select_test_using_wildcards(self): + testdir = os.path.join(self.unit_test_dir, '4 suite selection') + self.init(testdir) + self.build() + + o = self._run(self.mtest_command + ['--list', 'mainprj*']) + self.assertIn('mainprj-failing_test', o) + self.assertIn('mainprj-successful_test_no_suite', o) + self.assertNotIn('subprj', o) + + o = self._run(self.mtest_command + ['--list', '*succ*', 'subprjm*:']) + self.assertIn('mainprj-successful_test_no_suite', o) + self.assertIn('subprjmix-failing_test', o) + self.assertIn('subprjmix-successful_test', o) + self.assertIn('subprjsucc-successful_test_no_suite', o) + self.assertNotIn('subprjfail-failing_test', o) + def test_build_by_default(self): testdir = os.path.join(self.common_test_dir, '129 build by default') self.init(testdir) @@ -833,6 +949,53 @@ class AllPlatformTests(BasePlatformTests): self.build(target=('barprog' + exe_suffix)) self.assertPathExists(exe2) + def test_build_generated_pyx_directly(self): + # Check that the transpile stage also includes + # dependencies for the compilation stage as dependencies + testdir = os.path.join("test cases/cython", '2 generated sources') + env = get_fake_env(testdir, self.builddir, self.prefix) + try: + detect_compiler_for(env, "cython", MachineChoice.HOST, True) + except EnvironmentException: + raise SkipTest("Cython is not installed") + self.init(testdir) + # Need to get the full target name of the pyx.c target + # (which is unfortunately not provided by introspection :( ) + # We'll need to dig into the generated sources + targets = self.introspect('--targets') + name = None + for target in targets: + for target_sources in target["target_sources"]: + for generated_source in target_sources.get("generated_sources", []): + if "includestuff.pyx.c" in generated_source: + name = generated_source + break + # Split the path (we only want the includestuff.cpython-blahblah.so.p/includestuff.pyx.c) + name = os.path.normpath(name).split(os.sep)[-2:] + name = os.sep.join(name) # Glue list into a string + self.build(target=name) + + def test_build_pyx_depfiles(self): + # building regularly and then touching a depfile dependency should rebuild + testdir = os.path.join("test cases/cython", '2 generated sources') + env = get_fake_env(testdir, self.builddir, self.prefix) + try: + cython = detect_compiler_for(env, "cython", MachineChoice.HOST, True) + if not version_compare(cython.version, '>=0.29.33'): + raise SkipTest('Cython is too old') + except EnvironmentException: + raise SkipTest("Cython is not installed") + self.init(testdir) + + targets = self.introspect('--targets') + for target in targets: + if target['name'].startswith('simpleinclude'): + name = target['name'] + self.build() + self.utime(os.path.join(testdir, 'simplestuff.pxi')) + self.assertBuildRelinkedOnlyTarget(name) + + def test_internal_include_order(self): if mesonbuild.environment.detect_msys2_arch() and ('MESON_RSP_THRESHOLD' in os.environ): raise SkipTest('Test does not yet support gcc rsp files on msys2') @@ -910,8 +1073,8 @@ class AllPlatformTests(BasePlatformTests): intel = IntelGnuLikeCompiler msvc = (VisualStudioCCompiler, VisualStudioCPPCompiler) clangcl = (ClangClCCompiler, ClangClCPPCompiler) - ar = mesonbuild.linkers.ArLinker - lib = mesonbuild.linkers.VisualStudioLinker + ar = linkers.ArLinker + lib = linkers.VisualStudioLinker langs = [('c', 'CC'), ('cpp', 'CXX')] if not is_windows() and platform.machine().lower() != 'e2k': langs += [('objc', 'OBJC'), ('objcpp', 'OBJCXX')] @@ -955,39 +1118,39 @@ class AllPlatformTests(BasePlatformTests): if isinstance(cc, gnu): self.assertIsInstance(linker, ar) if is_osx(): - self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) + self.assertIsInstance(cc.linker, linkers.AppleDynamicLinker) elif is_sunos(): - self.assertIsInstance(cc.linker, (mesonbuild.linkers.SolarisDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)) + self.assertIsInstance(cc.linker, (linkers.SolarisDynamicLinker, linkers.GnuLikeDynamicLinkerMixin)) else: - self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin) + self.assertIsInstance(cc.linker, linkers.GnuLikeDynamicLinkerMixin) if isinstance(cc, clangcl): self.assertIsInstance(linker, lib) - self.assertIsInstance(cc.linker, mesonbuild.linkers.ClangClDynamicLinker) + self.assertIsInstance(cc.linker, linkers.ClangClDynamicLinker) if isinstance(cc, clang): self.assertIsInstance(linker, ar) if is_osx(): - self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) + self.assertIsInstance(cc.linker, linkers.AppleDynamicLinker) elif is_windows(): # This is clang, not clang-cl. This can be either an # ld-like linker of link.exe-like linker (usually the # former for msys2, the latter otherwise) - self.assertIsInstance(cc.linker, (mesonbuild.linkers.MSVCDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)) + self.assertIsInstance(cc.linker, (linkers.MSVCDynamicLinker, linkers.GnuLikeDynamicLinkerMixin)) else: - self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin) + self.assertIsInstance(cc.linker, linkers.GnuLikeDynamicLinkerMixin) if isinstance(cc, intel): self.assertIsInstance(linker, ar) if is_osx(): - self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker) + self.assertIsInstance(cc.linker, linkers.AppleDynamicLinker) elif is_windows(): - self.assertIsInstance(cc.linker, mesonbuild.linkers.XilinkDynamicLinker) + self.assertIsInstance(cc.linker, linkers.XilinkDynamicLinker) else: - self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuDynamicLinker) + self.assertIsInstance(cc.linker, linkers.GnuDynamicLinker) if isinstance(cc, msvc): self.assertTrue(is_windows()) self.assertIsInstance(linker, lib) self.assertEqual(cc.id, 'msvc') self.assertTrue(hasattr(cc, 'is_64')) - self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker) + self.assertIsInstance(cc.linker, linkers.MSVCDynamicLinker) # If we're on Windows CI, we know what the compiler will be if 'arch' in os.environ: if os.environ['arch'] == 'x64': @@ -1034,7 +1197,7 @@ class AllPlatformTests(BasePlatformTests): for cmd in self.get_compdb(): # Get compiler split = split_args(cmd['command']) - if split[0] == 'ccache': + if split[0] in ('ccache', 'sccache'): compiler = split[1] else: compiler = split[0] @@ -1091,7 +1254,7 @@ class AllPlatformTests(BasePlatformTests): def test_preprocessor_checks_CPPFLAGS(self): ''' - Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but + Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS/CXXFLAGS but not LDFLAGS. ''' testdir = os.path.join(self.common_test_dir, '132 get define') @@ -1102,11 +1265,13 @@ class AllPlatformTests(BasePlatformTests): # % and # confuse the MSVC preprocessor # !, ^, *, and < confuse lcc preprocessor value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`' - for env_var in ['CPPFLAGS', 'CFLAGS']: + for env_var in [{'CPPFLAGS'}, {'CFLAGS', 'CXXFLAGS'}]: env = {} - env[env_var] = f'-D{define}="{value}"' + for i in env_var: + env[i] = f'-D{define}="{value}"' env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read' self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env) + self.new_builddir() def test_custom_target_exe_data_deterministic(self): testdir = os.path.join(self.common_test_dir, '109 custom target capture') @@ -1215,6 +1380,8 @@ class AllPlatformTests(BasePlatformTests): # This assumes all of the targets support lto for t in targets: for s in t['target_sources']: + if 'linker' in s: + continue for e in expected: self.assertIn(e, s['parameters']) @@ -1240,8 +1407,8 @@ class AllPlatformTests(BasePlatformTests): targets = self.introspect('--targets') # This assumes all of the targets support lto for t in targets: - for s in t['target_sources']: - self.assertTrue(expected.issubset(set(s['parameters'])), f'Incorrect values for {t["name"]}') + for src in t['target_sources']: + self.assertTrue(expected.issubset(set(src['parameters'])), f'Incorrect values for {t["name"]}') def test_dist_git(self): if not shutil.which('git'): @@ -1481,12 +1648,12 @@ class AllPlatformTests(BasePlatformTests): test. Needs to be a unit test because it accesses Meson internals. ''' testdir = os.path.join(self.common_test_dir, '150 reserved targets') - targets = mesonbuild.coredata.FORBIDDEN_TARGET_NAMES + targets = set(mesonbuild.coredata.FORBIDDEN_TARGET_NAMES) # We don't actually define a target with this name - targets.pop('build.ninja') + targets.remove('build.ninja') # Remove this to avoid multiple entries with the same name # but different case. - targets.pop('PHONY') + targets.remove('PHONY') for i in targets: self.assertPathExists(os.path.join(testdir, i)) @@ -1631,6 +1798,48 @@ class AllPlatformTests(BasePlatformTests): self.build() self.run_tests() + def test_prebuilt_shared_lib_rpath_same_prefix(self) -> None: + (cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env() + orig_tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared') + + # Put the shared library in a location that shares a common prefix with + # the source directory: + # + # .../ + # foo-lib/ + # libalexandria.so + # foo/ + # meson.build + # ... + # + # This allows us to check that the .../foo-lib/libalexandria.so path is + # preserved correctly when meson processes it. + with tempfile.TemporaryDirectory() as d: + libdir = os.path.join(d, 'foo-lib') + os.mkdir(libdir) + + source = os.path.join(orig_tdir, 'alexandria.c') + objectfile = os.path.join(libdir, 'alexandria.' + object_suffix) + impfile = os.path.join(libdir, 'alexandria.lib') + if cc.get_argument_syntax() == 'msvc': + shlibfile = os.path.join(libdir, 'alexandria.' + shared_suffix) + elif is_cygwin(): + shlibfile = os.path.join(libdir, 'cygalexandria.' + shared_suffix) + else: + shlibfile = os.path.join(libdir, 'libalexandria.' + shared_suffix) + # Ensure MSVC extra files end up in the directory that gets deleted + # at the end + with chdir(libdir): + self.build_shared_lib(cc, source, objectfile, shlibfile, impfile) + + tdir = os.path.join(d, 'foo') + shutil.copytree(orig_tdir, tdir) + + # Run the test + self.init(tdir, extra_args=[f'-Dsearch_dir={libdir}']) + self.build() + self.run_tests() + def test_underscore_prefix_detection_list(self) -> None: ''' Test the underscore detection hardcoded lookup list @@ -1749,7 +1958,7 @@ class AllPlatformTests(BasePlatformTests): check_pcfile('libvartest2.pc', relocatable=False) self.wipe() - testdir_abs = os.path.join(self.unit_test_dir, '105 pkgconfig relocatable with absolute path') + testdir_abs = os.path.join(self.unit_test_dir, '106 pkgconfig relocatable with absolute path') self.init(testdir_abs) check_pcfile('libsimple.pc', relocatable=True, levels=3) @@ -1830,6 +2039,25 @@ class AllPlatformTests(BasePlatformTests): original = get_opt() self.assertDictEqual(original, expected) + def test_executable_names(self): + testdir = os.path.join(self.unit_test_dir, '121 executable suffix') + self.init(testdir) + self.build() + exe1 = os.path.join(self.builddir, 'foo' + exe_suffix) + exe2 = os.path.join(self.builddir, 'foo.bin') + self.assertPathExists(exe1) + self.assertPathExists(exe2) + self.assertNotEqual(exe1, exe2) + + # Wipe and run the compile command against the target names + self.init(testdir, extra_args=['--wipe']) + self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo']) + self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo.bin']) + self.assertPathExists(exe1) + self.assertPathExists(exe2) + self.assertNotEqual(exe1, exe2) + + def opt_has(self, name, value): res = self.introspect('--buildoptions') found = False @@ -1901,6 +2129,33 @@ class AllPlatformTests(BasePlatformTests): self.assertEqual(item['value'], ['b', 'c']) self.assertEqual(item['choices'], ['b', 'c', 'd']) + def test_options_listed_in_build_options(self) -> None: + """Detect when changed options become listed in build options.""" + testdir = os.path.join(self.unit_test_dir, '113 list build options') + + out = self.init(testdir) + for line in out.splitlines(): + if line.startswith('Message: Build options:'): + self.assertNotIn('-Dauto_features=auto', line) + self.assertNotIn('-Doptional=auto', line) + + self.wipe() + self.mac_ci_delay() + + out = self.init(testdir, extra_args=['-Dauto_features=disabled', '-Doptional=enabled']) + for line in out.splitlines(): + if line.startswith('Message: Build options:'): + self.assertIn('-Dauto_features=disabled', line) + self.assertIn('-Doptional=enabled', line) + + self.setconf('-Doptional=disabled') + out = self.build() + for line in out.splitlines(): + if line.startswith('Message: Build options:'): + self.assertIn('-Dauto_features=disabled', line) + self.assertNotIn('-Doptional=enabled', line) + self.assertIn('-Doptional=disabled', line) + def test_subproject_promotion(self): testdir = os.path.join(self.unit_test_dir, '12 promote') workdir = os.path.join(self.builddir, 'work') @@ -1927,6 +2182,7 @@ class AllPlatformTests(BasePlatformTests): self.assertFalse(os.path.isfile(promoted_wrap)) subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir) self.assertTrue(os.path.isfile(promoted_wrap)) + self.new_builddir() # Ensure builddir is not parent or workdir self.init(workdir) self.build() @@ -1980,9 +2236,9 @@ class AllPlatformTests(BasePlatformTests): for (t, f) in [ ('10 out of bounds', 'meson.build'), ('18 wrong plusassign', 'meson.build'), - ('59 bad option argument', 'meson_options.txt'), - ('97 subdir parse error', os.path.join('subdir', 'meson.build')), - ('98 invalid option file', 'meson_options.txt'), + ('57 bad option argument', 'meson_options.txt'), + ('95 subdir parse error', os.path.join('subdir', 'meson.build')), + ('96 invalid option file', 'meson_options.txt'), ]: tdir = os.path.join(self.src_root, 'test cases', 'failing', t) @@ -2016,9 +2272,9 @@ class AllPlatformTests(BasePlatformTests): langs = ['c'] env = get_fake_env() - for l in ['cpp', 'cs', 'd', 'java', 'cuda', 'fortran', 'objc', 'objcpp', 'rust']: + for l in ['cpp', 'cs', 'd', 'java', 'cuda', 'fortran', 'objc', 'objcpp', 'rust', 'vala']: try: - comp = detect_compiler_for(env, l, MachineChoice.HOST) + comp = detect_compiler_for(env, l, MachineChoice.HOST, True) with tempfile.TemporaryDirectory() as d: comp.sanity_check(d, env) langs.append(l) @@ -2032,31 +2288,32 @@ class AllPlatformTests(BasePlatformTests): for lang in langs: for target_type in ('executable', 'library'): - if is_windows() and lang == 'fortran' and target_type == 'library': - # non-Gfortran Windows Fortran compilers do not do shared libraries in a Fortran standard way - # see "test cases/fortran/6 dynamic" - fc = detect_compiler_for(env, 'fortran', MachineChoice.HOST) - if fc.get_id() in {'intel-cl', 'pgi'}: - continue - # test empty directory - with tempfile.TemporaryDirectory() as tmpdir: - self._run(self.meson_command + ['init', '--language', lang, '--type', target_type], - workdir=tmpdir) - self._run(self.setup_command + ['--backend=ninja', 'builddir'], - workdir=tmpdir) - self._run(ninja, - workdir=os.path.join(tmpdir, 'builddir')) - # test directory with existing code file - if lang in {'c', 'cpp', 'd'}: - with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, 'foo.' + lang), 'w', encoding='utf-8') as f: - f.write('int main(void) {}') - self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) - elif lang in {'java'}: - with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, 'Foo.' + lang), 'w', encoding='utf-8') as f: - f.write('public class Foo { public static void main() {} }') - self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) + with self.subTest(f'Language: {lang}; type: {target_type}'): + if is_windows() and lang == 'fortran' and target_type == 'library': + # non-Gfortran Windows Fortran compilers do not do shared libraries in a Fortran standard way + # see "test cases/fortran/6 dynamic" + fc = detect_compiler_for(env, 'fortran', MachineChoice.HOST, True) + if fc.get_id() in {'intel-cl', 'pgi'}: + continue + # test empty directory + with tempfile.TemporaryDirectory() as tmpdir: + self._run(self.meson_command + ['init', '--language', lang, '--type', target_type], + workdir=tmpdir) + self._run(self.setup_command + ['--backend=ninja', 'builddir'], + workdir=tmpdir) + self._run(ninja, + workdir=os.path.join(tmpdir, 'builddir')) + # test directory with existing code file + if lang in {'c', 'cpp', 'd'}: + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, 'foo.' + lang), 'w', encoding='utf-8') as f: + f.write('int main(void) {}') + self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) + elif lang in {'java'}: + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, 'Foo.' + lang), 'w', encoding='utf-8') as f: + f.write('public class Foo { public static void main() {} }') + self._run(self.meson_command + ['init', '-b'], workdir=tmpdir) def test_compiler_run_command(self): ''' @@ -2108,11 +2365,11 @@ class AllPlatformTests(BasePlatformTests): msg = ('''DEPRECATION: target prog links against shared module mymod, which is incorrect. This will be an error in the future, so please use shared_library() for mymod instead. If shared_module() was used for mymod because it has references to undefined symbols, - use shared_libary() with `override_options: ['b_lundef=false']` instead.''') + use shared_library() with `override_options: ['b_lundef=false']` instead.''') self.assertIn(msg, out) def test_mixed_language_linker_check(self): - testdir = os.path.join(self.unit_test_dir, '96 compiler.links file arg') + testdir = os.path.join(self.unit_test_dir, '97 compiler.links file arg') self.init(testdir) cmds = self.get_meson_log_compiler_checks() self.assertEqual(len(cmds), 5) @@ -2519,10 +2776,10 @@ class AllPlatformTests(BasePlatformTests): def test_native_dep_pkgconfig(self): testdir = os.path.join(self.unit_test_dir, '45 native dep pkgconfig var') - with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile: + with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as crossfile: crossfile.write(textwrap.dedent( '''[binaries] - pkgconfig = '{}' + pkg-config = '{}' [properties] @@ -2546,10 +2803,10 @@ class AllPlatformTests(BasePlatformTests): def test_pkg_config_libdir(self): testdir = os.path.join(self.unit_test_dir, '45 native dep pkgconfig var') - with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile: + with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as crossfile: crossfile.write(textwrap.dedent( '''[binaries] - pkgconfig = 'pkg-config' + pkg-config = 'pkg-config' [properties] pkg_config_libdir = ['{}'] @@ -2791,6 +3048,37 @@ class AllPlatformTests(BasePlatformTests): self.assertIn('cttest.cpp:4:20', out) self.assertNotIn(dummydir, out) + @skipIfNoExecutable('clang-tidy') + def test_clang_tidy_fix(self): + if self.backend is not Backend.ninja: + raise SkipTest(f'Clang-tidy is for now only supported on Ninja, not {self.backend.name}') + if shutil.which('c++') is None: + raise SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.') + if is_osx(): + raise SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.') + testdir = os.path.join(self.unit_test_dir, '68 clang-tidy') + + # Ensure that test project is in git even when running meson from tarball. + srcdir = os.path.join(self.builddir, 'src') + shutil.copytree(testdir, srcdir) + git_init(srcdir) + testdir = srcdir + self.new_builddir() + + dummydir = os.path.join(testdir, 'dummydir.h') + testfile = os.path.join(testdir, 'cttest.cpp') + fixedfile = os.path.join(testdir, 'cttest_fixed.cpp') + self.init(testdir, override_envvars={'CXX': 'c++'}) + # Make sure test files are different + self.assertNotEqual(Path(testfile).read_text(encoding='utf-8'), + Path(fixedfile).read_text(encoding='utf-8')) + out = self.run_target('clang-tidy-fix') + self.assertIn('cttest.cpp:4:20', out) + self.assertNotIn(dummydir, out) + # Make sure the test file is fixed + self.assertEqual(Path(testfile).read_text(encoding='utf-8'), + Path(fixedfile).read_text(encoding='utf-8')) + def test_identity_cross(self): testdir = os.path.join(self.unit_test_dir, '69 cross') # Do a build to generate a cross file where the host is this target @@ -2863,8 +3151,11 @@ class AllPlatformTests(BasePlatformTests): ('benchmarks', list), ('buildoptions', list), ('buildsystem_files', list), + ('compilers', dict), ('dependencies', list), + ('install_plan', dict), ('installed', dict), + ('machines', dict), ('projectinfo', dict), ('targets', list), ('tests', list), @@ -2881,6 +3172,7 @@ class AllPlatformTests(BasePlatformTests): ('depends', list), ('workdir', (str, None)), ('priority', int), + ('extra_paths', list), ] buildoptions_keylist = [ @@ -2906,9 +3198,16 @@ class AllPlatformTests(BasePlatformTests): dependencies_typelist = [ ('name', str), + ('type', str), ('version', str), ('compile_args', list), ('link_args', list), + ('include_directories', list), + ('sources', list), + ('extra_files', list), + ('dependencies', list), + ('depends', list), + ('meson_variables', list), ] targets_typelist = [ @@ -2921,8 +3220,12 @@ class AllPlatformTests(BasePlatformTests): ('target_sources', list), ('extra_files', list), ('subproject', (str, None)), + ('dependencies', list), + ('depends', list), ('install_filename', (list, None)), ('installed', bool), + ('vs_module_defs', (str, None)), + ('win_subsystem', (str, None)), ] targets_sources_typelist = [ @@ -2931,6 +3234,12 @@ class AllPlatformTests(BasePlatformTests): ('parameters', list), ('sources', list), ('generated_sources', list), + ('unity_sources', (list, None)), + ] + + target_sources_linker_typelist = [ + ('linker', list), + ('parameters', list), ] # First load all files @@ -2954,7 +3263,7 @@ class AllPlatformTests(BasePlatformTests): name_to_out.update({i['name']: i['filename']}) for group in i['target_sources']: src_to_id.update({os.path.relpath(src, testdir): i['id'] - for src in group['sources']}) + for src in group.get('sources', [])}) # Check Tests and benchmarks tests_to_find = ['test case 1', 'test case 2', 'benchmark 1'] @@ -3034,8 +3343,11 @@ class AllPlatformTests(BasePlatformTests): self.assertPathEqual(i['defined_in'], os.path.join(testdir, tgt[3])) targets_to_find.pop(i['name'], None) for j in i['target_sources']: - assertKeyTypes(targets_sources_typelist, j) - self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]]) + if 'compiler' in j: + assertKeyTypes(targets_sources_typelist, j) + self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]]) + else: + assertKeyTypes(target_sources_linker_typelist, j) self.assertDictEqual(targets_to_find, {}) def test_introspect_file_dump_equals_all(self): @@ -3048,9 +3360,11 @@ class AllPlatformTests(BasePlatformTests): 'benchmarks', 'buildoptions', 'buildsystem_files', + 'compilers', 'dependencies', 'installed', 'install_plan', + 'machines', 'projectinfo', 'targets', 'tests', @@ -3123,12 +3437,13 @@ class AllPlatformTests(BasePlatformTests): res_wb = [i for i in res_wb if i['type'] != 'custom'] for i in res_wb: i['filename'] = [os.path.relpath(x, self.builddir) for x in i['filename']] - if 'install_filename' in i: - del i['install_filename'] + for k in ('install_filename', 'dependencies', 'win_subsystem'): + if k in i: + del i[k] sources = [] for j in i['target_sources']: - sources += j['sources'] + sources += j.get('sources', []) i['target_sources'] = [{ 'language': 'unknown', 'compiler': [], @@ -3297,13 +3612,13 @@ class AllPlatformTests(BasePlatformTests): def test_summary(self): testdir = os.path.join(self.unit_test_dir, '71 summary') - out = self.init(testdir, extra_args=['-Denabled_opt=enabled']) + out = self.init(testdir, extra_args=['-Denabled_opt=enabled', f'-Dpython={sys.executable}']) expected = textwrap.dedent(r''' Some Subproject 2.0 string : bar integer: 1 - boolean: True + boolean: true subsub undefined @@ -3312,12 +3627,12 @@ class AllPlatformTests(BasePlatformTests): My Project 1.0 Configuration - Some boolean : False - Another boolean: True + Some boolean : false + Another boolean: true Some string : Hello World A list : string 1 - True + true empty list : enabled_opt : enabled A number : 1 @@ -3327,7 +3642,7 @@ class AllPlatformTests(BasePlatformTests): Stuff missing prog : NO - existing prog : ''' + sys.executable + ''' + existing prog : ''' + ExternalProgram('python3', [sys.executable], silent=True).path + ''' missing dep : NO external dep : YES 1.2.3 internal dep : YES @@ -3340,13 +3655,14 @@ class AllPlatformTests(BasePlatformTests): Subprojects sub : YES sub2 : NO Problem encountered: This subproject failed - subsub : YES + subsub : YES (from sub2) User defined options - backend : ''' + self.backend.name + ''' + backend : ''' + self.backend_name + ''' libdir : lib prefix : /usr enabled_opt : enabled + python : ''' + sys.executable + ''' ''') expected_lines = expected.split('\n')[1:] out_start = out.find(expected_lines[0]) @@ -3716,17 +4032,23 @@ class AllPlatformTests(BasePlatformTests): [properties] c_args = common_flags + ['-DSOMETHING'] cpp_args = c_args + ['-DSOMETHING_ELSE'] + rel_to_src = '@GLOBAL_SOURCE_ROOT@' / 'tool' + rel_to_file = '@DIRNAME@' / 'tool' + no_escaping = '@@DIRNAME@@' / 'tool' [binaries] c = toolchain / compiler ''')) - values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2]) + values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2], self.builddir) self.assertEqual(values['binaries']['c'], '/toolchain/gcc') self.assertEqual(values['properties']['c_args'], ['--sysroot=/toolchain/sysroot', '-DSOMETHING']) self.assertEqual(values['properties']['cpp_args'], ['--sysroot=/toolchain/sysroot', '-DSOMETHING', '-DSOMETHING_ELSE']) + self.assertEqual(values['properties']['rel_to_src'], os.path.join(self.builddir, 'tool')) + self.assertEqual(values['properties']['rel_to_file'], os.path.join(os.path.dirname(crossfile2), 'tool')) + self.assertEqual(values['properties']['no_escaping'], os.path.join(f'@{os.path.dirname(crossfile2)}@', 'tool')) @skipIf(is_windows(), 'Directory cleanup fails for some reason') def test_wrap_git(self): @@ -3884,7 +4206,7 @@ class AllPlatformTests(BasePlatformTests): self.init(srcdir, extra_args=['-Dbuild.b_lto=true']) def test_install_skip_subprojects(self): - testdir = os.path.join(self.unit_test_dir, '91 install skip subprojects') + testdir = os.path.join(self.unit_test_dir, '92 install skip subprojects') self.init(testdir) self.build() @@ -3902,7 +4224,8 @@ class AllPlatformTests(BasePlatformTests): ] bar_expected = [ 'bar', - 'share/foo/bar.dat', + 'share/bar', + 'share/bar/bar.dat', 'include/bar.h', 'bin/bar' + exe_suffix, 'bar/barfile' @@ -3930,7 +4253,7 @@ class AllPlatformTests(BasePlatformTests): check_installed_files(['--skip-subprojects', 'another'], all_expected) def test_adding_subproject_to_configure_project(self) -> None: - srcdir = os.path.join(self.unit_test_dir, '92 new subproject in configured project') + srcdir = os.path.join(self.unit_test_dir, '93 new subproject in configured project') self.init(srcdir) self.build() self.setconf('-Duse-sub=true') @@ -3947,13 +4270,40 @@ class AllPlatformTests(BasePlatformTests): self._run(cmd + python_command + [script]) self.assertEqual('This is text.', self._run(cmd + [app]).strip()) + cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump'] + o = self._run(cmd) + expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) + self.assertIn(f'TEST_C="{expected}"', o) + self.assertIn('export TEST_C', o) + + cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', '--dump-format', 'sh'] + o = self._run(cmd) + expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) + self.assertIn(f'TEST_C="{expected}"', o) + self.assertNotIn('export', o) + + cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', '--dump-format', 'vscode'] + o = self._run(cmd) + expected = os.pathsep.join(['/prefix', '/suffix']) + self.assertIn(f'TEST_C="{expected}"', o) + self.assertNotIn('export', o) + + fname = os.path.join(self.builddir, 'dump.env') + cmd = self.meson_command + ['devenv', '-C', self.builddir, '--dump', fname] + o = self._run(cmd) + self.assertEqual(o, '') + o = Path(fname).read_text(encoding='utf-8') + expected = os.pathsep.join(['/prefix', '$TEST_C', '/suffix']) + self.assertIn(f'TEST_C="{expected}"', o) + self.assertIn('export TEST_C', o) + def test_clang_format_check(self): if self.backend is not Backend.ninja: raise SkipTest(f'Skipping clang-format tests with {self.backend.name} backend') if not shutil.which('clang-format'): raise SkipTest('clang-format not found') - testdir = os.path.join(self.unit_test_dir, '93 clangformat') + testdir = os.path.join(self.unit_test_dir, '94 clangformat') newdir = os.path.join(self.builddir, 'testdir') shutil.copytree(testdir, newdir) self.new_builddir() @@ -3978,7 +4328,7 @@ class AllPlatformTests(BasePlatformTests): self.build('clang-format-check') def test_custom_target_implicit_include(self): - testdir = os.path.join(self.unit_test_dir, '94 custominc') + testdir = os.path.join(self.unit_test_dir, '95 custominc') self.init(testdir) self.build() compdb = self.get_compdb() @@ -4002,23 +4352,23 @@ class AllPlatformTests(BasePlatformTests): env = get_fake_env() # Get the compiler so we know which compiler class to mock. - cc = detect_compiler_for(env, 'c', MachineChoice.HOST) + cc = detect_compiler_for(env, 'c', MachineChoice.HOST, True) cc_type = type(cc) # Test a compiler that acts as a linker with mock.patch.object(cc_type, 'INVOKES_LINKER', True): - cc = detect_compiler_for(env, 'c', MachineChoice.HOST) + cc = detect_compiler_for(env, 'c', MachineChoice.HOST, True) link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) self.assertEqual(sorted(link_args), sorted(['-DCFLAG', '-flto'])) # And one that doesn't with mock.patch.object(cc_type, 'INVOKES_LINKER', False): - cc = detect_compiler_for(env, 'c', MachineChoice.HOST) + cc = detect_compiler_for(env, 'c', MachineChoice.HOST, True) link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) self.assertEqual(sorted(link_args), sorted(['-flto'])) def test_install_tag(self) -> None: - testdir = os.path.join(self.unit_test_dir, '98 install all targets') + testdir = os.path.join(self.unit_test_dir, '99 install all targets') self.init(testdir) self.build() @@ -4132,6 +4482,8 @@ class AllPlatformTests(BasePlatformTests): Path(installpath, 'usr/share/out3-custom.txt'), Path(installpath, 'usr/share/custom_files'), Path(installpath, 'usr/share/custom_files/data.txt'), + Path(installpath, 'usr/share/excludes'), + Path(installpath, 'usr/share/excludes/installed.txt'), Path(installpath, 'usr/lib'), Path(installpath, 'usr/lib/libbothcustom.a'), Path(installpath, 'usr/' + shared_lib_name('bothcustom')), @@ -4153,6 +4505,9 @@ class AllPlatformTests(BasePlatformTests): Path(installpath, 'usr/share/foo2.h'), Path(installpath, 'usr/share/out1.txt'), Path(installpath, 'usr/share/out2.txt'), + Path(installpath, 'usr/share/subproject'), + Path(installpath, 'usr/share/subproject/aaa.txt'), + Path(installpath, 'usr/share/subproject/bbb.txt'), } def do_install(tags, expected_files, expected_scripts): @@ -4169,8 +4524,22 @@ class AllPlatformTests(BasePlatformTests): do_install('runtime,custom', expected_runtime_custom, 1) do_install(None, expected_all, 2) + + def test_install_script_dry_run(self): + testdir = os.path.join(self.common_test_dir, '53 install script') + self.init(testdir) + self.build() + + cmd = self.meson_command + ['install', '--dry-run', '--destdir', self.installdir] + outputs = self._run(cmd, workdir=self.builddir) + + installpath = Path(self.installdir) + self.assertFalse((installpath / 'usr/diiba/daaba/file.dat').exists()) + self.assertIn("DRYRUN: Writing file file.dat", outputs) + + def test_introspect_install_plan(self): - testdir = os.path.join(self.unit_test_dir, '98 install all targets') + testdir = os.path.join(self.unit_test_dir, '99 install all targets') introfile = os.path.join(self.builddir, 'meson-info', 'intro-install_plan.json') self.init(testdir) self.assertPathExists(introfile) @@ -4185,8 +4554,7 @@ class AllPlatformTests(BasePlatformTests): structured_sources=None, objects=[], environment=env, compilers=env.coredata.compilers[MachineChoice.HOST], kwargs={}) - target.process_compilers() - target.process_compilers_late([]) + target.process_compilers_late() return target.filename shared_lib_name = lambda name: output_name(name, SharedLibrary) @@ -4198,134 +4566,184 @@ class AllPlatformTests(BasePlatformTests): f'{self.builddir}/out1-notag.txt': { 'destination': '{datadir}/out1-notag.txt', 'tag': None, + 'subproject': None, }, f'{self.builddir}/out2-notag.txt': { 'destination': '{datadir}/out2-notag.txt', 'tag': None, + 'subproject': None, }, f'{self.builddir}/libstatic.a': { 'destination': '{libdir_static}/libstatic.a', 'tag': 'devel', + 'subproject': None, }, f'{self.builddir}/' + exe_name('app'): { 'destination': '{bindir}/' + exe_name('app'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/' + exe_name('app-otherdir'): { 'destination': '{prefix}/otherbin/' + exe_name('app-otherdir'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/subdir/' + exe_name('app2'): { 'destination': '{bindir}/' + exe_name('app2'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/' + shared_lib_name('shared'): { 'destination': '{libdir_shared}/' + shared_lib_name('shared'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/' + shared_lib_name('both'): { 'destination': '{libdir_shared}/' + shared_lib_name('both'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/' + static_lib_name('both'): { 'destination': '{libdir_static}/' + static_lib_name('both'), 'tag': 'devel', + 'subproject': None, }, f'{self.builddir}/' + shared_lib_name('bothcustom'): { 'destination': '{libdir_shared}/' + shared_lib_name('bothcustom'), 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/' + static_lib_name('bothcustom'): { 'destination': '{libdir_static}/' + static_lib_name('bothcustom'), 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/subdir/' + shared_lib_name('both2'): { 'destination': '{libdir_shared}/' + shared_lib_name('both2'), 'tag': 'runtime', + 'subproject': None, }, f'{self.builddir}/subdir/' + static_lib_name('both2'): { 'destination': '{libdir_static}/' + static_lib_name('both2'), 'tag': 'devel', + 'subproject': None, }, f'{self.builddir}/out1-custom.txt': { 'destination': '{datadir}/out1-custom.txt', 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/out2-custom.txt': { 'destination': '{datadir}/out2-custom.txt', 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/out3-custom.txt': { 'destination': '{datadir}/out3-custom.txt', 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/subdir/out1.txt': { 'destination': '{datadir}/out1.txt', 'tag': None, + 'subproject': None, }, f'{self.builddir}/subdir/out2.txt': { 'destination': '{datadir}/out2.txt', 'tag': None, + 'subproject': None, }, f'{self.builddir}/out-devel.h': { 'destination': '{includedir}/out-devel.h', 'tag': 'devel', + 'subproject': None, }, f'{self.builddir}/out3-notag.txt': { 'destination': '{datadir}/out3-notag.txt', 'tag': None, + 'subproject': None, }, }, 'configure': { f'{self.builddir}/foo-notag.h': { 'destination': '{datadir}/foo-notag.h', 'tag': None, + 'subproject': None, }, f'{self.builddir}/foo2-devel.h': { 'destination': '{includedir}/foo2-devel.h', 'tag': 'devel', + 'subproject': None, }, f'{self.builddir}/foo-custom.h': { 'destination': '{datadir}/foo-custom.h', 'tag': 'custom', + 'subproject': None, }, f'{self.builddir}/subdir/foo2.h': { 'destination': '{datadir}/foo2.h', 'tag': None, + 'subproject': None, }, }, 'data': { f'{testdir}/bar-notag.txt': { 'destination': '{datadir}/bar-notag.txt', 'tag': None, + 'subproject': None, }, f'{testdir}/bar-devel.h': { 'destination': '{includedir}/bar-devel.h', 'tag': 'devel', + 'subproject': None, }, f'{testdir}/bar-custom.txt': { 'destination': '{datadir}/bar-custom.txt', 'tag': 'custom', + 'subproject': None, }, f'{testdir}/subdir/bar2-devel.h': { 'destination': '{includedir}/bar2-devel.h', 'tag': 'devel', + 'subproject': None, + }, + f'{testdir}/subprojects/subproject/aaa.txt': { + 'destination': '{datadir}/subproject/aaa.txt', + 'tag': None, + 'subproject': 'subproject', + }, + f'{testdir}/subprojects/subproject/bbb.txt': { + 'destination': '{datadir}/subproject/bbb.txt', + 'tag': 'data', + 'subproject': 'subproject', }, }, 'headers': { f'{testdir}/foo1-devel.h': { 'destination': '{includedir}/foo1-devel.h', 'tag': 'devel', + 'subproject': None, }, f'{testdir}/subdir/foo3-devel.h': { 'destination': '{includedir}/foo3-devel.h', 'tag': 'devel', + 'subproject': None, }, }, 'install_subdirs': { f'{testdir}/custom_files': { 'destination': '{datadir}/custom_files', - 'tag': 'custom' + 'tag': 'custom', + 'subproject': None, + 'exclude_dirs': [], + 'exclude_files': [], + }, + f'{testdir}/excludes': { + 'destination': '{datadir}/excludes', + 'tag': 'custom', + 'subproject': None, + 'exclude_dirs': ['excluded'], + 'exclude_files': ['excluded.txt'], } } } @@ -4373,9 +4791,9 @@ class AllPlatformTests(BasePlatformTests): }} ''') - testdir = os.path.join(self.unit_test_dir, '101 rlib linkage') + testdir = os.path.join(self.unit_test_dir, '102 rlib linkage') gen_file = os.path.join(testdir, 'lib.rs') - with open(gen_file, 'w') as f: + with open(gen_file, 'w', encoding='utf-8') as f: f.write(template.format(0)) self.addCleanup(windows_proof_rm, gen_file) @@ -4383,7 +4801,7 @@ class AllPlatformTests(BasePlatformTests): self.build() self.run_tests() - with open(gen_file, 'w') as f: + with open(gen_file, 'w', encoding='utf-8') as f: f.write(template.format(39)) self.build() @@ -4392,8 +4810,36 @@ class AllPlatformTests(BasePlatformTests): self.assertEqual(cm.exception.returncode, 1) self.assertIn('exit status 39', cm.exception.stdout) + @skip_if_not_language('rust') + def test_bindgen_drops_invalid(self) -> None: + if self.backend is not Backend.ninja: + raise unittest.SkipTest('Rust is only supported with ninja currently') + testdir = os.path.join(self.rust_test_dir, '12 bindgen') + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + # bindgen understands compiler args that clang understands, but not + # flags by other compilers + if cc.get_id() == 'gcc': + bad_arg = '-fdse' + elif cc.get_id() == 'msvc': + bad_arg = '/fastfail' + else: + raise unittest.SkipTest('Test only supports GCC and MSVC') + self.init(testdir, extra_args=[f"-Dc_args=['-DCMD_ARG', '{bad_arg}']"]) + intro = self.introspect(['--targets']) + for i in intro: + if i['type'] == 'custom' and i['id'].startswith('rustmod-bindgen'): + args = i['target_sources'][0]['compiler'] + self.assertIn('-DCMD_ARG', args) + self.assertIn('-DPROJECT_ARG', args) + self.assertIn('-DGLOBAL_ARG', args) + self.assertNotIn(bad_arg, args) + self.assertNotIn('-mtls-dialect=gnu2', args) + self.assertNotIn('/fp:fast', args) + return + def test_custom_target_name(self): - testdir = os.path.join(self.unit_test_dir, '99 custom target name') + testdir = os.path.join(self.unit_test_dir, '100 custom target name') self.init(testdir) out = self.build() if self.backend is Backend.ninja: @@ -4401,52 +4847,87 @@ class AllPlatformTests(BasePlatformTests): self.assertIn('Generating subdir/file.txt with a custom command', out) def test_symlinked_subproject(self): - testdir = os.path.join(self.unit_test_dir, '106 subproject symlink') + testdir = os.path.join(self.unit_test_dir, '107 subproject symlink') subproject_dir = os.path.join(testdir, 'subprojects') subproject = os.path.join(testdir, 'symlinked_subproject') symlinked_subproject = os.path.join(testdir, 'subprojects', 'symlinked_subproject') if not os.path.exists(subproject_dir): os.mkdir(subproject_dir) - os.symlink(subproject, symlinked_subproject) + try: + os.symlink(subproject, symlinked_subproject) + except OSError: + raise SkipTest("Symlinks are not available on this machine") self.addCleanup(os.remove, symlinked_subproject) self.init(testdir) self.build() def test_configure_same_noop(self): - testdir = os.path.join(self.unit_test_dir, '108 configure same noop') - self.init(testdir, extra_args=['-Dopt=val']) + testdir = os.path.join(self.unit_test_dir, '109 configure same noop') + args = [ + '-Dstring=val', + '-Dboolean=true', + '-Dcombo=two', + '-Dinteger=7', + '-Darray=[\'three\']', + '-Dfeature=disabled', + '--buildtype=plain', + '--prefix=/abc', + ] + self.init(testdir, extra_args=args) - filename = os.path.join(self.privatedir, 'coredata.dat') + filename = Path(self.privatedir) / 'coredata.dat' + + olddata = filename.read_bytes() oldmtime = os.path.getmtime(filename) - self.setconf(["-Dopt=val"]) - newmtime = os.path.getmtime(filename) - self.assertEqual(oldmtime, newmtime) - def test_scripts_loaded_modules(self): - ''' - Simulate a wrapped command, as done for custom_target() that capture - output. The script will print all python modules loaded and we verify - that it contains only an acceptable subset. Loading too many modules - slows down the build when many custom targets get wrapped. - ''' - es = ExecutableSerialisation(python_command + ['-c', 'exit(0)'], env=EnvironmentVariables()) - p = Path(self.builddir, 'exe.dat') - with p.open('wb') as f: - pickle.dump(es, f) - cmd = self.meson_command + ['--internal', 'test_loaded_modules', '--unpickle', str(p)] - p = subprocess.run(cmd, stdout=subprocess.PIPE) - all_modules = json.loads(p.stdout.splitlines()[0]) - meson_modules = [m for m in all_modules if 'meson' in m] - expected_meson_modules = [ - 'mesonbuild', - 'mesonbuild._pathlib', - 'mesonbuild.utils', - 'mesonbuild.utils.core', - 'mesonbuild.mesonmain', - 'mesonbuild.mlog', - 'mesonbuild.scripts', - 'mesonbuild.scripts.meson_exe', - 'mesonbuild.scripts.test_loaded_modules' - ] - self.assertEqual(sorted(expected_meson_modules), sorted(meson_modules)) + for opt in ('-Dstring=val', '--buildtype=plain', '-Dfeature=disabled', '-Dprefix=/abc'): + self.setconf([opt]) + newdata = filename.read_bytes() + newmtime = os.path.getmtime(filename) + self.assertEqual(oldmtime, newmtime) + self.assertEqual(olddata, newdata) + olddata = newdata + oldmtime = newmtime + + for opt in ('-Dstring=abc', '--buildtype=release', '-Dfeature=enabled', '-Dprefix=/def'): + self.setconf([opt]) + newdata = filename.read_bytes() + newmtime = os.path.getmtime(filename) + self.assertGreater(newmtime, oldmtime) + self.assertNotEqual(olddata, newdata) + olddata = newdata + oldmtime = newmtime + + def test_c_cpp_stds(self): + testdir = os.path.join(self.unit_test_dir, '115 c cpp stds') + self.init(testdir) + # Invalid values should fail whatever compiler we have + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=invalid') + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=c89,invalid') + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=c++11') + env = get_fake_env() + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_id() == 'msvc': + # default_option should have selected those + self.assertEqual(self.getconf('c_std'), 'c89') + self.assertEqual(self.getconf('cpp_std'), 'vc++11') + # This is deprecated but works for C + self.setconf('-Dc_std=gnu99') + self.assertEqual(self.getconf('c_std'), 'c99') + # C++ however never accepted that fallback + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dcpp_std=gnu++11') + # The first supported std should be selected + self.setconf('-Dcpp_std=gnu++11,vc++11,c++11') + self.assertEqual(self.getconf('cpp_std'), 'vc++11') + elif cc.get_id() == 'gcc': + # default_option should have selected those + self.assertEqual(self.getconf('c_std'), 'gnu89') + self.assertEqual(self.getconf('cpp_std'), 'gnu++98') + # The first supported std should be selected + self.setconf('-Dcpp_std=c++11,gnu++11,vc++11') + self.assertEqual(self.getconf('cpp_std'), 'c++11') diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py index d83ef3f..514f3b7 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -33,7 +33,7 @@ import mesonbuild.environment import mesonbuild.coredata import mesonbuild.modules.gnome from mesonbuild.mesonlib import ( - is_cygwin, join_args, windows_proof_rmtree, python_command + is_cygwin, join_args, split_args, windows_proof_rmtree, python_command ) import mesonbuild.modules.pkgconfig @@ -41,10 +41,15 @@ import mesonbuild.modules.pkgconfig from run_tests import ( Backend, ensure_backend_detects_changes, get_backend_commands, get_builddir_target_args, get_meson_script, run_configure_inprocess, - run_mtest_inprocess + run_mtest_inprocess, handle_meson_skip_test, ) +# magic attribute used by unittest.result.TestResult._is_relevant_tb_level +# This causes tracebacks to hide these internal implementation details, +# e.g. for assertXXX helpers. +__unittest = True + class BasePlatformTests(TestCase): prefix = '/usr' libdir = 'lib' @@ -55,8 +60,10 @@ class BasePlatformTests(TestCase): src_root = str(PurePath(__file__).parents[1]) self.src_root = src_root # Get the backend - self.backend = getattr(Backend, os.environ['MESON_UNIT_TEST_BACKEND']) - self.meson_args = ['--backend=' + self.backend.name] + self.backend_name = os.environ['MESON_UNIT_TEST_BACKEND'] + backend_type = 'vs' if self.backend_name.startswith('vs') else self.backend_name + self.backend = getattr(Backend, backend_type) + self.meson_args = ['--backend=' + self.backend_name] self.meson_native_files = [] self.meson_cross_files = [] self.meson_command = python_command + [get_meson_script()] @@ -70,6 +77,7 @@ class BasePlatformTests(TestCase): self.uninstall_command = get_backend_commands(self.backend) # Test directories self.common_test_dir = os.path.join(src_root, 'test cases/common') + self.python_test_dir = os.path.join(src_root, 'test cases/python') self.rust_test_dir = os.path.join(src_root, 'test cases/rust') self.vala_test_dir = os.path.join(src_root, 'test cases/vala') self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks') @@ -87,6 +95,7 @@ class BasePlatformTests(TestCase): # VS doesn't have a stable output when no changes are done # XCode backend is untested with unit tests, help welcome! self.no_rebuild_stdout = [f'UNKNOWN BACKEND {self.backend.name!r}'] + os.environ['COLUMNS'] = '80' self.builddirs = [] self.new_builddir() @@ -162,22 +171,23 @@ class BasePlatformTests(TestCase): env = os.environ.copy() env.update(override_envvars) - p = subprocess.run(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT if stderr else subprocess.PIPE, - env=env, - encoding='utf-8', - text=True, cwd=workdir, timeout=60 * 5) + proc = subprocess.run(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT if stderr else subprocess.PIPE, + env=env, + encoding='utf-8', + text=True, cwd=workdir, timeout=60 * 5) print('$', join_args(command)) print('stdout:') - print(p.stdout) + print(proc.stdout) if not stderr: print('stderr:') - print(p.stderr) - if p.returncode != 0: - if 'MESON_SKIP_TEST' in p.stdout: - raise SkipTest('Project requested skipping.') - raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout) - return p.stdout + print(proc.stderr) + if proc.returncode != 0: + skipped, reason = handle_meson_skip_test(proc.stdout) + if skipped: + raise SkipTest(f'Project requested skipping: {reason}') + raise subprocess.CalledProcessError(proc.returncode, command, output=proc.stdout) + return proc.stdout def init(self, srcdir, *, extra_args=None, @@ -198,7 +208,8 @@ class BasePlatformTests(TestCase): extra_args = [] if not isinstance(extra_args, list): extra_args = [extra_args] - args = [srcdir, self.builddir] + build_and_src_dir_args = [self.builddir, srcdir] + args = [] if default_args: args += ['--prefix', self.prefix] if self.libdir: @@ -210,7 +221,7 @@ class BasePlatformTests(TestCase): self.privatedir = os.path.join(self.builddir, 'meson-private') if inprocess: try: - returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args, override_envvars) + returncode, out, err = run_configure_inprocess(['setup'] + self.meson_args + args + extra_args + build_and_src_dir_args, override_envvars) except Exception as e: if not allow_fail: self._print_meson_log() @@ -221,11 +232,12 @@ class BasePlatformTests(TestCase): finally: # Close log file to satisfy Windows file locking mesonbuild.mlog.shutdown() - mesonbuild.mlog.log_dir = None - mesonbuild.mlog.log_file = None + mesonbuild.mlog._logger.log_dir = None + mesonbuild.mlog._logger.log_file = None - if 'MESON_SKIP_TEST' in out: - raise SkipTest('Project requested skipping.') + skipped, reason = handle_meson_skip_test(out) + if skipped: + raise SkipTest(f'Project requested skipping: {reason}') if returncode != 0: self._print_meson_log() print('Stdout:\n') @@ -236,9 +248,7 @@ class BasePlatformTests(TestCase): raise RuntimeError('Configure failed') else: try: - out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir) - except SkipTest: - raise SkipTest('Project requested skipping: ' + srcdir) + out = self._run(self.setup_command + args + extra_args + build_and_src_dir_args, override_envvars=override_envvars, workdir=workdir) except Exception: if not allow_fail: self._print_meson_log() @@ -293,6 +303,13 @@ class BasePlatformTests(TestCase): ensure_backend_detects_changes(self.backend) self._run(self.mconf_command + arg + [self.builddir]) + def getconf(self, optname: str): + opts = self.introspect('--buildoptions') + for x in opts: + if x.get('name') == optname: + return x.get('value') + self.fail(f'Option {optname} not found') + def wipe(self): windows_proof_rmtree(self.builddir) @@ -335,9 +352,10 @@ class BasePlatformTests(TestCase): Fetch a list command-lines run by meson for compiler checks. Each command-line is returned as a list of arguments. ''' - prefix = 'Command line:' + prefix = 'Command line: `' + suffix = '` -> 0\n' with self._open_meson_log() as log: - cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)] + cmds = [split_args(l[len(prefix):-len(suffix)]) for l in log if l.startswith(prefix)] return cmds def get_meson_log_sanitychecks(self): @@ -481,3 +499,6 @@ class BasePlatformTests(TestCase): def assertPathDoesNotExist(self, path): m = f'Path {path!r} should not exist' self.assertFalse(os.path.exists(path), msg=m) + + def assertLength(self, val, length): + assert len(val) == length, f'{val} is not length {length}' diff --git a/unittests/cargotests.py b/unittests/cargotests.py new file mode 100644 index 0000000..f0aedd0 --- /dev/null +++ b/unittests/cargotests.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright © 2022-2023 Intel Corporation + +from __future__ import annotations +import unittest +import typing as T + +from mesonbuild.cargo import builder, cfg +from mesonbuild.cargo.cfg import TokenType +from mesonbuild.cargo.version import convert + + +class CargoVersionTest(unittest.TestCase): + + def test_cargo_to_meson(self) -> None: + cases: T.List[T.Tuple[str, T.List[str]]] = [ + # Basic requirements + ('>= 1', ['>= 1']), + ('> 1', ['> 1']), + ('= 1', ['= 1']), + ('< 1', ['< 1']), + ('<= 1', ['<= 1']), + + # tilde tests + ('~1', ['>= 1', '< 2']), + ('~1.1', ['>= 1.1', '< 1.2']), + ('~1.1.2', ['>= 1.1.2', '< 1.2.0']), + + # Wildcards + ('*', []), + ('1.*', ['>= 1', '< 2']), + ('2.3.*', ['>= 2.3', '< 2.4']), + + # Unqualified + ('2', ['>= 2', '< 3']), + ('2.4', ['>= 2.4', '< 3']), + ('2.4.5', ['>= 2.4.5', '< 3']), + ('0.0.0', ['< 1']), + ('0.0', ['< 1']), + ('0', ['< 1']), + ('0.0.5', ['>= 0.0.5', '< 0.0.6']), + ('0.5.0', ['>= 0.5.0', '< 0.6']), + ('0.5', ['>= 0.5', '< 0.6']), + ('1.0.45', ['>= 1.0.45', '< 2']), + + # Caret (Which is the same as unqualified) + ('^2', ['>= 2', '< 3']), + ('^2.4', ['>= 2.4', '< 3']), + ('^2.4.5', ['>= 2.4.5', '< 3']), + ('^0.0.0', ['< 1']), + ('^0.0', ['< 1']), + ('^0', ['< 1']), + ('^0.0.5', ['>= 0.0.5', '< 0.0.6']), + ('^0.5.0', ['>= 0.5.0', '< 0.6']), + ('^0.5', ['>= 0.5', '< 0.6']), + + # Multiple requirements + ('>= 1.2.3, < 1.4.7', ['>= 1.2.3', '< 1.4.7']), + ] + + for (data, expected) in cases: + with self.subTest(): + self.assertListEqual(convert(data), expected) + + +class CargoCfgTest(unittest.TestCase): + + def test_lex(self) -> None: + cases: T.List[T.Tuple[str, T.List[T.Tuple[TokenType, T.Optional[str]]]]] = [ + ('"unix"', [(TokenType.STRING, 'unix')]), + ('unix', [(TokenType.IDENTIFIER, 'unix')]), + ('not(unix)', [ + (TokenType.NOT, None), + (TokenType.LPAREN, None), + (TokenType.IDENTIFIER, 'unix'), + (TokenType.RPAREN, None), + ]), + ('any(unix, windows)', [ + (TokenType.ANY, None), + (TokenType.LPAREN, None), + (TokenType.IDENTIFIER, 'unix'), + (TokenType.COMMA, None), + (TokenType.IDENTIFIER, 'windows'), + (TokenType.RPAREN, None), + ]), + ('target_arch = "x86_64"', [ + (TokenType.IDENTIFIER, 'target_arch'), + (TokenType.EQUAL, None), + (TokenType.STRING, 'x86_64'), + ]), + ('all(target_arch = "x86_64", unix)', [ + (TokenType.ALL, None), + (TokenType.LPAREN, None), + (TokenType.IDENTIFIER, 'target_arch'), + (TokenType.EQUAL, None), + (TokenType.STRING, 'x86_64'), + (TokenType.COMMA, None), + (TokenType.IDENTIFIER, 'unix'), + (TokenType.RPAREN, None), + ]), + ] + for data, expected in cases: + with self.subTest(): + self.assertListEqual(list(cfg.lexer(data)), expected) + + def test_parse(self) -> None: + cases = [ + ('target_os = "windows"', cfg.Equal(cfg.Identifier("target_os"), cfg.String("windows"))), + ('target_arch = "x86"', cfg.Equal(cfg.Identifier("target_arch"), cfg.String("x86"))), + ('target_family = "unix"', cfg.Equal(cfg.Identifier("target_family"), cfg.String("unix"))), + ('any(target_arch = "x86", target_arch = "x86_64")', + cfg.Any( + [ + cfg.Equal(cfg.Identifier("target_arch"), cfg.String("x86")), + cfg.Equal(cfg.Identifier("target_arch"), cfg.String("x86_64")), + ])), + ('all(target_arch = "x86", target_os = "linux")', + cfg.All( + [ + cfg.Equal(cfg.Identifier("target_arch"), cfg.String("x86")), + cfg.Equal(cfg.Identifier("target_os"), cfg.String("linux")), + ])), + ('not(all(target_arch = "x86", target_os = "linux"))', + cfg.Not( + cfg.All( + [ + cfg.Equal(cfg.Identifier("target_arch"), cfg.String("x86")), + cfg.Equal(cfg.Identifier("target_os"), cfg.String("linux")), + ]))), + ] + for data, expected in cases: + with self.subTest(): + self.assertEqual(cfg.parse(iter(cfg.lexer(data))), expected) + + def test_ir_to_meson(self) -> None: + build = builder.Builder('') + HOST_MACHINE = build.identifier('host_machine') + + cases = [ + ('target_os = "windows"', + build.equal(build.method('system', HOST_MACHINE), + build.string('windows'))), + ('target_arch = "x86"', + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86'))), + ('target_family = "unix"', + build.equal(build.method('system', HOST_MACHINE), + build.string('unix'))), + ('not(target_arch = "x86")', + build.not_(build.equal( + build.method('cpu_family', HOST_MACHINE), + build.string('x86')))), + ('any(target_arch = "x86", target_arch = "x86_64")', + build.or_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86')), + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86_64')))), + ('any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")', + build.or_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86')), + build.or_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86_64')), + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('aarch64'))))), + ('all(target_arch = "x86", target_arch = "x86_64")', + build.and_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86')), + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86_64')))), + ('all(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")', + build.and_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86')), + build.and_( + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('x86_64')), + build.equal(build.method('cpu_family', HOST_MACHINE), + build.string('aarch64'))))), + ] + for data, expected in cases: + with self.subTest(): + value = cfg.ir_to_meson(cfg.parse(iter(cfg.lexer(data))), build) + self.assertEqual(value, expected) diff --git a/unittests/darwintests.py b/unittests/darwintests.py index 6bf15aa..1f17760 100644 --- a/unittests/darwintests.py +++ b/unittests/darwintests.py @@ -138,7 +138,7 @@ class DarwinTests(BasePlatformTests): def test_objc_versions(self): # Objective-C always uses the C standard version. - # Objecttive-C++ always uses the C++ standard version. + # Objective-C++ always uses the C++ standard version. # This is what most people seem to want and in addition # it is the only setup supported by Xcode. testdir = os.path.join(self.objc_test_dir, '1 simple') @@ -148,3 +148,8 @@ class DarwinTests(BasePlatformTests): testdir = os.path.join(self.objcpp_test_dir, '1 simple') self.init(testdir) self.assertIn('-std=c++14', self.get_compdb()[0]['command']) + + def test_darwin_get_object_archs(self): + from mesonbuild.mesonlib import darwin_get_object_archs + archs = darwin_get_object_archs('/System/Library/CoreServices/Encodings/libSymbolConverter.dylib') + self.assertEqual(archs, ['x86_64', 'aarch64']) diff --git a/unittests/datatests.py b/unittests/datatests.py index 9a46ec4..70fdcba 100644 --- a/unittests/datatests.py +++ b/unittests/datatests.py @@ -219,11 +219,14 @@ class DataTests(unittest.TestCase): name = name.replace('_', '-') self.assertIn(name, html) + @unittest.mock.patch.dict(os.environ) def test_vim_syntax_highlighting(self): ''' Ensure that vim syntax highlighting files were updated for new functions in the global namespace in build files. ''' + # Disable unit test specific syntax + del os.environ['MESON_RUNNING_IN_PROJECT_TESTS'] env = get_fake_env() interp = Interpreter(FakeBuild(env), mock=True) with open('data/syntax-highlighting/vim/syntax/meson.vim', encoding='utf-8') as f: @@ -231,11 +234,14 @@ class DataTests(unittest.TestCase): defined = set([a.strip() for a in res.group().split('\\')][1:]) self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys()))) + @unittest.mock.patch.dict(os.environ) def test_all_functions_defined_in_ast_interpreter(self): ''' Ensure that the all functions defined in the Interpreter are also defined in the AstInterpreter (and vice versa). ''' + # Disable unit test specific syntax + del os.environ['MESON_RUNNING_IN_PROJECT_TESTS'] env = get_fake_env() interp = Interpreter(FakeBuild(env), mock=True) astint = AstInterpreter('.', '', '') diff --git a/unittests/failuretests.py b/unittests/failuretests.py index 54a6c58..2b7c73e 100644 --- a/unittests/failuretests.py +++ b/unittests/failuretests.py @@ -78,7 +78,9 @@ class FailureTests(BasePlatformTests): super().setUp() self.srcdir = os.path.realpath(tempfile.mkdtemp()) self.mbuild = os.path.join(self.srcdir, 'meson.build') - self.moptions = os.path.join(self.srcdir, 'meson_options.txt') + self.moptions = os.path.join(self.srcdir, 'meson.options') + if not os.path.exists(self.moptions): + self.moptions = os.path.join(self.srcdir, 'meson_options.txt') def tearDown(self): super().tearDown() @@ -240,12 +242,12 @@ class FailureTests(BasePlatformTests): dep = declare_dependency(dependencies : zlib_dep) dep.get_pkgconfig_variable('foo') ''' - self.assertMesonRaises(code, "Method.*pkgconfig.*is invalid.*internal") + self.assertMesonRaises(code, ".*is not a pkgconfig dependency") code = '''zlib_dep = dependency('zlib', required : false) dep = declare_dependency(dependencies : zlib_dep) dep.get_configtool_variable('foo') ''' - self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal") + self.assertMesonRaises(code, ".* is not a config-tool dependency") def test_objc_cpp_detection(self): ''' diff --git a/unittests/helpers.py b/unittests/helpers.py index d3d1560..7483f51 100644 --- a/unittests/helpers.py +++ b/unittests/helpers.py @@ -204,3 +204,11 @@ def get_path_without_cmd(cmd: str, path: str) -> str: paths.discard(dirname) path = pathsep.join([str(p) for p in paths]) return path + +def xfail_if_jobname(name: str): + if os.environ.get('MESON_CI_JOBNAME') == name: + return unittest.expectedFailure + + def wrapper(func): + return func + return wrapper diff --git a/unittests/internaltests.py b/unittests/internaltests.py index 29d4bb3..1c55b29 100644 --- a/unittests/internaltests.py +++ b/unittests/internaltests.py @@ -36,7 +36,9 @@ import mesonbuild.environment import mesonbuild.modules.gnome from mesonbuild import coredata from mesonbuild.compilers.c import ClangCCompiler, GnuCCompiler +from mesonbuild.compilers.cpp import VisualStudioCPPCompiler from mesonbuild.compilers.d import DmdDCompiler +from mesonbuild.linkers import linkers from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo from mesonbuild.mesonlib import ( @@ -45,7 +47,7 @@ from mesonbuild.mesonlib import ( OptionType ) from mesonbuild.interpreter.type_checking import in_set_validator, NoneType -from mesonbuild.dependencies import PkgConfigDependency +from mesonbuild.dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface, PkgConfigCLI from mesonbuild.programs import ExternalProgram import mesonbuild.modules.pkgconfig @@ -220,9 +222,31 @@ class InternalTests(unittest.TestCase): l.append_direct('/libbaz.a') self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a']) + + def test_compiler_args_class_visualstudio(self): + linker = linkers.MSVCDynamicLinker(MachineChoice.HOST, []) + # Version just needs to be > 19.0.0 + cc = VisualStudioCPPCompiler([], [], '20.00', MachineChoice.HOST, False, mock.Mock(), 'x64', linker=linker) + + a = cc.compiler_args(cc.get_always_args()) + self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/utf-8', '/Zc:__cplusplus']) + + # Ensure /source-charset: removes /utf-8 + a.append('/source-charset:utf-8') + self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/source-charset:utf-8']) + + # Ensure /execution-charset: removes /utf-8 + a = cc.compiler_args(cc.get_always_args() + ['/execution-charset:utf-8']) + self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/execution-charset:utf-8']) + + # Ensure /validate-charset- removes /utf-8 + a = cc.compiler_args(cc.get_always_args() + ['/validate-charset-']) + self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/validate-charset-']) + + def test_compiler_args_class_gnuld(self): ## Test --start/end-group - linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) + linker = linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) gcc = GnuCCompiler([], [], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker) ## Ensure that the fake compiler is never called by overriding the relevant function gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include'] @@ -250,7 +274,7 @@ class InternalTests(unittest.TestCase): def test_compiler_args_remove_system(self): ## Test --start/end-group - linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) + linker = linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', []) gcc = GnuCCompiler([], [], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker) ## Ensure that the fake compiler is never called by overriding the relevant function gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include'] @@ -429,7 +453,7 @@ class InternalTests(unittest.TestCase): # Can not be used as context manager because we need to # open it a second time and this is not possible on # Windows. - configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False) + configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') configfilename = configfile.name config.write(configfile) configfile.flush() @@ -445,7 +469,7 @@ class InternalTests(unittest.TestCase): 'needs_exe_wrapper': 'true' if desired_value else 'false' } - configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False) + configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8') configfilename = configfile.name config.write(configfile) configfile.close() @@ -525,11 +549,14 @@ class InternalTests(unittest.TestCase): if platform != 'openbsd': return with tempfile.TemporaryDirectory() as tmpdir: - for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1']: + for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1', + 'libbar.so.7.10', 'libbar.so.7.9', 'libbar.so.7.9.3']: libpath = Path(tmpdir) / i libpath.write_text('', encoding='utf-8') - found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED) + found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED, lib_prefix_warning=True) self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0') + found = cc._find_library_real('bar', env, [tmpdir], '', LibType.PREFER_SHARED, lib_prefix_warning=True) + self.assertEqual(os.path.basename(found[0]), 'libbar.so.7.10') def test_find_library_patterns(self): ''' @@ -616,22 +643,19 @@ class InternalTests(unittest.TestCase): create_static_lib(p1 / 'libdl.a') create_static_lib(p1 / 'librt.a') - def fake_call_pkgbin(self, args, env=None): - if '--libs' not in args: - return 0, '', '' - if args[-1] == 'foo': - return 0, f'-L{p2.as_posix()} -lfoo -L{p1.as_posix()} -lbar', '' - if args[-1] == 'bar': - return 0, f'-L{p2.as_posix()} -lbar', '' - if args[-1] == 'internal': - return 0, f'-L{p1.as_posix()} -lpthread -lm -lc -lrt -ldl', '' - - old_call = PkgConfigDependency._call_pkgbin - old_check = PkgConfigDependency.check_pkgconfig - PkgConfigDependency._call_pkgbin = fake_call_pkgbin - PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin - # Test begins - try: + class FakeInstance(PkgConfigCLI): + def _call_pkgbin(self, args, env=None): + if '--libs' not in args: + return 0, '', '' + if args[-1] == 'foo': + return 0, f'-L{p2.as_posix()} -lfoo -L{p1.as_posix()} -lbar', '' + if args[-1] == 'bar': + return 0, f'-L{p2.as_posix()} -lbar', '' + if args[-1] == 'internal': + return 0, f'-L{p1.as_posix()} -lpthread -lm -lc -lrt -ldl', '' + + with mock.patch.object(PkgConfigInterface, 'instance') as instance_method: + instance_method.return_value = FakeInstance(env, MachineChoice.HOST, silent=True) kwargs = {'required': True, 'silent': True} foo_dep = PkgConfigDependency('foo', env, kwargs) self.assertEqual(foo_dep.get_link_args(), @@ -646,13 +670,6 @@ class InternalTests(unittest.TestCase): for link_arg in link_args: for lib in ('pthread', 'm', 'c', 'dl', 'rt'): self.assertNotIn(f'lib{lib}.a', link_arg, msg=link_args) - finally: - # Test ends - PkgConfigDependency._call_pkgbin = old_call - PkgConfigDependency.check_pkgconfig = old_check - # Reset dependency class to ensure that in-process configure doesn't mess up - PkgConfigDependency.pkgbin_cache = {} - PkgConfigDependency.class_pkgbin = PerMachine(None, None) def test_version_compare(self): comparefunc = mesonbuild.mesonlib.version_compare_many @@ -924,23 +941,23 @@ class InternalTests(unittest.TestCase): def test_log_once(self): f = io.StringIO() - with mock.patch('mesonbuild.mlog.log_file', f), \ - mock.patch('mesonbuild.mlog._logged_once', set()): - mesonbuild.mlog.log_once('foo') - mesonbuild.mlog.log_once('foo') + with mock.patch('mesonbuild.mlog._logger.log_file', f), \ + mock.patch('mesonbuild.mlog._logger.logged_once', set()): + mesonbuild.mlog.log('foo', once=True) + mesonbuild.mlog.log('foo', once=True) actual = f.getvalue().strip() self.assertEqual(actual, 'foo', actual) def test_log_once_ansi(self): f = io.StringIO() - with mock.patch('mesonbuild.mlog.log_file', f), \ - mock.patch('mesonbuild.mlog._logged_once', set()): - mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo')) - mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo')) + with mock.patch('mesonbuild.mlog._logger.log_file', f), \ + mock.patch('mesonbuild.mlog._logger.logged_once', set()): + mesonbuild.mlog.log(mesonbuild.mlog.bold('foo'), once=True) + mesonbuild.mlog.log(mesonbuild.mlog.bold('foo'), once=True) actual = f.getvalue().strip() self.assertEqual(actual.count('foo'), 1, actual) - mesonbuild.mlog.log_once('foo') + mesonbuild.mlog.log('foo', once=True) actual = f.getvalue().strip() self.assertEqual(actual.count('foo'), 1, actual) @@ -1003,7 +1020,7 @@ class InternalTests(unittest.TestCase): schema = json.loads(Path('data/test.schema.json').read_text(encoding='utf-8')) - errors = [] # type: T.Tuple[str, Exception] + errors: T.List[T.Tuple[Path, Exception]] = [] for p in Path('test cases').glob('**/test.json'): try: validate(json.loads(p.read_text(encoding='utf-8')), schema=schema) @@ -1078,7 +1095,7 @@ class InternalTests(unittest.TestCase): _(None, mock.Mock(), ['string', 'var', 'args', 0], None) self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"') - def test_typed_pos_args_varargs_invalid_mulitple_types(self) -> None: + def test_typed_pos_args_varargs_invalid_multiple_types(self) -> None: @typed_pos_args('foo', str, varargs=(str, list)) def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None: self.assertTrue(False) # should not be reachable @@ -1392,6 +1409,11 @@ class InternalTests(unittest.TestCase): since_values={list: '1.9'}), KwargInfo('new_dict', (ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), default={}, since_values={dict: '1.1'}), + KwargInfo('foo', (str, int, ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str), ContainerTypeInfo(list, int)), default={}, + since_values={str: '1.1', ContainerTypeInfo(list, str): '1.2', ContainerTypeInfo(dict, str): '1.3'}, + deprecated_values={int: '0.8', ContainerTypeInfo(list, int): '0.9'}), + KwargInfo('tuple', (ContainerTypeInfo(list, (str, int))), default=[], listify=True, + since_values={ContainerTypeInfo(list, str): '1.1', ContainerTypeInfo(list, int): '1.2'}), ) def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None: pass @@ -1410,7 +1432,7 @@ class InternalTests(unittest.TestCase): with self.subTest('deprecated dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out: _(None, mock.Mock(subproject=''), [], {'output': {'foo2': 'a'}}) - self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo2". dont use it.*""") + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo2" in dict keys. dont use it.*""") with self.subTest('new dict string value'), mock.patch('sys.stdout', io.StringIO()) as out: _(None, mock.Mock(subproject=''), [], {'output': {'bar': 'b'}}) @@ -1418,7 +1440,40 @@ class InternalTests(unittest.TestCase): with self.subTest('new dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out: _(None, mock.Mock(subproject=''), [], {'output': {'bar2': 'a'}}) - self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar2". use this.*""") + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar2" in dict keys. use this.*""") + + with self.subTest('new string type'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'foo': 'foo'}) + self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "foo" of type str.*""") + + with self.subTest('new array of string type'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'foo': ['foo']}) + self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.2': "testfunc" keyword argument "foo" of type array\[str\].*""") + + with self.subTest('new dict of string type'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'foo': {'plop': 'foo'}}) + self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.3': "testfunc" keyword argument "foo" of type dict\[str\].*""") + + with self.subTest('deprecated int value'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'foo': 1}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.8': "testfunc" keyword argument "foo" of type int.*""") + + with self.subTest('deprecated array int value'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'foo': [1]}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "foo" of type array\[int\].*""") + + with self.subTest('new list[str] value'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'tuple': ['foo', 42]}) + self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "tuple" of type array\[str\].*""") + self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.2': "testfunc" keyword argument "tuple" of type array\[int\].*""") + + with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'input': 'foo'}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""") + + with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out: + _(None, mock.Mock(subproject=''), [], {'input': 'bar'}) + self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""") with self.subTest('non string union'), mock.patch('sys.stdout', io.StringIO()) as out: _(None, mock.Mock(subproject=''), [], {'install_dir': False}) @@ -1518,12 +1573,12 @@ class InternalTests(unittest.TestCase): ('ppc', 'ppc'), ('macppc', 'ppc'), ('power macintosh', 'ppc'), - ('mips64el', 'mips64'), - ('mips64', 'mips64'), + ('mips64el', 'mips'), + ('mips64', 'mips'), ('mips', 'mips'), ('mipsel', 'mips'), - ('ip30', 'mips64'), - ('ip35', 'mips64'), + ('ip30', 'mips'), + ('ip35', 'mips'), ('parisc64', 'parisc'), ('sun4u', 'sparc64'), ('sun4v', 'sparc64'), @@ -1534,15 +1589,27 @@ class InternalTests(unittest.TestCase): ('aarch64_be', 'aarch64'), ] + cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, False, mock.Mock()) + with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=False)): for test, expected in cases: with self.subTest(test, has_define=False), mock_trial(test): - actual = mesonbuild.environment.detect_cpu_family({}) + actual = mesonbuild.environment.detect_cpu_family({'c': cc}) self.assertEqual(actual, expected) with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=True)): - for test, expected in [('x86_64', 'x86'), ('aarch64', 'arm'), ('ppc', 'ppc64')]: + for test, expected in [('x86_64', 'x86'), ('aarch64', 'arm'), ('ppc', 'ppc64'), ('mips64', 'mips64')]: with self.subTest(test, has_define=True), mock_trial(test): + actual = mesonbuild.environment.detect_cpu_family({'c': cc}) + self.assertEqual(actual, expected) + + # machine_info_can_run calls detect_cpu_family with no compilers at all + with mock.patch( + 'mesonbuild.environment.any_compiler_has_define', + mock.Mock(side_effect=AssertionError('Should not be called')), + ): + for test, expected in [('mips64', 'mips64')]: + with self.subTest(test, has_compiler=False), mock_trial(test): actual = mesonbuild.environment.detect_cpu_family({}) self.assertEqual(actual, expected) @@ -1563,23 +1630,34 @@ class InternalTests(unittest.TestCase): ('x64', 'x86_64'), ('i86pc', 'x86_64'), ('earm', 'arm'), - ('mips64el', 'mips64'), - ('mips64', 'mips64'), + ('mips64el', 'mips'), + ('mips64', 'mips'), ('mips', 'mips'), ('mipsel', 'mips'), ('aarch64', 'aarch64'), ('aarch64_be', 'aarch64'), ] + cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, False, mock.Mock()) + with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=False)): for test, expected in cases: with self.subTest(test, has_define=False), mock_trial(test): - actual = mesonbuild.environment.detect_cpu({}) + actual = mesonbuild.environment.detect_cpu({'c': cc}) self.assertEqual(actual, expected) with mock.patch('mesonbuild.environment.any_compiler_has_define', mock.Mock(return_value=True)): - for test, expected in [('x86_64', 'i686'), ('aarch64', 'arm'), ('ppc', 'ppc64')]: + for test, expected in [('x86_64', 'i686'), ('aarch64', 'arm'), ('ppc', 'ppc64'), ('mips64', 'mips64')]: with self.subTest(test, has_define=True), mock_trial(test): + actual = mesonbuild.environment.detect_cpu({'c': cc}) + self.assertEqual(actual, expected) + + with mock.patch( + 'mesonbuild.environment.any_compiler_has_define', + mock.Mock(side_effect=AssertionError('Should not be called')), + ): + for test, expected in [('mips64', 'mips64')]: + with self.subTest(test, has_compiler=False), mock_trial(test): actual = mesonbuild.environment.detect_cpu({}) self.assertEqual(actual, expected) diff --git a/unittests/linuxliketests.py b/unittests/linuxliketests.py index 50c6b62..4fcf52e 100644 --- a/unittests/linuxliketests.py +++ b/unittests/linuxliketests.py @@ -45,7 +45,7 @@ from mesonbuild.compilers.c import AppleClangCCompiler from mesonbuild.compilers.cpp import AppleClangCPPCompiler from mesonbuild.compilers.objc import AppleClangObjCCompiler from mesonbuild.compilers.objcpp import AppleClangObjCPPCompiler -from mesonbuild.dependencies import PkgConfigDependency +from mesonbuild.dependencies.pkgconfig import PkgConfigDependency, PkgConfigCLI, PkgConfigInterface import mesonbuild.modules.pkgconfig PKG_CONFIG = os.environ.get('PKG_CONFIG', 'pkg-config') @@ -164,18 +164,19 @@ class LinuxlikeTests(BasePlatformTests): self.assertTrue(foo_dep.found()) self.assertEqual(foo_dep.get_version(), '1.0') self.assertIn('-lfoo', foo_dep.get_link_args()) - self.assertEqual(foo_dep.get_pkgconfig_variable('foo', [], None), 'bar') - self.assertPathEqual(foo_dep.get_pkgconfig_variable('datadir', [], None), '/usr/data') + self.assertEqual(foo_dep.get_variable(pkgconfig='foo'), 'bar') + self.assertPathEqual(foo_dep.get_variable(pkgconfig='datadir'), '/usr/data') libhello_nolib = PkgConfigDependency('libhello_nolib', env, kwargs) self.assertTrue(libhello_nolib.found()) self.assertEqual(libhello_nolib.get_link_args(), []) self.assertEqual(libhello_nolib.get_compile_args(), []) - self.assertEqual(libhello_nolib.get_pkgconfig_variable('foo', [], None), 'bar') - self.assertEqual(libhello_nolib.get_pkgconfig_variable('prefix', [], None), self.prefix) - if version_compare(PkgConfigDependency.check_pkgconfig(env, libhello_nolib.pkgbin),">=0.29.1"): - self.assertEqual(libhello_nolib.get_pkgconfig_variable('escaped_var', [], None), r'hello\ world') - self.assertEqual(libhello_nolib.get_pkgconfig_variable('unescaped_var', [], None), 'hello world') + self.assertEqual(libhello_nolib.get_variable(pkgconfig='foo'), 'bar') + self.assertEqual(libhello_nolib.get_variable(pkgconfig='prefix'), self.prefix) + impl = libhello_nolib.pkgconfig + if not isinstance(impl, PkgConfigCLI) or version_compare(impl.pkgbin_version, ">=0.29.1"): + self.assertEqual(libhello_nolib.get_variable(pkgconfig='escaped_var'), r'hello\ world') + self.assertEqual(libhello_nolib.get_variable(pkgconfig='unescaped_var'), 'hello world') cc = detect_c_compiler(env, MachineChoice.HOST) if cc.get_id() in {'gcc', 'clang'}: @@ -517,6 +518,15 @@ class LinuxlikeTests(BasePlatformTests): has_cpp20 = (compiler.get_id() not in {'clang', 'gcc'} or compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=10.0.0', None) or compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=10.0.0')) + has_cpp2b = (compiler.get_id() not in {'clang', 'gcc'} or + compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=12.0.0', None) or + compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=11.0.0')) + has_cpp23 = (compiler.get_id() not in {'clang', 'gcc'} or + compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=17.0.0', None) or + compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=11.0.0')) + has_cpp26 = (compiler.get_id() not in {'clang', 'gcc'} or + compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=17.0.0', None) or + compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=14.0.0')) has_c18 = (compiler.get_id() not in {'clang', 'gcc'} or compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=8.0.0', '>=11.0') or compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0')) @@ -533,6 +543,12 @@ class LinuxlikeTests(BasePlatformTests): continue elif '++20' in v and not has_cpp20: continue + elif '++2b' in v and not has_cpp2b: + continue + elif '++23' in v and not has_cpp23: + continue + elif ('++26' in v or '++2c' in v) and not has_cpp26: + continue # now C elif '17' in v and not has_cpp2a_c17: continue @@ -1024,7 +1040,7 @@ class LinuxlikeTests(BasePlatformTests): def test_cross_find_program(self): testdir = os.path.join(self.unit_test_dir, '11 cross prog') - crossfile = tempfile.NamedTemporaryFile(mode='w') + crossfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') print(os.path.join(testdir, 'some_cross_tool.py')) tool_path = os.path.join(testdir, 'some_cross_tool.py') @@ -1153,7 +1169,7 @@ class LinuxlikeTests(BasePlatformTests): # Regression test: This used to modify the value of `pkg_config_path` # option, adding the meson-uninstalled directory to it. - PkgConfigDependency.setup_env({}, env, MachineChoice.HOST, uninstalled=True) + PkgConfigInterface.setup_env({}, env, MachineChoice.HOST, uninstalled=True) pkg_config_path = env.coredata.options[OptionKey('pkg_config_path')].value self.assertEqual(pkg_config_path, [pkg_dir]) @@ -1350,7 +1366,7 @@ class LinuxlikeTests(BasePlatformTests): see: https://github.com/mesonbuild/meson/issues/9000 https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a ''' - testdir = os.path.join(self.unit_test_dir, '97 link full name','libtestprovider') + testdir = os.path.join(self.unit_test_dir, '98 link full name','libtestprovider') oldprefix = self.prefix # install into installdir without using DESTDIR installdir = self.installdir @@ -1363,7 +1379,7 @@ class LinuxlikeTests(BasePlatformTests): self.new_builddir() env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir), 'PKG_CONFIG_PATH': _prepend_pkg_config_path(os.path.join(installdir, self.libdir, 'pkgconfig'))} - testdir = os.path.join(self.unit_test_dir, '97 link full name','proguser') + testdir = os.path.join(self.unit_test_dir, '98 link full name','proguser') self.init(testdir,override_envvars=env) # test for link with full path @@ -1517,14 +1533,14 @@ class LinuxlikeTests(BasePlatformTests): def test_identity_cross(self): testdir = os.path.join(self.unit_test_dir, '60 identity cross') - constantsfile = tempfile.NamedTemporaryFile(mode='w') + constantsfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') constantsfile.write(textwrap.dedent('''\ [constants] py_ext = '.py' ''')) constantsfile.flush() - nativefile = tempfile.NamedTemporaryFile(mode='w') + nativefile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') nativefile.write(textwrap.dedent('''\ [binaries] c = ['{}' + py_ext] @@ -1532,7 +1548,7 @@ class LinuxlikeTests(BasePlatformTests): nativefile.flush() self.meson_native_files = [constantsfile.name, nativefile.name] - crossfile = tempfile.NamedTemporaryFile(mode='w') + crossfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') crossfile.write(textwrap.dedent('''\ [binaries] c = ['{}' + py_ext] @@ -1549,7 +1565,7 @@ class LinuxlikeTests(BasePlatformTests): 'CC_FOR_BUILD': '"' + os.path.join(testdir, 'build_wrapper.py') + '"', 'CC': '"' + os.path.join(testdir, 'host_wrapper.py') + '"', } - crossfile = tempfile.NamedTemporaryFile(mode='w') + crossfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8') crossfile.write('') crossfile.flush() self.meson_cross_files = [crossfile.name] @@ -1775,7 +1791,7 @@ class LinuxlikeTests(BasePlatformTests): @skipUnless(is_linux() or is_osx(), 'Test only applicable to Linux and macOS') def test_install_strip(self): - testdir = os.path.join(self.unit_test_dir, '103 strip') + testdir = os.path.join(self.unit_test_dir, '104 strip') self.init(testdir) self.build() @@ -1822,9 +1838,31 @@ class LinuxlikeTests(BasePlatformTests): self.assertFalse(cpp.compiler_args([f'-isystem{symlink}' for symlink in default_symlinks]).to_native()) def test_freezing(self): - testdir = os.path.join(self.unit_test_dir, '109 freeze') + testdir = os.path.join(self.unit_test_dir, '110 freeze') self.init(testdir) self.build() with self.assertRaises(subprocess.CalledProcessError) as e: self.run_tests() self.assertNotIn('Traceback', e.exception.output) + + @skipUnless(is_linux(), "Ninja file differs on different platforms") + def test_complex_link_cases(self): + testdir = os.path.join(self.unit_test_dir, '114 complex link cases') + self.init(testdir) + self.build() + with open(os.path.join(self.builddir, 'build.ninja'), encoding='utf-8') as f: + content = f.read() + # Verify link dependencies, see comments in meson.build. + self.assertIn('build libt1-s3.a: STATIC_LINKER libt1-s2.a.p/s2.c.o libt1-s3.a.p/s3.c.o\n', content) + self.assertIn('build t1-e1: c_LINKER t1-e1.p/main.c.o | libt1-s1.a libt1-s3.a\n', content) + self.assertIn('build libt2-s3.a: STATIC_LINKER libt2-s2.a.p/s2.c.o libt2-s1.a.p/s1.c.o libt2-s3.a.p/s3.c.o\n', content) + self.assertIn('build t2-e1: c_LINKER t2-e1.p/main.c.o | libt2-s3.a\n', content) + self.assertIn('build t3-e1: c_LINKER t3-e1.p/main.c.o | libt3-s3.so.p/libt3-s3.so.symbols\n', content) + self.assertIn('build t4-e1: c_LINKER t4-e1.p/main.c.o | libt4-s2.so.p/libt4-s2.so.symbols libt4-s3.a\n', content) + self.assertIn('build t5-e1: c_LINKER t5-e1.p/main.c.o | libt5-s1.so.p/libt5-s1.so.symbols libt5-s3.a\n', content) + self.assertIn('build t6-e1: c_LINKER t6-e1.p/main.c.o | libt6-s2.a libt6-s3.a\n', content) + self.assertIn('build t7-e1: c_LINKER t7-e1.p/main.c.o | libt7-s3.a\n', content) + self.assertIn('build t8-e1: c_LINKER t8-e1.p/main.c.o | libt8-s1.a libt8-s2.a libt8-s3.a\n', content) + self.assertIn('build t9-e1: c_LINKER t9-e1.p/main.c.o | libt9-s1.a libt9-s2.a libt9-s3.a\n', content) + self.assertIn('build t12-e1: c_LINKER t12-e1.p/main.c.o | libt12-s1.a libt12-s2.a libt12-s3.a\n', content) + self.assertIn('build t13-e1: c_LINKER t13-e1.p/main.c.o | libt12-s1.a libt13-s3.a\n', content) diff --git a/unittests/machinefiletests.py b/unittests/machinefiletests.py index bf109b2..d2309f8 100644 --- a/unittests/machinefiletests.py +++ b/unittests/machinefiletests.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import subprocess import tempfile import textwrap @@ -67,7 +69,7 @@ class NativeFileTests(BasePlatformTests): self.current_config = 0 self.current_wrapper = 0 - def helper_create_native_file(self, values): + def helper_create_native_file(self, values: T.Dict[str, T.Dict[str, T.Union[str, int, float, bool, T.Sequence[T.Union[str, int, float, bool]]]]]) -> str: """Create a config file as a temporary file. values should be a nested dictionary structure of {section: {key: @@ -81,10 +83,10 @@ class NativeFileTests(BasePlatformTests): for k, v in entries.items(): if isinstance(v, (bool, int, float)): f.write(f"{k}={v}\n") - elif isinstance(v, list): - f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v]))) - else: + elif isinstance(v, str): f.write(f"{k}='{v}'\n") + else: + f.write("{}=[{}]\n".format(k, ', '.join([f"'{w}'" for w in v]))) return filename def helper_create_binary_wrapper(self, binary, dir_=None, extra_args=None, **kwargs): @@ -140,7 +142,7 @@ class NativeFileTests(BasePlatformTests): return batfile def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST): - """Helper for generating tests for overriding compilers for langaugages + """Helper for generating tests for overriding compilers for languages with more than one implementation, such as C, C++, ObjC, ObjC++, and D. """ env = get_fake_env() @@ -373,7 +375,7 @@ class NativeFileTests(BasePlatformTests): def test_java_classpath(self): if self.backend is not Backend.ninja: raise SkipTest('Jar is only supported with Ninja') - testdir = os.path.join(self.unit_test_dir, '110 classpath') + testdir = os.path.join(self.unit_test_dir, '112 classpath') self.init(testdir) self.build() one_build_path = get_classpath(os.path.join(self.builddir, 'one.jar')) @@ -622,6 +624,29 @@ class NativeFileTests(BasePlatformTests): else: self.fail('Did not find bindir in build options?') + @skip_if_not_language('rust') + def test_bindgen_clang_arguments(self) -> None: + if self.backend is not Backend.ninja: + raise SkipTest('Rust is only supported with Ninja') + + testcase = os.path.join(self.rust_test_dir, '12 bindgen') + config = self.helper_create_native_file({ + 'properties': {'bindgen_clang_arguments': 'sentinal'} + }) + + self.init(testcase, extra_args=['--native-file', config]) + targets: T.List[T.Dict[str, T.Any]] = self.introspect('--targets') + for t in targets: + if t['id'].startswith('rustmod-bindgen'): + args: T.List[str] = t['target_sources'][0]['compiler'] + self.assertIn('sentinal', args, msg="Did not find machine file value") + cargs_start = args.index('--') + sent_arg = args.index('sentinal') + self.assertLess(cargs_start, sent_arg, msg='sentinal argument does not come after "--"') + break + else: + self.fail('Did not find a bindgen target') + class CrossFileTests(BasePlatformTests): @@ -719,7 +744,7 @@ class CrossFileTests(BasePlatformTests): # The test uses mocking and thus requires that the current process is the # one to run the Meson steps. If we are using an external test executable # (most commonly in Debian autopkgtests) then the mocking won't work. - @skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.') + @skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, cannot use mocking.') def test_cross_file_system_paths(self): if is_windows(): raise SkipTest('system crossfile paths not defined for Windows (yet)') @@ -729,7 +754,7 @@ class CrossFileTests(BasePlatformTests): with tempfile.TemporaryDirectory() as d: dir_ = os.path.join(d, 'meson', 'cross') os.makedirs(dir_) - with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f: + with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False, encoding='utf-8') as f: f.write(cross_content) name = os.path.basename(f.name) @@ -745,7 +770,7 @@ class CrossFileTests(BasePlatformTests): with tempfile.TemporaryDirectory() as d: dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross') os.makedirs(dir_) - with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f: + with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False, encoding='utf-8') as f: f.write(cross_content) name = os.path.basename(f.name) diff --git a/unittests/platformagnostictests.py b/unittests/platformagnostictests.py index 39965c6..31dea0f 100644 --- a/unittests/platformagnostictests.py +++ b/unittests/platformagnostictests.py @@ -12,17 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import os +import pickle import tempfile import subprocess import textwrap -from unittest import skipIf +import shutil +from unittest import skipIf, SkipTest from pathlib import Path from .baseplatformtests import BasePlatformTests from .helpers import is_ci -from mesonbuild.mesonlib import is_linux +from mesonbuild.mesonlib import EnvironmentVariables, ExecutableSerialisation, is_linux, python_command from mesonbuild.optinterpreter import OptionInterpreter, OptionException +from run_tests import Backend @skipIf(is_ci() and not is_linux(), "Run only on fast platforms") class PlatformAgnosticTests(BasePlatformTests): @@ -35,7 +39,7 @@ class PlatformAgnosticTests(BasePlatformTests): Tests that find_program() with a relative path does not find the program in current workdir. ''' - testdir = os.path.join(self.unit_test_dir, '100 relative find program') + testdir = os.path.join(self.unit_test_dir, '101 relative find program') self.init(testdir, workdir=testdir) def test_invalid_option_names(self): @@ -71,11 +75,11 @@ class PlatformAgnosticTests(BasePlatformTests): interp.process(fname) def test_python_dependency_without_pkgconfig(self): - testdir = os.path.join(self.unit_test_dir, '102 python without pkgconfig') + testdir = os.path.join(self.unit_test_dir, '103 python without pkgconfig') self.init(testdir, override_envvars={'PKG_CONFIG': 'notfound'}) def test_debug_function_outputs_to_meson_log(self): - testdir = os.path.join(self.unit_test_dir, '104 debug function') + testdir = os.path.join(self.unit_test_dir, '105 debug function') log_msg = 'This is an example debug output, should only end up in debug log' output = self.init(testdir) @@ -87,7 +91,7 @@ class PlatformAgnosticTests(BasePlatformTests): self.assertIn(log_msg, mesonlog) def test_new_subproject_reconfigure(self): - testdir = os.path.join(self.unit_test_dir, '107 new subproject on reconfigure') + testdir = os.path.join(self.unit_test_dir, '108 new subproject on reconfigure') self.init(testdir) self.build() @@ -121,3 +125,164 @@ class PlatformAgnosticTests(BasePlatformTests): ''')) subprocess.check_call(self.wrap_command + ['update-db'], cwd=testdir) self.init(testdir, workdir=testdir) + + def test_none_backend(self): + testdir = os.path.join(self.python_test_dir, '7 install path') + + self.init(testdir, extra_args=['--backend=none'], override_envvars={'NINJA': 'absolutely false command'}) + self.assertPathDoesNotExist(os.path.join(self.builddir, 'build.ninja')) + + self.run_tests(inprocess=True, override_envvars={}) + + out = self._run(self.meson_command + ['install', f'--destdir={self.installdir}'], workdir=self.builddir) + self.assertNotIn('Only ninja backend is supported to rebuild the project before installation.', out) + + with open(os.path.join(testdir, 'test.json'), 'rb') as f: + dat = json.load(f) + for i in dat['installed']: + self.assertPathExists(os.path.join(self.installdir, i['file'])) + + def test_change_backend(self): + if self.backend != Backend.ninja: + raise SkipTest('Only useful to test if backend is ninja.') + + testdir = os.path.join(self.python_test_dir, '7 install path') + self.init(testdir) + + # no-op change works + self.setconf(f'--backend=ninja') + self.init(testdir, extra_args=['--reconfigure', '--backend=ninja']) + + # Change backend option is not allowed + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.setconf('-Dbackend=none') + self.assertIn("ERROR: Tried modify read only option 'backend'", cm.exception.stdout) + + # Reconfigure with a different backend is not allowed + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.init(testdir, extra_args=['--reconfigure', '--backend=none']) + self.assertIn("ERROR: Tried modify read only option 'backend'", cm.exception.stdout) + + # Wipe with a different backend is allowed + self.init(testdir, extra_args=['--wipe', '--backend=none']) + + def test_validate_dirs(self): + testdir = os.path.join(self.common_test_dir, '1 trivial') + + # Using parent as builddir should fail + self.builddir = os.path.dirname(self.builddir) + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.init(testdir) + self.assertIn('cannot be a parent of source directory', cm.exception.stdout) + + # Reconfigure of empty builddir should work + self.new_builddir() + self.init(testdir, extra_args=['--reconfigure']) + + # Reconfigure of not empty builddir should work + self.new_builddir() + Path(self.builddir, 'dummy').touch() + self.init(testdir, extra_args=['--reconfigure']) + + # Setup a valid builddir should update options but not reconfigure + self.assertEqual(self.getconf('buildtype'), 'debug') + o = self.init(testdir, extra_args=['-Dbuildtype=release']) + self.assertIn('Directory already configured', o) + self.assertNotIn('The Meson build system', o) + self.assertEqual(self.getconf('buildtype'), 'release') + + # Wipe of empty builddir should work + self.new_builddir() + self.init(testdir, extra_args=['--wipe']) + + # Wipe of partial builddir should work + self.new_builddir() + Path(self.builddir, 'meson-private').mkdir() + Path(self.builddir, 'dummy').touch() + self.init(testdir, extra_args=['--wipe']) + + # Wipe of not empty builddir should fail + self.new_builddir() + Path(self.builddir, 'dummy').touch() + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.init(testdir, extra_args=['--wipe']) + self.assertIn('Directory is not empty', cm.exception.stdout) + + def test_scripts_loaded_modules(self): + ''' + Simulate a wrapped command, as done for custom_target() that capture + output. The script will print all python modules loaded and we verify + that it contains only an acceptable subset. Loading too many modules + slows down the build when many custom targets get wrapped. + + This list must not be edited without a clear rationale for why it is + acceptable to do so! + ''' + es = ExecutableSerialisation(python_command + ['-c', 'exit(0)'], env=EnvironmentVariables()) + p = Path(self.builddir, 'exe.dat') + with p.open('wb') as f: + pickle.dump(es, f) + cmd = self.meson_command + ['--internal', 'test_loaded_modules', '--unpickle', str(p)] + p = subprocess.run(cmd, stdout=subprocess.PIPE) + all_modules = json.loads(p.stdout.splitlines()[0]) + meson_modules = [m for m in all_modules if m.startswith('mesonbuild')] + expected_meson_modules = [ + 'mesonbuild', + 'mesonbuild._pathlib', + 'mesonbuild.utils', + 'mesonbuild.utils.core', + 'mesonbuild.mesonmain', + 'mesonbuild.mlog', + 'mesonbuild.scripts', + 'mesonbuild.scripts.meson_exe', + 'mesonbuild.scripts.test_loaded_modules' + ] + self.assertEqual(sorted(expected_meson_modules), sorted(meson_modules)) + + def test_setup_loaded_modules(self): + ''' + Execute a very basic meson.build and capture a list of all python + modules loaded. We verify that it contains only an acceptable subset. + Loading too many modules slows down `meson setup` startup time and + gives a perception that meson is slow. + + Adding more modules to the default startup flow is not an unreasonable + thing to do as new features are added, but keeping track of them is + good. + ''' + testdir = os.path.join(self.unit_test_dir, '116 empty project') + + self.init(testdir) + self._run(self.meson_command + ['--internal', 'regenerate', '--profile-self', testdir, self.builddir]) + with open(os.path.join(self.builddir, 'meson-logs', 'profile-startup-modules.json'), encoding='utf-8') as f: + data = json.load(f)['meson'] + + with open(os.path.join(testdir, 'expected_mods.json'), encoding='utf-8') as f: + expected = json.load(f)['meson']['modules'] + + self.assertEqual(data['modules'], expected) + self.assertEqual(data['count'], 68) + + def test_meson_package_cache_dir(self): + # Copy testdir into temporary directory to not pollute meson source tree. + testdir = os.path.join(self.unit_test_dir, '118 meson package cache dir') + srcdir = os.path.join(self.builddir, 'srctree') + shutil.copytree(testdir, srcdir) + builddir = os.path.join(srcdir, '_build') + self.change_builddir(builddir) + self.init(srcdir, override_envvars={'MESON_PACKAGE_CACHE_DIR': os.path.join(srcdir, 'cache_dir')}) + + def test_cmake_openssl_not_found_bug(self): + """Issue #12098""" + testdir = os.path.join(self.unit_test_dir, '119 openssl cmake bug') + self.meson_native_files.append(os.path.join(testdir, 'nativefile.ini')) + out = self.init(testdir, allow_fail=True) + self.assertNotIn('Unhandled python exception', out) + + def test_error_configuring_subdir(self): + testdir = os.path.join(self.common_test_dir, '152 index customtarget') + out = self.init(os.path.join(testdir, 'subdir'), allow_fail=True) + + self.assertIn('first statement must be a call to project()', out) + # provide guidance diagnostics by finding a file whose first AST statement is project() + self.assertIn(f'Did you mean to run meson from the directory: "{testdir}"?', out) diff --git a/unittests/pythontests.py b/unittests/pythontests.py index d49107f..afcfec3 100644 --- a/unittests/pythontests.py +++ b/unittests/pythontests.py @@ -12,19 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import unittest -import pathlib -import subprocess +import glob, os, pathlib, shutil, subprocess, unittest from run_tests import ( Backend ) from .allplatformstests import git_init - from .baseplatformtests import BasePlatformTests -from mesonbuild.mesonlib import TemporaryDirectoryWinProof +from .helpers import * + +from mesonbuild.mesonlib import MachineChoice, TemporaryDirectoryWinProof +from mesonbuild.modules.python import PythonModule class PythonTests(BasePlatformTests): ''' @@ -60,3 +59,41 @@ python = pymod.find_installation('python3', required: true) git_init(dirstr) self.init(dirstr) subprocess.check_call(self.meson_command + ['dist', '-C', self.builddir], stdout=subprocess.DEVNULL) + + def _test_bytecompile(self, py2=False): + testdir = os.path.join(self.src_root, 'test cases', 'python', '2 extmodule') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + + self.init(testdir, extra_args=['-Dpython2=auto', '-Dpython.bytecompile=1']) + self.build() + self.install() + + count = 0 + for root, dirs, files in os.walk(self.installdir): + for file in files: + realfile = os.path.join(root, file) + if file.endswith('.py'): + cached = glob.glob(realfile+'?') + glob.glob(os.path.join(root, '__pycache__', os.path.splitext(file)[0] + '*.pyc')) + if py2 and cc.get_id() == 'msvc': + # MSVC python installs python2/python3 into the same directory + self.assertLength(cached, 4) + else: + self.assertLength(cached, 2) + count += 1 + # there are 5 files x 2 installations + if py2 and not cc.get_id() == 'msvc': + self.assertEqual(count, 10) + else: + self.assertEqual(count, 5) + + def test_bytecompile_multi(self): + if not shutil.which('python2') and not PythonModule._get_win_pythonpath('python2'): + raise self.skipTest('python2 not installed') + self._test_bytecompile(True) + + def test_bytecompile_single(self): + if shutil.which('python2') or PythonModule._get_win_pythonpath('python2'): + raise self.skipTest('python2 installed, already tested') + self._test_bytecompile() diff --git a/unittests/rewritetests.py b/unittests/rewritetests.py index 4979c51..7932fec 100644 --- a/unittests/rewritetests.py +++ b/unittests/rewritetests.py @@ -13,11 +13,15 @@ # limitations under the License. import subprocess +from itertools import zip_longest import json import os +from pathlib import Path import shutil import unittest +from mesonbuild.ast import IntrospectionInterpreter, AstIDGenerator +from mesonbuild.ast.printer import RawPrinter from mesonbuild.mesonlib import windows_proof_rmtree from .baseplatformtests import BasePlatformTests @@ -156,7 +160,7 @@ class RewriterTests(BasePlatformTests): } self.assertDictEqual(out, expected) - def test_tatrget_add(self): + def test_target_add(self): self.prime('1 basic') self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json')) out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) @@ -396,3 +400,21 @@ class RewriterTests(BasePlatformTests): # Check the written file out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json')) self.assertDictEqual(out, expected) + + def test_raw_printer_is_idempotent(self): + test_path = Path(self.unit_test_dir, '120 rewrite') + meson_build_file = test_path / 'meson.build' + # original_contents = meson_build_file.read_bytes() + original_contents = meson_build_file.read_text(encoding='utf-8') + + interpreter = IntrospectionInterpreter(test_path, '', 'ninja', visitors = [AstIDGenerator()]) + interpreter.analyze() + + printer = RawPrinter() + interpreter.ast.accept(printer) + # new_contents = printer.result.encode('utf-8') + new_contents = printer.result + + # Do it line per line because it is easier to debug like that + for orig_line, new_line in zip_longest(original_contents.splitlines(), new_contents.splitlines()): + self.assertEqual(orig_line, new_line) diff --git a/unittests/subprojectscommandtests.py b/unittests/subprojectscommandtests.py index bca124d..d50828b 100644 --- a/unittests/subprojectscommandtests.py +++ b/unittests/subprojectscommandtests.py @@ -177,6 +177,17 @@ class SubprojectsCommandTests(BasePlatformTests): self.assertEqual(self._git_local_commit(subp_name), self._git_remote_commit(subp_name, 'newbranch')) self.assertTrue(self._git_local(['stash', 'list'], subp_name)) + # Untracked files need to be stashed too, or (re-)applying a patch + # creating one of those untracked files will fail. + untracked = self.subprojects_dir / subp_name / 'untracked.c' + untracked.write_bytes(b'int main(void) { return 0; }') + self._subprojects_cmd(['update', '--reset']) + self.assertTrue(self._git_local(['stash', 'list'], subp_name)) + assert not untracked.exists() + # Ensure it was indeed stashed, and we can get it back. + self.assertTrue(self._git_local(['stash', 'pop'], subp_name)) + assert untracked.exists() + # Create a new remote tag and update the wrap file. Checks that # "meson subprojects update --reset" checkout the new tag in detached mode. self._git_create_remote_tag(subp_name, 'newtag') diff --git a/unittests/windowstests.py b/unittests/windowstests.py index c81d924..be53d65 100644 --- a/unittests/windowstests.py +++ b/unittests/windowstests.py @@ -184,6 +184,93 @@ class WindowsTests(BasePlatformTests): # to the right reason). return self.build() + + @skipIf(is_cygwin(), 'Test only applicable to Windows') + def test_genvslite(self): + # The test framework itself might be forcing a specific, non-ninja backend across a set of tests, which + # includes this test. E.g. - + # > python.exe run_unittests.py --backend=vs WindowsTests + # Since that explicitly specifies a backend that's incompatible with (and essentially meaningless in + # conjunction with) 'genvslite', we should skip further genvslite testing. + if self.backend is not Backend.ninja: + raise SkipTest('Test only applies when using the Ninja backend') + + testdir = os.path.join(self.unit_test_dir, '117 genvslite') + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_argument_syntax() != 'msvc': + raise SkipTest('Test only applies when MSVC tools are available.') + + # We want to run the genvslite setup. I.e. - + # meson setup --genvslite vs2022 ... + # which we should expect to generate the set of _debug/_debugoptimized/_release suffixed + # build directories. Then we want to check that the solution/project build hooks (like clean, + # build, and rebuild) end up ultimately invoking the 'meson compile ...' of the appropriately + # suffixed build dir, for which we need to use 'msbuild.exe' + + # Find 'msbuild.exe' + msbuildprog = ExternalProgram('msbuild.exe') + self.assertTrue(msbuildprog.found(), msg='msbuild.exe not found') + + # Setup with '--genvslite ...' + self.new_builddir() + + # Firstly, we'd like to check that meson errors if the user explicitly specifies a non-ninja backend + # during setup. + with self.assertRaises(subprocess.CalledProcessError) as cm: + self.init(testdir, extra_args=['--genvslite', 'vs2022', '--backend', 'vs']) + self.assertIn("specifying a non-ninja backend conflicts with a 'genvslite' setup", cm.exception.stdout) + + # Wrap the following bulk of setup and msbuild invocation testing in a try-finally because any exception, + # failure, or success must always clean up any of the suffixed build dir folders that may have been generated. + try: + # Since this + self.init(testdir, extra_args=['--genvslite', 'vs2022']) + # We need to bear in mind that the BasePlatformTests framework creates and cleans up its own temporary + # build directory. However, 'genvslite' creates a set of suffixed build directories which we'll have + # to clean up ourselves. See 'finally' block below. + + # We intentionally skip the - + # self.build() + # step because we're wanting to test compilation/building through the solution/project's interface. + + # Execute the debug and release builds through the projects 'Build' hooks + genvslite_vcxproj_path = str(os.path.join(self.builddir+'_vs', 'genvslite@exe.vcxproj')) + # This use-case of invoking the .sln/.vcxproj build hooks, not through Visual Studio itself, but through + # 'msbuild.exe', in a VS tools command prompt environment (e.g. "x64 Native Tools Command Prompt for VS 2022"), is a + # problem: Such an environment sets the 'VSINSTALLDIR' variable which, mysteriously, has the side-effect of causing + # the spawned 'meson compile' command to fail to find 'ninja' (and even when ninja can be found elsewhere, all the + # compiler binaries that ninja wants to run also fail to be found). The PATH environment variable in the child python + # (and ninja) processes are fundamentally stripped down of all the critical search paths required to run the ninja + # compile work ... ONLY when 'VSINSTALLDIR' is set; without 'VSINSTALLDIR' set, the meson compile command does search + # for and find ninja (ironically, it finds it under the path where VSINSTALLDIR pointed!). + # For the above reason, this testing works around this bizarre behaviour by temporarily removing any 'VSINSTALLDIR' + # variable, prior to invoking the builds - + current_env = os.environ.copy() + current_env.pop('VSINSTALLDIR', None) + subprocess.check_call( + ['msbuild', '-target:Build', '-property:Configuration=debug', genvslite_vcxproj_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=current_env) + subprocess.check_call( + ['msbuild', '-target:Build', '-property:Configuration=release', genvslite_vcxproj_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=current_env) + + # Check this has actually built the appropriate exes + output_debug = subprocess.check_output(str(os.path.join(self.builddir+'_debug', 'genvslite.exe'))) + self.assertEqual( output_debug, b'Debug\r\n' ) + output_release = subprocess.check_output(str(os.path.join(self.builddir+'_release', 'genvslite.exe'))) + self.assertEqual( output_release, b'Non-debug\r\n' ) + + finally: + # Clean up our special suffixed temporary build dirs + suffixed_build_dirs = glob(self.builddir+'_*', recursive=False) + for build_dir in suffixed_build_dirs: + shutil.rmtree(build_dir) def test_install_pdb_introspection(self): testdir = os.path.join(self.platform_test_dir, '1 basic')