Files: ca90b5ca0579ebdcc0635288bce4978e3fb200f5 / prelude / install.js
12111 bytesRaw
1 | var os = require('os') |
2 | var fs = require('fs') |
3 | var zlib = require('zlib') |
4 | var http = require('http') |
5 | var https = require('https') |
6 | var stream = require('stream') |
7 | var path = require('path') |
8 | var STORE_CONTENT = 1 |
9 | |
10 | var settings = JSON.parse(fs.readFileSync(3)) |
11 | |
12 | function httpGet(url, cb) { |
13 | return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) |
14 | } |
15 | |
16 | function ReadBlobs(links) { |
17 | if (!Array.isArray(links)) throw new TypeError('blob links should be array') |
18 | this.links = links |
19 | stream.Readable.call(this, links) |
20 | } |
21 | ReadBlobs.prototype = new stream.Readable() |
22 | ReadBlobs.prototype.constructor = ReadBlobs |
23 | ReadBlobs.prototype.i = 0 |
24 | ReadBlobs.prototype.blobsBase = process.env.SSB_BLOBS_BASE |
25 | || 'http://localhost:8989/blobs/get/' |
26 | ReadBlobs.prototype._read = function (size) { |
27 | if (!this.req) this._makeReq(this) |
28 | else if (this.res) this.res.resume() |
29 | } |
30 | ReadBlobs.prototype._destroy = function (err, cb) { |
31 | if (this.req) this.req.destroy(), this.req = null |
32 | if (this.res) this.res.destroy(), this.res = null |
33 | cb(err) |
34 | } |
35 | ReadBlobs.prototype._makeReq = function () { |
36 | if (this.i >= this.links.length) return this.push(null) |
37 | var link = this.links[this.i++] |
38 | var id = typeof link === 'string' ? link : link && link.link |
39 | var url = this.blobsBase + id |
40 | console.error(id) |
41 | var onResp = (res) => { |
42 | this.res = res |
43 | if (res.statusCode !== 200) { |
44 | var reqStatus = res.statusCode + ' ' + res.statusMessage |
45 | return this.destroy(new Error(reqStatus)) |
46 | } |
47 | res.on('data', (data) => { |
48 | if (this.push(data) === false) res.pause() |
49 | }) |
50 | res.on('end', () => { |
51 | this._makeReq() |
52 | }) |
53 | } |
54 | var onError = (err) => { |
55 | if (err.code === 'ECONNRESET') { |
56 | // retry after timeout |
57 | setTimeout(() => { |
58 | this.req = httpGet(url, onResp).on('error', onError) |
59 | }, 1e3) |
60 | } else if (err.code === 'ECONNREFUSED' && err.address === '127.0.0.1' |
61 | && this.blobsBase.indexOf('localhost') > -1 |
62 | ) { |
63 | // sometimes localhost resolves to 127.0.0.1 but ssb-ws listens on ::1 |
64 | this.blobsBase = this.blobsBase.replace('localhost', '[::1]') |
65 | url = this.blobsBase + id |
66 | httpGet(url, onResp).on('error', onError) |
67 | } else { |
68 | this.destroy(err) |
69 | } |
70 | } |
71 | this.req = httpGet(url, onResp).on('error', onError) |
72 | } |
73 | |
74 | function Meter() { |
75 | stream.Transform.call(this) |
76 | } |
77 | Meter.prototype = new stream.Transform() |
78 | Meter.prototype.constructor = Meter |
79 | Meter.prototype.length = 0 |
80 | Meter.prototype._transform = function (buf, encoding, cb) { |
81 | this.length += buf.length |
82 | cb(null, buf) |
83 | } |
84 | |
85 | function collect(cb) { |
86 | var bufs = [] |
87 | return new stream.Writable({ |
88 | write: function (chunk, encoding, cb) { |
89 | bufs.push(Buffer.from(chunk, encoding)) |
90 | cb() |
91 | } |
92 | }).on('finish', function () { |
93 | cb(null, Buffer.concat(bufs)) |
94 | }).on('error', cb) |
95 | } |
96 | |
97 | function substitute(template, vars) { |
98 | return Object.keys(vars).reduce(function (str, key) { |
99 | return str.replace(key, vars[key]) |
100 | }, template) |
101 | } |
102 | |
103 | function discoverPlaceholder (binaryBuffer, searchString, padder) { |
104 | const placeholder = Buffer.from(searchString); |
105 | const position = binaryBuffer.indexOf(placeholder); |
106 | if (position === -1) return { notFound: true }; |
107 | return { position, size: placeholder.length, padder }; |
108 | } |
109 | |
110 | function injectPlaceholder (fd, placeholder, value, cb) { |
111 | const { notFound, position, size, padder } = placeholder; |
112 | if (notFound) assert(false, 'Placeholder for not found'); |
113 | if (typeof value === 'number') value = value.toString(); |
114 | if (typeof value === 'string') value = Buffer.from(value); |
115 | const padding = Buffer.from(padder.repeat(size - value.length)); |
116 | value = Buffer.concat([ value, padding ]); |
117 | fs.write(fd, value, 0, value.length, position, cb); |
118 | } |
119 | |
120 | function discoverPlaceholders (binaryBuffer) { |
121 | return { |
122 | BAKERY: discoverPlaceholder(binaryBuffer, '\0' + '// BAKERY '.repeat(20), '\0'), |
123 | PAYLOAD_POSITION: discoverPlaceholder(binaryBuffer, '// PAYLOAD_POSITION //', ' '), |
124 | PAYLOAD_SIZE: discoverPlaceholder(binaryBuffer, '// PAYLOAD_SIZE //', ' '), |
125 | PRELUDE_POSITION: discoverPlaceholder(binaryBuffer, '// PRELUDE_POSITION //', ' '), |
126 | PRELUDE_SIZE: discoverPlaceholder(binaryBuffer, '// PRELUDE_SIZE //', ' ') |
127 | }; |
128 | } |
129 | |
130 | function injectPlaceholders (fd, placeholders, values, cb) { |
131 | injectPlaceholder(fd, placeholders.BAKERY, values.BAKERY, (error) => { |
132 | if (error) return cb(error); |
133 | injectPlaceholder(fd, placeholders.PAYLOAD_POSITION, values.PAYLOAD_POSITION, (error2) => { |
134 | if (error2) return cb(error2); |
135 | injectPlaceholder(fd, placeholders.PAYLOAD_SIZE, values.PAYLOAD_SIZE, (error3) => { |
136 | if (error3) return cb(error3); |
137 | injectPlaceholder(fd, placeholders.PRELUDE_POSITION, values.PRELUDE_POSITION, (error4) => { |
138 | if (error4) return cb(error4); |
139 | injectPlaceholder(fd, placeholders.PRELUDE_SIZE, values.PRELUDE_SIZE, cb); |
140 | }); |
141 | }); |
142 | }); |
143 | }); |
144 | } |
145 | |
146 | function makeBakeryValueFromBakes (bakes) { |
147 | const parts = []; |
148 | if (bakes.length) { |
149 | for (let i = 0; i < bakes.length; i += 1) { |
150 | parts.push(Buffer.from(bakes[i])); |
151 | parts.push(Buffer.alloc(1)); |
152 | } |
153 | parts.push(Buffer.alloc(1)); |
154 | } |
155 | return Buffer.concat(parts); |
156 | } |
157 | |
158 | function makePreludeBufferFromPrelude (prelude) { |
159 | return Buffer.from( |
160 | '(function(process, require, console, EXECPATH_FD, PAYLOAD_POSITION, PAYLOAD_SIZE) { ' + |
161 | prelude + |
162 | '\n})' // dont remove \n |
163 | ); |
164 | } |
165 | |
166 | function plusx(file) { |
167 | const s = fs.statSync(file) |
168 | const newMode = s.mode | 64 | 8 | 1 |
169 | if (s.mode === newMode) return |
170 | const base8 = newMode.toString(8).slice(-3) |
171 | fs.chmodSync(file, base8) |
172 | } |
173 | |
174 | function mkdirp(dir) { |
175 | try { fs.mkdirSync(path.dirname(path.dirname(dir))) } catch(e) {} |
176 | try { fs.mkdirSync(path.dirname(dir)) } catch(e) {} |
177 | fs.mkdirSync(dir) |
178 | console.error(dir) |
179 | } |
180 | |
181 | function writableDir(dir) { |
182 | try { |
183 | fs.accessSync(dir, fs.constants.W_OK) |
184 | return true |
185 | } catch(e) { |
186 | if (e.code === 'ENOENT') { |
187 | try { |
188 | mkdirp(dir) |
189 | return true |
190 | } catch(e) {} |
191 | } |
192 | } |
193 | } |
194 | |
195 | function first(array, fn) { |
196 | for (var i = 0; i < array.length; i++) { |
197 | var value = array[i] |
198 | var result = fn(value) |
199 | if (result) return value |
200 | } |
201 | } |
202 | |
203 | function isReadable(file) { |
204 | try { |
205 | fs.accessSync(file, fs.constants.R_OK) |
206 | return true |
207 | } catch(e) {} |
208 | } |
209 | |
210 | function readProfile() { |
211 | try { |
212 | var profile = first([ |
213 | process.env.PROFILE, |
214 | path.join(homeDir, '.profile') |
215 | ].filter(Boolean), isReadable) |
216 | if (profile) return fs.readFileSync(profile, 'utf8') |
217 | } catch(e) {} |
218 | } |
219 | |
220 | function useConditionalDirFromProfile() { |
221 | // Use ~/.local/bin or ~/bin if ~/.profile would detect it. |
222 | |
223 | var profile = readProfile() |
224 | if (!profile) return |
225 | |
226 | if (profile.indexOf( |
227 | 'if [ -d "$HOME/.local/bin" ] ; then\n PATH="$HOME/.local/bin:$PATH"\nfi' |
228 | ) > 0) { |
229 | dir = path.join(homeDir, '.local', 'bin') |
230 | if (writableDir(dir)) return dir |
231 | } |
232 | |
233 | if (profile.indexOf( |
234 | 'if [ -d "$HOME/bin" ] ; then\n PATH="$HOME/bin:$PATH"\nfi' |
235 | ) > 0) { |
236 | dir = path.join(homeDir, 'bin') |
237 | if (writableDir(dir)) return dir |
238 | } |
239 | } |
240 | |
241 | function pickBinDir() { |
242 | var isRoot = (process.env.UID || process.geteuid()) === 0 |
243 | var userBinDirs = isRoot ? [ |
244 | // If root user, install system-wide. |
245 | ] : [ |
246 | path.join(homeDir, '.local', 'bin'), |
247 | path.join(homeDir, 'bin') |
248 | ] |
249 | var favoriteDirs = [ |
250 | process.env.PREFIX && path.join(process.env.PREFIX, 'bin') |
251 | ].concat(userBinDirs).concat([ |
252 | '/usr/local/bin', |
253 | '/opt/bin' |
254 | ]).filter(Boolean) |
255 | |
256 | var dirsInPath = process.env.PATH ? process.env.PATH.split(':') : [] |
257 | function isDirInPath(dir) { |
258 | return dirsInPath.indexOf(dir) > -1 |
259 | } |
260 | var favoriteDirsInPath = favoriteDirs.filter(isDirInPath) |
261 | var dir = first(favoriteDirsInPath, writableDir) |
262 | if (dir) return dir |
263 | |
264 | dir = useConditionalDirFromProfile() |
265 | if (dir) { |
266 | console.error('Note: restart your shell to use new $PATH') |
267 | return dir |
268 | } |
269 | |
270 | dir = first(favoriteDirs, writableDir) |
271 | if (dir) { |
272 | console.error(`Warning: installing to directory not in $PATH. |
273 | Run this and add it to your ~/.profile or similar: |
274 | export PATH="${dir.replace(homeDir, '$HOME')}:$PATH"`) |
275 | return dir |
276 | } |
277 | |
278 | console.error('Error: no directory to install to. Set $BIN_DIR, or add ' |
279 | + (isRoot ? '/usr/local/bin' : '~/.local/bin') |
280 | + ' to your $PATH, and make sure you can write to it.') |
281 | process.exit(1) |
282 | } |
283 | |
284 | var homeDir = process.env.HOME || os.homedir() |
285 | var binDir = process.env.BIN_DIR || pickBinDir() |
286 | var binPath = path.join(binDir, settings.binName) |
287 | var binTmp = binPath + '~' |
288 | |
289 | // assume the node binary is the pkg one to use |
290 | var nodeBuffer = fs.readFileSync(process.execPath) |
291 | |
292 | var placeholders = discoverPlaceholders(nodeBuffer) |
293 | var payloadPosition = nodeBuffer.length, payloadSize |
294 | var preludePosition, preludeSize |
295 | var vfs |
296 | var binFd, writeStream |
297 | |
298 | try { |
299 | fs.statSync(binDir) |
300 | } catch(e) { |
301 | mkdirp(binDir) |
302 | } |
303 | |
304 | function writeNodeBinary() { |
305 | fs.open(binTmp, 'w+', function (err, fd) { |
306 | if (err) throw err |
307 | binFd = fd |
308 | writeStream = fs.createWriteStream(null, {fd: fd}) |
309 | writeStream.write(nodeBuffer, function (err) { |
310 | if (err) throw err |
311 | getVfs() |
312 | }) |
313 | }) |
314 | } |
315 | |
316 | function getVfs() { |
317 | new ReadBlobs(settings.vfsBlobs) |
318 | .pipe(collect(function (err, vfsData) { |
319 | if (err) throw err |
320 | vfs = JSON.parse(vfsData) |
321 | getPayload() |
322 | })) |
323 | } |
324 | |
325 | function getPayload() { |
326 | var payloadMeter = new Meter() |
327 | var readPayload = new ReadBlobs(settings.payloadBlobs) |
328 | .pipe(zlib.createGunzip()) |
329 | .pipe(payloadMeter) |
330 | readPayload.pipe(writeStream, {end: false}) |
331 | readPayload.on('end', function () { |
332 | payloadSize = payloadMeter.length |
333 | addFileBlobs() |
334 | }) |
335 | } |
336 | |
337 | function addFileBlobs() { |
338 | var offset = payloadSize |
339 | var fileBlobs = settings.fileBlobs |
340 | var queue = [] |
341 | function enqueue(fileBlobsToAdd) { |
342 | for (var snap in fileBlobsToAdd) { |
343 | var ids = fileBlobsToAdd[snap] |
344 | queue.push({snap: snap, ids: ids}) |
345 | } |
346 | } |
347 | if (Array.isArray(fileBlobs)) { |
348 | enqueue(fileBlobs) |
349 | } else [ |
350 | os.platform() + '-' + os.arch(), |
351 | '*' |
352 | ].forEach(function (group) { |
353 | enqueue(fileBlobs[group]) |
354 | }) |
355 | ;(function next() { |
356 | if (!queue.length) return getPrelude() |
357 | var item = queue.shift() |
358 | var snap = item.snap, ids = item.ids |
359 | |
360 | var fileMeter = new Meter() |
361 | var readFile = new ReadBlobs(ids) |
362 | .pipe(fileMeter) |
363 | readFile.pipe(writeStream, {end: false}) |
364 | readFile.on('end', function () { |
365 | var fileSize = fileMeter.length |
366 | vfs[snap][STORE_CONTENT] = [offset, fileSize] |
367 | offset += fileSize |
368 | payloadSize += fileSize |
369 | next() |
370 | }) |
371 | })() |
372 | } |
373 | |
374 | function getPrelude() { |
375 | preludePosition = payloadPosition + payloadSize |
376 | new ReadBlobs(settings.preludeBlobs) |
377 | .pipe(collect(function (err, preludeData) { |
378 | if (err) throw err |
379 | preludeData = makePreludeBufferFromPrelude( |
380 | substitute(preludeData.toString('utf8'), { |
381 | '%VIRTUAL_FILESYSTEM%': JSON.stringify(vfs), |
382 | '%DEFAULT_ENTRYPOINT%': JSON.stringify(settings.entrypoint) |
383 | }) |
384 | ) |
385 | preludeSize = preludeData.length |
386 | writeStream.write(preludeData, function (err) { |
387 | if (err) throw err |
388 | inject() |
389 | }) |
390 | })) |
391 | } |
392 | |
393 | function inject() { |
394 | injectPlaceholders(binFd, placeholders, { |
395 | BAKERY: makeBakeryValueFromBakes(settings.bakes), |
396 | PAYLOAD_POSITION: payloadPosition, |
397 | PAYLOAD_SIZE: payloadSize, |
398 | PRELUDE_POSITION: preludePosition, |
399 | PRELUDE_SIZE: preludeSize |
400 | }, function (err) { |
401 | if (err) throw err |
402 | fs.close(binFd, function (err) { |
403 | if (err) throw err |
404 | putBins() |
405 | }) |
406 | }) |
407 | } |
408 | |
409 | function addOtherBin(name, snap) { |
410 | var script = `#!/bin/sh |
411 | PKG_ENTRYPOINT=${snap} exec ${settings.binName} "$@" |
412 | ` |
413 | var binPath = path.join(binDir, name) |
414 | var binTmp = binPath + '~' |
415 | fs.writeFileSync(binTmp, script) |
416 | plusx(binTmp) |
417 | fs.renameSync(binTmp, binPath) |
418 | console.log(binPath) |
419 | } |
420 | |
421 | function putBins() { |
422 | plusx(binTmp) |
423 | fs.renameSync(binTmp, binPath) |
424 | |
425 | var others = settings.otherEntrypoints |
426 | if (others) for (var name in others) { |
427 | var snap = others[name] |
428 | addOtherBin(name, snap) |
429 | } |
430 | |
431 | console.log(binPath) |
432 | process.exit(0) |
433 | } |
434 | |
435 | writeNodeBinary() |
436 |
Built with git-ssb-web