dependency-sources.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. import assert from 'node:assert/strict'
  4. import { createHash } from 'node:crypto'
  5. import { execFileSync } from 'node:child_process'
  6. async function download(url) {
  7. assert.ok(url.startsWith('https://registry.npmjs.org/') || url.startsWith('https://codeload.github.com/') || url.startsWith('https://code.haverbeke.berlin/'), 'Unexpected source host')
  8. const response = await fetch(url, { signal: AbortSignal.timeout(120000) })
  9. if (!response.ok) throw new Error(`Source download failed (${response.status}): ${url}`)
  10. return Buffer.from(await response.arrayBuffer())
  11. }
  12. /** Publish package distributions and pinned upstream sources, never local paths. */
  13. export async function collectDependencySources(root, output) {
  14. const components = new Map()
  15. for (const pkg of ['bundle', 'embedding-provider']) {
  16. const entries = JSON.parse(fs.readFileSync(path.join(root, 'packages', pkg, 'lib/third-party-inputs.json'), 'utf8'))
  17. for (const entry of entries) components.set(`${entry.name}@${entry.version}`, entry)
  18. }
  19. const directory = path.join(output, 'third-party-sources')
  20. fs.mkdirSync(directory)
  21. const upstream = new Map()
  22. const report = []
  23. const pinned = JSON.parse(fs.readFileSync(path.join(root, 'scripts/release/upstream-sources.json'), 'utf8'))
  24. for (const entry of components.values()) {
  25. const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(entry.name)}/${encodeURIComponent(entry.version)}`, { signal: AbortSignal.timeout(30000) })
  26. if (!response.ok) throw new Error(`Package source metadata unavailable: ${entry.name}@${entry.version}`)
  27. const metadata = await response.json()
  28. const tarball = await download(metadata.dist.tarball)
  29. const integrity = metadata.dist.integrity
  30. if (integrity) {
  31. const [algorithm, value] = integrity.split(' ')[0].split('-')
  32. assert.ok(['sha512', 'sha256', 'sha1'].includes(algorithm))
  33. assert.equal(createHash(algorithm).update(tarball).digest('base64'), value, 'Registry source integrity mismatch')
  34. } else assert.equal(createHash('sha1').update(tarball).digest('hex'), metadata.dist.shasum)
  35. const filename = `${entry.name.replaceAll('@', '').replaceAll('/', '__')}--${entry.version}.tgz`
  36. fs.writeFileSync(path.join(directory, filename), tarball)
  37. const repository = typeof metadata.repository === 'string' ? metadata.repository : metadata.repository?.url
  38. const github = /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#.*)?$/.exec(repository ?? '')
  39. const haverbeke = /code\.haverbeke\.berlin\/([\w.-]+)\/([\w.-]+?)(?:\.git)?$/.exec(repository ?? '')
  40. const override = pinned[`${entry.name}@${entry.version}`]
  41. const commit = override?.commit ?? metadata.gitHead
  42. assert.match(commit ?? '', /^[a-f0-9]{40}$/, `Pin upstream source for ${entry.name}@${entry.version}`)
  43. let upstreamFile = null, sourceUrl = null
  44. const upstreamRepo = override?.repository ?? (github ? `${github[1]}/${github[2]}` : null)
  45. if (upstreamRepo || haverbeke) {
  46. const key = `${upstreamRepo ?? `${haverbeke[1]}/${haverbeke[2]}`}-${commit}`.replaceAll('/', '-')
  47. sourceUrl = upstreamRepo ? `https://codeload.github.com/${upstreamRepo}/tar.gz/${commit}`
  48. : `https://code.haverbeke.berlin/${haverbeke[1]}/${haverbeke[2]}/archive/${commit}.tar.gz`
  49. if (!upstream.has(key)) {
  50. upstreamFile = `${key}.tar.gz`
  51. fs.writeFileSync(path.join(directory, upstreamFile), await download(sourceUrl))
  52. upstream.set(key, upstreamFile)
  53. } else upstreamFile = upstream.get(key)
  54. }
  55. assert.ok(upstreamFile, `No corresponding upstream source for ${entry.name}@${entry.version}`)
  56. report.push({ name: entry.name, version: entry.version, license: entry.license, repository,
  57. packageArchive: filename, packageIntegrity: integrity ?? `sha1:${metadata.dist.shasum}`,
  58. upstreamArchive: upstreamFile, upstreamUrl: sourceUrl, upstreamCommit: commit,
  59. upstreamSha256: createHash('sha256').update(fs.readFileSync(path.join(directory, upstreamFile))).digest('hex'),
  60. note: 'Pinned upstream source and registry distribution included.' })
  61. console.log(`[source] ${entry.name}@${entry.version}: package${upstreamFile ? ' + upstream' : ''}`)
  62. }
  63. fs.writeFileSync(path.join(directory, 'manifest.json'), JSON.stringify(report, null, 2) + '\n')
  64. fs.writeFileSync(path.join(directory, 'README.txt'), 'Corresponding third-party source materials for this release. See manifest.json for exact versions, original integrity and pinned upstream archives. Original licenses are also shipped with the application.\n')
  65. execFileSync('tar', ['-czf', 'third-party-sources.tar.gz', 'third-party-sources'], { cwd: output, windowsHide: true })
  66. return report
  67. }