From af49eb3fbb48e8a6a3e75b55d37bbb1ee2cbfa72 Mon Sep 17 00:00:00 2001 From: Jeremy Nimmer Date: Thu, 11 May 2023 13:57:02 -0700 Subject: [PATCH] [workspace] Build IPOPT from source on Ubuntu On Ubuntu, this is an upgrade from 3.11.9 to 3.14.12. On Ubuntu, we can now remove ipopt from the prereqs setup and skip it during wheel builds. It's licensed as EPL-2.0 so we can statically link it but we do need to distribute a patch file with our changes. We also teach vendor_cxx about gentler option for vendoring, where we only hide the symbols without prefixing them with "drake_vendor". On macOS, Homebrew usually tracks the newest upstream version so the urgency of rebuilding from source is reduced. Building from source would still help with linker issues, but since there is no MUMPS package in Homebrew the work to make it happen is not yet worth the time investment. --- .../binary_distribution/packages-focal.txt | 1 - .../binary_distribution/packages-jammy.txt | 1 - .../source_distribution/packages-focal.txt | 1 - .../source_distribution/packages-jammy.txt | 1 - tools/install/libdrake/BUILD.bazel | 16 +- tools/wheel/BUILD.bazel | 5 + tools/wheel/image/dependencies/projects.cmake | 18 +- .../image/dependencies/projects/ipopt.cmake | 24 +- tools/workspace/BUILD.bazel | 2 + tools/workspace/default.bzl | 9 + tools/workspace/ipopt/BUILD.bazel | 3 - tools/workspace/ipopt/repository.bzl | 33 +- .../ipopt_internal_fromsource/BUILD.bazel | 15 + .../package.BUILD.bazel | 512 ++++++++++++++++++ .../ipopt_internal_fromsource/repository.bzl | 16 + .../test/lint_test.py | 118 ++++ .../ipopt_internal_pkgconfig/BUILD.bazel | 3 + .../ipopt_internal_pkgconfig/repository.bzl | 26 + tools/workspace/mumps_internal/BUILD.bazel | 3 + .../mumps_internal/package.BUILD.bazel | 18 + tools/workspace/mumps_internal/repository.bzl | 27 + tools/workspace/vendor_cxx.bzl | 3 +- tools/workspace/vendor_cxx.py | 41 +- tools/workspace/vendor_cxx_test.py | 20 +- 24 files changed, 853 insertions(+), 63 deletions(-) create mode 100644 tools/workspace/ipopt_internal_fromsource/BUILD.bazel create mode 100644 tools/workspace/ipopt_internal_fromsource/package.BUILD.bazel create mode 100644 tools/workspace/ipopt_internal_fromsource/repository.bzl create mode 100644 tools/workspace/ipopt_internal_fromsource/test/lint_test.py create mode 100644 tools/workspace/ipopt_internal_pkgconfig/BUILD.bazel create mode 100644 tools/workspace/ipopt_internal_pkgconfig/repository.bzl create mode 100644 tools/workspace/mumps_internal/BUILD.bazel create mode 100644 tools/workspace/mumps_internal/package.BUILD.bazel create mode 100644 tools/workspace/mumps_internal/repository.bzl diff --git a/setup/ubuntu/binary_distribution/packages-focal.txt b/setup/ubuntu/binary_distribution/packages-focal.txt index bc7a0f999baf..7b97b927f824 100644 --- a/setup/ubuntu/binary_distribution/packages-focal.txt +++ b/setup/ubuntu/binary_distribution/packages-focal.txt @@ -1,6 +1,5 @@ coinor-libclp1 coinor-libcoinutils3v5 -coinor-libipopt1v5 default-jre jupyter-notebook libblas3 diff --git a/setup/ubuntu/binary_distribution/packages-jammy.txt b/setup/ubuntu/binary_distribution/packages-jammy.txt index bedd3f624b61..50dcf0ab505b 100644 --- a/setup/ubuntu/binary_distribution/packages-jammy.txt +++ b/setup/ubuntu/binary_distribution/packages-jammy.txt @@ -1,6 +1,5 @@ coinor-libclp1 coinor-libcoinutils3v5 -coinor-libipopt1v5 default-jre jupyter-notebook libblas-dev diff --git a/setup/ubuntu/source_distribution/packages-focal.txt b/setup/ubuntu/source_distribution/packages-focal.txt index af0fd3cb2606..b27c7556ac4d 100644 --- a/setup/ubuntu/source_distribution/packages-focal.txt +++ b/setup/ubuntu/source_distribution/packages-focal.txt @@ -1,7 +1,6 @@ clang-format-12 coinor-libclp-dev coinor-libcoinutils-dev -coinor-libipopt-dev default-jdk file gfortran diff --git a/setup/ubuntu/source_distribution/packages-jammy.txt b/setup/ubuntu/source_distribution/packages-jammy.txt index 6e16b6925f9c..c97034cfb770 100644 --- a/setup/ubuntu/source_distribution/packages-jammy.txt +++ b/setup/ubuntu/source_distribution/packages-jammy.txt @@ -1,7 +1,6 @@ clang-format-12 coinor-libclp-dev coinor-libcoinutils-dev -coinor-libipopt-dev default-jdk file gfortran diff --git a/tools/install/libdrake/BUILD.bazel b/tools/install/libdrake/BUILD.bazel index 57130ed3f24c..13091d7deb3a 100644 --- a/tools/install/libdrake/BUILD.bazel +++ b/tools/install/libdrake/BUILD.bazel @@ -1,5 +1,6 @@ package(default_visibility = ["//visibility:private"]) +load("@bazel_skylib//lib:selects.bzl", "selects") load( "@python//:version.bzl", "PYTHON_SITE_PACKAGES_RELPATH", @@ -137,14 +138,23 @@ cc_library( }), ) -# Depend on IPOPT's shared library iff IPOPT is enabled. +# Depend on IPOPT's shared library iff IPOPT is enabled and we're on a platform +# that uses the host OS shared library. +selects.config_setting_group( + name = "ipopt_not_shared", + match_any = [ + "//tools:no_ipopt", + "//tools/cc_toolchain:linux", + ], +) + cc_library( name = "ipopt_deps", - deps = select({ + deps = selects.with_or({ "//conditions:default": [ "@ipopt", ], - "//tools:no_ipopt": [], + ":ipopt_not_shared": [], }), ) diff --git a/tools/wheel/BUILD.bazel b/tools/wheel/BUILD.bazel index d97538a11e40..5fd3e9093ac4 100644 --- a/tools/wheel/BUILD.bazel +++ b/tools/wheel/BUILD.bazel @@ -1,6 +1,11 @@ load("@drake//tools/skylark:drake_py.bzl", "drake_py_binary") load("//tools/lint:lint.bzl", "add_lint_tests") +exports_files( + glob(["**"]), + visibility = ["//tools:__subpackages__"], +) + py_library( name = "module_py", srcs = ["__init__.py"], diff --git a/tools/wheel/image/dependencies/projects.cmake b/tools/wheel/image/dependencies/projects.cmake index 7709663b80f1..4797edbbf181 100644 --- a/tools/wheel/image/dependencies/projects.cmake +++ b/tools/wheel/image/dependencies/projects.cmake @@ -90,20 +90,22 @@ set(clp_md5 "f7c25af22d2f03398cbbdf38c8b4f6fd") set(clp_dlname "clp-${clp_version}.tar.gz") list(APPEND ALL_PROJECTS clp) +# ipopt (requires mumps) if(APPLE) set(mumps_version 5.4.1) # Latest available in Ubuntu. set(mumps_url - "http://archive.ubuntu.com/ubuntu/pool/universe/m/mumps/mumps_${mumps_version}.orig.tar.gz" + "http://archive.ubuntu.com/ubuntu/pool/universe/m/mumps/mumps_${mumps_version}.orig.tar.gz" "http://mumps.enseeiht.fr/MUMPS_${mumps_version}.tar.gz" ) set(mumps_md5 "93be789bf9c6c341a78c16038da3241b") set(mumps_dlname "mumps-${mumps_version}.tar.gz") list(APPEND ALL_PROJECTS mumps) -endif() -# ipopt -set(ipopt_version 3.11.9) -set(ipopt_url "https://github.com/coin-or/Ipopt/archive/refs/tags/releases/${ipopt_version}.tar.gz") -set(ipopt_md5 "55275c202072ad30db25d2b723ef9b7a") -set(ipopt_dlname "ipopt-${ipopt_version}.tar.gz") -list(APPEND ALL_PROJECTS ipopt) + # This must match the version in tools/workspace/ipopt_internal_fromsource. + # The matching is automatically enforced by a linter script. + set(ipopt_version 3.14.12) + set(ipopt_url "https://github.com/coin-or/Ipopt/archive/refs/tags/releases/${ipopt_version}.tar.gz") + set(ipopt_md5 "b2bcb362be4c10eccde02829d3025faa") + set(ipopt_dlname "ipopt-${ipopt_version}.tar.gz") + list(APPEND ALL_PROJECTS ipopt) +endif() diff --git a/tools/wheel/image/dependencies/projects/ipopt.cmake b/tools/wheel/image/dependencies/projects/ipopt.cmake index 154ea80426e7..b530f083a0b9 100644 --- a/tools/wheel/image/dependencies/projects/ipopt.cmake +++ b/tools/wheel/image/dependencies/projects/ipopt.cmake @@ -1,24 +1,12 @@ -if(APPLE) - set(ipopt_extra_dependencies mumps) - set(ipopt_mumps_configure_args - --with-mumps-lib=-ldmumps\ -lmumps_common\ -lpord\ -lmpiseq - --with-mumps-incdir=${CMAKE_INSTALL_PREFIX}/include - CPPFLAGS=-I${CMAKE_INSTALL_PREFIX}/include/mumps_seq - ) -else() - set(ipopt_extra_dependencies) - set(ipopt_mumps_configure_args - --with-mumps-lib=-ldmumps_seq - --with-mumps-incdir=/usr/include - CPPFLAGS=-I/usr/include/mumps_seq - ) +if(NOT APPLE) + message(FATAL_ERROR "mumps should only be built on macOS") endif() ExternalProject_Add(ipopt URL ${ipopt_url} URL_MD5 ${ipopt_md5} DOWNLOAD_NAME ${ipopt_dlname} - DEPENDS lapack ${ipopt_extra_dependencies} + DEPENDS lapack mumps ${COMMON_EP_ARGS} BUILD_IN_SOURCE 1 CONFIGURE_COMMAND ./configure @@ -28,11 +16,13 @@ ExternalProject_Add(ipopt CFLAGS=-fPIC CXXFLAGS=-fPIC LDFLAGS=-L${CMAKE_INSTALL_PREFIX}/lib - ${ipopt_mumps_configure_args} + --with-mumps-lflags=-ldmumps\ -lmpiseq\ -lmumps_common\ -lpord + --with-mumps-cflags=-I${CMAKE_INSTALL_PREFIX}/include + CPPFLAGS=-I${CMAKE_INSTALL_PREFIX}/include/mumps_seq BUILD_COMMAND make INSTALL_COMMAND make install ) extract_license(ipopt - Ipopt/LICENSE + LICENSE ) diff --git a/tools/workspace/BUILD.bazel b/tools/workspace/BUILD.bazel index fb4f4262746c..bd62ab734c8d 100644 --- a/tools/workspace/BUILD.bazel +++ b/tools/workspace/BUILD.bazel @@ -50,6 +50,7 @@ drake_py_binary( "@fcl_internal//:__pkg__", "@gz_math_internal//:__pkg__", "@gz_utils_internal//:__pkg__", + "@ipopt_internal_fromsource//:__pkg__", "@msgpack_internal//:__pkg__", "@nlopt_internal//:__pkg__", "@qhull_internal//:__pkg__", @@ -85,6 +86,7 @@ _DRAKE_EXTERNAL_PACKAGE_INSTALLS = ["@%s//:install" % p for p in [ "fmt", "gz_math_internal", "gz_utils_internal", + "ipopt", "lcm", "meshcat", "msgpack_internal", diff --git a/tools/workspace/default.bzl b/tools/workspace/default.bzl index 1892a3179cc9..9d87113a0e81 100644 --- a/tools/workspace/default.bzl +++ b/tools/workspace/default.bzl @@ -37,6 +37,8 @@ load("@drake//tools/workspace/gz_math_internal:repository.bzl", "gz_math_interna load("@drake//tools/workspace/gz_utils_internal:repository.bzl", "gz_utils_internal_repository") # noqa load("@drake//tools/workspace/intel_realsense_ros_internal:repository.bzl", "intel_realsense_ros_internal_repository") # noqa load("@drake//tools/workspace/ipopt:repository.bzl", "ipopt_repository") +load("@drake//tools/workspace/ipopt_internal_fromsource:repository.bzl", "ipopt_internal_fromsource_repository") # noqa +load("@drake//tools/workspace/ipopt_internal_pkgconfig:repository.bzl", "ipopt_internal_pkgconfig_repository") # noqa load("@drake//tools/workspace/lapack:repository.bzl", "lapack_repository") load("@drake//tools/workspace/lcm:repository.bzl", "lcm_repository") load("@drake//tools/workspace/libblas:repository.bzl", "libblas_repository") @@ -51,6 +53,7 @@ load("@drake//tools/workspace/libtiff:repository.bzl", "libtiff_repository") load("@drake//tools/workspace/meshcat:repository.bzl", "meshcat_repository") load("@drake//tools/workspace/mosek:repository.bzl", "mosek_repository") load("@drake//tools/workspace/msgpack_internal:repository.bzl", "msgpack_internal_repository") # noqa +load("@drake//tools/workspace/mumps_internal:repository.bzl", "mumps_internal_repository") # noqa load("@drake//tools/workspace/mypy_extensions_internal:repository.bzl", "mypy_extensions_internal_repository") # noqa load("@drake//tools/workspace/mypy_internal:repository.bzl", "mypy_internal_repository") # noqa load("@drake//tools/workspace/nanoflann_internal:repository.bzl", "nanoflann_internal_repository") # noqa @@ -182,6 +185,10 @@ def add_default_repositories(excludes = [], mirrors = DEFAULT_MIRRORS): intel_realsense_ros_internal_repository(name = "intel_realsense_ros_internal", mirrors = mirrors) # noqa if "ipopt" not in excludes: ipopt_repository(name = "ipopt") + if "ipopt_internal_fromsource" not in excludes: + ipopt_internal_fromsource_repository(name = "ipopt_internal_fromsource", mirrors = mirrors) # noqa + if "ipopt_internal_pkgconfig" not in excludes: + ipopt_internal_pkgconfig_repository(name = "ipopt_internal_pkgconfig") if "lapack" not in excludes: lapack_repository(name = "lapack") if "lcm" not in excludes: @@ -210,6 +217,8 @@ def add_default_repositories(excludes = [], mirrors = DEFAULT_MIRRORS): mosek_repository(name = "mosek") if "msgpack_internal" not in excludes: msgpack_internal_repository(name = "msgpack_internal", mirrors = mirrors) # noqa + if "mumps_internal" not in excludes: + mumps_internal_repository(name = "mumps_internal") if "mypy_extensions_internal" not in excludes: mypy_extensions_internal_repository(name = "mypy_extensions_internal", mirrors = mirrors) # noqa if "mypy_internal" not in excludes: diff --git a/tools/workspace/ipopt/BUILD.bazel b/tools/workspace/ipopt/BUILD.bazel index b77b93ae0dbc..67914ea7e0a0 100644 --- a/tools/workspace/ipopt/BUILD.bazel +++ b/tools/workspace/ipopt/BUILD.bazel @@ -1,6 +1,3 @@ -# This file exists to make our directory into a Bazel package, so that our -# neighboring *.bzl file can be loaded elsewhere. - load("//tools/lint:lint.bzl", "add_lint_tests") add_lint_tests() diff --git a/tools/workspace/ipopt/repository.bzl b/tools/workspace/ipopt/repository.bzl index e0fc5a311a7f..8e5671caa829 100644 --- a/tools/workspace/ipopt/repository.bzl +++ b/tools/workspace/ipopt/repository.bzl @@ -1,22 +1,17 @@ -load( - "@drake//tools/workspace:pkg_config.bzl", - "pkg_config_repository", -) +load("@drake//tools/workspace:os.bzl", "os_specific_alias_repository") -def ipopt_repository( - name, - licenses = [ - "reciprocal", # CPL-1.0 - "unencumbered", # Public-Domain - ], - modname = "ipopt", - pkg_config_paths = [], - homebrew_subdir = "opt/ipopt/lib/pkgconfig", - **kwargs): - pkg_config_repository( +# How we build IPOPT depends on which platform we're on. +def ipopt_repository(name): + os_specific_alias_repository( name = name, - licenses = licenses, - modname = modname, - pkg_config_paths = pkg_config_paths, - **kwargs + mapping = { + "macOS default": [ + "ipopt=@ipopt_internal_pkgconfig//:ipopt_internal_pkgconfig", + "install=@ipopt_internal_pkgconfig//:install", + ], + "Ubuntu default": [ + "ipopt=@ipopt_internal_fromsource//:ipopt", + "install=@ipopt_internal_fromsource//:install", + ], + }, ) diff --git a/tools/workspace/ipopt_internal_fromsource/BUILD.bazel b/tools/workspace/ipopt_internal_fromsource/BUILD.bazel new file mode 100644 index 000000000000..0ee18c093828 --- /dev/null +++ b/tools/workspace/ipopt_internal_fromsource/BUILD.bazel @@ -0,0 +1,15 @@ +load("@drake//tools/skylark:drake_py.bzl", "drake_py_unittest") +load("//tools/lint:lint.bzl", "add_lint_tests") + +drake_py_unittest( + name = "lint_test", + data = [ + ":package.BUILD.bazel", + "//tools/wheel:image/dependencies/projects.cmake", + "@ipopt_internal_fromsource//:drake_repository_metadata.json", + "@ipopt_internal_fromsource//:src/Makefile.am", + ], + tags = ["lint"], +) + +add_lint_tests() diff --git a/tools/workspace/ipopt_internal_fromsource/package.BUILD.bazel b/tools/workspace/ipopt_internal_fromsource/package.BUILD.bazel new file mode 100644 index 000000000000..bb20ee18c84e --- /dev/null +++ b/tools/workspace/ipopt_internal_fromsource/package.BUILD.bazel @@ -0,0 +1,512 @@ +# -*- bazel -*- + +load("@bazel_skylib//lib:paths.bzl", "paths") +load( + "@drake//tools/install:install.bzl", + "install", +) +load( + "@drake//tools/skylark:drake_cc.bzl", + "cc_linkonly_library", +) +load( + "@drake//tools/workspace:cmake_configure_file.bzl", + "cmake_configure_file", +) +load( + "@drake//tools/workspace:vendor_cxx.bzl", + "cc_library_vendored", +) + +licenses(["reciprocal"]) # EPL-2.0 + +package(default_visibility = ["//visibility:private"]) + +exports_files([ + # Always provide access to license texts. + "LICENSE", + # This is used by our lint_test. + "src/Makefile.am", +]) + +# The list of headers to expose to the user (i.e., Drake). +# +# This is exactly the includeipopt_HEADERS from upstream, as enforced by +# drake/tools/workspace/ipopt_internal/test/lint_test.py. +_HDRS_PUBLIC = [ + "src/Common/IpCachedResults.hpp", + "src/Common/IpDebug.hpp", + "src/Common/IpException.hpp", + "src/Common/IpJournalist.hpp", + "src/Common/IpLibraryLoader.hpp", + "src/Common/IpObserver.hpp", + "src/Common/IpOptionsList.hpp", + "src/Common/IpReferenced.hpp", + "src/Common/IpRegOptions.hpp", + "src/Common/IpSmartPtr.hpp", + "src/Common/IpTaggedObject.hpp", + "src/Common/IpTimedTask.hpp", + "src/Common/IpTypes.hpp", + "src/Common/IpTypes.h", + "src/Common/IpUtils.hpp", + "src/LinAlg/IpBlas.hpp", + "src/LinAlg/IpCompoundMatrix.hpp", + "src/LinAlg/IpCompoundSymMatrix.hpp", + "src/LinAlg/IpCompoundVector.hpp", + "src/LinAlg/IpDenseVector.hpp", + "src/LinAlg/IpDiagMatrix.hpp", + "src/LinAlg/IpExpansionMatrix.hpp", + "src/LinAlg/IpIdentityMatrix.hpp", + "src/LinAlg/IpLapack.hpp", + "src/LinAlg/IpMatrix.hpp", + "src/LinAlg/IpScaledMatrix.hpp", + "src/LinAlg/IpSumSymMatrix.hpp", + "src/LinAlg/IpSymMatrix.hpp", + "src/LinAlg/IpSymScaledMatrix.hpp", + "src/LinAlg/IpVector.hpp", + "src/LinAlg/IpZeroSymMatrix.hpp", + "src/LinAlg/TMatrices/IpGenTMatrix.hpp", + "src/LinAlg/TMatrices/IpSymTMatrix.hpp", + "src/LinAlg/TMatrices/IpTripletHelper.hpp", + "src/Algorithm/IpAlgBuilder.hpp", + "src/Algorithm/IpAlgStrategy.hpp", + "src/Algorithm/IpAugSystemSolver.hpp", + "src/Algorithm/IpConvCheck.hpp", + "src/Algorithm/IpEqMultCalculator.hpp", + "src/Algorithm/IpHessianUpdater.hpp", + "src/Algorithm/IpIpoptAlg.hpp", + "src/Algorithm/IpIpoptCalculatedQuantities.hpp", + "src/Algorithm/IpIpoptData.hpp", + "src/Algorithm/IpIpoptNLP.hpp", + "src/Algorithm/IpIterateInitializer.hpp", + "src/Algorithm/IpIteratesVector.hpp", + "src/Algorithm/IpIterationOutput.hpp", + "src/Algorithm/IpOrigIpoptNLP.hpp", + "src/Algorithm/IpLineSearch.hpp", + "src/Algorithm/IpMuUpdate.hpp", + "src/Algorithm/IpNLPScaling.hpp", + "src/Algorithm/IpPDSystemSolver.hpp", + "src/Algorithm/IpSearchDirCalculator.hpp", + "src/Algorithm/IpStdAugSystemSolver.hpp", + "src/Algorithm/IpTimingStatistics.hpp", + "src/Algorithm/LinearSolvers/IpLinearSolvers.h", + "src/Algorithm/LinearSolvers/IpSlackBasedTSymScalingMethod.hpp", + "src/Algorithm/LinearSolvers/IpSparseSymLinearSolverInterface.hpp", + "src/Algorithm/LinearSolvers/IpSymLinearSolver.hpp", + "src/Algorithm/LinearSolvers/IpTripletToCSRConverter.hpp", + "src/Algorithm/LinearSolvers/IpTSymLinearSolver.hpp", + "src/Algorithm/LinearSolvers/IpTSymScalingMethod.hpp", + "src/Interfaces/IpAlgTypes.hpp", + "src/Interfaces/IpIpoptApplication.hpp", + "src/Interfaces/IpNLP.hpp", + "src/Interfaces/IpReturnCodes.h", + "src/Interfaces/IpReturnCodes.hpp", + "src/Interfaces/IpReturnCodes_inc.h", + "src/Interfaces/IpReturnCodes.inc", + "src/Interfaces/IpSolveStatistics.hpp", + "src/Interfaces/IpStdCInterface.h", + "src/Interfaces/IpTNLP.hpp", + "src/Interfaces/IpTNLPAdapter.hpp", + "src/Interfaces/IpTNLPReducer.hpp", +] + +# The include paths for _HDRS_PUBLIC. +_INCLUDES_PUBLIC = depset([paths.dirname(x) for x in _HDRS_PUBLIC]).to_list() + +# The baseline list of sources to compile. +# +# This is exactly the initial libipopt_la_SOURCES from upstream but without the +# problematic IpStdCInterface.cpp and IpStdFInterface.c files which we don't +# use and cannot easily be marked as hidden. The correct value for this list is +# enforced by drake/tools/workspace/ipopt_internal/test/lint_test.py. +# +# The Makefile.am conditionally adds more sources depending on other +# configuration options. Likewise, we'll add some more sources later on. +_SRCS_INITIAL = [ + "src/Common/IpDebug.cpp", + "src/Common/IpJournalist.cpp", + "src/Common/IpObserver.cpp", + "src/Common/IpOptionsList.cpp", + "src/Common/IpRegOptions.cpp", + "src/Common/IpTaggedObject.cpp", + "src/Common/IpUtils.cpp", + "src/Common/IpLibraryLoader.cpp", + "src/LinAlg/IpBlas.cpp", + "src/LinAlg/IpCompoundMatrix.cpp", + "src/LinAlg/IpCompoundSymMatrix.cpp", + "src/LinAlg/IpCompoundVector.cpp", + "src/LinAlg/IpDenseGenMatrix.cpp", + "src/LinAlg/IpDenseSymMatrix.cpp", + "src/LinAlg/IpDenseVector.cpp", + "src/LinAlg/IpDiagMatrix.cpp", + "src/LinAlg/IpExpandedMultiVectorMatrix.cpp", + "src/LinAlg/IpExpansionMatrix.cpp", + "src/LinAlg/IpIdentityMatrix.cpp", + "src/LinAlg/IpLapack.cpp", + "src/LinAlg/IpLowRankUpdateSymMatrix.cpp", + "src/LinAlg/IpMatrix.cpp", + "src/LinAlg/IpMultiVectorMatrix.cpp", + "src/LinAlg/IpScaledMatrix.cpp", + "src/LinAlg/IpSumMatrix.cpp", + "src/LinAlg/IpSumSymMatrix.cpp", + "src/LinAlg/IpSymScaledMatrix.cpp", + "src/LinAlg/IpTransposeMatrix.cpp", + "src/LinAlg/IpVector.cpp", + "src/LinAlg/IpZeroMatrix.cpp", + "src/LinAlg/IpZeroSymMatrix.cpp", + "src/LinAlg/TMatrices/IpGenTMatrix.cpp", + "src/LinAlg/TMatrices/IpSymTMatrix.cpp", + "src/LinAlg/TMatrices/IpTripletHelper.cpp", + "src/Algorithm/IpAdaptiveMuUpdate.cpp", + "src/Algorithm/IpAlgBuilder.cpp", + "src/Algorithm/IpAlgorithmRegOp.cpp", + "src/Algorithm/IpAugRestoSystemSolver.cpp", + "src/Algorithm/IpBacktrackingLineSearch.cpp", + "src/Algorithm/IpDefaultIterateInitializer.cpp", + "src/Algorithm/IpEquilibrationScaling.cpp", + "src/Algorithm/IpExactHessianUpdater.cpp", + "src/Algorithm/IpFilter.cpp", + "src/Algorithm/IpFilterLSAcceptor.cpp", + "src/Algorithm/IpGenAugSystemSolver.cpp", + "src/Algorithm/IpGradientScaling.cpp", + "src/Algorithm/IpIpoptAlg.cpp", + "src/Algorithm/IpIpoptCalculatedQuantities.cpp", + "src/Algorithm/IpIpoptData.cpp", + "src/Algorithm/IpIteratesVector.cpp", + "src/Algorithm/IpLeastSquareMults.cpp", + "src/Algorithm/IpLimMemQuasiNewtonUpdater.cpp", + "src/Algorithm/IpLoqoMuOracle.cpp", + "src/Algorithm/IpLowRankAugSystemSolver.cpp", + "src/Algorithm/IpLowRankSSAugSystemSolver.cpp", + "src/Algorithm/IpMonotoneMuUpdate.cpp", + "src/Algorithm/IpNLPBoundsRemover.cpp", + "src/Algorithm/IpNLPScaling.cpp", + "src/Algorithm/IpOptErrorConvCheck.cpp", + "src/Algorithm/IpOrigIpoptNLP.cpp", + "src/Algorithm/IpOrigIterationOutput.cpp", + "src/Algorithm/IpPDFullSpaceSolver.cpp", + "src/Algorithm/IpPDPerturbationHandler.cpp", + "src/Algorithm/IpPDSearchDirCalc.cpp", + "src/Algorithm/IpPenaltyLSAcceptor.cpp", + "src/Algorithm/IpProbingMuOracle.cpp", + "src/Algorithm/IpQualityFunctionMuOracle.cpp", + "src/Algorithm/IpRestoConvCheck.cpp", + "src/Algorithm/IpRestoFilterConvCheck.cpp", + "src/Algorithm/IpRestoIpoptNLP.cpp", + "src/Algorithm/IpRestoIterateInitializer.cpp", + "src/Algorithm/IpRestoIterationOutput.cpp", + "src/Algorithm/IpRestoMinC_1Nrm.cpp", + "src/Algorithm/IpRestoPenaltyConvCheck.cpp", + "src/Algorithm/IpRestoRestoPhase.cpp", + "src/Algorithm/IpStdAugSystemSolver.cpp", + "src/Algorithm/IpTimingStatistics.cpp", + "src/Algorithm/IpUserScaling.cpp", + "src/Algorithm/IpWarmStartIterateInitializer.cpp", + "src/Algorithm/LinearSolvers/IpLinearSolversRegOp.cpp", + "src/Algorithm/LinearSolvers/IpLinearSolvers.c", + "src/Algorithm/LinearSolvers/IpSlackBasedTSymScalingMethod.cpp", + "src/Algorithm/LinearSolvers/IpTripletToCSRConverter.cpp", + "src/Algorithm/LinearSolvers/IpTSymDependencyDetector.cpp", + "src/Algorithm/LinearSolvers/IpTSymLinearSolver.cpp", + "src/contrib/CGPenalty/IpCGPenaltyCq.cpp", + "src/contrib/CGPenalty/IpCGPenaltyData.cpp", + "src/contrib/CGPenalty/IpCGPenaltyLSAcceptor.cpp", + "src/contrib/CGPenalty/IpCGPenaltyRegOp.cpp", + "src/contrib/CGPenalty/IpCGPerturbationHandler.cpp", + "src/contrib/CGPenalty/IpCGSearchDirCalc.cpp", + "src/contrib/CGPenalty/IpPiecewisePenalty.cpp", + "src/Interfaces/IpInterfacesRegOp.cpp", + "src/Interfaces/IpIpoptApplication.cpp", + "src/Interfaces/IpSolveStatistics.cpp", + "src/Interfaces/IpStdInterfaceTNLP.cpp", + "src/Interfaces/IpTNLP.cpp", + "src/Interfaces/IpTNLPAdapter.cpp", + "src/Interfaces/IpTNLPReducer.cpp", +] + +# In addition to _SRCS_INITIAL, we also add extra sources for certain solvers. +# +# Mumps is our preferred linear solver. +_SRCS_SOLVER_MUMPS = [ + "src/Algorithm/LinearSolvers/IpMumpsSolverInterface.cpp", +] + +# In addition to _SRCS_INITIAL, we also add extra sources for certain solvers. +# These (unused) solvers are still required at link-time. +# +# This list is cross-checked vs the Makefile.am contents via +# drake/tools/workspace/ipopt_internal/test/lint_test.py. +_SRCS_SOLVER_INT32 = [ + "src/Algorithm/LinearSolvers/IpMc19TSymScalingMethod.cpp", + "src/Algorithm/LinearSolvers/IpMa27TSolverInterface.cpp", + "src/Algorithm/LinearSolvers/IpMa57TSolverInterface.cpp", + "src/Algorithm/LinearSolvers/IpMa77SolverInterface.cpp", + "src/Algorithm/LinearSolvers/IpMa86SolverInterface.cpp", + "src/Algorithm/LinearSolvers/IpMa97SolverInterface.cpp", + "src/Algorithm/LinearSolvers/IpPardisoSolverInterface.cpp", +] + +# All of the sources together (baseline + extra). +_SRCS = _SRCS_INITIAL + _SRCS_SOLVER_MUMPS + _SRCS_SOLVER_INT32 + +# When compiling the sources, we'll provide access to _HDRS_PRIVATE, which is +# all of the headers that live in the same directories as the sources. These +# are the include paths for _HDRS_PRIVATE. +_INCLUDES_PRIVATE = depset([paths.dirname(x) for x in _SRCS]).to_list() + +# All of the headers that live in the same directories as the sources (except +# for the config headers which we need to handle separately, below). +_HDRS_PRIVATE = glob([ + x + "/*.h*" + for x in _INCLUDES_PRIVATE +], exclude = [ + "src/Common/config*", + "src/Common/IpoptConfig.h", +], allow_empty = False) + +# The next three rules are for the private flavor of IpoptConfig.h. +# +# The upstream IpoptConfig.h is a tricksy little beast. When compiling the +# library source code, it refers to the configure-generated header. When +# installing into include paths for the user, it uses something different +# (a narrower header with just the IPOPT version numbers & etc). +# +# Here we'll generate the private header with the configuration we want, +# by respelling it from autoconf to cmake and then setting our definitions. +# +# We can use cc_library (not cc_library_vendored) to declare the header +# because it only has preprocessor definitions (no C++ object code). +genrule( + name = "_respell_autoconf_to_cmakeconfig", + srcs = ["src/Common/config.h.in"], + outs = ["src/Common/config.h.in.cmake"], + cmd = "sed -e 's|#undef \\(.*\\)|#cmakedefine \\1 @\\1@|' $< > $@", +) + +cmake_configure_file( + name = "_configure", + src = ":src/Common/config.h.in.cmake", + out = "hdr_private/IpoptConfig.h", + defines = [ + "IPOPT_VERSION=\"drake_vendor\"", + # Features of the standard library and/or host system. + "HAVE_CFLOAT=1", + "HAVE_CIEEEFP=1", + "HAVE_CMATH=1", + "HAVE_FLOAT_H=1", + "HAVE_IEEEFP_H=1", + "HAVE_INTTYPES_H=1", + "HAVE_MATH_H=1", + "HAVE_STDINT_H=1", + "HAVE_STDIO_H=1", + "HAVE_STDLIB_H=1", + "HAVE_STRINGS_H=1", + "HAVE_STRING_H=1", + "HAVE_SYS_STAT_H=1", + "HAVE_SYS_TYPES_H=1", + "HAVE_UNISTD_H=1", + "HAVE_VSNPRINTF=1", + "IPOPT_C_FINITE=std::isfinite", + "IPOPT_HAS_RAND=1", + "IPOPT_HAS_STD__RAND=1", + "IPOPT_HAS_VA_COPY=1", + "STDC_HEADERS=1", + # Optional dependencies that we do actually want to use. + "IPOPT_HAS_LAPACK=1", + "IPOPT_HAS_MUMPS=1", + # No debug self-checks (the default). + "IPOPT_CHECKLEVEL=0", + "IPOPT_VERBOSITY=0", + # These are no-ops, but they can't be omitted. + "HSLLIB_EXPORT=", + "IPOPTAMPLINTERFACELIB_EXPORT=", + "IPOPTLIB_EXPORT=__attribute__ ((visibility (\"hidden\")))", + "SIPOPTAMPLINTERFACELIB_EXPORT=", + "SIPOPTLIB_EXPORT=", + ], + undefines = [ + # Don't use these features of the standard library and/or host system. + "CXX_NO_MINUS_C_MINUS_O", + "F77_DUMMY_MAIN", + "F77_FUNC", + "F77_FUNC_", + "F77_NO_MINUS_C_MINUS_O", + "FC_DUMMY_MAIN_EQ_F77", + "HAVE_DLFCN_H", + "HAVE__VSNPRINTF", + "IPOPT_HAS_DRAND48", + "IPOPT_HAS_FEENABLEEXCEPT", + "IPOPT_INT64", + "IPOPT_SINGLE", + # Optional dependencies that we don't use. + "BUILD_INEXACT", + "HAVE_MPI_INITIALIZED", + "HAVE_WINDOWS_H", + "IPOPT_HAS_ASL", + "IPOPT_HAS_HSL", + "IPOPT_HAS_LINEARSOLVERLOADER", + "IPOPT_HAS_PARDISO_MKL", + "IPOPT_HAS_SPRAL", + "IPOPT_HAS_WSMP", + "IPOPT_HSL_FUNC", + "IPOPT_HSL_FUNC_", + "IPOPT_MPIINIT", + "IPOPT_WSMP_FUNC", + "IPOPT_WSMP_FUNC_", + "PARDISO_LIB", + # Chaff that's not used by the C++ code anyway. + "IPOPT_LAPACK_FUNC_", + "IPOPT_VERSION_MAJOR", + "IPOPT_VERSION_MINOR", + "IPOPT_VERSION_RELEASE", + "LT_OBJDIR", + "PACKAGE_BUGREPORT", + "PACKAGE_NAME", + "PACKAGE_STRING", + "PACKAGE_TARNAME", + "PACKAGE_URL", + "PACKAGE_VERSION", + "SIZEOF_INT_P", + # This is actually used by the C++ code, but cmake_configure_file can't + # handle it. We'll use `defines = []` for this instead (see below). + "IPOPT_LAPACK_FUNC", + ], + strict = True, +) + +cc_library( + name = "_ipopt_config_private", + hdrs = [":hdr_private/IpoptConfig.h"], + strip_include_prefix = "hdr_private", + defines = [ + "IPOPT_LAPACK_FUNC(name,NAME)=name##_", + ], + linkstatic = True, +) + +# Compile all of the object code for the library. +cc_library_vendored( + name = "_build", + srcs = _SRCS, + srcs_vendored = [ + x.replace("src/", "drake_src/") + for x in _SRCS + ], + hdrs = _HDRS_PRIVATE, + hdrs_vendored = [ + x.replace("src/", "drake_src/") + for x in _HDRS_PRIVATE + ], + includes = [ + x.replace("src/", "drake_src/") + for x in _INCLUDES_PRIVATE + ], + vendor_tool_args = [ + "--no-inline-namespace", + ], + linkstatic = True, + deps = [ + ":_ipopt_config_private", + "@blas", + "@lapack", + "@mumps_internal//:dmumps_seq", + ], +) + +# Discard the headers, leaving only the object code. +cc_linkonly_library( + name = "_objs", + deps = [":_build"], +) + +# The next two rules are for the public flavor of IpoptConfig.h. +# +# We can use cc_library (not cc_library_vendored) to declare the header +# because it only has preprocessor definitions (no C++ object code). +genrule( + name = "_ipopt_config_public_genrule", + srcs = ["src/Common/config_ipopt_default.h"], + outs = ["hdr_public/IpoptConfig.h"], + cmd = "cp $< $@", +) + +cc_library( + name = "_ipopt_config_public", + hdrs = [":hdr_public/IpoptConfig.h"], + strip_include_prefix = "hdr_public", + linkstatic = True, +) + +# Assemble the public headers + object code into the library Drake will use. +cc_library_vendored( + name = "ipopt", + hdrs = _HDRS_PUBLIC, + hdrs_vendored = [ + x.replace("src/", "drake_hdr/") + for x in _HDRS_PUBLIC + ], + includes = [ + x.replace("src/", "drake_hdr/") + for x in _INCLUDES_PUBLIC + ], + vendor_tool_args = [ + "--no-inline-namespace", + ], + linkstatic = True, + deps = [ + ":_ipopt_config_public", + ":_objs", + ], + visibility = ["//visibility:public"], +) + +# The next three stanzas create a patch file of our diffs to install alongside +# our binaries (which is required by Ipopt's EPL-2.0 license). +_DIFF_INPUTS = [ + ("src/Common/config.h.in", "hdr_private/IpoptConfig.h"), +] + zip( + _SRCS + _HDRS_PRIVATE, + [ + x.replace("src/", "drake_src/") + for x in _SRCS + _HDRS_PRIVATE + ], +) + +[ + genrule( + name = "genrule_{}_patch".format(vendor_src), + srcs = [upstream_src, vendor_src], + outs = [vendor_src + ".patch"], + cmd = " ".join([ + "(diff -u0", + "--label={upstream_src} $(execpath {upstream_src})", + "--label={vendor_src} $(execpath {vendor_src})", + "> $@ || [[ $$? == 1 ]])", + ]).format( + upstream_src = upstream_src, + vendor_src = vendor_src, + ), + ) + for upstream_src, vendor_src in _DIFF_INPUTS +] + +genrule( + name = "genrule_full_patch", + srcs = [ + vendor_src + ".patch" + for (_, vendor_src) in _DIFF_INPUTS + ], + outs = ["drake_ipopt.patch"], + cmd = "cat $(SRCS) > $@", +) + +install( + name = "install", + docs = [ + "LICENSE", + ":drake_ipopt.patch", + ], + visibility = ["//visibility:public"], +) + +exports_files(["drake_repository_metadata.json"]) diff --git a/tools/workspace/ipopt_internal_fromsource/repository.bzl b/tools/workspace/ipopt_internal_fromsource/repository.bzl new file mode 100644 index 000000000000..6ac016362530 --- /dev/null +++ b/tools/workspace/ipopt_internal_fromsource/repository.bzl @@ -0,0 +1,16 @@ +load( + "@drake//tools/workspace:github.bzl", + "github_archive", +) + +def ipopt_internal_fromsource_repository( + name, + mirrors = None): + github_archive( + name = name, + repository = "coin-or/Ipopt", + commit = "releases/3.14.12", + sha256 = "6b06cd6280d5ca52fc97ca95adaaddd43529e6e8637c274e21ee1072c3b4577f", # noqa + build_file = ":package.BUILD.bazel", + mirrors = mirrors, + ) diff --git a/tools/workspace/ipopt_internal_fromsource/test/lint_test.py b/tools/workspace/ipopt_internal_fromsource/test/lint_test.py new file mode 100644 index 000000000000..f76d9e6dbd96 --- /dev/null +++ b/tools/workspace/ipopt_internal_fromsource/test/lint_test.py @@ -0,0 +1,118 @@ +import json +import unittest + + +class IpoptLintTest(unittest.TestCase): + + def _read(self, filename): + """Returns the contents of the given filename.""" + with open(filename, encoding="utf-8") as f: + return f.read() + + def _parse_build(self, varname): + """Parses a constant list of filenames from a BUILD file. + The only supported format is like this: + + {varname} = [ + "path/to/file1", + "path/to/file2", + ] + """ + result = [] + contents = self._read( + "tools/workspace/ipopt_internal_fromsource/package.BUILD.bazel") + lines = contents.splitlines() + start_line = f"{varname} = [" + end_line = "]" + start_index = lines.index(start_line) + end_index = lines.index(end_line, start_index + 1) + for i in range(start_index + 1, end_index): + line = lines[i] + prefix = ' "' + suffix = '",' + self.assertTrue(line.startswith(prefix), line) + self.assertTrue(line.endswith(suffix), line) + result.append(line[len(prefix):-len(suffix)]) + return set(result) + + def _parse_make(self, varname, *, guard_line=None): + """Parses a constant list of filenames from Makefile.am. + + The only supported formats are like this (with no `guard_line`): + + {varname} = \ + path/to/file1 \ + path/to/file2 + + Or this (with a `guard_line`): + + {guard_line} + {varname} += \ + path/to/file1 \ + path/to/file2 + """ + result = [] + contents = self._read( + "external/ipopt_internal_fromsource/src/Makefile.am") + lines = contents.splitlines() + if guard_line is None: + start_line = f"{varname} = \\" + index = lines.index(start_line) + 1 + else: + index = lines.index(guard_line) + 1 + self.assertEqual(lines[index], f"{varname} += \\") + index += 1 + while True: + line = lines[index] + has_slash = line.endswith("\\") + if has_slash: + line = line[:-1] + result.append("src/" + line.strip()) + index += 1 + if not has_slash: + break + return set(result) + + def test_hdrs_public(self): + """Checks that _HDRS_PUBLIC matches includeipopt_HEADERS.""" + self.assertSetEqual(self._parse_build("_HDRS_PUBLIC"), + self._parse_make("includeipopt_HEADERS")) + + def test_srcs_initial(self): + """Checks that _SRCS_INITIAL matches libipopt_la_SOURCES, except for + two specific unwanted sources. + """ + make_sources = self._parse_make("libipopt_la_SOURCES") + make_sources.remove("src/Interfaces/IpStdCInterface.cpp") + make_sources.remove("src/Interfaces/IpStdFInterface.c") + self.assertSetEqual(self._parse_build("_SRCS_INITIAL"), + make_sources) + + def test_srcs_solver_int32(self): + """Checks that _SRCS_SOLVER_INT32 matches !IPOPT_INT64's effect.""" + self.assertSetEqual(self._parse_build("_SRCS_SOLVER_INT32"), + self._parse_make("libipopt_la_SOURCES", + guard_line="if !IPOPT_INT64")) + + def test_wheel_verison_pin(self): + """Checks that the repository rule and wheel agree on which version of + IPOPT we should be using. + """ + # Parse the Bazel version. + commit = json.loads(self._read( + "external/ipopt_internal_fromsource/" + "drake_repository_metadata.json"))["commit"] + prefix = "releases/" + self.assertTrue(commit.startswith(prefix), commit) + bazel_version = commit[len(prefix):] + + # Parse the Wheel version from the line `set(ipopt_version #.#.#)`. + projects = self._read( + "tools/wheel/image/dependencies/projects.cmake") + prefix = "set(ipopt_version " + start = projects.index(prefix) + len(prefix) + end = projects.index(")", start) + wheel_version = projects[start:end] + + # Exact string match. + self.assertEqual(wheel_version, bazel_version) diff --git a/tools/workspace/ipopt_internal_pkgconfig/BUILD.bazel b/tools/workspace/ipopt_internal_pkgconfig/BUILD.bazel new file mode 100644 index 000000000000..67914ea7e0a0 --- /dev/null +++ b/tools/workspace/ipopt_internal_pkgconfig/BUILD.bazel @@ -0,0 +1,3 @@ +load("//tools/lint:lint.bzl", "add_lint_tests") + +add_lint_tests() diff --git a/tools/workspace/ipopt_internal_pkgconfig/repository.bzl b/tools/workspace/ipopt_internal_pkgconfig/repository.bzl new file mode 100644 index 000000000000..aa514a269d64 --- /dev/null +++ b/tools/workspace/ipopt_internal_pkgconfig/repository.bzl @@ -0,0 +1,26 @@ +load( + "@drake//tools/workspace:pkg_config.bzl", + "pkg_config_repository", +) + +def ipopt_internal_pkgconfig_repository( + name, + licenses = [ + "reciprocal", # CPL-1.0 + ], + modname = "ipopt", + pkg_config_paths = [], + homebrew_subdir = "opt/ipopt/lib/pkgconfig", + **kwargs): + pkg_config_repository( + name = name, + licenses = licenses, + modname = modname, + pkg_config_paths = pkg_config_paths, + # When using ipopt from pkg-config, there is nothing to install. + build_epilog = """ +load("@drake//tools/install:install.bzl", "install") +install(name = "install") +""", + **kwargs + ) diff --git a/tools/workspace/mumps_internal/BUILD.bazel b/tools/workspace/mumps_internal/BUILD.bazel new file mode 100644 index 000000000000..67914ea7e0a0 --- /dev/null +++ b/tools/workspace/mumps_internal/BUILD.bazel @@ -0,0 +1,3 @@ +load("//tools/lint:lint.bzl", "add_lint_tests") + +add_lint_tests() diff --git a/tools/workspace/mumps_internal/package.BUILD.bazel b/tools/workspace/mumps_internal/package.BUILD.bazel new file mode 100644 index 000000000000..8724737d2b79 --- /dev/null +++ b/tools/workspace/mumps_internal/package.BUILD.bazel @@ -0,0 +1,18 @@ +# -*- bazel -*- + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "dmumps_seq", + hdrs = glob([ + "include/**", + ], allow_empty = False), + includes = [ + "include", + "include/mumps_seq", + ], + linkopts = [ + "-ldmumps_seq", + ], + licenses = ["reciprocal"], # CECILL-C + BSD-3-Clause + Public Domain +) diff --git a/tools/workspace/mumps_internal/repository.bzl b/tools/workspace/mumps_internal/repository.bzl new file mode 100644 index 000000000000..27a4b6ee1a7b --- /dev/null +++ b/tools/workspace/mumps_internal/repository.bzl @@ -0,0 +1,27 @@ +def _impl(repo_ctx): + # Symlink the relevant headers. + hdrs = [ + "dmumps_c.h", + "mumps_c_types.h", + "mumps_compat.h", + "mumps_int_def.h", + "mumps_seq/elapse.h", + "mumps_seq/mpi.h", + "mumps_seq/mpif.h", + ] + for hdr in hdrs: + repo_ctx.symlink("/usr/include/" + hdr, "include/" + hdr) + + # Add the BUILD file. + repo_ctx.symlink( + Label("@drake//tools/workspace/mumps_internal:package.BUILD.bazel"), + "BUILD.bazel", + ) + +mumps_internal_repository = repository_rule( + doc = """Adds a repository rule for the host mumps library from Ubuntu. + This repository is not used on macOS. + """, + local = True, + implementation = _impl, +) diff --git a/tools/workspace/vendor_cxx.bzl b/tools/workspace/vendor_cxx.bzl index 49c251fbc738..1a3296836651 100644 --- a/tools/workspace/vendor_cxx.bzl +++ b/tools/workspace/vendor_cxx.bzl @@ -5,6 +5,7 @@ def cc_library_vendored( edit_include = None, srcs = None, srcs_vendored = None, + vendor_tool_args = None, **kwargs): """ Compiles a third-party C++ library using altered include paths and @@ -46,7 +47,7 @@ def cc_library_vendored( outs = hdrs_vendored + srcs_vendored, cmd = " ".join([ "$(execpath @drake//tools/workspace:vendor_cxx)", - ] + [ + ] + (vendor_tool_args or []) + [ "'--edit-include={}:{}'".format(k, v) for k, v in edit_include.items() ] + [ diff --git a/tools/workspace/vendor_cxx.py b/tools/workspace/vendor_cxx.py index 699a05928d56..991a542d6b19 100644 --- a/tools/workspace/vendor_cxx.py +++ b/tools/workspace/vendor_cxx.py @@ -131,7 +131,7 @@ class Flag(Enum): return [x == Flag.WRAP for x in flags] -def _rewrite_one_text(*, text, edit_include): +def _rewrite_one_text(*, text, edit_include, inline_namespace): """Rewrites the C++ file contents in `text` with specific alterations: - The paths in #include statements are replaced per the (old, new) pairs in @@ -141,6 +141,13 @@ def _rewrite_one_text(*, text, edit_include): - Wraps an inline namespace "drake_vendor" with hidden symbol visibility around all of the code in file (but not any #include statements). + - Or when inline_namespace is False, simply marks all of the existing + namespaces as hidden without any extra inline namespace wrapping. + This does not hide the vendored library as thoroughly (it's still + a potential ODR conflict during static linking) but has the benefit + of working on more complicated projects that our wrapping heuristics + cannot handle. + Returns the new C++ contents. These changes should suffice for the most typical flavors of C++ code. @@ -157,18 +164,34 @@ def _rewrite_one_text(*, text, edit_include): if '\nextern "C" {\n' in text: return text - # We'll add an inline namespace around the C++ code in this file. - # Designate each line of the file for whether it should be wrapped. + # Prepare to edit one line at a time. lines = text.split('\n') if lines[-1] == '': lines.pop() + hidden = '__attribute__ ((visibility ("hidden")))' + + # If we are only changing namespaces (not adding new ones), do that now: + if not inline_namespace: + # Match either 'namespace foo' or 'namespace foo {'. + regex = re.compile(r'^\s*namespace\s+([^{]+?)(\s*{)?$') + for i, line in enumerate(lines): + match = regex.match(line) + if not match: + continue + name, brace = match.groups() + lines[i] = f'namespace {name} {hidden}{brace or ""}' + text = '\n'.join(lines) + '\n' + return text + + # We'll add an inline namespace around the C++ code in this file. + # Designate each line of the file for whether it should be wrapped. should_wrap = _designate_wrapped_lines(lines) # Anytime the sense of wrapping switches, we'll insert a line. # Do this in reverse order so that the indices into lines[] are stable. open_inline = ' '.join([ 'inline namespace drake_vendor', - '__attribute__ ((visibility ("hidden")))', + hidden, '{']) close_inline = '} /* inline namespace drake_vendor */' for i in range(len(lines), -1, -1): @@ -183,7 +206,8 @@ def _rewrite_one_text(*, text, edit_include): return text -def _rewrite_one_file(*, old_filename, new_filename, edit_include): +def _rewrite_one_file(*, old_filename, new_filename, edit_include, + inline_namespace): """Reads in old_filename and write into new_filename with specific alterations as described by _rewrite_one_string(). """ @@ -191,7 +215,8 @@ def _rewrite_one_file(*, old_filename, new_filename, edit_include): with open(old_filename, 'r', encoding='utf-8') as in_file: old_text = in_file.read() - new_text = _rewrite_one_text(text=old_text, edit_include=edit_include) + new_text = _rewrite_one_text(text=old_text, edit_include=edit_include, + inline_namespace=inline_namespace) # Write out the altered file. with open(new_filename, 'w', encoding='utf-8') as out_file: @@ -211,6 +236,9 @@ def _main(): '--edit-include', action='append', default=[], type=_split_pair, metavar='OLD:NEW', help='Project-local include spellings rewrite') + parser.add_argument( + '--no-inline-namespace', dest='inline_namespace', action='store_false', + help='Set visibility directly without an inline namespace wrapper') parser.add_argument( 'rewrite', nargs='+', type=_split_pair, help='Filename pairs to rewrite, given as IN:OUT') @@ -218,6 +246,7 @@ def _main(): for old_filename, new_filename in args.rewrite: _rewrite_one_file( edit_include=args.edit_include, + inline_namespace=args.inline_namespace, old_filename=old_filename, new_filename=new_filename) diff --git a/tools/workspace/vendor_cxx_test.py b/tools/workspace/vendor_cxx_test.py index f23fa20d52ae..7420f1ecefba 100644 --- a/tools/workspace/vendor_cxx_test.py +++ b/tools/workspace/vendor_cxx_test.py @@ -18,14 +18,30 @@ def setUp(self): self._open = 'inline namespace drake_vendor __attribute__ ((visibility ("hidden"))) {' # noqa self._close = '} /* inline namespace drake_vendor */' - def _check(self, old_lines, expected_new_lines): + def _check(self, old_lines, expected_new_lines, inline_namespace=True): """Tests one call to _rewrite_one_text for expected output.""" old_text = '\n'.join(old_lines) + '\n' new_text = _rewrite_one_text( - text=old_text, edit_include=self._edit_include.items()) + text=old_text, edit_include=self._edit_include.items(), + inline_namespace=inline_namespace) expected_new_text = '\n'.join(expected_new_lines) + '\n' self.assertMultiLineEqual(expected_new_text, new_text) + def test_without_inline_namespace(self): + self._check([ + 'namespace foo', + '{', + '}', + ' namespace bar {', + '}', + ], [ + 'namespace foo __attribute__ ((visibility ("hidden")))', + '{', + '}', + 'namespace bar __attribute__ ((visibility ("hidden"))) {', + '}', + ], inline_namespace=False) + def test_comments(self): self._check([ ' // file comment',