UNPKG

4.84 kBPlain TextView Raw
1import fs from 'fs'
2import path from 'path'
3import { SoftwareEnvironment, SoftwarePackage } from '@stencila/schema'
4
5import DockerParser from './DockerParser'
6import parsers from './parsers'
7
8import DockerGenerator from './DockerGenerator'
9import DockerBuilder from './DockerBuilder'
10import DockerExecutor from './DockerExecutor'
11import IUrlFetcher from './IUrlFetcher'
12
13/**
14 * Compiles a project into a Dockerfile, or Docker image
15 */
16export default class DockerCompiler {
17
18 /**
19 * The instance of IUrlFetcher to fetch URLs
20 */
21 private readonly urlFetcher: IUrlFetcher
22
23 constructor (urlFetcher: IUrlFetcher) {
24 this.urlFetcher = urlFetcher
25 }
26
27 /**
28 * Compile a project
29 *
30 * @param source The folder, Dockerfile or `SoftwareEnvironment` to compile
31 * @param build Should the Docker image be built?
32 * @param comments Should comments be added to the Dockerfile?
33 * @param stencila Should relevant Stencila language packages be installed in the image?
34 */
35 async compile (source: string, build: boolean = true, comments: boolean = true, stencila: boolean = false): Promise<SoftwareEnvironment | null> {
36 let folder
37 if (source.substring(0, 7) === 'file://') {
38 folder = source.substring(7)
39 } else {
40 folder = source
41 }
42
43 let dockerfile
44 let environ
45
46 if (fs.existsSync(path.join(folder, 'Dockerfile'))) {
47 // Dockerfile found so use that
48 dockerfile = 'Dockerfile'
49 environ = await new DockerParser(this.urlFetcher, folder).parse()
50 } else {
51 if (fs.existsSync(path.join(folder, 'environ.jsonld'))) {
52 // Read existing environment from file
53 const jsonld = fs.readFileSync(path.join(folder, 'environ.jsonld'), 'utf8')
54 const initializer = JSON.parse(jsonld)
55 environ = new SoftwareEnvironment(initializer)
56 } else {
57 // Create environment by merging packages
58 // generated by each language parser
59 environ = new SoftwareEnvironment()
60 environ.name = path.basename(folder)
61 for (let ParserClass of parsers) {
62 const parser = new ParserClass(this.urlFetcher, folder)
63 const pkg = await parser.parse()
64 if (pkg) environ.softwareRequirements.push(pkg)
65 }
66
67 // Save environ as an intermediate file
68 const jsonld = JSON.stringify(environ.toJSONLD(), null, ' ')
69 fs.writeFileSync(path.join(folder, '.environ.jsonld'), jsonld)
70 }
71
72 // Generate Dockerfile
73 dockerfile = '.Dockerfile'
74 new DockerGenerator(this.urlFetcher, environ, folder).generate(comments, stencila)
75 }
76
77 if (build) {
78 // Use the name of the environment, if possible
79 let name = (environ && environ.name) || undefined
80 // Build the image!
81 const builder = new DockerBuilder()
82 await builder.build(folder, name, dockerfile)
83 }
84
85 return environ
86 }
87
88 /**
89 * Execute the project by compiling, building and running a Docker container for it
90 *
91 * @param source The project to execute
92 */
93 async execute (source: string, command: string = '') {
94 let folder
95 if (source.substring(0, 7) === 'file://') {
96 folder = source.substring(7)
97 } else {
98 folder = source
99 }
100
101 // Compile the environment first
102 let environ = await this.compile(source)
103 if (!environ) throw new Error('Environment not created')
104 if (!environ.name) throw new Error('Environment does not have a name')
105
106 // Execute the environment's image (which is built in compile())
107 const executor = new DockerExecutor()
108 return executor.execute(environ.name, folder, command)
109 }
110
111 /**
112 * Find out who contributed to the packages that your project
113 * depends upon.
114 *
115 * @param folder The project to examine
116 * @param maxDepth The maximum dependency recursion depth
117 */
118 async who (folder: string, maxDepth: number = 100): Promise<object> {
119 let environ = await this.compile(folder, false)
120 if (!environ) throw new Error('Environment not created')
121
122 const people: {[key: string]: Array<string>} = {}
123
124 /**
125 * Get the people for a software package
126 *
127 * @param pkg The package
128 * @param depth The current recursion depth
129 */
130 function get (pkg: SoftwarePackage | SoftwareEnvironment, depth: number = 0) {
131 let all = pkg.authors.concat(pkg.contributors).concat(pkg.creators)
132 for (let person of all) {
133 const name = person.name
134 if (people[name]) {
135 if (!people[name].includes(pkg.name)) people[name].push(pkg.name)
136 } else {
137 people[name] = [pkg.name]
138 }
139 }
140 // Keep going deeper, if we haven't yet reached the maximum depth
141 if (depth < maxDepth) {
142 for (let req of pkg.softwareRequirements) {
143 get(req, depth + 1)
144 }
145 }
146 }
147 get(environ)
148
149 return people
150 }
151}