diff --git a/.github/environment.yml b/.github/environment.yml index bd57cd368..8c1ded0f6 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -4,6 +4,7 @@ dependencies: - numpy >=2.0 - swig - meson >=1.3.2 + - meson-python - compilers - pkg-config - pip diff --git a/meson.build b/meson.build index 1524e924c..dc19f205e 100644 --- a/meson.build +++ b/meson.build @@ -1,24 +1,33 @@ -# Much of this is from SciPy - project( 'pyoptsparse', 'c', 'cpp', -# unnecessary metadata commented out until Meson supports PEP517 and installation with pip -# version: 'x.x.x', -# license: 'GPL-3', - meson_version: '>= 0.60', + version: run_command( + 'python', '-c', + ''' +import re +from pathlib import Path +init_file = Path("pyoptsparse/__init__.py") +match = re.search(r'__version__ = ["\\\']([\d\\.]+)["\\\']', init_file.read_text()) +print(match.group(1)) + ''' + ).stdout().strip(), + meson_version: '>= 0.64', default_options: [ 'buildtype=debugoptimized', - 'c_std=c99', - 'cpp_std=c++14', + 'b_ndebug=if-release', + 'c_std=c17', + 'cpp_std=c++17', ], ) -fortranobject_c = '../fortranobject.c' +# + +# install python sources +install_subdir('pyoptsparse', install_dir: py3.get_install_dir()) + +# install non-python sources +subdir('pyoptsparse') diff --git a/pyoptsparse/meson.build b/pyoptsparse/meson.build index 7f1d811bc..752f042b8 100644 --- a/pyoptsparse/meson.build +++ b/pyoptsparse/meson.build @@ -1,69 +1,56 @@ -# NumPy include directory - needed in all submodules -incdir_numpy = get_option('incdir_numpy') -if incdir_numpy == '' - incdir_numpy = run_command(py3_target, - [ - '-c', - 'import os; os.chdir(".."); import numpy; print(numpy.get_include())' - ], - check: true - ).stdout().strip() +# subdir('pySNOPT') subdir('pySLSQP') @@ -71,27 +58,4 @@ subdir('pyCONMIN') subdir('pyNLPQLP') subdir('pyNSGA2') subdir('pyPSQP') -#subdir('pyALPSO') -#subdir('pyParOpt') -#subdir('postprocessing') - -# test imports -# envdata = environment() -# python_paths = [join_paths(meson.current_build_dir(), '..')] -# envdata.prepend('PYTHONPATH', python_paths) - -# progs = [['SLSQP', 'pySLSQP', 'slsqp'], -# ['CONMIN', 'pyCONMIN', 'conmin'], -# ['PSQP', 'pyPSQP', 'psqp'], -# ['NSGA2', 'pyNSGA2', 'nsga2']] - -# foreach p : progs -# import_command = 'from pyoptsparse.' + p[1] + ' import '+p[2]+'; print('+p[2]+'.__file__)' -# test( -# 'import test for '+p[0], -# py3_command, -# args: ['-c', import_command], -# env: envdata -# ) -# endforeach diff --git a/pyoptsparse/postprocessing/meson.build b/pyoptsparse/postprocessing/meson.build deleted file mode 100644 index 837a8257a..000000000 --- a/pyoptsparse/postprocessing/meson.build +++ /dev/null @@ -1,27 +0,0 @@ - -python_sources = [ - 'OptView.py', - 'OptView_baseclass.py', - 'OptView_dash.py', - '__init__.py', - 'view_saved_figure.py' -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/postprocessing' -) - -asset_sources = [ - 'assets/OptViewIcon.gif', - 'assets/base-styles.css', - 'assets/custom-styles.css', - 'assets/logo.png' -] - -py3_target.install_sources( - asset_sources, - pure: true, - subdir: 'pyoptsparse/postprocessing/assets' -) \ No newline at end of file diff --git a/pyoptsparse/pyALPSO/meson.build b/pyoptsparse/pyALPSO/meson.build deleted file mode 100644 index b49fafe2d..000000000 --- a/pyoptsparse/pyALPSO/meson.build +++ /dev/null @@ -1,13 +0,0 @@ -python_sources = [ - '__init__.py', - 'alpso.py', - 'alpso_ext.py', - 'pyALPSO.py', - 'LICENSE' -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/pyALPSO' -) \ No newline at end of file diff --git a/pyoptsparse/pyCONMIN/meson.build b/pyoptsparse/pyCONMIN/meson.build index a2eb7e5c4..9d9b905f4 100644 --- a/pyoptsparse/pyCONMIN/meson.build +++ b/pyoptsparse/pyCONMIN/meson.build @@ -2,11 +2,11 @@ conmin_source = custom_target('conminmodule.c', input : ['source/f2py/conmin.pyf', ], output : ['conminmodule.c', 'conmin-f2pywrappers.f'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyCONMIN'] ) -py3_target.extension_module('conmin', +py3.extension_module('conmin', 'source/openunit.f', 'source/cnmn00.f', 'source/cnmn01.f', @@ -21,21 +21,7 @@ py3_target.extension_module('conmin', 'source/conmin.f', 'source/closeunit.f', conmin_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pyCONMIN', - install : false, + install: true, build_rpath: '') - -#python_sources = [ -# '__init__.py', -# 'pyCONMIN.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyCONMIN' -#) diff --git a/pyoptsparse/pyIPOPT/meson.build b/pyoptsparse/pyIPOPT/meson.build deleted file mode 100644 index 7ea72e616..000000000 --- a/pyoptsparse/pyIPOPT/meson.build +++ /dev/null @@ -1,10 +0,0 @@ -# python_sources = [ -# '__init__.py', -# 'pyIPOPT.py', -# ] - -# py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyIPOPT' -# ) diff --git a/pyoptsparse/pyNLPQLP/meson.build b/pyoptsparse/pyNLPQLP/meson.build index 352d9bc65..6bf7e6fa5 100644 --- a/pyoptsparse/pyNLPQLP/meson.build +++ b/pyoptsparse/pyNLPQLP/meson.build @@ -11,11 +11,11 @@ if HAS_NLPQLP nlpqlp_source = custom_target('nlpqlpmodule.c', input : ['source/f2py/nlpqlp.pyf'], output : ['nlpqlpmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyNLPQLP'] ) - py3_target.extension_module('nlpqlp', + py3.extension_module('nlpqlp', 'source/wrapper.F90', 'source/NLPQLP.F', 'source/QL.F', @@ -24,18 +24,6 @@ if HAS_NLPQLP include_directories: [inc_np, inc_f2py], dependencies : py3_dep, subdir: 'pyoptsparse/pyNLPQLP', - install : false + install: true ) endif - -#python_sources = [ -# '__init__.py', -# 'pyNLPQLP.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyNLPQLP' -#) diff --git a/pyoptsparse/pyNSGA2/meson.build b/pyoptsparse/pyNSGA2/meson.build index e9cfdcca4..4365014c7 100644 --- a/pyoptsparse/pyNSGA2/meson.build +++ b/pyoptsparse/pyNSGA2/meson.build @@ -7,7 +7,7 @@ if swig.found() command: ['swig', '-o', 'pyoptsparse/pyNSGA2/nsga2_wrap.c', '-python', '-interface', 'nsga2', '@INPUT@'] ) - py3_target.extension_module('nsga2', + py3.extension_module('nsga2', 'source/allocate.c', 'source/auxiliary.c', 'source/crossover.c', @@ -31,19 +31,7 @@ if swig.found() dependencies : py3_dep, subdir: 'pyoptsparse/pyNSGA2', link_language: 'c', - install : false) + install: true) else message('SWIG was not found, therefore NSGA2 will not be built.') endif - -#python_sources = [ -# '__init__.py', -# 'pyNSGA2.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyNSGA2' -#) diff --git a/pyoptsparse/pyPSQP/meson.build b/pyoptsparse/pyPSQP/meson.build index 47bc59b0f..1cf920ecd 100644 --- a/pyoptsparse/pyPSQP/meson.build +++ b/pyoptsparse/pyPSQP/meson.build @@ -1,11 +1,11 @@ psqp_source = custom_target('psqpmodule.c', input : ['source/f2py/psqp.pyf'], output : ['psqpmodule.c', 'psqp-f2pywrappers.f'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyPSQP', '--no-wrap-functions'] ) -py3_target.extension_module('psqp', +py3.extension_module('psqp', 'source/closeunit.f', 'source/mqsubs.f', 'source/openunit.f', @@ -13,20 +13,6 @@ py3_target.extension_module('psqp', 'source/psqp.f', 'source/psqp_wrap.f90', psqp_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pyPSQP', - install : false) - -#python_sources = [ -# '__init__.py', -# 'pyPSQP.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyPSQP' -#) + install: true) diff --git a/pyoptsparse/pyParOpt/meson.build b/pyoptsparse/pyParOpt/meson.build deleted file mode 100644 index 0bc4f8e62..000000000 --- a/pyoptsparse/pyParOpt/meson.build +++ /dev/null @@ -1,10 +0,0 @@ -python_sources = [ - '__init__.py', - 'ParOpt.py', -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/pyParOpt' -) \ No newline at end of file diff --git a/pyoptsparse/pySLSQP/meson.build b/pyoptsparse/pySLSQP/meson.build index e898636bb..29717b7ce 100644 --- a/pyoptsparse/pySLSQP/meson.build +++ b/pyoptsparse/pySLSQP/meson.build @@ -1,11 +1,11 @@ slsqp_source = custom_target('slsqpmodule.c', input : ['source/f2py/slsqp.pyf'], output : ['slsqpmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pySLSQP'] ) -py3_target.extension_module('slsqp', +py3.extension_module('slsqp', 'source/closeunit.f', 'source/daxpy.f', 'source/dcopy.f', @@ -24,20 +24,6 @@ py3_target.extension_module('slsqp', 'source/slsqp.f', 'source/slsqpb.f', slsqp_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pySLSQP', - install : false) - -#python_sources = [ -# '__init__.py', -# 'pySLSQP.py', -# 'LICENSE' -#] - -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pySLSQP' -#) + install: true) diff --git a/pyoptsparse/pySNOPT/meson.build b/pyoptsparse/pySNOPT/meson.build index 24a97ef7e..e93b266c1 100644 --- a/pyoptsparse/pySNOPT/meson.build +++ b/pyoptsparse/pySNOPT/meson.build @@ -14,30 +14,16 @@ if HAS_SNOPT snopt_source = custom_target('snoptmodule.c', input : ['source/f2py/snopt.pyf'], output : ['snoptmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pySNOPT'] ) - py3_target.extension_module('snopt', + py3.extension_module('snopt', snopt_source, - fortranobject_c, snopt_source_files, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies : [fortranobject_dep], subdir: 'pyoptsparse/pySNOPT', - install : false, + install: true, fortran_args: '-ffixed-line-length-80' ) endif - -#python_sources = [ -# '__init__.py', -# 'pySNOPT.py', -# 'LICENSE' -#] - -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pySNOPT' -#) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e70531965..0b6199222 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,41 @@ [build-system] -requires = ["setuptools>=42", "meson>=0.60.0", "ninja", "numpy>=2.0"] -build-backend = "setuptools.build_meta" +requires = ["meson-python", "numpy"] +build-backend = "mesonpy" + +[project] +name = "pyoptsparse" +description = "Python package for formulating and solving nonlinear constrained optimization problems" +readme = "README.md" +license = {file = "LICENSE"} +requires-python = ">=3.9" +dependencies = [ + "packaging", + "sqlitedict>=1.6", + "numpy>=1.21", + "scipy>=1.7", + "mdolab-baseclasses>=1.3.1" +] +dynamic = ["version"] # version is dynamically populated from meson project + +[project.optional-dependencies] +optview = [ + "dash", + "plotly", + "matplotlib" +] +docs = [ + "sphinx", + "sphinx_rtd_theme" +] +testing = [ + "testflo>=1.4.5", + "parameterized", + "cyipopt" +] + +[project.urls] +Homepage = "https://github.com/mdolab/pyoptsparse" + +[project.scripts] +optview = "pyoptsparse.postprocessing.OptView:main" +optview_dash = "pyoptsparse.postprocessing.OptView_dash:main" diff --git a/setup.py b/setup.py deleted file mode 100644 index d2e6863a5..000000000 --- a/setup.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -import re -import shutil -import setuptools -import subprocess - - -def run_meson_build(): - prefix = os.path.join(os.getcwd(), staging_dir) - purelibdir = "." - - # check if meson extra args are specified - meson_args = "" - if "MESON_ARGS" in os.environ: - meson_args = os.environ["MESON_ARGS"] - - # configure - meson_path = shutil.which("meson") - meson_call = ( - f"{meson_path} setup {staging_dir} --prefix={prefix} " - + f"-Dpython.purelibdir={purelibdir} -Dpython.platlibdir={purelibdir} {meson_args}" - ) - sysargs = meson_call.split(" ") - sysargs = [arg for arg in sysargs if arg != ""] - p1 = subprocess.run(sysargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - print(p1.stdout.decode()) - setup_log = os.path.join(staging_dir, "setup.log") - with open(setup_log, "wb") as f: - f.write(p1.stdout) - if p1.returncode != 0: - raise OSError(sysargs, f"The meson setup command failed! Check the log at {setup_log} for more information.") - - # build - meson_call = f"{meson_path} compile -C {staging_dir}" - sysargs = meson_call.split(" ") - p2 = subprocess.run(sysargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - print(p2.stdout.decode()) - compile_log = os.path.join(staging_dir, "compile.log") - with open(compile_log, "wb") as f: - f.write(p2.stdout) - if p2.returncode != 0: - raise OSError( - sysargs, f"The meson compile command failed! Check the log at {compile_log} for more information." - ) - - -def copy_shared_libraries(): - build_path = os.path.join(staging_dir, "pyoptsparse") - for root, _dirs, files in os.walk(build_path): - for file in files: - # move pyoptsparse to just under staging_dir - if file.endswith((".so", ".lib", ".pyd", ".pdb", ".dylib")): - if ".so.p" in root or ".pyd.p" in root: # excludes intermediate object files - continue - file_path = os.path.join(root, file) - new_path = str(file_path) - match = re.search(staging_dir, new_path) - new_path = new_path[match.span()[1] + 1 :] - print(f"Copying build file {file_path} -> {new_path}") - shutil.copy(file_path, new_path) - - -if __name__ == "__main__": - # This is where the meson build system will install to, it is then - # used as the sources for setuptools - staging_dir = "meson_build" - - # this keeps the meson build system from running more than once - if "dist" not in str(os.path.abspath(__file__)) and not os.path.isdir(staging_dir): - cwd = os.getcwd() - run_meson_build() - os.chdir(cwd) - copy_shared_libraries() - - docs_require = "" - req_txt = os.path.join("doc", "requirements.txt") - if os.path.isfile(req_txt): - with open(req_txt) as f: - docs_require = f.read().splitlines() - - init_file = os.path.join("pyoptsparse", "__init__.py") - __version__ = re.findall( - r"""__version__ = ["']+([0-9\.]*)["']+""", - open(init_file).read(), - )[0] - - setuptools.setup( - name="pyoptsparse", - version=__version__, - description="Python package for formulating and solving nonlinear constrained optimization problems", - long_description="pyOptSparse is a Python package for formulating and solving nonlinear constrained optimization problems", - platforms=["Linux"], - keywords="optimization", - install_requires=[ - "packaging", - "sqlitedict>=1.6", - "numpy>=1.21", - "scipy>=1.7", - "mdolab-baseclasses>=1.3.1", - ], - extras_require={ - "optview": [ - "dash", - "plotly", - "matplotlib", - ], - "docs": docs_require, - "testing": ["testflo>=1.4.5", "parameterized", "cyipopt"], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Science/Research", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Software Development", - "Topic :: Education", - ], - package_dir={"": "."}, - packages=setuptools.find_packages(where="."), - package_data={ - "": ["*.so", "*.lib", "*.pyd", "*.pdb", "*.dylib", "assets/*", "LICENSE"], - }, - python_requires=">=3.9", - entry_points={ - "gui_scripts": [ - "optview = pyoptsparse.postprocessing.OptView:main", - "optview_dash = pyoptsparse.postprocessing.OptView_dash:main", - ] - }, - )