utils.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import fs from 'node:fs/promises'
  2. import { spawn } from 'node:child_process'
  3. import { envVars } from './config'
  4. import { downloadFile } from './download'
  5. export async function fileExistsAsync(filePath: string): Promise<boolean> {
  6. try {
  7. await fs.access(filePath)
  8. return true
  9. }
  10. catch {
  11. return false
  12. }
  13. }
  14. export async function removeDir(dir: string): Promise<void> {
  15. await fs.rm(dir, { recursive: true, force: true })
  16. }
  17. export async function createDir(dirPath: string): Promise<void> {
  18. await fs.mkdir(dirPath, { recursive: true })
  19. }
  20. export async function runCommand(
  21. command: string,
  22. args: string[] = [],
  23. options: {
  24. task?: { output?: string }
  25. env?: NodeJS.ProcessEnv
  26. disableOutput?: boolean
  27. maxOutputLines?: number
  28. } = { disableOutput: false },
  29. ): Promise<void> {
  30. if (!options.env) options.env = envVars
  31. const maxOutputLines = options.maxOutputLines ?? 10
  32. return new Promise((resolve, reject) => {
  33. const finalEnv = { ...envVars, ...(options.env || {}) }
  34. let outputLines: string[] = [`$ ${command} ${args.join(' ')}`]
  35. let remainingData = ''
  36. const child = spawn(command, args, { env: finalEnv, shell: true })
  37. const updateOutput = (data: Buffer) => {
  38. const text = data.toString()
  39. const fullText = remainingData + text
  40. const lines = fullText.split('\n')
  41. if (!fullText.endsWith('\n')) {
  42. remainingData = lines.pop() ?? ''
  43. }
  44. else {
  45. remainingData = ''
  46. }
  47. outputLines.push(...lines)
  48. if (outputLines.length > maxOutputLines) {
  49. outputLines = outputLines.slice(outputLines.length - maxOutputLines)
  50. }
  51. if (options.task) {
  52. options.task.output = outputLines.join('\n')
  53. }
  54. }
  55. if (!options.disableOutput) {
  56. child.stdout.on('data', updateOutput)
  57. child.stderr.on('data', updateOutput)
  58. }
  59. child.on('error', reject)
  60. child.on('close', (code) => {
  61. if (remainingData) {
  62. outputLines.push(remainingData)
  63. }
  64. if (outputLines.length > maxOutputLines) {
  65. outputLines = outputLines.slice(outputLines.length - maxOutputLines)
  66. }
  67. if (options.task) {
  68. options.task.output = outputLines.join('\n')
  69. }
  70. if (code === 0) {
  71. resolve()
  72. }
  73. else {
  74. reject(
  75. new Error(
  76. `Command "${command} ${args.join(' ')}" exited with code ${code}\nOutput:\n${outputLines.join('\n')}\nENV: ${JSON.stringify(options.env)}`,
  77. ),
  78. )
  79. }
  80. })
  81. })
  82. }
  83. export async function extractArchive(
  84. archiveName: string,
  85. extractCommand: string[],
  86. options: { task?: { output?: string } } = {},
  87. ) {
  88. // TODO: extract zip using library
  89. await runCommand('bash', ['-c', `"${extractCommand.join(' && ')}"`], options)
  90. }
  91. export async function ensureDownloadedAndExtracted(
  92. filePath: string,
  93. downloadUrl: string,
  94. archiveName: string,
  95. extractCommand: string[],
  96. options: { task?: { output?: string } } = {},
  97. ): Promise<void> {
  98. if (!(await fileExistsAsync(filePath))) {
  99. await downloadFile(options.task, downloadUrl, archiveName)
  100. await extractArchive(archiveName, extractCommand, options)
  101. }
  102. }