diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..86fa4cd --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,118 @@ +name: Release +on: + push: + branches: + - default + # To simplify the release process, the publishing is triggered on tag. + # We should make sure to only push tags for new releases. + # If we start using tags for non-release purposes, + # this needs to be updated. + # + # We need to explicitly configure an expression that matches anything. + tags: [ "**" ] + pull_request: + + +env: + LINUX_URL: "https://bin.chevah.com:20443/third-party-stuff/ibm-mqc-redist/9.4.3.1-IBM-MQC-Redist-LinuxX64.tar.gz" + WINDOWS_URL: "https://bin.chevah.com:20443/third-party-stuff/ibm-mqc-redist/9.4.3.1-IBM-MQC-Redist-Win64.zip" + +defaults: + run: + # Use bash on Windows for consistency. + shell: bash + + +jobs: + build_wheels: + name: Build ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + runs-on: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + name: Install Python + with: + python-version: '3.9' + + - name: Install deps + run: | + python -m pip install -r tools/requirements-dev.txt + + - name: Get IBM MQ C Linux Redistributables + if: matrix.runs-on == 'ubuntu-latest' + run: | + python tools/getRedistributables.py "$LINUX_URL" ibm-mq-c + + - name: Get IBM MQ C Windows Redistributables + if: matrix.runs-on == 'windows-latest' + run: | + python tools/getRedistributables.py "$WINDOWS_URL" ibm-mq-c + + - name: Build wheels + run: | + export MQ_FILE_PATH=`pwd`/ibm-mq-c + python -m build --wheel + + - name: Check files + run: ls -al dist/ + + - name: Audit ABI3 wheels + run: | + abi3audit -vsS dist/*.whl + + - uses: actions/upload-artifact@v4 + with: + name: artifact-wheels-${{ matrix.runs-on }} + path: ./dist/*.whl + + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + name: Install Python + with: + python-version: '3.9' + + - name: Install build + run: | + python -m pip install build + + - name: Build sdist + run: python -m build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: artifact-sdist + path: dist/*.tar.gz + + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: artifact-* + merge-multiple: true + path: dist + + - name: Check files + run: ls -al dist/ + + - name: Publish to PyPI - on tag + # Skip upload to PyPI if we don't have a tag + if: startsWith(github.ref, 'refs/tags/') + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/docs/Release.md b/docs/Release.md new file mode 100644 index 0000000..cb073d8 --- /dev/null +++ b/docs/Release.md @@ -0,0 +1,33 @@ +# Release + +GitHub Action is used to build the release files and publish them on PyPi. +This is automatically triggered when a new tag is created. + + +## Pre-release steps + +* Create a new branch with a name that starts with `release-`. + Ex: `release-2.1.0` +* Update the version inside setup.py +* Update CHANGELOG.md with the latest version and release date. + Add a note to the IBM MQ C Redistributables version used for this release. +* Create a pull request and make sure all checks pass. + The wheels are generated as part of the PR checks, + but they are not yet published to PyPI. + + +## Release steps + +* Use [GitHub Release](https://github.com/ibm-messaging/mq-mqi-python/releases/new) to create a new release together with a new tag. +* You don't have to create a GitHub Release, the important part is to + create a new tag. +* The tag value is the version. Without any prefix. +* Once a tag is pushed to the repo, GitHub Action will re-run all the jobs + and will publish to PyPI. + + +## Post-release steps + +* Update the version inside setup.py to the next development version. + Increment the micro version and add a .dev0 suffix. +* Merge the pull request diff --git a/setup.py b/setup.py index b2bec80..36f3d35 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,15 @@ # to indicate alpha/beta/release-candidate versions. VERSION = '2.0.0' +_ABI_LIMITS = { + # Minimum Python version that this package supports. + 'python_requires': ">=3.9", + # Used as a macro. + 'Py_LIMITED_API': 0x03090000, + # Used for bdist_wheel option. + "py_limited_api": "cp39", +} + # If the MQ SDK is in a non-default location, set MQ_FILE_PATH environment variable. custom_path = os.environ.get('MQ_FILE_PATH', None) @@ -138,12 +147,14 @@ def get_locations_by_command_path(command_path): # Can we find the MQ C header files? If not, there's no point in continuing, and we can # give a reasonable error message immediately instead of trying to decode C compiler errors. +# If we are in the CI environment, we still build the package as we want +# to be able to build the source package without the MQ C headers. found_headers = False # pylint: disable=invalid-name for d in include_dirs: p = os.path.join(d, "cmqc.h") if os.path.isfile(p): found_headers = True -if not found_headers: +if not found_headers and not os.environ.get('CI', ''): msg = "Cannot find MQ C header files.\n" msg += "Ensure you have already installed the MQ Client and SDK.\n" msg += "Use the MQ_FILE_PATH environment variable to identify a non-default location." @@ -195,7 +206,7 @@ def get_locations_by_command_path(command_path): # Limited API which should make the binary extension forwards compatible. mqi_extension = Extension('ibmmq.ibmmqc', c_source, define_macros=[('PYVERSION', '"' + VERSION + '"'), - ('Py_LIMITED_API', 0x03090000) + ('Py_LIMITED_API', _ABI_LIMITS['Py_LIMITED_API']) ], py_limited_api=True, library_dirs=library_dirs, @@ -214,7 +225,7 @@ def get_locations_by_command_path(command_path): platforms='OS Independent', package_dir={'': 'code'}, packages=['ibmmq'], - python_requires=">=3.9", + python_requires=_ABI_LIMITS['python_requires'], license_files=['LICENSE*'], license='Python-2.0', keywords=('pymqi IBMMQ MQ WebSphere WMQ MQSeries IBM middleware messaging queueing asynchronous SOA EAI ESB integration'), @@ -225,4 +236,6 @@ def get_locations_by_command_path(command_path): 'Programming Language :: C', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules'], - ext_modules=[mqi_extension]) + ext_modules=[mqi_extension], + options={"bdist_wheel": {"py_limited_api": _ABI_LIMITS["py_limited_api"]}}, +) diff --git a/tools/getRedistributables.py b/tools/getRedistributables.py new file mode 100644 index 0000000..8706e7d --- /dev/null +++ b/tools/getRedistributables.py @@ -0,0 +1,62 @@ +# Download and extract the redistributable package to support building +# the wheels on Windows and Linux. +# +# Pass the source URL as the first argument and the destination directory +# as the second argument. +# +# The source URL should end with .tar.gz or .zip +# +import os +import sys +import tarfile +import zipfile + +import requests + +if len(sys.argv) != 3: + print("Usage: this_script.py ") + sys.exit(1) + +SOURCE_URL = sys.argv[1] +DESTINATION_PATH = sys.argv[2] + +if SOURCE_URL.endswith('.tar.gz'): + temp_path = DESTINATION_PATH + '-temp.tar.gz' +elif SOURCE_URL.endswith('.zip'): + temp_path = DESTINATION_PATH + '-temp.zip' +else: + print("Unsupported archive format") + sys.exit(1) + +if os.path.exists(DESTINATION_PATH): + print( + f"Destination path {DESTINATION_PATH} already exists. " + "Remove it to use this script.") + sys.exit(1) + +_CHUNK_SIZE = 64 * 1024 + + +def download_file(source_url, destination_path): + with requests.get(source_url, stream=True) as r: + r.raise_for_status() + with open(destination_path, 'wb') as f: + for chunk in r.iter_content(chunk_size=_CHUNK_SIZE): + f.write(chunk) + +def extract_archive(archive_path, extract_to): + if archive_path.endswith('.tar.gz'): + with tarfile.open(archive_path, 'r:gz') as tar: + tar.extractall(path=extract_to) + elif archive_path.endswith('.zip'): + with zipfile.ZipFile(archive_path, 'r') as zip_ref: + zip_ref.extractall(extract_to) + else: + raise ValueError("Unsupported archive format") + + +print(f'Downloading "{SOURCE_URL}" to "{DESTINATION_PATH}"') +download_file(SOURCE_URL, temp_path) +extract_archive(temp_path, DESTINATION_PATH) +os.unlink(temp_path) +print(f'Done via "{temp_path}"') diff --git a/tools/requirements-dev.txt b/tools/requirements-dev.txt new file mode 100644 index 0000000..a4165b6 --- /dev/null +++ b/tools/requirements-dev.txt @@ -0,0 +1,7 @@ +# +# Python packages required during the dev,test,release process. +# +requests==2.32.5 +abi3audit==0.0.22 +wheel==0.45.1 +build==1.3.0