Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
# It was a bit of overkill before testing every minor version, and since this project is all about
# SemVer, we should expect Python to adhere to that model to. Therefore Only test across 2 OS's but
# the lowest supported minor version and the latest stable minor version (just in case).
python-versions-linux: '["3.8", "3.13"]'
python-versions-linux: '["3.8", "3.14"]'
# Since the test suite takes ~4 minutes to complete on windows, and windows is billed higher
# we are only going to run it on the oldest version of python we support. The older version
# will be the most likely area to fail as newer minor versions maintain compatibility.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ jobs:
# It was a bit of overkill before testing every minor version, and since this project is all about
# SemVer, we should expect Python to adhere to that model to. Therefore Only test across 2 OS's but
# the lowest supported minor version and the latest stable minor version.
python-versions-linux: '["3.8", "3.13"]'
python-versions-windows: '["3.8", "3.13"]'
python-versions-linux: '["3.8", "3.14"]'
python-versions-windows: '["3.8", "3.14"]'
files-changed: ${{ needs.eval-changes.outputs.any-file-changes }}
build-files-changed: ${{ needs.eval-changes.outputs.build-changes }}
ci-files-changed: ${{ needs.eval-changes.outputs.ci-changes }}
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on:
type: boolean
required: true
default: true
python3-14:
description: 'Test Python 3.14?'
type: boolean
required: true
default: true
python3-13:
description: 'Test Python 3.13?'
type: boolean
Expand Down Expand Up @@ -79,6 +84,7 @@ jobs:
"3.11" if str(os.getenv("INPUT_PY3_11", False)).lower() == str(True).lower() else None,
"3.12" if str(os.getenv("INPUT_PY3_12", False)).lower() == str(True).lower() else None,
"3.13" if str(os.getenv("INPUT_PY3_13", False)).lower() == str(True).lower() else None,
"3.14" if str(os.getenv("INPUT_PY3_14", False)).lower() == str(True).lower() else None,
]))

