diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 563807425..d95a51d1a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,5 +1,6 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python +# This workflow builds binary wheels using cibuildwheel and a source distribution. +# cibuildwheel handles manylinux and Windows wheel building across Python versions. +# Publishes to TestPyPI on pushes to dev, and PyPI on pushes to main. name: Build Packages @@ -10,20 +11,18 @@ on: branches: [ "main", "dev" ] jobs: - build: - + make-wheels: + name: Build wheels on ${{ matrix.os }} (compiler ${{ matrix.compiler-version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["3.10","3.11","3.12","3.13"] - os: [ubuntu-latest, windows-latest] #, macos-latest] - compiler: [gcc] - compiler-version: [13, 15] - exclude: - - os: windows-latest - compiler-version: 15 + include: - os: ubuntu-latest + compiler: gcc + compiler-version: 15 + - os: windows-latest + compiler: gcc compiler-version: 13 steps: @@ -31,59 +30,148 @@ jobs: uses: fortran-lang/setup-fortran@v1 id: setup-fortran with: - compiler: ${{ matrix.toolchain.compiler }} - version: ${{ matrix.toolchain.version }} + compiler: ${{ matrix.compiler }} + version: ${{ matrix.compiler-version }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: submodules: recursive - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x64' - cache: 'pip' - - - name: show-versions + fetch-depth: 0 + + - name: Show compiler versions run: | gcc --version gfortran --version - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install click numpy pandas matplotlib build wheel setuptools setuptools_scm meson-python pytest ninja - - name: Install Patchelf - if: runner.os == 'Linux' - run: | - python -m pip install patchelf - - - name: Build and package - run: | - python -m build --no-isolation - - - name: Store Wheel - uses: actions/upload-artifact@v4 + - name: Build wheels + uses: pypa/cibuildwheel@v3.4.1 + env: + # Build for Python 3.10–3.13, 64-bit only + CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* cp314-* + CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux*" + CIBW_ARCHS_LINUX: x86_64 + CIBW_ARCHS_WINDOWS: AMD64 + + # Use manylinux_2_28 to get a gfortran-15-capable image + CIBW_MANYLINUX_X86_64_IMAGE: quay.io/pypa/manylinux_2_28_x86_64 + + # Install build dependencies inside the wheel-build environment + CIBW_BEFORE_BUILD_LINUX: | + dnf install -y gcc-gfortran ninja-build && + pip install click numpy pandas build wheel setuptools setuptools_scm meson-python ninja patchelf + + CIBW_BEFORE_BUILD_WINDOWS: | + pip install click numpy pandas build wheel setuptools setuptools_scm meson-python ninja + + CIBW_BUILD_FRONTEND: "build" + CIBW_BUILD_VERBOSITY: 1 + with: - name: pyfvs-${{ runner.os }}-py${{ matrix.python-version }}-wheels + output-dir: dist + + - name: Store wheels + uses: actions/upload-artifact@v7 + with: + name: pyfvs-${{ matrix.os }}-wheels path: dist/*.whl if-no-files-found: warn overwrite: true - - - name: Store Sdist - uses: actions/upload-artifact@v4 + + make-sdist: + name: Build source distribution + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + architecture: x64 + cache: pip + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + python -m pip install click numpy pandas build wheel \ + setuptools setuptools_scm meson-python ninja patchelf + + - name: Create sdist + run: python -m build --sdist --no-isolation + + - name: Store sdist + uses: actions/upload-artifact@v7 with: name: pyfvs-sdist path: dist/*.tar.gz if-no-files-found: warn overwrite: true - - name: Test with pytest - working-directory: . - run: | - pip install confuse typing-extensions openpyxl - pip install dist/*.whl - pytest + publish-testpypi: + name: Publish to TestPyPI (dev branch) + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + needs: [ make-wheels, make-sdist ] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/pyfvs + permissions: + id-token: write # Required for OIDC trusted publishing + + steps: + - name: Download all wheels + uses: actions/download-artifact@v4 + with: + pattern: pyfvs-*-wheels + path: dist + merge-multiple: true + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: pyfvs-sdist + path: dist + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + verbose: true + + publish-pypi: + name: Publish to PyPI (main branch) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: [ make-wheels, make-sdist ] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pyfvs + permissions: + id-token: write # Required for OIDC trusted publishing + + steps: + - name: Download all wheels + uses: actions/download-artifact@v4 + with: + pattern: pyfvs-*-wheels + path: dist + merge-multiple: true + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: pyfvs-sdist + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + verbose: true diff --git a/README.md b/README.md index c517aebe7..39e996026 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ Python wrappers and utilities for using the Forest Vegetation Simulator The PyFVS [FVS source code](https://github.com/forest-modeling/ForestVegetationSimulator/tree/open-dev) is forked from the [USFS FVS GitHub](https://github.com/USDAForestService/ForestVegetationSimulator) repository, [open-dev](https://github.com/USDAForestService/ForestVegetationSimulator/tree/open-dev) branch + _____ ______ __ __ _____ + | __ \ | ____|\ \ / / / ____| + | |__) | _ _ | |__ \ \ / / | (____ + | ___/ | | | | | __| \ \/ / \___ \ + | | | |__| | | | \ / ____) | + |_| \____ | |_| \/ |______/ + __/ | + |___/ + ## Documentation Check out the new AI generated documentation [wiki](https://deepwiki.com/forest-modeling/PyFVS). diff --git a/meson.build b/meson.build index c60dbfb45..cf2c5c666 100644 --- a/meson.build +++ b/meson.build @@ -358,8 +358,8 @@ foreach variant : variants variant_args += '-DFVS_MORTS_WRAP' # Add the wrappers to the file lists - f_sources += 'api/variant/@0@/morts_fvs.f'.format(variant) - f90_sources += 'api/morts_wrap.f90' + f_sources += 'src/api/variant/@0@/morts_fvs.f'.format(variant) + f90_sources += 'src/api/morts_wrap.f90' endif @@ -368,7 +368,7 @@ foreach variant : variants conf_data = configuration_data() conf_data.set('variant', variant) configure_file( - input: 'api/globals.f90.in', + input: 'src/api/globals.f90.in', output: variant + '_globals.f90', configuration: conf_data ) @@ -377,26 +377,26 @@ foreach variant : variants f90_sources += join_paths(meson.project_build_root(), variant + '_globals.f90') # Additional API files to compile - # f90_sources += 'api/apisubs.f90' - - f90_sources += 'api/version.f90' - f90_sources += 'api/fvs_api.f90' - # f90_sources += 'api/sim_monitor.f90' - # f90_sources += 'api/initialize.f90' - f90_sources += 'api/inventory_trees.f90' - f90_sources += 'api/fvs_step.f90' - f90_sources += 'api/tree_data.f90' - f90_sources += 'api/snag_data.f90' - f90_sources += 'api/carbon_data.f90' - f90_sources += 'api/downwood_data.f90' - # f90_sources += 'api/step_tregro.f90' - f90_sources += 'api/prtrls_wrap.f90' - # f90_sources += 'api/stop_wrap.f90' - # f90_sources += 'api/foo.f90' - # f90_sources += 'api/test.f90' + # f90_sources += 'src/api/apisubs.f90' + + f90_sources += 'src/api/version.f90' + f90_sources += 'src/api/fvs_api.f90' + # f90_sources += 'src/api/sim_monitor.f90' + # f90_sources += 'src/api/initialize.f90' + f90_sources += 'src/api/inventory_trees.f90' + f90_sources += 'src/api/fvs_step.f90' + f90_sources += 'src/api/tree_data.f90' + f90_sources += 'src/api/snag_data.f90' + f90_sources += 'src/api/carbon_data.f90' + f90_sources += 'src/api/downwood_data.f90' + # f90_sources += 'src/api/step_tregro.f90' + f90_sources += 'src/api/prtrls_wrap.f90' + # f90_sources += 'src/api/stop_wrap.f90' + # f90_sources += 'src/api/foo.f90' + # f90_sources += 'src/api/test.f90' # ## TODO: Auto-generate F90 includes from F77 - # f90_inc_dirs += 'api/gen/@0@/include'.format(variant) + # f90_inc_dirs += 'src/api/gen/@0@/include'.format(variant) # # Add the include folders as a dependency to trigger # inc_dep = declare_dependency( @@ -537,4 +537,4 @@ foreach variant : variants # End variant loop endforeach -subdir('pyfvs') +subdir('src/pyfvs') diff --git a/meson_options.txt b/meson_options.txt index 1bb80dc6c..575929a96 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,9 +1,9 @@ option('fvsvariants', type: 'array', choices: ['pn','wc','so','op','oc','ec','ca','nc','bm','ie','ci','ak','ws'], #,'em','tt','cr','ut','ls','cs','sn','ne'], - value: ['pn','wc','so','op','oc','ec','ca','nc','bm','ie','ci','ak','ws'], #,'em','tt','cr','ut','ls','cs','sn','ne'], + # value: ['pn','wc','so','op','oc','ec','ca','nc','bm','ie','ci','ak','ws'], #,'em','tt','cr','ut','ls','cs','sn','ne'], # value: ['pn','wc','so','op','oc','ec','ca','nc','bm'], # value: ['ie','ci','ak',], - # value: ['pn',], + value: ['pn',], # value: ['ws',], description: 'FVS variants to build' ) \ No newline at end of file diff --git a/pyfvs/pyfvs.cfg b/pyfvs/pyfvs.cfg deleted file mode 100644 index 718b70ab1..000000000 --- a/pyfvs/pyfvs.cfg +++ /dev/null @@ -1,39 +0,0 @@ -{ - 'fvs_lib':{ - 'fvslib_path':'../../bin/build/Open-FVS/python/lib' - } - - ,'logging':{ - 'version':1 - ,'disable_existing_loggers':True - ,'incremental':False - ,'formatters':{ - 'file':{ - 'format' : '%(asctime)s %(levelname)-8s %(name)-15s %(message)s' - ,'datefmt' : '%Y-%m-%d %H:%M:%S' - } - } - - ,'handlers':{ - 'console':{ - 'class':'logging.StreamHandler' - ,'level':'NOTSET' - ,'stream':'ext://sys.stdout' - } - ,'file':{ - 'class':'logging.FileHandler' - ,'filename':'./pyfvs.log' - ,'mode':'w' - ,'level':'NOTSET' - ,'formatter':'file' - } - } - ,'loggers':{ - 'pyfvs':{ - 'level':'INFO' - ,'handlers':['console','file',] - ,'propagate':False - } - } - } - } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d12f48a06..2c7c86b80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,10 @@ requires = [ "meson", "meson-python>=0.15", "numpy", -# "ninja", + "ninja", "patchelf ; platform_system != 'Windows'", ## Only linux??? # "cython", - "build>=1.5.0,<2" + "build" ] build-backend = "mesonpy" @@ -30,9 +30,8 @@ dynamic = ["version"] dependencies = [ "confuse>=2.0.0,<3", "click>=8.0.0,<9", - "pandas>=2.2.2", + "pandas>=2.2.2,<4", "numpy>=2.1.0,<3", - "pandas>=3.0.2,<4", "typing-extensions" # required by confuse ] @@ -59,7 +58,8 @@ packages = ["pyfvs",] # check out https://github.com/pypa/setuptools_scm version_scheme = "post-release" # version_scheme = "guess-next-dev" -version_file = "pyfvs/_version.py" +local_scheme = "no-local-version" +version_file = "src/pyfvs/_version.py" [tool.devpy] package = 'pyfvs' # used by pytest @@ -70,13 +70,17 @@ addopts = [ "--import-mode=importlib", ] +[tool.cibuildwheel] +test-requires = ["pytest","openpyxl"] +test-command = "pytest {project}/tests" + [tool.pixi.workspace] channels = ["conda-forge"] platforms = ["linux-64"] [tool.pixi.tasks] -dev = "python -m pip install --no-build-isolation -e ." -build = "python -m build --no-isolation" +dev = "python -m pip install -e . --no-build-isolation --verbose" +build = "python -m build --no-isolation --verbose" [tool.pixi.dependencies] # Python runtime and build tools diff --git a/api/carbon_data.f90 b/src/api/carbon_data.f90 similarity index 100% rename from api/carbon_data.f90 rename to src/api/carbon_data.f90 diff --git a/api/downwood_data.f90 b/src/api/downwood_data.f90 similarity index 100% rename from api/downwood_data.f90 rename to src/api/downwood_data.f90 diff --git a/api/fvs_api.f90 b/src/api/fvs_api.f90 similarity index 100% rename from api/fvs_api.f90 rename to src/api/fvs_api.f90 diff --git a/api/fvs_step.f90 b/src/api/fvs_step.f90 similarity index 100% rename from api/fvs_step.f90 rename to src/api/fvs_step.f90 diff --git a/api/globals.f90 b/src/api/globals.f90 similarity index 100% rename from api/globals.f90 rename to src/api/globals.f90 diff --git a/api/globals.f90.in b/src/api/globals.f90.in similarity index 100% rename from api/globals.f90.in rename to src/api/globals.f90.in diff --git a/api/inventory_trees.f90 b/src/api/inventory_trees.f90 similarity index 100% rename from api/inventory_trees.f90 rename to src/api/inventory_trees.f90 diff --git a/api/morts_wrap.f90 b/src/api/morts_wrap.f90 similarity index 100% rename from api/morts_wrap.f90 rename to src/api/morts_wrap.f90 diff --git a/api/prtrls_wrap.f90 b/src/api/prtrls_wrap.f90 similarity index 100% rename from api/prtrls_wrap.f90 rename to src/api/prtrls_wrap.f90 diff --git a/api/snag_data.f90 b/src/api/snag_data.f90 similarity index 100% rename from api/snag_data.f90 rename to src/api/snag_data.f90 diff --git a/api/tree_data.f90 b/src/api/tree_data.f90 similarity index 100% rename from api/tree_data.f90 rename to src/api/tree_data.f90 diff --git a/api/variant/pn/morts_fvs.f b/src/api/variant/pn/morts_fvs.f similarity index 100% rename from api/variant/pn/morts_fvs.f rename to src/api/variant/pn/morts_fvs.f diff --git a/api/variant/wc/morts_fvs.f b/src/api/variant/wc/morts_fvs.f similarity index 100% rename from api/variant/wc/morts_fvs.f rename to src/api/variant/wc/morts_fvs.f diff --git a/api/version.f90 b/src/api/version.f90 similarity index 100% rename from api/version.f90 rename to src/api/version.f90 diff --git a/pyfvs/README.md b/src/pyfvs/README.md similarity index 100% rename from pyfvs/README.md rename to src/pyfvs/README.md diff --git a/pyfvs/README.txt b/src/pyfvs/README.txt similarity index 100% rename from pyfvs/README.txt rename to src/pyfvs/README.txt diff --git a/pyfvs/__init__.py b/src/pyfvs/__init__.py similarity index 100% rename from pyfvs/__init__.py rename to src/pyfvs/__init__.py diff --git a/pyfvs/__main__.py b/src/pyfvs/__main__.py similarity index 100% rename from pyfvs/__main__.py rename to src/pyfvs/__main__.py diff --git a/pyfvs/config_default.yaml b/src/pyfvs/config_default.yaml similarity index 100% rename from pyfvs/config_default.yaml rename to src/pyfvs/config_default.yaml diff --git a/pyfvs/fvs.py b/src/pyfvs/fvs.py similarity index 99% rename from pyfvs/fvs.py rename to src/pyfvs/fvs.py index b4f7fcd00..7c5469b9a 100644 --- a/pyfvs/fvs.py +++ b/src/pyfvs/fvs.py @@ -10,7 +10,6 @@ """ from dataclasses import field -from distutils.log import debug import os import sys import re diff --git a/pyfvs/keywords/__init__.py b/src/pyfvs/keywords/__init__.py similarity index 100% rename from pyfvs/keywords/__init__.py rename to src/pyfvs/keywords/__init__.py diff --git a/pyfvs/keywords/_fields.py b/src/pyfvs/keywords/_fields.py similarity index 100% rename from pyfvs/keywords/_fields.py rename to src/pyfvs/keywords/_fields.py diff --git a/pyfvs/keywords/_utils.py b/src/pyfvs/keywords/_utils.py similarity index 100% rename from pyfvs/keywords/_utils.py rename to src/pyfvs/keywords/_utils.py diff --git a/pyfvs/keywords/eventmonitor.py b/src/pyfvs/keywords/eventmonitor.py similarity index 100% rename from pyfvs/keywords/eventmonitor.py rename to src/pyfvs/keywords/eventmonitor.py diff --git a/pyfvs/keywords/keywords.py b/src/pyfvs/keywords/keywords.py similarity index 100% rename from pyfvs/keywords/keywords.py rename to src/pyfvs/keywords/keywords.py diff --git a/pyfvs/meson.build b/src/pyfvs/meson.build similarity index 66% rename from pyfvs/meson.build rename to src/pyfvs/meson.build index b05ed62bc..2e6120284 100644 --- a/pyfvs/meson.build +++ b/src/pyfvs/meson.build @@ -1,6 +1,8 @@ # Generate the variants file conf_data = configuration_data() +pyfvs_install_dir = py3.get_install_dir() / 'pyfvs' + # stringify the variant library dictionary libs_str = '{' foreach k,v : variant_libs @@ -14,7 +16,7 @@ configure_file( output: 'variants.py', configuration: conf_data, install: true, - install_dir: py3.get_install_dir() / 'pyfvs' + install_dir: pyfvs_install_dir ) python_sources = [ @@ -31,18 +33,18 @@ py3.install_sources( install_subdir( 'keywords', - install_dir: py3.get_install_dir() / 'pyfvs', + install_dir: pyfvs_install_dir, exclude_directories : ['__pycache__'] ) install_data( - project_folder / 'pyfvs/config_default.yaml', - install_dir: py3.get_install_dir() / 'pyfvs', + 'config_default.yaml', + install_dir: pyfvs_install_dir, ) ## FIXME: Filter out all the extraneous files -# exclude_files = run_command(py3,'-c', 'import os.path as pth,glob;f=glob.glob("../pyfvs/test/*");print(";".join([pth.basename(p) for p in f if pth.isfile(p) and not pth.splitext(p)[-1] in (".py",".ini")]))').stdout().strip().split(';') -# exclude_dirs = run_command(py3,'-c', 'import os.path as pth,glob;f=glob.glob("../pyfvs/test/*");print(";".join([pth.basename(p) for p in f if pth.isdir(p) and not pth.splitext(p)[-1] in ("rmrs","reg_test")]))').stdout().strip().split(';') +# exclude_files = run_command(py3,'-c', 'import os.path as pth,glob;f=glob.glob("../../tests");print(";".join([pth.basename(p) for p in f if pth.isfile(p) and not pth.splitext(p)[-1] in (".py",".ini")]))').stdout().strip().split(';') +# exclude_dirs = run_command(py3,'-c', 'import os.path as pth,glob;f=glob.glob("../../tests/*");print(";".join([pth.basename(p) for p in f if pth.isdir(p) and not pth.splitext(p)[-1] in ("rmrs","reg_test")]))').stdout().strip().split(';') # exclude_files += '.pytest_cache' # Because Python interprets this dir as a file??? # message('Exclude Files') @@ -52,7 +54,7 @@ install_data( # install_subdir( # 'test', -# install_dir: py3.get_install_dir() / 'pyfvs', +# install_dir: pyfvs_install_dir, # # exclude_files: exclude_files, # exclude_directories : ['__pycache__','.pytest_cache','notebooks'], #,'reg_test','fia_test','api_test'] # # exclude_directories: exclude_dirs, diff --git a/pyfvs/variants.py.in b/src/pyfvs/variants.py.in similarity index 100% rename from pyfvs/variants.py.in rename to src/pyfvs/variants.py.in