Browse Source

优化docker镜像 (#459)

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

Co-authored-by: Copilot <[email protected]>
New Future 6 months ago
parent
commit
4f82472c54
14 changed files with 185 additions and 77 deletions
  1. 12 9
      .build/Dockerfile
  2. 1 3
      .build/entrypoint.sh
  3. 65 0
      .build/remove_python2.py
  4. 70 16
      .github/workflows/build.yml
  5. 3 0
      .github/workflows/publish.yml
  6. 4 6
      dns/alidns.py
  7. 4 6
      dns/callback.py
  8. 4 6
      dns/cloudflare.py
  9. 4 7
      dns/dnscom.py
  10. 5 6
      dns/dnspod.py
  11. 4 6
      dns/he.py
  12. 5 6
      dns/huaweidns.py
  13. 1 1
      run.py
  14. 3 5
      util/ip.py

+ 12 - 9
.build/Dockerfile

@@ -1,9 +1,11 @@
-FROM alpine:latest
+FROM alpine
 WORKDIR /build
 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 -m nuitka run.py \
+RUN python3 .build/remove_python2.py
+RUN python3 -O -m nuitka run.py \
     --mode=onefile\
     --output-dir=./dist\
     --no-deployment-flag=self-execution\
@@ -11,15 +13,16 @@ RUN python3 -m nuitka run.py \
     --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\
+    --lto=yes \
+    --onefile-tempdir-spec="{TEMP}/{PRODUCT}_{VERSION}" \
     --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"
 WORKDIR /ddns
-COPY --from=0 /build/bin/* /bin/
+COPY --from=0 /build/docker-bin/* /bin/
 ENTRYPOINT [ "/bin/entrypoint.sh" ]

+ 1 - 3
.build/entrypoint.sh

@@ -3,9 +3,7 @@
 if [ $# -eq 0 ]; then
   printenv > /etc/environment
   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
   first=`echo $1 | cut -c1`
   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
         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:
     runs-on: ubuntu-latest
     timeout-minutes: 5
@@ -55,6 +72,7 @@ jobs:
           path: dist/
           retention-days: 5
 
+          
   pyinstaller:
     strategy:
       # fail-fast: false
@@ -95,6 +113,7 @@ jobs:
           retention-days: 3
 
   nuitka:
+    needs: [ python ]
     strategy:
       fail-fast: false
       matrix:
@@ -122,8 +141,9 @@ jobs:
         with:
           python-version: 3.x
           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
       - name: Replace build version
@@ -133,34 +153,67 @@ jobs:
       - name: Set up on Linux
         if: runner.os == 'Linux'
         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
 
       - name: Set up on 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
 
-      - 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 -h
 
       # Upload build result
-      - uses: actions/upload-artifact@v4
+      - name: Upload Artifacts
+        uses: actions/upload-artifact@v4
         with:
           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:
     if: github.event_name == 'pull_request'
+    needs: [ python ]
     strategy:
       matrix:
         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:
     runs-on: ubuntu-latest
     if: github.event_name == 'push'
-    needs: [lint, pypi]
+    needs: [lint, pypi, python]
     timeout-minutes: 3
     environment:
       name: preview
@@ -207,8 +260,9 @@ jobs:
           print-hash: true
 
   preview-docker:
-    runs-on: ubuntu-latest
     if: github.event_name == 'push'
+    needs: [lint, python]
+    runs-on: ubuntu-latest
     timeout-minutes: 120
     environment:
       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
         shell: bash
 
+      - name: remove python2 code
+        run:  python3 .build/remove_python2.py
+
       - name: setup on Linux
         if: runner.os == 'Linux'
         run: |

+ 4 - 6
dns/alidns.py

@@ -14,14 +14,12 @@ from json import loads as jsondecode
 from logging import debug, info, warning
 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 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'
 # __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 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 urlparse import urlparse, parse_qsl
     from urllib import urlencode
-except ImportError:
-    # python 3
-    from http.client import HTTPSConnection, HTTPConnection
-    from urllib.parse import urlencode, urlparse, parse_qsl
 
 __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 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 urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 __author__ = 'TongYifan'
 

+ 4 - 7
dns/dnscom.py

@@ -13,15 +13,12 @@ from logging import debug, info, warning
 from time import mktime
 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 urllib.parse import urlencode
-
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 __author__ = 'Bigjin'
 # __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 logging import debug, info, warning
 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 urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 __author__ = 'New Future'
 

+ 4 - 6
dns/he.py

@@ -8,14 +8,12 @@ https://dns.he.net/docs.html
 
 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 urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
 
 __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 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 urllib.parse import urlencode
+except ImportError:  # python 2
+    from httplib import HTTPSConnection
+    from urllib import urlencode
+
 
 __author__ = 'New Future'
 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
     value = None
     try:
-        debug(f"get_ip({ip_type}, {index})")
+        debug("get_ip(%s, %s)", ip_type, index)
         if index is False:  # disabled
             return False
         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 socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
 from logging import debug, error
-try:
-    # python2
-    from urllib2 import urlopen, Request
-except ImportError:
-    # python3
+try:  # python3
     from urllib.request import urlopen, Request
+except ImportError:  # python2
+    from urllib2 import urlopen, Request
 
 # 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])'