1
0
Эх сурвалжийг харах

优化docker镜像 (#459)

* 删除重复库, 减少docker体积
* 首次失败自动停止
* python3 优先
* 怎加python2检查
* Nuitka/Nuitka-Action

Co-authored-by: Copilot <[email protected]>
New Future 6 сар өмнө
parent
commit
4f82472c54

+ 12 - 9
.build/Dockerfile

@@ -1,9 +1,11 @@
-FROM alpine:latest
+FROM alpine
 WORKDIR /build
 WORKDIR /build
 COPY . .
 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 apk add py3-pip python3-dev patchelf build-base libffi-dev
 RUN pip3 install -U nuitka --break-system-packages
 RUN pip3 install -U nuitka --break-system-packages
-RUN python3 -m nuitka run.py \
+RUN python3 .build/remove_python2.py
+RUN python3 -O -m nuitka run.py \
     --mode=onefile\
     --mode=onefile\
     --output-dir=./dist\
     --output-dir=./dist\
     --no-deployment-flag=self-execution\
     --no-deployment-flag=self-execution\
@@ -11,15 +13,16 @@ RUN python3 -m nuitka run.py \
     --remove-output\
     --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\
     --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\
     --product-name=DDNS\
+    --lto=yes \
+    --onefile-tempdir-spec="{TEMP}/{PRODUCT}_{VERSION}" \
     --python-flag=no_site,no_asserts,no_docstrings,isolated,static_hashes\
     --python-flag=no_site,no_asserts,no_docstrings,isolated,static_hashes\
-    --nofollow-import-to=unittest,pydoc\
-    --onefile-tempdir-spec="{CACHE_DIR}/{PRODUCT}/{VERSION}"
-RUN mkdir bin
-RUN cp dist/ddns bin/
-RUN cp .build/entrypoint.sh bin/
+    --nofollow-import-to=tkinter,unittest,pydoc,doctest,distutils,setuptools,lib2to3,test,idlelib,lzma \
+    --noinclude-dlls=liblzma.so.* \
+    $(cat nuitka_exclude_so.txt)
+RUN mkdir docker-bin && cp dist/ddns docker-bin/ && cp .build/entrypoint.sh docker-bin/
 
 
-FROM alpine:latest
+FROM alpine
 LABEL maintainer="NN708, newfuture"
 LABEL maintainer="NN708, newfuture"
 WORKDIR /ddns
 WORKDIR /ddns
-COPY --from=0 /build/bin/* /bin/
+COPY --from=0 /build/docker-bin/* /bin/
 ENTRYPOINT [ "/bin/entrypoint.sh" ]
 ENTRYPOINT [ "/bin/entrypoint.sh" ]

+ 1 - 3
.build/entrypoint.sh

@@ -3,9 +3,7 @@
 if [ $# -eq 0 ]; then
 if [ $# -eq 0 ]; then
   printenv > /etc/environment
   printenv > /etc/environment
   echo "*/5 * * * *  cd /ddns && /bin/ddns" > /etc/crontabs/root
   echo "*/5 * * * *  cd /ddns && /bin/ddns" > /etc/crontabs/root
-  /bin/ddns
-  echo "Cron daemon will run every 5 minutes..."
-  exec crond -f
+  /bin/ddns &&  echo "Cron daemon will run every 5 minutes..." && exec crond -f
 else
 else
   first=`echo $1 | cut -c1`
   first=`echo $1 | cut -c1`
   if [ "$first" = "-" ]; then
   if [ "$first" = "-" ]; then

+ 65 - 0
.build/remove_python2.py

@@ -0,0 +1,65 @@
+#!/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()

+ 70 - 16
.github/workflows/build.yml

@@ -33,6 +33,23 @@ jobs:
       - name: check complexity and length # the GitHub editor is 127 chars wide
       - name: check complexity and length # the GitHub editor is 127 chars wide
         run: flake8 . --count --max-complexity=12 --max-line-length=127 --statistics
         run: flake8 . --count --max-complexity=12 --max-line-length=127 --statistics
 
 
+  python:
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: [ "2.7","3" ]
+    runs-on: ubuntu-22.04
+    timeout-minutes: 5
+    steps:
+      - uses: actions/checkout@v4
+      - run: sudo apt-get update && sudo apt-get install -y python${{ matrix.python-version }}
+      - name: test help command
+        run: python${{ matrix.python-version }} run.py -h
+      - name: test config generation
+        run: python${{ matrix.python-version }} run.py || test -e config.json
+      - name: test version
+        run: python${{ matrix.python-version }} run.py --version
+
   pypi:
   pypi:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 5
     timeout-minutes: 5
@@ -55,6 +72,7 @@ jobs:
           path: dist/
           path: dist/
           retention-days: 5
           retention-days: 5
 
 
+          
   pyinstaller:
   pyinstaller:
     strategy:
     strategy:
       # fail-fast: false
       # fail-fast: false
@@ -95,6 +113,7 @@ jobs:
           retention-days: 3
           retention-days: 3
 
 
   nuitka:
   nuitka:
+    needs: [ python ]
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
@@ -122,8 +141,9 @@ jobs:
         with:
         with:
           python-version: 3.x
           python-version: 3.x
           architecture: ${{ matrix.arch }}
           architecture: ${{ matrix.arch }}
-      - name: Install dependencies
-        run: python3 -m pip install -U nuitka
+
+      - name: remove python2 code
+        run:  python3 .build/remove_python2.py
 
 
       # Prepare build version and cert
       # Prepare build version and cert
       - name: Replace build version
       - name: Replace build version
@@ -133,34 +153,67 @@ jobs:
       - name: Set up on Linux
       - name: Set up on Linux
         if: runner.os == 'Linux'
         if: runner.os == 'Linux'
         run: |
         run: |
-          sudo apt-get update
-          sudo apt-get install -y patchelf
-          echo " --static-libpython=yes --linux-icon=.build/icon.png" >> .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
           cp /etc/ssl/certs/ca-certificates.crt cert.pem && export SSL_CERT_FILE=${PWD}/cert.pem
 
 
       - name: Set up on macOS
       - name: Set up on macOS
         if: runner.os == 'macOS'
         if: runner.os == 'macOS'
-        run: |
-          python3 -m pip install imageio
-          echo " --macos-app-name=DDNS --macos-app-icon=.build/icon.png" >> .build/nuitka.cmd
-
+        run: python3 -m pip install imageio
+   
       - run: python3 ./run.py -h
       - run: python3 ./run.py -h
 
 
-      - name: Package binary
-        run: ./.build/nuitka.cmd
+      - name: Build Executable
+        uses: Nuitka/Nuitka-Action@main
+        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' && '.build/icon.png' || '' }}
+          static-libpython: ${{ runner.os == 'yes' || 'auto' }}
+          macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }}
+          macos-app-icon: ${{ runner.os == 'macOS' && '.build/icon.png' || '' }}
+
 
 
       - run: ./dist/ddns || test -e config.json
       - run: ./dist/ddns || test -e config.json
       - run: ./dist/ddns -h
       - run: ./dist/ddns -h
 
 
       # Upload build result
       # Upload build result
