1 | ;
|
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
4 | };
|
5 | Object.defineProperty(exports, "__esModule", { value: true });
|
6 | const path_1 = __importDefault(require("path"));
|
7 | const Parser_1 = __importDefault(require("./Parser"));
|
8 | const 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 | */
|
17 | class 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 | }
|
211 | exports.default = RParser;
|