@platforma-sdk/r-builder 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@platforma-sdk/r-builder",
3
3
  "description": "Build R run environment for Platforma",
4
4
  "type": "module",
5
- "version": "0.0.2",
5
+ "version": "0.0.3",
6
6
  "files": [
7
7
  "assets/",
8
8
  "scripts/"
@@ -12,12 +12,10 @@
12
12
  },
13
13
  "license": "UNLICENSED",
14
14
  "dependencies": {
15
- "undici": "~7.2.3",
15
+ "undici": "~7.4.0",
16
16
  "fs-extra": "^11.2.0",
17
17
  "tar": "^7.4.3",
18
- "unzipper": "^0.12.3",
19
- "winston": "^3.17.0",
20
- "xz": "^2.0.2"
18
+ "winston": "^3.17.0"
21
19
  },
22
20
  "devDependencies": {
23
21
  "@milaboratories/eslint-config": "^1.0.1"
@@ -22,12 +22,6 @@ import * as windows from './lib/windows.js';
22
22
 
23
23
  export async function buildRDist(logger, version) {
24
24
  const currentOS = os.platform();
25
-
26
- const doneFile = `${version}.build-done`
27
- if (fs.existsSync(doneFile)) {
28
- logger.info(`Build of R is already done in this environment.\nTo force rebuild remove '${path.resolve(doneFile)}' file and run again.`)
29
- return
30
- }
31
25
 
32
26
  switch (currentOS) {
33
27
  case 'darwin': {
@@ -50,8 +44,6 @@ export async function buildRDist(logger, version) {
50
44
  process.exit(1);
51
45
  }
52
46
  }
53
-
54
- fs.writeFileSync(doneFile, '');
55
47
  }
56
48
 
57
49
  export async function buildRDeps(logger, version) {
@@ -7,6 +7,39 @@ import * as util from './util.js';
7
7
 
8
8
  const platformName = os.arch() === 'arm64' ? 'linux-aarch64' : `linux-x64`;
9
9
 
10
+ function useCaching(logger) {
11
+ if (util.isOK('which ccache')) {
12
+ useCCache(logger)
13
+ } else if (util.isOK('which sccache')) {
14
+ useSCCache(logger)
15
+ } else {
16
+ logger.warn('No caching tool found. Builds will be slow.')
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Setup current process environment to integrate with sccache
22
+ *
23
+ * @param {winston.Logger} logger
24
+ */
25
+ function useSCCache(logger) {
26
+ logger.info("Using sccache as build caching tool...")
27
+ process.env.SCCACHE_GHA_ENABLED = "true"
28
+ process.env.CC = `sccache gcc`
29
+ process.env.CXX = `sccache g++`
30
+ }
31
+
32
+ /**
33
+ * Setup current process environment to integrate with ccache
34
+ *
35
+ * @param {winston.Logger} logger
36
+ */
37
+ function useCCache(logger) {
38
+ logger.info("Using ccache as build caching tool...")
39
+ process.env.CC = `ccache gcc`
40
+ process.env.CXX = `ccache g++`
41
+ }
42
+
10
43
  async function installSystemPackages(logger) {
11
44
  const originalAptSources = '/etc/apt/sources.list';
12
45
  logger.info(`Analyzing '${originalAptSources}'`);
@@ -224,19 +257,6 @@ function patchBinElf(logger, binaryFile, rpath) {
224
257
  util.runInherit(`patchelf --remove-rpath '${binaryFile}'`);
225
258
  util.runInherit(`patchelf --set-rpath '${rpath}' '${binaryFile}'`);
226
259
  }
227
- /**
228
- * Setup current process environment to integrate with sccache
229
- *
230
- * @param {winston.Logger} logger
231
- */
232
- function useSCCache(logger) {
233
- if (util.isOK('which sccache')) {
234
- logger.info("Using sccache as build caching tool...")
235
- process.env.SCCACHE_GHA_ENABLED = "true"
236
- process.env.CC = `sccache gcc`
237
- process.env.CXX = `sccache g++`
238
- }
239
- }
240
260
 
241
261
  /**
242
262
  * Build portable R distribution and all required packages from sources
@@ -250,7 +270,7 @@ export async function buildR(logger, version) {
250
270
  process.exit(1);
251
271
  }
252
272
 
253
- useSCCache(logger)
273
+ useCaching(logger)
254
274
 
255
275
  const distDir = path.join(util.rDistDir, platformName);
256
276
 
@@ -267,6 +287,7 @@ export async function buildR(logger, version) {
267
287
 
268
288
  fs.renameSync(path.join(distDir, 'bin/R'), path.join(distDir, 'bin/R.orig'))
269
289
  fs.copyFileSync(pkg.asset('R.linux.sh'), path.join(distDir, 'bin/R'));
290
+ fs.chmodSync(path.join(distDir, 'bin/R'), 0o755)
270
291
 
271
292
  const gccVersion = getGCCMajorVersion()
272
293
  const gccUtilsPath = path.join('/usr/lib/gcc/x86_64-linux-gnu', gccVersion.toString())
@@ -280,7 +301,7 @@ export async function buildDeps(logger, version) {
280
301
  process.exit(1);
281
302
  }
282
303
 
283
- useSCCache(logger);
304
+ useCaching(logger);
284
305
 
285
306
  const distDir = path.join(util.rDistDir, platformName);
286
307
 
@@ -52,6 +52,7 @@ export async function buildR(logger, version) {
52
52
 
53
53
  fs.renameSync(path.join(distDir, 'bin/R'), path.join(distDir, 'bin/R.orig'))
54
54
  fs.copyFileSync(pkg.asset('R.macos.sh'), path.join(distDir, 'bin/R'))
55
+ fs.chmodSync(path.join(distDir, 'bin/R'), 0o755)
55
56
 
56
57
  util.installBasicRPackages(logger, distDir);
57
58
  }
@@ -20,7 +20,10 @@ export const downloadsDir = `${buildDir}/artifacts/dld`;
20
20
  export const artifactsDir = `${buildDir}/artifacts/pkg`;
21
21
 
22
22
  export const dependenciesDir = 'dependencies'; // where to search for dependencies specification (init.R or renv.lock)
23
- const renvDirName = 'renv-root' // directory with all renv cache stored inside final R run environment distribution
23
+
24
+ // directory with all renv cache stored inside final R run environment distribution.
25
+ // When changing this name, remember to update R preparation steps in workflow-tengo SDK.
26
+ const renvDirName = 'renv-root'
24
27
 
25
28
  const pipelineAsync = promisify(pipeline);
26
29
 
@@ -134,15 +137,10 @@ export async function download(logger, url, destination) {
134
137
  }
135
138
 
136
139
  export async function extractTarXz(archivePath, destination) {
137
- const archiveStream = fs.createReadStream(archivePath);
138
-
139
- const decompress = new xz.Decompressor();
140
-
141
- await pipelineAsync(
142
- archiveStream,
143
- decompress,
144
- tar.extract({ cwd: destination })
145
- );
140
+ if (!fs.existsSync(destination)) {
141
+ fs.mkdirSync(destination, { recursive: true });
142
+ }
143
+ runInherit(`tar -xJf '${archivePath}' -C '${destination}'`);
146
144
  }
147
145
 
148
146
  export async function extractTarGz(archivePath, destination) {
@@ -234,7 +232,9 @@ export function createLogger(level = 'debug') {
234
232
 
235
233
  format: winston.format.combine(
236
234
  winston.format.printf(({ level, message }) => {
237
- const indent = ' '.repeat(level.length + 2); // For ': ' after the level
235
+ const now = new Date();
236
+ const time = `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`;
237
+ const indent = ' '.repeat(time.length + 1 + level.length + 2); // For ': ' after the level
238
238
  const indentedMessage = message
239
239
  .split('\n')
240
240
  .map((line, index) => (index === 0 ? line : indent + line))
@@ -242,7 +242,7 @@ export function createLogger(level = 'debug') {
242
242
 
243
243
  const colorize = (l) => winston.format.colorize().colorize(l, l);
244
244
 
245
- return `${colorize(level)}: ${indentedMessage}`;
245
+ return `${time} ${colorize(level)}: ${indentedMessage}`;
246
246
  })
247
247
  ),
248
248
 
@@ -270,7 +270,7 @@ export function runR(logger, rRoot, args, opts = {}) {
270
270
  const cmdToRun = `${path.resolve(rRoot, 'bin/R')} ${args.join(' ')}`;
271
271
  const renvRoot = path.join(path.resolve(rRoot), renvDirName)
272
272
 
273
- env = {
273
+ const env = {
274
274
  RHOME: path.resolve(rRoot),
275
275
  R_HOME_DIR: path.resolve(rRoot),
276
276
  RENV_PATHS_ROOT: renvRoot,
@@ -403,22 +403,25 @@ export function buildRDependencies(
403
403
  '--no-echo',
404
404
  '-e', ...quote(`renv::restore( clean = TRUE )`),
405
405
  ], {
406
- wd: tempRenvDir,
406
+ wd: depsDir,
407
407
  paths: additionalPaths,
408
408
  })
409
409
 
410
+ } else if (fs.existsSync(path.join(depsDir, initFile))) {
411
+ runInRenv(logger, rRoot, ['--no-echo', `--file=${initFile}`], {
412
+ wd: depsDir,
413
+ paths: additionalPaths,
414
+ })
415
+
416
+ if (!fs.existsSync(path.join(depsDir, lockFile))) {
417
+ runInRenv(logger, rRoot, ['--no-echo', '-e', ...quote(`renv::snapshot()`)])
418
+ }
419
+
410
420
  } else {
411
- if (!fs.existsSync(path.join(depsDir, initFile))) {
412
421
  throw new Error(
413
422
  `directory ${depsDir} has no ${lockFile} or ${initFile}. No dependencies detected.\n`+
414
423
  `Create empty init.R file if you really need empty R run environment without any preinstalled dependencies`
415
424
  )
416
- }
417
-
418
- runInRenv(logger, rRoot, ['--no-echo', `--file=${initFile}`], {
419
- wd: tempRenvDir,
420
- paths: additionalPaths,
421
- })
422
425
  }
423
426
 
424
427
  const lockData = fs.readFileSync(lockPath, { encoding: 'utf-8' });
@@ -433,8 +436,12 @@ export function buildRDependencies(
433
436
  }
434
437
  }
435
438
 
436
- fs.rmSync(tempRenvDir, { recursive: true })
439
+ // Store resulting 'renv.lock' file in R installation root directory,
440
+ // so we can use it when preparing renv on Platforma Backend side (workflow-tengo SDK).
441
+ fs.copyFileSync(path.join(depsDir, lockFile), path.join(rRoot, renvDirName, lockFile))
442
+
437
443
  if (fs.existsSync(origProjectsFile)) {
444
+ // Restore original 'projects' file to not leave traces of environment preparation.
438
445
  fs.renameSync(origProjectsFile, projectsFile)
439
446
  }
440
447
 
@@ -474,6 +481,8 @@ function patchREnviron(
474
481
  envs = {},
475
482
  comment = "",
476
483
  ) {
484
+ logger.info(`Patching Renviron file in ${rRoot} directory`)
485
+
477
486
  let rEnvironPath = path.join(rRoot, 'etc', 'Renviron')
478
487
  if (!fs.existsSync(rEnvironPath)) {
479
488
  rEnvironPath = path.join(rRoot, 'etc', 'Renviron.site')
@@ -487,20 +496,7 @@ function patchREnviron(
487
496
  }
488
497
 
489
498
  for (const [envName, envValue] of Object.entries(envs)) {
490
- line = `${envName}=\${${envName}:-'${envValue}'}`
499
+ const line = `${envName}=\${${envName}:-'${envValue}'}`
491
500
  fs.appendFileSync(rEnvironPath, line + '\n', 'utf8');
492
501
  }
493
502
  }
494
-
495
- /**
496
- * Recreate directory by removing it and creating back.
497
- *
498
- * @param {string} dir - path to directory
499
- */
500
- function recreateDir(dir) {
501
- if (fs.existsSync(dir)) {
502
- fs.rmSync(dir, { recursive: true })
503
- }
504
- fs.mkdirSync(dir, { recursive: true })
505
-
506
- }
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
- import fs from 'node:fs';
3
+ import fsextra from 'fs-extra';
4
4
 
5
5
  import * as pkg from './pkg.js';
6
6
  import * as util from './util.js';
@@ -17,12 +17,7 @@ export async function buildR(logger, version) {
17
17
 
18
18
  // Copy Windows R stub files to dist directory
19
19
  const stubDir = pkg.asset('windows-x64-stub');
20
- await fs.promises.cp(stubDir, distDir, {
21
- recursive: true,
22
- preserveTimestamps: true,
23
- force: true,
24
- errorOnExist: false
25
- });
20
+ await fsextra.copy(stubDir, distDir);
26
21
 
27
22
  // TODO: R for windows
28
23
  }