-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
516 lines (468 loc) · 22.1 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
'use strict';
const fs = require('fs')
const os = require('os')
const path = require('path')
const core = require('@actions/core')
const marked = require('marked')
const { execSync } = require('child_process')
const {context, GitHub} = require('@actions/github')
const startedDatetime = new Date()
const tripleBackticks = "```"
const gitTempPath = `${ process.cwd() }/Nim`
const temporaryFile = `${ process.cwd() }/temp.nim`
const temporaryFile2 = `${ process.cwd() }/dumper.nim`
const temporaryFileAsm = `${ process.cwd() }/@mtemp.nim.c`
const temporaryOutFile = temporaryFile.replace(".nim", "")
const extraFlags = ` -d:nimDebug -d:nimDebugDlOpen -d:ssl -d:nimDisableCertificateValidation --forceBuild:on --colors:off --verbosity:0 --hints:off --lineTrace:off --nimcache:${ process.cwd() } --out:${temporaryOutFile} ${temporaryFile}`
const nimFinalVersions = ["devel", "stable", "2.0.10", "2.0.0", "1.6.20", "1.4.8", "1.2.18", "1.0.10"]
const choosenimNoAnal = {env: {...process.env, CHOOSENIM_NO_ANALYTICS: "1", SOURCE_DATE_EPOCH: Math.floor(Date.now() / 1000).toString()}} // SOURCE_DATE_EPOCH is same in all runs.
const valgrindLeakChck = {env: {...process.env, VALGRIND_OPTS: "--quiet --tool=memcheck --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=all --undef-value-errors=yes --track-origins=yes --show-error-list=yes --keep-debuginfo=yes --show-emwarns=yes --demangle=yes --smc-check=none --num-callers=9 --max-threads=9"}}
let nimFileCounter = 0
function cfg(key) {
console.assert(typeof key === "string", `key must be string, but got ${ typeof key }`)
const result = core.getInput(key, {required: true}).trim()
console.assert(typeof result === "string", `result must be string, but got ${ typeof result }`)
return result;
};
function formatDuration(seconds) {
if (typeof seconds === "string") {
seconds = parseInt(seconds, 10)
}
console.assert(typeof seconds === "number", `seconds must be number, but got ${ typeof seconds }`)
let result = "now"
if (!isNaN(seconds) && seconds > 0) {
const hours = Math.floor(((seconds % 31536000) % 86400) / 3600);
const minutes = Math.floor(((seconds % 31536000) % 86400) % 60);
const second = (((seconds % 31536000) % 86400) % 3600) % 0;
const y = (hours > 0) ? hours + " hs" : "";
const z = (minutes > 0) ? minutes + " mins" : "";
const u = (second > 0) ? second + " secs" : "";
result = y + z + u
}
console.assert(typeof result === "string", `result must be string, but got ${ typeof result }`)
return result
}
function formatSizeUnits(bytes) {
console.assert(typeof bytes === "number", `bytes must be number, but got ${ typeof bytes }`)
const bites = ` (${ bytes.toLocaleString() } bytes)`
if (bytes >= 1073741824) { bytes = (bytes / 1073741824).toFixed(2) + " Gb"; }
else if (bytes >= 1048576) { bytes = (bytes / 1048576).toFixed(2) + " Mb"; }
else if (bytes >= 1024) { bytes = (bytes / 1024).toFixed(2) + " Kb"; }
else if (bytes >= 1) { bytes = bytes + " bytes"; }
else { bytes = "0"; }
return bytes + bites;
}
function getFilesizeInBytes(filename) {
console.assert(typeof filename === "string", `filename must be string, but got ${ typeof filename }`)
let result = (fs.existsSync(filename)) ? fs.statSync(filename).size : 0
console.assert(typeof result === "number", `result must be number, but got ${ typeof filename }`)
return result
}
function checkAuthorAssociation() {
const authorPerm = context.payload.comment.author_association.trim().toLowerCase()
let result = (authorPerm === "owner" || authorPerm === "collaborator" || authorPerm === "member" || context.payload.comment.user.login.toLowerCase() === "juancarlospaco")
console.assert(typeof result === "boolean", `result must be boolean, but got ${ typeof result }`)
return result
};
function hasArc(cmd) {
console.assert(typeof cmd === "string", `cmd must be string, but got ${ typeof cmd }`)
const s = cmd.trim().toLowerCase()
return (s.includes("--gc:arc") || s.includes("--gc:orc") || s.includes("--gc:atomicarc") || s.includes("--mm:arc") || s.includes("--mm:orc") || s.includes("--mm:atomicarc"))
}
function hasMalloc(cmd) {
console.assert(typeof cmd === "string", `cmd must be string, but got ${ typeof cmd }`)
const s = cmd.trim().toLowerCase()
return (s.includes("-d:usemalloc") || s.includes("--define:usemalloc"))
}
function semverParser(str) {
let result = "0.0.0"
const match = str.split("\n")[0].match(/\b(\d+\.\d+(\.\d+)?)\b/)
if (match) {
result = match[1]
}
return result
}
function versionInfos() {
return [
semverParser(execSync("gcc --version").toString()),
semverParser(execSync("clang --version").toString()),
semverParser(execSync("node --version").toString()),
]
}
async function addReaction(githubClient, reaction) {
console.assert(typeof reaction === "string", `reaction must be string, but got ${ typeof reaction }`)
return (await githubClient.reactions.createForIssueComment({
comment_id: context.payload.comment.id,
content : reaction.trim().toLowerCase(),
owner : context.repo.owner,
repo : context.repo.repo,
}) !== undefined)
};
async function addIssueComment(githubClient, issueCommentBody) {
console.assert(typeof issueCommentBody === "string", `issueCommentBody must be string, but got ${ typeof issueCommentBody }`)
console.log(`BODY LEN = ${ issueCommentBody.length } (${ 65536 - issueCommentBody.length } Chars left)`)
return (await githubClient.issues.createComment({
issue_number: context.issue.number,
owner : context.repo.owner,
repo : context.repo.repo,
body : issueCommentBody, // GitHub max body len .substring(65536)
}) !== undefined)
};
function parseGithubComment(comment) {
console.assert(typeof comment === "string", `comment must be string, but got ${ typeof comment }`)
const tokens = marked.Lexer.lex(comment)
const allowedFileExtensions = ["c", "cpp", "c++", "h", "hpp", "js", "json", "txt"]
let result = ""
for (const token of tokens) {
if (token.type === 'code' && token.text.length > 0 && token.lang !== undefined) {
if (token.lang === 'nim') {
if (nimFileCounter > 0) {
const xtraFile = temporaryFile.replace(".nim", `${ nimFileCounter }.nim`)
if (!fs.existsSync(xtraFile)) {
fs.writeFileSync(xtraFile, token.text.trim())
fs.chmodSync(xtraFile, "444")
}
} else {
nimFileCounter += 1
result = token.text.trim()
result = result.split('\n').filter(line => line.trim() !== '').join('\n')
}
} else if (allowedFileExtensions.includes(token.lang)) {
const xtraFile = `${ process.cwd() }/temp.${token.lang}`
if (!fs.existsSync(xtraFile)) {
fs.writeFileSync(xtraFile, token.text.trim())
fs.chmodSync(xtraFile, "444")
}
} else if (token.lang === 'cfg' || token.lang === 'ini') {
const xtraFile = `${ temporaryFile }.cfg`
if (!fs.existsSync(xtraFile)) {
fs.writeFileSync(xtraFile, token.text.trim())
fs.chmodSync(xtraFile, "444")
}
}
}
}
return result
}
function parseGithubCommand(comment) {
console.assert(typeof comment === "string", `comment must be string, but got ${ typeof comment }`)
let result = comment.trim().split("\n")[0].trim()
if (typeof result === "string" && result.length > 0) {
// Basic checkings
const bannedSeps = [";", ";;", "&&", "||"]
if (bannedSeps.some(s => result.includes(s))) {
core.setFailed(`Github comment must not contain ${bannedSeps}`)
}
if (!result.startsWith("!nim c") && !result.startsWith("!nim cpp") && !result.startsWith("!nim js")) {
core.setFailed("Github comment must start with '!nim c' or '!nim cpp' or '!nim js'")
}
// Extra arguments based on different targets
if (result.startsWith("!nim js")) {
result = result + " -d:nodejs -d:nimExperimentalAsyncjsThen -d:nimExperimentalJsfetch "
}
const useArc = hasArc(result)
const useValgrind = useArc && hasMalloc(result) && process.env.RUNNER_OS !== "Windows"
if (useArc) {
result = result + " -d:nimArcDebug -d:nimArcIds "
}
if (useValgrind) {
console.log(installValgrind())
result = result + " -d:nimAllocPagesViaMalloc -d:useSysAssert -d:useGcAssert -d:nimLeakDetector --debugger:native --debuginfo:on "
} else {
result = result + " --run "
}
result = result + extraFlags
if (useValgrind) {
result = result + ` && valgrind ${temporaryOutFile}`
}
result = result.substring(1) // Remove the leading "!"
console.assert(typeof result === "string", `result must be string, but got ${ typeof result }`)
return result.trim()
} else {
console.warn('Failed to parse github command')
return ""
}
};
function executeChoosenim(semver) {
console.assert(typeof semver === "string", `semver must be string, but got ${ typeof semver }`)
if (typeof semver === "string" && semver.length > 0) {
for (let i = 0; i < 3; i++) {
try {
const result = execSync(`choosenim --noColor --skipClean --yes update "${semver}"`, choosenimNoAnal).toString().trim()
if (result) {
return result
}
} catch (error) {
console.warn(error)
if (i === 2) {
console.warn('choosenim failed >3 times, giving up...')
return ""
}
}
}
} else {
console.warn('choosenim received an empty string semver')
return ""
}
}
function executeChoosenimRemove(semver) {
console.assert(typeof semver === "string", `semver must be string, but got ${ typeof semver }`)
// Clean out already checked Nim versions to not fill up the disk, leave stable and devel alone.
if (typeof semver === "string" && semver.length > 0 && semver !== "devel" && semver !== "stable") {
try {
// Can not remove the Nim version currently active, so switch to devel.
console.log(execSync("choosenim --noColor --yes devel", choosenimNoAnal).toString())
return execSync(`choosenim --noColor --yes remove "${semver}"`, choosenimNoAnal).toString().trim()
} catch (error) {
console.warn(error)
return ""
}
}
return ""
}
function executeNim(cmd, codes) {
console.assert(typeof cmd === "string", `cmd must be string, but got ${ typeof cmd }`)
console.assert(typeof codes === "string", `codes must be string, but got ${ typeof codes }`)
if (typeof cmd === "string" && cmd.length > 0 && typeof codes === "string" && codes.length > 0) {
if (!fs.existsSync(temporaryFile)) {
fs.writeFileSync(temporaryFile, codes)
fs.chmodSync(temporaryFile, "444")
}
console.log("COMMAND:\t", cmd)
try {
return [true, execSync(cmd, valgrindLeakChck).toString().trim()]
} catch (error) {
console.warn(error)
return [false, `${error}`]
}
} else {
console.warn('executeNim received an empty string code')
return [false, ""]
}
}
function installValgrind() {
try {
return execSync((process.env.RUNNER_OS === "Linux" ? "sudo apt-get -yq update && sudo apt-get install --no-install-recommends -yq valgrind" : "brew update && brew install valgrind")).toString().trim()
} catch (error) {
console.warn(error)
return ""
}
}
function gitInit() {
// Git clone Nim repo and checkout devel
if (!fs.existsSync(gitTempPath)) {
console.log(execSync(`git clone https://github.com/nim-lang/Nim.git ${gitTempPath}`).toString())
console.log(execSync("git config --global advice.detachedHead false && git checkout devel", {cwd: gitTempPath}).toString())
}
}
function gitMetadata(commit) {
// Git get useful metadata from current commit
console.assert(typeof commit === "string", `commit must be string, but got ${ typeof commit }`)
if (typeof commit === "string" && commit.length > 0) {
console.log(execSync(`git checkout ${ commit.replace("#", "") }`, {cwd: gitTempPath}).toString())
const user = execSync("git log -1 --pretty=format:'%an'", {cwd: gitTempPath}).toString().trim().toLowerCase()
const mesage = execSync("git log -1 --pretty='%B'", {cwd: gitTempPath}).toString().trim().replace(tripleBackticks, ' ').substring(1024)
const date = execSync("git log -1 --pretty=format:'%ai'", {cwd: gitTempPath}).toString().trim().toLowerCase()
const files = execSync("git diff-tree --no-commit-id --name-only -r HEAD", {cwd: gitTempPath}).toString().trim().substring(1024)
return [user, mesage, date, files]
} else {
console.warn('gitMetadata received an empty string commit')
return ["", "", "", ""]
}
}
function gitCommitsBetween(commitOld, commitNew) {
// Git get all commit short hash between commitOld and commitNew
console.assert(typeof commitOld === "string", `commitOld must be string, but got ${ typeof commitOld }`)
console.assert(typeof commitNew === "string", `commitNew must be string, but got ${ typeof commitNew }`)
if (typeof commitOld === "string" && commitOld.length > 0 && typeof commitNew === "string" && commitNew.length > 0) {
let result = execSync(`git log --pretty=format:'#%h' ${commitOld}..${commitNew}`, {cwd: gitTempPath}).toString().trim().toLowerCase()
console.assert(typeof result === "string", `result must be string, but got ${ typeof result }`)
result = result.split('\n').filter(line => line.trim() !== '')
return result
} else {
console.warn('gitCommitsBetween received an empty string commit')
return []
}
}
function gitCommitForVersion(semver) {
// Get Git commit for an specific Nim semver
console.assert(typeof semver === "string", `semver must be string, but got ${ typeof semver }`)
let result = null
if (typeof semver === "string" && semver.length > 0) {
semver = semver.trim().toLowerCase()
if (semver === "2.2.0") {
result = "78983f1"
} else if (semver === "2.0.10") {
result = "e941ee1"
} else if (semver === "2.0.0") {
result = "a488067"
} else if (semver === "1.6.20") {
result = "19fdbfc"
} else if (semver === "1.6.0") {
result = "727c637"
} else if (semver === "1.4.8") {
result = "44e653a"
} else if (semver === "1.4.0") {
result = "018ae96"
} else if (semver === "1.2.18") {
result = "8a5c8d3"
} else if (semver === "1.2.0") {
result = "7e83adf"
} else if (semver === "1.0.10") {
result = "0ca09f6"
} else if (semver === "1.0.0") {
result = "f7a8fc4"
} else if (semver === "devel" || semver === "stable") {
// For semver === "devel" or semver === "stable" we use choosenim
executeChoosenim(semver) // devel and stable are moving targets.
const nimversion = execSync("nim --version").toString().trim().toLowerCase().split('\n').filter(line => (typeof line === "string" && line.trim() !== ''))
for (const s of nimversion) {
if (s.startsWith("git hash:")) {
result = s.replace("git hash:", "").trim().toLowerCase()
break
}
}
} else {
// For semver == "x.x.x" we use Git
result = execSync(`git checkout "v${semver}" && git rev-parse --short HEAD`, {cwd: gitTempPath}).toString().trim().toLowerCase()
execSync(`git checkout devel`, {cwd: gitTempPath}) // Go back to devel
}
console.assert(typeof result === "string", `result must be string, but got ${ typeof result }`)
return result
} else {
console.warn('gitCommitForVersion received an empty string semver')
return null
}
}
// Only run if this is an "issue_comment" and comment startsWith commentPrefixes.
if (context.payload.comment.body.trim().toLowerCase().startsWith("!nim ") && checkAuthorAssociation()) {
// Check if we have permissions.
const githubClient = new GitHub(cfg('github-token'))
// Add Reaction of "Eyes" as seen.
if (addReaction(githubClient, "eyes")) {
const githubComment = context.payload.comment.body.trim()
const codes = parseGithubComment(githubComment)
const cmd = parseGithubCommand(githubComment)
let fails = null
let works = null
let commitsLen = nimFinalVersions.length
const osEmoji = process.env.RUNNER_OS === "Linux" ? ":penguin:" : process.env.RUNNER_OS === "Windows" ? ":window:" : ":apple:"
let issueCommentStr = `<details><summary>${ osEmoji } ${ process.env.RUNNER_OS } bisect by @${ context.actor } (${ context.payload.comment.author_association.toLowerCase() })</summary>`
// Check the same code agaisnt all versions of Nim from devel to 1.0
for (let semver of nimFinalVersions) {
console.log(executeChoosenim(semver))
const started = new Date()
let [isOk, output] = executeNim(cmd, codes)
const finished = new Date()
const thumbsUp = (isOk ? ":+1: OK" : ":-1: FAIL")
// Remember which version works and which version breaks.
if (isOk && works === null) {
works = semver
}
else if (!isOk && fails === null) {
fails = semver
}
// Append to reports.
issueCommentStr += `<details><summary><kbd>${semver}</kbd>\t${thumbsUp}</summary><h3>Output</h3>\n
${ tripleBackticks }
${ output.trim().split('\n').filter(line => line.trim() !== '').join('\n').substring(4098) }
${ tripleBackticks }\n
<b>Filesize</b>\t<code>${ formatSizeUnits(getFilesizeInBytes(temporaryOutFile)) }</code>\t
<b>Duration</b>\t<code>${ formatDuration((((finished - started) % 60000) / 1000)) }</code></ul></details>\n`
// Clean out already checked Nim versions to not fill up the disk.
console.log(executeChoosenimRemove(semver))
}
// This part is about finding the specific commit that breaks
if (fails !== null && works !== null && fails !== works) {
// Get a range of commits between "FAILS..WORKS"
gitInit()
const failsCommit = gitCommitForVersion(fails)
const worksCommit = gitCommitForVersion(works)
if (failsCommit !== null && worksCommit !== null && failsCommit !== worksCommit) {
let commits = gitCommitsBetween(worksCommit, failsCommit)
if (commits.length > 10) {
commitsLen += commits.length
// Split commits in half and check if that commit works or fails,
// then repeat the split there until we got less than 10 commits.
while (commits.length > 10) {
let midIndex = Math.ceil(commits.length / 2)
if (midIndex) {
console.log(executeChoosenim(commits[midIndex]))
let [isOk, output] = executeNim(cmd, codes)
if (isOk) {
// iff its OK then split 0..mid
commits = commits.slice(0, midIndex);
} else {
// else NOT OK then split mid..end
commits = commits.slice(midIndex);
}
// Clean out already checked Nim versions to not fill up the disk.
console.log(executeChoosenimRemove(commits[midIndex]))
}
}
let commitsNear = "\n<ul>"
for (let commit of commits) {
commitsNear += `<li><a href=https://github.com/nim-lang/Nim/commit/${ commit.replace("#", "") } >${ commit }</a>\n`
}
commitsNear += "</ul>\n"
let bugFound = false
let index = 0
for (let commit of commits) {
// Choosenim switch semver
console.log(executeChoosenim(commit))
// Run code
const [isOk, output] = executeNim(cmd, codes)
// if this commit works, then previous commit is the breakingCommit
if (isOk) {
if (!bugFound) {
bugFound = true
}
const breakingCommit = (index > 0) ? commits[index - 1] : commits[index]
const [user, mesage, date, files] = gitMetadata(breakingCommit)
const comit = breakingCommit.replace('"', '')
// Report the breaking commit diagnostics
issueCommentStr += `<details><summary><kbd>${comit}</kbd> :arrow_right: :bug:</summary><h3>Diagnostics</h3>\n
${user} introduced a bug at <code>${date}</code> on commit <a href=https://github.com/nim-lang/Nim/commit/${ comit.replace("#", "") } >${ comit }</a> with message:\n
${ tripleBackticks }
${ mesage }
${ tripleBackticks }
\nThe bug is in the files:\n
${ tripleBackticks }
${files}
${ tripleBackticks }
\nThe bug can be in the commits:\n
${commitsNear}
(Diagnostics sometimes off-by-one).</details>\n`
// Break out of the for
break
}
index++
// Clean out already checked Nim versions to not fill up the disk.
console.log(executeChoosenimRemove(commit))
}
if (!bugFound) {
issueCommentStr += `<details><summary>??? :arrow_right: :bug:</summary><h3>Diagnostics</h3>\n
The commit that introduced the bug can not be found, but the bug is in the commits:
${commitsNear}
(Can not find the commit because Nim can not be re-built commit-by-commit to bisect).\n</details>\n`
}
}
}
else { console.warn(`failsCommit and worksCommit not found, at least 1 working commit and 1 non-working commit are required for Bisect commit-by-commit.\nfailsCommit = ${failsCommit}\tworksCommit = ${worksCommit}`) }
}
else { console.warn(`works and fails not found, at least 1 working commit and 1 non-working commit are required for Bisect commit-by-commit.\nfails = ${fails}\tworks = ${works}`) }
// Report results back as a comment on the issue.
const duration = ((( (new Date()) - startedDatetime) % 60000) / 1000)
const v = versionInfos()
issueCommentStr += `<details><summary>Stats</summary><ul>
<li><b>GCC</b>\t<code>${ v[0] }</code>
<li><b>Clang</b>\t<code>${ v[1] }</code>
<li><b>NodeJS</b>\t<code>${ v[2] }</code>
<li><b>Created</b>\t<code>${ context.payload.comment.created_at }</code>
<li><b>Comments</b>\t<code>${ context.payload.issue.comments }</code>
<li><b>Commands</b>\t<code>${ cmd }</code></ul></details>\n
:robot: Bug found in <code>${ formatDuration(duration) }</code> bisecting <code>${commitsLen}</code> commits at <code>${ Math.round(commitsLen / duration) }</code> commits per second</details>`
addIssueComment(githubClient, issueCommentStr.trim())
}
else { console.warn("githubClient.addReaction failed, repo permissions error?.") }
}