utils.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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 renameWithOverwrite(oldPath: string, newPath: string): Promise<void> {
  15. try {
  16. try {
  17. await fs.access(newPath);
  18. await fs.unlink(newPath);
  19. } catch (err) {
  20. if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
  21. throw err;
  22. }
  23. }
  24. await fs.rename(oldPath, newPath);
  25. } catch (err) {
  26. const error = err as NodeJS.ErrnoException;
  27. throw error;
  28. }
  29. }
  30. export async function removeDir(dir: string): Promise<void> {
  31. await fs.rm(dir, { recursive: true, force: true })
  32. }
  33. export async function createDir(dirPath: string): Promise<void> {
  34. await fs.mkdir(dirPath, { recursive: true })
  35. }
  36. export async function runCommand(
  37. command: string,
  38. args: string[] = [],
  39. options: {
  40. task?: { output?: string }
  41. env?: NodeJS.ProcessEnv
  42. disableOutput?: boolean
  43. maxOutputLines?: number
  44. } = { disableOutput: false },
  45. ): Promise<void> {
  46. if (!options.env) options.env = envVars
  47. const maxOutputLines = options.maxOutputLines ?? 10
  48. return new Promise((resolve, reject) => {
  49. const finalEnv = { ...envVars, ...(options.env || {}) }
  50. let outputLines: string[] = [`$ ${command} ${args.join(' ')}`]
  51. let remainingData = ''
  52. const child = spawn(command, args, { env: finalEnv, shell: true })
  53. const updateOutput = (data: Buffer) => {
  54. const text = data.toString()
  55. const fullText = remainingData + text
  56. const lines = fullText.split('\n')
  57. if (!fullText.endsWith('\n')) {
  58. remainingData = lines.pop() ?? ''
  59. }
  60. else {
  61. remainingData = ''
  62. }
  63. outputLines.push(...lines)
  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. }
  71. if (!options.disableOutput) {
  72. child.stdout.on('data', updateOutput)
  73. child.stderr.on('data', updateOutput)
  74. }
  75. child.on('error', reject)
  76. child.on('close', (code) => {
  77. if (remainingData) {
  78. outputLines.push(remainingData)
  79. }
  80. if (outputLines.length > maxOutputLines) {
  81. outputLines = outputLines.slice(outputLines.length - maxOutputLines)
  82. }
  83. if (options.task) {
  84. options.task.output = outputLines.join('\n')
  85. }
  86. if (code === 0) {
  87. resolve()
  88. }
  89. else {
  90. reject(
  91. new Error(
  92. `Command "${command} ${args.join(' ')}" exited with code ${code}\nOutput:\n${outputLines.join('\n')}\nENV: ${JSON.stringify(options.env)}`,
  93. ),
  94. )
  95. }
  96. })
  97. })
  98. }
  99. export async function extractArchive(
  100. archiveName: string,
  101. extractCommand: string[],
  102. options: { task?: { output?: string } } = {},
  103. ) {
  104. // TODO: extract zip using library
  105. await runCommand('bash', ['-c', `"${extractCommand.join(' && ')}"`], options)
  106. }
  107. export async function ensureDownloadedAndExtracted(
  108. filePath: string,
  109. downloadUrl: string,
  110. archiveName: string,
  111. extractCommand: string[],
  112. options: { task?: { output?: string } } = {},
  113. ): Promise<void> {
  114. if (!(await fileExistsAsync(filePath))) {
  115. await downloadFile(options.task, downloadUrl, archiveName)
  116. await extractArchive(archiveName, extractCommand, options)
  117. }
  118. }