linux_versions = (
Expand All @@ -105,6 +111,7 @@ jobs:
INPUT_PY3_11: ${{ inputs.python3-11 }}
INPUT_PY3_12: ${{ inputs.python3-12 }}
INPUT_PY3_13: ${{ inputs.python3-13 }}
INPUT_PY3_14: ${{ inputs.python3-14 }}
INPUT_LINUX: ${{ inputs.linux }}
INPUT_WINDOWS: ${{ inputs.windows }}
run: |
Expand Down
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
readme = "README.rst"
authors = [{ name = "Rolf Erik Lekang", email = "me@rolflekang.com" }]
authors = [
{ name = "Rolf Erik Lekang", email = "me@rolflekang.com" },
{ name = "codejedi365", email = "codejedi365@gmail.com" },
]
dependencies = [
"click ~= 8.1.0",
"click-option-group ~= 0.5",
"gitpython ~= 3.0",
"requests ~= 2.25",
"jinja2 ~= 3.1",
"python-gitlab >= 4.0.0, < 7.0.0",
"tomlkit ~= 0.11",
"tomlkit ~= 0.13.0",
"dotty-dict ~= 1.3",
"importlib-resources ~= 6.0",
"pydantic ~= 2.0",
Expand All @@ -52,7 +56,8 @@ repository = "http://github.com/python-semantic-release/python-semantic-release.

[project.optional-dependencies]
build = [
"build ~= 1.2"
"build ~= 1.2",
"tomlkit ~= 0.13.0",
]
docs = [
"Sphinx ~= 7.4",
Expand Down
224 changes: 224 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#!/bin/bash

set -eu -o pipefail

function load_env() {
set -eu -o pipefail

if [ "${UTILITIES_SH_LOADED:-false}" = "false" ]; then
local __FILE__=""
__FILE__="$(realpath "${BASH_SOURCE[0]}")"

local __DIR__=""
__DIR__="$(realpath "$(dirname "$__FILE__")")"

local ROOT_DIR=""
ROOT_DIR="$(realpath "$(dirname "$__DIR__")")"

# shellcheck source=scripts/utils.sh
source "$ROOT_DIR/scripts/utils.sh"

load_base_env
fi
}

function with_temp_working_dir() {
local working_dir="${1:?"Working directory not specified, but is required!"}"
pushd "$working_dir" >/dev/null || return 1
explicit_run_cmd "${@:2}" || return 1
popd >/dev/null || return 1
}

function build_sdist() {
local status_msg="${1:?"Status message not specified, but is required!"}"
local output_dir="${2:?"Output directory not specified, but is required!"}"

if ! explicit_run_cmd_w_status_wrapper \
"$status_msg" \
python3 -m build --sdist . --outdir "$output_dir" ">/dev/null";
then
return 1
fi
find "$output_dir" -type f -name "*.tar.gz" -exec \
sh -c 'printf "%s\n" "Successfully built $1"' shell {} \;
}

function unpack_sdist() {
local sdist_file="${1:?"Source distribution file not specified, but is required!"}"
local output_dir="${2:?"Output directory not specified, but is required!"}"

mkdir -p "$output_dir"
if ! explicit_run_cmd_w_status_wrapper \
"Unpacking sdist code into '$output_dir'" \
tar -xzf "$sdist_file" -C "$output_dir" --strip-components=1;
then
return 1
fi
}


function strip_optional_dependencies {
local -r pyproject_file="${1:?'param[1]: Path to pyproject.toml file is required'}"
local -r exclude_groups=("${@:2}")
local python_snippet="\
from pathlib import Path
from sys import argv, exit
try:
import tomlkit
except ModuleNotFoundError:
print('Failed Import: Missing build requirement \'tomlkit\'.')
exit(1)

pyproject_file = Path(argv[1])
config = tomlkit.loads(pyproject_file.read_text())
proj_config = config.get('project', {})

if not (opt_deps := proj_config.get('optional-dependencies', {})):
exit(0)

if not (dep_group_to_remove := argv[2:]):
exit(0)

for group in dep_group_to_remove:
if group in opt_deps:
opt_deps.pop(group)

if not opt_deps:
proj_config.pop('optional-dependencies')

pyproject_file.write_text(tomlkit.dumps(config))
"
# make whitespace nice for python (remove indent)
python_snippet="$(printf '%s\n' "$python_snippet" | sed -E 's/([ ]{4,8}|\t)(.*)/\2/')"

if [ "${#exclude_groups[@]}" -eq 0 ]; then
error "At least one dependency group to exclude must be specified!"
return 1
fi

python3 -c "$python_snippet" "$pyproject_file" "${exclude_groups[@]}" || return 1
}

remove_empty_init_files() {
local dirpath="${1:-.}"

# SNIPPET: Remove empty __init__.py files
local python_snippet='\
from pathlib import Path
from sys import exit, argv, stderr

if len(argv) < 2 or not (dirpath := Path(argv[1])).is_dir():
print("Usage: <existing_dirpath>", file=stderr)
exit(1)

for filepath in dirpath.resolve().rglob("__init__.py"):
if not filepath.is_file():
continue
if not filepath.read_text().strip():
filepath.unlink()
print(f"Removed {filepath}")
'
# make whitespace nice for python
python_snippet="$(printf '%s\n' "$python_snippet" | sed -E 's/([ ]{4,8}|\t)(.*)/\2/')"

python3 -c "$python_snippet" "$dirpath" || return 1
}

function build_production_whl() {
# Assumes the current working directory is the directory to modify
local dest_dir="${1:?"param[1]: output directory not specified, but required!"}"

# Strip out development dependencies
explicit_run_cmd_w_status_wrapper \
"Masking development dependencies" \
strip_optional_dependencies "pyproject.toml" "build" "dev" "docs" "test" "mypy" || return 1

# Optimize code for runtime
explicit_run_cmd_w_status_wrapper \
"Removing empty '__init__.py' files" \
remove_empty_init_files "src" || return 1

# Remove editable info from the source directory before wheel build
rm -rf src/*.egg-info/

# Build the wheel into the output directory
explicit_run_cmd_w_status_wrapper \
"Constructing wheel package" \
python3 -m build --wheel . --outdir "$dest_dir" || return 1
}

function build_wheel_from_sdist() {
local build_dir="${1:?"param[1]: Build directory not specified, but is required!"}"
local dest_dir="${2:?"param[2]: Output directory not specified, but is required!"}"
local tmp_src_dir="$build_dir/sdist"

unpack_sdist "$build_dir/*.tar.gz" "$tmp_src_dir" || return 1

with_temp_working_dir "$tmp_src_dir" build_production_whl "$dest_dir" || return 1

rm -rf "$tmp_src_dir"
}

function build_production_package() {
local dest_dir
local output_dir="${1:?"param[1]: Output directory not specified, but required!"}"
local build_dir="build"

# If the output directory is not an absolute path, make it absolute
if ! stdout "$output_dir" | grep -q -E '^/'; then
dest_dir="$(realpath ".")/$output_dir"
else
dest_dir="$output_dir"
fi

# Clean up any existing output directory
if [ -d "$dest_dir" ]; then
rm -rf "$dest_dir"
fi

# Clean up any existing build directory
if [ -d "$build_dir" ]; then
rm -rf "$build_dir"
fi

build_sdist "Bundling source code" "$build_dir" || return 1

explicit_run_cmd_w_status_wrapper \
"Building production wheel from sdist" \
build_wheel_from_sdist "$build_dir" "$dest_dir" || return 1

rm -rf "$build_dir"
}

function main() {
set -eu -o pipefail

cd "$PROJ_ROOT_DIR"

if ! explicit_run_cmd_w_status_wrapper \
"Verifying Python environment" \
verify_python "$MINIMUM_PYTHON_VERSION";
then
info "Please run the dev setup script and activate the virtual environment first."
return 1
fi

explicit_run_cmd_w_status_wrapper \
"Verifying build dependencies exist" \
python3 -m pip install -e ".[build]" ">/dev/null"

explicit_run_cmd_w_status_wrapper \
"Building production package" \
build_production_package "dist"
}

########################################################################
# CONDITIONAL AUTO-EXECUTE #
########################################################################

if ! (return 0 2>/dev/null); then
# Since this script is not being sourced, run the main function
unset -v UTILITIES_SH_LOADED # Ensure utils are reloaded when called from another script
load_env
main "$@"
fi
Loading
Loading