UNPKG

4.83 kBPlain TextView Raw
1import { SoftwarePackage } from '@stencila/schema'
2import path from 'path'
3
4import PackageGenerator from './PackageGenerator'
5import PythonSystemPackageLookup from './PythonSystemPackageLookup'
6import IUrlFetcher from './IUrlFetcher'
7
8const GENERATED_REQUIREMENTS_FILE = '.requirements.txt'
9
10/**
11 * A Dockerfile generator for Python packages
12 */
13export default class PythonGenerator extends PackageGenerator {
14 /**
15 * The Python Major version, i.e. 2 or 3
16 */
17 private readonly pythonMajorVersion: number
18
19 /**
20 * An instance of `PythonSystemPackageLookup` with which to look up system dependencies of Python packages
21 */
22 private readonly systemPackageLookup: PythonSystemPackageLookup
23
24 // Methods that override those in `Generator`
25
26 constructor (urlFetcher: IUrlFetcher, pkg: SoftwarePackage, folder?: string, pythonMajorVersion: number = 3) {
27 super(urlFetcher, pkg, folder)
28
29 this.pythonMajorVersion = pythonMajorVersion
30 this.systemPackageLookup = PythonSystemPackageLookup.fromFile(path.join(__dirname, 'PythonSystemDependencies.json'))
31 }
32
33 /**
34 * Return the `pythonMajorVersion` (as string) if it is not 2, otherwise return an empty string (if it is 2). This is
35 * for appending to things like pip{3} or python{3}.
36 */
37 pythonVersionSuffix (): string {
38 return this.pythonMajorVersion === 2 ? '' : `${this.pythonMajorVersion}`
39 }
40
41 /**
42 * Check if this Generator's package applies (if it is Python).
43 */
44 applies (): boolean {
45 return this.package.runtimePlatform === 'Python'
46 }
47
48 /**
49 * Generate a list of system (apt) packages by looking up with `this.systemPackageLookup`.
50 */
51 aptPackages (sysVersion: string): Array<string> {
52 let aptRequirements: Array<string> = []
53
54 this.package.softwareRequirements.map(requirement => {
55 aptRequirements = aptRequirements.concat(
56 this.systemPackageLookup.lookupSystemPackage(
57 requirement.name, this.pythonMajorVersion, 'deb', sysVersion
58 )
59 )
60 })
61
62 let dedupedRequirements: Array<string> = []
63 aptRequirements.map(aptRequirement => {
64 if (!dedupedRequirements.includes(aptRequirement)) {
65 dedupedRequirements.push(aptRequirement)
66 }
67 })
68 return [`python${this.pythonVersionSuffix()}`, `python${this.pythonVersionSuffix()}-pip`].concat(
69 dedupedRequirements
70 )
71 }
72
73 /**
74 * Build the contents of a `requirements.txt` file by joining the Python package name to its version specifier.
75 */
76 generateRequirementsContent (): string {
77 if (!this.package.softwareRequirements) {
78 return ''
79 }
80
81 return this.filterPackages('Python').map(
82 requirement => `${requirement.name}${requirement.version}`
83 ).join('\n')
84 }
85
86 /**
87 * Get the pip command to install the Stencila package
88 */
89 stencilaInstall (sysVersion: string): string | undefined {
90 return `pip${this.pythonVersionSuffix()} install --no-cache-dir https://github.com/stencila/py/archive/91a05a139ac120a89fc001d9d267989f062ad374.zip`
91 }
92
93 /**
94 * Write out the generated requirements content to `GENERATED_REQUIREMENTS_FILE` or none exists, just instruct the
95 * copy of a `requirements.txt` file as part of the Dockerfile. If that does not exist, then no COPY should be done.
96 */
97 installFiles (sysVersion: string): Array<[string, string]> {
98 let requirementsContent = this.generateRequirementsContent()
99
100 if (requirementsContent !== '') {
101 this.write(GENERATED_REQUIREMENTS_FILE, requirementsContent)
102 return [[GENERATED_REQUIREMENTS_FILE, 'requirements.txt']]
103 }
104
105 if (this.exists('requirements.txt')) {
106 return [['requirements.txt', 'requirements.txt']]
107 }
108
109 return []
110 }
111
112 /**
113 * Generate the right pip command to install the requirements, appends the correct Python major version to `pip`.
114 */
115 installCommand (sysVersion: string): string | undefined {
116 return `pip${this.pythonVersionSuffix()} install --requirement requirements.txt`
117 }
118
119 /**
120 * The files to copy into the Docker image
121 *
122 * Copies all `*.py` files to the container
123 */
124 projectFiles (): Array<[string, string]> {
125 const pyFiles = this.glob('**/*.py')
126 return pyFiles.map(file => [file, file]) as Array<[string, string]>
127 }
128
129 /**
130 * The command to execute in a container created from the Docker image
131 *
132 * If there is a top-level `main.py` or `cmd.py` then that will be used,
133 * otherwise, the first `*.R` files by alphabetical order will be used.
134 */
135 runCommand (): string | undefined {
136 const pyFiles = this.glob('**/*.py')
137 if (pyFiles.length === 0) return
138 let script
139 if (pyFiles.includes('main.py')) script = 'main.py'
140 else if (pyFiles.includes('cmd.py')) script = 'cmd.py'
141 else script = pyFiles[0]
142 return `python${this.pythonVersionSuffix()} ${script}`
143 }
144
145}