UNPKG

9.54 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6const path_1 = __importDefault(require("path"));
7const Parser_1 = __importDefault(require("./Parser"));
8const schema_1 = require("@stencila/schema");
9/**
10 * Dockter `Parser` class for R requirements files and source code.
11 *
12 * For each package, meta-data is obtained from http://crandb.r-pkg.org and used to create a `SoftwarePackage` instance
13 * using crosswalks from column "R Package Description" in https://github.com/codemeta/codemeta/blob/master/crosswalk.csv
14 *
15 * System dependencies for each package are obtained from https://sysreqs.r-hub.io.
16 */
17class RParser extends Parser_1.default {
18 /**
19 * Parse a folder by detecting any R requirements or source code files
20 * and return a `SoftwarePackage` instance
21 */
22 async parse() {
23 const pkg = new schema_1.SoftwarePackage();
24 let name;
25 let version;
26 let date = undefined;
27 let packages = [];
28 if (this.exists('DESCRIPTION')) {
29 // Read the existing/generated DESCRIPTION file
30 let desc = this.read('DESCRIPTION');
31 // Get `name`
32 const matchName = desc.match(/^Package:\s*(.+)/m);
33 if (matchName) {
34 name = matchName[1];
35 }
36 // Get `date`, if no date then use yesterday's date to ensure
37 // packages are available on MRAN
38 const matchDate = desc.match(/^Date:\s*(.+)/m);
39 if (matchDate) {
40 let dateNum = Date.parse(matchDate[1]);
41 if (isNaN(dateNum)) {
42 throw new Error('Unable to parse date in DESCRIPTION file: ' + matchDate[1]);
43 }
44 else {
45 date = new Date(dateNum);
46 }
47 }
48 // Get dependencies
49 const start = /^Imports:[ \t]*\n/gm.exec(desc);
50 if (start) {
51 // Find next un-indented line or use end of string
52 let match = desc.substring(start.index + start[0].length).match(/\n^\w/m);
53 let end;
54 if (match)
55 end = match.index;
56 else
57 end = desc.length - 1;
58 const imports = desc.substring(start.index + start[0].length, end);
59 for (let imported of imports.split(',')) {
60 let pkg;
61 const match = imported.match(/^\s*(\w+).*/);
62 if (match) {
63 pkg = match[1];
64 }
65 else {
66 pkg = imported.trim();
67 }
68 if (pkg.length)
69 packages.push(pkg);
70 }
71 }
72 }
73 else {
74 // Scan the directory for any R or Rmd files
75 const files = this.glob(['**/*.R', '**/*.Rmd']);
76 if (files.length) {
77 // Analyse files for `library(<pkg>)`, `require(<pkg>)`, `<pkg>::<member>`, `<pkg>:::<member>`
78 // Wondering WTF this regex does? See https://regex101.com/r/hG4iij/4
79 const regex = /(?:(?:library|require)\s*\(\s*(?:(?:\s*(\w+)\s*)|(?:"([^"]*)")|(?:'([^']*)'))\s*\))|(?:(\w+):::?\w+)/g;
80 for (let file of files) {
81 let code = this.read(file);
82 let match = regex.exec(code);
83 while (match) {
84 const pkg = match[1] || match[2] || match[3] || match[4];
85 if (!packages.includes(pkg))
86 packages.push(pkg);
87 match = regex.exec(code);
88 }
89 }
90 packages.sort();
91 }
92 else {
93 // If no R files detected, return null
94 return null;
95 }
96 }
97 // Default to the folder name, with any non alphanumerics removed to ensure compatibility
98 // with R package name requirements
99 if (!name)
100 name = path_1.default.basename(this.folder).replace(/[^a-zA-Z0-9]/g, '');
101 // Default to yesterday's date (to ensure MRAN is available for the date)
102 if (!date)
103 date = new Date(Date.now() - 24 * 3600 * 1000);
104 // Set package properties
105 pkg.name = name;
106 pkg.runtimePlatform = 'R';
107 pkg.datePublished = date.toISOString().substring(0, 10);
108 // For each dependency, query https://crandb.r-pkg.org to get a manifest including it's own
109 // dependencies and convert it to a `SoftwarePackage`
110 pkg.softwareRequirements = await Promise.all(packages.map(name => this.createPackage(name)));
111 return pkg;
112 }
113 /**
114 * Create a `SoftwarePackage` instance from a R package name
115 *
116 * This method fetches meta-data for a R package to populate the properties
117 * of a `SoftwarePackage` instance. It recursively fetches meta-data on the package's
118 * dependencies, including system dependencies.
119 *
120 * @param name Name of the R package
121 */
122 async createPackage(name) {
123 // Create new package instance and populate it's
124 // properties in order of type hierarchy:
125 // Thing > CreativeWork > SoftwareSourceCode > SoftwarePackage
126 const pkg = new schema_1.SoftwarePackage();
127 pkg.name = name;
128 // These packages are built-in to R distributions, so we don't need to collect
129 // meta-data for them.
130 if (['stats', 'graphics', 'grDevices', 'tools', 'utils', 'datasets', 'methods'].includes(name)) {
131 return pkg;
132 }
133 // Fetch meta-data from CRANDB
134 // If null (i.e. 404) then return package as is
135 const crandb = await this.fetch(`http://crandb.r-pkg.org/${name}`);
136 if (crandb === null)
137 return pkg;
138 // schema:Thing
139 pkg.description = crandb.Description;
140 if (crandb.URL)
141 pkg.urls = crandb.URL.split(',');
142 // schema:CreativeWork
143 if (crandb.Author) {
144 crandb.Author.split(',\n').map((author) => {
145 const match = author.match(/^([^\[]+?) \[([^\]]+)\]/);
146 if (match) {
147 const name = match[1];
148 const person = schema_1.Person.fromText(name);
149 const roles = match[2].split(', ');
150 if (roles.includes('aut'))
151 pkg.authors.push(person);
152 if (roles.includes('ctb'))
153 pkg.contributors.push(person);
154 if (roles.includes('cre'))
155 pkg.creators.push(person);
156 }
157 else {
158 pkg.authors.push(schema_1.Person.fromText(author));
159 }
160 });
161 }
162 pkg.datePublished = crandb['Date/Publication'];
163 pkg.license = crandb.License;
164 // schema:SoftwareSourceCode
165 pkg.runtimePlatform = 'R';
166 if (crandb.URL)
167 pkg.codeRepository = crandb.URL.split(','); // See issue #35
168 // stencila:SoftwarePackage
169 // Create `SoftwarePackage` for each dependency
170 if (crandb.Imports) {
171 pkg.softwareRequirements = await Promise.all(Object.entries(crandb.Imports).map(([name, version]) => this.createPackage(name)));
172 }
173 // Required system dependencies are obtained from https://sysreqs.r-hub.io and
174 // added as `softwareRequirements` with "deb" as `runtimePlatform`
175 const sysreqs = await this.fetch(`https://sysreqs.r-hub.io/pkg/${name}`);
176 for (let sysreq of sysreqs) {
177 const keys = Object.keys(sysreq);
178 if (keys.length > 1)
179 throw new Error(`Expected on one key for each sysreq but got: ${keys.join(',')}`);
180 const name = keys[0];
181 const debPackage = sysreq[name].platforms['DEB'];
182 // The deb package can be null e.g. `curl https://sysreqs.r-hub.io/pkg/lubridate`
183 if (typeof debPackage === 'string') {
184 // Handle strings e.g. curl https://sysreqs.r-hub.io/pkg/XML
185 const required = new schema_1.SoftwarePackage();
186 required.name = debPackage;
187 required.runtimePlatform = 'deb';
188 pkg.softwareRequirements.push(required);
189 }
190 else if (Array.isArray(debPackage)) {
191 // Handle arrays e.g. curl https://sysreqs.r-hub.io/pkg/gsl
192 for (let deb of debPackage.filter(deb => deb.distribution === 'Ubuntu' && deb.releases === undefined)) {
193 if (deb.buildtime) {
194 const required = new schema_1.SoftwarePackage();
195 required.name = deb.buildtime;
196 required.runtimePlatform = 'deb';
197 pkg.softwareRequirements.push(required);
198 }
199 if (deb.runtime) {
200 const required = new schema_1.SoftwarePackage();
201 required.name = deb.runtime;
202 required.runtimePlatform = 'deb';
203 pkg.softwareRequirements.push(required);
204 }
205 }
206 }
207 }
208 return pkg;
209 }
210}
211exports.default = RParser;