-      - uses: actions/upload-artifact@v4
+      - name: Upload Artifacts
+        uses: actions/upload-artifact@v4
         with:
         with:
           name: ddns-${{ runner.os }}-${{ matrix.arch }}
           name: ddns-${{ runner.os }}-${{ matrix.arch }}
-          path: dist/
-          retention-days: 7
+          if-no-files-found: error
+          path: |
+            dist/*.exe
+            dist/*.bin
+            dist/*.app
+            dist/ddns
 
 
   docker:
   docker:
     if: github.event_name == 'pull_request'
     if: github.event_name == 'pull_request'
+    needs: [ python ]
     strategy:
     strategy:
       matrix:
       matrix:
         platforms: [ linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x ]
         platforms: [ linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x ]
@@ -189,7 +242,7 @@ jobs:
   preview-pypi:
   preview-pypi:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     if: github.event_name == 'push'
     if: github.event_name == 'push'
-    needs: [lint, pypi]
+    needs: [lint, pypi, python]
     timeout-minutes: 3
     timeout-minutes: 3
     environment:
     environment:
       name: preview
       name: preview
@@ -207,8 +260,9 @@ jobs:
           print-hash: true
           print-hash: true
 
 
   preview-docker:
   preview-docker:
-    runs-on: ubuntu-latest
     if: github.event_name == 'push'
     if: github.event_name == 'push'
+    needs: [lint, python]
+    runs-on: ubuntu-latest
     timeout-minutes: 120
     timeout-minutes: 120
     environment:
     environment:
       name: preview
       name: preview

+ 3 - 0
.github/workflows/publish.yml

@@ -99,6 +99,9 @@ jobs:
         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
         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
         shell: bash
 
 
+      - name: remove python2 code
+        run:  python3 .build/remove_python2.py
+
       - name: setup on Linux
       - name: setup on Linux
         if: runner.os == 'Linux'
         if: runner.os == 'Linux'
         run: |
         run: |

+ 4 - 6
dns/alidns.py

@@ -14,14 +14,12 @@ from json import loads as jsondecode
 from logging import debug, info, warning
 from logging import debug, info, warning
 from datetime import datetime
 from datetime import datetime
 
 
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode, quote_plus, quote
-except ImportError:
-    # python 3
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode, quote_plus, quote
     from urllib.parse import urlencode, quote_plus, quote
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode, quote_plus, quote
 
 
 __author__ = 'New Future'
 __author__ = 'New Future'
 # __all__ = ["request", "ID", "TOKEN", "PROXY"]
 # __all__ = ["request", "ID", "TOKEN", "PROXY"]

+ 4 - 6
dns/callback.py

@@ -10,15 +10,13 @@ from json import loads as jsondecode
 from logging import debug, info, warning
 from logging import debug, info, warning
 from time import time
 from time import time
 
 
-try:
-    # python 2
+try:  # python 3
+    from http.client import HTTPSConnection, HTTPConnection
+    from urllib.parse import urlencode, urlparse, parse_qsl
+except ImportError:  # python 2
     from httplib import HTTPSConnection, HTTPConnection
     from httplib import HTTPSConnection, HTTPConnection
     from urlparse import urlparse, parse_qsl
     from urlparse import urlparse, parse_qsl
     from urllib import urlencode
     from urllib import urlencode
-except ImportError:
-    # python 3
-    from http.client import HTTPSConnection, HTTPConnection
-    from urllib.parse import urlencode, urlparse, parse_qsl
 
 
 __author__ = '老周部落'
 __author__ = '老周部落'
 
 

+ 4 - 6
dns/cloudflare.py

@@ -9,14 +9,12 @@ https://api.cloudflare.com/#dns-records-for-a-zone-properties
 from json import loads as jsondecode, dumps as jsonencode
 from json import loads as jsondecode, dumps as jsonencode
 from logging import debug, info, warning
 from logging import debug, info, warning
 
 
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode
-except ImportError:
-    # python 3
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode
     from urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 
 __author__ = 'TongYifan'
 __author__ = 'TongYifan'
 
 

+ 4 - 7
dns/dnscom.py

@@ -13,15 +13,12 @@ from logging import debug, info, warning
 from time import mktime
 from time import mktime
 from datetime import datetime
 from datetime import datetime
 
 
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode
-except ImportError:
-    # python 3
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode
     from urllib.parse import urlencode
-
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 
 __author__ = 'Bigjin'
 __author__ = 'Bigjin'
 # __all__ = ["request", "ID", "TOKEN", "PROXY"]
 # __all__ = ["request", "ID", "TOKEN", "PROXY"]

+ 5 - 6
dns/dnspod.py

@@ -9,14 +9,13 @@ http://www.dnspod.cn/docs/domains.html
 from json import loads as jsondecode
 from json import loads as jsondecode
 from logging import debug, info, warning
 from logging import debug, info, warning
 from os import environ
 from os import environ
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode
-except ImportError:
-    # python 3
+
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode
     from urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 
 __author__ = 'New Future'
 __author__ = 'New Future'
 
 

+ 4 - 6
dns/he.py

@@ -8,14 +8,12 @@ https://dns.he.net/docs.html
 
 
 from logging import debug, info, warning
 from logging import debug, info, warning
 
 
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode
-except ImportError:
-    # python 3
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode
     from urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 
 __author__ = 'NN708'
 __author__ = 'NN708'
 
 

+ 5 - 6
dns/huaweidns.py

@@ -14,14 +14,13 @@ from json import loads as jsondecode, dumps as jsonencode
 from logging import debug, info, warning
 from logging import debug, info, warning
 from datetime import datetime
 from datetime import datetime
 
 
-try:
-    # python 2
-    from httplib import HTTPSConnection
-    from urllib import urlencode
-except ImportError:
-    # python 3
+try:  # python 3
     from http.client import HTTPSConnection
     from http.client import HTTPSConnection
     from urllib.parse import urlencode
     from urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
+
 
 
 __author__ = 'New Future'
 __author__ = 'New Future'
 BasicDateFormat = "%Y%m%dT%H%M%SZ"
 BasicDateFormat = "%Y%m%dT%H%M%SZ"

+ 1 - 1
run.py

@@ -49,7 +49,7 @@ def get_ip(ip_type, index="default"):
     # EN: Catch exceptions
     # EN: Catch exceptions
     value = None
     value = None
     try:
     try:
-        debug(f"get_ip({ip_type}, {index})")
+        debug("get_ip(%s, %s)", ip_type, index)
         if index is False:  # disabled
         if index is False:  # disabled
             return False
             return False
         elif isinstance(index, list):  # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到找到一个可以正确获取到的IP
         elif isinstance(index, list):  # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到找到一个可以正确获取到的IP

+ 3 - 5
util/ip.py

@@ -4,12 +4,10 @@ from re import compile
 from os import name as os_name, popen
 from os import name as os_name, popen
 from socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
 from socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
 from logging import debug, error
 from logging import debug, error
-try:
-    # python2
-    from urllib2 import urlopen, Request
-except ImportError:
-    # python3
+try:  # python3
     from urllib.request import urlopen, Request
     from urllib.request import urlopen, Request
+except ImportError:  # python2
+    from urllib2 import urlopen, Request
 
 
 # IPV4正则
 # IPV4正则
 IPV4_REG = r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])'
 IPV4_REG = r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])'