From 8d9a757ef5feee4266d1464743813548ced3cf43 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 13 May 2026 11:36:13 +0800 Subject: [PATCH] feat: initial EcoArk HarmonyOS wheel pipeline skeleton - wheel_pipeline.py: CLI entry point for repack/rebuild flow - pipeline/detect.py: detect pure vs native extension wheels - pipeline/repack.py: repack pure-Python wheels with harmonyos_arm64 tag - pipeline/rebuild.py: native wheel rebuild entry (stub for OHOS NDK) - pipeline/cli.py: argument parsing and orchestration - Makefile: convenience targets (repack, rebuild, clean) - README.md: documentation with usage and limitations - examples/gen_test_wheels.py: test wheel generator - output/: output directory for generated wheels --- Makefile | 32 +++++++ README.md | 83 ++++++++++++++++++ examples/gen_test_wheels.py | 80 +++++++++++++++++ output/.gitkeep | 0 pipeline/__init__.py | 0 pipeline/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 143 bytes pipeline/__pycache__/cli.cpython-312.pyc | Bin 0 -> 3937 bytes pipeline/__pycache__/detect.cpython-312.pyc | Bin 0 -> 1908 bytes pipeline/__pycache__/rebuild.cpython-312.pyc | Bin 0 -> 3099 bytes pipeline/__pycache__/repack.cpython-312.pyc | Bin 0 -> 1722 bytes pipeline/cli.py | 63 +++++++++++++ pipeline/detect.py | 43 +++++++++ pipeline/rebuild.py | 46 ++++++++++ pipeline/repack.py | 30 +++++++ wheel_pipeline.py | 9 ++ 15 files changed, 386 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/gen_test_wheels.py create mode 100644 output/.gitkeep create mode 100644 pipeline/__init__.py create mode 100644 pipeline/__pycache__/__init__.cpython-312.pyc create mode 100644 pipeline/__pycache__/cli.cpython-312.pyc create mode 100644 pipeline/__pycache__/detect.cpython-312.pyc create mode 100644 pipeline/__pycache__/rebuild.cpython-312.pyc create mode 100644 pipeline/__pycache__/repack.cpython-312.pyc create mode 100644 pipeline/cli.py create mode 100644 pipeline/detect.py create mode 100644 pipeline/rebuild.py create mode 100644 pipeline/repack.py create mode 100755 wheel_pipeline.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9dd1f80 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +SHELL := /bin/bash + +INPUT ?= examples/sample.whl +OUTPUT ?= output + +.PHONY: help repack rebuild native clean + +help: + @echo "EcoArk HarmonyOS Wheel Pipeline" + @echo "" + @echo "Usage:" + @echo " make repack INPUT=path/to/pkg.whl — Repack a pure-Python wheel for HarmonyOS" + @echo " make rebuild INPUT=path/to/pkg.whl — Trigger native rebuild entry (stub)" + @echo " make clean — Remove output/" + @echo "" + @echo "Examples:" + @echo " make repack INPUT=some_package.whl" + @echo " make rebuild INPUT=some_native.whl" + +repack: + python3 wheel_pipeline.py "$(INPUT)" --output-dir "$(OUTPUT)" --force-repack + +rebuild: + python3 wheel_pipeline.py "$(INPUT)" --output-dir "$(OUTPUT)" --target-os harmonyos --arch arm64 + +native: + python3 wheel_pipeline.py "$(INPUT)" --output-dir "$(OUTPUT)" --target-os harmonyos --arch arm64 + +clean: + rm -rf "$(OUTPUT)" + mkdir -p "$(OUTPUT)" + touch "$(OUTPUT)/.gitkeep" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4509a7 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# EcoArk HarmonyOS Wheel Pipeline + +为 **EcoArk / HarmonyOS Python 环境** 准备的 wheel 重打包与重编译流水线。 + +## 用途 + +- 接收一个 `.whl` 文件作为输入 +- 自动检测是 **纯 Python wheel** 还是 **含 native 扩展的 wheel** +- 纯 Python wheel → 直接重打包为 `harmonyos_arm64` 兼容标签,安装即用 +- Native wheel → 预留 HarmonyOS/arm64 交叉编译入口(当前为骨架,可扩展) + +## 目录结构 + +``` +ecoarkpywhl/ +├── wheel_pipeline.py # CLI 入口(最简调用) +├── pipeline/ +│ ├── cli.py # 参数解析 & 主流程 +│ ├── detect.py # 检测 wheel 类型(纯 Python / native) +│ ├── repack.py # 纯 Python wheel 重打包 +│ └── rebuild.py # Native wheel 重编译入口(骨架) +├── Makefile # 常用命令 +├── output/ # 输出目录(生成文件放这里) +└── README.md +``` + +## 快速开始 + +```bash +# 纯 Python wheel → 重打包 +python3 wheel_pipeline.py some_pure_package.whl + +# 指定输出目录 +python3 wheel_pipeline.py some_pure_package.whl --output-dir /tmp/wheels + +# Native wheel → 触发重编译入口(当前只复制 + 写 stub) +python3 wheel_pipeline.py some_native_package.whl +``` + +或通过 Makefile: + +```bash +make repack INPUT=some_package.whl +make rebuild INPUT=some_native_package.whl +``` + +## 限制 + +| 场景 | 状态 | +|------|------| +| 纯 Python wheel → 重打包安装 | ✅ 支持 | +| Pure Python → 重新打 tag | ✅ 支持 | +| Native wheel → HarmonyOS 重编译 | 🚧 预留入口,需接入 OHOS NDK 工具链 | +| 依赖复杂的 native 包(numpy/scipy 等) | 🚧 需要对应依赖也移植到 HarmonyOS | + +### 纯 Python wheel + +纯 Python wheel(文件名中包含 `py3-none-any` 等标签,且不含 `.so`/`.pyd` 文件)可以直接重打包为 `harmonyos_arm64` 标签,在 HarmonyOS Python 环境中安装运行。 + +```bash +python3 wheel_pipeline.py mypackage.whl -o output/ +# → output/mypackage-1.0.0-py3-none-harmonyos_arm64.whl +``` + +### Native wheel + +含 C/C++/Rust 等 native 扩展的 wheel 需要: + +1. HarmonyOS SDK / OHOS NDK +2. 对应架构(arm64)的交叉编译工具链 +3. Python 头文件与目标 Python 版本匹配 +4. 所有 C 依赖也已移植到 HarmonyOS + +当前版本仅输出 stub,待后续完善。 + +## 依赖 + +- Python ≥ 3.8(标准库,无需额外安装包) +- 对 native 包:HarmonyOS 工具链(外部,不在本仓库内) + +## 许可证 + +本项目许可证见仓库根目录。 diff --git a/examples/gen_test_wheels.py b/examples/gen_test_wheels.py new file mode 100644 index 0000000..0dfad07 --- /dev/null +++ b/examples/gen_test_wheels.py @@ -0,0 +1,80 @@ +import zipfile +import os +import json + +EXAMPLES_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def make_pure_wheel(name, version, out_dir): + dist_info = f"{name}-{version}.dist-info" + os.makedirs(f"/tmp/{dist_info}", exist_ok=True) + + metadata = f"""Metadata-Version: 2.1 +Name: {name} +Version: {version} +Summary: Test pure-Python wheel +""" + + with open(f"/tmp/{dist_info}/METADATA", "w") as f: + f.write(metadata) + with open(f"/tmp/{dist_info}/WHEEL", "w") as f: + f.write("Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n") + with open(f"/tmp/{dist_info}/RECORD", "w") as f: + f.write("") + + wheel_name = f"{name}-{version}-py3-none-any.whl" + wheel_path = os.path.join(out_dir, wheel_name) + + with zipfile.ZipFile(wheel_path, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(f"/tmp/{dist_info}/METADATA", f"{dist_info}/METADATA") + zf.write(f"/tmp/{dist_info}/WHEEL", f"{dist_info}/WHEEL") + zf.write(f"/tmp/{dist_info}/RECORD", f"{dist_info}/RECORD") + pkg_dir = f"{name}" + zf.writestr(f"{pkg_dir}/__init__.py", "def hello(): return 'hello'\n") + + for f in os.listdir(f"/tmp/{dist_info}"): + os.remove(f"/tmp/{dist_info}/{f}") + os.rmdir(f"/tmp/{dist_info}") + + return wheel_path + + +def make_native_wheel(name, version, out_dir): + dist_info = f"{name}-{version}.dist-info" + os.makedirs(f"/tmp/{dist_info}", exist_ok=True) + + metadata = f"""Metadata-Version: 2.1 +Name: {name} +Version: {version} +Summary: Test native wheel +""" + + with open(f"/tmp/{dist_info}/METADATA", "w") as f: + f.write(metadata) + with open(f"/tmp/{dist_info}/WHEEL", "w") as f: + f.write("Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: false\nTag: cp38-cp38-manylinux_2_17_x86_64\n") + with open(f"/tmp/{dist_info}/RECORD", "w") as f: + f.write("") + + wheel_name = f"{name}-{version}-cp38-cp38-manylinux_2_17_x86_64.whl" + wheel_path = os.path.join(out_dir, wheel_name) + + with zipfile.ZipFile(wheel_path, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(f"/tmp/{dist_info}/METADATA", f"{dist_info}/METADATA") + zf.write(f"/tmp/{dist_info}/WHEEL", f"{dist_info}/WHEEL") + zf.write(f"/tmp/{dist_info}/RECORD", f"{dist_info}/RECORD") + zf.writestr(f"{name}/__init__.py", "def hello(): return 'hello'\n") + zf.writestr(f"{name}/_native.cpython-38-x86_64-linux-gnu.so", b"\x7fELF...fake") + + for f in os.listdir(f"/tmp/{dist_info}"): + os.remove(f"/tmp/{dist_info}/{f}") + os.rmdir(f"/tmp/{dist_info}") + + return wheel_path + + +if __name__ == "__main__": + out = EXAMPLES_DIR + pure = make_pure_wheel("ecoark_demo", "0.1.0", out) + native = make_native_wheel("ecoark_native_demo", "0.1.0", out) + print(json.dumps({"pure": pure, "native": native}, indent=2)) diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pipeline/__init__.py b/pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pipeline/__pycache__/__init__.cpython-312.pyc b/pipeline/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37d8e7036b05722ea556065baf43176fadccc742 GIT binary patch literal 143 zcmX@j%ge<81V2A8XMyO)AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdrKg{fpPQ;*P?Vou zl$fiZnw+0llwD9+o{^(pkXewLlbM&QA0MBYmst`YuUAlci^C>2KczG$)vkyYsGSjr Qi$RQ!%#4hTMa)1J0M>~i@Bjb+ literal 0 HcmV?d00001 diff --git a/pipeline/__pycache__/cli.cpython-312.pyc b/pipeline/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4abf1d3ce875eadde5289dedcf41ccd143141b12 GIT binary patch literal 3937 zcmcInT}&I<6`rxjfX3+1L({B^$Dh8ihogq$PAW+HGkoYw+CQF}BCO zGY;4_T2^Y+M5+`bRSK)Bs?(P`0%`Nu*GiSD{z$dK2s+^*Z6j6tmNt3nQ_r38Faz6c zt4ceP=bn4+chCR1bNz=vz(>I|@DF#|57ght#Clv!;^hnw4=IU~=nOS$j`S>T%w4lC z$tAfn?kqFQWIeNKQ&Qj;^hR)tIVT->pX<>nszwfRxXz5GM(m}D<-|{(u}$wv$X&7>**t(=au)Pt&VGL z*1B}8?D{G7GnW>!IO(uO+2h7>I|o|3#ZMo#D0`H;teiqzUFqZYibY9pScn65bbonv zXm41$)7=(jk9J&^-O`D&`woRC9P#NX1DCk*Wkt!Ms~2<$mM$wwH040_0iyp45d(l2 z{11pPu9sdr9u@_aCs?#ST2?JH8gFsa{T5}9Wkwp>IB8c6ST#(nTB21yR0f?Mw6PYo z>?wQH3l48jS=t~5!xkzHC)VMo>?$*su4UFCAyQ^Z-W+o5${(@0MPkv&VX1I3o^i-} z)5iaw)|@_UP0^X#s6&3#vLzj}D0?K{g1u*Kjzs}?%%bgaBW9h82VxUHY>mU$Gqx>7 zDl$4<_yELRNh+7{g7B`0vr4Xb^L^pN6hawcM$V&*oI}E2SAHUxp9dihgzmD*=SEhT7*APTX?R7SWhXAnS{ z6w2gF{%BMwXaI{!GA_BIiq4wz(%?;lF907tR|+l)rzCV+EM&AvAr?15k%&w8Ho2Je zMTw6_H4)DvEvl$`OUiUmQ6p{~fMwkS?iH4FMo9tBg~@@ddy^?eP9n9$Mx!E5rgV?# zjT@5+)4d{LPMp=*rMD*%6K6}oXcU}JqG(Rk*q2dtPR@iHCQmu|+tG;o#Zva(?aBB6JRS4$_ZV65Ot z!3ek!U?HbykfTCQniNQ|NzgI*PL>#wgwpVV9od4a33JGqq0_>=0+H4|h=?wEoA^@4 zxAV;^`L1w{`0yE0u!0HTm2Sr0xdE#jzZxql0%oNf@Ib%p5_Y$CQfwdf3l2t9C zEaM86hd=NKoXQ zs)?BllA5KMDGHFl{oaX{CLPiZ%gpU&vSHgC1j($JW=~=Cy#i7-B*jXdj=k?07c$sD zOgnqfv}vQ(n&m{=Gl!6*3Q4S}YBZ^2^Ku3dN-hTV)M9o(0y=;pwOcyoP_pm7q2>HR zE&BmzmY3D~D5pXH2xY zxyuCN6R?DP;1%)etl<+FNxag1M(-sID;PNk8p+A8x5#Q!g6;;}RB}axjv>##BJq@x zMe#gV=CPQKqog9@g?tg(Gj8=*JeiSW`J&EbMLCB_7mz!ox(N5^6>2Zgu@e}q2L^Y; znOAOCkX^a@f~7qC4%1m@I@i3llNV}CXM=fXm+>`u!7B44RlBla@G~|)Q2qYq)SSUh z+FY(Wym{eU26uL!TRV2@Nq22((O@qfX3GXUz00@l@ZEL3dyDT`nR?dN@v!Sb*UI%h zK5+lsy>s_3-n+Oq(%^^oxInEnT1{8KQwvW1rv0~FzwX-RzP88l_qluAYTJ+bUA|+7 z@2T@WYj+y_NdtcFSH36ZT5#%*r>Xqcz`HgBgF`*8Z^bLFdEX2fdZ>FMA*LZnceqzYP1M8z0|T^;K?d zGb7Jh!>iwX5Zr7XuMF<=kJtOhSFZhq4->C~*Bji8yF2)Zo}b| z6}5tD!P5;cYCylS&As=GKLYXHiK!>lUP;zwD`FA%HwVi-o5;a zqXMB?_;mIC>Xlk(tl=Lw^n88WKm8KwkJ7nBf>dN85oy8X#)${Wi`?vV%IH#~%ZyGk zs+MfXiCL6LJf`pkf+Z)9&b^aWq(TN=#9xIe`3tGW;q}5z)AUp7$WyB2Ddqb+HB_gD h{>hHhH|ZA{nqvHSOPlWFe`dHJUVY)Fm@Y%$zX8$(xDfyV literal 0 HcmV?d00001 diff --git a/pipeline/__pycache__/detect.cpython-312.pyc b/pipeline/__pycache__/detect.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee9ecf35e0fb9ab3c26bcd5718e1ee113eab9853 GIT binary patch literal 1908 zcmah~O>7fa5PtjbUmV+Ueo1VaAK@e+M_MpXxO4ee)ve`k4Ra8Z?Jef&JN+&eYn6!8z z88bvlQugj`;z2U-dv=Ysi!mHON`d_SkcsYW6jHT2YmRnjTNy+j?M5i?GF|_A7gbI-bn059 zTu;z++4q;{DL4mfKa}usdO#tQGC3bIIg`Ia=`@9K7+2qBM^OtpfOIMYo=b>=&$Y-E z<`T+KEyxHtHKpntZAZ2uA^-}7Lo$`5FQHMCL6x2H$cV*Bgs3D}bxKZ4F-0|E6Y7kD zkHm6pcV;r33ygqEjM;i@0zxcm-=;lWn|}dW$P#9hDMigFn}33Rir5e1g`8VUi^haX zrjOXIT(OBSbVY1y!t zw4_)9k>IRJ$#TE{!TF)Yg%hVoEdlB)Zx+U6&2FpZviGHB-LO0-MZE2eyNL_BMH+xv zoF)=Ow|q|H5^yq0fXxcCTmcPBJ}`-lr-Q*tYAe=R9<98R+C64z}fZb33&As(1zDP-#gbk zKQK42*snkHg|i!t&5JX)jw}a1JAPGKIQgl(;x3$i609klDOJ}L&OH&rxBFIw z)@5Pkt%rO2N|E}jo>D{0(%Gefm72T4O14;c;;R2=Z)A&hx%)SQbvL5dqe~5I!LAj$ zw7dDs;hV!>oLd+y)i&P9U(XlYPW@PWy3`h5^prY=?%{hEi+j&5`hTix+H#|&ju#9H z?c1tGp@vdp+rr1C(C$(w`Y<$HY#Dy)=6w7X;yC_A7=ntyu0NjpP*v@EV9#n`&+mwG z_m`?0i%mz0;iC_IN4FSoywvwYqwlkk!8p2K-!|C9+&}IC{=n@5&x0mzuw8iYhG6UQ z+98Jdj;4Ue1rmjAqz0Hp6Nxn23ZomuVzHbw$~X8mdInxZI_x!9|Fnu{($WAq0H*zq nqPGKs8Yt>9+VvQ@;rom4P{F;$w^002%N7E$-1^)W#IEsgBemlh literal 0 HcmV?d00001 diff --git a/pipeline/__pycache__/rebuild.cpython-312.pyc b/pipeline/__pycache__/rebuild.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5e814185fd7e2d545ce1c3ee0460866bc770fa2 GIT binary patch literal 3099 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 dict: + if not os.path.isfile(wheel_path): + return {"error": f"File not found: {wheel_path}"} + + if not wheel_path.endswith(".whl"): + return {"error": "Not a .whl file"} + + basename = os.path.basename(wheel_path) + parts = basename.split("-") + platform_tag = parts[-1].replace(".whl", "") if len(parts) >= 4 else "unknown" + abi_tag = parts[-2] if len(parts) >= 4 else "unknown" + + has_native = False + native_detail = "" + + if platform_tag not in PURE_TAGS or abi_tag != "none": + has_native = True + native_detail = f"platform={platform_tag}, abi={abi_tag}" + + if not has_native: + try: + with zipfile.ZipFile(wheel_path) as zf: + for name in zf.namelist(): + if name.endswith((".so", ".pyd", ".dll", ".dylib")): + has_native = True + native_detail = f"contains native extension: {name}" + break + except zipfile.BadZipFile: + return {"error": "Corrupt or invalid zip file"} + + return { + "wheel": basename, + "pure_python": not has_native, + "native": has_native, + "detail": native_detail or "tag=any/none, no native binaries found", + } diff --git a/pipeline/rebuild.py b/pipeline/rebuild.py new file mode 100644 index 0000000..c69435a --- /dev/null +++ b/pipeline/rebuild.py @@ -0,0 +1,46 @@ +import os +import sys + + +def rebuild_native_wheel(wheel_path: str, output_dir: str, target_os: str = "harmonyos", arch: str = "arm64") -> str: + basename = os.path.basename(wheel_path) + print(f"[rebuild] Native wheel rebuild triggered: {basename}") + print(f"[rebuild] Target: {target_os}/{arch}") + print(f"[rebuild] Output: {output_dir}") + print() + print(" Native extension rebuild for HarmonyOS requires:") + print(" - HarmonyOS SDK / OHOS NDK") + print(" - Cross-compilation toolchain for arm64") + print(" - Python headers matching the target Python build") + print(" - All C/Fortran source dependencies ported to HarmonyOS") + print() + print(" This entry point will be fleshed out in a future iteration.") + print(" For now, the source wheel is copied as-is for manual inspection.") + print() + + output_path = os.path.join(output_dir, basename) + os.makedirs(output_dir, exist_ok=True) + + with open(wheel_path, "rb") as src, open(output_path, "wb") as dst: + while True: + buf = src.read(65536) + if not buf: + break + dst.write(buf) + + print(f"[rebuild] Copied original wheel to: {output_path}") + + stub_path = output_path.replace(".whl", f"_{target_os}_{arch}_stub.txt") + with open(stub_path, "w") as f: + f.write(f"Native rebuild stub for {basename} ({target_os}/{arch})\n") + f.write("Replace this file with the actual rebuilt harmonyos wheel.\n") + + print(f"[rebuild] Stub marker: {stub_path}") + return output_path + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python -m pipeline.rebuild [output_dir]") + sys.exit(1) + rebuild_native_wheel(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "output") diff --git a/pipeline/repack.py b/pipeline/repack.py new file mode 100644 index 0000000..90bbc1c --- /dev/null +++ b/pipeline/repack.py @@ -0,0 +1,30 @@ +import zipfile +import os +import shutil + + +def repack_wheel(wheel_path: str, output_dir: str) -> str: + basename = os.path.basename(wheel_path) + name_parts = basename.split("-") + + if len(name_parts) >= 4: + wheel_name = "-".join(name_parts[:-3]) + version = name_parts[-3] + pure_tag = f"{name_parts[-2]}-{name_parts[-1]}" + else: + wheel_name = name_parts[0] + version = "0.0.0" + pure_tag = "none-any" + + harmony_tag = f"{wheel_name}-{version}-py3-none-harmonyos_arm64.whl" + output_path = os.path.join(output_dir, harmony_tag) + + os.makedirs(output_dir, exist_ok=True) + + with zipfile.ZipFile(wheel_path, "r") as zin: + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zout: + for item in zin.infolist(): + data = zin.read(item.filename) + zout.writestr(item, data) + + return output_path diff --git a/wheel_pipeline.py b/wheel_pipeline.py new file mode 100755 index 0000000..d5c79c7 --- /dev/null +++ b/wheel_pipeline.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from pipeline.cli import main + +if __name__ == "__main__": + main()