@platforma-sdk/r-builder 0.0.1

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 ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@platforma-sdk/r-builder",
3
+ "description": "Build R run environment for Platforma",
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "files": [
7
+ "scripts/"
8
+ ],
9
+ "bin": {
10
+ "pl-r-builder": "./scripts/builder.js"
11
+ },
12
+ "license": "UNLICENSED",
13
+ "dependencies": {
14
+ "undici": "~7.2.3",
15
+ "fs-extra": "^11.2.0",
16
+ "tar": "^7.4.3",
17
+ "unzipper": "^0.12.3",
18
+ "winston": "^3.17.0",
19
+ "xz": "^2.0.2"
20
+ },
21
+ "devDependencies": {
22
+ "@milaboratories/eslint-config": "^1.0.1"
23
+ }
24
+ }
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ * Usage check
5
+ */
6
+
7
+ const args = process.argv.slice(2);
8
+
9
+ if (args.length !== 0) {
10
+ console.error(`Usage: ${process.argv[1]}`);
11
+ process.exit(1);
12
+ }
13
+
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+
18
+ import * as util from './lib/util.js';
19
+ import * as macos from './lib/macos.js';
20
+ import * as linux from './lib/linux.js';
21
+ import * as windows from './lib/windows.js';
22
+
23
+ export async function buildRDist(logger, version) {
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
+
32
+ switch (currentOS) {
33
+ case 'darwin': {
34
+ await macos.buildR(logger, version);
35
+ break;
36
+ }
37
+
38
+ case 'linux': {
39
+ await linux.buildR(logger, version);
40
+ break;
41
+ }
42
+
43
+ case 'win32': {
44
+ await windows.buildR(logger, version);
45
+ break;
46
+ }
47
+
48
+ default: {
49
+ logger.error(`unsupported OS: ${currentOS}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ fs.writeFileSync(doneFile, '');
55
+ }
56
+
57
+ export async function buildRDeps(logger, version) {
58
+ const currentOS = os.platform();
59
+
60
+ switch (currentOS) {
61
+ case 'darwin': {
62
+ await macos.buildDeps(logger, version);
63
+ break;
64
+ }
65
+
66
+ case 'linux': {
67
+ await linux.buildDeps(logger, version);
68
+ break;
69
+ }
70
+
71
+ case 'win32': {
72
+ await windows.buildDeps(logger, version);
73
+ break;
74
+ }
75
+
76
+ default: {
77
+ logger.error(`unsupported OS: ${currentOS}`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+ }
82
+
83
+ const logger = util.createLogger('debug');
84
+
85
+ (async () => {
86
+ const version = util.rVersion();
87
+
88
+ await buildRDist(logger, version)
89
+ await buildRDeps(logger, version)
90
+
91
+ util.runInherit(`pl-pkg build`);
92
+ })().catch((reason) => {
93
+ logger.error(reason)
94
+ logger.debug(reason?.stack)
95
+ process.exit(1)
96
+ });
@@ -0,0 +1,301 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs';
4
+
5
+ import * as pkg from './pkg.js';
6
+ import * as util from './util.js';
7
+
8
+ const platformName = os.arch() === 'arm64' ? 'linux-aarch64' : `linux-x64`;
9
+
10
+ async function installSystemPackages(logger) {
11
+ const originalAptSources = '/etc/apt/sources.list';
12
+ logger.info(`Analyzing '${originalAptSources}'`);
13
+
14
+ const modifiedAptSources = path.join(os.tmpdir(), 'apt-sources-list');
15
+ let srcExists = false;
16
+
17
+ const lines = fs.readFileSync(originalAptSources).toString().split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i].trim();
20
+ if (/^\s*#\s*(deb-src)\s+.*\s+(universe)\s*$/.test(line)) {
21
+ srcExists = true;
22
+ lines[i] = line.replace(/^\s*#\s*/, '');
23
+ }
24
+ if (/^\s*deb-src\s+/.test(line)) {
25
+ srcExists = true;
26
+ }
27
+ }
28
+
29
+ if (srcExists) {
30
+ logger.info(`Patching '${originalAptSources}'`);
31
+ fs.writeFileSync(modifiedAptSources, lines.join('\n'));
32
+ util.runInherit(`sudo mv '${modifiedAptSources}' '${originalAptSources}'`);
33
+ } else {
34
+ logger.warn(
35
+ `WARNING! No 'deb-src' records found in '${originalAptSources}'. 'apt-get build-dep' would probably fail.`
36
+ );
37
+ }
38
+
39
+ logger.info('Installing packages');
40
+ util.runInherit(`sudo apt-get update`);
41
+ util.runInherit(`sudo apt-get install --yes rsync build-essential unzip curl patchelf r-recommended`);
42
+ util.runInherit(`sudo apt-get install --yes libssl-dev libcurl4-openssl-dev libpng-dev liblapack-dev libblas-dev libfontconfig1-dev libxml2-dev`);
43
+ util.runInherit(`sudo apt-get build-dep --yes r-base`);
44
+ }
45
+
46
+ function getGCCMajorVersion() {
47
+ const output = util.run('gcc --version')
48
+
49
+ const firstLine = output.split('\n')[0];
50
+ const versionPart = firstLine.split(' ')[2];
51
+ const majorVersion = versionPart.split('.')[0];
52
+ return parseInt(majorVersion, 10);
53
+ }
54
+
55
+ async function getRSources(logger, version) {
56
+ const rSourcesArchive = path.join(util.downloadsDir, `R-${version}.tar.gz`);
57
+
58
+ logger.info(`Downloading R ${version} sources...`);
59
+ await util.download(
60
+ logger,
61
+ `https://cran.r-project.org/src/base/R-4/R-${version}.tar.gz`,
62
+ rSourcesArchive
63
+ );
64
+
65
+ const rSourcesDir = path.join(util.artifactsDir);
66
+ fs.mkdirSync(rSourcesDir, { recursive: true });
67
+
68
+ logger.info(`Extracting R sources from archive...`);
69
+ await util.extractTarGz(rSourcesArchive, rSourcesDir);
70
+
71
+ return path.resolve(rSourcesDir, `R-${version}`);
72
+ }
73
+
74
+ function configureRBuild(logger, version, sourcesDir, installDir) {
75
+ logger.info(`Configuring R build`);
76
+
77
+ util.runInherit(`${sourcesDir}/tools/rsync-recommended`, {
78
+ cwd: sourcesDir
79
+ });
80
+
81
+ const buildDir = path.join(util.buildDir, 'R', version);
82
+ fs.mkdirSync(buildDir, { recursive: true });
83
+
84
+ const configSiteName = 'config.site';
85
+ fs.copyFileSync(
86
+ path.join(sourcesDir, configSiteName),
87
+ path.join(buildDir, configSiteName)
88
+ );
89
+
90
+ logger.info(` running '${sourcesDir}/configure'...`);
91
+ const installDirAbs = path.resolve(installDir);
92
+ util.runInherit(
93
+ `${sourcesDir}/configure --with-blas=no --with-lapack=no --with-x=no --prefix=${installDirAbs}`,
94
+ { cwd: buildDir }
95
+ );
96
+
97
+ return buildDir;
98
+ }
99
+
100
+ function buildRDist(logger, buildDir, installRoot, rRoot) {
101
+ util.runInherit(`make`, { cwd: buildDir });
102
+ util.runInherit(`make install`, { cwd: buildDir });
103
+
104
+ // Move core of R installation into desired R root, leaving behind all supporting
105
+ // (and unnecessary) files.
106
+ if (fs.existsSync(rRoot)) {
107
+ fs.rmSync(rRoot, { recursive: true })
108
+ }
109
+ fs.renameSync(path.join(installRoot, 'lib', 'R'), rRoot);
110
+
111
+ // 1st round of dependencies collection with binaries ELF patching to make R work from
112
+ // its new location. We'll collect dependencies of installed packages later by one more round.
113
+ collectDependencies(logger, rRoot, true);
114
+ }
115
+
116
+ const systemLibs = [
117
+ /ld-linux-x86-64\.so(\.[0-9.]+)?$/,
118
+ /libc\.so(\.[0-9.]+)?$/,
119
+ /libm\.so(\.[0-9.]+)?$/,
120
+ /libcom_err\.so(\.[0-9.]+)?$/,
121
+ /libstdc\+\+\.so(\.[0-9.]+)?$/,
122
+ /libresolv\.so(\.[0-9.]+)?$/
123
+ ];
124
+
125
+ function collectDependencies(logger, installDir, patchElf = false) {
126
+ logger.info('Collecting libraries to make R distribution portable');
127
+
128
+ const libsDir = path.join(installDir, 'lib');
129
+ const allLibs = () => util.findFiles(installDir, /\.so(\.[0-9.]+)?$/);
130
+ const executables = [path.join(installDir, 'bin/exec/R')];
131
+
132
+ const collected = collectSoLibs(
133
+ logger,
134
+ [...executables, ...allLibs()],
135
+ libsDir,
136
+ systemLibs
137
+ );
138
+ logger.info(
139
+ `Libraries collected into '${libsDir}':\n ${collected.join('\n ')}`
140
+ );
141
+
142
+ if (patchElf) {
143
+ for (const binName of [...executables]) {
144
+ const relativeLibLocation = path.relative(path.dirname(binName), libsDir);
145
+ const newRPath = `$ORIGIN/${relativeLibLocation}`;
146
+ logger.info(` patching ELF in '${binName}' (rpath = '${newRPath}')...`);
147
+ patchBinElf(logger, binName, newRPath);
148
+ }
149
+ }
150
+ }
151
+
152
+ function collectSoLibs(logger, binaryFiles, libsDir, libsToIgnore) {
153
+ const allDependencies = new Set();
154
+
155
+ binaryFiles.forEach((binaryFile) => {
156
+ logger.info(`Loading library dependencies of '${binaryFile}'`);
157
+
158
+ libLoop: for (const libPath of getBinDependencies(binaryFile)) {
159
+ if (path.resolve(libPath).startsWith(path.resolve(libsDir))) {
160
+ continue libLoop; // already in desired libs dir
161
+ }
162
+
163
+ for (const ignoreRule of libsToIgnore) {
164
+ if (
165
+ (typeof ignoreRule === 'function' && ignoreRule(libPath)) ||
166
+ (ignoreRule instanceof RegExp && ignoreRule.test(libPath))
167
+ ) {
168
+ continue libLoop; // lib is ignored
169
+ }
170
+ }
171
+
172
+ allDependencies.add(libPath);
173
+ }
174
+ });
175
+
176
+ const collectedLibs = Array.from(allDependencies);
177
+
178
+ for (const libPath of collectedLibs) {
179
+ logger.info(` copying '${libPath}'`);
180
+ const targetPath = path.join(libsDir, path.basename(libPath));
181
+ fs.copyFileSync(libPath, targetPath);
182
+ }
183
+
184
+ return collectedLibs;
185
+ }
186
+
187
+ function getBinDependencies(binaryFile) {
188
+ const output = util.run(`ldd '${binaryFile}'`).toString();
189
+ /*
190
+ The output of ldd looks like this:
191
+ linux-vdso.so.1 (0x00007f2f6859d000)
192
+ libRblas.so => not found
193
+ libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2f684ac000)
194
+ libreadline.so.8 => not found
195
+ libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f2f67f66000)
196
+ liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007f2f6847a000)
197
+ libbz2.so.1.0 => /lib/x86_64-linux-gnu/libbz2.so.1.0 (0x00007f2f68464000)
198
+ libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f2f67f4a000)
199
+ libtirpc.so.3 => not found
200
+ libicuuc.so.70 => not found
201
+ libicui18n.so.70 => not found
202
+ libgomp.so.1 => not found
203
+ libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2f67c00000)
204
+ /lib64/ld-linux-x86-64.so.2 (0x00007f2f6859f000)
205
+ */
206
+
207
+ // Split by every space symbol. Lines that start with '/' are most likely absolute paths to libraries.
208
+ return util.uniq(
209
+ output
210
+ .split(/\s/)
211
+ .filter((l) => l.startsWith('/'))
212
+ .sort()
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Patch binary file updating its RPATH
218
+ *
219
+ * @param {winston.Logger} logger
220
+ * @param {string} binaryFile - binary file to patch. .so or executable.
221
+ * @param {string} rpath - new RPATH to set
222
+ */
223
+ function patchBinElf(logger, binaryFile, rpath) {
224
+ util.runInherit(`patchelf --remove-rpath '${binaryFile}'`);
225
+ util.runInherit(`patchelf --set-rpath '${rpath}' '${binaryFile}'`);
226
+ }
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
+
241
+ /**
242
+ * Build portable R distribution and all required packages from sources
243
+ *
244
+ * @param {winston.Logger} logger
245
+ * @param {string} version - R version to build
246
+ */
247
+ export async function buildR(logger, version) {
248
+ if (os.platform() !== 'linux') {
249
+ logger.error(`Current OS is not Linux (os.platform=${os.platform()})`);
250
+ process.exit(1);
251
+ }
252
+
253
+ useSCCache(logger)
254
+
255
+ const distDir = path.join(util.rDistDir, platformName);
256
+
257
+ fs.mkdirSync(path.dirname(distDir), { recursive: true });
258
+ fs.mkdirSync(util.downloadsDir, { recursive: true });
259
+
260
+ await installSystemPackages(logger);
261
+
262
+ const rSourcesDir = await getRSources(logger, version);
263
+ const installDir = path.join(util.rDistDir, 'tmp-install');
264
+ const rBuildDir = configureRBuild(logger, version, rSourcesDir, installDir);
265
+
266
+ buildRDist(logger, rBuildDir, installDir, distDir);
267
+
268
+ fs.renameSync(path.join(distDir, 'bin/R'), path.join(distDir, 'bin/R.orig'))
269
+ fs.copyFileSync(pkg.asset('R.linux.sh'), path.join(distDir, 'bin/R'));
270
+
271
+ const gccVersion = getGCCMajorVersion()
272
+ const gccUtilsPath = path.join('/usr/lib/gcc/x86_64-linux-gnu', gccVersion.toString())
273
+
274
+ util.installBasicRPackages(logger, distDir, [gccUtilsPath]);
275
+ }
276
+
277
+ export async function buildDeps(logger, version) {
278
+ if (os.platform() !== 'linux') {
279
+ logger.error(`Current OS is not Linux (os.platform=${os.platform()})`);
280
+ process.exit(1);
281
+ }
282
+
283
+ useSCCache(logger);
284
+
285
+ const distDir = path.join(util.rDistDir, platformName);
286
+
287
+ const gccVersion = getGCCMajorVersion()
288
+ const gccUtilsPath = path.join('/usr/lib/gcc/x86_64-linux-gnu', gccVersion.toString())
289
+
290
+ util.buildRDependencies(
291
+ logger,
292
+ distDir,
293
+ path.join(distDir, 'packages'),
294
+ 'dependencies',
295
+ [gccUtilsPath],
296
+ )
297
+
298
+ // We need to collect dependencies AFTER all packages intallation
299
+ // to save also deps of the packages (which are wider than R itself)
300
+ collectDependencies(logger, distDir);
301
+ }
@@ -0,0 +1,68 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+ import fsextra from 'fs-extra';
5
+
6
+ import * as pkg from './pkg.js';
7
+ import * as util from './util.js';
8
+ import * as srcs from './macos_src.js';
9
+
10
+ const platformName = os.arch() === 'arm64' ? 'macosx-aarch64' : `macosx-x64`;
11
+
12
+ function currentArch() {
13
+ return util.run('uname -m'); // 'x86_64' or 'arm64'
14
+ }
15
+
16
+ async function installR(logger, version) {
17
+ logger.info(`Downloading R ${version}...`);
18
+
19
+ const rPkgPath = path.join(util.downloadsDir, `R-${version}.pkg`);
20
+ const rArch = currentArch()
21
+ await util.download(
22
+ logger,
23
+ `https://cloud.r-project.org/bin/macosx/big-sur-${rArch}/base/R-${version}-${rArch}.pkg`,
24
+ rPkgPath
25
+ );
26
+
27
+ logger.info(`Installing R ${version}...`);
28
+ util.runInherit(`sudo installer -pkg '${rPkgPath}' -target /`);
29
+
30
+ return fs.realpathSync('/Library/Frameworks/R.framework/Resources')
31
+ }
32
+
33
+ export async function buildR(logger, version) {
34
+ if (os.platform() !== 'darwin') {
35
+ logger.error(`Current OS is not MacOS X (os.platform=${os.platform()})`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const distDir = path.join(util.rDistDir, platformName);
40
+
41
+ fs.mkdirSync(distDir, { recursive: true });
42
+ fs.mkdirSync(util.downloadsDir, { recursive: true });
43
+
44
+ const gFortran = await srcs.installGNUFortran(logger)
45
+ if (gFortran) {
46
+ logger.info(`Adding '${gFortran}/bin' into PATH`);
47
+ process.env.PATH = `${gFortran}/bin:${process.env.PATH}`;
48
+ }
49
+
50
+ const rRootPath = await installR(logger, version)
51
+ await fsextra.copy(rRootPath, distDir)
52
+
53
+ fs.renameSync(path.join(distDir, 'bin/R'), path.join(distDir, 'bin/R.orig'))
54
+ fs.copyFileSync(pkg.asset('R.macos.sh'), path.join(distDir, 'bin/R'))
55
+
56
+ util.installBasicRPackages(logger, distDir);
57
+ }
58
+
59
+ export async function buildDeps(logger, version) {
60
+ const distDir = path.join(util.rDistDir, platformName);
61
+
62
+ util.buildRDependencies(
63
+ logger,
64
+ distDir,
65
+ path.join(distDir, 'packages'),
66
+ 'dependencies',
67
+ )
68
+ }
@@ -0,0 +1,314 @@
1
+ // This imodule contains code for building R on Mac OS X from sources.
2
+ // As R project provides official binary builds in their installer AND
3
+ // their installers can relatively simply be converted to 'portable'
4
+ // versions, we do not have to build our own R binaries.
5
+ // But still, if we will need it any time in future, this scripts would be
6
+ // very helpful starting point.
7
+
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import fs from 'fs';
11
+
12
+ import * as util from './util.js';
13
+
14
+ const platformName = os.arch() === 'arm64' ? 'macosx-aarch64' : `macosx-x64`;
15
+
16
+ function currentMacOSXVersion() {
17
+ return util.run('sw_vers -productVersion').toString().trim(); // i.e. 14.4.1
18
+ }
19
+
20
+ function currentArch() {
21
+ return util.run('uname -m');
22
+ }
23
+
24
+ export async function installGNUFortran(logger) {
25
+ const gfortranArchivePath = path.join(util.downloadsDir, 'gfortran.tar.xz');
26
+ const gfortranDistPath = path.join(util.artifactsDir, 'gfortran');
27
+ const gfortranGlobal = '/opt/gfortran';
28
+
29
+ logger.info(`Checking GNU Fortran...`);
30
+
31
+ if (util.isOK('gfortran --help')) {
32
+ logger.info(` GNU Fortran is already available.`);
33
+ return ''; // gfortran's 'bin/' already in PATH
34
+ }
35
+ if (fs.existsSync(path.join(gfortranGlobal, 'bin', 'gfortran'))) {
36
+ logger.info(` GNU Fortran is already installed to '${gfortranGlobal}'.`);
37
+ return gfortranGlobal; // should add it's 'bin/' to PATH
38
+ }
39
+
40
+ logger.info(`Downloading GNU Fortran...`);
41
+ await util.download(
42
+ logger,
43
+ 'https://github.com/R-macos/gcc-12-branch/releases/download/12.2-darwin-r0/gfortran-12.2-darwin20-r0-universal.tar.xz',
44
+ gfortranArchivePath
45
+ );
46
+
47
+ logger.info(` installing GNU Fortran to '${gfortranDistPath}'...`);
48
+
49
+ fs.mkdirSync(gfortranDistPath, { recursive: true });
50
+ await util.extractTarXz(gfortranArchivePath, gfortranDistPath);
51
+ const localGfortranRoot = path.join(gfortranDistPath, 'opt', 'gfortran');
52
+
53
+ const systemSDKPath = util.run('xcrun --show-sdk-path').toString().trim();
54
+ logger.info(` linking system SDK path '${systemSDKPath}' into GNU Fortran`);
55
+ fs.rmSync(path.join(localGfortranRoot, 'SDK'));
56
+
57
+ fs.symlinkSync(systemSDKPath, path.join(localGfortranRoot, 'SDK'));
58
+
59
+ logger.info(` linking GNU Fortran to '${gfortranGlobal}'...`);
60
+ util.runInherit(
61
+ `sudo ln -sfn '${path.resolve(localGfortranRoot)}' '${gfortranGlobal}'`
62
+ );
63
+
64
+ return gfortranGlobal;
65
+ }
66
+
67
+ export async function installMacTeX(logger) {
68
+ logger.info(`Downloading MacTeX...`);
69
+
70
+ const macTeXPkgPath = path.join(util.downloadsDir, 'MacTeX.pkg');
71
+ await util.download(
72
+ logger,
73
+ 'https://mirror.ctan.org/systems/mac/mactex/MacTeX.pkg',
74
+ macTeXPkgPath
75
+ );
76
+
77
+ if (util.isOK('tex')) {
78
+ logger.info(`MacTeX seems to be already installed.`);
79
+ return;
80
+ }
81
+
82
+ logger.info(`Installing MacTeX...`);
83
+ util.runInherit(`sudo installer -pkg '${macTeXPkgPath}' -target /`);
84
+ }
85
+
86
+ export async function installXQuartz(logger) {
87
+ logger.info(`Downloading XQuartz...`);
88
+
89
+ const xQuartzPkgPath = path.join(util.downloadsDir, 'XQuartz.pkg');
90
+ await util.download(
91
+ logger,
92
+ 'https://github.com/XQuartz/XQuartz/releases/download/XQuartz-2.8.5/XQuartz-2.8.5.pkg',
93
+ xQuartzPkgPath
94
+ );
95
+
96
+ if (util.isOK('XQuartz')) {
97
+ logger.info(`XQuartz seems to be already installed.`);
98
+ return;
99
+ }
100
+
101
+ logger.info(`Installing XQuartz...`);
102
+ util.runInherit(`sudo installer -pkg '${xQuartzPkgPath}' -target /`);
103
+ }
104
+
105
+ export async function installRTools(logger) {
106
+ const toolsRepoDir = path.join(util.artifactsDir, 'r-recipes');
107
+ fs.mkdirSync(path.dirname(toolsRepoDir), { recursive: true });
108
+
109
+ if (!fs.existsSync(toolsRepoDir)) {
110
+ logger.info(`Downloading additional R tools recipes...`);
111
+ util.runInherit(
112
+ `git clone https://github.com/R-macos/recipes.git ${toolsRepoDir}`
113
+ );
114
+ }
115
+
116
+ logger.info(`Installing additional R tools...`);
117
+
118
+ const arch = currentArch();
119
+ const installPrefix = `/opt/R/${arch.toString().trim()}`;
120
+ util.runInherit(`sudo mkdir -p '${installPrefix}'`);
121
+
122
+ // Sometimes r-base-dev fails because of network errors, like rate limiting on remote side, or failed archive downloads.
123
+ // We try to re-run the build script, because it is several times faster than restarting whole CI build, as
124
+ // 'make' under r-base-dev skips already built targets.
125
+ util.retry(logger, 3, 'Additional tool installation failed', () =>
126
+ util.runInherit('sudo -E ./build.sh r-base-dev', {
127
+ cwd: toolsRepoDir,
128
+ env: {
129
+ PREFIX: installPrefix
130
+ }
131
+ })
132
+ );
133
+
134
+ return path.resolve(installPrefix);
135
+ }
136
+
137
+ export async function getRSources(logger, version) {
138
+ const rSourcesArchive = path.join(util.downloadsDir, `R-${version}.tar.gz`);
139
+
140
+ logger.info(`Downloading R ${version} sources...`);
141
+ await util.download(
142
+ logger,
143
+ `https://cran.r-project.org/src/base/R-4/R-${version}.tar.gz`,
144
+ rSourcesArchive
145
+ );
146
+
147
+ const rSourcesDir = path.join(util.artifactsDir);
148
+ fs.mkdirSync(rSourcesDir, { recursive: true });
149
+
150
+ logger.info(`Extracting R sources from archive...`);
151
+ await util.extractTarGz(rSourcesArchive, rSourcesDir);
152
+
153
+ return path.resolve(rSourcesDir, `R-${version}`);
154
+ }
155
+
156
+ export function configureRBuild(
157
+ logger,
158
+ version,
159
+ sourcesDir,
160
+ installDir,
161
+ rToolsDir,
162
+ fortranRoot
163
+ ) {
164
+ logger.info(`Configuring R build...`);
165
+
166
+ util.runInherit(`${sourcesDir}/tools/rsync-recommended`, {
167
+ cwd: sourcesDir
168
+ });
169
+
170
+ const buildDir = path.join(util.buildDir, 'R', version);
171
+ fs.mkdirSync(buildDir, { recursive: true });
172
+
173
+ const configSiteName = 'config.site';
174
+ logger.info(` patching '${path.join(sourcesDir, configSiteName)}'...`);
175
+ let configSiteContent = fs
176
+ .readFileSync(path.join(sourcesDir, configSiteName), 'utf-8')
177
+ .toString();
178
+
179
+ let hasARCH = false;
180
+ let hasOS = false;
181
+ for (const line in configSiteContent.split('\n')) {
182
+ hasARCH = hasARCH || /^\s*ARCH\s*=/.test(line);
183
+ hasOS = hasOS || /^\s*OSVER\s*=/.test(line);
184
+ }
185
+
186
+ const osMajor = currentMacOSXVersion().split('.')[0] + '.0'; // 14.4.1 -> 14.0
187
+ const arch = currentArch();
188
+ if (hasOS) {
189
+ configSiteContent = replace(/^\s*OS\s*=/, `OSVER=${osMajor}`);
190
+ } else {
191
+ configSiteContent += `\nOSVER=${osMajor}\n`;
192
+ }
193
+ if (hasARCH) {
194
+ configSiteContent = replace(/^\s*ARCH\s*=/, `ARCH=${arch}`);
195
+ } else {
196
+ configSiteContent += `\nARCH=${arch}\n`;
197
+ }
198
+
199
+ fs.writeFileSync(path.join(buildDir, configSiteName), configSiteContent);
200
+
201
+ logger.info(` running '${sourcesDir}/configure'...`);
202
+ const installDirAbs = path.resolve(installDir);
203
+ util.runInherit(
204
+ `${sourcesDir}/configure --with-blas=no --with-lapack=no --with-x=no --prefix=${installDirAbs}`,
205
+ {
206
+ cwd: buildDir,
207
+ env: {
208
+ CC: 'sccache clang',
209
+ CFLAGS: `-Wall -g -O2 -pedantic -mmacosx-version-min=${osMajor} -arch ${arch} -falign-functions=64 -Wno-error=implicit-function-declaration`,
210
+ CPPFLAGS: `-I${rToolsDir}/include`,
211
+ FC: `${fortranRoot}/bin/gfortran`,
212
+ FCFLAGS: `-Wall -g -O2 -pedantic -mmacosx-version-min=${osMajor} -arch ${arch} -mtune=native`,
213
+ LDFLAGS: `-L${rToolsDir}/lib`,
214
+ CXX: 'sccache clang',
215
+ CXXFLAGS: `-Wall -g -O2 -pedantic -mmacosx-version-min=${osMajor} -arch ${arch} -falign-functions=64`,
216
+ R_LD_LIBRARY_PATH: `${rToolsDir}/lib`,
217
+ PKG_CONFIG: `${rToolsDir}/bin/pkg-config`,
218
+ PKG_CONFIG_PATH: `${rToolsDir}/lib/pkgconfig:${rToolsDir}/share/pkgconfig:/usr/lib/pkgconfig:/opt/X11/lib/pkgconfig:/opt/X11/share/pkgconfig`
219
+ }
220
+ }
221
+ );
222
+
223
+ return buildDir;
224
+ }
225
+
226
+ export function buildRFramework(logger, buildDir) {
227
+ logger.info('Bundling and installing R');
228
+ util.runInherit(`make`, { cwd: buildDir });
229
+ util.runInherit(`make install`, { cwd: buildDir });
230
+ }
231
+
232
+ export function collectLibraries(logger, rRoot) {
233
+ const dstLibsDir = 'lib/R/lib';
234
+
235
+ const allLibs = () => util.findFiles(path.resolve(rRoot), /\.(dylib|so)$/);
236
+
237
+ logger.info("Consolidating all dynamic library dependencies into R's root");
238
+
239
+ const fixRLibs = allLibs()
240
+ .map((libPath) => `--fix-file '${libPath}'`)
241
+ .join(' ');
242
+
243
+ // Collect all libraries into '<rRoot>/lib/R/lib'
244
+ util.runInherit(
245
+ `dylibbundler` +
246
+ ` --bundle-deps` +
247
+ ` --dest-dir '${dstLibsDir}'` +
248
+ ` --search-path '${dstLibsDir}'` +
249
+ ` --skip-missing` +
250
+ ` --skip-existing` +
251
+ ` --install-path '@rpath'` +
252
+ ` ${fixRLibs}` +
253
+ ` --fix-file 'lib/R/bin/exec/R'`,
254
+ { cwd: rRoot }
255
+ );
256
+
257
+ // Patch R binary to set RPATH to be relative to executable.
258
+ util.runInherit(
259
+ `install_name_tool` +
260
+ ` -add_rpath` +
261
+ ` '@executable_path/../../lib/'` +
262
+ ` 'lib/R/bin/exec/R'`,
263
+ { cwd: rRoot }
264
+ );
265
+ }
266
+
267
+ export async function buildRPackage() {
268
+ if (os.platform() !== 'darwin') {
269
+ logger.error(`Current OS is not MacOS X (os.platform=${os.platform()})`);
270
+ process.exit(1);
271
+ }
272
+
273
+ const distDir = path.join(util.rDistDir, version, platformName);
274
+
275
+ process.chdir(util.repoRoot);
276
+ fs.mkdirSync(distDir, { recursive: true });
277
+ fs.mkdirSync(util.downloadsDir, { recursive: true });
278
+
279
+ if (!util.isOK('xcode-select --print-path')) {
280
+ // 1. Install Command Line Tools for XCode as they are not present in current system
281
+ util.runInherit('sudo xcode-select --install');
282
+ }
283
+
284
+ const fortranPrefix = await installGNUFortran(logger);
285
+ if (fortranPrefix) {
286
+ logger.info(`Adding '${fortranPrefix}/bin' into PATH`);
287
+ process.env.PATH = `${fortranPrefix}/bin:${process.env.PATH}`;
288
+ }
289
+
290
+ await installMacTeX(logger);
291
+ await installXQuartz(logger);
292
+ const toolsPrefix = await installRTools(logger);
293
+ util.patchEnvPath('PATH', `${toolsPrefix}/bin`);
294
+
295
+ const rSourcesDir = await getRSources(logger, version);
296
+ const rBuildDir = configureRBuild(
297
+ logger,
298
+ version,
299
+ rSourcesDir,
300
+ distDir,
301
+ toolsPrefix,
302
+ fortranPrefix
303
+ );
304
+
305
+ buildRFramework(logger, rBuildDir);
306
+
307
+ const rStartScriptPath = path.join(util.repoRoot, util.assetsRoot, 'R.macos.sh');
308
+
309
+ fs.copyFileSync(rStartScriptPath, path.join(distDir, 'bin/R'));
310
+ fs.copyFileSync(rStartScriptPath, path.join(distDir, 'lib/R/bin/R'));
311
+
312
+ installBasicRPackages(logger, distDir);
313
+ collectLibraries(logger, distDir);
314
+ }
@@ -0,0 +1,11 @@
1
+ import * as nodePath from 'node:path';
2
+
3
+ const _root = nodePath.resolve(nodePath.join(import.meta.dirname, '..', '..'))
4
+
5
+ export function path(p) {
6
+ return nodePath.join(_root, ...p);
7
+ }
8
+
9
+ export function asset(p) {
10
+ return path('assets', ...p)
11
+ }
@@ -0,0 +1,506 @@
1
+ import child_process from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pipeline } from 'node:stream';
5
+ import { promisify } from 'node:util';
6
+ import { fetch } from 'undici';
7
+
8
+ import * as tar from 'tar';
9
+ import winston from 'winston';
10
+ import zlib from 'zlib';
11
+ import xz from 'xz';
12
+
13
+ import * as pkg from './pkg.js';
14
+
15
+ // All paths are relative here by intention:
16
+ // we maintain the same directory hierarchy across all packages.
17
+ export const rDistDir = 'rdist'; // where built R would be stored
18
+ export const buildDir = 'build'; // where intermediate build files would be stored
19
+ export const downloadsDir = `${buildDir}/artifacts/dld`;
20
+ export const artifactsDir = `${buildDir}/artifacts/pkg`;
21
+
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
24
+
25
+ const pipelineAsync = promisify(pipeline);
26
+
27
+ export function run(command, opts = {}) {
28
+ const processOpts = {
29
+ ...opts,
30
+ env: {
31
+ ...process.env,
32
+ ...opts.env,
33
+ },
34
+ stdio: 'pipe'
35
+ }
36
+
37
+ const stdout = child_process.execSync(command, processOpts);
38
+ return stdout.toString();
39
+ }
40
+
41
+ export function packageJson() {
42
+ if (!fs.existsSync("package.json")) {
43
+ throw new Error("file 'package.json' not found in current directory. The script should be executed from package root dir.")
44
+ }
45
+
46
+ return JSON.parse(fs.readFileSync("package.json"))
47
+ }
48
+
49
+ export function rVersion() {
50
+ const v = packageJson()?.["block-software"]?.entrypoints?.main?.environment?.artifact?.["r-version"]
51
+ if (!v) {
52
+ throw new Error("Expected main entrypoint to have R type and to have R version defined: 'block-software.entrypoints.main.environment.artifact.r-version' is not set")
53
+ }
54
+
55
+ return v
56
+ }
57
+
58
+ export function runInherit(command, opts = {}) {
59
+ const processOpts = {
60
+ ...opts,
61
+ env: {
62
+ ...process.env,
63
+ ...opts.env,
64
+ },
65
+ stdio: 'inherit'
66
+ }
67
+ child_process.execSync(command, processOpts);
68
+ }
69
+
70
+ export function isOK(command) {
71
+ try {
72
+ child_process.execSync(command, { stdio: 'ignore' });
73
+ return true;
74
+ } catch (error) {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ export function patchEnvPath(varName, pathToAdd) {
80
+ if (!process.env[varName]) {
81
+ process.env[varName] = pathToAdd;
82
+ return;
83
+ }
84
+
85
+ const items = process.env[varName].split(path.delimiter);
86
+ const newValue = [pathToAdd, ...items].join(path.delimiter);
87
+ process.env[varName] = newValue;
88
+ }
89
+
90
+ var showProgress = process.stdout.isTTY;
91
+
92
+ export async function download(logger, url, destination) {
93
+ if (fs.existsSync(destination)) {
94
+ logger.info(` download skipped: ${destination} already exists`);
95
+ return Promise.resolve();
96
+ }
97
+
98
+ const writer = fs.createWriteStream(destination);
99
+ const response = await fetch(url);
100
+
101
+ if (!response.ok) {
102
+ writer.close();
103
+ throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
104
+ }
105
+
106
+ if (showProgress) {
107
+ const totalBytes = parseInt(response.headers.get('content-length') || '0', 10);
108
+ const knownSize = totalBytes > 0;
109
+ let downloadedBytes = 0;
110
+
111
+ const reader = response.body.getReader();
112
+ const pump = async () => {
113
+ const { done, value } = await reader.read();
114
+ if (done) return;
115
+
116
+ downloadedBytes += value.length;
117
+ const progress = knownSize
118
+ ? (downloadedBytes / totalBytes) * 100
119
+ : downloadedBytes;
120
+ const percents = knownSize ? progress.toFixed(2) + '% ' : '';
121
+ const bytes = knownSize
122
+ ? `(${downloadedBytes.toString()}/${totalBytes.toString()})`
123
+ : downloadedBytes.toString();
124
+ process.stdout.write(` downloading: ${percents}${bytes}\r`);
125
+
126
+ writer.write(value);
127
+ await pump();
128
+ };
129
+
130
+ await pump();
131
+ } else {
132
+ await pipeline(response.body, writer);
133
+ }
134
+ }
135
+
136
+ 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
+ );
146
+ }
147
+
148
+ export async function extractTarGz(archivePath, destination) {
149
+ await pipelineAsync(
150
+ fs.createReadStream(archivePath),
151
+ zlib.createUnzip(),
152
+ tar.extract({ cwd: destination })
153
+ );
154
+ }
155
+
156
+ export function findFiles(dir, filter) {
157
+ let results = [];
158
+
159
+ const list = fs.readdirSync(dir);
160
+ list.forEach((file) => {
161
+ const filePath = path.join(dir, file);
162
+ const stat = fs.statSync(filePath);
163
+ if (stat && stat.isDirectory()) {
164
+ results = results.concat(findFiles(filePath, filter));
165
+ } else {
166
+ if (
167
+ (typeof filter === 'function' && filter(file)) ||
168
+ (filter instanceof RegExp && filter.test(file))
169
+ ) {
170
+ results.push(filePath);
171
+ }
172
+ }
173
+ });
174
+
175
+ return results;
176
+ }
177
+
178
+ export function uniq(list) {
179
+ return list.filter((item, index) => {
180
+ return index === 0 || item !== list[index - 1];
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Quote each item in given <list>
186
+ *
187
+ * @param {string[] | string} list - items to quote
188
+ * @param {' | "} q - quote symbol to use: ' or "
189
+ * @returns {string[]} - list of quoted items
190
+ */
191
+ export function quote(list, q = "'") {
192
+ if (!list) {
193
+ return [];
194
+ }
195
+
196
+ if (typeof list === 'string') {
197
+ list = [list];
198
+ }
199
+
200
+ return list.map((item) => `${q}${item.replaceAll(q, '\\' + q)}${q}`);
201
+ }
202
+
203
+ /**
204
+ * Convert list of strings into R list definition (c("item1", "item2", "..."))
205
+ *
206
+ * @param {string[]} items - list of items to convert
207
+ * @param {' | "} q - quote symbol to use (' or ")
208
+ * @returns {string}
209
+ */
210
+ function rList(items, q = '"') {
211
+ return `c(${quote(items, q).join(',')})`
212
+ }
213
+
214
+ export function retry(logger, maxAttempts, msg, cb) {
215
+ let i = 0;
216
+
217
+ while (true) {
218
+ i++;
219
+
220
+ try {
221
+ return cb();
222
+ } catch (e) {
223
+ logger.error(msg + ': ' + e);
224
+ if (i >= maxAttempts) {
225
+ throw e;
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ export function createLogger(level = 'debug') {
232
+ return winston.createLogger({
233
+ level: level,
234
+
235
+ format: winston.format.combine(
236
+ winston.format.printf(({ level, message }) => {
237
+ const indent = ' '.repeat(level.length + 2); // For ': ' after the level
238
+ const indentedMessage = message
239
+ .split('\n')
240
+ .map((line, index) => (index === 0 ? line : indent + line))
241
+ .join('\n');
242
+
243
+ const colorize = (l) => winston.format.colorize().colorize(l, l);
244
+
245
+ return `${colorize(level)}: ${indentedMessage}`;
246
+ })
247
+ ),
248
+
249
+ transports: [
250
+ new winston.transports.Console({
251
+ stderrLevels: ['error', 'warn', 'info', 'debug'],
252
+ handleExceptions: true
253
+ })
254
+ ]
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Run R command
260
+ *
261
+ * @param {winston.Logger} logger
262
+ * @param {string} rRoot - path to R installation root
263
+ * @param {string[]} args - command run arguments
264
+ * @param {{ paths?: string[],
265
+ * wd?: string,
266
+ * createWD?: boolean,
267
+ * env?: Record<string, string> }} opts - additional run parameters
268
+ */
269
+ export function runR(logger, rRoot, args, opts = {}) {
270
+ const cmdToRun = `${path.resolve(rRoot, 'bin/R')} ${args.join(' ')}`;
271
+ const renvRoot = path.join(path.resolve(rRoot), renvDirName)
272
+
273
+ env = {
274
+ RHOME: path.resolve(rRoot),
275
+ R_HOME_DIR: path.resolve(rRoot),
276
+ RENV_PATHS_ROOT: renvRoot,
277
+ RENV_PATHS_BINARY: path.join(renvRoot, 'binaries'),
278
+ RENV_PATHS_SOURCE: path.join(renvRoot, 'sources'),
279
+ RENV_PATHS_CACHE: path.join(renvRoot, 'cache'),
280
+ RENV_PATHS_PREFIX: 'common',
281
+ RENV_PATHS_PREFIX_AUTO: 'FALSE',
282
+ RENV_CONFIG_AUTO_SNAPSHOT: 'FALSE',
283
+ RENV_CONFIG_SYNCHRONIZED_CHECK: 'FALSE',
284
+
285
+ ...opts?.env,
286
+ }
287
+
288
+ if (opts?.paths) {
289
+ env.PATH = `${process.env['PATH']}:${opts.paths.join(':')}`
290
+ }
291
+
292
+ const runOpts = {
293
+ cwd: opts?.wd,
294
+ env: env,
295
+ }
296
+
297
+ logger.debug("Running command", cmdToRun, runOpts)
298
+
299
+ if (opts?.wd && !fs.existsSync(opts.wd) && opts?.createWD) {
300
+ logger.debug("creating working directory for command")
301
+ fs.mkdirSync(opts.wd, { recursive: true })
302
+ }
303
+ runInherit(cmdToRun, runOpts);
304
+ }
305
+
306
+ /**
307
+ * Run R command
308
+ *
309
+ * @param {winston.Logger} logger
310
+ * @param {string} rRoot - path to R installation root
311
+ * @param {string[]} args - command run arguments
312
+ * @param {{ paths?: string[],
313
+ * wd?: string,
314
+ * env?: Record<string, string> }} opts - additional run parameters
315
+ */
316
+ function runInRenv(logger, rRoot, args, opts = {}) {
317
+ runR(logger, rRoot, ['--quiet', '--no-echo', '--no-save', '--no-restore', '-e', quote('renv::activate()')], opts)
318
+ runR(logger, rRoot, args, opts)
319
+ }
320
+
321
+ const basicRPackages = [
322
+ 'renv',
323
+ 'devtools',
324
+ 'usethis',
325
+ "pkgdepends",
326
+ ];
327
+
328
+ /**
329
+ * Install the most basic R packages that are required all the time (like alternative package managers)
330
+ *
331
+ * @param {winston.Logger} logger
332
+ * @param {string} rRoot - path to R installation root
333
+ * @param {string[]} additionalPaths - list of paths to be appended to PATH environment variable during execution
334
+ */
335
+ export function installBasicRPackages(logger, rRoot, additionalPaths = []) {
336
+ const install = (...pkgs) => {
337
+ return `install.packages(${rList(pkgs)}, repos="https://cloud.r-project.org/")`;
338
+ };
339
+
340
+ logger.info(`Installing basic R packages to make them be available by default.`, basicRPackages);
341
+
342
+ runR(logger, rRoot,
343
+ ['--no-echo',
344
+ '-e', ...quote(install(...basicRPackages), "'")],
345
+ { paths: additionalPaths },
346
+ );
347
+ }
348
+
349
+ /**
350
+ * Read and parse R dependencies file, providing structured information on list of packages to be installed.
351
+ *
352
+ * @param {winston.Logger} logger
353
+ * @param {string} depsFile
354
+ * @returns {{
355
+ * type: string,
356
+ * version?: string,
357
+ * packages: string[]
358
+ * }[]}
359
+ */
360
+ export function readRDependencies(logger, depsFile) {
361
+ const fileContent = fs.readFileSync(depsFile, 'utf-8');
362
+ const deps = JSON.parse(fileContent)
363
+ logger.debug(`R dependencies loaded`, deps)
364
+ return deps
365
+ }
366
+
367
+ /**
368
+ * Build R dependencies and store prebuilt package archives in <packagesRoot> directory, so it can later be used
369
+ * as packages source during installation process on client side.
370
+ *
371
+ * @param {winston.Logger} logger
372
+ * @param {string} rRoot - path to R installation root
373
+ * @param {string} packagesRoot - path to directory wher R binary package archives should be stored.
374
+ * @param {string[]} additionalPaths - list of paths to be appended to PATH environment variable during execution
375
+ */
376
+ export function buildRDependencies(
377
+ logger,
378
+ rRoot,
379
+ packagesRoot,
380
+ depsDir,
381
+ additionalPaths = [],
382
+ ) {
383
+ if (!fs.existsSync(packagesRoot)) {
384
+ fs.mkdirSync(packagesRoot, { recursive: true })
385
+ }
386
+
387
+ const projectsFile = path.join(path.resolve(rRoot), renvDirName, 'projects')
388
+ const origProjectsFile = projectsFile + '.orig'
389
+ if (fs.existsSync(projectsFile)) {
390
+ fs.renameSync(projectsFile, origProjectsFile)
391
+ }
392
+
393
+ const allDeps = new Set();
394
+ let biocVersion = ''
395
+
396
+ const lockFile = 'renv.lock'
397
+ const initFile = 'init.R'
398
+
399
+ const lockPath = path.join(depsDir, lockFile)
400
+
401
+ if (fs.existsSync(lockPath)) {
402
+ runInRenv(logger, rRoot, [
403
+ '--no-echo',
404
+ '-e', ...quote(`renv::restore( clean = TRUE )`),
405
+ ], {
406
+ wd: tempRenvDir,
407
+ paths: additionalPaths,
408
+ })
409
+
410
+ } else {
411
+ if (!fs.existsSync(path.join(depsDir, initFile))) {
412
+ throw new Error(
413
+ `directory ${depsDir} has no ${lockFile} or ${initFile}. No dependencies detected.\n`+
414
+ `Create empty init.R file if you really need empty R run environment without any preinstalled dependencies`
415
+ )
416
+ }
417
+
418
+ runInRenv(logger, rRoot, ['--no-echo', `--file=${initFile}`], {
419
+ wd: tempRenvDir,
420
+ paths: additionalPaths,
421
+ })
422
+ }
423
+
424
+ const lockData = fs.readFileSync(lockPath, { encoding: 'utf-8' });
425
+ const lockInfo = JSON.parse(lockData.toString())
426
+ for (const pkgInfo of Object.values(lockInfo.Packages)) {
427
+ allDeps.add(`${pkgInfo.Package}@${pkgInfo.Version}`)
428
+ if (pkgInfo.Package === 'BiocVersion') {
429
+ if (biocVersion !== '' && biocVersion !== pkgInfo.Version) {
430
+ throw new Error(`BiocVersion conflict: ${pkgInfo.Version} and ${biocVersion} cannot be used in the same run environment`)
431
+ }
432
+ biocVersion = pkgInfo.Version
433
+ }
434
+ }
435
+
436
+ fs.rmSync(tempRenvDir, { recursive: true })
437
+ if (fs.existsSync(origProjectsFile)) {
438
+ fs.renameSync(origProjectsFile, projectsFile)
439
+ }
440
+
441
+ runR(logger, rRoot,
442
+ [
443
+ `--file=${pkg.asset('collect-dependencies.R')}`,
444
+ `--args ${quote(Array.from(allDeps)).join(' ')}`
445
+ ], {
446
+ wd: packagesRoot,
447
+ createWD: true,
448
+ paths: additionalPaths,
449
+ })
450
+
451
+ biocVersion = biocVersion.split('.').slice(0, -1).join('.') // 3.20.0 -> 3.20
452
+
453
+ patchREnviron(logger, rRoot, {
454
+ R_BIOC_VERSION: biocVersion,
455
+ BIOCONDUCTOR_ONLINE_VERSION_DIAGNOSIS: "FALSE",
456
+
457
+ RENV_PATHS_PREFIX: "common",
458
+ RENV_CONFIG_AUTO_SNAPSHOT: "FALSE",
459
+ RENV_CONFIG_SYNCHRONIZED_CHECK: "FALSE",
460
+ },
461
+ "Environment customisation for Platforma Backend");
462
+ }
463
+
464
+ /**
465
+ * Appends Renviron file with defaults for given environment variables.
466
+ *
467
+ * @param {winston.Logger} logger
468
+ * @param {string} rRoot
469
+ * @param {Record<string, string>} envs
470
+ */
471
+ function patchREnviron(
472
+ logger,
473
+ rRoot,
474
+ envs = {},
475
+ comment = "",
476
+ ) {
477
+ let rEnvironPath = path.join(rRoot, 'etc', 'Renviron')
478
+ if (!fs.existsSync(rEnvironPath)) {
479
+ rEnvironPath = path.join(rRoot, 'etc', 'Renviron.site')
480
+ }
481
+ if (!fs.existsSync(rEnvironPath)) {
482
+ throw new Error(`Renviron file not found in ${path.join(rRoot, 'etc')} directory. Can't patch default R environment settings`)
483
+ }
484
+
485
+ if (comment) {
486
+ fs.appendFileSync(rEnvironPath, `\n\n##\n## ${comment}\n##\n\n`, 'utf8');
487
+ }
488
+
489
+ for (const [envName, envValue] of Object.entries(envs)) {
490
+ line = `${envName}=\${${envName}:-'${envValue}'}`
491
+ fs.appendFileSync(rEnvironPath, line + '\n', 'utf8');
492
+ }
493
+ }
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
+ }
@@ -0,0 +1,37 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs';
4
+
5
+ import * as pkg from './pkg.js';
6
+ import * as util from './util.js';
7
+
8
+ const platformName = os.arch() === 'arm64' ? 'windows-aarch64' : `windows-x64`;
9
+
10
+ export async function buildR(logger, version) {
11
+ if (platformName !== 'windows-x64') {
12
+ logger.error(`Current OS is not Windows x64 (${platformName})`);
13
+ process.exit(1);
14
+ }
15
+
16
+ const distDir = path.join(util.rDistDir, platformName);
17
+
18
+ // Copy Windows R stub files to dist directory
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
+ });
26
+
27
+ // TODO: R for windows
28
+ }
29
+
30
+ export async function buildDeps(logger, version) {
31
+ const distDir = path.join(util.rDistDir, platformName);
32
+
33
+ // Copy original R to current package as we cannot distribute them separately now.
34
+ // util.runInherit(`rsync '${rDistDir}/' '${distDir}/'`)
35
+
36
+ // TODO: R for windows
37
+ }