Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ This action will output the following variables:
- `version`: Version of V that was used.
- `architecture`: Architecture that was used to install V.

## Troubleshooting

### Build failures from source (`unknown function: ...`)

When installing from the default branch, the action downloads the V source and
builds it using the prebuilt C translation (`vc`). If the V source references
functions that are not yet available in the prebuilt `vc` (a transient V
upstream issue), the build can fail with errors such as:

```
error: unknown function: help.print_and_exit
```

This is usually a temporary V infrastructure issue. Workarounds:

- Set `stable: true` to install the latest stable release instead of the
default branch.
- Specify a concrete `version` (for example `weekly.2024.01` or a tag/SHA).

The action retries the build once and, if it still fails, reports the original
build output together with these suggestions.

## Contributors

<a href="https://github.com/vlang/setup-v/contributors">
Expand Down
69 changes: 56 additions & 13 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions src/installer.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import {describe, expect, test, vi, beforeEach, afterEach} from 'vitest'
import * as cp from 'child_process'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import {
cleanInstallation,
getInstallDir,
getVExecutable,
getVlang,
getWindowsBuildCommand,
resolveVersionRef
} from './installer'
import * as githubApiHelper from './github-api-helper'

interface CommandError extends Error {
stdout?: Buffer
stderr?: Buffer
}

vi.mock('child_process', async importOriginal => {
const actual = await importOriginal<typeof import('child_process')>()
return {...actual, execSync: vi.fn()}
})

describe('getVExecutable', () => {
const platformSpy = vi.spyOn(process, 'platform', 'get')

Expand Down Expand Up @@ -270,3 +282,70 @@ describe('cleanInstallation', () => {
}).not.toThrow()
})
})

describe('getVlang build failure handling', () => {
let tempHome = ''
let originalHome: string | undefined

beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-v-home-'))
originalHome = process.env.HOME
process.env.HOME = tempHome
vi.spyOn(githubApiHelper, 'downloadRepository').mockResolvedValue(undefined)
})

afterEach(() => {
vi.restoreAllMocks()
vi.mocked(cp.execSync).mockReset()
if (originalHome === undefined) {
delete process.env.HOME
} else {
process.env.HOME = originalHome
}
if (tempHome) {
fs.rmSync(tempHome, {recursive: true, force: true})
}
})

test('throws a user-friendly error when the build fails', async () => {
const buildError = new Error('make failed') as CommandError
buildError.stdout = Buffer.from('compiling v.c...')
buildError.stderr = Buffer.from(
'error: unknown function: help.print_and_exit'
)
vi.mocked(cp.execSync).mockImplementation(() => {
throw buildError
})

await expect(
getVlang({
authToken: 'token',
version: '',
checkLatest: false,
stable: false
})
).rejects.toThrow(
/Failed to build V from source[\s\S]*help\.print_and_exit[\s\S]*transient[\s\S]*stable: true/
)
})

test('retries the build once and succeeds on the second attempt', async () => {
const buildError = new Error('make failed') as CommandError
buildError.stderr = Buffer.from('error: transient hiccup')
vi.mocked(cp.execSync)
.mockImplementationOnce(() => {
throw buildError
})
.mockImplementationOnce(() => Buffer.from('build succeeded'))

const installDir = await getVlang({
authToken: 'token',
version: '',
checkLatest: false,
stable: false
})

expect(installDir).toBe(getInstallDir())
expect(vi.mocked(cp.execSync)).toHaveBeenCalledTimes(2)
})
})
78 changes: 65 additions & 13 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,17 @@ export async function getVlang({
)

if (!fs.existsSync(vBinPath)) {
buildV(installDir)
try {
buildV(installDir)
} catch (error) {
const firstError = error instanceof Error ? error.message : String(error)
core.warning(`Initial V build failed, retrying once...\n${firstError}`)
try {
buildV(installDir)
} catch (retryError) {
throw new Error(buildFailureMessage(retryError))
}
}
}

if (clean) {
Expand All @@ -151,22 +161,28 @@ export function getWindowsBuildCommand(
throw new Error(`No Windows build script found in ${installDir}`)
}

interface CommandError extends Error {
stdout?: Buffer
stderr?: Buffer
}

function buildV(installDir: string): void {
let command: string
let shell: string | undefined

if (process.platform === 'win32') {
// GHA Windows runners ship MSVC (Visual Studio Build Tools), which
// provides the POSIX-compatible headers V needs to bootstrap. MinGW
// (-gcc) lacks sys/mman.h, termios.h and pthread types, so try MSVC
// first and fall back to GCC only if the MSVC build fails.
try {
const msvcCommand = getWindowsBuildCommand(installDir, false)
core.info(`Running ${msvcCommand} (MSVC)...`)
// eslint-disable-next-line no-console
console.log(
execSync(msvcCommand, {
runBuildCommand(msvcCommand, {
cwd: installDir,
shell: process.env.ComSpec ?? 'cmd.exe',
stdio: 'pipe'
}).toString()
shell: process.env.ComSpec ?? 'cmd.exe'
})
)
} catch (msvcError) {
const msvcMessage =
Expand All @@ -175,22 +191,58 @@ function buildV(installDir: string): void {
`MSVC build failed (${msvcMessage}), falling back to GCC (MinGW)...`
)
const gccCommand = getWindowsBuildCommand(installDir, true)
core.info(`Running ${gccCommand} (GCC)...`)
// eslint-disable-next-line no-console
console.log(
execSync(gccCommand, {
runBuildCommand(gccCommand, {
cwd: installDir,
shell: process.env.ComSpec ?? 'cmd.exe',
stdio: 'pipe'
}).toString()
shell: process.env.ComSpec ?? 'cmd.exe'
})
)
}
return
} else {
command = 'make'
}

core.info('Running make...')
// eslint-disable-next-line no-console
console.log(execSync('make', {cwd: installDir, stdio: 'pipe'}).toString())
console.log(runBuildCommand(command, {cwd: installDir, shell}))
}

function runBuildCommand(
command: string,
options: {cwd: string; shell?: string}
): string {
core.info(`Running ${command}...`)
try {
return execSync(command, {
cwd: options.cwd,
shell: options.shell,
stdio: 'pipe'
}).toString()
} catch (error) {
const cmdError = error as CommandError
const stderrText = cmdError.stderr?.toString() ?? ''
const stdoutText = cmdError.stdout?.toString() ?? ''
const parts = [`Command '${command}' failed in ${options.cwd}`]
if (stderrText) {
parts.push(`stderr:\n${stderrText}`)
}
if (stdoutText) {
parts.push(`stdout:\n${stdoutText}`)
}
throw new Error(parts.join('\n'))
}
}

function buildFailureMessage(error: unknown): string {
const buildError = error instanceof Error ? error.message : String(error)
return (
`Failed to build V from source.\n\n` +
`Build error:\n${buildError}\n\n` +
`This may be a transient issue with the V upstream bootstrap. ` +
`Try using \`stable: true\` to install the latest stable release, ` +
`or specify a specific version with the \`version\` input.`
)
}

export function cleanInstallation(installDir: string): void {
Expand Down
Loading