diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c5ab14e..a2356054 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.24) project(pyexiv2bind) find_package(Python3 COMPONENTS Interpreter Development) +find_package(Catch2) if (WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_DEBUG_POSTFIX "d") @@ -17,18 +18,12 @@ option(pyexiv2bind_build_tests "build test suite" ON) include_directories(${CMAKE_BINARY_DIR}) include(FetchContent) FetchContent_Declare(libpybind11 - URL https://github.com/pybind/pybind11/archive/refs/tags/v2.10.4.tar.gz - URL_HASH SHA1=5c366a92fc4b3937bcc3389405edbe362b1f3cbd + URL https://github.com/pybind/pybind11/archive/refs/tags/v2.13.6.tar.gz + URL_HASH SHA1=8c7e3e8fec829ced31a495dec281153511f33c63 DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL ) -FetchContent_Declare(libcatch2 - URL https://github.com/catchorg/Catch2/archive/v3.7.1.tar.gz - URL_HASH SHA1=0c67df1524fd3ce88f656a3865ba4d4e4886168f - DOWNLOAD_EXTRACT_TIMESTAMP TRUE - ) - if(pyexiv2bind_generate_python_bindings) FetchContent_MakeAvailable(libpybind11) endif(pyexiv2bind_generate_python_bindings) diff --git a/Jenkinsfile b/Jenkinsfile index c939dae1..fbf7b24f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -518,23 +518,50 @@ pipeline { stages{ stage('Setup Testing Environment'){ environment{ - CFLAGS='--coverage -fprofile-arcs -ftest-coverage' - LFLAGS="-lgcov --coverage" + CXXFLAGS='--coverage -fprofile-arcs -ftest-coverage' } steps{ + sh( + label: 'Create virtual environment', + script: '''mkdir -p build/python + uv sync --group ci --no-install-project + mkdir -p build/temp + mkdir -p build/lib + mkdir -p build/docs + mkdir -p build/python + mkdir -p build/coverage + mkdir -p coverage_data/python_extension + mkdir -p coverage_data/cpp + mkdir -p logs + mkdir -p reports + mkdir -p reports/coverage + ''' + ) sh( label: 'Install project as editable module with ci dependencies', - script: '''mkdir -p build/build_wrapper_output_directory - build-wrapper-linux --out-dir build/build_wrapper_output_directory uv sync --frozen --group ci --refresh-package py3exiv2bind --verbose + script: '''uv pip install "pybind11>=2.13" "uiucprescon.build @ https://github.com/UIUCLibrary/uiucprescon_build/releases/download/v0.4.2/uiucprescon_build-0.4.2-py3-none-any.whl" + mkdir -p build/build_wrapper_output_directory + build-wrapper-linux --out-dir build/build_wrapper_output_directory uv run setup.py build_clib build_ext --inplace --build-temp build/temp --build-lib build/lib --debug -v ''' ) + cleanWs( + deleteDirs: true, + patterns: [ + [pattern: 'build/**/cmake_builds/**/*.gcno', type: 'INCLUDE'], + ] + ) + sh 'find build -name "*.gcno"' } } stage('Building Documentation'){ + environment{ + GCOV_PREFIX='build/temp' + GCOV_PREFIX_STRIP=5 + } steps { catchError(buildResult: 'UNSTABLE', message: 'Building Sphinx documentation has issues', stageResult: 'UNSTABLE') { sh(label: 'Running Sphinx', - script: 'uv run -m sphinx -b html docs/source build/docs/html -d build/docs/doctrees -v -w logs/build_sphinx.log -W --keep-going' + script: 'uv run -m sphinx -b html docs/source build/docs/html -d build/docs/doctrees -v -w logs/build_sphinx.log -W --keep-going && find build/temp -name "*.gcda"' ) } } @@ -556,219 +583,245 @@ pipeline { when{ equals expected: true, actual: params.RUN_CHECKS } - stages{ - stage('Building C++ Tests with coverage data'){ + parallel{ + stage('Python tests'){ + environment{ + GCOV_PREFIX='build/temp' + GCOV_PREFIX_STRIP=5 + } steps{ - tee('logs/cmake-build.log'){ - sh(label: 'Building C++ Code', - script: '''uvx conan install conanfile.py -of build/cpp --build=missing -pr:b=default - uv run cmake --preset conan-release -B build/cpp/ -Wdev -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=true -DBUILD_TESTING:BOOL=true -Dpyexiv2bind_generate_python_bindings:BOOL=true -DCMAKE_CXX_FLAGS="-fprofile-arcs -ftest-coverage -Wall -Wextra" - ''' - ) + script{ + parallel([ + failFast: false, + 'Run Doctest Tests': { + try{ + sh 'uv run coverage run --parallel-mode --source=src/py3exiv2bind -m sphinx docs/source reports/doctest -b doctest -d build/docs/.doctrees --no-color -w logs/doctest_warnings.log' + } finally { + recordIssues(tools: [sphinxBuild(name: 'Doctest', pattern: 'logs/doctest_warnings.log', id: 'doctest')]) + } + }, + 'MyPy Static Analysis': { + try{ + tee('logs/mypy.log'){ + sh(returnStatus: true, + script: 'uv run mypy -p py3exiv2bind --html-report reports/mypy/html' + ) + } + } finally { + recordIssues(tools: [myPy(name: 'MyPy', pattern: 'logs/mypy.log')]) + publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'reports/mypy/html/', reportFiles: 'index.html', reportName: 'MyPy HTML Report', reportTitles: '']) + } + }, + 'Run Pylint Static Analysis': { + try{ + catchError(buildResult: 'SUCCESS', message: 'Pylint found issues', stageResult: 'UNSTABLE') { + sh( + script: '''mkdir -p logs + mkdir -p reports + PYLINTHOME=. uv run pylint src/py3exiv2bind -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > reports/pylint.txt + ''', + label: 'Running pylint' + ) + } + sh( + label: 'Running pylint for sonarqube', + script: 'PYLINTHOME=. uv run pylint -r n --msg-template="{path}:{module}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > reports/pylint_issues.txt', + returnStatus: true + ) + } finally { + stash includes: 'reports/pylint_issues.txt,reports/pylint.txt', name: 'PYLINT_REPORT' + recordIssues(tools: [pyLint(pattern: 'reports/pylint.txt')]) + } + }, + 'Flake8': { + try{ + sh( + returnStatus: true, + script: 'uv run flake8 src/py3exiv2bind --tee --output-file ./logs/flake8.log' + ) + } finally { + stash includes: 'logs/flake8.log', name: 'FLAKE8_REPORT' + recordIssues(tools: [flake8(name: 'Flake8', pattern: 'logs/flake8.log')]) + } + }, + 'Running Unit Tests': { + try{ + sh 'uv run coverage run --parallel-mode --source=src/py3exiv2bind -m pytest --junitxml=./reports/pytest/junit-pytest.xml' + } finally { + stash includes: 'reports/pytest/junit-pytest.xml', name: 'PYTEST_REPORT' + junit 'reports/pytest/junit-pytest.xml' + } + } + ]) } - sh '''mkdir -p build/build_wrapper_output_directory - build-wrapper-linux --out-dir build/build_wrapper_output_directory uv run cmake --build build/cpp -j $(grep -c ^processor /proc/cpuinfo) --target all - ''' } - post{ + post { always{ - recordIssues( - filters: [excludeFile('build/cpp/_deps/*')], - tools: [gcc(pattern: 'logs/cmake-build.log'), [$class: 'Cmake', pattern: 'logs/cmake-build.log']] - ) - } - } - } - stage('Running Tests'){ - parallel { - stage('Clang Tidy Analysis') { - steps{ - tee('logs/clang-tidy.log') { - catchError(buildResult: 'SUCCESS', message: 'Clang-Tidy found issues', stageResult: 'UNSTABLE') { - sh(label: 'Run Clang Tidy', script: 'run-clang-tidy -clang-tidy-binary clang-tidy -p ./build/cpp/ src/py3exiv2bind/') - } - } - } - post{ - always { - recordIssues( - tools: [clangTidy(pattern: 'logs/clang-tidy.log')] + script{ + try{ + sh(label: 'Creating gcovr coverage report', + script: 'uv run gcovr --root $WORKSPACE --exclude \'\\.venv\' --exclude \'/.*/build/\' --exclude-directories=$WORKSPACE/.venv --exclude-directories=$WORKSPACE/build/cpp --exclude-directories=$WORKSPACE/build/temp/cmake_builds/exiv2/_deps --print-summary --json=$WORKSPACE/reports/coverage/coverage-c-extension_tests.json --txt=$WORKSPACE/reports/coverage/coverage-c-extension_tests.txt --exclude-throw-branches --fail-under-line=1 --gcov-object-directory=$WORKSPACE/build/temp/src build/temp/src' ) + } catch (e){ + sh(label: 'locating gcno and gcda files', script: 'find . \\( -name "*.gcno" -o -name "*.gcda" \\)') + throw e + } finally { + if(fileExists('reports/coverage/coverage-c-extension_tests.txt')){ + sh 'cat reports/coverage/coverage-c-extension_tests.txt' + } } } } - stage('Task Scanner'){ - steps{ - recordIssues(tools: [taskScanner(highTags: 'FIXME', includePattern: 'src/py3exiv2bind/**/*.py, src/py3exiv2bind/**/*.cpp, src/py3exiv2bind/**/*.h', normalTags: 'TODO')]) - } - } - stage('Memcheck'){ - when{ - equals expected: true, actual: params.RUN_MEMCHECK - } + } + } + stage('C++ tests'){ + stages{ + stage('Building C++ Tests with coverage data'){ steps{ - generate_ctest_memtest_script('memcheck.cmake') - timeout(30){ - sh( label: 'Running memcheck', - script: 'uv run ctest -S memcheck.cmake --verbose -j $(grep -c ^processor /proc/cpuinfo)' - ) - } - } - post{ - always{ - recordIssues( - filters: [ - excludeFile('build/cpp/_deps/*'), - ], - tools: [ - drMemory(pattern: 'build/cpp/Testing/Temporary/DrMemory/**/results.txt') - ] + tee('logs/cmake-build.log'){ + sh(label: 'Building C++ Code', + script: '''uvx conan install conanfile.py -of build/cpp --build=missing -pr:b=default + uv run cmake --preset conan-release -B build/cpp/ -Wdev -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=true -DBUILD_TESTING:BOOL=true -Dpyexiv2bind_generate_python_bindings:BOOL=false -DCMAKE_CXX_FLAGS="-fprofile-arcs -ftest-coverage -Wall -Wextra" + mkdir -p build/build_wrapper_output_directory + build-wrapper-linux --out-dir build/build_wrapper_output_directory uv run cmake --build build/cpp --target all + ''' ) } } - } - stage('CPP Check'){ - steps{ - catchError(buildResult: 'SUCCESS', message: 'cppcheck found issues', stageResult: 'UNSTABLE') { - sh(label: 'Running cppcheck', - script: 'cppcheck --error-exitcode=1 --project=build/cpp/compile_commands.json -i_deps --enable=all --suppressions-list=cppcheck_suppression_file.txt -rp=$PWD/build/cpp --xml --output-file=logs/cppcheck_debug.xml' - ) - } - } post{ - always { + always{ recordIssues( - filters: [ - excludeType('unmatchedSuppression'), - excludeType('missingIncludeSystem'), - excludeFile('catch.hpp'), - excludeFile('value.hpp'), - ], - tools: [ - cppCheck(pattern: 'logs/cppcheck_debug.xml') - ] + filters: [excludeFile('build/cpp/_deps/*')], + tools: [gcc(pattern: 'logs/cmake-build.log'), [$class: 'Cmake', pattern: 'logs/cmake-build.log']] ) } } } - stage('CTest'){ + stage('Running Tests'){ steps{ - sh(label: 'Running CTest', - script: '''cd build/cpp - uv run ctest --output-on-failure --no-compress-output -T Test - ''' - ) - } - post{ - always{ - xunit( - testTimeMargin: '3000', - thresholdMode: 1, - thresholds: [ - failed(), - skipped() - ], - tools: [ - CTest( - deleteOutputFiles: true, - failIfNotNew: true, - pattern: 'build/Testing/**/*.xml', - skipNoTestFiles: true, - stopProcessingIfError: true - ) - ] - ) - } - } - } - stage('Run Doctest Tests'){ - steps { - sh 'uv run coverage run --parallel-mode --source=src/py3exiv2bind -m sphinx docs/source reports/doctest -b doctest -d build/docs/.doctrees --no-color -w logs/doctest_warnings.log' - } - post{ - always { - recordIssues(tools: [sphinxBuild(name: 'Doctest', pattern: 'logs/doctest_warnings.log', id: 'doctest')]) - } - } - } - stage('MyPy Static Analysis') { - steps{ - tee('logs/mypy.log'){ - sh(returnStatus: true, - script: 'uv run mypy -p py3exiv2bind --html-report reports/mypy/html' - ) - } - } - post { - always { - recordIssues(tools: [myPy(name: 'MyPy', pattern: 'logs/mypy.log')]) - publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'reports/mypy/html/', reportFiles: 'index.html', reportName: 'MyPy HTML Report', reportTitles: '']) - } - } - } - stage('Run Pylint Static Analysis') { - steps{ - catchError(buildResult: 'SUCCESS', message: 'Pylint found issues', stageResult: 'UNSTABLE') { - sh( - script: '''mkdir -p logs - mkdir -p reports - PYLINTHOME=. uv run pylint src/py3exiv2bind -r n --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > reports/pylint.txt - ''', - label: 'Running pylint' - ) - } - sh( - label: 'Running pylint for sonarqube', - script: 'PYLINTHOME=. uv run pylint -r n --msg-template="{path}:{module}:{line}: [{msg_id}({symbol}), {obj}] {msg}" > reports/pylint_issues.txt', - returnStatus: true - ) - } - post{ - always{ - stash includes: 'reports/pylint_issues.txt,reports/pylint.txt', name: 'PYLINT_REPORT' - recordIssues(tools: [pyLint(pattern: 'reports/pylint.txt')]) + script{ + parallel([ + failFast: false, + 'Clang Tidy Analysis': { + try{ + tee('logs/clang-tidy.log') { + catchError(buildResult: 'SUCCESS', message: 'Clang-Tidy found issues', stageResult: 'UNSTABLE') { + sh(label: 'Run Clang Tidy', script: 'run-clang-tidy -clang-tidy-binary clang-tidy -p ./build/cpp/ src/py3exiv2bind/') + } + } + } finally { + recordIssues(tools: [clangTidy(pattern: 'logs/clang-tidy.log')]) + } + }, + 'Memcheck': { + if (params.RUN_MEMCHECK){ + generate_ctest_memtest_script('memcheck.cmake') + try{ + timeout(30){ + sh(label: 'Running memcheck', script: 'uv run ctest -S memcheck.cmake --verbose -j $(grep -c ^processor /proc/cpuinfo)') + } + } finally { + recordIssues( + filters: [excludeFile('build/cpp/_deps/*'),], + tools: [ + drMemory(pattern: 'build/cpp/Testing/Temporary/DrMemory/**/results.txt') + ] + ) + } + } else { + Utils.markStageSkippedForConditional('Memcheck') + } + }, + 'CPP Check': { + try{ + catchError(buildResult: 'SUCCESS', message: 'cppcheck found issues', stageResult: 'UNSTABLE') { + sh(label: 'Running cppcheck', + script: 'cppcheck --error-exitcode=1 --project=build/cpp/compile_commands.json -i_deps --enable=all --suppressions-list=cppcheck_suppression_file.txt -rp=$PWD/build/cpp --xml --output-file=logs/cppcheck_debug.xml' + ) + } + } finally { + recordIssues( + filters: [ + excludeType('unmatchedSuppression'), + excludeType('missingIncludeSystem'), + excludeFile('catch.hpp'), + excludeFile('value.hpp'), + ], + tools: [ + cppCheck(pattern: 'logs/cppcheck_debug.xml') + ] + ) + } + }, + 'CTest': { + try{ + sh(label: 'Running CTest', + script: '''cd build/cpp + uv run ctest --output-on-failure --no-compress-output -T Test + ''' + ) + } finally { + xunit( + testTimeMargin: '3000', + thresholdMode: 1, + thresholds: [ + failed(), + skipped() + ], + tools: [ + CTest( + deleteOutputFiles: true, + failIfNotNew: true, + pattern: 'build/Testing/**/*.xml', + skipNoTestFiles: true, + stopProcessingIfError: true + ) + ] + ) + } + }, + ]) } } } - stage('Flake8') { - steps{ - sh( - returnStatus: true, - script: 'uv run flake8 src/py3exiv2bind --tee --output-file ./logs/flake8.log' - ) - } - post { - always { - stash includes: 'logs/flake8.log', name: 'FLAKE8_REPORT' - recordIssues(tools: [flake8(name: 'Flake8', pattern: 'logs/flake8.log')]) - } - } + } + post{ + always{ + sh(label: 'Creating gcovr coverage report', + script: '''uv run gcovr --root $WORKSPACE --print-summary --exclude \'/.*/build/\' --json=$WORKSPACE/reports/coverage/coverage_cpp_tests.json --txt=$WORKSPACE/reports/coverage/text_cpp_tests_summary.txt --exclude-throw-branches --gcov-object-directory=$WORKSPACE/build/cpp build/cpp + cat reports/coverage/text_cpp_tests_summary.txt + ''' + ) } - stage('Running Unit Tests'){ - steps{ - sh 'uv run coverage run --parallel-mode --source=src/py3exiv2bind -m pytest --junitxml=./reports/pytest/junit-pytest.xml' - } - post{ - always{ - stash includes: 'reports/pytest/junit-pytest.xml', name: 'PYTEST_REPORT' - junit 'reports/pytest/junit-pytest.xml' - } - } + } + } + stage('Misc tests'){ + steps{ + script{ + parallel([ + 'Task Scanner': { + recordIssues(tools: [taskScanner(highTags: 'FIXME', includePattern: 'src/py3exiv2bind/**/*.py, src/py3exiv2bind/**/*.cpp, src/py3exiv2bind/**/*.h', normalTags: 'TODO')]) + }, + ]) } } } } post{ always{ - sh(label: 'combining coverage data', - script: '''mkdir -p reports/coverage - uv run coverage combine - uv run coverage xml -o ./reports/coverage/coverage-python.xml - uv run gcovr --root . --filter src/py3exiv2bind --exclude-directories build/cpp/_deps/libcatch2-build --exclude-directories build/python/temp/conan_cache --exclude-throw-branches --exclude-unreachable-branches --print-summary --keep --json -o reports/coverage/coverage-c-extension.json - uv run gcovr --root . --filter src/py3exiv2bind --exclude-directories build/cpp/_deps/libcatch2-build --exclude-throw-branches --exclude-unreachable-branches --print-summary --keep --json -o reports/coverage/coverage_cpp.json - uv run gcovr --add-tracefile reports/coverage/coverage-c-extension.json --add-tracefile reports/coverage/coverage_cpp.json --keep --print-summary --xml -o reports/coverage/coverage_cpp.xml --sonarqube -o reports/coverage/coverage_cpp_sonar.xml - ''' - ) + script{ + if(fileExists('reports/coverage/coverage_cpp_tests.json') && fileExists('reports/coverage/coverage-c-extension_tests.json')){ + sh(label: 'combining coverage data', + script: '''mkdir -p reports/coverage + uv run coverage combine + uv run coverage xml -o ./reports/coverage/coverage-python.xml + uv run gcovr --root $WORKSPACE --filter=src/py3exiv2bind --add-tracefile reports/coverage/coverage_cpp_tests.json --add-tracefile reports/coverage/coverage-c-extension_tests.json --keep --print-summary --cobertura reports/coverage/coverage_cpp.xml --txt reports/coverage/text_merged_summary.txt + cat reports/coverage/text_merged_summary.txt + ''' + ) + } + } + archiveArtifacts artifacts: 'reports/coverage/*.xml' recordCoverage(tools: [[parser: 'COBERTURA', pattern: 'reports/coverage/*.xml']]) } } @@ -783,6 +836,7 @@ pipeline { when{ allOf{ equals expected: true, actual: params.USE_SONARQUBE + equals expected: true, actual: params.RUN_CHECKS expression{ try{ withCredentials([string(credentialsId: params.SONARCLOUD_TOKEN, variable: 'dddd')]) { @@ -799,17 +853,10 @@ pipeline { script{ withSonarQubeEnv(installationName:'sonarcloud', credentialsId: params.SONARCLOUD_TOKEN) { withCredentials([string(credentialsId: params.SONARCLOUD_TOKEN, variable: 'token')]) { - if (env.CHANGE_ID){ - sh( - label: 'Running Sonar Scanner', - script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.pullrequest.key=${env.CHANGE_ID} -Dsonar.pullrequest.base=${env.CHANGE_TARGET} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory" - ) - } else { - sh( - label: 'Running Sonar Scanner', - script: "uv run pysonar -t \$token -Dsonar.projectVersion=\$VERSION -Dsonar.buildString=\"${env.BUILD_TAG}\" -Dsonar.branch.name=${env.BRANCH_NAME} -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=\$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory" - ) - } + sh( + label: 'Running Sonar Scanner', + script: 'uv run pysonar -t $token -Dsonar.projectVersion=$VERSION -Dsonar.buildString="$BUILD_TAG" -Dsonar.cfamily.cache.enabled=false -Dsonar.cfamily.threads=$(grep -c ^processor /proc/cpuinfo) -Dsonar.cfamily.build-wrapper-output=build/build_wrapper_output_directory -Dsonar.python.coverage.reportPaths=./reports/coverage/coverage-python.xml -Dsonar.cfamily.cobertura.reportPaths=reports/coverage/coverage_cpp.xml ' + (env.CHANGE_ID ? '-Dsonar.pullrequest.key=$CHANGE_ID -Dsonar.pullrequest.base=$CHANGE_TARGET' : '-Dsonar.branch.name=$BRANCH_NAME') + ) } } timeout(time: 1, unit: 'HOURS') { diff --git a/MANIFEST.in b/MANIFEST.in index d70a7203..8d1bb72b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ # Include the license file include tox.ini CHANGELOG.rst README.rst CMakeLists.txt tests/CMakeLists.txt -include conanfile.py +include conanfile.py conan.lock include src/py3exiv2bind/core.pyi include patches/*.patch include patches/*.cmake diff --git a/ci/docker/linux/jenkins/Dockerfile b/ci/docker/linux/jenkins/Dockerfile index d0ec47c5..f52cfa80 100644 --- a/ci/docker/linux/jenkins/Dockerfile +++ b/ci/docker/linux/jenkins/Dockerfile @@ -61,7 +61,6 @@ ARG PIP_INDEX_URL ARG CONAN_USER_HOME ARG CONAN_DOWNLOAD_CACHE=/tmp/conan_download_cache ARG CONAN_CENTER_PROXY_V2_URL -COPY conanfile.py /tmp/conanfile.py ARG UV_CACHE_DIR ARG PIP_DOWNLOAD_CACHE COPY --from=uv_builder /uv /uvx /bin/ @@ -73,6 +72,8 @@ RUN --mount=type=cache,target=${PIP_DOWNLOAD_CACHE} \ --mount=type=cache,target=${CONAN_DOWNLOAD_CACHE} \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=conanfile.py,target=/tmp/conanfile.py \ + --mount=type=bind,source=conan.lock,target=/tmp/conan.lock \ uv run --only-group conan conan profile detect --exist-ok && \ cp ${CONAN_HOME}/global.conf /tmp/global.conf.original && \ echo "core.sources:download_cache = $CONAN_DOWNLOAD_CACHE" >> ${CONAN_HOME}/global.conf && \ diff --git a/ci/docker/linux/tox/Dockerfile b/ci/docker/linux/tox/Dockerfile index 8fb708d2..581098a9 100644 --- a/ci/docker/linux/tox/Dockerfile +++ b/ci/docker/linux/tox/Dockerfile @@ -42,7 +42,6 @@ ARG CONAN_HOME COPY ci/docker/linux/tox/conan/profile.ini ${CONAN_HOME}/profiles/default COPY ci/docker/shared/conan/remotes.json ${CONAN_HOME}/remotes.json -COPY conanfile.py /tmp/conanfile.py ARG UV_CACHE_DIR ARG PIP_DOWNLOAD_CACHE ARG CONAN_CENTER_PROXY_V2_URL @@ -52,6 +51,8 @@ RUN --mount=type=cache,target=${PIP_DOWNLOAD_CACHE} \ --mount=type=cache,target=${CONAN_DOWNLOAD_CACHE} \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=conanfile.py,target=/tmp/conanfile.py \ + --mount=type=bind,source=conan.lock,target=/tmp/conan.lock \ uv run --only-group conan conan profile detect --exist-ok && \ cp ${CONAN_HOME}/global.conf /tmp/global.conf.original && \ echo "core.sources:download_cache = $CONAN_DOWNLOAD_CACHE" >> ${CONAN_HOME}/global.conf && \ diff --git a/cmake_scripts/external_exiv22.cmake b/cmake_scripts/external_exiv22.cmake index 191fbbea..504928e4 100644 --- a/cmake_scripts/external_exiv22.cmake +++ b/cmake_scripts/external_exiv22.cmake @@ -56,7 +56,7 @@ FetchContent_Declare( # URL https://github.com/Exiv2/exiv2/archive/refs/tags/v0.28.1.tar.gz # URL_HASH SHA256=3078651f995cb6313b1041f07f4dd1bf0e9e4d394d6e2adc6e92ad0b621291fa GIT_REPOSITORY https://github.com/Exiv2/exiv2.git - GIT_TAG v0.28.1 + GIT_TAG v0.28.7 # GIT_REPOSITORY https://github.com/Exiv2/exiv2.git # GIT_TAG ${EXIV2_VERSION_TAG} SYSTEM diff --git a/conan.lock b/conan.lock new file mode 100644 index 00000000..32a6a205 --- /dev/null +++ b/conan.lock @@ -0,0 +1,15 @@ +{ + "version": "0.5", + "requires": [ + "zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1755184481.162043", + "libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1755178573.409999", + "expat/2.5.0#91e43e4544923e4c934bfad1fa4306f9%1755186526.919869", + "catch2/3.11.0#a560b55882ff2deb1f4bafad832a1b98%1763673919.270116", + "brotli/1.0.9#8419c3e664bc762bc479561c5c7d2274%1755186526.576017" + ], + "build_requires": [ + "cmake/3.31.9#f9b9bb4bdfa37937619a33e9218703a6%1761647880.115" + ], + "python_requires": [], + "config_requires": [] +} diff --git a/conanfile.py b/conanfile.py index 1da18baf..bb7867a5 100644 --- a/conanfile.py +++ b/conanfile.py @@ -19,6 +19,9 @@ def requirements(self): self.requires("zlib/1.3.1") if self.settings.os in ["Macos", "Linux"]: self.requires("libiconv/1.17") + + def build_requirements(self): + self.test_requires('catch2/3.11.0') def imports(self): self.copy("*.dll", dst="bin", src="bin") # From bin to bin self.copy("*.dylib*", dst=".", src="lib") # From lib to bin diff --git a/pyproject.toml b/pyproject.toml index b2410a59..b22c3ca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,10 @@ [build-system] requires = [ "setuptools>=77.0.0", - "wheel", "ninja", - "uiucprescon.build @ https://github.com/UIUCLibrary/uiucprescon_build/releases/download/v0.4.0/uiucprescon_build-0.4.0-py3-none-any.whl", + "pybind11>=2.13", + "uiucprescon.build @ https://github.com/UIUCLibrary/uiucprescon_build/releases/download/v0.4.2/uiucprescon_build-0.4.2-py3-none-any.whl", "cmake<4.0", -# "conan>=1.50.0,<2.0" "conan>=2.0" ] build-backend = "uiucprescon.build" diff --git a/scripts/resources/package_for_linux/Dockerfile b/scripts/resources/package_for_linux/Dockerfile index 458d5eae..261b487a 100644 --- a/scripts/resources/package_for_linux/Dockerfile +++ b/scripts/resources/package_for_linux/Dockerfile @@ -44,7 +44,6 @@ ARG UV_INDEX_URL COPY scripts/resources/package_for_linux/conan/profile.ini ${CONAN_HOME}/profiles/default COPY ci/docker/shared/conan/remotes.json ${CONAN_HOME}/remotes.json -COPY conanfile.py /tmp/conanfile.py COPY ci/docker/linux/tox/update_conan_compiler.py /tmp/update_conan_compiler.py ARG UV_CACHE_DIR ARG PIP_DOWNLOAD_CACHE @@ -54,6 +53,8 @@ RUN --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=cache,target=${PIP_DOWNLOAD_CACHE} \ --mount=type=cache,target=${UV_CACHE_DIR} \ + --mount=type=bind,source=conanfile.py,target=/tmp/conanfile.py \ + --mount=type=bind,source=conan.lock,target=/tmp/conan.lock \ uv run --only-group conan conan profile detect --exist-ok && \ cp ${CONAN_HOME}/global.conf /tmp/global.conf.original && \ echo "core.sources:download_cache = $CONAN_DOWNLOAD_CACHE" >> ${CONAN_HOME}/global.conf && \ diff --git a/scripts/resources/windows/tox/Dockerfile b/scripts/resources/windows/tox/Dockerfile index 6c4a14b6..6a32316f 100644 --- a/scripts/resources/windows/tox/Dockerfile +++ b/scripts/resources/windows/tox/Dockerfile @@ -18,7 +18,7 @@ ARG chocolateyVersion FROM ${FROM_IMAGE} AS certsgen RUN certutil -generateSSTFromWU roots.sst -FROM ${FROM_IMAGE} AS BASE_BUILDER +FROM ${FROM_IMAGE} AS base_builder SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"] @@ -62,13 +62,13 @@ RUN Set-ExecutionPolicy Bypass -Scope Process -Force; ` # ============================================================================== -FROM BASE_BUILDER AS CONAN_BUILDER +FROM base_builder AS conan_builder ARG CONAN_HOME ARG PIP_EXTRA_INDEX_URL ARG PIP_INDEX_URL ENV UV_FROZEN=1 -COPY conanfile.py c:/temp/conanfile.py +COPY conanfile.py conan.lock c:/temp/ COPY ci/docker/shared/conan/remotes.json ${CONAN_HOME}/remotes.json COPY scripts/resources/windows/tox/conan/profile.ini ${CONAN_HOME}/profiles/default COPY scripts/resources/windows/tox/conan/python.ini ${CONAN_HOME}/profiles/python @@ -82,17 +82,19 @@ RUN uv run --only-group conan conan profile detect --exist-ok ; ` Copy-Item -Path "${Env:CONAN_HOME}\remotes.json" -Destination "c:\remotes.json" ; ` uv run --only-group conan conan remote update conan-center --url ${env:CONAN_CENTER_PROXY_V2_URL}; ` }; ` - uv run --only-group conan conan install c:/temp/ -pr:h=default -pr:b=python; ` + uv run --only-group conan conan install c:/temp/ -pr:h=default -pr:b=python -of c:/temp/delme; ` if ($LASTEXITCODE -ne 0) { ` throw \"Command 'conan install' failed with exit code: $LASTEXITCODE\"` } ;` + Remove-Item -Path "c:\temp\delme" -Recurse -Force ; ` uv run --only-group conan conan cache clean "*" -b --source --build --temp ; ` + Remove-Item -Path "${env:UV_PROJECT}\.venv" -Recurse -Force ; ` uv cache clean --no-progress ; ` if (${env:CONAN_CENTER_PROXY_V2_URL} -ne $(Get-Content -Raw -Path ${Env:CONAN_HOME}\remotes.json)) { ` Move-Item -Path "c:\remotes.json" -Destination "${Env:CONAN_HOME}\remotes.json" -Force ;` } -FROM BASE_BUILDER +FROM base_builder COPY --from=certsgen c:/roots.sst c:/roots.sst RUN certutil -addstore -f root c:/roots.sst ; ` del c:/roots.sst @@ -102,7 +104,7 @@ ARG UV_CACHE_DIR RUN New-Item -type directory -path ${Env:PIP_DOWNLOAD_CACHE} -Force | Out-Null ; ` New-Item -type directory -path ${Env:UV_CACHE_DIR} -Force | Out-Null ARG CONAN_HOME -COPY --from=CONAN_BUILDER ${CONAN_HOME}/ ${CONAN_HOME}/ +COPY --from=conan_builder ${CONAN_HOME}/ ${CONAN_HOME}/ ARG CONAN_USER_HOME ENV CONAN_USER_HOME=${CONAN_USER_HOME}` CONAN_HOME=${CONAN_HOME}` diff --git a/sonar-project.properties b/sonar-project.properties index 69e8257b..0a89ef3b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,7 +6,6 @@ sonar.links.scm=https://github.com/UIUCLibrary/pyexiv2bind sonar.links.homepage=https://github.com/UIUCLibrary/pyexiv2bind sonar.tests=tests sonar.scm.provider=git -sonar.python.coverage.reportPaths=reports/coverage/coverage-python.xml,reports/coverage/coverage_cpp_sonar.xml sonar.python.flake8.reportPaths=logs/flake8.log sonar.python.pylint.reportPaths=reports/pylint.txt sonar.python.version=3 diff --git a/src/py3exiv2bind/core/CMakeLists.txt b/src/py3exiv2bind/core/CMakeLists.txt index 0f5b2b33..dfa92b99 100644 --- a/src/py3exiv2bind/core/CMakeLists.txt +++ b/src/py3exiv2bind/core/CMakeLists.txt @@ -7,7 +7,7 @@ if (pyexiv2bind_generate_python_bindings) Python3_add_library(core core.cpp $ ) set_target_properties(core PROPERTIES EXCLUDE_FROM_ALL on) add_custom_command(TARGET core - POST_BUILD + POST_BUILD COMMAND ${Python3_EXECUTABLE} setup.py build_py --build-lib ${PROJECT_BINARY_DIR} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) diff --git a/src/py3exiv2bind/core/core.cpp b/src/py3exiv2bind/core/core.cpp index 9623c8b9..e536fcd2 100644 --- a/src/py3exiv2bind/core/core.cpp +++ b/src/py3exiv2bind/core/core.cpp @@ -5,13 +5,15 @@ #include "glue/glue.h" #include "glue/Image.h" #include "glue/glue_execeptions.h" +#include #include #include +#include #include // PyBind11 library generates warnings for clang-tidy that I can't do anything about // NOLINTNEXTLINE -PYBIND11_MODULE(core, m) { +PYBIND11_MODULE(core, m, pybind11::mod_gil_not_used()){ pybind11::options options; options.enable_function_signatures(); m.doc() = R"pbdoc( @@ -21,8 +23,8 @@ PYBIND11_MODULE(core, m) { m.def("exiv2_version", &exiv2_version, "Version of exiv2 that is built with"); pybind11::class_(m, "Image", "The c++ binding for libexiv2") .def(pybind11::init()) - .def_property_readonly("filename", [](const Image &i) { - return pybind11::str(i.getFilename()); + .def_property_readonly("filename", [](const Image &img) { + return pybind11::str(img.getFilename()); }, "Name of file loaded") .def_property_readonly("pixelHeight", &Image::get_pixelHeight, "Number of pixels high") .def_property_readonly("pixelWidth", &Image::get_pixelWidth, "Number of pixels wide") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 149bc9aa..0fd7b832 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,6 @@ include(FetchContent) -if (BUILD_TESTING) - +if (BUILD_TESTING AND Catch2_FOUND) + include(Catch) FetchContent_Declare(test_images URL https://nexus.library.illinois.edu/repository/sample-data/images/sample_images.tar.gz SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/sample_images @@ -19,6 +19,7 @@ if (BUILD_TESTING) ) add_dependencies(add_python_tests core) add_custom_command(TARGET add_python_tests + POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/conftest.py ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/test_core.py ${CMAKE_CURRENT_BINARY_DIR} # COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/test_common.py ${CMAKE_CURRENT_BINARY_DIR} @@ -50,8 +51,6 @@ if (BUILD_TESTING) endif() endif() - FetchContent_MakeAvailable(libcatch2) - FetchContent_GetProperties(libcatch2) list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) add_executable(cpp_tests test_basic.cpp @@ -68,8 +67,8 @@ if (BUILD_TESTING) ) target_include_directories(cpp_tests PRIVATE ${PROJECT_SOURCE_DIR}/src/py3exiv2bind/core) target_link_libraries(cpp_tests PRIVATE Catch2::Catch2WithMain exiv2lib) - include(Catch) + list(APPEND CMAKE_MODULE_PATH ${catch_SOURCE_DIR}/contrib/) catch_discover_tests(cpp_tests TEST_PREFIX ${PROJECT_NAME}::test-pyexiv2::) add_executable(test_editing test_editing.cpp diff --git a/uv.lock b/uv.lock index 5a75e8e8..76cb039c 100644 --- a/uv.lock +++ b/uv.lock @@ -617,7 +617,7 @@ wheels = [ [[package]] name = "gcovr" -version = "8.3" +version = "8.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, @@ -626,9 +626,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/9f/2883275d71f27f81919a7f000afe7eb344496ab74d62e1c0e4a804918b9f/gcovr-8.3.tar.gz", hash = "sha256:faa371f9c4a7f78c9800da655107d4f99f04b718d1c0d9f48cafdcbef0049079", size = 175323, upload-time = "2025-01-19T22:24:58.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/ce/b7516854699f807f58c3c9801ad44de7f51a952be16b62a5948b358f1aa4/gcovr-8.4.tar.gz", hash = "sha256:8ea0cf23176b1029f28db679d712ca6477b3807097c3755c135bdc53b51cfa72", size = 188132, upload-time = "2025-09-27T22:13:28.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/59/22844af9ee07d106aeefcf1f19355b7dcf36deef01eb7bc9045839b5a0aa/gcovr-8.3-py3-none-any.whl", hash = "sha256:d613a90aeea967b4972fbff69587bf8995ee3cd80df2556983b73141f30642d2", size = 224188, upload-time = "2025-01-19T22:24:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/b049fc9af7d3b0e4bd3af90ebba0e2f4ec42857674c8ba5ed15b5d719170/gcovr-8.4-py3-none-any.whl", hash = "sha256:1016d013d6c55225b1f716a4325e16ad5d626ff05b20b8a12d542e2d9a82ac21", size = 240591, upload-time = "2025-09-27T22:13:26.936Z" }, ] [[package]]