diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index b697708763d..9086297661a 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -38,8 +38,8 @@ jobs: - name: build wheels run: | - pip install wheel - python setup.py bdist_wheel + python -m pip install build + python -m build . - uses: actions/upload-artifact@v3 with: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..a0ec415598e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "energyplus-core" +dynamic = ["version"] +description = "EnergyPlus Python Bindings" +requires-python = ">=3.8" +readme = "README.md" +license = {text = "Modified BSD"} +authors = [{name = "United States Department of Energy"}] +urls = {homepage = "https://github.com/NREL/EnergyPlus"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[tool.scikit-build] +cmake.source-dir = "." +wheel.py-api = "cp38" +# create a namespace package +wheel.install-dir = "energyplus/core/" +# use git tags for versioning +metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" + +[tool.scikit-build.cmake.define] +# this is required for pyenergyplus to be included +BUILD_PACKAGE = "ON" +# optional switches +BUILD_FORTRAN = "OFF" +DOCUMENTATION_BUILD = "DoNotBuild" + +# NOTE section required by metadata.version.provider +[tool.setuptools_scm] +version_scheme = "no-guess-dev" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9968de6fe52..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# requirements for building an EnergyPlus wheel -wheel \ No newline at end of file diff --git a/scripts/dev/versioning.py b/scripts/dev/versioning.py new file mode 100644 index 00000000000..836a8444773 --- /dev/null +++ b/scripts/dev/versioning.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# EnergyPlus, Copyright (c) 1996-2024, The Board of Trustees of the University +# of Illinois, The Regents of the University of California, through Lawrence +# Berkeley National Laboratory (subject to receipt of any required approvals +# from the U.S. Dept. of Energy), Oak Ridge National Laboratory, managed by UT- +# Battelle, Alliance for Sustainable Energy, LLC, and other contributors. All +# rights reserved. +# +# NOTICE: This Software was developed under funding from the U.S. Department of +# Energy and the U.S. Government consequently retains certain rights. As such, +# the U.S. Government has been granted for itself and others acting on its +# behalf a paid-up, nonexclusive, irrevocable, worldwide license in the +# Software to reproduce, distribute copies to the public, prepare derivative +# works, and perform publicly and display publicly, and to permit others to do +# so. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# (1) Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# (2) Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# (3) Neither the name of the University of California, Lawrence Berkeley +# National Laboratory, the University of Illinois, U.S. Dept. of Energy nor +# the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in +# stand-alone form without changes from the version obtained under this +# License, or (ii) Licensee makes a reference solely to the software +# portion of its product, Licensee must refer to the software as +# "EnergyPlus version X" software, where "X" is the version number Licensee +# obtained under this License and may not use a different name for the +# software. Except as specifically required in this Section (4), Licensee +# shall not use in a company name, a product name, in advertising, +# publicity, or other promotional activities any name, trade name, +# trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or +# confusingly similar designation, without the U.S. Department of Energy's +# prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +r""" +Script/module for versioning related activities. + +Versions are stored in Git tags. +Valid "versioning" tags are prefixed with 'v' and MUST be PEP 440 compliant. + +See also: https://peps.python.org/pep-0440 +""" + +import argparse +import subprocess +try: from packaging.version import Version +except ModuleNotFoundError: + raise ModuleNotFoundError('Install via `python3 -m pip install packaging`.') + + +class VersionManager: + def __init__(self, repository): + self.repository = repository + + def get(self) -> Version: + version_str = subprocess.check_output( + ['git', 'describe', '--tags', '--abbr=0', '--match=v*'], + cwd=self.repository, + text=True, + ) + return Version(version_str) + + def set(self, version) -> Version: + version_ = ( + version + if isinstance(version, Version) else + Version(version) + ) + subprocess.run( + ['git', 'tag', f'v{version_!s}'], + cwd=self.repository, + check=True, + ) + return version_ + + +def main(args=None): + parser = argparse.ArgumentParser( + description="Manage versions of a Git repository, PEP 440 compliant.", + ) + parser.add_argument( + '-c', '--cwd', + default='.', + help="Path to the Git repository.", + ) + + subparsers = parser.add_subparsers(dest='command', required=True) + # command: get + parser_get = subparsers.add_parser( + 'get', + help="Get the version attached to current commit.", + ) + # command: set + parser_set = subparsers.add_parser( + 'set', + help="Tag current commit to a new version.", + ) + parser_set.add_argument( + 'version', + help="New version to set.", + ) + + args = parser.parse_args(args=args) + vm = VersionManager(args.cwd) + if args.command == 'get': + print(str(vm.get())) + if args.command == 'set': + print(str(vm.set(args.version))) + + +if __name__ == '__main__': + main() + + +__all__ = [ + 'VersionManager', + 'main', +] \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 2db904ed868..00000000000 --- a/setup.py +++ /dev/null @@ -1,235 +0,0 @@ -# EnergyPlus, Copyright (c) 1996-2024, The Board of Trustees of the University -# of Illinois, The Regents of the University of California, through Lawrence -# Berkeley National Laboratory (subject to receipt of any required approvals -# from the U.S. Dept. of Energy), Oak Ridge National Laboratory, managed by UT- -# Battelle, Alliance for Sustainable Energy, LLC, and other contributors. All -# rights reserved. -# -# NOTICE: This Software was developed under funding from the U.S. Department of -# Energy and the U.S. Government consequently retains certain rights. As such, -# the U.S. Government has been granted for itself and others acting on its -# behalf a paid-up, nonexclusive, irrevocable, worldwide license in the -# Software to reproduce, distribute copies to the public, prepare derivative -# works, and perform publicly and display publicly, and to permit others to do -# so. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# (1) Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# (2) Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# (3) Neither the name of the University of California, Lawrence Berkeley -# National Laboratory, the University of Illinois, U.S. Dept. of Energy nor -# the names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in -# stand-alone form without changes from the version obtained under this -# License, or (ii) Licensee makes a reference solely to the software -# portion of its product, Licensee must refer to the software as -# "EnergyPlus version X" software, where "X" is the version number Licensee -# obtained under this License and may not use a different name for the -# software. Except as specifically required in this Section (4), Licensee -# shall not use in a company name, a product name, in advertising, -# publicity, or other promotional activities any name, trade name, -# trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or -# confusingly similar designation, without the U.S. Department of Energy's -# prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -""" -Cross-platform setup.py for building the EnergyPlus Python module. -Bare-bones library, no pre- or post-processing tools. -""" - -from setuptools import setup, Extension -from setuptools.command.build_ext import build_ext -from shutil import rmtree, copy -from platform import machine, system -from os import cpu_count -from pathlib import Path -from subprocess import check_call, CalledProcessError -from re import findall - -from wheel.bdist_wheel import bdist_wheel - - -def get_ep_version_string(repository_root_dir: Path) -> str: - version_info_file = repository_root_dir / 'cmake' / 'Version.cmake' - version_contents = version_info_file.read_text() - version_major = findall(r'CMAKE_VERSION_MAJOR (\d+)', version_contents)[0] - version_minor = findall(r'CMAKE_VERSION_MINOR (\d+)', version_contents)[0] - version_patch = findall(r'CMAKE_VERSION_PATCH (\d+)', version_contents)[0] - return f"{version_major}.{version_minor}.{version_patch}" - - -def get_current_wheel_details(): - wheels = { - "Darwin": { - "x86_64": { - "wheel": "macosx_10_13_x86_64", # TODO: Dynamically determine Mac version numbers? - "zip_tag": "OSX", - "build_tool": "Unix Makefiles", - "extension": "dylib", - }, - "arm64": { - "wheel": "macosx_11_0_arm64", - "zip_tag": "OSX_arm64", - "build_tool": "Unix Makefiles", - "extension": "dylib", - }, - }, - "Linux": { - "x86_64": { - "wheel": "manylinux_2_17_x86_64", - "zip_tag": "Linux", - "build_tool": "Unix Makefiles", - "extension": "so", - } - }, - "Windows": { - "i386": { # I would love to not build 32-bit, but I know there is a good amount of 32 bit Python out there - "wheel": "win32", - "zip_tag": "Windows", - "arch": "Win32", - "build_tool": "Visual Studio 16 2019", - "extension": "dll", - }, - "AMD64": { - "wheel": "win_amd64", - "zip_tag": "Windows", - "arch": "x64", - "build_tool": "Visual Studio 16 2019", - "extension": "dll", - }, - }, - } - return wheels[system()][machine()] - - -class PyenergyplusBDistWheel(bdist_wheel): - def get_tag(self): - return "py3", "none", get_current_wheel_details()["wheel"] - - -class EnergyPlusBuild(build_ext): - @staticmethod - def cmake_configure_command() -> list[str]: - current_config = get_current_wheel_details() - cmake_cmd = ["cmake", "-G", current_config["build_tool"]] - if "arch" in current_config: - cmake_cmd += ["-A", current_config['arch']] - cmake_cmd.append("-DBUILD_FORTRAN=OFF") - if system() != "Windows": - cmake_cmd.append("-DCMAKE_BUILD_TYPE=Release") - cmake_cmd.append(str(repo_root_directory)) - return cmake_cmd - - @staticmethod - def cmake_build_command() -> list[str]: - cmake_build_cmd = ["cmake", "--build", "."] - if system() == "Windows": # VS builds require specifying the build config - cmake_build_cmd.extend(["--config", "Release"]) - else: # MSBuild doesn't like the -j passed in, so only do this on Non-Windows - cmake_build_cmd.extend(['--', '-j', f"{cpu_count() - 1}"]) - return cmake_build_cmd - - @staticmethod - def fixup_copied_python_file(python_file: Path): - t = python_file.read_text() - t = t.replace('from pyenergyplus.', 'from energyplus.') - t = t.replace( - 'api_dll_dir = os.path.dirname(os.path.normpath(this_script_dir))', - 'api_dll_dir = os.path.normpath(this_script_dir)' - ) - python_file.write_text(t) - - def run(self): - self.build_lib = 'build/energyplus' # I feel like this variable has meaning on this class, so leaving it - - try: - cmake_cmd = self.cmake_configure_command() - check_call(cmake_cmd, cwd=build_root_directory) - except CalledProcessError as cpe: - raise Exception( - f"CMake failed to configure EnergyPlus, check error logs, raw error message: {cpe}" - ) from None - - try: - cmake_build_cmd = self.cmake_build_command() - check_call(cmake_build_cmd, cwd=build_root_directory) - except CalledProcessError as cpe: - raise Exception( - f"CMake failed to build EnergyPlus, check error logs, raw error message: {cpe}" - ) from None - - # while EnergyPlus is built in the repo/build-wheel folder, set up the path to the actual wheel build - # this will be in repo/build-wheel/build/energyplus to avoid conflicting with dev's normal repo/build folders - # we will wipe this folder each build to get a clean wheel - wheel_build_directory = Path(build_root_directory / self.build_lib) - if wheel_build_directory.exists(): - rmtree(wheel_build_directory) - wheel_build_directory.unlink(missing_ok=True) - wheel_build_directory.mkdir(parents=True) - - # Copy the shared library files and Python API files to the output directory - products_dir = build_root_directory / 'Products' - if system() == "Windows": - products_dir /= 'Release' - built_shared_libraries = products_dir.glob(f"*.{get_current_wheel_details()['extension']}*") - for lib in built_shared_libraries: - copy(lib, wheel_build_directory) - - # Copy the Python source code files to the output directory - energyplus_py_dir = products_dir / "pyenergyplus" - # TODO: Include some weather files and example files? But where? Inside the wheel, API functions to access? - # TODO: Consider adding install_requires to get the extra EnergyPlus stuff...and fixing it up after install! - # TODO: Try uploading to test-pypi and see what happens! - for lib in energyplus_py_dir.glob("*.py"): - copy(lib, wheel_build_directory) - # do quick fix-ups on the copied file to work in the pip installed configuration - self.fixup_copied_python_file(wheel_build_directory / lib.name) - - -# find the repository root early, and set up a new build-directory called build-wheel where all build ops will occur -repo_root_directory = Path(__file__).resolve().parent -build_root_directory = repo_root_directory / 'build-wheel' -build_root_directory.mkdir(exist_ok=True) - -setup( - name="energyplus", - version=get_ep_version_string(repo_root_directory), - packages=[], - license="Modified BSD", - author="United States Department of Energy", - author_email="", - url="https://github.com/NREL/EnergyPlus", - description="EnergyPlus is a building simulation program for modeling energy and water use in buildings.", - long_description=(repo_root_directory / "README.md").read_text(), - long_description_content_type='text/markdown', - ext_modules=[Extension("energyplus", sources=[])], - cmdclass={ - "build_ext": EnergyPlusBuild, - "bdist_wheel": PyenergyplusBDistWheel, - }, - options={ - 'bdist_wheel': {'bdist_dir': str(build_root_directory / 'build')} - }, - build_base='.' -) diff --git a/src/EnergyPlus/api/api.py b/src/EnergyPlus/api/api.py index 57733daf076..543662f17a5 100644 --- a/src/EnergyPlus/api/api.py +++ b/src/EnergyPlus/api/api.py @@ -57,11 +57,11 @@ import os import sys -from pyenergyplus.func import Functional -from pyenergyplus.datatransfer import DataExchange -from pyenergyplus.runtime import Runtime -from pyenergyplus.state import StateManager -# from pyenergyplus.autosizing import Autosizing +from .func import Functional +from .datatransfer import DataExchange +from .runtime import Runtime +from .state import StateManager +# from .autosizing import Autosizing def api_path() -> str: diff --git a/src/EnergyPlus/api/autosizing.py b/src/EnergyPlus/api/autosizing.py index 56193632ee4..4f61ca8a909 100644 --- a/src/EnergyPlus/api/autosizing.py +++ b/src/EnergyPlus/api/autosizing.py @@ -54,7 +54,7 @@ # POSSIBILITY OF SUCH DAMAGE. from ctypes import cdll, c_char_p, c_int, c_void_p -from pyenergyplus.common import RealEP +from .common import RealEP class BaseSizerWorker: diff --git a/src/EnergyPlus/api/datatransfer.py b/src/EnergyPlus/api/datatransfer.py index 2ca5b9f47ab..b2b74779337 100644 --- a/src/EnergyPlus/api/datatransfer.py +++ b/src/EnergyPlus/api/datatransfer.py @@ -54,7 +54,7 @@ # POSSIBILITY OF SUCH DAMAGE. from ctypes import cdll, c_int, c_char_p, c_void_p, POINTER, Structure, byref -from pyenergyplus.common import RealEP, EnergyPlusException, is_number +from .common import RealEP, EnergyPlusException, is_number from typing import List, Union diff --git a/src/EnergyPlus/api/func.py b/src/EnergyPlus/api/func.py index 91be55d9fb1..e5a59801f6c 100644 --- a/src/EnergyPlus/api/func.py +++ b/src/EnergyPlus/api/func.py @@ -55,7 +55,7 @@ from ctypes import cdll, c_int, c_char_p, c_void_p, CFUNCTYPE from types import FunctionType -from pyenergyplus.common import RealEP +from .common import RealEP # CFUNCTYPE wrapped Python callbacks need to be kept in memory explicitly, otherwise GC takes it # This causes undefined behavior but generally segfaults and illegal access violations diff --git a/src/EnergyPlus/api/plugin.py b/src/EnergyPlus/api/plugin.py index 1cbb3f98aed..2efb18f34c6 100644 --- a/src/EnergyPlus/api/plugin.py +++ b/src/EnergyPlus/api/plugin.py @@ -59,7 +59,7 @@ from typing import List -from pyenergyplus.api import EnergyPlusAPI +from .api import EnergyPlusAPI class EnergyPlusPlugin(object): diff --git a/src/EnergyPlus/api/plugin_tester.py b/src/EnergyPlus/api/plugin_tester.py index d60e9fdcaf0..91e224816cc 100644 --- a/src/EnergyPlus/api/plugin_tester.py +++ b/src/EnergyPlus/api/plugin_tester.py @@ -65,7 +65,7 @@ from importlib import util as import_util from unittest.mock import Mock -from pyenergyplus.plugin import EnergyPlusPlugin +from .plugin import EnergyPlusPlugin def generate_mock_api(bare_mock_api_instance: Mock) -> Mock: