diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index a5064115b..148b9681f 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -19,6 +19,16 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - uses: bazelbuild/setup-bazelisk@v3 - name: Set up Python uses: actions/setup-python@v5 @@ -30,7 +40,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-bazel-${{ matrix.os }} + name: opencc-${{ env.VERSION }}-bazel-${{ matrix.os }} path: | bazel-bin/** bazel-testlogs/** diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 95fd26b8e..a3ca6ecf3 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -18,6 +18,16 @@ jobs: os: [ubuntu-latest, macos-latest, macos-14] steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: make build run: make build VERBOSE=1 REL_BUILD_DOCUMENTATION=OFF - name: make test @@ -28,7 +38,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-cmake-${{ matrix.os }} + name: opencc-${{ env.VERSION }}-cmake-${{ matrix.os }} path: | build/rel/** build/dbg/** diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 6ee1bccab..569c4fe9d 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -19,6 +19,15 @@ jobs: steps: - uses: msys2/setup-msys2@v2 - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: Install dependencies run: pacman -S --noconfirm base-devel mingw-w64-x86_64-toolchain cmake ninja python - name: Build with mingw-w64 @@ -48,7 +57,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-mingw + name: opencc-${{ env.VERSION }}-mingw path: | install/** build/src/tools/*.exe diff --git a/.github/workflows/msvc.yml b/.github/workflows/msvc.yml index 22a16cc8b..15be85e5f 100644 --- a/.github/workflows/msvc.yml +++ b/.github/workflows/msvc.yml @@ -30,6 +30,16 @@ jobs: can_run_binaries: false steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ matrix.arch }} @@ -90,7 +100,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-msvc-${{ matrix.arch }} + name: opencc-${{ env.VERSION }}-msvc-${{ matrix.arch }} path: | dist/OpenCC-msvc-${{ matrix.package_suffix }}.zip dist/opencc-msvc-${{ matrix.package_suffix }}/** diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 627de3014..782b25821 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -27,6 +27,16 @@ jobs: node-version: 20 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -37,7 +47,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-nodejs-${{ matrix.os }}-node-${{ matrix.node-version }} + name: opencc-${{ env.VERSION }}-nodejs-${{ matrix.os }}-node-${{ matrix.node-version }} path: | node/build/** **/npm-debug.log diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ad48d5aab..7ac251f48 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -19,6 +19,16 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -42,7 +52,7 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-python-${{ matrix.python-version }} + name: opencc-${{ env.VERSION }}-python-${{ matrix.python-version }} path: | pytest.xml @@ -54,6 +64,16 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: Build package and upload from docker (Linux) if: runner.os == 'Linux' @@ -61,6 +81,7 @@ jobs: docker run --rm -v "${PWD}:/opt/OpenCC" \ -e TWINE_USERNAME=__token__ \ -e TWINE_PASSWORD=${{ secrets.PYPI_TOKEN }} \ + -e VERSION=${VERSION} \ ubuntu:22.04 /bin/bash /opt/OpenCC/release-pypi-linux.sh testonly - name: Build package and upload (macOS) @@ -82,6 +103,6 @@ jobs: if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: opencc-pypi-${{ matrix.os }} + name: opencc-${{ env.VERSION }}-pypi-${{ matrix.os }} path: | dist/** diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 2e6e33cdd..e5602b136 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -12,6 +12,16 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute version + id: version + shell: bash + run: | + VERSION=$(bash scripts/compute-version.sh) + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION" - name: Build package and upload from docker (Linux) if: runner.os == 'Linux' @@ -19,6 +29,7 @@ jobs: docker run --rm -v "${PWD}:/opt/OpenCC" \ -e TWINE_USERNAME=__token__ \ -e TWINE_PASSWORD=${{ secrets.PYPI_TOKEN }} \ + -e VERSION=${VERSION} \ ubuntu:22.04 /bin/bash /opt/OpenCC/release-pypi-linux.sh - name: Build package and upload (macOS) diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml index 60a35c2e2..eabe4efb7 100644 --- a/.github/workflows/release-winget.yml +++ b/.github/workflows/release-winget.yml @@ -21,6 +21,8 @@ jobs: contents: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: ilammy/msvc-dev-cmd@v1 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ac5072f1..0372978f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,18 +41,10 @@ option(USE_SYSTEM_TCLAP "Use system version of TCLAP" OFF) ######## Package information set (PACKAGE_URL https://github.com/BYVoid/OpenCC) set (PACKAGE_BUGREPORT https://github.com/BYVoid/OpenCC/issues) -set (OPENCC_VERSION_MAJOR 1) -set (OPENCC_VERSION_MINOR 2) -set (OPENCC_VERSION_REVISION 1) -if (CMAKE_BUILD_TYPE MATCHES Debug) - set (version_suffix .Debug) -endif (CMAKE_BUILD_TYPE MATCHES Debug) - -set ( - OPENCC_VERSION - ${OPENCC_VERSION_MAJOR}.${OPENCC_VERSION_MINOR}.${OPENCC_VERSION_REVISION}${version_suffix} -) +# Derive version from Git tags (falls back to hardcoded default when Git is unavailable) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") +include(GitVersion) set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${OPENCC_VERSION_MAJOR}.${OPENCC_VERSION_MINOR}.${OPENCC_VERSION_REVISION}" diff --git a/MANIFEST.in b/MANIFEST.in index 38ab0ada5..91fa6a9d1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,8 @@ graft deps graft test graft data graft doc -include CMakeLists.txt OpenCCConfig.cmake.in opencc.pc.in README* LICENSE* +graft cmake +include AUTHORS CMakeLists.txt OpenCCConfig.cmake.in opencc.pc.in README* LICENSE* global-exclude *~ *.py[cod] *.so include python/**/__init__.py graft python/tests diff --git a/cmake/GitVersion.cmake b/cmake/GitVersion.cmake new file mode 100644 index 000000000..dbd4f6743 --- /dev/null +++ b/cmake/GitVersion.cmake @@ -0,0 +1,101 @@ +# +# GitVersion.cmake — Derive project version from Git tags. +# +# Sets in parent scope: +# OPENCC_VERSION Full version string (e.g. "1.2.1" or "1.2.1.dev4+gabc1234") +# OPENCC_VERSION_MAJOR Major component +# OPENCC_VERSION_MINOR Minor component +# OPENCC_VERSION_REVISION Patch/revision component +# OPENCC_VERSION_SUFFIX Everything after M.m.p (e.g. ".dev4+gabc1234.dirty") or "" +# OPENCC_VERSION_IS_RELEASE TRUE if this is an exact-tag release build +# +# Falls back to hardcoded default when Git is unavailable. +# + +set(_OPENCC_FALLBACK_MAJOR 1) +set(_OPENCC_FALLBACK_MINOR 2) +set(_OPENCC_FALLBACK_REVISION 1) + +find_package(Git QUIET) + +set(_git_version_computed FALSE) + +if(GIT_FOUND) + # Try git describe --tags --long --always + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --long --always + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + OUTPUT_VARIABLE _git_raw + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _git_result + ) + + if(_git_result EQUAL 0 AND _git_raw) + # Detect dirty working tree + set(_dirty_suffix "") + execute_process( + COMMAND "${GIT_EXECUTABLE}" diff --quiet + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE _diff_result + ) + execute_process( + COMMAND "${GIT_EXECUTABLE}" diff --cached --quiet + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE _diff_cached_result + ) + if(NOT _diff_result EQUAL 0 OR NOT _diff_cached_result EQUAL 0) + set(_dirty_suffix ".dirty") + endif() + + # Case 1: Exact tag — v1.2.3-0-g or ver.1.2.3-0-g + # NOTE: version tag patterns here must be kept in sync with scripts/compute-version.sh + string(REGEX MATCH "^(v|ver\\.)([0-9]+)\\.([0-9]+)\\.([0-9]+)-0-g[0-9a-f]+$" _match_tag "${_git_raw}") + if(_match_tag) + set(OPENCC_VERSION_MAJOR "${CMAKE_MATCH_2}") + set(OPENCC_VERSION_MINOR "${CMAKE_MATCH_3}") + set(OPENCC_VERSION_REVISION "${CMAKE_MATCH_4}") + set(OPENCC_VERSION_SUFFIX "${_dirty_suffix}") + set(OPENCC_VERSION_IS_RELEASE TRUE) + set(_git_version_computed TRUE) + endif() + + # Case 2: Dev build — v1.2.3-N-g or ver.1.2.3-N-g (N > 0) + if(NOT _git_version_computed) + string(REGEX MATCH "^(v|ver\\.)([0-9]+)\\.([0-9]+)\\.([0-9]+)-([0-9]+)-g([0-9a-f]+)$" _match_dev "${_git_raw}") + if(_match_dev) + set(OPENCC_VERSION_MAJOR "${CMAKE_MATCH_2}") + set(OPENCC_VERSION_MINOR "${CMAKE_MATCH_3}") + set(OPENCC_VERSION_REVISION "${CMAKE_MATCH_4}") + set(OPENCC_VERSION_SUFFIX ".dev${CMAKE_MATCH_5}+g${CMAKE_MATCH_6}${_dirty_suffix}") + set(OPENCC_VERSION_IS_RELEASE FALSE) + set(_git_version_computed TRUE) + endif() + endif() + + # Case 3: No tag — raw SHA only + if(NOT _git_version_computed) + set(OPENCC_VERSION_MAJOR "${_OPENCC_FALLBACK_MAJOR}") + set(OPENCC_VERSION_MINOR "${_OPENCC_FALLBACK_MINOR}") + set(OPENCC_VERSION_REVISION "${_OPENCC_FALLBACK_REVISION}") + set(OPENCC_VERSION_SUFFIX "+g${_git_raw}${_dirty_suffix}") + set(OPENCC_VERSION_IS_RELEASE FALSE) + set(_git_version_computed TRUE) + endif() + endif() +endif() + +# Fallback: no Git or git describe failed +if(NOT _git_version_computed) + message(STATUS "Git not available — using fallback version ${_OPENCC_FALLBACK_MAJOR}.${_OPENCC_FALLBACK_MINOR}.${_OPENCC_FALLBACK_REVISION}") + set(OPENCC_VERSION_MAJOR "${_OPENCC_FALLBACK_MAJOR}") + set(OPENCC_VERSION_MINOR "${_OPENCC_FALLBACK_MINOR}") + set(OPENCC_VERSION_REVISION "${_OPENCC_FALLBACK_REVISION}") + set(OPENCC_VERSION_SUFFIX "") + set(OPENCC_VERSION_IS_RELEASE FALSE) +endif() + +# Compose full version string +set(OPENCC_VERSION "${OPENCC_VERSION_MAJOR}.${OPENCC_VERSION_MINOR}.${OPENCC_VERSION_REVISION}${OPENCC_VERSION_SUFFIX}") + +message(STATUS "OpenCC version: ${OPENCC_VERSION}") diff --git a/release-pypi-linux.sh b/release-pypi-linux.sh index 316867533..e947f9a6c 100644 --- a/release-pypi-linux.sh +++ b/release-pypi-linux.sh @@ -29,12 +29,12 @@ eval "$(conda shell.bash hook)" conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r -for VERSION in 3.9 3.10 3.11 3.12 3.13 3.14; do +for PYTHON_VERSION in 3.9 3.10 3.11 3.12 3.13 3.14; do # Create and activate environment conda config --add channels conda-forge conda config --set channel_priority strict - conda create -y -n py$VERSION python=$VERSION - conda activate py$VERSION + conda create -y -n py$PYTHON_VERSION python=$PYTHON_VERSION + conda activate py$PYTHON_VERSION # Build and package pip install --no-cache-dir build diff --git a/release-pypi-macos.sh b/release-pypi-macos.sh index 5f3d87a73..aa1a80653 100644 --- a/release-pypi-macos.sh +++ b/release-pypi-macos.sh @@ -23,12 +23,12 @@ eval "$(conda shell.bash hook)" conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r -for VERSION in 3.9 3.10 3.11 3.12 3.13 3.14; do +for PYTHON_VERSION in 3.9 3.10 3.11 3.12 3.13 3.14; do # Create and activate environment conda config --add channels conda-forge conda config --set channel_priority strict - conda create -y -n py$VERSION python=$VERSION - conda activate py$VERSION + conda create -y -n py$PYTHON_VERSION python=$PYTHON_VERSION + conda activate py$PYTHON_VERSION # Build and package pip install --no-cache-dir build diff --git a/scripts/compute-version.sh b/scripts/compute-version.sh new file mode 100755 index 000000000..ed5b8e3fc --- /dev/null +++ b/scripts/compute-version.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# compute-version.sh — Derive a version string from Git state. +# +# Output format: +# Tag build (release): v1.2.3 +# Dev build (clean): v1.2.3.dev4+gabc1234 +# Dev build (dirty): v1.2.3.dev4+gabc1234.dirty +# No tag (clean): v1.2.1+gabc1234 +# No tag (dirty): v1.2.1+gabc1234.dirty +# +# Usage: +# ./scripts/compute-version.sh # prints version to stdout +# VERSION=$(./scripts/compute-version.sh) +# +set -euo pipefail + +# Fallback version — must be kept in sync with cmake/GitVersion.cmake +FALLBACK_VERSION="1.2.1" + +# Detect dirty working tree +DIRTY="" +if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + DIRTY=".dirty" +fi + +# Get raw git describe output +RAW=$(git describe --tags --long --always 2>/dev/null) || RAW="" + +if [ -z "$RAW" ]; then + # No git available at all — use fallback version + SHA=$(git rev-parse --short HEAD 2>/dev/null) || SHA="" + if [ -n "$SHA" ]; then + echo "v${FALLBACK_VERSION}+g${SHA}${DIRTY}" + else + echo "v${FALLBACK_VERSION}${DIRTY}" + fi + exit 0 +fi + +# Case 1: Exact tag (e.g. v1.2.3-0-gabc1234 or ver.1.2.3-0-gabc1234 from --long) +# NOTE: version tag patterns here must be kept in sync with cmake/GitVersion.cmake +if [[ "$RAW" =~ ^(v|ver\.)([0-9]+\.[0-9]+\.[0-9]+)-0-g[0-9a-f]+$ ]]; then + BASE="${BASH_REMATCH[2]}" + echo "v${BASE}${DIRTY}" + exit 0 +fi + +# Case 2: Dev build with tag (e.g. v1.2.3-4-gabc1234 or ver.1.2.3-4-gabc1234) +if [[ "$RAW" =~ ^(v|ver\.)([0-9]+\.[0-9]+\.[0-9]+)-([0-9]+)-g([0-9a-f]+)$ ]]; then + BASE="${BASH_REMATCH[2]}" + COUNT="${BASH_REMATCH[3]}" + SHA="${BASH_REMATCH[4]}" + echo "v${BASE}.dev${COUNT}+g${SHA}${DIRTY}" + exit 0 +fi + +# Case 3: No tag — use fallback version with commit SHA +echo "v${FALLBACK_VERSION}+g${RAW}${DIRTY}" diff --git a/setup.py b/setup.py index d6b5b9055..5dad66f3d 100644 --- a/setup.py +++ b/setup.py @@ -15,25 +15,60 @@ _cmake_file = os.path.join(_this_dir, 'CMakeLists.txt') _author_file = os.path.join(_this_dir, 'AUTHORS') _readme_file = os.path.join(_this_dir, 'README.md') +_fallback_version = '1.2.1' + + +def _get_version_from_git(): + try: + raw = subprocess.check_output( + ['git', 'describe', '--tags', '--long', '--always'], + cwd=_this_dir, + stderr=subprocess.DEVNULL, + ).decode('utf-8').strip() + except (OSError, subprocess.CalledProcessError): + return '' + + dirty = '' + for diff_cmd in (['git', 'diff', '--quiet'], ['git', 'diff', '--cached', '--quiet']): + try: + subprocess.check_call( + diff_cmd, + cwd=_this_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError): + dirty = '.dirty' + break + + release_match = re.match(r'^(?:v|ver\.)(\d+\.\d+\.\d+)-0-g[0-9a-f]+$', raw) + if release_match: + return '{}{}'.format(release_match.group(1), dirty) + + dev_match = re.match(r'^(?:v|ver\.)(\d+\.\d+\.\d+)-(\d+)-g([0-9a-f]+)$', raw) + if dev_match: + return '{}.dev{}+g{}{}'.format( + dev_match.group(1), + dev_match.group(2), + dev_match.group(3), + dirty, + ) + + return '{}+g{}{}'.format(_fallback_version, raw, dirty) def get_version_info(): - version_info = ['1', '0', '0'] - version_pattern = re.compile( - r'OPENCC_VERSION_(MAJOR|MINOR|REVISION) (\d+)') - with open(_cmake_file, 'rb') as f: - for l in f: - match = version_pattern.search(l.decode('utf-8')) - if not match: - continue - if match.group(1) == 'MAJOR': - version_info[0] = match.group(2) - elif match.group(1) == 'MINOR': - version_info[1] = match.group(2) - elif match.group(1) == 'REVISION': - version_info[2] = match.group(2) - version = '.'.join(version_info) - return version + # Prefer Git-derived version from environment (set by CI or compute-version.sh) + env_version = os.environ.get('VERSION', '') + if env_version: + # Strip leading 'v' prefix for PEP 440 compatibility + return env_version.lstrip('v') + + git_version = _get_version_from_git() + if git_version: + return git_version + + return _fallback_version def get_author_info():