build.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. #!/usr/bin/env python3
  2. """
  3. 统一构建脚本 — 合并 build_v6~v11 为单入口。
  4. 用法:
  5. python build.py # 基于最新 jar 自动递增版本号
  6. python build.py --version 12 # 指定输出版本号
  7. python build.py --base path/to/base.jar # 指定基底 jar
  8. 功能:
  9. 1. 收集 lib/ 下所有 jar 作为 classpath(首次运行自动从基底 jar 解压)
  10. 2. 用 javac 编译 src/main/java 下全部源码
  11. 3. 以基底 fat jar 为模板,替换 BOOT-INF/classes 下的 class 和资源文件,
  12. 并补打基底中不存在的新增类 / 资源
  13. 4. 输出 target/resource-platform-v{N}.jar
  14. """
  15. import zipfile, os, subprocess, glob, sys, shutil, argparse, re
  16. # ── 路径常量 ──────────────────────────────────────────────
  17. BASE = os.path.dirname(os.path.abspath(__file__))
  18. SRC = os.path.join(BASE, "src", "main", "java")
  19. RES = os.path.join(BASE, "src", "main", "resources")
  20. OUT = os.path.join(BASE, "build_tmp_classes")
  21. LIB = os.path.join(BASE, "lib")
  22. TARGET = os.path.join(BASE, "target")
  23. JAVAC = r"D:\Program Files\Java\jdk-17\bin\javac.exe"
  24. LOMBOK = r"C:\Users\BXD-XTYW\.m2\repository\org\projectlombok\lombok\1.18.32\lombok-1.18.32.jar"
  25. # ── 辅助函数 ──────────────────────────────────────────────
  26. def find_latest_version():
  27. """扫描 target/ 目录,返回已有的最高版本号;无则返回 0。"""
  28. versions = []
  29. pattern = re.compile(r"resource-platform-v(\d+)\.jar$")
  30. if os.path.isdir(TARGET):
  31. for f in os.listdir(TARGET):
  32. m = pattern.match(f)
  33. if m:
  34. versions.append(int(m.group(1)))
  35. return max(versions) if versions else 0
  36. def find_base_jar(current_version):
  37. """
  38. 按优先级查找基底 jar:
  39. 1. target/resource-platform-v{N}.jar(最高版本)
  40. 2. target/resource-platform.jar(Maven 产出,无版本号)
  41. """
  42. if current_version > 0:
  43. versioned = os.path.join(TARGET, f"resource-platform-v{current_version}.jar")
  44. if os.path.isfile(versioned):
  45. return versioned
  46. # 回退到 Maven 产出的无版本号 jar
  47. maven_jar = os.path.join(TARGET, "resource-platform.jar")
  48. if os.path.isfile(maven_jar):
  49. return maven_jar
  50. return None
  51. def extract_libs_from_jar(jar_path):
  52. """从 fat jar 解压 BOOT-INF/lib/*.jar 到 lib/ 目录。"""
  53. os.makedirs(LIB, exist_ok=True)
  54. with zipfile.ZipFile(jar_path) as z:
  55. for name in z.namelist():
  56. if name.startswith("BOOT-INF/lib/") and name.endswith(".jar"):
  57. out = os.path.join(LIB, os.path.basename(name))
  58. with open(out, "wb") as f:
  59. f.write(z.read(name))
  60. def ensure_lombok():
  61. """确保 lombok jar 存在于 lib/ 目录。"""
  62. dst = os.path.join(LIB, "lombok-1.18.32.jar")
  63. if not os.path.exists(dst) and os.path.exists(LOMBOK):
  64. shutil.copy(LOMBOK, dst)
  65. def collect_classpath():
  66. """收集 lib/ 下所有 jar,拼成 classpath 字符串。"""
  67. dep_jars = sorted(glob.glob(os.path.join(LIB, "*.jar")))
  68. print(f"classpath entries: {len(dep_jars)}")
  69. return ";".join(dep_jars)
  70. def compile_sources(cp):
  71. """编译 src/main/java 下全部 .java 到 build_tmp_classes/。"""
  72. sources = []
  73. for root, _, files in os.walk(SRC):
  74. for f in files:
  75. if f.endswith(".java"):
  76. sources.append(os.path.join(root, f))
  77. print(f"source files: {len(sources)}")
  78. os.makedirs(OUT, exist_ok=True)
  79. cmd = [JAVAC, "-parameters", "-encoding", "UTF-8", "-cp", cp, "-d", OUT] + sources
  80. r = subprocess.run(cmd, capture_output=True)
  81. print(f"javac returncode: {r.returncode}")
  82. if r.returncode != 0:
  83. err = (r.stderr or b"").decode("utf-8", "replace")
  84. print("=== STDERR (first 60 lines) ===")
  85. print("\n".join(err.splitlines()[:60]))
  86. sys.exit(1)
  87. class_count = len(glob.glob(os.path.join(OUT, "**", "*.class"), recursive=True))
  88. print(f"compiled classes: {class_count}")
  89. return class_count
  90. def repackage(base_jar, dst_jar):
  91. """
  92. 以 base_jar 为基底,替换 / 补打 class 和资源文件,生成 dst_jar。
  93. 逻辑合并自 build_v7(资源注入)+ build_v8(新增类补打)。
  94. """
  95. with zipfile.ZipFile(base_jar) as zin:
  96. infos = zin.infolist()
  97. all_names = set(zin.namelist())
  98. def local_for(entry_name):
  99. # 已编译 class: 优先用本地编译产物
  100. if entry_name.startswith("BOOT-INF/classes/") and entry_name.endswith(".class"):
  101. rel = entry_name[len("BOOT-INF/classes/"):]
  102. local = os.path.join(OUT, rel)
  103. if os.path.isfile(local):
  104. return local
  105. return None
  106. # 资源文件: 用 src/main/resources 中的版本
  107. if entry_name.startswith("BOOT-INF/classes/"):
  108. rel = entry_name[len("BOOT-INF/classes/"):]
  109. local = os.path.join(RES, rel)
  110. if os.path.isfile(local):
  111. return local
  112. return None
  113. manifest_name = "META-INF/MANIFEST.MF"
  114. # 收集 src/main/resources 中新增的、基底 jar 里没有的资源条目
  115. extra_resources = []
  116. for root, _, files in os.walk(RES):
  117. for f in files:
  118. full = os.path.join(root, f)
  119. rel = os.path.relpath(full, RES).replace("\\", "/")
  120. entry = "BOOT-INF/classes/" + rel
  121. if entry not in all_names:
  122. extra_resources.append((entry, full))
  123. # 收集本地编译产物中新增的、基底 jar 里不存在的 class
  124. extra_classes = []
  125. for root, _, files in os.walk(OUT):
  126. for f in files:
  127. if f.endswith(".class"):
  128. full = os.path.join(root, f)
  129. rel = os.path.relpath(full, OUT).replace("\\", "/")
  130. entry = "BOOT-INF/classes/" + rel
  131. if entry not in all_names:
  132. extra_classes.append((entry, full))
  133. with zipfile.ZipFile(dst_jar, "w", compression=zipfile.ZIP_DEFLATED) as zout:
  134. # MANIFEST 必须第一个写入
  135. mi = next(i for i in infos if i.filename == manifest_name)
  136. zout.writestr(mi, zin.read(manifest_name))
  137. for i in infos:
  138. if i.filename == manifest_name:
  139. continue
  140. local = local_for(i.filename)
  141. if local:
  142. with open(local, "rb") as lf:
  143. data = lf.read()
  144. zi = zipfile.ZipInfo(i.filename)
  145. zi.external_attr = i.external_attr
  146. zi.date_time = i.date_time
  147. zout.writestr(zi, data)
  148. else:
  149. zout.writestr(i, zin.read(i.filename))
  150. # 补打新增资源
  151. for entry, full in extra_resources:
  152. with open(full, "rb") as lf:
  153. data = lf.read()
  154. zout.writestr(zipfile.ZipInfo(entry), data)
  155. # 补打新增 class
  156. for entry, full in extra_classes:
  157. with open(full, "rb") as lf:
  158. data = lf.read()
  159. zout.writestr(zipfile.ZipInfo(entry), data)
  160. # 校验
  161. zm = zipfile.ZipFile(dst_jar)
  162. print(f"manifest first: {zm.namelist()[0] == manifest_name}")
  163. print(f"extra resources injected: {len(extra_resources)}")
  164. print(f"extra classes injected: {len(extra_classes)}")
  165. return zm
  166. # ── 主流程 ────────────────────────────────────────────────
  167. def main():
  168. parser = argparse.ArgumentParser(description="统一构建脚本")
  169. parser.add_argument("--version", type=int, help="指定输出版本号(默认自动递增)")
  170. parser.add_argument("--base", help="指定基底 jar 路径(默认使用最新版本)")
  171. args = parser.parse_args()
  172. # 1. 确定版本号
  173. latest = find_latest_version()
  174. if args.version:
  175. new_version = args.version
  176. else:
  177. new_version = latest + 1
  178. # 2. 确定基底 jar
  179. if args.base:
  180. base_jar = args.base
  181. else:
  182. base_jar = find_base_jar(latest)
  183. if not base_jar or not os.path.isfile(base_jar):
  184. print(f"ERROR: 基底 jar 不存在: {base_jar}")
  185. print("请先通过 'mvn clean package -DskipTests' 生成初始 jar,")
  186. print("或使用 --base 参数指定基底 jar 路径。")
  187. sys.exit(1)
  188. dst_jar = os.path.join(TARGET, f"resource-platform-v{new_version}.jar")
  189. print(f"base jar: {base_jar}")
  190. print(f"output: {dst_jar}")
  191. print(f"version: {new_version}")
  192. print()
  193. # 3. 准备 classpath
  194. os.makedirs(LIB, exist_ok=True)
  195. if not glob.glob(os.path.join(LIB, "*.jar")):
  196. print("lib/ 为空, 从基底 jar 解压依赖...")
  197. extract_libs_from_jar(base_jar)
  198. ensure_lombok()
  199. cp = collect_classpath()
  200. print()
  201. # 4. 编译
  202. compile_sources(cp)
  203. print()
  204. # 5. 重打包
  205. zm = repackage(base_jar, dst_jar)
  206. print()
  207. print(f"built: {dst_jar}")
  208. print(f"exists: {os.path.exists(dst_jar)}")
  209. if __name__ == "__main__":
  210. main()