-
-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathinstall-binary.mjs
196 lines (162 loc) · 4.67 KB
/
install-binary.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#!/usr/bin/env node
'use strict'
import { spawn } from 'node:child_process'
import {
chmodSync,
createWriteStream,
existsSync,
mkdirSync,
renameSync,
} from 'node:fs'
import { dirname, join } from 'node:path'
import { Readable } from 'node:stream'
import { fileURLToPath } from 'node:url'
import { extract } from 'tar'
import { temporaryDirectory } from 'tempy'
const PLATFORM = process.platform
const ARCH = process.arch
const JQ_INFO = {
name: 'jq',
url: 'https://github.com/jqlang/jq/releases/download/',
version: 'jq-1.7.1',
}
const JQ_NAME_MAP = {
def: 'jq',
win32: 'jq.exe',
}
const JQ_NAME =
PLATFORM in JQ_NAME_MAP ? JQ_NAME_MAP[PLATFORM] : JQ_NAME_MAP.def
const __dirname = dirname(fileURLToPath(import.meta.url))
const OUTPUT_DIR = join(__dirname, '..', 'bin')
const fileExist = (path) => {
try {
return existsSync(path)
} catch (err) {
return false
}
}
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR)
console.info(`${OUTPUT_DIR} directory was created`)
}
if (fileExist(join(OUTPUT_DIR, JQ_NAME))) {
console.log('jq is already installed')
process.exit(0)
}
if (process.env.NODE_JQ_SKIP_INSTALL_BINARY === 'true') {
console.log('node-jq is skipping the download of jq binary')
process.exit(0)
}
// if platform or arch is missing, download source instead of executable
const DOWNLOAD_MAP = {
win32: {
x64: 'jq-windows-amd64.exe',
ia32: 'jq-windows-i386.exe',
},
darwin: {
x64: 'jq-macos-amd64',
arm64: 'jq-macos-arm64',
},
linux: {
x64: 'jq-linux-amd64',
ia32: 'jq-linux-i386',
arm64: 'jq-linux-arm64',
},
}
try {
if (PLATFORM in DOWNLOAD_MAP && ARCH in DOWNLOAD_MAP[PLATFORM]) {
const filename = DOWNLOAD_MAP[PLATFORM][ARCH]
const url = `${JQ_INFO.url}${JQ_INFO.version}/${filename}`
downloadJqBinary(url, filename)
} else {
const url = `${JQ_INFO.url}${JQ_INFO.version}/${JQ_INFO.version}.tar.gz`
buildJqSource(url)
}
} catch (err) {
console.error(err)
process.exit(1)
}
async function downloadJqBinary(url, filename) {
console.log(`Downloading jq from ${url}`)
await downloadFile(url, `${OUTPUT_DIR}/${filename}`)
const distPath = join(OUTPUT_DIR, JQ_NAME)
renameSync(join(OUTPUT_DIR, filename), distPath)
if (fileExist(distPath)) {
// fs.chmodSync(distPath, fs.constants.S_IXUSR || 0o100)
// Huan(202111): we need the read permission so that the build system can pack the node_modules/ folder,
// i.e. build with Heroku CI/CD, docker build, etc.
chmodSync(distPath, 0o755)
}
console.log(`Downloaded in ${OUTPUT_DIR}`)
}
async function buildJqSource(url) {
const tempDir = temporaryDirectory()
console.log(`Building jq from ${url}`)
try {
const tarballPath = join(tempDir, `${JQ_INFO.version}.tar.gz`)
await downloadFile(url, tarballPath)
await extract({
file: tarballPath,
cwd: tempDir,
})
const sourceDir = join(tempDir, JQ_INFO.version)
await runCommand(
'./configure',
[
'--with-oniguruma=builtin',
`--prefix=${tempDir}`,
`--bindir=${OUTPUT_DIR}`,
],
{ cwd: sourceDir },
)
await runCommand('make', ['-j8'], { cwd: sourceDir })
await runCommand('make', ['install'], { cwd: sourceDir })
console.log(`jq installed successfully in ${OUTPUT_DIR}`)
} catch (err) {
console.error(err)
process.exit(1)
}
}
async function downloadFile(url, dest) {
const res = await fetch(url)
if (!res.ok) throw new Error(`Failed to download jq: ${res.statusText}`)
const totalSize = parseInt(res.headers.get('content-length'), 10)
let downloadedSize = 0
const fileStream = createWriteStream(dest)
const dataStream = Readable.from(res.body)
return new Promise((resolve, reject) => {
dataStream.on('data', (chunk) => {
downloadedSize += chunk.length
const percentage = ((downloadedSize / totalSize) * 100).toFixed(2)
process.stdout.write(`Downloading jq: ${percentage}%\r`)
})
dataStream.pipe(fileStream)
dataStream.on('end', () => {
console.log('\nDownload complete')
fileStream.end()
resolve()
})
dataStream.on('error', (err) => {
fileStream.end()
reject(err)
})
})
}
async function runCommand(command, args, options) {
return new Promise((resolve, reject) => {
const proc = spawn(command, args, options)
proc.stdout.on('data', (data) => {
process.stdout.write(`${command}: ${data}`)
})
proc.stderr.on('data', (data) => {
process.stderr.write(`${command}: error: ${data}`)
})
proc.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Command failed with exit code ${code}`))
} else {
resolve()
}
})
})
}