package.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. const child_process = require('child_process');
  2. const crypto = require('crypto');
  3. const fs = require('fs').promises;
  4. const os = require('os');
  5. const path = require('path');
  6. const args = process.argv.slice(2);
  7. const read = (match) => {
  8. if (args.length > 0 || (!match || args[0] === match)) {
  9. return args.shift();
  10. }
  11. return null;
  12. };
  13. let configuration = null;
  14. const load = async () => {
  15. const file = path.join(__dirname, 'package.json');
  16. const content = await fs.readFile(file, 'utf-8');
  17. configuration = JSON.parse(content);
  18. };
  19. const clearLine = () => {
  20. if (process.stdout.clearLine) {
  21. process.stdout.clearLine();
  22. }
  23. };
  24. const write = (message) => {
  25. if (process.stdout.write) {
  26. process.stdout.write(message);
  27. }
  28. };
  29. const writeLine = (message) => {
  30. write(message + os.EOL);
  31. };
  32. const access = async (path) => {
  33. try {
  34. await fs.access(path);
  35. return true;
  36. } catch (error) {
  37. return false;
  38. }
  39. };
  40. const rm = async (...args) => {
  41. const dir = path.join(__dirname, ...args);
  42. const exists = await access(dir);
  43. if (exists) {
  44. writeLine('rm ' + path.join(...args));
  45. const options = { recursive: true, force: true };
  46. await fs.rm(dir, options);
  47. }
  48. };
  49. const mkdir = async (...args) => {
  50. const dir = path.join(__dirname, ...args);
  51. const exists = await access(dir);
  52. if (!exists) {
  53. writeLine('mkdir ' + path.join(...args));
  54. const options = { recursive: true };
  55. await fs.mkdir(dir, options);
  56. }
  57. return dir;
  58. };
  59. const copy = async (source, target, filter) => {
  60. let files = await fs.readdir(source);
  61. files = filter ? files.filter((file) => filter(file)) : files;
  62. const promises = files.map((file) => fs.copyFile(path.join(source, file), path.join(target, file)));
  63. await Promise.all(promises);
  64. };
  65. const unlink = async (dir, filter) => {
  66. let files = await fs.readdir(dir);
  67. files = filter ? files.filter((file) => filter(file)) : files;
  68. const promises = files.map((file) => fs.unlink(path.join(dir, file)));
  69. await Promise.all(promises);
  70. };
  71. const exec = async (command, encoding) => {
  72. if (encoding) {
  73. return child_process.execSync(command, { cwd: __dirname, encoding: encoding });
  74. }
  75. child_process.execSync(command, { cwd: __dirname, stdio: [ 0,1,2 ] });
  76. return '';
  77. /*
  78. return new Promise((resolve, reject) => {
  79. const child = child_process.exec(command, { cwd: __dirname }, (error, stdout, stderr) => {
  80. if (error) {
  81. stderr = '\n' + stderr ;
  82. if (error.message && error.message.endsWith(stderr)) {
  83. error.message = error.message.slice(0, -stderr.length);
  84. }
  85. reject(error);
  86. } else {
  87. resolve(stdout);
  88. }
  89. });
  90. child.stdout.pipe(process.stdout);
  91. child.stderr.pipe(process.stderr);
  92. });
  93. */
  94. };
  95. const sleep = (delay) => {
  96. return new Promise((resolve) => {
  97. setTimeout(resolve, delay);
  98. });
  99. };
  100. const request = async (url, init, status) => {
  101. const response = await fetch(url, init);
  102. if (status !== false && !response.ok) {
  103. throw new Error(response.status.toString());
  104. }
  105. if (response.body) {
  106. const reader = response.body.getReader();
  107. let position = 0;
  108. const stream = new ReadableStream({
  109. start(controller) {
  110. const read = async () => {
  111. try {
  112. const result = await reader.read();
  113. if (result.done) {
  114. clearLine();
  115. controller.close();
  116. } else {
  117. position += result.value.length;
  118. write(' ' + position + ' bytes\r');
  119. controller.enqueue(result.value);
  120. read();
  121. }
  122. } catch (error) {
  123. controller.error(error);
  124. }
  125. };
  126. read();
  127. }
  128. });
  129. return new Response(stream, {
  130. status: response.status,
  131. statusText: response.statusText,
  132. headers: response.headers
  133. });
  134. }
  135. return response;
  136. };
  137. const download = async (url) => {
  138. writeLine('download ' + url);
  139. const response = await request(url);
  140. return response.arrayBuffer().then((buffer) => new Uint8Array(buffer));
  141. };
  142. const hash = async (url, algorithm) => {
  143. const data = await download(url);
  144. const hash = crypto.createHash(algorithm);
  145. hash.update(data);
  146. return hash.digest('hex');
  147. };
  148. const fork = async (organization, repository) => {
  149. const headers = {
  150. Authorization: 'Bearer ' + process.env.GITHUB_TOKEN
  151. };
  152. writeLine('github delete ' + repository);
  153. await request('https://api.github.com/repos/' + process.env.GITHUB_USER + '/' + repository, {
  154. method: 'DELETE',
  155. headers: headers
  156. }, false);
  157. await sleep(4000);
  158. writeLine('github fork ' + repository);
  159. await request('https://api.github.com/repos/' + organization + '/' + repository + '/forks', {
  160. method: 'POST',
  161. headers: headers,
  162. body: ''
  163. });
  164. await sleep(4000);
  165. await rm('dist', repository);
  166. writeLine('github clone ' + repository);
  167. await exec('git clone --depth=2 https://x-access-token:' + process.env.GITHUB_TOKEN + '@github.com/' + process.env.GITHUB_USER + '/' + repository + '.git ' + 'dist/' + repository);
  168. };
  169. const pullrequest = async (organization, repository, body) => {
  170. writeLine('github push ' + repository);
  171. await exec('git -C dist/' + repository + ' push');
  172. writeLine('github pullrequest homebrew-cask');
  173. const headers = {
  174. Authorization: 'Bearer ' + process.env.GITHUB_TOKEN
  175. };
  176. await request('https://api.github.com/repos/' + organization + '/' + repository + '/pulls', {
  177. method: 'POST',
  178. headers: headers,
  179. body: JSON.stringify(body)
  180. });
  181. };
  182. const install = async () => {
  183. const node_modules = path.join(__dirname, 'node_modules');
  184. const exists = await access(node_modules);
  185. if (!exists) {
  186. await exec('npm install');
  187. }
  188. };
  189. const start = async () => {
  190. await install();
  191. await exec('npx electron .');
  192. };
  193. const clean = async () => {
  194. await rm('dist');
  195. await rm('node_modules');
  196. await rm('package-lock.json');
  197. await rm('yarn.lock');
  198. };
  199. const purge = async () => {
  200. await clean();
  201. await rm('third_party', 'bin');
  202. await rm('third_party', 'env');
  203. await rm('third_party', 'source');
  204. };
  205. const build = async (target) => {
  206. switch (target || read()) {
  207. case 'web': {
  208. writeLine('build web');
  209. await rm('dist', 'web');
  210. await mkdir('dist', 'web');
  211. writeLine('cp source/dir dist/dir');
  212. const source_dir = path.join(__dirname, 'source');
  213. const dist_dir = path.join(__dirname, 'dist', 'web');
  214. const extensions = new Set([ 'html', 'css', 'js', 'json', 'ico', 'png' ]);
  215. await copy(source_dir, dist_dir, (file) => extensions.has(file.split('.').pop()));
  216. await rm('dist', 'web', 'app.js');
  217. await rm('dist', 'web', 'electron.js');
  218. const manifestFile = path.join(__dirname, 'package.json');
  219. const contentFile = path.join(__dirname, 'dist', 'web', 'index.html');
  220. const manifestContent = await fs.readFile(manifestFile, 'utf-8');
  221. const manifest = JSON.parse(manifestContent);
  222. let content = await fs.readFile(contentFile, 'utf-8');
  223. content = content.replace(/(<meta\s*name="version"\s*content=")(.*)(">)/m, (match, p1, p2, p3) => {
  224. return p1 + manifest.version + p3;
  225. });
  226. content = content.replace(/(<meta\s*name="date"\s*content=")(.*)(">)/m, (match, p1, p2, p3) => {
  227. return p1 + manifest.date + p3;
  228. });
  229. await fs.writeFile(contentFile, content, 'utf-8');
  230. break;
  231. }
  232. case 'electron': {
  233. writeLine('build electron');
  234. await install();
  235. await exec('npx electron-builder install-app-deps');
  236. await exec('npx electron-builder --mac --universal --publish never -c.mac.identity=null');
  237. await exec('npx electron-builder --win --x64 --arm64 --publish never');
  238. await exec('npx electron-builder --linux appimage --x64 --publish never');
  239. await exec('npx electron-builder --linux snap --x64 --publish never');
  240. break;
  241. }
  242. case 'python': {
  243. writeLine('build python');
  244. await exec('python package.py build version');
  245. await exec('python -m pip install --user build wheel --quiet');
  246. await exec('python -m build --no-isolation --wheel --outdir dist/pypi dist/pypi');
  247. if (read('install')) {
  248. await exec('python -m pip install --force-reinstall dist/pypi/*.whl');
  249. }
  250. break;
  251. }
  252. default: {
  253. writeLine('build');
  254. await rm('dist');
  255. await install();
  256. await build('web');
  257. await build('electron');
  258. await build('python');
  259. break;
  260. }
  261. }
  262. };
  263. const publish = async (target) => {
  264. const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
  265. const GITHUB_USER = process.env.GITHUB_USER;
  266. switch (target || read()) {
  267. case 'web': {
  268. writeLine('publish web');
  269. await build('web');
  270. await rm('dist', 'gh-pages');
  271. const url = 'https://x-access-token:' + GITHUB_TOKEN + '@github.com/' + GITHUB_USER + '/netron.git';
  272. await exec('git clone --depth=1 ' + url + ' --branch gh-pages ./dist/gh-pages 2>&1 > /dev/null');
  273. writeLine('cp dist/web dist/gh-pages');
  274. const source_dir = path.join(__dirname, 'dist', 'web');
  275. const target_dir = path.join(__dirname, 'dist', 'gh-pages');
  276. await unlink(target_dir, (file) => file !== '.git');
  277. await copy(source_dir, target_dir);
  278. await exec('git -C dist/gh-pages add --all');
  279. await exec('git -C dist/gh-pages commit --amend --no-edit');
  280. await exec('git -C dist/gh-pages push --force origin gh-pages');
  281. break;
  282. }
  283. case 'electron': {
  284. writeLine('publish electron');
  285. await install();
  286. await exec('npx electron-builder install-app-deps');
  287. await exec('npx electron-builder --mac --universal --publish always');
  288. await exec('npx electron-builder --win --x64 --arm64 --publish always');
  289. await exec('npx electron-builder --linux appimage --x64 --publish always');
  290. await exec('npx electron-builder --linux snap --x64 --publish always');
  291. break;
  292. }
  293. case 'python': {
  294. writeLine('publish python');
  295. await build('python');
  296. await exec('python -m pip install --user twine');
  297. await exec('python -m twine upload --non-interactive --skip-existing --verbose dist/pypi/*.whl');
  298. break;
  299. }
  300. case 'cask': {
  301. writeLine('publish cask');
  302. await fork('Homebrew', 'homebrew-cask');
  303. const repository = 'https://github.com/' + configuration.repository;
  304. const url = repository + '/releases/download/v#{version}/' + configuration.productName + '-#{version}-mac.zip';
  305. const sha256 = await hash(url.replace(/#{version}/g, configuration.version), 'sha256');
  306. writeLine('update manifest');
  307. const dir = await mkdir('dist', 'homebrew-cask', 'Casks', 'n');
  308. const file = path.join(dir, 'netron.rb');
  309. await fs.writeFile(file, [
  310. 'cask "' + configuration.name + '" do',
  311. ' version "' + configuration.version + '"',
  312. ' sha256 "' + sha256.toLowerCase() + '"',
  313. '',
  314. ' url "' + url + '"',
  315. ' name "' + configuration.productName + '"',
  316. ' desc "' + configuration.description + '"',
  317. ' homepage "' + repository + '"',
  318. '',
  319. ' auto_updates true',
  320. '',
  321. ' app "' + configuration.productName + '.app"',
  322. '',
  323. ' zap trash: [',
  324. ' "~/Library/Application Support/' + configuration.productName + '",',
  325. ' "~/Library/Preferences/' + configuration.build.appId + '.plist",',
  326. ' "~/Library/Saved Application State/' + configuration.build.appId + '.savedState",',
  327. ' ]',
  328. 'end',
  329. ''
  330. ].join('\n'));
  331. writeLine('git push homebrew-cask');
  332. await exec('git -C dist/homebrew-cask add --all');
  333. await exec('git -C dist/homebrew-cask commit -m "Update ' + configuration.name + ' to ' + configuration.version + '"');
  334. await pullrequest('Homebrew', 'homebrew-cask', {
  335. title: 'Update ' + configuration.name + ' to ' + configuration.version,
  336. body: 'Update version and sha256',
  337. head: process.env.GITHUB_USER + ':master',
  338. base: 'master'
  339. });
  340. await rm('dist', 'homebrew-cask');
  341. break;
  342. }
  343. case 'winget': {
  344. writeLine('publish winget');
  345. await fork('microsoft', 'winget-pkgs');
  346. const name = configuration.name;
  347. const version = configuration.version;
  348. const product = configuration.productName;
  349. const publisher = configuration.author.name;
  350. const identifier = publisher.replace(' ', '') + '.' + product;
  351. const copyright = 'Copyright (c) ' + publisher;
  352. const repository = 'https://github.com/' + configuration.repository;
  353. const url = repository + '/releases/download/v' + version + '/' + product + '-Setup-' + version + '.exe';
  354. const extensions = configuration.build.fileAssociations.map((entry) => '- ' + entry.ext).sort().join('\n');
  355. writeLine('download ' + url);
  356. const sha256 = await hash(url, 'sha256');
  357. const paths = [ 'dist', 'winget-pkgs', 'manifests', publisher[0].toLowerCase(), publisher.replace(' ', ''), product, version ];
  358. await mkdir(...paths);
  359. writeLine('update manifest');
  360. const manifestFile = path.join(__dirname, ...paths, identifier);
  361. await fs.writeFile(manifestFile + '.yaml', [
  362. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.2.0.schema.json',
  363. 'PackageIdentifier: ' + identifier,
  364. 'PackageVersion: ' + version,
  365. 'DefaultLocale: en-US',
  366. 'ManifestType: version',
  367. 'ManifestVersion: 1.2.0',
  368. ''
  369. ].join('\n'));
  370. await fs.writeFile(manifestFile + '.installer.yaml', [
  371. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.2.0.schema.json',
  372. 'PackageIdentifier: ' + identifier,
  373. 'PackageVersion: ' + version,
  374. 'Platform:',
  375. '- Windows.Desktop',
  376. 'InstallModes:',
  377. '- silent',
  378. '- silentWithProgress',
  379. 'Installers:',
  380. '- Architecture: x86',
  381. ' Scope: user',
  382. ' InstallerType: nullsoft',
  383. ' InstallerUrl: ' + url,
  384. ' InstallerSha256: ' + sha256.toUpperCase(),
  385. ' InstallerLocale: en-US',
  386. ' InstallerSwitches:',
  387. ' Custom: /NORESTART',
  388. ' UpgradeBehavior: install',
  389. '- Architecture: arm64',
  390. ' Scope: user',
  391. ' InstallerType: nullsoft',
  392. ' InstallerUrl: ' + url,
  393. ' InstallerSha256: ' + sha256.toUpperCase(),
  394. ' InstallerLocale: en-US',
  395. ' InstallerSwitches:',
  396. ' Custom: /NORESTART',
  397. ' UpgradeBehavior: install',
  398. 'FileExtensions:',
  399. extensions,
  400. 'ManifestType: installer',
  401. 'ManifestVersion: 1.2.0',
  402. ''
  403. ].join('\n'));
  404. await fs.writeFile(manifestFile + '.locale.en-US.yaml', [
  405. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.2.0.schema.json',
  406. 'PackageIdentifier: ' + identifier,
  407. 'PackageVersion: ' + version,
  408. 'PackageName: ' + product,
  409. 'PackageLocale: en-US',
  410. 'PackageUrl: ' + repository,
  411. 'Publisher: ' + publisher,
  412. 'PublisherUrl: ' + repository,
  413. 'PublisherSupportUrl: ' + repository + '/issues',
  414. 'Author: ' + publisher,
  415. 'License: ' + configuration.license,
  416. 'Copyright: ' + copyright,
  417. 'CopyrightUrl: ' + repository + '/blob/main/LICENSE',
  418. 'ShortDescription: ' + configuration.description,
  419. 'Description: ' + configuration.description,
  420. 'Moniker: ' + name,
  421. 'Tags:',
  422. '- machine-learning',
  423. '- deep-learning',
  424. '- neural-network',
  425. 'ManifestType: defaultLocale',
  426. 'ManifestVersion: 1.2.0',
  427. ''
  428. ].join('\n'));
  429. writeLine('git push winget-pkgs');
  430. await exec('git -C dist/winget-pkgs add --all');
  431. await exec('git -C dist/winget-pkgs commit -m "Update ' + configuration.name + ' to ' + configuration.version + '"');
  432. await pullrequest('microsoft', 'winget-pkgs', {
  433. title: 'Update ' + configuration.productName + ' to ' + configuration.version,
  434. body: '',
  435. head: process.env.GITHUB_USER + ':master',
  436. base: 'master'
  437. });
  438. await rm('dist', 'winget-pkgs');
  439. break;
  440. }
  441. default: {
  442. writeLine('publish');
  443. await rm('dist');
  444. await install();
  445. await publish('web');
  446. await publish('electron');
  447. await publish('python');
  448. await publish('cask');
  449. await publish('winget');
  450. break;
  451. }
  452. }
  453. };
  454. const lint = async () => {
  455. await install();
  456. writeLine('eslint');
  457. await exec('npx eslint *.js source/*.js test/*.js publish/*.js tools/*.js');
  458. writeLine('pylint');
  459. await exec('python -m pip install --upgrade --quiet pylint');
  460. await exec('python -m pylint -sn --recursive=y source test publish tools *.py');
  461. };
  462. const update = async () => {
  463. const targets = process.argv.length > 3 ? process.argv.slice(3) : [
  464. 'armnn',
  465. 'bigdl',
  466. 'caffe',
  467. 'circle',
  468. 'cntk',
  469. 'coreml',
  470. 'dlc',
  471. 'dnn',
  472. 'mnn',
  473. 'mslite',
  474. 'megengine',
  475. 'nnabla',
  476. 'onnx',
  477. 'om',
  478. 'paddle',
  479. 'pytorch',
  480. 'rknn',
  481. 'sklearn',
  482. 'tf',
  483. 'uff',
  484. 'xmodel'
  485. ];
  486. for (const target of targets) {
  487. /* eslint-disable no-await-in-loop */
  488. await exec('tools/' + target + ' sync install schema metadata');
  489. /* eslint-enable no-await-in-loop */
  490. }
  491. };
  492. const pull = async () => {
  493. await exec('git fetch --prune origin "refs/tags/*:refs/tags/*"');
  494. const before = await exec('git rev-parse HEAD', 'utf-8');
  495. await exec('git pull --prune --rebase');
  496. const after = await exec('git rev-parse HEAD', 'utf-8');
  497. if (before.trim() !== after.trim()) {
  498. const output = await exec('git diff --name-only ' + before.trim() + ' ' + after.trim(), 'utf-8');
  499. const files = new Set(output.split('\n'));
  500. if (files.has('package.json')) {
  501. await clean();
  502. await install();
  503. }
  504. }
  505. };
  506. const coverage = async () => {
  507. await rm('.nyc_output');
  508. await rm('coverage');
  509. await rm('dist', 'nyc');
  510. await mkdir('dist', 'nyc');
  511. await exec('cp package.json dist/nyc');
  512. await exec('cp -R source dist/nyc');
  513. await exec('nyc instrument --compact false source dist/nyc/source');
  514. await exec('nyc --reporter=lcov --instrument npx electron ./dist/nyc');
  515. };
  516. const analyze = async () => {
  517. const exists = await access('third_party/tools/codeql');
  518. if (!exists) {
  519. await exec('git clone --depth=1 https://github.com/github/codeql.git third_party/tools/codeql');
  520. }
  521. await rm('dist', 'codeql');
  522. await mkdir('dist', 'codeql', 'netron');
  523. await exec('cp -r publish source test tools dist/codeql/netron/');
  524. await exec('codeql database create dist/codeql/database --source-root dist/codeql/netron --language=javascript --threads=3');
  525. await exec('codeql database analyze dist/codeql/database ./third_party/tools/codeql/javascript/ql/src/codeql-suites/javascript-security-and-quality.qls --format=csv --output=dist/codeql/results.csv --threads=3');
  526. await exec('cat dist/codeql/results.csv');
  527. };
  528. const version = async () => {
  529. const file = path.join(__dirname, 'package.json');
  530. let content = await fs.readFile(file, 'utf-8');
  531. content = content.replace(/(\s*"version":\s")(\d\.\d\.\d)(",)/m, (match, p1, p2, p3) => {
  532. const version = Array.from((parseInt(p2.split('.').join(''), 10) + 1).toString()).join('.');
  533. return p1 + version + p3;
  534. });
  535. content = content.replace(/(\s*"date":\s")(.*)(",)/m, (match, p1, p2, p3) => {
  536. const date = new Date().toISOString().split('.').shift().split('T').join(' ');
  537. return p1 + date + p3;
  538. });
  539. await fs.writeFile(file, content, 'utf-8');
  540. await load();
  541. await exec('git add package.json');
  542. await exec('git commit -m "Update to ' + configuration.version + '"');
  543. await exec('git tag v' + configuration.version);
  544. await exec('git push');
  545. await exec('git push --tags');
  546. };
  547. const next = async () => {
  548. try {
  549. const task = read();
  550. switch (task) {
  551. case 'start': await start(); break;
  552. case 'clean': await clean(); break;
  553. case 'purge': await purge(); break;
  554. case 'build': await build(); break;
  555. case 'publish': await publish(); break;
  556. case 'version': await version(); break;
  557. case 'lint': await lint(); break;
  558. case 'update': await update(); break;
  559. case 'pull': await pull(); break;
  560. case 'analyze': await analyze(); break;
  561. case 'coverage': await coverage(); break;
  562. default: throw new Error("Unsupported task '" + task + "'.");
  563. }
  564. } catch (err) {
  565. if (process.stdout.write) {
  566. process.stdout.write(err.message + os.EOL);
  567. }
  568. process.exit(1);
  569. }
  570. };
  571. load();
  572. next();