patch.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/env python3
  2. import sys
  3. import os
  4. import re
  5. import urllib.request
  6. import time
  7. import json
  8. from datetime import datetime, timezone
  9. ROOT = "."
  10. init_py_path = os.path.join(ROOT, "ddns", "__init__.py")
  11. # 匹配 try-except 块,去除导入前缩进,保证import顶格,删除的行用空行代替
  12. PATTERN = re.compile(
  13. r"^[ \t]*try:[^\n]*python 3[^\n]*\n" # try: # python 3
  14. r"((?:[ \t]+[^\n]*\n)+?)" # python3 导入内容
  15. r"^[ \t]*except ImportError:[^\n]*\n" # except ImportError: # python 2
  16. r"((?:[ \t]+from[^\n]*\n|[ \t]+import[^\n]*\n)*)", # except块内容
  17. re.MULTILINE,
  18. )
  19. def dedent_imports_with_blank(import_block, try_block, except_block):
  20. """
  21. 保留python3导入并去除缩进,try/except及except内容用空行代替
  22. """
  23. try_lines = try_block.count("\n")
  24. except_lines = except_block.count("\n")
  25. imports = "".join(line.lstrip() for line in import_block.splitlines(keepends=True))
  26. return ("\n" * try_lines) + imports + ("\n" * except_lines)
  27. def extract_pure_version(version_str):
  28. """
  29. 提取前4组数字并用点拼接,如 v1.2.3.beta4.5 -> 1.2.3.4
  30. """
  31. nums = re.findall(r"\d+", version_str)
  32. return ".".join(nums[:4]) if nums else "0.0.0"
  33. def update_nuitka_version(pyfile, version=None):
  34. """
  35. 读取 __version__ 并替换 nuitka-project 版本号
  36. """
  37. pure_version = extract_pure_version(version)
  38. with open(pyfile, "r", encoding="utf-8") as f:
  39. content = f.read()
  40. # 替换 nuitka-project 行
  41. new_content, n = re.subn(r"(# nuitka-project: --product-version=)[^\n]*", r"\g<1>" + pure_version, content)
  42. if n > 0:
  43. with open(pyfile, "w", encoding="utf-8") as f:
  44. f.write(new_content)
  45. print(f"update nuitka-project version: {pure_version} in {pyfile}")
  46. return True
  47. return False
  48. def add_nuitka_file_description(pyfile):
  49. """
  50. 添加 --file-description 配置,使用 __description__ 变量的值
  51. """
  52. with open(init_py_path, "r", encoding="utf-8") as f:
  53. content = f.read()
  54. # 提取 __description__ 变量的值
  55. desc_match = re.search(r'__description__\s*=\s*[\'"]([^\'"]+)[\'"]', content)
  56. if not desc_match:
  57. print(f"No __description__ found in {init_py_path}")
  58. return False
  59. description = desc_match.group(1)
  60. description_line = f'\n# nuitka-project: --file-description="{description}"\n'
  61. with open(pyfile, "a", encoding="utf-8") as f:
  62. f.write(description_line)
  63. return True
  64. def add_nuitka_include_modules(pyfile):
  65. """
  66. 读取 dns 目录下的所有 Python 模块,并添加到 run.py 末尾
  67. """
  68. dns_dir = os.path.join(ROOT, "ddns/provider")
  69. if not os.path.exists(dns_dir):
  70. print(f"DNS directory not found: {dns_dir}")
  71. return False
  72. # 获取所有 Python 模块文件
  73. modules = []
  74. for filename in os.listdir(dns_dir):
  75. if filename.endswith(".py") and filename != "__init__.py":
  76. module_name = filename[:-3] # 去掉 .py 扩展名
  77. modules.append(f"ddns.provider.{module_name}")
  78. if not modules:
  79. print("No DNS modules found")
  80. return False
  81. # 直接在文件末尾追加配置行
  82. with open(pyfile, "a", encoding="utf-8") as f:
  83. for module in sorted(modules):
  84. f.write(f"# nuitka-project: --include-module={module}\n")
  85. print(f'Added {len(modules)} DNS modules to {pyfile}: {", ".join(modules)}')
  86. return True
  87. def remove_python2_compatibility(pyfile):
  88. """
  89. 自动将所有 try-except python2/3 兼容导入替换为 python3 only 导入,并显示处理日志
  90. 删除指定文件中的 python2 兼容代码,逐行处理
  91. """
  92. with open(pyfile, "r", encoding="utf-8") as f:
  93. lines = f.readlines()
  94. new_lines = []
  95. i = 0
  96. changed = False
  97. while i < len(lines):
  98. line = lines[i]
  99. # 匹配 "try: # python3" 或 "try: # python 3"
  100. if re.match(r"^[ \t]*try:[^\n]*python ?3", line):
  101. try_block = []
  102. except_block = []
  103. i += 1
  104. # 收集try块内容
  105. while i < len(lines) and lines[i].startswith((" ", "\t")):
  106. try_block.append(lines[i].lstrip())
  107. i += 1
  108. # 跳过空行
  109. while i < len(lines) and lines[i].strip() == "":
  110. i += 1
  111. # 检查是否存在except块 (不检查具体错误类型,但必须包含python2或python 2)
  112. if i < len(lines) and re.match(r"^[ \t]*except[^\n]*python ?2", lines[i]):
  113. i += 1
  114. # 收集except块内容
  115. except_block = []
  116. while i < len(lines) and lines[i].startswith((" ", "\t")):
  117. except_block.append(lines[i])
  118. i += 1
  119. # 添加try块内容,except块用空行替代
  120. new_lines.extend(["\n"] + try_block + ["\n"] * (len(except_block) + 1))
  121. changed = True
  122. else:
  123. # 没有except块,原样保留
  124. new_lines.append(line)
  125. new_lines.extend(try_block)
  126. else:
  127. new_lines.append(line)
  128. i += 1
  129. if changed:
  130. with open(pyfile, "w", encoding="utf-8") as f:
  131. f.writelines(new_lines)
  132. print(f"Removed python2 compatibility from {pyfile}")
  133. def get_latest_tag():
  134. url = "https://api.github.com/repos/NewFuture/DDNS/tags?per_page=1"
  135. try:
  136. with urllib.request.urlopen(url) as response:
  137. data = json.load(response)
  138. if data and isinstance(data, list):
  139. return data[0]["name"] # 获取第一个 tag 的 name
  140. except Exception as e:
  141. print("Error fetching tag:", e)
  142. return None
  143. def normalize_tag(tag: str) -> str:
  144. v = tag.lower().lstrip("v")
  145. v = re.sub(r"-beta(\d*)", r"b\1", v)
  146. v = re.sub(r"-alpha(\d*)", r"a\1", v)
  147. v = re.sub(r"-rc(\d*)", r"rc\1", v)
  148. return v
  149. def ten_minute_bucket_id():
  150. epoch_minutes = int(time.time() // 60) # 当前时间(分钟级)
  151. bucket = epoch_minutes // 10 # 每10分钟为一个 bucket
  152. return bucket % 65536 # 限制在 0~65535 (2**16)
  153. def generate_version():
  154. ref = os.environ.get("GITHUB_REF_NAME", "")
  155. if re.match(r"^v\d+\.\d+", ref):
  156. return normalize_tag(ref)
  157. base = "4.0.0"
  158. suffix = ten_minute_bucket_id()
  159. if ref == "master" or ref == "main":
  160. tag = get_latest_tag()
  161. if tag:
  162. base = normalize_tag(tag)
  163. return f"{base}.dev{suffix}"
  164. def replace_version_and_date(pyfile: str, version: str, date_str: str):
  165. with open(pyfile, "r", encoding="utf-8") as f:
  166. text = f.read()
  167. text = text.replace("${BUILD_VERSION}", version)
  168. text = text.replace("${BUILD_DATE}", date_str)
  169. if text is not None:
  170. with open(pyfile, "w", encoding="utf-8") as f:
  171. f.write(text)
  172. print(f"Updated {pyfile}: version={version}, date={date_str}")
  173. else:
  174. exit(1)
  175. def main():
  176. """
  177. 遍历所有py文件并替换兼容导入,同时更新nuitka版本号
  178. """
  179. if len(sys.argv) > 1 and sys.argv[1].lower() != "version":
  180. print(f"unknown arguments: {sys.argv}")
  181. exit(1)
  182. version = generate_version()
  183. date_str = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
  184. print(f"Version: {version}")
  185. print(f"Date: {date_str}")
  186. # 修改__init__.py 中的 __version__
  187. replace_version_and_date(init_py_path, version, date_str)
  188. if len(sys.argv) > 1 and sys.argv[1].lower() == "version":
  189. # python version only
  190. exit(0)
  191. run_py_path = os.path.join(ROOT, "run.py")
  192. update_nuitka_version(run_py_path, version)
  193. add_nuitka_file_description(run_py_path)
  194. # add_nuitka_include_modules(run_py_path)
  195. changed_files = 0
  196. for dirpath, _, filenames in os.walk(ROOT):
  197. for fname in filenames:
  198. if fname.endswith(".py"):
  199. fpath = os.path.join(dirpath, fname)
  200. remove_python2_compatibility(fpath)
  201. changed_files += 1
  202. print("done")
  203. print(f"Total processed files: {changed_files}")
  204. if __name__ == "__main__":
  205. main()