taotoolkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,823 @@
1
+ #!/usr/bin/env python
2
+ # -*- encoding: utf-8 -*-
3
+ """版本下载与烧录相关命令模块。"""
4
+
5
+ import os
6
+ import sys
7
+ import shutil
8
+ import zipfile
9
+ import argparse
10
+ import requests
11
+ import re
12
+ import jenkins # pip install python-jenkins
13
+ import threading
14
+ import time
15
+ from time import sleep
16
+ from bs4 import BeautifulSoup as bs
17
+
18
+
19
+ from .tao_common import (
20
+ debugCtrl,
21
+ eExit,
22
+ logi,
23
+ logw,
24
+ loge,
25
+ logd,
26
+ logh,
27
+ oCmd,
28
+ rCmd,
29
+ pCmd,
30
+ refreshWait,
31
+ inputSerial,
32
+ createFolder,
33
+ findFilesBySuffixKeyword,
34
+ sysVersion,
35
+ normalize_args,
36
+ register_command,
37
+ )
38
+
39
+ from .tao_device import DevOp
40
+
41
+
42
+ class jenkinsTool:
43
+ # logging.getLogger("urllib3").setLevel(logging.WARNING)
44
+ def __init__(self, branch, buildTag, ciOwner, isUserDebug="1", jenkinsUser=None, jenkinsPassword=None):
45
+ self.jenkinsUrl = "http://build.asrmicro.com/"
46
+ self.jenkinsUser = jenkinsUser if jenkinsUser else os.environ.get("JENKINS_USERNAME", "")
47
+ self.jenkinsPassword = jenkinsPassword if jenkinsPassword else os.environ.get("JENKINS_PASSWORD", "")
48
+ self.baseUrl = "http://10.1.24.40/"
49
+ self.branch = branch
50
+ self.buildTag = buildTag
51
+ self.ciOwner = ciOwner
52
+ self.isUserDebug = True if isUserDebug == "1" else False
53
+ self.packageFile = "release.zip"
54
+ self.retainUserData = False # 是否保留用户数据,默认不保留
55
+ if sysVersion == "Linux":
56
+ self.downloadPath = "/home/local/Downloads/jenkin" # 下载路径和解压路径
57
+ self.extractPath = "/home/local/Downloads/jenkin"
58
+ self.aimgTool = "aimgtool"
59
+ elif sysVersion == "Windows":
60
+ self.downloadPath = r"D:\jenkin" # 下载路径和解压路径
61
+ self.extractPath = r"D:\jenkin"
62
+ self.aimgTool = "aimgtool.exe"
63
+ # logi(f"download package to {self.downloadPath}")
64
+
65
+ def pickBuildBoard(self, build_info, jobName):
66
+ logi(f"在 branch[{jobName}] buildID[{build_info['number']}] 找到如下版本,请选择一个序号下载:")
67
+ filtered_list = []
68
+ logd(f"artifacts: {build_info['artifacts']}")
69
+ for i in build_info["artifacts"]:
70
+ logd(f"relativePath: {i['relativePath']}")
71
+ if i["fileName"].endswith(self.packageFile):
72
+ if self.isUserDebug:
73
+ if "userdebug" in i["relativePath"]:
74
+ filtered_list.append(i["relativePath"].split("/")[1])
75
+ else:
76
+ filtered_list.append(i["relativePath"].split("/")[1])
77
+
78
+ foundBoard = {key: index for key, index in enumerate(filtered_list)}
79
+ if len(foundBoard) == 0:
80
+ buildType = "userdebug" if self.isUserDebug else "release"
81
+ eExit(f"no board found in {jobName} buildID {build_info['number']} buildType {buildType}")
82
+ for k, v in foundBoard.items():
83
+ print(k, v)
84
+
85
+ pickBoard = foundBoard[inputSerial(0, len(foundBoard) - 1)]
86
+ zip_artifact = next(
87
+ a
88
+ for a in build_info["artifacts"]
89
+ if a["fileName"].endswith(self.packageFile) and pickBoard in a["relativePath"]
90
+ )
91
+ zip_url = f"{self.jenkinsUrl}/job/{jobName}/{build_info['number']}/artifact/{zip_artifact['relativePath']}"
92
+ # print(pickBoard,zip_url)
93
+ return zip_url
94
+
95
+ def getZipArtifact(self, build_info, jobName, buildBoard):
96
+ zipList = list()
97
+
98
+ logd(f"artifacts: {build_info['artifacts']}")
99
+ for a in build_info["artifacts"]:
100
+ if (
101
+ a["fileName"].endswith(self.packageFile)
102
+ and buildBoard in a["relativePath"]
103
+ and "xts" not in a["relativePath"]
104
+ ):
105
+ if self.isUserDebug:
106
+ if "userdebug" in a["relativePath"]:
107
+ zipList.append(a)
108
+ else:
109
+ zipList.append(a)
110
+
111
+ if len(zipList) == 1:
112
+ zip_artifact = zipList[0]
113
+ elif len(zipList) == 0:
114
+ loge(f"find {jobName} board[{buildBoard}] failed")
115
+ zip_url = self.pickBuildBoard(build_info, jobName)
116
+ return zip_url
117
+ else:
118
+ print(f"找到如下包含 {buildBoard} 的版本,请选择一个board用于下载:")
119
+ for index, value in enumerate(zipList):
120
+ print(f"{index}: {value['relativePath']}")
121
+ zip_artifact = zipList[inputSerial(0, len(zipList) - 1)]
122
+
123
+ zip_url = f"{self.jenkinsUrl}/job/{jobName}/{build_info['number']}/artifact/{zip_artifact['relativePath']}"
124
+ return zip_url
125
+
126
+ def _getFileserverUrl(self, jobName, buildNum, buildBoard):
127
+ url = f"{self.baseUrl}{jobName}/{buildNum}/{buildBoard}/{self.packageFile}"
128
+ return url
129
+
130
+ def _verifyFileserverUrl(self, url):
131
+ try:
132
+ resp = requests.head(url, auth=(self.jenkinsUser, self.jenkinsPassword), timeout=10)
133
+ return resp.status_code == 200
134
+ except Exception:
135
+ return False
136
+
137
+ def _listFileserverBoards(self, jobName, buildNum):
138
+ url = f"{self.baseUrl}{jobName}/{buildNum}/"
139
+ try:
140
+ resp = requests.get(url, auth=(self.jenkinsUser, self.jenkinsPassword), timeout=10)
141
+ resp.raise_for_status()
142
+ soup = bs(resp.text, "html.parser")
143
+ boards = []
144
+ for a in soup.find_all("a", href=True):
145
+ href = str(a["href"])
146
+ if href in ("/", "../", "?") or href.startswith("?"):
147
+ continue
148
+ if href.endswith("/"):
149
+ name = href.rstrip("/")
150
+ if "-" in name and "sp01" in name:
151
+ boards.append(name)
152
+ return boards
153
+ except Exception as e:
154
+ loge(f"列出文件服务器 board 失败: {e}")
155
+ return []
156
+
157
+ def findTargetBuild(
158
+ self, buildNum=0, triggerUser="taotaoli", jobName="adroid16.0_ci", buildBoard="larkpro_phone-sp01-userdebug"
159
+ ):
160
+ getDB = "ci" not in jobName
161
+ server = jenkins.Jenkins(self.jenkinsUrl, self.jenkinsUser, self.jenkinsPassword)
162
+ buildNum = int(buildNum)
163
+ zip_url = ""
164
+ build_info = None
165
+
166
+ if buildNum != 0:
167
+ try:
168
+ logi(f"getting build info for {jobName} buildNum {buildNum}")
169
+ build_info = server.get_build_info(jobName, buildNum)
170
+
171
+ if build_info.get("building"):
172
+ eExit(f"build {buildNum} is still running, please wait")
173
+
174
+ logi(f"构建状态: {build_info.get('result')}")
175
+ logd(f"artifacts: {build_info.get('artifacts')}")
176
+
177
+ if build_info.get("artifacts"):
178
+ # ── 正常走 Jenkins artifact ──
179
+ zip_url = self.getZipArtifact(build_info, jobName, buildBoard)
180
+ else:
181
+ # ── artifact 已被清理,回退到文件服务器 ──
182
+ logw(f"build #{buildNum} artifact 已被清理,尝试从文件服务器下载")
183
+ zip_url = self._fallbackFileserver(jobName, buildNum, buildBoard)
184
+
185
+ except Exception as e:
186
+ loge(f"获取 build info 失败: {str(e).splitlines()[0]}")
187
+ if build_info and build_info.get("artifacts"):
188
+ zip_url = self.pickBuildBoard(build_info, jobName)
189
+ else:
190
+ # build_info 获取失败或 artifact 为空,直接试文件服务器
191
+ logw("尝试直接从文件服务器获取")
192
+ zip_url = self._fallbackFileserver(jobName, buildNum, buildBoard)
193
+ finally:
194
+ if not zip_url:
195
+ eExit(f"find {buildNum} {jobName} board[{buildBoard}] failed")
196
+ return str(build_info["number"]) if build_info else str(buildNum), zip_url
197
+
198
+ else:
199
+ if getDB:
200
+ try:
201
+ # 先获取最后一次完成构建的编号
202
+ last_build_number = server.get_job_info(jobName)["lastCompletedBuild"]["number"]
203
+ # 再用编号获取详细信息
204
+ build_info = server.get_build_info(jobName, last_build_number)
205
+
206
+ build_time_local = time.strftime(
207
+ "%Y-%m-%d %H:%M:%S", time.localtime(build_info["timestamp"] / 1000)
208
+ )
209
+ if build_info["result"] != "SUCCESS":
210
+ logw(
211
+ f"目标构建号: {build_info['number']}, 状态: {build_info['result']}, 时间: {build_time_local}"
212
+ )
213
+ else:
214
+ logi(
215
+ f"目标构建号: {build_info['number']}, 状态: {build_info['result']}, 时间: {build_time_local}"
216
+ )
217
+
218
+ if build_info.get("artifacts"):
219
+ zip_url = self.getZipArtifact(build_info, jobName, buildBoard)
220
+ else:
221
+ logw(f"最新 build #{build_info['number']} artifact 已清理,回退文件服务器")
222
+ zip_url = self._fallbackFileserver(jobName, build_info["number"], buildBoard)
223
+
224
+ if not zip_url:
225
+ raise Exception("no zip found")
226
+ except Exception as e:
227
+ loge(f"find {jobName} board[{buildBoard}] newest DB failed: {e}")
228
+ if build_info and build_info.get("artifacts"):
229
+ zip_url = self.pickBuildBoard(build_info, jobName)
230
+ elif build_info:
231
+ zip_url = self._fallbackFileserver(jobName, build_info["number"], buildBoard)
232
+
233
+ if build_info:
234
+ return str(build_info["number"]), zip_url
235
+ else:
236
+ eExit(f"find {jobName} board[{buildBoard}] failed")
237
+
238
+ else:
239
+ # 获取指定用户最新 CI(逻辑不变,artifact 为空时同样回退)
240
+ builds = server.get_job_info(jobName, fetch_all_builds=True)["builds"]
241
+ buildTmp = None
242
+ for build in builds:
243
+ build_info = server.get_build_info(jobName, build["number"])
244
+ actions = build_info.get("actions", [])
245
+ triggered_by = next(
246
+ (action.get("causes", [{}])[0].get("userId", "") for action in actions if "causes" in action),
247
+ "",
248
+ )
249
+ if triggerUser and triggered_by != triggerUser:
250
+ continue
251
+ if build_info["result"] != "SUCCESS":
252
+ continue
253
+
254
+ build_time_local = time.strftime(
255
+ "%Y-%m-%d %H:%M:%S", time.localtime(build_info["timestamp"] / 1000)
256
+ )
257
+ logi(f"目标构建号: {build_info['number']}, 状态: {build_info['result']}, 时间: {build_time_local}")
258
+ buildTmp = build_info
259
+
260
+ if build_info.get("artifacts"):
261
+ zip_url = self.getZipArtifact(build_info, jobName, buildBoard)
262
+ else:
263
+ logw(f"build #{build_info['number']} artifact 已清理,回退文件服务器")
264
+ zip_url = self._fallbackFileserver(jobName, build_info["number"], buildBoard)
265
+
266
+ if zip_url:
267
+ return str(build_info["number"]), zip_url
268
+
269
+ logw(f"find {triggerUser} {jobName} board[{buildBoard}] newest CI failed")
270
+ if buildTmp:
271
+ if buildTmp.get("artifacts"):
272
+ zip_url = self.pickBuildBoard(buildTmp, jobName)
273
+ else:
274
+ zip_url = self._fallbackFileserver(jobName, buildTmp["number"], buildBoard)
275
+ return str(buildTmp["number"]), zip_url
276
+ else:
277
+ eExit("no build found")
278
+
279
+ def _fallbackFileserver(self, jobName, buildNum, buildBoard):
280
+ """
281
+ artifact 为空时从文件服务器获取下载 URL
282
+ 优先精确匹配 buildBoard,找不到时列出所有 board 让用户选择
283
+ """
284
+ url = self._getFileserverUrl(jobName, buildNum, buildBoard)
285
+ logd(f"尝试文件服务器: {url}")
286
+
287
+ if self._verifyFileserverUrl(url):
288
+ logi(f"文件服务器找到: {url}")
289
+ return url
290
+
291
+ # 精确匹配失败,列出可用 board 让用户选
292
+ logw(f"文件服务器未找到 {buildBoard},列出可用 board:")
293
+ boards = self._listFileserverBoards(jobName, buildNum)
294
+
295
+ if not boards:
296
+ loge(f"文件服务器 {self.baseUrl}{jobName}/{buildNum}/ 下没有找到任何 board")
297
+ return ""
298
+
299
+ # 先尝试模糊匹配(board 名包含 buildBoard 关键词)
300
+ keyword = buildBoard.split("-")[0] # 如 larkpro_phone
301
+ matched = [b for b in boards if keyword in b]
302
+
303
+ if len(matched) == 1:
304
+ logi(f"自动匹配到: {matched[0]}")
305
+ return self._getFileserverUrl(jobName, buildNum, matched[0])
306
+
307
+ # 需要用户手动选择
308
+ candidates = matched if matched else boards
309
+ if self.isUserDebug:
310
+ candidates = [b for b in candidates if "userdebug" in b] or candidates
311
+
312
+ print(f"\n在文件服务器找到如下 board,请选择:")
313
+ for i, b in enumerate(candidates):
314
+ print(f" {i}: {b}")
315
+ chosen = candidates[inputSerial(0, len(candidates) - 1)]
316
+ return self._getFileserverUrl(jobName, buildNum, chosen)
317
+
318
+ def getHref(self, rooturl):
319
+ headers = {
320
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
321
+ }
322
+ sleep(1)
323
+ with requests.Session() as session:
324
+ response = requests.get(rooturl, headers=headers, auth=(self.jenkinsUser, self.jenkinsPassword))
325
+ soup = bs(response.text, features="html.parser")
326
+ result = []
327
+ for link in soup.find_all("a"):
328
+ result.append(link.get("href"))
329
+ return result
330
+
331
+ def getJenkinsBoard(self, rooturl):
332
+ # 默认解码方式utf-8
333
+ logd(rooturl)
334
+ result = self.getHref(rooturl)
335
+
336
+ filtered_list = [s for s in result if "-" in s and "/" in s]
337
+ dict_from_list = {key: index for key, index in enumerate(filtered_list)}
338
+ logd(dict_from_list)
339
+ if dict_from_list == {}:
340
+ eExit(f"No board found in {rooturl}")
341
+ return dict_from_list
342
+
343
+ def browserDownload(self, url, timeOut=600):
344
+ if sysVersion == "Linux":
345
+ browser = "wget"
346
+ else:
347
+ browser = "C:\\Program Files\\Mozilla Firefox\\firefox.exe -headless"
348
+ # browser = "C:\\Program Files\\Mozilla Firefox\\firefox.exe"
349
+ info, err = pCmd(f"{browser} {url}", showCmd=True, showRet=False, timeOut=timeOut, logLevel="info")
350
+ # logi(err)
351
+ if "ERROR 404: Not Found" in err:
352
+ eExit("ERROR 404: Not Found")
353
+
354
+ def downloadByRequest(self, url, save_path, useSession=False):
355
+ """下载需要认证的文件(支持断点续传)"""
356
+
357
+ total_size = 0
358
+ if os.path.exists(save_path):
359
+ downloaded = os.path.getsize(save_path)
360
+ else:
361
+ downloaded = 0
362
+ open(save_path, "wb").close() # 创建空文件
363
+
364
+ # 设置断点续传头部
365
+ headers = {"Range": f"bytes={downloaded}-"} if downloaded else {}
366
+ CHUNK = 4 * 1024 * 1024 # 4MB per chunk
367
+ if useSession:
368
+ session = requests.Session()
369
+ session.auth = (self.jenkinsUser, self.jenkinsPassword)
370
+ session.headers.update(headers)
371
+ try:
372
+ with session.get(url, stream=True, timeout=30) as response:
373
+ response.raise_for_status() # 检查HTTP错误
374
+ if response.status_code == 401:
375
+ raise PermissionError("认证失败,请检查账户密码")
376
+
377
+ # 获取文件总大小(考虑断点续传)
378
+ total_size = int(response.headers.get("content-length", 0)) + downloaded
379
+ with open(save_path, "ab") as f:
380
+ for chunk in response.iter_content(chunk_size=CHUNK):
381
+ if chunk: # 过滤空块
382
+ f.write(chunk)
383
+ downloaded += len(chunk)
384
+ progress = downloaded / total_size * 100 if total_size > 0 else 0
385
+ sys.stdout.write(
386
+ f"\r下载进度: {progress:.1f}% ({downloaded/1024/1024:.1f}/{total_size/1024/1024:.1f} MB)"
387
+ )
388
+ sys.stdout.flush()
389
+ print("\r")
390
+ logd(f"\n文件已保存至: {os.path.abspath(save_path)}")
391
+
392
+ except requests.exceptions.RequestException as e:
393
+ loge(f"下载失败: {str(e)}")
394
+ if os.path.getsize(save_path) == 0:
395
+ os.remove(save_path) # 删除空文件
396
+ exit()
397
+ else:
398
+ try:
399
+ with requests.get(
400
+ url, headers=headers, stream=True, auth=(self.jenkinsUser, self.jenkinsPassword), timeout=30
401
+ ) as response:
402
+ response.raise_for_status() # 检查HTTP错误
403
+ if response.status_code == 401:
404
+ raise PermissionError("认证失败,请检查账户密码")
405
+
406
+ # 获取文件总大小(考虑断点续传)
407
+ total_size = int(response.headers.get("content-length", 0)) + downloaded
408
+ with open(save_path, "ab") as f:
409
+ for chunk in response.iter_content(chunk_size=CHUNK):
410
+ if chunk: # 过滤空块
411
+ f.write(chunk)
412
+ downloaded += len(chunk)
413
+ progress = downloaded / total_size * 100 if total_size > 0 else 0
414
+ sys.stdout.write(
415
+ f"\r下载进度: {progress:.1f}% ({downloaded/1024/1024:.1f}/{total_size/1024/1024:.1f} MB)"
416
+ )
417
+ sys.stdout.flush()
418
+ print("\r")
419
+ logd(f"\n文件已保存至: {os.path.abspath(save_path)}")
420
+
421
+ except requests.exceptions.RequestException as e:
422
+ loge(f"下载失败: {str(e)}")
423
+ if os.path.getsize(save_path) == 0:
424
+ os.remove(save_path) # 删除空文件
425
+ exit()
426
+ return total_size
427
+
428
+ def downloadByRequestMutithread(self, url, save_path):
429
+ """下载需要认证的文件(支持断点续传 + 多线程分块)"""
430
+ auth = (self.jenkinsUser, self.jenkinsPassword)
431
+
432
+ # 先获取文件总大小,顺便验证认证
433
+ head = requests.head(url, auth=auth, timeout=10)
434
+ if head.status_code == 401:
435
+ raise PermissionError("认证失败,请检查账户密码")
436
+ head.raise_for_status()
437
+
438
+ total_size = int(head.headers.get("content-length", 0))
439
+ supports_range = head.headers.get("Accept-Ranges", "") == "bytes"
440
+
441
+ CHUNK = 4 * 1024 * 1024 # 4MB per chunk
442
+ if supports_range and total_size > 0:
443
+ self._download_multithread(url, save_path, total_size, auth, chunk_size=CHUNK)
444
+ else:
445
+ self._download_single(url, save_path, auth, chunk_size=CHUNK)
446
+
447
+ return total_size
448
+
449
+ def _download_multithread(self, url, save_path, total_size, auth, workers=8, chunk_size=1024 * 1024):
450
+ from concurrent.futures import ThreadPoolExecutor, as_completed
451
+
452
+ """并发分块下载,最后按顺序写入"""
453
+ chunk_count = (total_size + chunk_size - 1) // chunk_size
454
+ ranges = [(i * chunk_size, min((i + 1) * chunk_size - 1, total_size - 1)) for i in range(chunk_count)]
455
+
456
+ # 预分配文件大小(避免碎片)
457
+ with open(save_path, "wb") as f:
458
+ f.seek(total_size - 1)
459
+ f.write(b"\0")
460
+
461
+ write_lock = threading.Lock()
462
+ downloaded_bytes = [0]
463
+
464
+ def fetch_and_write(idx, start, end):
465
+ headers = {"Range": f"bytes={start}-{end}"}
466
+ r = requests.get(url, headers=headers, auth=auth, timeout=60)
467
+ r.raise_for_status()
468
+ data = r.content
469
+
470
+ # 直接写到正确的文件偏移位置,无需排序等待
471
+ with write_lock:
472
+ with open(save_path, "r+b") as f:
473
+ f.seek(start)
474
+ f.write(data)
475
+ downloaded_bytes[0] += len(data)
476
+ pct = downloaded_bytes[0] / total_size * 100
477
+ print(
478
+ f"\r进度: {pct:.1f}% ({downloaded_bytes[0]/1024/1024:.1f}/{total_size/1024/1024:.1f} MB)",
479
+ end="",
480
+ flush=True,
481
+ )
482
+
483
+ with ThreadPoolExecutor(max_workers=workers) as pool:
484
+ futures = [pool.submit(fetch_and_write, i, s, e) for i, (s, e) in enumerate(ranges)]
485
+ for f in as_completed(futures):
486
+ f.result() # 抛出异常(如有)
487
+ print()
488
+
489
+ def _download_single(self, url, save_path, auth, chunk_size=1024 * 1024):
490
+ """单线程大 chunk 下载(服务端不支持 Range 时回退)"""
491
+ downloaded = os.path.getsize(save_path) if os.path.exists(save_path) else 0
492
+ headers = {"Range": f"bytes={downloaded}-"} if downloaded else {}
493
+
494
+ with requests.get(url, headers=headers, stream=True, auth=auth, timeout=30) as r:
495
+ if r.status_code == 401:
496
+ raise PermissionError("认证失败,请检查账户密码")
497
+ r.raise_for_status()
498
+ total = int(r.headers.get("content-length", 0)) + downloaded
499
+ with open(save_path, "ab") as f:
500
+ for chunk in r.iter_content(chunk_size=chunk_size): # 4MB chunks
501
+ if chunk:
502
+ f.write(chunk)
503
+ downloaded += len(chunk)
504
+ pct = downloaded / total * 100 if total else 0
505
+ print(f"\r下载进度: {pct:.1f}%", end="", flush=True)
506
+ print()
507
+
508
+ def getPackage(self, url, dest_path):
509
+ """从 Jenkins 下载文件"""
510
+
511
+ logd(f"{url} {dest_path}")
512
+ start = time.time()
513
+
514
+ windowsDownloadPath = r"D:\jenkin"
515
+ if sysVersion == "Linux":
516
+ targetZip = os.path.join(os.getcwd(), self.packageFile)
517
+ else:
518
+ targetZip = os.path.join(windowsDownloadPath, self.packageFile)
519
+ partFile = findFilesBySuffixKeyword(windowsDownloadPath, ".part", "release")
520
+ for i in partFile:
521
+ os.remove(targetZip)
522
+
523
+ if os.path.exists(targetZip):
524
+ os.remove(targetZip)
525
+
526
+ logi(f"download {url}")
527
+ totalSize = self.downloadByRequest(url, targetZip)
528
+ # totalSize = self.downloadByRequestMutithread(url, targetZip)
529
+ shutil.move(targetZip, dest_path)
530
+ end = time.time()
531
+ costTime = end - start
532
+ downloadSpeed = totalSize / 1024 / 1024 / costTime if costTime > 0 else 0
533
+ logi(f"Download cost: {costTime:.2f}秒, 平均速度: {downloadSpeed:.2f} MB/s")
534
+
535
+ def extractPackage(self, file_path, extract_to):
536
+ """解压下载的文件"""
537
+ fpath = os.path.join(extract_to, file_path[:-4])
538
+ logi(f"Extracting file {os.path.split(file_path)[1]} to {fpath}")
539
+
540
+ createFolder(extract_to)
541
+
542
+ if file_path.endswith(".zip"):
543
+ try:
544
+ with zipfile.ZipFile(file_path, "r") as zip_ref:
545
+ zip_ref.extractall(extract_to)
546
+ except zipfile.BadZipfile:
547
+ eExit(f"{file_path} is a bad zip file")
548
+ else:
549
+ eExit(f"Unsupported file format: {file_path}")
550
+
551
+ def getCfg(self, boardTag, imgPath):
552
+ fileList = os.listdir(imgPath)
553
+ cfgFlag = boardTag.split("-")[0]
554
+ boardList = list()
555
+ for i in fileList:
556
+ if i.endswith(".cfg"):
557
+ if cfgFlag in i:
558
+ boardList.append(i)
559
+ if len(boardList) == 1:
560
+ return boardList[0]
561
+ elif len(boardList) == 0:
562
+ logw(f"get {boardTag} table cfg failed")
563
+ return None
564
+ else:
565
+ print("请选择一个cfg用于烧录版本:")
566
+ for index, value in enumerate(boardList):
567
+ print(f"{index}: {value}")
568
+ return boardList[inputSerial(0, len(boardList) - 1)]
569
+
570
+ def getInfo(self, devSerial):
571
+ if devSerial != None:
572
+ cmd = f"adb -s {devSerial} reboot download"
573
+ else:
574
+ cmd = "adb reboot download"
575
+ rCmd(cmd, showCmd=True, logLevel="debug")
576
+ info = rCmd(f"./{self.aimgTool} portscan", timeOut=5)[0]
577
+ sleep(0.5)
578
+ if "usbdownload" in info:
579
+ return True
580
+
581
+ return False
582
+
583
+ def replace_last_Y_with_N(self, filename, search_string):
584
+ """
585
+ 在文件中找到包含 search_string 的行,并将该行最后一个字符 Y 替换为 N。
586
+ (假设行末没有多余空格,且最后一个字符为 Y)
587
+ """
588
+ with open(filename, "r", encoding="utf-8") as f:
589
+ lines = f.readlines()
590
+
591
+ with open(filename, "w", encoding="utf-8") as f:
592
+ for line in lines:
593
+ if search_string in line:
594
+ # 去掉末尾换行符,便于操作
595
+ stripped = line.rstrip("\n")
596
+ # 如果最后一个字符是 Y,则替换
597
+ if stripped and stripped[-1] == "Y":
598
+ stripped = stripped[:-1] + "N"
599
+ line = stripped + "\n" # 恢复换行符
600
+ f.write(line)
601
+
602
+ def downloadToBoard(self, boardTag, imgPath, devSerial):
603
+ logd(f"{boardTag} {imgPath}")
604
+ os.chdir(imgPath)
605
+ os.chmod(self.aimgTool, 0o755)
606
+ cfgList = []
607
+ fileList = os.listdir(imgPath)
608
+ for i in fileList:
609
+ if i.endswith(".cfg"):
610
+ cfgList.append(i)
611
+ if boardTag == None:
612
+ if len(cfgList) == 1:
613
+ cfgFIle = cfgList[0]
614
+ else:
615
+ print("请选择一个cfg用于烧录版本:")
616
+ for index, value in enumerate(cfgList):
617
+ print(f"{index}: {value}")
618
+
619
+ cfgFIle = cfgList[inputSerial(0, len(cfgList) - 1)]
620
+ else:
621
+ cfgFIle = self.getCfg(boardTag, imgPath)
622
+
623
+ finalCfg = cfgFIle
624
+ if self.retainUserData:
625
+ logw("retainUserData is True, will replace tzdata,metadata,userdata Y to N in cfg to retain user data")
626
+ if cfgFIle != None:
627
+ finalCfg = cfgFIle.split(".")[0] + "_retain_user_data.cfg"
628
+ shutil.copy2(cfgFIle, finalCfg)
629
+ self.replace_last_Y_with_N(finalCfg, "tzdata")
630
+ self.replace_last_Y_with_N(finalCfg, "metadata")
631
+ self.replace_last_Y_with_N(finalCfg, "userdata")
632
+ else:
633
+ eExit("cfg file not found")
634
+
635
+ logi(f"download {finalCfg}, wait for download port ready ... ")
636
+ refreshWait("usbdownload", 60, 0.3, self.getInfo, devSerial)
637
+ cmd = ""
638
+ if sysVersion == "Linux":
639
+ cmd = f"./{self.aimgTool} download -t {finalCfg}"
640
+ elif sysVersion == "Windows":
641
+ cmd = f"{self.aimgTool} download -t {finalCfg}"
642
+ logi(cmd)
643
+ oCmd(cmd)
644
+
645
+ def joinBackslash(self, *args):
646
+ # 将字符串用/拼接起来,最后一个不加
647
+ return "".join(f"{arg}/" if i < len(args) - 1 else f"{arg}" for i, arg in enumerate(args))
648
+
649
+ def downloadJenkins(self, buildNumber, buildBoard):
650
+ package = self.packageFile
651
+ branch = self.branch
652
+ buildTag = self.buildTag
653
+ pattern = re.compile("adroid" + r"(\d+)")
654
+ adVersion = pattern.findall(branch)[0]
655
+
656
+ ciTag = branch
657
+ if buildTag == "ci":
658
+ ciTag = branch + "_ci"
659
+ elif buildTag == "db":
660
+ ciTag = branch
661
+
662
+ logh(
663
+ f"num={buildNumber} Board={buildBoard} branch={branch} Tag={buildTag} ciOwner={self.ciOwner} adVersion={adVersion} isUserDebug={self.isUserDebug}"
664
+ )
665
+
666
+ buildNumber, targetUrl = self.findTargetBuild(buildNumber, self.ciOwner, ciTag, buildBoard)
667
+ buildBoard = targetUrl.split("/")[-2]
668
+
669
+ logi(f"download {buildNumber} {targetUrl}")
670
+ # exit()
671
+ packageName = buildTag + buildNumber + "android" + adVersion + "_" + buildBoard
672
+ imgName = os.path.join(self.downloadPath, packageName + ".zip")
673
+ createFolder(self.downloadPath)
674
+
675
+ if not os.path.exists(imgName):
676
+ if self.getPackage(targetUrl, imgName) == -1:
677
+ # 如果下载失败,列出所有board,用户选择一个board下载
678
+ boardUrl = f"http://10.1.24.40/{ciTag}/{buildNumber}"
679
+ boardDict = self.getJenkinsBoard(boardUrl)
680
+ logi(f"choose a board from {self.joinBackslash(self.baseUrl,self.branch,buildNumber)}")
681
+ for k, v in boardDict.items():
682
+ print(k, v[:-1])
683
+ buildBoard = boardDict[inputSerial(0, len(boardDict) - 1)]
684
+ buildBoard = buildBoard[:-1]
685
+ logi(f"choose board {buildBoard} to download")
686
+ targetUrl = self.joinBackslash(boardUrl, buildBoard, package)
687
+ packageName = buildTag + buildNumber + "android" + adVersion + "_" + buildBoard
688
+ imgName = os.path.join(self.downloadPath, packageName + ".zip")
689
+ if not os.path.exists(imgName):
690
+ if self.getPackage(targetUrl, imgName) == -1:
691
+ exit()
692
+ else:
693
+ logi(f"{packageName}.zip already exist, skip download from jenkins")
694
+
695
+ imgPath = os.path.join(self.extractPath, packageName)
696
+ if not os.path.exists(imgPath):
697
+ self.extractPackage(imgName, self.extractPath)
698
+ os.rename(os.path.join(self.extractPath, "release"), imgPath)
699
+ else:
700
+ logi(f"{imgPath} already exist, skip extract")
701
+
702
+ out, error = rCmd("adb devices -l")
703
+ serial = None
704
+ if len(out.split("\n")) == 3:
705
+ boardTag = None
706
+ else:
707
+ dev = DevOp()
708
+ serial = dev.serial
709
+ soc = dev.getSoc()
710
+ if soc == "larkl":
711
+ boardTag = "larkl_"
712
+ elif soc == "lark":
713
+ boardTag = "lark_"
714
+ elif soc == "dove":
715
+ boardTag = "dove_"
716
+ elif soc == "larkpro":
717
+ boardTag = "larkpro_"
718
+ else:
719
+ boardTag = dev.slectBoard() + "_"
720
+
721
+ # pCmd("adb reboot download", showCmd=True, waitDone=False)
722
+
723
+ self.downloadToBoard(boardTag, imgPath, serial)
724
+
725
+
726
+ def CLI_downJKS(argsList):
727
+ parser = argparse.ArgumentParser(prog="dj", description="自动下载并烧录版本")
728
+ parser.add_argument("-n", "--num", type=int, default=0, help="jenkins上的编译号,0表示最新编译结果")
729
+ parser.add_argument("-b", "--board", type=str, default="larkpro_phone-sp01-userdebug", help="烧录板卡")
730
+ parser.add_argument("-t", "--type", type=str, default="db", choices=["db", "ci"], help="编译类型 db/ci")
731
+ parser.add_argument(
732
+ "-a", "--android", type=str, default="16", choices=["16", "15", "16sdk", "15sdk"], help="安卓分支"
733
+ )
734
+ parser.add_argument("-u", "--user", type=str, default="taotaoli", help="触发用户")
735
+ parser.add_argument("-d", "--debug", type=str, default="1", choices=["0", "1"], help="是否烧录debug版本")
736
+ parser.add_argument("-r", "--ratain", type=str, default="0", choices=["0", "1"], help="烧录是否保留userdata")
737
+ parser.add_argument("--username", type=str, default=None, help="jenkins用户名")
738
+ parser.add_argument("--password", type=str, default=None, help="jenkins密码或API Token")
739
+ args = parser.parse_args(argsList)
740
+ buildBranch = ("16", "15", "16sdk", "15sdk")
741
+ ad = args.android
742
+ if ad not in buildBranch:
743
+ eExit(f"{ad} not in {buildBranch}")
744
+ branchDict = {
745
+ "15sdk": "adroid15_sdk_r1",
746
+ "15": "adroid15.0",
747
+ "16": "adroid16.0",
748
+ "16sdk": "adroid16_sdk_r1",
749
+ }
750
+ branch = branchDict.get(ad)
751
+ jenks = jenkinsTool(branch, args.type, args.user, args.debug, args.username, args.password)
752
+ jenks.retainUserData = True if args.ratain == "1" else False
753
+ jenks.downloadJenkins(args.num, args.board)
754
+
755
+
756
+ def getInfo(acmd):
757
+ rCmd("adb reboot download", showCmd=True)
758
+ info = rCmd(f"{acmd} portscan", timeOut=5)[0]
759
+ sleep(0.5)
760
+ return "usbdownload" in info
761
+
762
+
763
+ def CLI_aimgDownload(args):
764
+ if sysVersion == "Linux":
765
+ aimgTool = "aimgtool"
766
+ acmd = "./aimgtool"
767
+ else:
768
+ aimgTool = "aimgtool.exe"
769
+ acmd = "aimgtool.exe"
770
+ aimg = os.path.join(os.path.curdir, aimgTool)
771
+ if not os.path.exists(aimg):
772
+ eExit(f"get {aimg} failed")
773
+ os.chmod(aimgTool, 0o755)
774
+ rCmd("adb reboot download")
775
+ fileList = os.listdir(os.path.curdir)
776
+ cfgList = [i for i in fileList if i.endswith(".cfg")]
777
+ if not cfgList:
778
+ eExit("get table cfg failed")
779
+ if len(cfgList) == 1:
780
+ cfgFIle = cfgList[0]
781
+ else:
782
+ print("Please select the target cfg:")
783
+ for index, value in enumerate(cfgList):
784
+ print(f"{index}: {value}")
785
+ cfgFIle = cfgList[inputSerial(0, len(cfgList) - 1)]
786
+ refreshWait("usbdownload", 60, 0.3, getInfo, acmd)
787
+ cmd = f"{acmd} download -t {cfgFIle}"
788
+ logi(cmd)
789
+ oCmd(cmd)
790
+
791
+
792
+ _COMMAND_REGISTRY = {}
793
+
794
+
795
+ def printHelp():
796
+ print("[flash]")
797
+ print("adown :download and flash aimg")
798
+ print("dj -h :download and flash jenkins build")
799
+
800
+
801
+ CLI_aimgDownload = register_command("adown", registry=_COMMAND_REGISTRY)(CLI_aimgDownload)
802
+ CLI_downJKS = register_command("dj", registry=_COMMAND_REGISTRY)(CLI_downJKS)
803
+
804
+
805
+ def flashMain(args):
806
+ debugCtrl(1) if "debug" in args else debugCtrl(0)
807
+ args.remove("debug") if "debug" in args else args
808
+ args = normalize_args(args)
809
+ if len(args) < 1 or args[0] in {"help", "-h", "--help"}:
810
+ printHelp()
811
+ return
812
+ cmd = args[0]
813
+ handler = _COMMAND_REGISTRY.get(cmd)
814
+ if handler is None:
815
+ loge(f"unsupport command {cmd}")
816
+ return
817
+ handler(args[1:])
818
+
819
+
820
+ if __name__ == "__main__":
821
+ debugCtrl(1) if "debug" in sys.argv else debugCtrl(0)
822
+ sys.argv.remove("debug") if "debug" in sys.argv else sys.argv
823
+ flashMain(sys.argv)