diff --git a/.github/workflows/build-nuget-package.yml b/.github/workflows/build-nuget-package.yml index f2c3cf84bd..ab9bb2d3af 100644 --- a/.github/workflows/build-nuget-package.yml +++ b/.github/workflows/build-nuget-package.yml @@ -7,9 +7,9 @@ concurrency: cancel-in-progress: true jobs: - # macos 12 is Intel - build_macos_12: - runs-on: macos-12 + # macos 13 is Intel + build_macos_13: + runs-on: macos-13 # strategy: # matrix: # python: [3.11] @@ -32,7 +32,7 @@ jobs: name: macos-x64 path: ${{runner.workspace}}/build/dotnet/Highs.Native/runtimes - # macos 14 is M1 (beta) + # macos 14 is M1 build_macos_14: runs-on: macos-14 steps: @@ -124,7 +124,7 @@ jobs: build_windows: runs-on: windows-latest - needs: [build_macos_12, build_macos_14, build_windows_32, build_linux, build_linux_arm64] + needs: [build_macos_13, build_macos_14, build_windows_32, build_linux, build_linux_arm64] steps: - uses: actions/checkout@v4 - name: Build HiGHS Windows native @@ -185,7 +185,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/build-python-package.yml b/.github/workflows/build-python-package.yml index c408458a13..9268eb2964 100644 --- a/.github/workflows/build-python-package.yml +++ b/.github/workflows/build-python-package.yml @@ -14,20 +14,19 @@ jobs: - name: Build sdist run: | - python3 -m pip install build --user + python3 -m pip install build python3 -m build --sdist - name: Install sdist run: | ls dist - python3 -m pip install meson-python meson pybind11 ninja --user python3 -m pip install dist/*.tar.gz - name: Test highspy run: | - python3 -m pip install pytest --user - python3 -m pytest $GITHUB_WORKSPACE/tests - + python3 -m pip install pytest + python3 -m pytest $GITHUB_WORKSPACE + build_sdist_mac: runs-on: macos-latest steps: @@ -35,19 +34,18 @@ jobs: - name: Build sdist run: | - python3 -m pip install build --user --break-system-packages + python3 -m pip install build --break-system-packages python3 -m build --sdist - name: Install sdist run: | ls dist - python3 -m pip install meson-python meson pybind11 ninja --user --break-system-packages - python3 -m pip install dist/*.tar.gz --user --break-system-packages + python3 -m pip install dist/*.tar.gz --break-system-packages - name: Test highspy run: | - python3 -m pip install pytest --user --break-system-packages - python3 -m pytest $GITHUB_WORKSPACE/tests + python3 -m pip install pytest --break-system-packages + python3 -m pytest $GITHUB_WORKSPACE build_sdist_win: runs-on: windows-2019 @@ -61,23 +59,22 @@ jobs: - name: Build sdist run: | - python -m pip install build --user + python -m pip install build python -m build --sdist - name: Install sdist run: | - python3 -m pip install meson-python meson pybind11 ninja --user $item = Get-ChildItem dist - python -m pip install "$item" --user + python -m pip install "$item" python -c "import highspy; print(dir(highspy))" - name: Test highspy run: | - python -m pip install pytest --user - python -m pytest tests - + python -m pip install pytest + python -m pytest + build_wheel_linux: - # ubuntu 22 has a latest version of cmake, but setup-python + # ubuntu 22 has a latest version of cmake, but setup-python # does not seem to provide all necessary modules to find python # from cmake. works on my machine, test the wheels manually runs-on: ubuntu-latest @@ -106,12 +103,11 @@ jobs: - name: Test highspy run: | python3 -m pip install pytest - python3 -m pytest $GITHUB_WORKSPACE/tests - - - # macos 12 is Intel - build_wheel_macos_12: - runs-on: macos-12 + python3 -m pytest $GITHUB_WORKSPACE + + # macos 13 is Intel + build_wheel_macos_13: + runs-on: macos-13 strategy: matrix: python: [3.11] @@ -125,27 +121,27 @@ jobs: - name: Build wheel run: | - python3 -m pip install cibuildwheel --break-system-packages + python3 -m pip install cibuildwheel python3 -m cibuildwheel --only cp311-macosx_x86_64 $GITHUB_WORKSPACE - name: Install wheel run: | ls wheelhouse python3 --version - python3 -m pip install wheelhouse/*.whl --break-system-packages + python3 -m pip install wheelhouse/*.whl python3 -c "import highspy; print(dir(highspy))" - name: Test highspy run: | - python3 -m pip install pytest --break-system-packages - python3 -m pytest $GITHUB_WORKSPACE/tests - - # macos 13 is Intel - build_wheel_macos_13: - runs-on: macos-13 + python3 -m pip install pytest + python3 -m pytest $GITHUB_WORKSPACE + + # macos 14 is M1 + build_wheel_macos_14: + runs-on: macos-14 strategy: matrix: - python: [3.11] + python: [3.11] steps: - uses: actions/checkout@v4 @@ -156,51 +152,47 @@ jobs: - name: Build wheel run: | - python3 -m pip install cibuildwheel --break-system-packages - python3 -m cibuildwheel --only cp311-macosx_x86_64 $GITHUB_WORKSPACE + python3 -m pip install cibuildwheel + python3 -m cibuildwheel --only cp311-macosx_arm64 $GITHUB_WORKSPACE - name: Install wheel run: | ls wheelhouse python3 --version - python3 -m pip install wheelhouse/*.whl --break-system-packages + python3 -m pip install wheelhouse/*.whl python3 -c "import highspy; print(dir(highspy))" - name: Test highspy run: | - python3 -m pip install pytest --break-system-packages - python3 -m pytest $GITHUB_WORKSPACE/tests - - # macos 14 is M1 (beta) - build_wheel_macos_14: - runs-on: macos-14 - strategy: - matrix: - python: [3.11] + python3 -m pip install pytest + python3 -m pytest $GITHUB_WORKSPACE + + build_wheel_windows: + runs-on: windows-2019 steps: - uses: actions/checkout@v4 - name: Install correct python version uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python }} + python-version: 3.9 - name: Build wheel run: | - python3 -m pip install cibuildwheel --break-system-packages - python3 -m cibuildwheel --only cp311-macosx_arm64 $GITHUB_WORKSPACE + python -m pip install cibuildwheel + python -m cibuildwheel --only cp39-win_amd64 $GITHUB_WORKSPACE - name: Install wheel run: | ls wheelhouse - python3 --version - python3 -m pip install wheelhouse/*.whl --break-system-packages - python3 -c "import highspy; print(dir(highspy))" + $item = Get-ChildItem wheelhouse + python -m pip install "$item" + python -c "import highspy; print(dir(highspy))" - name: Test highspy run: | - python3 -m pip install pytest --break-system-packages - python3 -m pytest $GITHUB_WORKSPACE/tests + python -m pip install pytest + python -m pytest build_wheel_windows: runs-on: windows-2019 @@ -210,12 +202,12 @@ jobs: - name: Install correct python version uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.13 - name: Build wheel run: | python -m pip install cibuildwheel - python -m cibuildwheel --only cp39-win_amd64 $GITHUB_WORKSPACE + python -m cibuildwheel --only cp313-win_amd64 $GITHUB_WORKSPACE - name: Install wheel run: | @@ -227,4 +219,4 @@ jobs: - name: Test highspy run: | python -m pip install pytest - python -m pytest tests + python -m pytest diff --git a/.github/workflows/build-wheels-push.yml b/.github/workflows/build-wheels-push.yml index 394ea0987f..6bd76ef7cd 100644 --- a/.github/workflows/build-wheels-push.yml +++ b/.github/workflows/build-wheels-push.yml @@ -12,6 +12,24 @@ concurrency: cancel-in-progress: true jobs: + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + shell: bash + run: pipx run build --sdist + + # - name: check metadata + # run: pipx run twine check python/dist/* + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: python/dist/*.tar.gz + build_wheels: name: Build wheel for ${{ matrix.python }}-${{ matrix.buildplat[1] }} runs-on: ${{ matrix.buildplat[0] }} @@ -30,47 +48,38 @@ jobs: - [ubuntu-20.04, musllinux_x86_64] # No OpenBlas, no test - [ubuntu-20.04, musllinux_i686] - [ubuntu-20.04, musllinux_aarch64] - - [macos-12, macosx_x86_64] + - [macos-13, macosx_x86_64] - [macos-14, macosx_arm64] - [windows-2019, win_amd64] - [windows-2019, win32] - python: ["cp38", "cp39","cp310", "cp311","cp312"] + python: ["cp38", "cp39","cp310", "cp311","cp312", "cp313"] steps: - uses: actions/checkout@v4 + - name: Set up QEMU # Required for aarch64 builds if: ${{ contains(matrix.buildplat[1], 'aarch64') }} uses: docker/setup-qemu-action@v3 with: platforms: all + - name: Build wheels (aarch64) if: ${{ contains(matrix.buildplat[1], 'aarch64') }} - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21 env: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }} CIBW_ARCHS_LINUX: aarch64 + - name: Build wheels (not aarch64) if: ${{ !contains(matrix.buildplat[1], 'aarch64') }} - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21 env: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }} - - uses: actions/upload-artifact@v3 - with: - path: ./wheelhouse/*.whl - - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build sdist - shell: bash -l {0} - run: pipx run build --sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - path: dist/*.tar.gz + name: cibw-wheels-${{ matrix.python }}-${{ matrix.buildplat[1] }} + path: wheelhouse/*.whl # upload_testpypi: # name: >- @@ -88,12 +97,11 @@ jobs: # permissions: # id-token: write # IMPORTANT: mandatory for trusted publishing # steps: - # - uses: actions/download-artifact@v3 + # - uses: actions/download-artifact@v4 # with: - # # unpacks default artifact into dist/ - # # if `name: artifact` is omitted, the action will create extra parent dir - # name: artifact + # pattern: cibw-* # path: dist + # merge-multiple: true # - name: Download all # uses: pypa/gh-action-pypi-publish@release/v1 @@ -107,7 +115,7 @@ jobs: needs: [build_wheels, build_sdist] # upload to PyPI on every tag starting with 'v' - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') environment: name: pypi @@ -117,12 +125,11 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - # unpacks default artifact into dist/ - # if `name: artifact` is omitted, the action will create extra parent dir - name: artifact + pattern: cibw-* path: dist + merge-multiple: true - name: Download all uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index aa658d85a7..ce012369bf 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -8,6 +8,16 @@ concurrency: cancel-in-progress: true jobs: + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + shell: bash + run: pipx run build --sdist + build_wheels: name: Build wheel for ${{ matrix.python }}-${{ matrix.buildplat[1] }} runs-on: ${{ matrix.buildplat[0] }} @@ -25,11 +35,11 @@ jobs: - [ubuntu-20.04, musllinux_x86_64] # No OpenBlas, no test - [ubuntu-20.04, musllinux_i686] - [ubuntu-20.04, musllinux_aarch64] - - [macos-12, macosx_x86_64] + - [macos-13, macosx_x86_64] - [macos-14, macosx_arm64] - [windows-2019, win_amd64] - [windows-2019, win32] - python: ["cp38", "cp39","cp310", "cp311","cp312"] + python: ["cp38", "cp39","cp310", "cp311","cp312", "cp313"] steps: - uses: actions/checkout@v4 @@ -38,24 +48,16 @@ jobs: uses: docker/setup-qemu-action@v3 with: platforms: all + - name: Build wheels (aarch64) if: ${{ contains(matrix.buildplat[1], 'aarch64') }} - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21 env: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }} CIBW_ARCHS_LINUX: aarch64 + - name: Build wheels (not aarch64) if: ${{ !contains(matrix.buildplat[1], 'aarch64') }} - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21 env: CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }} - - build_sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build sdist - shell: bash -l {0} - run: pipx run build --sdist diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index f14d66e150..b2ddc66b08 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -229,7 +229,7 @@ jobs: # Note the current convention is to use the -S and -B options here to specify source # and build directories, but this is only available with CMake 3.13 and higher. # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 - run: cmake $GITHUB_WORKSPACE -DFAST_BUILD=OFF -DHIGHSINT64=on -DHIGHS_NO_DEFAULT_THREADS=ON + run: cmake $GITHUB_WORKSPACE -DHIGHSINT64=on -DHIGHS_NO_DEFAULT_THREADS=ON - name: Build working-directory: ${{runner.workspace}}/build @@ -263,7 +263,7 @@ jobs: # Note the current convention is to use the -S and -B options here to specify source # and build directories, but this is only available with CMake 3.13 and higher. # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 - run: cmake $GITHUB_WORKSPACE -DFAST_BUILD=OFF -DHIGHSINT64=on -DHIGHS_NO_DEFAULT_THREADS=ON + run: cmake $GITHUB_WORKSPACE -DHIGHSINT64=on -DHIGHS_NO_DEFAULT_THREADS=ON -DALL_TESTS=ON - name: Build working-directory: ${{runner.workspace}}/build diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 5dfb9d9591..8e368317c8 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -8,9 +8,10 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: DoozyX/clang-format-lint-action@v0.14 + - uses: DoozyX/clang-format-lint-action@v0.18 with: - source: 'app/ src/Highs.h ./src/lp_data ./src/mip ./src/model ./src/simplex ./src/presolve ./src/simplex ./src/util ./src/test' - #./src/test ./interfaces' + source: + 'app/ src/Highs.h ./src/lp_data ./src/mip ./src/model ./src/simplex ./src/presolve ./src/simplex ./src/util ./src/test ./src/qpsolver' + # ./src/test ./interfaces' extensions: 'h,cpp,c' - clangFormatVersion: 14 + clangFormatVersion: 18 diff --git a/.github/workflows/sanitizers-cmake.yml b/.github/workflows/sanitizers-cmake.yml new file mode 100644 index 0000000000..6af8517c18 --- /dev/null +++ b/.github/workflows/sanitizers-cmake.yml @@ -0,0 +1,55 @@ +name: sanitizers-cmake +on: [] #push + +jobs: + sanitizer_release: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest] + sanitizer: [Address, Thread, Leak] + steps: + - uses: actions/checkout@v4 + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake and Build + shell: bash + working-directory: ${{runner.workspace}}/build + run: | + cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDEBUG_MEMORY=${{ matrix.sanitizer }} + cmake --build . --parallel + + - name: Run + working-directory: ${{runner.workspace}}/build + shell: bash + run: ./bin/highs $GITHUB_WORKSPACE/check/instances/afiro.mps + + sanitizer_debug: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest] + sanitizer: [Address, Thread, Leak] + steps: + - uses: actions/checkout@v4 + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure CMake and Build + shell: bash + working-directory: ${{runner.workspace}}/build + run: | + cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Debug -DDEBUG_MEMORY=${{ matrix.sanitizer }} + cmake --build . --parallel + + - name: Run + working-directory: ${{runner.workspace}}/build + shell: bash + run: ./bin/highs $GITHUB_WORKSPACE/check/instances/afiro.mps \ No newline at end of file diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers-meson.yml similarity index 100% rename from .github/workflows/sanitizers.yml rename to .github/workflows/sanitizers-meson.yml diff --git a/.github/workflows/test-fortran-macos.yml b/.github/workflows/test-fortran-macos.yml index e90da71869..599e1f1043 100644 --- a/.github/workflows/test-fortran-macos.yml +++ b/.github/workflows/test-fortran-macos.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: fast_build_release: - runs-on: macos-12 + runs-on: [macos-13] steps: diff --git a/.github/workflows/test-nuget-macos.yml b/.github/workflows/test-nuget-macos.yml index 32724a3691..942494ebaf 100644 --- a/.github/workflows/test-nuget-macos.yml +++ b/.github/workflows/test-nuget-macos.yml @@ -7,9 +7,9 @@ concurrency: cancel-in-progress: true jobs: - # macos 12 is Intel - build_macos_12: - runs-on: macos-12 + # macos 13 is Intel + build_macos_13: + runs-on: macos-13 # strategy: # matrix: # python: [3.11] @@ -35,7 +35,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -54,9 +54,8 @@ jobs: dotnet add package Highs.Native -s ${{runner.workspace}}/nugets dotnet run - - # macos 14 is M1 (beta) + # macos 14 is M1 build_macos_14: runs-on: macos-14 steps: @@ -81,7 +80,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets diff --git a/.github/workflows/test-nuget-package.yml b/.github/workflows/test-nuget-package.yml index 00fa60ca12..76393bb97a 100644 --- a/.github/workflows/test-nuget-package.yml +++ b/.github/workflows/test-nuget-package.yml @@ -7,9 +7,9 @@ concurrency: cancel-in-progress: true jobs: - # macos 12 is Intel - build_macos_12: - runs-on: macos-12 + # macos 13 is Intel + build_macos_13: + runs-on: macos-13 # strategy: # matrix: # python: [3.11] @@ -35,7 +35,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -56,7 +56,7 @@ jobs: - # macos 14 is M1 (beta) + # macos 14 is M1 build_macos_14: runs-on: macos-14 steps: @@ -81,7 +81,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -124,7 +124,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -167,7 +167,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -212,7 +212,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source -n name ${{runner.workspace}}\nugets @@ -228,5 +228,5 @@ jobs: dotnet new console rm Program.cs cp ${{runner.workspace}}\HiGHS\examples\call_highs_from_csharp.cs . - dotnet add package Highs.Native -v 1.7.2 -s ${{runner.workspace}}\nugets + dotnet add package Highs.Native -v 1.8.0 -s ${{runner.workspace}}\nugets dotnet run diff --git a/.github/workflows/test-nuget-ubuntu.yml b/.github/workflows/test-nuget-ubuntu.yml index 7ad24fe9e2..7c233abd36 100644 --- a/.github/workflows/test-nuget-ubuntu.yml +++ b/.github/workflows/test-nuget-ubuntu.yml @@ -31,7 +31,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets @@ -74,7 +74,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source ${{runner.workspace}}/nugets diff --git a/.github/workflows/test-nuget-win.yml b/.github/workflows/test-nuget-win.yml index 965fce54d3..47bdf0ac60 100644 --- a/.github/workflows/test-nuget-win.yml +++ b/.github/workflows/test-nuget-win.yml @@ -33,7 +33,7 @@ jobs: - name: Dotnet pack working-directory: ${{runner.workspace}}/build/dotnet/Highs.Native - run: dotnet pack -c Release /p:Version=1.7.2 + run: dotnet pack -c Release /p:Version=1.8.0 - name: Add local feed run: dotnet nuget add source -n name ${{runner.workspace}}\nugets @@ -49,5 +49,5 @@ jobs: dotnet new console rm Program.cs cp ${{runner.workspace}}\HiGHS\examples\call_highs_from_csharp.cs . - dotnet add package Highs.Native -v 1.7.2 -s ${{runner.workspace}}\nugets + dotnet add package Highs.Native -v 1.8.0 -s ${{runner.workspace}}\nugets dotnet run diff --git a/CMakeLists.txt b/CMakeLists.txt index aa59302874..cfeea6ee9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,23 @@ if(EXISTS "${LOC_PATH}") endif() option(FAST_BUILD "Fast build: " ON) +find_program(GIT git) + +if((GIT) AND(EXISTS ${HIGHS_SOURCE_DIR}/.git)) + execute_process( + COMMAND ${GIT} status + WORKING_DIRECTORY ${HIGHS_SOURCE_DIR} OUTPUT_QUIET) + + execute_process( + COMMAND ${GIT} describe --always + WORKING_DIRECTORY ${HIGHS_SOURCE_DIR} + OUTPUT_VARIABLE GITHASH OUTPUT_STRIP_TRAILING_WHITESPACE) + # string(REGEX REPLACE "^.*-g" "" GITHASH ${GITHASH}) +else() + set(GITHASH "n/a") +endif() + +message(STATUS "Git hash: " ${GITHASH}) # By default only build the C++ library. option(BUILD_CXX "Build C++ library" ON) message(STATUS "Build C++ library: ${BUILD_CXX}") @@ -71,6 +88,12 @@ if (PYTHON_BUILD_SETUP) set(ZLIB OFF) endif() +# Address | Thread | Leak +# Linux atm +# Only Debug is theted atm +# See below for RelWithDeb info, todo test wip +set(DEBUG_MEMORY "Off" CACHE STRING "Sanitizers") + # emscripten option(EMSCRIPTEN_HTML "Emscripten HTML output" OFF) @@ -358,12 +381,6 @@ if(NOT FAST_BUILD OR CSHARP) endif(CMAKE_CSharp_COMPILER) endif() -# uncomment for memory debugging -# set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fno-omit-frame-pointer -fsanitize=address -fsanitize=undefined") -# set (CMAKE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_LINKER_FLAGS_RELWITHDEBINFO} -fno-omit-frame-pointer -fsanitize=address -fsanitize=undefined") -# set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=undefined") -# set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=undefined") - # if zlib is found, then we can enable reading zlib-compressed input if(ZLIB AND NOT TARGET ZLIB::ZLIB) find_package(ZLIB 1.2.3) @@ -375,18 +392,6 @@ set(CPACK_PACKAGE_VERSION_MINOR "${HIGHS_VERSION_MINOR}") set(CPACK_PACKAGE_VERSION_PATCH "${HIGHS_VERSION_PATCH}") set(CPACK_PACKAGE_VENDOR "University of Edinburgh") -find_program(GIT git) - -if((GIT) AND(EXISTS ${HIGHS_SOURCE_DIR}/.git)) - execute_process( - COMMAND ${GIT} describe --always --dirty - WORKING_DIRECTORY ${HIGHS_SOURCE_DIR} - OUTPUT_VARIABLE GITHASH OUTPUT_STRIP_TRAILING_WHITESPACE) - string(REGEX REPLACE "^.*-g" "" GITHASH ${GITHASH}) -else() - set(GITHASH "n/a") -endif() -message(STATUS "Git hash: " ${GITHASH}) # Deprecate # string(TIMESTAMP TODAY "%Y-%m-%d") @@ -394,6 +399,56 @@ message(STATUS "Git hash: " ${GITHASH}) configure_file(${HIGHS_SOURCE_DIR}/src/HConfig.h.in ${HIGHS_BINARY_DIR}/HConfig.h) +if (DEBUG_MEMORY STREQUAL "Address") + set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} \ + -fsanitize=address,undefined \ + -fno-omit-frame-pointer \ + -fsanitize-address-use-after-scope") + set (CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} \ + -fsanitize=address,undefined \ + -fno-omit-frame-pointer \ + -fsanitize-address-use-after-scope") + + set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} \ + -fsanitize=address,undefined \ + -fno-omit-frame-pointer \ + -fno-optimize-sibling-calls ") + set (CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} \ + -fsanitize=address,undefined \ + -fno-omit-frame-pointer \ + -fno-optimize-sibling-calls ") + +elseif (DEBUG_MEMORY STREQUAL "Thread") + set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} \ + -fsanitize=thread,undefined \ + -fno-omit-frame-pointer") + set (CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} \ + -fsanitize=thread,undefined \ + -fno-omit-frame-pointer") + + set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} \ + -fsanitize=thread,undefined \ + -fno-omit-frame-pointer ") + set (CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} \ + -fsanitize=thread,undefined \ + -fno-omit-frame-pointer ") + +elseif (DEBUG_MEMORY STREQUAL "Leak") + set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} \ + -fsanitize=leak,undefined \ + -fno-omit-frame-pointer") + set (CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} \ + -fsanitize=leak,undefined \ + -fno-omit-frame-pointer") + + set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} \ + -fsanitize=leak,undefined \ + -fno-omit-frame-pointer ") + set (CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} \ + -fsanitize=leak,undefined \ + -fno-omit-frame-pointer ") +endif() + if(NOT FAST_BUILD) # For the moment keep above coverage part in case we are testing at CI. option(CI "CI extended tests" ON) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 225f329932..e45876de54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Contact [Julian](https://github.com/jajhall) (General issues and solvers), [Ivet ## Improve the documentation -The top level [documentation](https://ergo-code.github.io/HiGHS/) is created using [Docsy](https://www.docsy.dev/), with the files held on the [HiGHS repository](https://github.com/ERGO-Code/HiGHS/tree/docsy). If your change is small (like fixing typos, or one or two sentence corrections), the easiest way to do this is to fork the branch and create a pull request. (See *Contribute code to HiGHS* below for more on this.) If your change is larger, or touches multiple files, please raise an issue describing what you want to do. +The top level [documentation](https://ergo-code.github.io/HiGHS/) is created using [Docsy](https://www.docsy.dev/), with the files held on the [HiGHS repository](https://github.com/ERGO-Code/HiGHS/tree/master/docs). If your change is small (like fixing typos, or one or two sentence corrections), the easiest way to do this is to fork the branch and create a pull request. (See *Contribute code to HiGHS* below for more on this.) If your change is larger, or touches multiple files, please raise an issue describing what you want to do. ## Raise an issue diff --git a/FEATURES.md b/FEATURES.md index a5e198a407..708e928092 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -2,12 +2,34 @@ ## Code changes -Added `int64_t mip_total_lp_iterations` to `HighsCallbackDataOut` and modified accessor function +When primal infeasiblity is detected in presolve, no dual ray is available so, previously, the `has_dual_ray` parameter of `Highs::getDualRay` returned false and that was it. Now, if a null pointer is not passed for `dual_ray_value`, `Highs::getDualRay` will compute a dual ray - at the cost of solving the feasiblility LP without presolve. The same is now true for `Highs::getPrimalRay`. `Highs::getDualUnboundednessDirection` has been introduced to determine the product between the constraint matrix and the dual ray, forcing the calculation of the latter if necessary. Once a dual ray is known for the incumbent model in HiGHS, subsequent calls to `Highs::getDualRay` and `Highs::getDualUnboundednessDirection` will be vastly cheaper + +The method `Highs::getDualObjectiveValue` now exitsts to compute the dual objective value, returning `HighsStatus::kError` if it is not possible. + +The method `Highs::getStandardFormLp` now exists to return the incumbent LP in standard form - overlooking any integrality or Hessian. To determine the sizes of the vectors of data, the method is called without specifying pointers to the data arrays. + +Added documentation on the use of presolve when solving an incumbent model, and clarifying the use of the method `Highs::presolve`. + +HiGHS will now read a `MIPLIB` solution file + +Added time limit check to `HPresolve::strengthenInequalities` + +Added `getColIntegrality` to `highspy` + +Now computing the primal-dual integral, reporting it, and making it available as `HighsInfo::primal_dual_integral` + +Trivial primal heuristics "all zero", "all lower bound", "all upper bound", and "lock point" added to the MIP solver + + + + + + + + + -`Highs::writeSolution` and `Highs::writeBasis` now being done via `HighsIO` logging, so can be redirected to logging callback. -Introduced `const double kHighsUndefined` as value of undefined values in a user solution. It's equal to `kHighsInf` -Added `Highs::setSolution(const HighsInt num_entries, const HighsInt* index, const double* value);` to allow a sparse primal solution to be defined. diff --git a/MODULE.bazel b/MODULE.bazel index 9eff6f4a45..44a64ec712 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( name = "highs", - version = "1.7.2", + version = "1.8.0", ) bazel_dep( diff --git a/README.md b/README.md index 8ea121d8bd..2758c02fdd 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ The nuget package Highs.Native is on https://www.nuget.org, at https://www.nuget It can be added to your C# project with `dotnet` ```shell -dotnet add package Highs.Native --version 1.7.2 +dotnet add package Highs.Native --version 1.8.0 ``` The nuget package contains runtime libraries for diff --git a/Version.txt b/Version.txt index ee0905004d..baa9e163b1 100644 --- a/Version.txt +++ b/Version.txt @@ -1,4 +1,4 @@ HIGHS_MAJOR=1 -HIGHS_MINOR=7 -HIGHS_PATCH=2 +HIGHS_MINOR=8 +HIGHS_PATCH=0 #PRE_RELEASE=YES diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0363d7dcab..78a96e4d12 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -13,7 +13,7 @@ if(FAST_BUILD) set(win_version_file) endif() - target_sources(highs-bin PRIVATE RunHighs.cpp ${win_version_file}) + target_sources(highs-bin PRIVATE RunHighs.cpp HighsRuntimeOptions.h ${win_version_file}) target_include_directories(highs-bin PRIVATE $ diff --git a/src/lp_data/HighsRuntimeOptions.h b/app/HighsRuntimeOptions.h similarity index 99% rename from src/lp_data/HighsRuntimeOptions.h rename to app/HighsRuntimeOptions.h index c4b7ecc1e0..04d3133e4b 100644 --- a/src/lp_data/HighsRuntimeOptions.h +++ b/app/HighsRuntimeOptions.h @@ -245,7 +245,7 @@ bool loadOptions(const HighsLogOptions& report_log_options, int argc, return false; } - } catch (const cxxopts::OptionException& e) { + } catch (const cxxopts::exceptions::exception& e) { highsLogUser(report_log_options, HighsLogType::kError, "Error parsing options: %s\n", e.what()); return false; diff --git a/app/RunHighs.cpp b/app/RunHighs.cpp index 183bb917c8..4b6842afb0 100644 --- a/app/RunHighs.cpp +++ b/app/RunHighs.cpp @@ -13,7 +13,10 @@ */ #include "Highs.h" // #include "io/HighsIO.h" -#include "lp_data/HighsRuntimeOptions.h" +#include "HighsRuntimeOptions.h" + +// uncomment if we will be shutting down task executor from exe +// #include "parallel/HighsParallel.h" void reportModelStatsOrError(const HighsLogOptions& log_options, const HighsStatus read_status, @@ -62,6 +65,24 @@ int main(int argc, char** argv) { return (int)read_solution_status; } } + if (options.write_presolved_model_to_file) { + // Run presolve and write the presolved model to a file + HighsStatus status = highs.presolve(); + if (status == HighsStatus::kError) return int(status); + HighsPresolveStatus model_presolve_status = highs.getModelPresolveStatus(); + const bool ok_to_write = + model_presolve_status == HighsPresolveStatus::kNotReduced || + model_presolve_status == HighsPresolveStatus::kReduced || + model_presolve_status == HighsPresolveStatus::kReducedToEmpty || + model_presolve_status == HighsPresolveStatus::kTimeout; + if (!ok_to_write) { + highsLogUser(log_options, HighsLogType::kInfo, + "No presolved model to write to file\n"); + return int(status); + } + status = highs.writePresolvedModel(options.write_presolved_model_file); + return int(status); + } // Solve the model HighsStatus run_status = highs.run(); if (run_status == HighsStatus::kError) return int(run_status); @@ -78,6 +99,10 @@ int main(int argc, char** argv) { if (write_model_status == HighsStatus::kError) return (int)write_model_status; // todo: change to write model error } + + // Shut down task executor: optional and wip + // HighsTaskExecutor::shutdown(true); + return (int)run_status; } diff --git a/app/cxxopts.hpp b/app/cxxopts.hpp index 080a7c0391..30575e4659 100644 --- a/app/cxxopts.hpp +++ b/app/cxxopts.hpp @@ -1,6 +1,6 @@ /* -Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck +Copyright (c) 2014-2022 Jarryd Beck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22,41 +22,111 @@ THE SOFTWARE. */ +// vim: ts=2:sw=2:expandtab + #ifndef CXXOPTS_HPP_INCLUDED #define CXXOPTS_HPP_INCLUDED +#include #include -#include #include -#include +#include +#include #include #include -#include #include #include #include #include +#include #include +#include +#include + +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +#endif + +#define CXXOPTS_FALLTHROUGH +#ifdef __has_cpp_attribute + #if __has_cpp_attribute(fallthrough) + #undef CXXOPTS_FALLTHROUGH + #define CXXOPTS_FALLTHROUGH [[fallthrough]] + #endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif -#ifdef __cpp_lib_optional -#include -#define CXXOPTS_HAS_OPTIONAL +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' #endif -#define CXXOPTS__VERSION_MAJOR 2 +#define CXXOPTS__VERSION_MAJOR 3 #define CXXOPTS__VERSION_MINOR 2 #define CXXOPTS__VERSION_PATCH 0 -namespace cxxopts -{ - static constexpr struct { - uint8_t major, minor, patch; - } version = { - CXXOPTS__VERSION_MAJOR, - CXXOPTS__VERSION_MINOR, - CXXOPTS__VERSION_PATCH - }; -} +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 + #define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif + +namespace cxxopts { +static constexpr struct { + uint8_t major, minor, patch; +} version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH +}; +} // namespace cxxopts //when we ask cxxopts to use Unicode, help strings are processed using ICU, //which results in the correct lengths being computed for strings when they @@ -67,1428 +137,2137 @@ namespace cxxopts #ifdef CXXOPTS_USE_UNICODE #include -namespace cxxopts -{ - typedef icu::UnicodeString String; - - inline - String - toLocalString(std::string s) - { - return icu::UnicodeString::fromUTF8(std::move(s)); - } - - class UnicodeStringIterator : public - std::iterator - { - public: - - UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) - : s(string) - , i(pos) - { - } - - value_type - operator*() const - { - return s->char32At(i); - } - - bool - operator==(const UnicodeStringIterator& rhs) const - { - return s == rhs.s && i == rhs.i; - } +namespace cxxopts { - bool - operator!=(const UnicodeStringIterator& rhs) const - { - return !(*this == rhs); - } +using String = icu::UnicodeString; - UnicodeStringIterator& - operator++() - { - ++i; - return *this; - } +inline +String +toLocalString(std::string s) +{ + return icu::UnicodeString::fromUTF8(std::move(s)); +} - UnicodeStringIterator - operator+(int32_t v) - { - return UnicodeStringIterator(s, i + v); - } +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") +// This will be ignored under other compilers like LLVM clang. +class UnicodeStringIterator +{ + public: - private: - const icu::UnicodeString* s; - int32_t i; - }; + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; - inline - String& - stringAppend(String&s, String a) + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) { - return s.append(std::move(a)); } - inline - String& - stringAppend(String& s, int n, UChar32 c) + value_type + operator*() const { - for (int i = 0; i != n; ++i) - { - s.append(c); - } - - return s; + return s->char32At(i); } - template - String& - stringAppend(String& s, Iterator begin, Iterator end) + bool + operator==(const UnicodeStringIterator& rhs) const { - while (begin != end) - { - s.append(*begin); - ++begin; - } - - return s; + return s == rhs.s && i == rhs.i; } - inline - size_t - stringLength(const String& s) + bool + operator!=(const UnicodeStringIterator& rhs) const { - return s.length(); + return !(*this == rhs); } - inline - std::string - toUTF8String(const String& s) + UnicodeStringIterator& + operator++() { - std::string result; - s.toUTF8String(result); - - return result; + ++i; + return *this; } - inline - bool - empty(const String& s) + UnicodeStringIterator + operator+(int32_t v) { - return s.isEmpty(); + return UnicodeStringIterator(s, i + v); } + + private: + const icu::UnicodeString* s; + int32_t i; +}; +CXXOPTS_DIAGNOSTIC_POP + +inline +String& +stringAppend(String&s, String a) +{ + return s.append(std::move(a)); } -namespace std +inline +String& +stringAppend(String& s, std::size_t n, UChar32 c) { - inline - cxxopts::UnicodeStringIterator - begin(const icu::UnicodeString& s) + for (std::size_t i = 0; i != n; ++i) { - return cxxopts::UnicodeStringIterator(&s, 0); + s.append(c); } - inline - cxxopts::UnicodeStringIterator - end(const icu::UnicodeString& s) + return s; +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + while (begin != end) { - return cxxopts::UnicodeStringIterator(&s, s.length()); + s.append(*begin); + ++begin; } + + return s; +} + +inline +size_t +stringLength(const String& s) +{ + return static_cast(s.length()); +} + +inline +std::string +toUTF8String(const String& s) +{ + std::string result; + s.toUTF8String(result); + + return result; +} + +inline +bool +empty(const String& s) +{ + return s.isEmpty(); +} + +} // namespace cxxopts + +namespace std { + +inline +cxxopts::UnicodeStringIterator +begin(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, 0); } +inline +cxxopts::UnicodeStringIterator +end(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, s.length()); +} + +} // namespace std + //ifdef CXXOPTS_USE_UNICODE #else -namespace cxxopts +namespace cxxopts { + +using String = std::string; + +template +T +toLocalString(T&& t) +{ + return std::forward(t); +} + +inline +std::size_t +stringLength(const String& s) { - typedef std::string String; + return s.length(); +} - template - T - toLocalString(T&& t) +inline +String& +stringAppend(String&s, const String& a) +{ + return s.append(a); +} + +inline +String& +stringAppend(String& s, std::size_t n, char c) +{ + return s.append(n, c); +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + return s.append(begin, end); +} + +template +std::string +toUTF8String(T&& t) +{ + return std::forward(t); +} + +inline +bool +empty(const std::string& s) +{ + return s.empty(); +} + +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts { + +namespace { +CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); +} // namespace + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we +// want to silence it: warning: base class 'class +// std::enable_shared_from_this' has accessible non-virtual +// destructor This will be ignored under other compilers like LLVM clang. +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + +// some older versions of GCC warn under this warning +CXXOPTS_IGNORE_WARNING("-Weffc++") +class Value : public std::enable_shared_from_this +{ + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + add(const std::string& text) const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; +}; + +CXXOPTS_DIAGNOSTIC_POP + +namespace exceptions { + +class exception : public std::exception +{ + public: + explicit exception(std::string message) + : m_message(std::move(message)) { - return std::forward(t); } - inline - size_t - stringLength(const String& s) + CXXOPTS_NODISCARD + const char* + what() const noexcept override { - return s.length(); + return m_message.c_str(); } - inline - String& - stringAppend(String&s, String a) + private: + std::string m_message; +}; + +class specification : public exception +{ + public: + + explicit specification(const std::string& message) + : exception(message) { - return s.append(std::move(a)); } +}; - inline - String& - stringAppend(String& s, size_t n, char c) +class parsing : public exception +{ + public: + explicit parsing(const std::string& message) + : exception(message) { - return s.append(n, c); } +}; - template - String& - stringAppend(String& s, Iterator begin, Iterator end) +class option_already_exists : public specification +{ + public: + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") { - return s.append(begin, end); } +}; - template - std::string - toUTF8String(T&& t) +class invalid_option_format : public specification +{ + public: + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) { - return std::forward(t); } +}; - inline - bool - empty(const std::string& s) +class invalid_option_syntax : public parsing { + public: + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") { - return s.empty(); } -} +}; -//ifdef CXXOPTS_USE_UNICODE -#endif +class no_such_option : public parsing +{ + public: + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } +}; -namespace cxxopts +class missing_argument : public parsing { - namespace + public: + explicit missing_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) { -#ifdef _WIN32 - const std::string LQUOTE("\'"); - const std::string RQUOTE("\'"); -#else - const std::string LQUOTE("‘"); - const std::string RQUOTE("’"); -#endif } +}; - class Value : public std::enable_shared_from_this +class option_requires_argument : public parsing +{ + public: + explicit option_requires_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) { - public: + } +}; - virtual ~Value() = default; +class gratuitous_argument_for_option : public parsing +{ + public: + gratuitous_argument_for_option + ( + const std::string& option, + const std::string& arg + ) + : parsing( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } +}; - virtual - std::shared_ptr - clone() const = 0; +class requested_option_not_present : public parsing +{ + public: + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") + { + } +}; - virtual void - parse(const std::string& text) const = 0; +class option_has_no_value : public exception +{ + public: + explicit option_has_no_value(const std::string& option) + : exception( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } +}; - virtual void - parse() const = 0; +class incorrect_argument_type : public parsing +{ + public: + explicit incorrect_argument_type + ( + const std::string& arg + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } +}; - virtual bool - has_default() const = 0; +} // namespace exceptions - virtual bool - is_container() const = 0; - virtual bool - has_implicit() const = 0; +template +void throw_or_mimic(const std::string& text) +{ + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); - virtual std::string - get_default_value() const = 0; +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{text}; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif +} - virtual std::string - get_implicit_value() const = 0; +using OptionNames = std::vector; - virtual std::shared_ptr - default_value(const std::string& value) = 0; +namespace values { - virtual std::shared_ptr - implicit_value(const std::string& value) = 0; +namespace parser_tool { - virtual bool - is_boolean() const = 0; - }; +struct IntegerDesc +{ + std::string negative = ""; + std::string base = ""; + std::string value = ""; +}; +struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; +}; + +#ifdef CXXOPTS_NO_REGEX +inline IntegerDesc SplitInteger(const std::string &text) +{ + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char *pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; +} - class OptionException : public std::exception +inline bool IsTrueText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') { - public: - OptionException(const std::string& message) - : m_message(message) + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) { + return true; } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; +} - virtual const char* - what() const noexcept +inline bool IsFalseText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) { - return m_message.c_str(); + return true; } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; +} - private: - std::string m_message; - }; +inline OptionNames split_option_names(const std::string &text) +{ + OptionNames split_names; - class OptionSpecException : public OptionException + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) { - public: + throw_or_mimic(text); + } - OptionSpecException(const std::string& message) - : OptionException(message) + while (token_start_pos < length) { + const auto &npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { + throw_or_mimic(text); + } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { + next_delimiter_pos = length; + } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } } - }; + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; +} - class OptionParseException : public OptionException +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + ArguDesc argu_desc; + const char *pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) { - public: - OptionParseException(const std::string& message) - : OptionException(message) + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) { - } - }; - - class option_exists_error : public OptionSpecException + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) { - public: - option_exists_error(const std::string& option) - : OptionSpecException(u8"Option " + LQUOTE + option + RQUOTE + u8" already exists") + pdata += 1; + argu_desc.grouping = true; + while (isalnum(*pdata, std::locale::classic())) { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; } - }; + matched = !argu_desc.arg_name.empty() && *pdata == '\0'; + } + return argu_desc; +} + +#else // CXXOPTS_NO_REGEX + +namespace { +CXXOPTS_LINKONCE +const char* const integer_pattern = + "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; +CXXOPTS_LINKONCE +const char* const truthy_pattern = + "(t|T)(rue)?|1"; +CXXOPTS_LINKONCE +const char* const falsy_pattern = + "(f|F)(alse)?|0"; +CXXOPTS_LINKONCE +const char* const option_pattern = + "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; +CXXOPTS_LINKONCE +const char* const option_specifier_pattern = + "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; +CXXOPTS_LINKONCE +const char* const option_specifier_separator_pattern = ", *"; + +} // namespace + +inline IntegerDesc SplitInteger(const std::string &text) +{ + static const std::basic_regex integer_matcher(integer_pattern); - class invalid_option_format_error : public OptionSpecException + std::smatch match; + std::regex_match(text, match, integer_matcher); + + if (match.length() == 0) { - public: - invalid_option_format_error(const std::string& format) - : OptionSpecException(u8"Invalid option format " + LQUOTE + format + RQUOTE) - { - } - }; + throw_or_mimic(text); + } - class option_syntax_exception : public OptionParseException { - public: - option_syntax_exception(const std::string& text) - : OptionParseException(u8"Argument " + LQUOTE + text + RQUOTE + - u8" starts with a - but has incorrect syntax") - { - } - }; + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; - class option_not_exists_exception : public OptionParseException + if (match.length(4) > 0) { - public: - option_not_exists_exception(const std::string& option) - : OptionParseException(u8"Option " + LQUOTE + option + RQUOTE + u8" does not exist") - { - } - }; + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + static const std::basic_regex truthy_matcher(truthy_pattern); + std::smatch result; + std::regex_match(text, result, truthy_matcher); + return !result.empty(); +} + +inline bool IsFalseText(const std::string &text) +{ + static const std::basic_regex falsy_matcher(falsy_pattern); + std::smatch result; + std::regex_match(text, result, falsy_matcher); + return !result.empty(); +} - class missing_argument_exception : public OptionParseException +// Gets the option names specified via a single, comma-separated string, +// and returns the separate, space-discarded, non-empty names +// (without considering which or how many are single-character) +inline OptionNames split_option_names(const std::string &text) +{ + static const std::basic_regex option_specifier_matcher(option_specifier_pattern); + if (!std::regex_match(text.c_str(), option_specifier_matcher)) { - public: - missing_argument_exception(const std::string& option) - : OptionParseException( - u8"Option " + LQUOTE + option + RQUOTE + u8" is missing an argument" - ) + throw_or_mimic(text); + } + + OptionNames split_names; + + static const std::basic_regex option_specifier_separator_matcher(option_specifier_separator_pattern); + constexpr int use_non_matches { -1 }; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator_matcher, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + static const std::basic_regex option_matcher(option_pattern); + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); } - }; + } + + return argu_desc; +} + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX +} // namespace parser_tool + +namespace detail { + +template +struct SignedCheck; - class option_requires_argument_exception : public OptionParseException +template +struct SignedCheck +{ + template + void + operator()(bool negative, U u, const std::string& text) { - public: - option_requires_argument_exception(const std::string& option) - : OptionParseException( - u8"Option " + LQUOTE + option + RQUOTE + u8" requires an argument" - ) + if (negative) { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } } - }; - - class option_not_has_argument_exception : public OptionParseException - { - public: - option_not_has_argument_exception - ( - const std::string& option, - const std::string& arg - ) - : OptionParseException( - u8"Option " + LQUOTE + option + RQUOTE + - u8" does not take an argument, but argument " + - LQUOTE + arg + RQUOTE + " given" - ) + else { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } } - }; + } +}; + +template +struct SignedCheck +{ + template + void + operator()(bool, U, const std::string&) const {} +}; + +template +void +check_signed_range(bool negative, U value, const std::string& text) +{ + SignedCheck::is_signed>()(negative, value, text); +} + +} // namespace detail + +template +void +checked_negate(R& r, T&& t, const std::string&, std::true_type) +{ + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t-1)-1); +} - class option_not_present_exception : public OptionParseException +template +void +checked_negate(R&, T&&, const std::string& text, std::false_type) +{ + throw_or_mimic(text); +} + +template +void +integer_parser(const std::string& text, T& value) +{ + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string & value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) { - public: - option_not_present_exception(const std::string& option) - : OptionParseException(u8"Option " + LQUOTE + option + RQUOTE + u8" not present") + US digit = 0; + + if (ch >= '0' && ch <= '9') { + digit = static_cast(ch - '0'); } - }; - - class argument_incorrect_type : public OptionParseException - { - public: - argument_incorrect_type - ( - const std::string& arg - ) - : OptionParseException( - u8"Argument " + LQUOTE + arg + RQUOTE + u8" failed to parse" - ) + else if (base == 16 && ch >= 'a' && ch <= 'f') { + digit = static_cast(ch - 'a' + 10); } - }; - - class option_required_exception : public OptionParseException - { - public: - option_required_exception(const std::string& option) - : OptionParseException( - u8"Option " + LQUOTE + option + RQUOTE + u8" is required but not present" - ) + else if (base == 16 && ch >= 'A' && ch <= 'F') { + digit = static_cast(ch - 'A' + 10); } - }; - - namespace values - { - namespace + else { - std::basic_regex integer_pattern - ("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); - std::basic_regex truthy_pattern - ("(t|T)(rue)?"); - std::basic_regex falsy_pattern - ("((f|F)(alse)?)?"); + throw_or_mimic(text); } - namespace detail + US limit = 0; + if (negative) { - template - struct SignedCheck; - - template - struct SignedCheck - { - template - void - operator()(bool negative, U u, const std::string& text) - { - if (negative) - { - if (u > static_cast(std::numeric_limits::min())) - { - throw argument_incorrect_type(text); - } - } - else - { - if (u > static_cast(std::numeric_limits::max())) - { - throw argument_incorrect_type(text); - } - } - } - }; - - template - struct SignedCheck - { - template - void - operator()(bool, U, const std::string&) {} - }; - - template - void - check_signed_range(bool negative, U value, const std::string& text) - { - SignedCheck::is_signed>()(negative, value, text); - } + limit = static_cast(std::abs(static_cast(std::numeric_limits::min()))); } - - template - R - checked_negate(T&& t, const std::string&, std::true_type) + else { - // if we got to here, then `t` is a positive number that fits into - // `R`. So to avoid MSVC C4146, we first cast it to `R`. - // See https://github.com/jarro2783/cxxopts/issues/62 for more details. - return -static_cast(t); + limit = std::numeric_limits::max(); } - template - T - checked_negate(T&&, const std::string& text, std::false_type) + if (base != 0 && result > limit / base) { - throw argument_incorrect_type(text); + throw_or_mimic(text); } - - template - void - integer_parser(const std::string& text, T& value) + if (result * base > limit - digit) { - std::smatch match; - std::regex_match(text, match, integer_pattern); + throw_or_mimic(text); + } - if (match.length() == 0) - { - throw argument_incorrect_type(text); - } + result = static_cast(result * base + digit); + } - if (match.length(4) > 0) - { - value = 0; - return; - } + detail::check_signed_range(negative, result, text); - using US = typename std::make_unsigned::type; + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } +} - constexpr auto umax = std::numeric_limits::max(); - constexpr bool is_signed = std::numeric_limits::is_signed; - const bool negative = match.length(1) > 0; - const uint8_t base = match.length(2) > 0 ? 16 : 10; +template +void stringstream_parser(const std::string& text, T& value) +{ + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } +} - auto value_match = match[3]; +template ::value>::type* = nullptr + > +void parse_value(const std::string& text, T& value) +{ + integer_parser(text, value); +} - US result = 0; +inline +void +parse_value(const std::string& text, bool& value) +{ + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } - for (auto iter = value_match.first; iter != value_match.second; ++iter) - { - US digit = 0; + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } - if (*iter >= '0' && *iter <= '9') - { - digit = *iter - '0'; - } - else if (base == 16 && *iter >= 'a' && *iter <= 'f') - { - digit = *iter - 'a' + 10; - } - else if (base == 16 && *iter >= 'A' && *iter <= 'F') - { - digit = *iter - 'A' + 10; - } - else - { - throw argument_incorrect_type(text); - } + throw_or_mimic(text); +} - if (umax - digit < result * base) - { - throw argument_incorrect_type(text); - } +inline +void +parse_value(const std::string& text, std::string& value) +{ + value = text; +} - result = result * base + digit; - } +// The fallback parser. It uses the stringstream parser to parse all types +// that have not been overloaded explicitly. It has to be placed in the +// source code before all other more specialized templates. +template ::value>::type* = nullptr + > +void +parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); +} - detail::check_signed_range(negative, result, text); +template +void +parse_value(const std::string& text, std::vector& value) +{ + if (text.empty()) { + T v; + parse_value(text, v); + value.emplace_back(std::move(v)); + return; + } + std::stringstream in(text); + std::string token; + while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + T v; + parse_value(token, v); + value.emplace_back(std::move(v)); + } +} - if (negative) - { - value = checked_negate(result, - text, - std::integral_constant()); - } - else - { - value = result; - } - } +template +void +add_value(const std::string& text, T& value) +{ + parse_value(text, value); +} + +template +void +add_value(const std::string& text, std::vector& value) +{ + T v; + add_value(text, v); + value.emplace_back(std::move(v)); +} - template - void stringstream_parser(const std::string& text, T& value) +#ifdef CXXOPTS_HAS_OPTIONAL +template +void +parse_value(const std::string& text, std::optional& value) +{ + T result; + parse_value(text, result); + value = std::move(result); +} +#endif + +inline +void parse_value(const std::string& text, char& c) +{ + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; +} + +template +struct type_is_container +{ + static constexpr bool value = false; +}; + +template +struct type_is_container> +{ + static constexpr bool value = true; +}; + +template +class abstract_value : public Value +{ + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) { - std::stringstream in(text); - in >> value; - if (!in) { - throw argument_incorrect_type(text); - } + m_result = std::make_shared(); + m_store = m_result.get(); } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + add(const std::string& text) const override + { + add_value(text, *m_store); + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } - inline - void - parse_value(const std::string& text, uint8_t& value) + const T& + get() const + { + if (m_store == nullptr) { - integer_parser(text, value); + return *m_result; } + return *m_store; + } - inline - void - parse_value(const std::string& text, int8_t& value) - { - integer_parser(text, value); - } + protected: + std::shared_ptr m_result{}; + T* m_store{}; - inline - void - parse_value(const std::string& text, uint16_t& value) - { - integer_parser(text, value); - } + bool m_default = false; + bool m_implicit = false; - inline - void - parse_value(const std::string& text, int16_t& value) - { - integer_parser(text, value); - } + std::string m_default_value{}; + std::string m_implicit_value{}; +}; - inline - void - parse_value(const std::string& text, uint32_t& value) - { - integer_parser(text, value); - } +template +class standard_value : public abstract_value +{ + public: + using abstract_value::abstract_value; - inline - void - parse_value(const std::string& text, int32_t& value) - { - integer_parser(text, value); - } + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } +}; - inline - void - parse_value(const std::string& text, uint64_t& value) - { - integer_parser(text, value); - } +template <> +class standard_value : public abstract_value +{ + public: + ~standard_value() override = default; - inline - void - parse_value(const std::string& text, int64_t& value) - { - integer_parser(text, value); - } + standard_value() + { + set_default_and_implicit(); + } - inline - void - parse_value(const std::string& text, bool& value) - { - std::smatch result; - std::regex_match(text, result, truthy_pattern); + explicit standard_value(bool* b) + : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } - if (!result.empty()) - { - value = true; - return; - } + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } - std::regex_match(text, result, falsy_pattern); - if (!result.empty()) - { - value = false; - return; - } + private: - throw argument_incorrect_type(text); - } + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } +}; - inline - void - parse_value(const std::string& text, std::string& value) - { - value = text; - } +} // namespace values - // The fallback parser. It uses the stringstream parser to parse all types - // that have not been overloaded explicitly. It has to be placed in the - // source code before all other more specialized templates. - template - void - parse_value(const std::string& text, T& value) { - stringstream_parser(text, value); - } +template +std::shared_ptr +value() +{ + return std::make_shared>(); +} - template - void - parse_value(const std::string& text, std::vector& value) - { - T v; - parse_value(text, v); - value.push_back(v); - } +template +std::shared_ptr +value(T& t) +{ + return std::make_shared>(&t); +} -#ifdef CXXOPTS_HAS_OPTIONAL - template - void - parse_value(const std::string& text, std::optional& value) - { - T result; - parse_value(text, result); - value = std::move(result); - } -#endif +class OptionAdder; - template - struct type_is_container - { - static constexpr bool value = false; - }; +CXXOPTS_NODISCARD +inline +const std::string& +first_or_empty(const OptionNames& long_names) +{ + static const std::string empty{""}; + return long_names.empty() ? empty : long_names.front(); +} - template - struct type_is_container> - { - static constexpr bool value = true; - }; +class OptionDetails +{ + public: + OptionDetails + ( + std::string short_, + OptionNames long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(first_long_name() + m_short); + } - template - class abstract_value : public Value - { - using Self = abstract_value; + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } - public: - abstract_value() - : m_result(std::make_shared()) - , m_store(m_result.get()) - { - } + OptionDetails(OptionDetails&& rhs) = default; - abstract_value(T* t) - : m_store(t) - { - } + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } - virtual ~abstract_value() = default; + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } - abstract_value(const abstract_value& rhs) - { - if (rhs.m_result) - { - m_result = std::make_shared(); - m_store = m_result.get(); - } - else - { - m_store = rhs.m_store; - } + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } - m_default = rhs.m_default; - m_implicit = rhs.m_implicit; - m_default_value = rhs.m_default_value; - m_implicit_value = rhs.m_implicit_value; - } + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } - void - parse(const std::string& text) const - { - parse_value(text, *m_store); - } + CXXOPTS_NODISCARD + const std::string& + first_long_name() const + { + return first_or_empty(m_long); + } - bool - is_container() const - { - return type_is_container::value; - } + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long.front(); + } - void - parse() const - { - parse_value(m_default_value, *m_store); - } + CXXOPTS_NODISCARD + const OptionNames & + long_names() const + { + return m_long; + } - bool - has_default() const - { - return m_default; - } + std::size_t + hash() const + { + return m_hash; + } - bool - has_implicit() const - { - return m_implicit; - } + private: + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; - std::shared_ptr - default_value(const std::string& value) - { - m_default = true; - m_default_value = value; - return shared_from_this(); - } + std::size_t m_hash{}; +}; - std::shared_ptr - implicit_value(const std::string& value) - { - m_implicit = true; - m_implicit_value = value; - return shared_from_this(); - } +struct HelpOptionDetails +{ + std::string s; + OptionNames l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; +}; + +struct HelpGroupDetails +{ + std::string name{}; + std::string description{}; + std::vector options{}; +}; - std::string - get_default_value() const - { - return m_default_value; - } +class OptionValue +{ + public: + void + add + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->add(text); + m_long_names = &details->long_names(); + } - std::string - get_implicit_value() const - { - return m_implicit_value; - } + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_names = &details->long_names(); + } - bool - is_boolean() const - { - return std::is_same::value; - } + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_names = &details->long_names(); + m_value->parse(); + } - const T& - get() const - { - if (m_store == nullptr) - { - return *m_result; - } - else - { - return *m_store; - } - } + void + parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } - protected: - std::shared_ptr m_result; - T* m_store; +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif - bool m_default = false; - bool m_implicit = false; + CXXOPTS_NODISCARD + std::size_t + count() const noexcept + { + return m_count; + } - std::string m_default_value; - std::string m_implicit_value; - }; +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_POP +#endif - template - class standard_value : public abstract_value - { - public: - using abstract_value::abstract_value; + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } - std::shared_ptr - clone() const - { - return std::make_shared>(*this); - } - }; + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } - template <> - class standard_value : public abstract_value - { - public: - ~standard_value() = default; + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } - standard_value() - { - set_default_and_implicit(); - } + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } - standard_value(bool* b) - : abstract_value(b) - { - set_default_and_implicit(); - } - std::shared_ptr - clone() const - { - return std::make_shared>(*this); - } + const OptionNames * m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; +}; - private: +class KeyValue +{ + public: + KeyValue(std::string key_, std::string value_) noexcept + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } - void - set_default_and_implicit() - { - m_default = true; - m_default_value = "false"; - m_implicit = true; - m_implicit_value = "true"; - } - }; + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; } - template - std::shared_ptr - value() + CXXOPTS_NODISCARD + const std::string& + value() const { - return std::make_shared>(); + return m_value; } template - std::shared_ptr - value(T& t) + T + as() const { - return std::make_shared>(&t); + T result; + values::parse_value(m_value, result); + return result; } - class OptionAdder; + private: + std::string m_key; + std::string m_value; +}; + +using ParsedHashMap = std::unordered_map; +using NameHashMap = std::unordered_map; - class OptionDetails +class ParseResult +{ + public: + class Iterator { public: - OptionDetails - ( - const std::string& short_, - const std::string& long_, - const String& desc, - std::shared_ptr val - ) - : m_short(short_) - , m_long(long_) - , m_desc(desc) - , m_value(val) - , m_count(0) - { + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + +// GCC complains about m_iter not being initialised in the member +// initializer list +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult *pr, bool end=false) + : m_pr(pr) + { + if (end) + { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } + else + { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } } +CXXOPTS_DIAGNOSTIC_POP - OptionDetails(const OptionDetails& rhs) - : m_desc(rhs.m_desc) - , m_count(rhs.m_count) + Iterator& operator++() { - m_value = rhs.m_value->clone(); + ++m_iter; + if(m_sequential && m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; } - OptionDetails(OptionDetails&& rhs) = default; - - const String& - description() const + Iterator operator++(int) { - return m_desc; + Iterator retval = *this; + ++(*this); + return retval; } - const Value& value() const { - return *m_value; + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); } - std::shared_ptr - make_storage() const + bool operator!=(const Iterator& other) const { - return m_value->clone(); + return !(*this == other); } - const std::string& - short_name() const + const KeyValue& operator*() { - return m_short; + return *m_iter; } - const std::string& - long_name() const + const KeyValue* operator->() { - return m_long; + return m_iter.operator->(); } private: - std::string m_short; - std::string m_long; - String m_desc; - std::shared_ptr m_value; - int m_count; + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; }; - struct HelpOptionDetails - { - std::string s; - std::string l; - String desc; - bool has_default; - std::string default_value; - bool has_implicit; - std::string implicit_value; - std::string arg_help; - bool is_container; - bool is_boolean; - }; + ParseResult() = default; + ParseResult(const ParseResult&) = default; - struct HelpGroupDetails + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) { - std::string name; - std::string description; - std::vector options; - }; + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; - class OptionValue + Iterator + begin() const { - public: - void - parse - ( - std::shared_ptr details, - const std::string& text - ) - { - ensure_value(details); - ++m_count; - m_value->parse(text); - } + return Iterator(this); + } - void - parse_default(std::shared_ptr details) - { - ensure_value(details); - m_value->parse(); - } + Iterator + end() const + { + return Iterator(this, true); + } - size_t - count() const + std::size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) { - return m_count; + return 0; } - template - const T& - as() const - { - if (m_value == nullptr) { - throw std::domain_error("No value"); - } - -#ifdef CXXOPTS_NO_RTTI - return static_cast&>(*m_value).get(); -#else - return dynamic_cast&>(*m_value).get(); -#endif - } + auto viter = m_values.find(iter->second); - private: - void - ensure_value(std::shared_ptr details) + if (viter == m_values.end()) { - if (m_value == nullptr) - { - m_value = details->make_storage(); - } + return 0; } - std::shared_ptr m_value; - size_t m_count = 0; - }; + return viter->second.count(); + } - class KeyValue + const OptionValue& + operator[](const std::string& option) const { - public: - KeyValue(std::string key_, std::string value_) - : m_key(std::move(key_)) - , m_value(std::move(value_)) + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) { + throw_or_mimic(option); } - const - std::string& - key() const + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) { - return m_key; + throw_or_mimic(option); } - const std::string - value() const + return viter->second; + } + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for(const auto& kv: m_sequential) { - return m_value; + result += kv.key() + " = " + kv.value() + "\n"; } - - template - T - as() const + for(const auto& kv: m_defaults) { - T result; - values::parse_value(m_value, result); - return result; + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; } + return result; + } - private: - std::string m_key; - std::string m_value; - }; + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; +}; - class ParseResult +struct Option +{ + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) { - public: + } - ParseResult( - const std::shared_ptr< - std::unordered_map> - >, - std::vector, - bool allow_unrecognised, - int&, char**&); + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; +}; - size_t - count(const std::string& o) const - { - auto iter = m_options->find(o); - if (iter == m_options->end()) - { - return 0; - } +using OptionMap = std::unordered_map>; +using PositionalList = std::vector; +using PositionalListIterator = PositionalList::const_iterator; + +class OptionParser +{ + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(const std::shared_ptr& value, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: - auto riter = m_results.find(iter->second); + void finalise_aliases(); - return riter->second.count(); - } + const OptionMap& m_options; + const PositionalList& m_positional; - const OptionValue& - operator[](const std::string& option) const - { - auto iter = m_options->find(option); + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; - if (iter == m_options->end()) - { - throw option_not_present_exception(option); - } + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; +}; - auto riter = m_results.find(iter->second); +class Options +{ + public: + + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } - return riter->second; - } + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } - const std::vector& - arguments() const - { - return m_sequential; - } + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } - private: + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } - void - parse(int& argc, char**& argv); + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } - void - add_to_option(const std::string& option, const std::string& arg); + Options& + set_width(std::size_t width) + { + m_width = width; + return *this; + } - bool - consume_positional(std::string a); + Options& + set_tab_expansion(bool expansion=true) + { + m_tab_expansion = expansion; + return *this; + } - void - parse_option - ( - std::shared_ptr value, - const std::string& name, - const std::string& arg = "" - ); + ParseResult + parse(int argc, const char* const* argv); - void - parse_default(std::shared_ptr details); - - void - checked_parse_arg - ( - int argc, - char* argv[], - int& current, - std::shared_ptr value, - const std::string& name - ); + OptionAdder + add_options(std::string group = ""); - const std::shared_ptr< - std::unordered_map> - > m_options; - std::vector m_positional; - std::vector::iterator m_next_positional; - std::unordered_set m_positional_set; - std::unordered_map, OptionValue> m_results; + void + add_options + ( + const std::string& group, + std::initializer_list