diff --git a/.build/Dockerfile b/.build/Dockerfile index 1d1f9f276..5647ba3c9 100755 --- a/.build/Dockerfile +++ b/.build/Dockerfile @@ -4,7 +4,7 @@ COPY . . RUN find /lib /usr/lib -name '*.so*' | sed 's|.*/||' | awk '{print "--noinclude-dlls="$0}' > nuitka_exclude_so.txt RUN apk add py3-pip python3-dev patchelf build-base libffi-dev RUN pip3 install -U nuitka --break-system-packages -RUN python3 .build/remove_python2.py +RUN python3 .build/patch.py RUN python3 -O -m nuitka run.py \ --mode=onefile\ --output-dir=./dist\ diff --git a/.build/nuitka.cmd b/.build/nuitka.sh similarity index 88% rename from .build/nuitka.cmd rename to .build/nuitka.sh index 4df5bf911..be6408db9 100755 --- a/.build/nuitka.cmd +++ b/.build/nuitka.sh @@ -1 +1 @@ -python3 -m nuitka run.py --mode=onefile --output-dir=./dist --no-deployment-flag=self-execution --output-filename=ddns --remove-output --include-module=dns.dnspod --include-module=dns.alidns --include-module=dns.dnspod_com --include-module=dns.dnscom --include-module=dns.cloudflare --include-module=dns.he --include-module=dns.huaweidns --include-module=dns.callback --product-name=DDNS --file-description="DDNS Client 自动更新域名解析到本机IP" --company-name="New Future" --copyright="https://ddns.newfuture.cc" --windows-icon-from-ico="favicon.ico" --assume-yes-for-downloads --lto=yes --nofollow-import-to=unittest,pydoc --onefile-tempdir-spec="{CACHE_DIR}/{PRODUCT}/{VERSION}" \ No newline at end of file +python3 -m nuitka run.py --mode=onefile --output-dir=./dist --no-deployment-flag=self-execution --output-filename=ddns --remove-output --include-module=dns.dnspod --include-module=dns.alidns --include-module=dns.dnspod_com --include-module=dns.dnscom --include-module=dns.cloudflare --include-module=dns.he --include-module=dns.huaweidns --include-module=dns.callback --product-name=DDNS --file-description="DDNS Client 自动更新域名解析到本机IP" --company-name="New Future" --copyright="https://ddns.newfuture.cc" --windows-icon-from-ico="favicon.ico" --assume-yes-for-downloads --lto=yes --nofollow-import-to=unittest,pydoc --onefile-tempdir-spec="{CACHE_DIR}/{PRODUCT}/{VERSION}" --linux-icon=doc/img/ddns.svg \ No newline at end of file diff --git a/.build/patch.py b/.build/patch.py new file mode 100644 index 000000000..3560804f4 --- /dev/null +++ b/.build/patch.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +自动将所有 try-except python2/3 兼容导入替换为 python3 only 导入,并显示处理日志 +""" +import os +import re + +ROOT = '.' +# 匹配 try-except 块,去除导入前缩进,保证import顶格,删除的行用空行代替 +PATTERN = re.compile( + r'^[ \t]*try:[^\n]*python 3[^\n]*\n' # try: # python 3 + r'((?:[ \t]+[^\n]*\n)+?)' # python3 导入内容 + r'^[ \t]*except ImportError:[^\n]*\n' # except ImportError: # python 2 + r'((?:[ \t]+from[^\n]*\n|[ \t]+import[^\n]*\n)*)', # except块内容 + re.MULTILINE +) + + +def dedent_imports_with_blank(import_block, try_block, except_block): + """ + 保留python3导入并去除缩进,try/except及except内容用空行代替 + """ + try_lines = try_block.count('\n') + except_lines = except_block.count('\n') + imports = ''.join(line.lstrip() + for line in import_block.splitlines(keepends=True)) + return ('\n' * try_lines) + imports + ('\n' * except_lines) + + +def extract_pure_version(version_str): + """ + 提取前4组数字并用点拼接,如 v1.2.3.beta4.5 -> 1.2.3.4 + """ + import re + nums = re.findall(r'\d+', version_str) + return '.'.join(nums[:4]) if nums else "0.0.0" + + +def update_nuitka_version(pyfile): + """ + 读取 __version__ 并替换 nuitka-project 版本号 + """ + with open(pyfile, 'r', encoding='utf-8') as f: + content = f.read() + + # 提取 __version__ 变量 + version_match = re.search(r'__version__\s*=\s*[\'"]([^\'"]+)[\'"]', content) + if not version_match: + print(f'No __version__ found in {pyfile}') + return False + + version_str = version_match.group(1) + pure_version = extract_pure_version(version_str) + + # 替换 nuitka-project 行 + new_content, n = re.subn( + r'(# nuitka-project: --product-version=)[^\n]*', + r'\g<1>' + pure_version, + content + ) + if n > 0: + with open(pyfile, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f'update nuitka-project version: {pure_version} in {pyfile}') + return True + return False + + +def remove_windows_textiowrapper(pyfile): + """ + 如果当前系统不是 Windows,则删除 run.py 中的 TextIOWrapper 兼容代码块 + """ + if os.name == 'nt': + return # Windows 下不处理 + + with open(pyfile, 'r', encoding='utf-8') as f: + content = f.read() + + # 匹配并删除 if sys.version_info.major == 3 and os_name == 'nt': ... 代码块 + pattern = re.compile( + r'(?m)^[ \t]*if sys\.version_info\.major == 3 and os_name == [\'"]nt[\'"]:\n' + r'(?:[ \t]+from io import TextIOWrapper\n)?' + r'(?:[ \t]+sys\.stdout = TextIOWrapper\(sys\.stdout\.detach\(\), encoding=[\'"]utf-8[\'"]\)\n)?' + r'(?:[ \t]+sys\.stderr = TextIOWrapper\(sys\.stderr\.detach\(\), encoding=[\'"]utf-8[\'"]\)\n)?' + ) + new_content, n = pattern.subn('', content) + if n > 0: + with open(pyfile, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f'Removed Windows TextIOWrapper code from {pyfile}') + + +def main(): + """ + 遍历所有py文件并替换兼容导入,同时更新nuitka版本号 + """ + changed_files = 0 + # 先处理 run.py 的 nuitka-project 版本号 + update_nuitka_version(os.path.join(ROOT, "run.py")) + # 非 Windows 平台,移除 TextIOWrapper 兼容代码 + remove_windows_textiowrapper(os.path.join(ROOT, "run.py")) + for dirpath, _, filenames in os.walk(ROOT): + for fname in filenames: + if fname.endswith('.py'): + fpath = os.path.join(dirpath, fname) + with open(fpath, 'r', encoding='utf-8') as f: + content = f.read() + + def repl(match): + try_block = re.match( + r'^[ \t]*try:[^\n]*python 3[^\n]*\n', match.group(0) + ).group(0) + except_block = re.search( + r'^[ \t]*except ImportError:[^\n]*\n((?:[ \t]+from[^\n]*\n|[ \t]+import[^\n]*\n)*)', + match.group(0), re.MULTILINE + ) + except_block = except_block.group( + 0) if except_block else '' + return dedent_imports_with_blank(match.group(1), try_block, except_block) + + new_content, n = PATTERN.subn(repl, content) + if n > 0: + with open(fpath, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f'change: {fpath}') + changed_files += 1 + print('done') + print(f'Total changed files: {changed_files}') + + +if __name__ == '__main__': + main() diff --git a/.build/remove_python2.py b/.build/remove_python2.py deleted file mode 100644 index 0c1bdda96..000000000 --- a/.build/remove_python2.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -""" -自动将所有 try-except python2/3 兼容导入替换为 python3 only 导入,并显示处理日志 -""" -import os -import re - -ROOT = '.' -# 匹配 try-except 块,去除导入前缩进,保证import顶格,删除的行用空行代替 -PATTERN = re.compile( - r'^[ \t]*try:[^\n]*python 3[^\n]*\n' # try: # python 3 - r'((?:[ \t]+[^\n]*\n)+?)' # python3 导入内容 - r'^[ \t]*except ImportError:[^\n]*\n' # except ImportError: # python 2 - r'((?:[ \t]+from[^\n]*\n|[ \t]+import[^\n]*\n)*)', # except块内容 - re.MULTILINE -) - - -def dedent_imports_with_blank(import_block, try_block, except_block): - """ - 保留python3导入并去除缩进,try/except及except内容用空行代替 - """ - try_lines = try_block.count('\n') - except_lines = except_block.count('\n') - imports = ''.join(line.lstrip() - for line in import_block.splitlines(keepends=True)) - return ('\n' * try_lines) + imports + ('\n' * except_lines) - - -def main(): - """ - 遍历所有py文件并替换兼容导入 - """ - changed_files = 0 - for dirpath, _, filenames in os.walk(ROOT): - for fname in filenames: - if fname.endswith('.py'): - fpath = os.path.join(dirpath, fname) - with open(fpath, 'r', encoding='utf-8') as f: - content = f.read() - - def repl(match): - try_block = re.match( - r'^[ \t]*try:[^\n]*python 3[^\n]*\n', match.group(0) - ).group(0) - except_block = re.search( - r'^[ \t]*except ImportError:[^\n]*\n((?:[ \t]+from[^\n]*\n|[ \t]+import[^\n]*\n)*)', - match.group(0), re.MULTILINE - ) - except_block = except_block.group( - 0) if except_block else '' - return dedent_imports_with_blank(match.group(1), try_block, except_block) - - new_content, n = PATTERN.subn(repl, content) - if n > 0: - with open(fpath, 'w', encoding='utf-8') as f: - f.write(new_content) - print(f'change: {fpath}') - changed_files += 1 - print('done') - print(f'Total changed files: {changed_files}') - - -if __name__ == '__main__': - main() diff --git a/.github/release.md b/.github/release.md new file mode 100644 index 000000000..82eb9ca61 --- /dev/null +++ b/.github/release.md @@ -0,0 +1,74 @@ +[![PyPI version](https://img.shields.io/badge/DDNS-${BUILD_VERSION}-1abc9c.svg?style=social)](https://pypi.org/project/ddns/${BUILD_VERSION}/) ![Deploy OK](https://img.shields.io/badge/release-success-brightgreen.svg?style=flat-square) + +## 版本下载方式一览表 + +| 平台/方式 |架构支持 | +| ----------- |---------------------- | +| Docker | `newfuture/ddns:${BUILD_VERSION}` 8种架构| +| Windows | [64位(x64)](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-x64.exe) / [32位(x86)](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-x86.exe) / [arm64](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-arm64.exe) | +| Linux | [64位(x64)](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-linux-x64) / [arm64](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-linux-arm64) | +| Mac OS X | [Apple Silicon (ARM64)](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-mac-arm64.app) / [Intel x64](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-mac-x64.app) | +| Python/PIP | any | + +## Docker (推荐) ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/newfuture/ddns/${BUILD_VERSION}?style=flat-square)[![Docker Platforms](https://img.shields.io/badge/arch-amd64%20%7C%20arm64%20%7C%20arm%2Fv7%20%7C%20arm%2Fv6%20%7C%20ppc64le%20%7C%20s390x%20%7C%20386%20%7C%20mips64le-blue?logo=docker&style=flat-square)](https://hub.docker.com/r/newfuture/ddns) + +```bash +# 当前版本 +docker --name ddns -v $(pwd)/:/DDNS newfuture/ddns:${BUILD_VERSION} -h +# 最新版本(可能有缓存) +docker --name ddns -v $(pwd)/:/DDNS newfuture/ddns -h +``` + +请将 `$(pwd)/` 替换为你的配置文件夹。 + +* 命令行参数使用 `-h` +* 配置文件config.json,使用vscode等编辑会有自动提示 +* 环境变量`DDNS_XXX` + +支持源: + +* Docker官方源: [docker.io/newfuture/ddns](https://hub.docker.com/r/newfuture/ddns) +* Github镜像源: [ghcr.io/newfuture/ddns](https://github.com/NewFuture/DDNS/pkgs/container/ddns/426721813?tag=${BUILD_VERSION}) + +--- + +## 使用二进制文件 ![cross platform](https://img.shields.io/badge/system-windows_%7C%20linux_%7C%20osx-success.svg?style=flat-square) + +各平台下载/使用方式 + +* ### Windows + +1. 下载 [`ddns-windows-x64.exe`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-x64.exe) 或者 [`ddns-windows-x32.exe`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-x32.exe) 或者[`ddns-windows-arm64.exe`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-windows-arm64.exe) 保存为`ddns.exe` 运行 +2. [可选] 定时任务 下载 [`create-task.bat`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/create-task.bat) 于**相同目录**,以管理员权限运行 + +* ### Linux 系统 + +```bash +# 1. 下载ddns +# x64 版本 +curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-linux-x64 -#SLo ddns && chmod +x ddns + +# arm64 版本 +curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-linux-arm64 -#SLo ddns && chmod +x ddns + +# 2. [可选] 定时任务(当前目录): +curl -sSL https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/create-task.sh | bash +``` + +* ### Mac OSX + +```sh +# 命令行下载 +# arm64 +curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-mac-arm64.app -#SLo ddns && chmod +x ddns + +# intel x64 +curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-mac-x64.app -#SLo ddns && chmod +x ddns +``` + +## 使用PIP 安装 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns/${BUILD_VERSION}.svg?style=flat-square) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?style=flat-square) + +Pypi 安装当前版本或者更新最新版本 + +* 安装当前版本[current version]: `pip install ddns=${BUILD_VERSION}` +* 更新最新版[update latest version]: `pip install -U ddns` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1bd91133..be011623d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,7 +115,6 @@ jobs: nuitka: needs: [ python ] strategy: - fail-fast: false matrix: include: - os: windows-latest @@ -143,7 +142,7 @@ jobs: architecture: ${{ matrix.arch }} - name: remove python2 code - run: python3 .build/remove_python2.py + run: python3 .build/patch.py # Prepare build version and cert - name: Replace build version @@ -180,7 +179,7 @@ jobs: dns.he dns.huaweidns dns.callback - file-description: "DDNS Client 更新域名解析本机IP" + file-description: "DDNS Client 更新域名解析本机IP-预览版" product-name: DDNS company-name: "New Future" copyright: "https://ddns.newfuture.cc" @@ -190,10 +189,10 @@ jobs: nofollow-import-to: tkinter,unittest,pydoc,doctest,distutils,setuptools,lib2to3,test,idlelib,lzma onefile-tempdir-spec: "{CACHE_DIR}/{PRODUCT}_{VERSION}" windows-icon-from-ico: ${{ runner.os == 'Windows' && 'favicon.ico' || '' }} - linux-icon: ${{ runner.os == 'Linux' && '.build/ddns.svg' || '' }} + linux-icon: ${{ runner.os == 'Linux' && 'doc/img/ddns.svg' || '' }} static-libpython: ${{ runner.os == 'Linux' && 'yes' || 'auto' }} macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }} - macos-app-icon: ${{ runner.os == 'macOS' && '.build/ddns.svg' || '' }} + macos-app-icon: ${{ runner.os == 'macOS' && 'doc/img/ddns.svg' || '' }} - run: ./dist/ddns || test -e config.json @@ -212,12 +211,13 @@ jobs: dist/ddns nuitka-linux: + needs: [ python ] strategy: fail-fast: false matrix: os: [ ubuntu-latest, ubuntu-24.04-arm ] runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -238,7 +238,7 @@ jobs: && dnf repolist \ && dnf install -y gcc strace patchelf ccache gdb make python3-devel python3-zstandard python3-ordered-set python3 -m pip install nuitka - cd /DDNS && python3 .build/remove_python2.py && .build/nuitka.cmd + cd /DDNS && python3 .build/patch.py && .build/nuitka.sh - run: ./dist/ddns || test -e config.json - run: ./dist/ddns -h diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57780c05c..f5fb6c2be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,6 @@ jobs: permissions: packages: write env: - DOCKER_BUILD_RECORD_UPLOAD: false platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x steps: - uses: actions/checkout@v4 @@ -41,6 +40,8 @@ jobs: images: | ghcr.io/newfuture/ddns newfuture/ddns + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index - uses: docker/build-push-action@v6 with: context: . @@ -49,10 +50,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=target,\ - annotation-index.org.opencontainers.image.description=DDNS alpine container,\ - annotation-index.org.opencontainers.image.source=https://github.com/NewFuture/DDNS,\ - annotation-index.org.opencontainers.image.licenses=MIT + annotations: ${{ steps.meta.outputs.annotations }} publish-pypi: @@ -80,56 +78,140 @@ jobs: with: print-hash: true - build-binary: + publish-binary: strategy: - # fail-fast: false + fail-fast: false matrix: - os: [windows-latest, windows-11-arm, ubuntu-latest, ubuntu-24.04-arm, macos-13, macos-latest] + include: + - os: windows-latest + arch: x64 + - os: windows-latest + arch: x86 + - os: windows-11-arm + arch: arm64 + - os: ubuntu-latest + arch: x64 + - os: ubuntu-24.04-arm + arch: arm64 + - os: macos-13 + arch: x64 + - os: macos-latest + arch: arm64 runs-on: ${{ matrix.os }} timeout-minutes: 10 + permissions: + contents: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - - name: Install dependencies - run: pip install nuitka + architecture: ${{ matrix.arch }} - name: Replace build version run: sed -i.tmp -e "s#\${BUILD_VERSION}#${{ github.ref_name }}#" -e "s/\${BUILD_DATE}/$(date --iso-8601=seconds)/" run.py && rm run.py.tmp shell: bash - name: remove python2 code - run: python3 .build/remove_python2.py + run: python3 .build/patch.py - - name: setup on Linux + - name: Set up on Linux if: runner.os == 'Linux' run: | - apt-get update && apt install -y patchelf ccache - echo " --static-libpython=yes --linux-icon=.build/ddns.svg" >> .build/nuitka.cmd + sudo apt-get update && sudo apt-get install -y patchelf cp /etc/ssl/certs/ca-certificates.crt cert.pem && export SSL_CERT_FILE=${PWD}/cert.pem - - - name: setup on macOS + - name: Set up on macOS if: runner.os == 'macOS' - run: | - python3 -m pip install imageio - echo " --macos-app-name=DDNS --macos-app-icon=.build/ddns.svg" >> .build/nuitka.cmd + run: python3 -m pip install imageio - - name: Package binary - run: ./.build/nuitka.cmd + - name: test run.py + run: python3 ./run.py -h + + - name: Build Executable + uses: Nuitka/Nuitka-Action@v1.3 + with: + nuitka-version: main + script-name: run.py + mode: onefile + output-dir: dist + output-file: ddns + no-deployment-flag: self-execution + include-module: | + dns.dnspod + dns.alidns + dns.dnspod_com + dns.dnscom + dns.cloudflare + dns.he + dns.huaweidns + dns.callback + file-description: "DDNS Client 更新域名解析本机IP" + product-name: DDNS + company-name: "New Future" + copyright: "https://ddns.newfuture.cc" + assume-yes-for-downloads: true + lto: auto + python-flag: no_site,no_asserts,no_docstrings,isolated,static_hashes + nofollow-import-to: tkinter,unittest,pydoc,doctest,distutils,setuptools,lib2to3,test,idlelib,lzma + onefile-tempdir-spec: "{CACHE_DIR}/{PRODUCT}_{VERSION}" + windows-icon-from-ico: ${{ runner.os == 'Windows' && 'favicon.ico' || '' }} + linux-icon: ${{ runner.os == 'Linux' && 'doc/img/ddns.svg' || '' }} + static-libpython: ${{ runner.os == 'Linux' && 'yes' || 'auto' }} + macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }} + macos-app-icon: ${{ runner.os == 'macOS' && 'doc/img/ddns.svg' || '' }} - run: ./dist/ddns || test -e config.json - run: ./dist/ddns -h - - name: Move and rename binary with lowercase OS and arch - run: mv ./dist/ddns "./dist/ddns-$(echo ${{ runner.os }}-${{ runner.arch }} | tr '[:upper:]' '[:lower:]')" - shell: bash + - run: mv ./dist/ddns ./dist/ddns-ubuntu24.04-${{ runner.arch }} + if: runner.os == 'Linux' + - run: mv ./dist/ddns ./dist/ddns-mac-${{ runner.arch }}.app + if: runner.os == 'macOS' + - run: mv ./dist/ddns.exe ./dist/ddns-windows-${{ runner.arch }}.exe + if: runner.os == 'Windows' + - name: Upload files + run: gh release upload ${{ github.ref_name }} dist/ddns* --clobber + env: + GH_TOKEN: ${{ github.token }} - - uses: actions/upload-artifact@v4 + publish-linux-general: + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, ubuntu-24.04-arm ] + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + # Prepare build + - name: Replace build version + run: sed -i.tmp -e "s#\${BUILD_VERSION}#${{ github.ref_name }}#" -e "s/\${BUILD_DATE}/$(date --iso-8601=seconds)/" run.py && rm run.py.tmp + shell: bash + + # https://github.com/Nuitka/Nuitka/issues/2723#issuecomment-1960831891 + - name: Run the build process with CentOS Docker + uses: addnab/docker-run-action@v3 with: - name: ${{ runner.os }} - path: dist/ - retention-days: 10 + image: docker.io/centos:8 + options: -v ${{ github.workspace }}:/DDNS + run: | + cd /etc/yum.repos.d/ && sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* + dnf install -y epel-release \ + && dnf repolist \ + && dnf install -y gcc strace patchelf ccache gdb make python3-devel python3-zstandard python3-ordered-set + python3 -m pip install nuitka + cd /DDNS && python3 .build/patch.py && .build/nuitka.sh + + - run: ./dist/ddns || test -e config.json + - run: ./dist/ddns -h + - run: mv ./dist/ddns ./dist/ddns-linux-${{ runner.arch }} + - name: Upload files + run: gh release upload ${{ github.ref_name }} dist/ddns* --clobber + env: + GH_TOKEN: ${{ github.token }} github-release: runs-on: ubuntu-latest @@ -139,16 +221,13 @@ jobs: url: https://github.com/NewFuture/DDNS/releases/tag/${{ github.ref_name }} permissions: contents: write - needs: [publish-docker, publish-pypi, build-binary] + needs: [publish-docker, publish-pypi, publish-binary, publish-linux-general] steps: - uses: actions/checkout@v4 - - run: sed -i -e "s#\${BUILD_VERSION}#${{ github.ref_name }}#" .release/README.md - - uses: actions/download-artifact@v4 - with: - path: dist + - run: sed -i -e "s#\${BUILD_VERSION}#${{ github.ref_name }}#" .github/release.md - name: Upload files - run: gh release upload ${{ github.ref_name }} .release/*.sh .release/*.bat dist/*/* --clobber + run: gh release upload ${{ github.ref_name }} .release/*.sh .release/*.bat --clobber env: GH_TOKEN: ${{ github.token }} @@ -156,6 +235,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh release view ${{ github.ref_name }} --json body -q '.body' > .release/notes.md - cat .release/README.md >> .release/notes.md - gh release edit ${{ github.ref_name }} --notes-file .release/notes.md --latest --prerelease=false + gh release view ${{ github.ref_name }} --json body -q '.body' > release_notes.md + cat .github/release.md >> release_notes.md + gh release edit ${{ github.ref_name }} --notes-file release_notes.md diff --git a/.release/README.md b/.release/README.md deleted file mode 100644 index 0e984c7a7..000000000 --- a/.release/README.md +++ /dev/null @@ -1,32 +0,0 @@ -[![PyPI version](https://img.shields.io/badge/DDNS-${BUILD_VERSION}-1abc9c.svg?style=social)](https://pypi.org/project/ddns/${BUILD_VERSION}/) ![Deploy OK](https://img.shields.io/badge/release-success-brightgreen.svg?style=flat-square) - -## 使用二进制文件 ![cross platform](https://img.shields.io/badge/platform-windows_%7C%20linux_%7C%20osx-success.svg?style=flat-square) - -* Windows 下载 [ddns.exe](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns.exe) -* Ubuntu 下载 [ddns](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns) -* Mac OS X下载 [ddns-osx](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-osx) - -各平台下载/使用方式 - -* ### Windows - 1. 下载 [`ddns.exe`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns.exe) 运行 - 2. [可选] 定时任务 下载 [`create-task.bat`](https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/create-task.bat) 于**相同目录**,以管理员权限运行 -* ### Ubuntu -```bash -# 1. 下载ddns -curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns -#SLo ddns && chmod +x ddns -# 2. [可选] 定时任务(当前目录): -curl -sSL https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/create-task.sh | bash -``` -* ### Mac OSX -```sh -# 命令行下载 -curl https://github.com/NewFuture/DDNS/releases/download/${BUILD_VERSION}/ddns-osx -#SLo ddns && chmod +x ddns -``` - -## 使用PIP 安装 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns.svg?style=flat-square) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?style=flat-square) - -Pypi 安装当前版本或者更新最新版本 - -* 安装当前版本[current version]: `pip install ddns=${BUILD_VERSION}` -* 更新最新版[update latest version]: `pip install -U ddns` \ No newline at end of file diff --git a/README.md b/README.md index 35a3dbf92..dc73ad986 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# [DDNS](https://github.com/NewFuture/DDNS) [](https://ddns.newfuture.cc) +# [](https://ddns.newfuture.cc) [DDNS](https://github.com/NewFuture/DDNS) > 自动更新 DNS 解析 到本机 IP 地址,支持 ipv4 和 ipv6 以 本地(内网)IP 和 公网 IP。 > 代理模式,支持自动创建域名记录。 [![PyPI](https://img.shields.io/pypi/v/ddns.svg?label=DDNS&style=social)](https://pypi.org/project/ddns/) +[![Docker Image Version](https://img.shields.io/docker/v/newfuture/ddns?label=Docker:newfuture/ddns&sort=semver&style=social)](https://hub.docker.com/r/newfuture/ddns)[![Docker Pulls](https://img.shields.io/docker/pulls/newfuture/ddns?style=social)](https://hub.docker.com/r/newfuture/ddns) +[![PyPi version](https://img.shields.io/pypi/v/ddns.svg?style=social)](https://pypi.org/project/ddns/) [![Build Status](https://github.com/NewFuture/DDNS/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/NewFuture/DDNS/actions/workflows/build.yml) [![Publish Status](https://github.com/NewFuture/DDNS/actions/workflows/publish.yml/badge.svg)](https://github.com/NewFuture/DDNS/releases/latest) @@ -12,42 +14,43 @@ ## Features - 兼容和跨平台: - - [x] 可执行文件(无需 python 环境) - - [x] 多系统兼容 ![cross platform](https://img.shields.io/badge/platform-windows_%7C%20linux_%7C%20osx-success.svg?style=social) - - [x] python3 支持 ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns.svg?style=social)(2.x支持python2和python3) - - [x] PIP 安装 ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?style=social) - - [x] Docker 支持(@NN708) + - [Docker (@NN708)](https://hub.docker.com/r/newfuture/ddns) [![Docker Image Size](https://img.shields.io/docker/image-size/newfuture/ddns/latest?style=social)](https://hub.docker.com/r/newfuture/ddns)[![Docker Platforms](https://img.shields.io/badge/arch-amd64%20%7C%20arm64%20%7C%20arm%2Fv7%20%7C%20arm%2Fv6%20%7C%20ppc64le%20%7C%20s390x%20%7C%20386%20%7C%20mips64le-blue?logo=docker&style=flat-square)](https://hub.docker.com/r/newfuture/ddns) + - [PIP 安装 (兼容Python2)](https://pypi.org/project/ddns/) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?style=social) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns.svg?style=social) + - [二进制文件](https://github.com/NewFuture/DDNS/releases/latest) ![cross platform](https://img.shields.io/badge/system-windows_%7C%20linux_%7C%20mac-success.svg?style=social) + - 域名支持: - - [x] 多个域名支持 - - [x] 多级域名解析 - - [x] 自动创建新记录 + - 多个域名支持 + - 多级域名解析 + - 自动创建新记录 - IP 类型: - - [x] 内网 IPv4 / IPv6 - - [x] 公网 IPv4 / IPv6 (支持自定义 API) - - [x] 自定义命令(shell) - - [x] 正则选取支持(@rufengsuixing) + - 内网 IPv4 / IPv6 + - 公网 IPv4 / IPv6 (支持自定义 API) + - 自定义命令(shell) + - 正则选取支持(@rufengsuixing) - 网络代理: - - [x] http 代理支持 - - [x] 多代理自动切换 + - http 代理支持 + - 多代理自动切换 - 服务商支持: - - [x] [DNSPOD](https://www.dnspod.cn/) - - [x] [阿里 DNS](http://www.alidns.com/) - - [x] [DNS.COM](https://www.dns.com/)(@loftor-git) - - [x] [DNSPOD 国际版](https://www.dnspod.com/) - - [x] [CloudFlare](https://www.cloudflare.com/)(@tongyifan) - - [x] [HE.net](https://dns.he.net/)(@NN708) (不支持自动创建记录) - - [x] [华为云](https://huaweicloud.com/)(@cybmp3) + - [DNSPOD](https://www.dnspod.cn/) + - [阿里 DNS](http://www.alidns.com/) + - [DNS.COM](https://www.dns.com/)(@loftor-git) + - [DNSPOD 国际版](https://www.dnspod.com/) + - [CloudFlare](https://www.cloudflare.com/)(@tongyifan) + - [HE.net](https://dns.he.net/)(@NN708) (不支持自动创建记录) + - [华为云](https://huaweicloud.com/)(@cybmp3) - 其他: - - [x] 可设置定时任务 - - [x] TTL 配置支持 - - [x] 本地文件缓存(减少 API 请求) - - [x] 地址变更时触发自定义回调API(与 DDNS 功能互斥) + - 可设置定时任务 + - TTL 配置支持 + - 本地文件缓存(减少 API 请求) + - 地址变更时触发自定义回调API(与 DDNS 功能互斥) ## 使用 ### ① 安装 -根据需要选择一种方式: `二进制`版,`pip`版,`源码`运行,或者`Docker` +根据需要选择一种方式: `二进制`版,`pip`版,`源码`运行,或者`Docker`. + +推荐docker版,兼容性最佳,体积小,性能优化。 - #### pip 安装(需要 pip 或 easy_install) @@ -58,7 +61,7 @@ - Windows [ddns.exe](https://github.com/NewFuture/DDNS/releases/latest) - Linux (仅 Ubuntu 测试) [ddns](https://github.com/NewFuture/DDNS/releases/latest) - - Mac OSX [ddns-osx](https://github.com/NewFuture/DDNS/releases/latest) + - Mac OSX [ddns-mac](https://github.com/NewFuture/DDNS/releases/latest) - #### 源码运行(无任何依赖, 需 python 环境) @@ -69,7 +72,7 @@ - 使用环境变量: - ``` + ```sh docker run -d \ -e DDNS_DNS=dnspod \ -e DDNS_ID=12345 \ @@ -82,7 +85,7 @@ - 使用配置文件(docker 工作目录`/ddds/`,默认配置位置`/ddns/config.json`): - ``` + ```sh docker run -d \ -v /local/config/path/:/ddns/ \ --network host \ diff --git a/.build/ddns.svg b/doc/img/ddns.svg similarity index 100% rename from .build/ddns.svg rename to doc/img/ddns.svg diff --git a/run.py b/run.py index 94bda2c40..72f284cd0 100755 --- a/run.py +++ b/run.py @@ -6,10 +6,7 @@ @modified: rufengsuixing """ -# nuitka-project-if: os.getenv("BUILD_VERSION") is not None: -# nuitka-project: --product-version=${BUILD_VERSION} -# nuitka-project-else: -# nuitka-project: --product-version=0.0.0 +# nuitka-project: --product-version=0.0.0 from os import path, environ, name as os_name from tempfile import gettempdir diff --git a/util/config.py b/util/config.py index 2647cf70d..1aa51240e 100644 --- a/util/config.py +++ b/util/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding:utf-8 -*- -from argparse import ArgumentParser, ArgumentTypeError, Namespace, RawTextHelpFormatter # noqa: F401 +from argparse import Action, ArgumentParser, Namespace, RawTextHelpFormatter from json import load as loadjson, dump as dumpjson from os import stat, environ from logging import error, getLevelName @@ -10,7 +10,7 @@ import sys -__cli_args = {} # type: Namespace +__cli_args = Namespace() __config = {} # type: dict log_levels = ['CRITICAL', 'FATAL', 'ERROR', 'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] @@ -53,14 +53,16 @@ def init_config(description, doc, version): 'alidns', 'cloudflare', 'dnscom', 'dnspod', 'dnspod_com', 'he', 'huaweidns', 'callback']) parser.add_argument('--id', help="api ID [授权账户]") parser.add_argument('--token', help="api token or Secret key [授权访问凭证或密钥]") - parser.add_argument('--ipv4', nargs="*", + parser.add_argument('--index4', nargs="*", action=ExtendAction, + help="list to get ipv4 [IPV4 获取方式]") + parser.add_argument('--index6', nargs="*", action=ExtendAction, + help="list to get ipv6 [IPV6获取方式]") + parser.add_argument('--ipv4', nargs="*", action=ExtendAction, help="ipv4 domain list [IPV4域名列表]") - parser.add_argument('--ipv6', nargs="*", + parser.add_argument('--ipv6', nargs="*", action=ExtendAction, help="ipv6 domain list [IPV6域名列表]") - parser.add_argument('--index4', help="the way to get ipv4 [IPV4 获取方式]") - parser.add_argument('--index6', help="the way to get ipv6 [IPV6获取方式]") parser.add_argument('--ttl', type=int, help="ttl for DNS [DNS 解析 TTL 时间]") - parser.add_argument('--proxy', nargs="*", + parser.add_argument('--proxy', nargs="*", action=ExtendAction, help="https proxy [设置http 代理,多代理逐个尝试直到成功]") parser.add_argument('--cache', type=str2bool, nargs='?', const=True, help="cache flag [启用缓存,可配配置路径或开关]") @@ -148,3 +150,20 @@ def get_config(key, default=None): elif env_name.lower() in environ: # 小写环境变量 return environ.get(env_name.lower()) return default + + +class ExtendAction(Action): + """ + 兼容 Python <3.8 的 extend action + """ + + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest, None) + if items is None: + items = [] + # values 可能是单个值或列表 + if isinstance(values, list): + items.extend(values) + else: + items.append(values) + setattr(namespace, self.dest, items)