@jpetit/toolkit 3.0.23 → 3.1.2
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/assets/prompts/creators/create-solution.tpl.txt +10 -0
- package/assets/prompts/creators/create-statement.tpl.txt +21 -0
- package/assets/prompts/creators/create-translation.tpl.txt +5 -0
- package/assets/prompts/creators/private-test-cases.txt +6 -0
- package/assets/prompts/creators/sample-test-cases.txt +6 -0
- package/assets/prompts/creators/system-prompt.txt +2 -0
- package/assets/prompts/examples/statement-coda.tex +7 -0
- package/assets/prompts/examples/statement.tex +19 -0
- package/assets/prompts/generators/efficiency.md +41 -0
- package/assets/prompts/generators/hard.md +47 -0
- package/assets/prompts/generators/random.md +39 -0
- package/assets/prompts/proglangs/cc.md +3 -0
- package/assets/prompts/proglangs/py.md +40 -0
- package/lib/ai.ts +60 -4
- package/lib/cleaner.ts +24 -13
- package/lib/compilers/base.ts +70 -14
- package/lib/compilers/clojure.ts +21 -10
- package/lib/compilers/gcc.ts +4 -33
- package/lib/compilers/ghc.ts +4 -40
- package/lib/compilers/gxx.ts +4 -33
- package/lib/compilers/index.ts +9 -0
- package/lib/compilers/java.ts +105 -0
- package/lib/compilers/python3.ts +44 -37
- package/lib/compilers/run-clojure.ts +101 -0
- package/lib/compilers/run-haskell.ts +26 -22
- package/lib/compilers/run-python.ts +29 -35
- package/lib/compilers/rust.ts +39 -0
- package/lib/create-with-jutgeai.ts +407 -0
- package/lib/create-with-template.ts +55 -0
- package/lib/data.ts +6 -0
- package/lib/doctor.ts +86 -6
- package/lib/generate.ts +132 -290
- package/lib/helpers.ts +48 -0
- package/lib/inspector.ts +253 -0
- package/lib/jutge_api_client.ts +4631 -0
- package/lib/maker.ts +202 -289
- package/lib/settings.ts +26 -17
- package/lib/tui.ts +25 -15
- package/lib/types.ts +40 -5
- package/lib/upload.ts +216 -0
- package/lib/utils.ts +82 -14
- package/lib/versions.ts +46 -0
- package/package.json +50 -11
- package/toolkit/about.ts +43 -0
- package/toolkit/ai.ts +44 -18
- package/toolkit/check.ts +16 -0
- package/toolkit/clean.ts +16 -26
- package/toolkit/compilers.ts +4 -4
- package/toolkit/config.ts +91 -0
- package/toolkit/create.ts +30 -58
- package/toolkit/doctor.ts +15 -11
- package/toolkit/generate.ts +195 -98
- package/toolkit/index.ts +32 -21
- package/toolkit/make.ts +12 -48
- package/toolkit/upgrade.ts +9 -0
- package/toolkit/upload.ts +19 -0
- package/toolkit/create-jutge-ai.ts +0 -101
- package/toolkit/create-template.ts +0 -55
- package/toolkit/create-wizard.ts +0 -6
- package/toolkit/init.ts +0 -56
- package/toolkit/verify.ts +0 -19
package/lib/maker.ts
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
import tui from '../lib/tui'
|
|
2
|
-
import { filesAreEqual, fileSize, nothing, projectDir, readText, readYaml, writeText } from '../lib/utils'
|
|
3
1
|
import chalk from 'chalk'
|
|
4
2
|
import { execa } from 'execa'
|
|
5
|
-
import { cp, exists, glob, mkdir,
|
|
3
|
+
import { cp, exists, glob, mkdir, rename } from 'fs/promises'
|
|
6
4
|
import { imageSizeFromFile } from 'image-size/fromFile'
|
|
7
|
-
import {
|
|
8
|
-
import { basename, dirname, join, normalize, resolve, sep } from 'path'
|
|
5
|
+
import { basename, dirname, join, normalize, resolve } from 'path'
|
|
9
6
|
import prettyBytes from 'pretty-bytes'
|
|
10
7
|
import prettyMs from 'pretty-ms'
|
|
8
|
+
import tui from '../lib/tui'
|
|
9
|
+
import {
|
|
10
|
+
filesAreEqual,
|
|
11
|
+
fileSize,
|
|
12
|
+
humanid,
|
|
13
|
+
isDirectory,
|
|
14
|
+
isDirectoryInDir,
|
|
15
|
+
nothing,
|
|
16
|
+
projectDir,
|
|
17
|
+
readText,
|
|
18
|
+
toolkitPrefix,
|
|
19
|
+
writeText,
|
|
20
|
+
} from '../lib/utils'
|
|
11
21
|
import { getCompilerByExtension, getCompilerById } from './compilers'
|
|
12
22
|
import type { Compiler } from './compilers/base'
|
|
13
|
-
import { languageNames
|
|
14
|
-
import {
|
|
15
|
-
import { th } from 'zod/locales'
|
|
16
|
-
|
|
17
|
-
export interface MakerOptions {
|
|
18
|
-
verbose?: boolean
|
|
19
|
-
directory: string
|
|
20
|
-
}
|
|
23
|
+
import { languageNames } from './data'
|
|
24
|
+
import { newInspector, Inspector } from './inspector'
|
|
21
25
|
|
|
22
|
-
export async function newMaker(
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
return maker
|
|
26
|
+
export async function newMaker(directory: string): Promise<Maker> {
|
|
27
|
+
const inspector = await newInspector(directory)
|
|
28
|
+
return new Maker(inspector)
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
type ExecutionResult = {
|
|
@@ -34,210 +37,21 @@ type ExecutionResult = {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export class Maker {
|
|
37
|
-
|
|
38
|
-
directory: string
|
|
39
|
-
structure: 'multi' | 'single' = 'multi'
|
|
40
|
-
// multi: many languages in the same directory
|
|
41
|
-
// single: one language in a subdirectory of a .pbm directory
|
|
42
|
-
language: string | null = null // only for single structure, tells which language it is
|
|
43
|
-
|
|
44
|
-
// @ts-expect-error: will be initialized later
|
|
45
|
-
handler: Handler
|
|
46
|
-
languages: string[] = []
|
|
47
|
-
originalLanguage: string | null = null
|
|
48
|
-
problemYmls: Record<string, any> = {} // language -> content
|
|
49
|
-
solutions: string[] = []
|
|
50
|
-
goldenSolution: string | null = null
|
|
51
|
-
testcases: string[] = [] // testcase names without extensions
|
|
52
|
-
|
|
53
|
-
public constructor(options: MakerOptions) {
|
|
54
|
-
this.verbose = options.verbose || false
|
|
55
|
-
this.directory = normalize(join(options.directory, '.')) // normalize path and ensure no trailing slash
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public async makeInspection() {
|
|
59
|
-
await tui.section('Inspecting problem', async () => {
|
|
60
|
-
await this.loadStructure()
|
|
61
|
-
await this.loadLanguages()
|
|
62
|
-
await this.loadHandler()
|
|
63
|
-
await this.loadOriginalLanguage()
|
|
64
|
-
await this.loadSolutions()
|
|
65
|
-
await this.loadGoldenSolution()
|
|
66
|
-
await this.loadTestcases()
|
|
67
|
-
await this.loadScores()
|
|
68
|
-
await this.loadAwards()
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async loadStructure() {
|
|
73
|
-
await tui.section('Determining structure', async () => {
|
|
74
|
-
await nothing()
|
|
75
|
-
if (this.directory.endsWith('.pbm')) {
|
|
76
|
-
this.structure = 'multi'
|
|
77
|
-
} else {
|
|
78
|
-
this.structure = 'single'
|
|
79
|
-
}
|
|
80
|
-
console.log(this.structure)
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private async loadLanguages() {
|
|
85
|
-
await tui.section('Loading languages', async () => {
|
|
86
|
-
if (this.structure === 'multi') {
|
|
87
|
-
await this.loadLanguagesMulti()
|
|
88
|
-
} else {
|
|
89
|
-
await this.loadLanguagesSingle()
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private async loadLanguagesMulti() {
|
|
95
|
-
const files = await Array.fromAsync(glob('problem.*.yml', { cwd: this.directory }))
|
|
96
|
-
const languages = files
|
|
97
|
-
.map((file) => {
|
|
98
|
-
const match = file.match(/problem\.(.+)\.yml/)
|
|
99
|
-
return match ? match[1] : null
|
|
100
|
-
})
|
|
101
|
-
.filter((language) => language && language in languageNames)
|
|
102
|
-
this.languages = languages as string[]
|
|
103
|
-
tui.yaml(this.languages)
|
|
104
|
-
|
|
105
|
-
for (const language of this.languages) {
|
|
106
|
-
await tui.section(`Loading problem.${language}.yml`, async () => {
|
|
107
|
-
this.problemYmls[language] = await readYaml(`${this.directory}/problem.${language}.yml`)
|
|
108
|
-
tui.yaml(this.problemYmls[language])
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private async loadLanguagesSingle() {
|
|
114
|
-
for (const language of Object.keys(languageNames)) {
|
|
115
|
-
const name = join(this.directory, '..', language, `problem.${language}.yml`)
|
|
116
|
-
if (await exists(name)) {
|
|
117
|
-
this.languages.push(language)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
tui.yaml(this.languages)
|
|
121
|
-
|
|
122
|
-
for (const language of this.languages) {
|
|
123
|
-
const name = join(this.directory, '..', language, `problem.${language}.yml`)
|
|
124
|
-
await tui.section(`Loading ..${sep}${language}${sep}problem.${language}.yml`, async () => {
|
|
125
|
-
this.problemYmls[language] = await readYaml(name)
|
|
126
|
-
tui.yaml(this.problemYmls[language])
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.language = basename(this.directory)
|
|
131
|
-
}
|
|
40
|
+
inspector: Inspector
|
|
132
41
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const data = await readYaml(`${this.directory}/handler.yml`)
|
|
136
|
-
this.handler = ZHandler.parse(data)
|
|
137
|
-
tui.yaml(this.handler)
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private async loadScores() {
|
|
142
|
-
await tui.section('Loading scores.yml', async () => {
|
|
143
|
-
if (await exists(`${this.directory}/scores.yml`)) {
|
|
144
|
-
const data = await readYaml(`${this.directory}/scores.yml`)
|
|
145
|
-
const scores = ZScores.parse(data)
|
|
146
|
-
tui.yaml(scores)
|
|
147
|
-
} else {
|
|
148
|
-
tui.print('scores.yml not defined')
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
private async loadOriginalLanguage() {
|
|
153
|
-
await tui.section('Determining original language', async () => {
|
|
154
|
-
await nothing()
|
|
155
|
-
for (const language of this.languages) {
|
|
156
|
-
if ('author' in this.problemYmls[language]) {
|
|
157
|
-
this.originalLanguage = language
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (!this.originalLanguage) {
|
|
162
|
-
throw new Error('No original language found (a language with an author field)')
|
|
163
|
-
}
|
|
164
|
-
tui.print(this.originalLanguage)
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private async loadSolutions() {
|
|
169
|
-
await tui.section('Loading solutions', async () => {
|
|
170
|
-
const comaSeparatedExtensions = Object.keys(proglangNames).join(',')
|
|
171
|
-
const files = await Array.fromAsync(glob(`solution.{${comaSeparatedExtensions}}`, { cwd: this.directory }))
|
|
172
|
-
this.solutions = files
|
|
173
|
-
tui.yaml(this.solutions)
|
|
174
|
-
if (this.solutions.length === 0) throw new Error('No solutions found')
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private async loadGoldenSolution() {
|
|
179
|
-
await tui.section('Determining golden solution', async () => {
|
|
180
|
-
if (this.handler.compilers === 'RunPython') {
|
|
181
|
-
this.goldenSolution = 'solution.py'
|
|
182
|
-
} else if (this.handler.compilers === 'RunHaskell' || this.handler.compilers === 'GHC') {
|
|
183
|
-
this.goldenSolution = 'solution.hs'
|
|
184
|
-
} else if (this.handler.compilers === 'RunClojure' || this.handler.compilers === 'Clojure') {
|
|
185
|
-
this.goldenSolution = 'solution.clj'
|
|
186
|
-
} else {
|
|
187
|
-
// any compilers
|
|
188
|
-
const solutionProglang = this.handler.solution
|
|
189
|
-
const extension = proglangExtensions[solutionProglang]
|
|
190
|
-
if (!extension) {
|
|
191
|
-
throw new Error(`Unknown programming language ${solutionProglang} for solution`)
|
|
192
|
-
}
|
|
193
|
-
const goldenSolutionPath = join(this.directory, `solution.${extension}`)
|
|
194
|
-
const fileExists = await exists(goldenSolutionPath)
|
|
195
|
-
if (!fileExists) {
|
|
196
|
-
throw new Error(`Golden solution file ${goldenSolutionPath} not found`)
|
|
197
|
-
}
|
|
198
|
-
this.goldenSolution = `solution.${extension}`
|
|
199
|
-
}
|
|
200
|
-
tui.print(this.goldenSolution)
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private async loadTestcases() {
|
|
205
|
-
await tui.section('Loading testcases', async () => {
|
|
206
|
-
const files = await Array.fromAsync(glob('*.inp', { cwd: this.directory }))
|
|
207
|
-
this.testcases = files.map((file) => file.replace('.inp', '')).sort()
|
|
208
|
-
tui.yaml(this.testcases)
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async loadAwards() {
|
|
213
|
-
await tui.section('Loading awards', async () => {
|
|
214
|
-
if (await exists(join(this.directory, 'award.html'))) {
|
|
215
|
-
tui.success(tui.hyperlink(this.directory, 'award.html') + ' found')
|
|
216
|
-
} else {
|
|
217
|
-
tui.warning('award.html not found')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (await exists(join(this.directory, 'award.png'))) {
|
|
221
|
-
tui.success(tui.hyperlink(this.directory, 'award.png') + ' found')
|
|
222
|
-
const dimensions = await imageSizeFromFile(join(this.directory, 'award.png'))
|
|
223
|
-
await tui.image(join(this.directory, 'award.png'), 6, 3)
|
|
224
|
-
tui.print(`${dimensions.width}x${dimensions.height}`)
|
|
225
|
-
} else {
|
|
226
|
-
tui.warning('award.png not found')
|
|
227
|
-
}
|
|
228
|
-
})
|
|
42
|
+
public constructor(inspector: Inspector) {
|
|
43
|
+
this.inspector = inspector
|
|
229
44
|
}
|
|
230
45
|
|
|
231
46
|
async showDirectory() {
|
|
232
47
|
await tui.section('Directory', async () => {
|
|
233
48
|
await nothing()
|
|
234
|
-
tui.directory(resolve(this.directory))
|
|
235
|
-
const fullPath = normalize(resolve(this.directory))
|
|
49
|
+
tui.directory(resolve(this.inspector.directory))
|
|
50
|
+
const fullPath = normalize(resolve(this.inspector.directory))
|
|
236
51
|
if (!fullPath.endsWith('.pbm')) {
|
|
237
52
|
const lastPath = basename(fullPath)
|
|
238
53
|
const butLastPath = dirname(fullPath)
|
|
239
|
-
|
|
240
|
-
*/
|
|
54
|
+
|
|
241
55
|
if (!Object.keys(languageNames).includes(lastPath) || !butLastPath.endsWith('.pbm')) {
|
|
242
56
|
throw new Error(
|
|
243
57
|
'The problem directory should end with .pbm or be a language id inside a .pbm directory',
|
|
@@ -246,28 +60,58 @@ export class Maker {
|
|
|
246
60
|
}
|
|
247
61
|
})
|
|
248
62
|
}
|
|
63
|
+
|
|
249
64
|
public async makeProblem() {
|
|
250
65
|
await this.makeGoldenExecutable()
|
|
251
66
|
await this.makeCorrects()
|
|
252
|
-
await this.
|
|
67
|
+
await this.checkSolutions()
|
|
253
68
|
await this.makePdfs()
|
|
254
69
|
await this.makeTexts()
|
|
255
70
|
}
|
|
256
71
|
|
|
257
72
|
public async makeExecutable(program: string) {
|
|
258
73
|
const compiler = this.selectCompiler()
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
74
|
+
const newProgram = `${toolkitPrefix()}-${program}`
|
|
75
|
+
await tui.section(
|
|
76
|
+
`Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
77
|
+
async () => {
|
|
78
|
+
await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
await tui.section(
|
|
82
|
+
`Compiling ${tui.hyperlink(this.inspector.directory, newProgram)} with ${compiler.name()} (${this.inspector.handler.source_modifier})`,
|
|
83
|
+
async () => {
|
|
84
|
+
try {
|
|
85
|
+
let outputPath: string
|
|
86
|
+
if (this.inspector.handler.source_modifier === 'none') {
|
|
87
|
+
outputPath = await compiler.compileNormal(
|
|
88
|
+
this.inspector.handler,
|
|
89
|
+
this.inspector.directory,
|
|
90
|
+
newProgram,
|
|
91
|
+
)
|
|
92
|
+
} else if (this.inspector.handler.source_modifier === 'no_main') {
|
|
93
|
+
outputPath = await compiler.compileWithMain(
|
|
94
|
+
this.inspector.handler,
|
|
95
|
+
this.inspector.directory,
|
|
96
|
+
newProgram,
|
|
97
|
+
)
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Unknown source modifier: ${this.inspector.handler.source_modifier as string}`)
|
|
100
|
+
}
|
|
101
|
+
if (!(await exists(join(this.inspector.directory, outputPath)))) {
|
|
102
|
+
throw new Error(`Compilation failed for ${newProgram}`)
|
|
103
|
+
}
|
|
104
|
+
tui.success(`Compiled ${newProgram} to ${outputPath}`)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`Compilation failed`)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
)
|
|
266
110
|
}
|
|
267
111
|
|
|
268
112
|
public async makeExecutables() {
|
|
269
113
|
await tui.section('Compiling solutions', async () => {
|
|
270
|
-
for (const solution of this.solutions) {
|
|
114
|
+
for (const solution of this.inspector.solutions) {
|
|
271
115
|
await this.makeExecutable(solution)
|
|
272
116
|
}
|
|
273
117
|
})
|
|
@@ -275,26 +119,32 @@ export class Maker {
|
|
|
275
119
|
|
|
276
120
|
public async makeGoldenExecutable() {
|
|
277
121
|
await tui.section(
|
|
278
|
-
`Compiling golden solution ${tui.hyperlink(this.directory, this.goldenSolution!)}`,
|
|
122
|
+
`Compiling golden solution from ${tui.hyperlink(this.inspector.directory, this.inspector.goldenSolution!)}`,
|
|
279
123
|
async () => {
|
|
280
|
-
if (!this.goldenSolution) {
|
|
124
|
+
if (!this.inspector.goldenSolution) {
|
|
281
125
|
throw new Error('Golden solution not set')
|
|
282
126
|
}
|
|
283
|
-
await this.makeExecutable(this.goldenSolution)
|
|
127
|
+
await this.makeExecutable(this.inspector.goldenSolution)
|
|
284
128
|
},
|
|
285
129
|
)
|
|
286
130
|
}
|
|
287
131
|
|
|
288
|
-
public async
|
|
289
|
-
for (const solution of this.solutions) {
|
|
290
|
-
if (solution !== this.goldenSolution) {
|
|
291
|
-
await this.
|
|
132
|
+
public async checkSolutions() {
|
|
133
|
+
for (const solution of this.inspector.solutions) {
|
|
134
|
+
if (solution !== this.inspector.goldenSolution) {
|
|
135
|
+
await this.checkCandidate(solution)
|
|
292
136
|
}
|
|
293
137
|
}
|
|
294
138
|
}
|
|
295
139
|
|
|
296
140
|
async makeCorrect(testcase: string, compiler: Compiler, sourcePath: string): Promise<ExecutionResult> {
|
|
297
|
-
return await this.runTestcase(
|
|
141
|
+
return await this.runTestcase(
|
|
142
|
+
testcase,
|
|
143
|
+
`${testcase}.inp`,
|
|
144
|
+
`${testcase}.cor`,
|
|
145
|
+
compiler,
|
|
146
|
+
toolkitPrefix() + '-' + sourcePath,
|
|
147
|
+
)
|
|
298
148
|
}
|
|
299
149
|
|
|
300
150
|
async runTestcase(
|
|
@@ -307,7 +157,7 @@ export class Maker {
|
|
|
307
157
|
let error = false
|
|
308
158
|
const start = Date.now()
|
|
309
159
|
try {
|
|
310
|
-
await compiler.execute(this.handler, this.directory, sourcePath, input, output)
|
|
160
|
+
await compiler.execute(this.inspector.handler, this.inspector.directory, sourcePath, input, output)
|
|
311
161
|
} catch (e) {
|
|
312
162
|
tui.error(`Execution failed for testcase '${testcase}'`)
|
|
313
163
|
error = true
|
|
@@ -315,37 +165,37 @@ export class Maker {
|
|
|
315
165
|
const end = Date.now()
|
|
316
166
|
const time = end - start
|
|
317
167
|
|
|
318
|
-
if (this.handler.handler === 'graphic') {
|
|
319
|
-
await rename(join(this.directory, 'output.png'), join(this.directory, output))
|
|
168
|
+
if (this.inspector.handler.handler === 'graphic') {
|
|
169
|
+
await rename(join(this.inspector.directory, 'output.png'), join(this.inspector.directory, output))
|
|
320
170
|
}
|
|
321
171
|
|
|
322
|
-
const inputSize = await fileSize(
|
|
323
|
-
const outputSize = await fileSize(
|
|
172
|
+
const inputSize = await fileSize(join(this.inspector.directory, input))
|
|
173
|
+
const outputSize = await fileSize(join(this.inspector.directory, output))
|
|
324
174
|
|
|
325
175
|
return { testcase, error, time, inputSize, outputSize }
|
|
326
176
|
}
|
|
327
177
|
|
|
328
178
|
selectCompiler(): Compiler {
|
|
329
|
-
if (this.handler.compilers === 'RunPython') {
|
|
179
|
+
if (this.inspector.handler.compilers === 'RunPython') {
|
|
330
180
|
return getCompilerById('RunPython')
|
|
331
|
-
} else if (this.handler.compilers === 'RunHaskell') {
|
|
181
|
+
} else if (this.inspector.handler.compilers === 'RunHaskell') {
|
|
332
182
|
return getCompilerById('RunHaskell')
|
|
333
|
-
} else if (this.handler.compilers === 'RunClojure') {
|
|
183
|
+
} else if (this.inspector.handler.compilers === 'RunClojure') {
|
|
334
184
|
return getCompilerById('RunClojure')
|
|
335
185
|
} else {
|
|
336
|
-
const extension = this.goldenSolution!.split('.').pop()!
|
|
186
|
+
const extension = this.inspector.goldenSolution!.split('.').pop()!
|
|
337
187
|
return getCompilerByExtension(extension)
|
|
338
188
|
}
|
|
339
189
|
}
|
|
340
190
|
|
|
341
191
|
public async makeCorrects() {
|
|
342
192
|
const compiler = this.selectCompiler()
|
|
343
|
-
await tui.section(`Making
|
|
193
|
+
await tui.section(`Making correct outputs with golden solution`, async () => {
|
|
344
194
|
await tui.section(`Executing testcases using ${compiler.name()}`, async () => {
|
|
345
195
|
const results: ExecutionResult[] = []
|
|
346
196
|
|
|
347
|
-
for (const testcase of this.testcases) {
|
|
348
|
-
results.push(await this.makeCorrect(testcase, compiler, this.goldenSolution!))
|
|
197
|
+
for (const testcase of this.inspector.testcases) {
|
|
198
|
+
results.push(await this.makeCorrect(testcase, compiler, this.inspector.goldenSolution!))
|
|
349
199
|
}
|
|
350
200
|
|
|
351
201
|
console.log()
|
|
@@ -374,44 +224,44 @@ export class Maker {
|
|
|
374
224
|
|
|
375
225
|
public async makePdfs() {
|
|
376
226
|
await tui.section('Making PDF statements', async () => {
|
|
377
|
-
const tmpDirBase =
|
|
227
|
+
const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-pdf', humanid())
|
|
228
|
+
await mkdir(tmpDirBase, { recursive: true })
|
|
378
229
|
await tui.section('Creating working directory', async () => {
|
|
379
230
|
await nothing()
|
|
380
231
|
tui.directory(tmpDirBase)
|
|
381
232
|
})
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
233
|
+
for (const language of this.inspector.languages) {
|
|
234
|
+
if (
|
|
235
|
+
this.inspector.structure === 'multi' ||
|
|
236
|
+
(this.inspector.structure === 'single' && language === this.inspector.language)
|
|
237
|
+
) {
|
|
238
|
+
await tui.section(`Making PDF statement for ${languageNames[language]}`, async () => {
|
|
239
|
+
await this.makePdf(tmpDirBase, language)
|
|
240
|
+
})
|
|
389
241
|
}
|
|
390
|
-
} finally {
|
|
391
|
-
// do not clean up because of hyperlinks
|
|
392
242
|
}
|
|
393
243
|
})
|
|
394
244
|
}
|
|
395
245
|
|
|
396
246
|
async makeSamples(tmpDir: string, language: string): Promise<[string, string]> {
|
|
397
|
-
const graphic = this.handler.handler === 'graphic'
|
|
398
|
-
const
|
|
399
|
-
const
|
|
247
|
+
const graphic = this.inspector.handler.handler === 'graphic'
|
|
248
|
+
const samples1col: string[] = []
|
|
249
|
+
const samples2col: string[] = []
|
|
400
250
|
let index = 1
|
|
401
|
-
for (const testcase of this.testcases) {
|
|
251
|
+
for (const testcase of this.inspector.testcases) {
|
|
402
252
|
if (testcase.startsWith('sample')) {
|
|
403
253
|
let size = ''
|
|
404
254
|
if (graphic) {
|
|
405
|
-
await cp(join(this.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
|
|
255
|
+
await cp(join(this.inspector.directory, `${testcase}.cor`), join(tmpDir, `${testcase}.cor.png`))
|
|
406
256
|
const dimensions = await imageSizeFromFile(join(tmpDir, `${testcase}.cor.png`))
|
|
407
257
|
size = `(${dimensions.width}$\\times$${dimensions.height})`
|
|
408
258
|
}
|
|
409
|
-
|
|
410
|
-
|
|
259
|
+
samples1col.push(`\n\\SampleOneColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
260
|
+
samples2col.push(`\n\\SampleTwoColInputOutput[${size}]{${testcase}}{${index}}\n`)
|
|
411
261
|
index++
|
|
412
262
|
}
|
|
413
263
|
}
|
|
414
|
-
return [
|
|
264
|
+
return [samples1col.join('\n'), samples2col.join('\n')]
|
|
415
265
|
}
|
|
416
266
|
|
|
417
267
|
async makePdf(tmpDirBase: string, language: string) {
|
|
@@ -420,9 +270,9 @@ export class Maker {
|
|
|
420
270
|
|
|
421
271
|
const date = new Date().toISOString().substring(0, 10)
|
|
422
272
|
const year = new Date().getFullYear()
|
|
423
|
-
const author = this.
|
|
424
|
-
const authorEmail = this.
|
|
425
|
-
const translator = this.
|
|
273
|
+
const author = this.inspector.problemLangYmls[language].author || 'Unknown'
|
|
274
|
+
const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
|
|
275
|
+
const translator = this.inspector.problemLangYmls[language].translator || ''
|
|
426
276
|
|
|
427
277
|
const [samples1c, samples2c] = await this.makeSamples(tmpDir, language)
|
|
428
278
|
|
|
@@ -455,7 +305,20 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
455
305
|
`
|
|
456
306
|
|
|
457
307
|
// copy files to tmpDir
|
|
458
|
-
|
|
308
|
+
// TODO: only copy needed files
|
|
309
|
+
for await (const entry of glob('*', { cwd: this.inspector.directory })) {
|
|
310
|
+
if (
|
|
311
|
+
entry.startsWith(toolkitPrefix()) ||
|
|
312
|
+
entry.endsWith('.exe') ||
|
|
313
|
+
entry.endsWith('.html') ||
|
|
314
|
+
entry.endsWith('.md') ||
|
|
315
|
+
entry.endsWith('.txt') ||
|
|
316
|
+
(await isDirectoryInDir(this.inspector.directory, entry))
|
|
317
|
+
) {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
|
|
321
|
+
}
|
|
459
322
|
|
|
460
323
|
// write root.tex
|
|
461
324
|
await writeText(join(tmpDir, 'root.tex'), root)
|
|
@@ -480,9 +343,9 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
480
343
|
// stdout: 'inherit',
|
|
481
344
|
cwd: tmpDir,
|
|
482
345
|
})`xelatex -interaction=nonstopmode -file-line-error root.tex`
|
|
483
|
-
await cp(join(tmpDir, 'root.pdf'), join(this.directory, `problem.${language}.pdf`))
|
|
346
|
+
await cp(join(tmpDir, 'root.pdf'), join(this.inspector.directory, `problem.${language}.pdf`))
|
|
484
347
|
tui.success(
|
|
485
|
-
`Generated ${tui.hyperlink(this.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
|
|
348
|
+
`Generated ${tui.hyperlink(this.inspector.directory, `problem.${language}.pdf`)} see ${tui.hyperlink(tmpDir, `root.log`)}`,
|
|
486
349
|
)
|
|
487
350
|
} catch (e) {
|
|
488
351
|
tui.error(`Error in LaTeX: ${tui.hyperlink(tmpDir, `root.log`)}`)
|
|
@@ -504,15 +367,19 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
504
367
|
}
|
|
505
368
|
|
|
506
369
|
public async makeTexts() {
|
|
507
|
-
await tui.section('Making text statements', async () => {
|
|
508
|
-
const tmpDirBase =
|
|
370
|
+
await tui.section('Making text statements (.{md,txt,html})', async () => {
|
|
371
|
+
const tmpDirBase = join(this.inspector.directory, toolkitPrefix() + '-text', humanid())
|
|
372
|
+
await mkdir(tmpDirBase, { recursive: true })
|
|
509
373
|
await tui.section('Creating working directory', async () => {
|
|
510
374
|
await nothing()
|
|
511
375
|
tui.directory(tmpDirBase)
|
|
512
376
|
})
|
|
513
377
|
try {
|
|
514
|
-
for (const language of this.languages) {
|
|
515
|
-
if (
|
|
378
|
+
for (const language of this.inspector.languages) {
|
|
379
|
+
if (
|
|
380
|
+
this.inspector.structure === 'multi' ||
|
|
381
|
+
(this.inspector.structure === 'single' && language === this.inspector.language)
|
|
382
|
+
) {
|
|
516
383
|
await tui.section(`Making text statements for ${languageNames[language]}`, async () => {
|
|
517
384
|
await this.makeText(tmpDirBase, language)
|
|
518
385
|
})
|
|
@@ -530,9 +397,9 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
530
397
|
|
|
531
398
|
const date = new Date().toISOString().substring(0, 10)
|
|
532
399
|
const year = new Date().getFullYear()
|
|
533
|
-
const author = this.
|
|
534
|
-
const authorEmail = this.
|
|
535
|
-
const translator = this.
|
|
400
|
+
const author = this.inspector.problemLangYmls[language].author || 'Unknown'
|
|
401
|
+
const authorEmail = this.inspector.problemLangYmls[language].author_email || 'unknown email'
|
|
402
|
+
const translator = this.inspector.problemLangYmls[language].translator || ''
|
|
536
403
|
|
|
537
404
|
const root = `
|
|
538
405
|
|
|
@@ -570,7 +437,21 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
570
437
|
`
|
|
571
438
|
|
|
572
439
|
// copy files to tmpDir
|
|
573
|
-
|
|
440
|
+
// TODO: only copy needed files
|
|
441
|
+
for await (const entry of glob('*', { cwd: this.inspector.directory })) {
|
|
442
|
+
if (
|
|
443
|
+
entry.startsWith(toolkitPrefix()) ||
|
|
444
|
+
entry.endsWith('.pdf') ||
|
|
445
|
+
entry.endsWith('.exe') ||
|
|
446
|
+
entry.endsWith('.html') ||
|
|
447
|
+
entry.endsWith('.md') ||
|
|
448
|
+
entry.endsWith('.txt') ||
|
|
449
|
+
(await isDirectoryInDir(this.inspector.directory, entry))
|
|
450
|
+
) {
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
await cp(join(this.inspector.directory, entry), join(tmpDir, entry))
|
|
454
|
+
}
|
|
574
455
|
|
|
575
456
|
// write root.tex
|
|
576
457
|
await writeText(join(tmpDir, 'root.tex'), root)
|
|
@@ -604,8 +485,8 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
604
485
|
try {
|
|
605
486
|
tui.command('pandoc --quiet root.tex --to plain --output root.txt')
|
|
606
487
|
await execa({ cwd: tmpDir })`pandoc --quiet root.tex --to plain --output root.txt`
|
|
607
|
-
await cp(join(tmpDir, 'root.txt'), join(this.directory, `problem.${language}.txt`))
|
|
608
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.txt`))
|
|
488
|
+
await cp(join(tmpDir, 'root.txt'), join(this.inspector.directory, `problem.${language}.txt`))
|
|
489
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.txt`))
|
|
609
490
|
} catch (e) {
|
|
610
491
|
console.error('pandoc error', e)
|
|
611
492
|
}
|
|
@@ -618,35 +499,64 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
618
499
|
await execa({
|
|
619
500
|
cwd: tmpDir,
|
|
620
501
|
})`pandoc --quiet root.tex --to markdown --to markdown-header_attributes --lua-filter=fixCodeBlocks.lua --output root.md`
|
|
621
|
-
await cp(join(tmpDir, 'root.md'), join(this.directory, `problem.${language}.md`))
|
|
622
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.md`))
|
|
502
|
+
await cp(join(tmpDir, 'root.md'), join(this.inspector.directory, `problem.${language}.md`))
|
|
503
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.md`))
|
|
623
504
|
} catch (e) {
|
|
624
505
|
console.error('pandoc error', e)
|
|
625
506
|
}
|
|
626
507
|
|
|
627
|
-
//
|
|
508
|
+
// html
|
|
628
509
|
try {
|
|
629
510
|
tui.command('pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html')
|
|
630
511
|
await execa({
|
|
631
512
|
cwd: tmpDir,
|
|
632
513
|
})`pandoc --quiet root.tex --to html --mathml --embed-resources --output root.html`
|
|
633
|
-
await cp(join(tmpDir, 'root.html'), join(this.directory, `problem.${language}.html`))
|
|
634
|
-
tui.success('Generated ' + tui.hyperlink(this.directory, `problem.${language}.html`))
|
|
514
|
+
await cp(join(tmpDir, 'root.html'), join(this.inspector.directory, `problem.${language}.html`))
|
|
515
|
+
tui.success('Generated ' + tui.hyperlink(this.inspector.directory, `problem.${language}.html`))
|
|
635
516
|
} catch (e) {
|
|
636
517
|
console.error('pandoc error', e)
|
|
637
518
|
}
|
|
638
519
|
}
|
|
639
520
|
|
|
640
|
-
public async
|
|
641
|
-
await tui.section(`
|
|
521
|
+
public async checkCandidate(program: string) {
|
|
522
|
+
await tui.section(`Checking ${program}`, async () => {
|
|
642
523
|
const extension = program.split('.').pop()!
|
|
643
524
|
const compiler = getCompilerByExtension(extension)
|
|
525
|
+
const newProgram = `${toolkitPrefix()}-${program}`
|
|
526
|
+
|
|
527
|
+
await tui.section(
|
|
528
|
+
`Copying ${tui.hyperlink(this.inspector.directory, program)} to ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
529
|
+
async () => {
|
|
530
|
+
await cp(join(this.inspector.directory, program), join(this.inspector.directory, newProgram))
|
|
531
|
+
},
|
|
532
|
+
)
|
|
644
533
|
|
|
645
534
|
await tui.section(
|
|
646
|
-
`Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.directory,
|
|
535
|
+
`Using compiler ${compiler.name()} to compile ${tui.hyperlink(this.inspector.directory, newProgram)}`,
|
|
647
536
|
async () => {
|
|
648
537
|
try {
|
|
649
|
-
|
|
538
|
+
let outputPath: string
|
|
539
|
+
if (this.inspector.handler.source_modifier === 'none') {
|
|
540
|
+
outputPath = await compiler.compileNormal(
|
|
541
|
+
this.inspector.handler,
|
|
542
|
+
this.inspector.directory,
|
|
543
|
+
newProgram,
|
|
544
|
+
)
|
|
545
|
+
} else if (this.inspector.handler.source_modifier === 'no_main') {
|
|
546
|
+
outputPath = await compiler.compileWithMain(
|
|
547
|
+
this.inspector.handler,
|
|
548
|
+
this.inspector.directory,
|
|
549
|
+
newProgram,
|
|
550
|
+
)
|
|
551
|
+
} else {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`Unknown source modifier: ${this.inspector.handler.source_modifier as string}`,
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
if (!(await exists(join(this.inspector.directory, outputPath)))) {
|
|
557
|
+
throw new Error(`Compilation failed for ${newProgram}`)
|
|
558
|
+
}
|
|
559
|
+
tui.success(`Compiled ${newProgram} to ${outputPath}`)
|
|
650
560
|
} catch (error) {
|
|
651
561
|
throw new Error(`Compilation failed`)
|
|
652
562
|
}
|
|
@@ -655,14 +565,14 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
655
565
|
|
|
656
566
|
const results: ExecutionResult[] = []
|
|
657
567
|
await tui.section('Executing testcases', async () => {
|
|
658
|
-
for (const testcase of this.testcases) {
|
|
568
|
+
for (const testcase of this.inspector.testcases) {
|
|
659
569
|
results.push(
|
|
660
570
|
await this.runTestcase(
|
|
661
571
|
testcase,
|
|
662
572
|
`${testcase}.inp`,
|
|
663
|
-
`${testcase}.${extension}.out`,
|
|
573
|
+
`${toolkitPrefix()}-${testcase}.${extension}.out`,
|
|
664
574
|
compiler,
|
|
665
|
-
|
|
575
|
+
newProgram,
|
|
666
576
|
),
|
|
667
577
|
)
|
|
668
578
|
}
|
|
@@ -674,8 +584,11 @@ Draft generated with \\textbf{new-jutge-toolkit}.
|
|
|
674
584
|
const status = result.error
|
|
675
585
|
? 'EE'
|
|
676
586
|
: (await filesAreEqual(
|
|
677
|
-
|
|
678
|
-
|
|
587
|
+
join(this.inspector.directory, `${result.testcase}.cor`),
|
|
588
|
+
join(
|
|
589
|
+
this.inspector.directory,
|
|
590
|
+
`${toolkitPrefix()}-${result.testcase}.${extension}.out`,
|
|
591
|
+
),
|
|
679
592
|
))
|
|
680
593
|
? 'OK'
|
|
681
594
|
: 'WA'
|