Merge remote-tracking branch 'origin/master' into perf/loom-transform-2

This commit is contained in:
Alexander Medvednikov committed 2026-10-14 02:18:14 +03:00
commit cf850c2eae
61 files changed
+1622 -168

No files matched your search

+3 -1
View File
@@ -69,7 +69,9 @@ its checkpoints, so the following invocation starts from the beginning.
The aggregate `ci` command sets `VTEST_FAIL_FAST=1` and `VJOBS=1`, overriding inherited
values. Test and example-build sessions stop at the first reported failure, with no
other files already running in parallel. `test-cleancode` also stops after a failed
other files already running in parallel. The `self_tests` task retains this single-worker
setting; standalone GitHub Actions self-test tasks still use every available CPU.
`test-cleancode` also stops after a failed
vetting session instead of starting formatting checks. The failed task's checkpoint
is retained. Existing per-test retry and known-flaky-test policies are unchanged.
+4 -3
View File
@@ -65,10 +65,11 @@ fn self_tests() {
// Individual files still fall back to it when the default compiler cannot
// build them.
// Exercise default caching throughout the standard library suite. Use every
// core on the 7 GB runners instead of the one-job memory default.
// core on the 7 GB runners unless the aggregate runner requires fail-fast.
if common.is_github_job {
exec_args(['env', 'VJOBS=' + '${runtime.nr_cpus()}', 'v', '-no-memory-limit', '-silent',
'test-self', 'vlib'])
vjobs := if os.getenv('VTEST_FAIL_FAST') == '1' { '1' } else { '${runtime.nr_cpus()}' }
exec_args(['env', 'VJOBS=' + '${vjobs}', 'v', '-no-memory-limit', '-silent', 'test-self',
'vlib'])
} else {
vjobs := os.getenv_opt('VJOBS') or { '1' }
exec_args(['env', 'VJOBS=' + '${vjobs}', 'v', '-no-memory-limit', '-progress', 'test-self',
+13
View File
@@ -37,6 +37,12 @@ if [ "$1" = doctor ]; then
echo doctor >> direct.log
exit 0
fi
if [ "$1" = -no-memory-limit ]; then
[ "$2" = -silent ] && [ "$3" = test-self ] && [ "$4" = vlib ] || exit 98
[ "${VTEST_FAIL_FAST:-}" = 1 ] && [ "${VJOBS:-}" = 1 ] || exit 99
echo self-tests >> self.log
exit 0
fi
[ "$1" = run ] && [ "$2" = ci/macos_ci.vsh ] || exit 90
[ "$CI" = true ] && [ "$GITHUB_ACTIONS" = true ] || exit 91
[ "$VFLAGS" = '-cc clang' ] && [ "$V_MACOS_V3_NO_FALLBACK" = 1 ] || exit 92
@@ -128,6 +134,13 @@ run_ci 0 v_doctor
cmp "$work/saved" "$checkpoint"
[ "$(cat "$checkout/direct.log")" = doctor ] || fail 'individual task did not run'
# Execute the real self_tests task: its GitHub branch must retain the aggregate
# runner's serial fail-fast setting instead of replacing it with the CPU count.
(cd "$checkout" && GITHUB_JOB=clang-macos VTEST_FAIL_FAST=1 VJOBS=1 \
V_CI_VEXE="$fake_v" "$runner" self_tests) > "$work/output" 2>&1 || \
fail 'self_tests replaced the fail-fast worker count'
[ "$(cat "$checkout/self.log")" = self-tests ] || fail 'self_tests did not run'
# A second checkout must not consume the first checkout's saved progress.
first_checkout=$checkout
checkout="$work/other checkout"
+6
View File
@@ -154,3 +154,9 @@ Registry-qualified dependency keys remain distinct in lock data.
The candidate search behind `resolve_with_pubgrub` is test-only: it delegates to the consistent
backtracking search, which the test suite exercises. Dependency constraints are checked in both
directions; a complete conflict-driven PubGrub algorithm is pending.
On Windows, VPM enables Git `core.longpaths` for the Git processes it starts. Existing
`GIT_CONFIG_PARAMETERS` and indexed `GIT_CONFIG_*` entries are preserved, with the long-path
setting appended using the assignment syntax supported before Git 2.31. Invalid or unextendable
indexed counts are left unchanged and reported.
Local Windows `file:///C:/...` origins compare consistently with ordinary drive paths.
+1 -1
View File
@@ -28,7 +28,7 @@ struct ErrorOptions {
verbose bool // is used to only output the error message if the verbose setting is enabled.
}
const vexe = os.quoted_path(os.getenv('VEXE'))
const vexe = os.getenv('VEXE')
const home_dir = os.home_dir()
const selected_server_url_env = 'VPM_SELECTED_SERVER_URL'
const vpm_http_timeout = 10 * time.second
+2 -6
View File
@@ -1,17 +1,14 @@
// vtest build: network?
import os
import time
import rand
import v.vmod
import test_utils { cmd_ok_args }
const v = os.quoted_path(@VEXE)
const v = @VEXE
const test_path = os.join_path(os.vtmp_dir(), 'vpm_dependency_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
os.mkdir_all(test_path) or {}
@@ -69,7 +66,6 @@ fn test_install_dependencies_in_module_dir() {
fn test_resolve_external_dependencies_during_module_install() {
res := cmd_ok_args(@LOCATION, ['${v}', 'install', '-v',
'https://github.com/ttytm/emoji-mart-desktop'])
assert res.output.contains('Found 2 dependencies'), res.output
expect_installing(@LOCATION, res.output, 'ttytm.webview')
expect_installing(@LOCATION, res.output, 'miniaudio')
// `ttytm.webview` is a registered VPM module, so it lands in `<vmodules_dir>/ttytm/webview`.
+22 -14
View File
@@ -8,7 +8,7 @@ import test_utils { cmd_fail_args, cmd_ok_args }
const test_path = os.join_path(os.vtmp_dir(), 'vpm_install_local_test_${rand.ulid()}')
struct LocalInstallCase {
args string
args []string
module_name string
workdir string
}
@@ -21,52 +21,59 @@ fn testsuite_end() {
os.rmdir_all(test_path) or {}
}
fn test_install_from_local_git_repository_variants() {
fn test_install_from_local_git_repository_variants() ! {
repo_path := os.join_path(test_path, 'local_repo')
repo_dot_git_path := os.join_path(test_path, 'local_repo.git')
create_local_git_module(repo_path, 'local_repo_pkg')
create_local_git_module(repo_dot_git_path, 'local_repo_dot_git_pkg')
// A temporary root with a space in it, such as a profile path, must still be one argument.
spaced_repo_path := os.join_path(test_path, 'space dir', 'local repo')
create_local_git_module(spaced_repo_path, 'spaced_repo_pkg')
cases := [
LocalInstallCase{
args: os.quoted_path(repo_path)
args: [spaced_repo_path]
module_name: 'spaced_repo_pkg'
},
LocalInstallCase{
args: [repo_path]
module_name: 'local_repo_pkg'
},
LocalInstallCase{
args: file_url(repo_path)
args: [file_url(repo_path)]
module_name: 'local_repo_pkg'
},
LocalInstallCase{
args: '--git ${os.quoted_path(repo_path)}'
args: ['--git', repo_path]
module_name: 'local_repo_pkg'
},
LocalInstallCase{
args: '--git ${file_url(repo_path)}'
args: ['--git', file_url(repo_path)]
module_name: 'local_repo_pkg'
},
LocalInstallCase{
args: os.file_name(repo_path)
args: [os.file_name(repo_path)]
module_name: 'local_repo_pkg'
workdir: test_path
},
LocalInstallCase{
args: os.quoted_path(repo_dot_git_path)
args: [repo_dot_git_path]
module_name: 'local_repo_dot_git_pkg'
},
LocalInstallCase{
args: file_url(repo_dot_git_path)
args: [file_url(repo_dot_git_path)]
module_name: 'local_repo_dot_git_pkg'
},
LocalInstallCase{
args: '--git ${os.quoted_path(repo_dot_git_path)}'
args: ['--git', repo_dot_git_path]
module_name: 'local_repo_dot_git_pkg'
},
LocalInstallCase{
args: '--git ${file_url(repo_dot_git_path)}'
args: ['--git', file_url(repo_dot_git_path)]
module_name: 'local_repo_dot_git_pkg'
},
LocalInstallCase{
args: os.file_name(repo_dot_git_path)
args: [os.file_name(repo_dot_git_path)]
module_name: 'local_repo_dot_git_pkg'
workdir: test_path
},
@@ -74,12 +81,13 @@ fn test_install_from_local_git_repository_variants() {
for i, c in cases {
vmodules_path := os.join_path(test_path, 'vmodules_case_${i}')
test_utils.set_test_env(vmodules_path)
cmd := '${vexe} install ${c.args}'
old_dir := os.getwd()
if c.workdir != '' {
os.chdir(c.workdir) or { panic(err) }
}
res := cmd_ok_args(@LOCATION, [vexe, 'install', '${c.args}'])
mut argv := [vexe, 'install']
argv << c.args
res := cmd_ok_args(@LOCATION, argv)
if c.workdir != '' {
os.chdir(old_dir) or {}
}
+5 -9
View File
@@ -1,4 +1,4 @@
// vtest build: !musl? && !sanitized_job?
// vtest build: network? && !musl? && !sanitized_job?
// vtest retry: 3
module main
@@ -12,10 +12,6 @@ import test_utils { cmd_fail_args, cmd_ok_args }
const test_path = os.join_path(os.vtmp_dir(), 'vpm_install_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
}
@@ -57,7 +53,7 @@ fn test_install_from_git_url() {
assert res.output.contains('`http` is deprecated'), res.output
manifest = get_vmod(os.join_path('wertzui123', 'hashmap'))
res = cmd_ok_args(@LOCATION, [os.getenv('VEXE'), 'install', 'http://github.com/Wertzui123/HashMap'])
assert res.output.contains('Updating module `wertzui123.hashmap`'), res.output
assert res.output.contains('Module `HashMap` is already installed.'), res.output
assert res.output.contains('`http` is deprecated'), res.output
res = cmd_ok_args(@LOCATION, [os.getenv('VEXE'), 'install', 'https://gitlab.com/tobealive/webview'])
assert res.output.contains('Installed `webview`'), res.output
@@ -76,20 +72,20 @@ fn test_install_from_git_url_uses_registered_package_name() ! {
assert manifest.name == 'nedpals.args'
res = cmd_ok_args(@LOCATION, [os.getenv('VEXE'), 'install', 'https://github.com/nedpals/v-args'])
assert res.output.contains('Updating module `nedpals.args`'), res.output
assert res.output.contains('Module `nedpals.args` is already installed.'), res.output
manifest = get_vmod(os.join_path('nedpals', 'args'))
assert manifest.name == 'nedpals.args'
}
fn test_install_already_existent() {
mut res := cmd_ok_args(@LOCATION, [os.getenv('VEXE'), 'install', 'https://github.com/vlang/markdown'])
assert res.output.contains('Updating module `markdown`'), res.output
assert res.output.contains('Module `markdown` is already installed.'), res.output
manifest := get_vmod('markdown')
assert manifest.name == 'markdown'
assert manifest.dependencies == []string{}
// The same module but with the `.git` extension added.
res = cmd_ok_args(@LOCATION, [os.getenv('VEXE'), 'install', 'https://github.com/vlang/markdown.git'])
assert res.output.contains('Updating module `markdown`'), res.output
assert res.output.contains('Module `markdown` is already installed.'), res.output
}
fn test_install_once() {
+1 -5
View File
@@ -1,4 +1,4 @@
// vtest build: !musl? && !sanitized_job?
// vtest build: network? && !musl? && !sanitized_job?
// vtest retry: 3
import os
import rand
@@ -14,10 +14,6 @@ const expect_exe = os.quoted_path(os.find_abs_path_of_executable('expect') or {
})
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
// Explicitly disable fail on prompt.
+7 -11
View File
@@ -1,4 +1,4 @@
// vtest build: !musl? && !sanitized_job?
// vtest build: network? && !musl? && !sanitized_job?
// vtest retry: 3
module main
@@ -10,10 +10,6 @@ import test_utils { cmd_fail_args, cmd_ok_args }
const test_path = os.join_path(os.vtmp_dir(), 'vpm_install_version_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
}
@@ -41,7 +37,7 @@ fn test_install_from_vpm_with_git_version_tag() {
assert manifest.version == '0.6.0'
// Install same version without force flag.
res = cmd_ok_args(@LOCATION, [vexe, 'install', '${ident}' + '@' + '${tag}'])
assert res.output.contains('Module `${ident}@${tag}` is already installed, use --force to overwrite'), res.output
assert res.output.contains('Module `${ident}@${tag}` is already installed.'), res.output
// Install another version, add force flag to surpass confirmation.
tag = 'v0.5.0'
res = cmd_ok_args(@LOCATION, [vexe, 'install', '-f', '${ident}' + '@' + '${tag}'])
@@ -52,18 +48,18 @@ fn test_install_from_vpm_with_git_version_tag() {
// Install invalid version.
tag = '6.0'
res = cmd_fail_args(@LOCATION, [vexe, 'install', '-f', '${ident}' + '@' + '${tag}'])
assert res.output.contains('failed to install `${ident}`'), res.output
assert res.output.contains('failed to install `${ident}@${tag}`'), res.output
// Install invalid version verbose.
res = cmd_fail_args(@LOCATION, [vexe, 'install', '-f', '-v', '${ident}' + '@' + '${tag}'])
assert res.output.contains('failed to install `${ident}`'), res.output
assert res.output.contains('failed to install `${ident}@${tag}`'), res.output
assert res.output.contains('Remote branch 6.0 not found in upstream origin'), res.output
// Install without version tag after a version was installed
res = cmd_ok_args(@LOCATION, [vexe, 'install', '-f', '${ident}'])
assert res.output.contains('Installing `${ident}`'), res.output
// Re-install latest version (without a tag). Should trigger an update, force should not be required.
// Re-install latest version (without a tag). The same revision is already installed, so it is skipped without force.
res = cmd_ok_args(@LOCATION, [vexe, 'install', '${ident}'])
assert res.output.contains('Updating module `${ident}`'), res.output
assert res.output.contains('Module `${ident}` is already installed.'), res.output
}
fn test_install_from_git_url_with_version_tag() {
@@ -77,7 +73,7 @@ fn test_install_from_git_url_with_version_tag() {
assert manifest.version == '0.1.50'
// Install same version without force flag.
res = cmd_ok_args(@LOCATION, [vexe, 'install', '${url}' + '@' + '${tag}'])
assert res.output.contains('Module `vsl@${tag}` is already installed, use --force to overwrite'), res.output
assert res.output.contains('Module `vsl@${tag}` is already installed.'), res.output
// Install another version, add force flag to surpass confirmation.
tag = 'v0.1.47'
res = cmd_ok_args(@LOCATION, [vexe, 'install', '-f', '${url}' + '@' + '${tag}'])
@@ -0,0 +1,22 @@
module main
// The stored origin of a module installed on Windows is a `file:///S:/...` URL.
// It must normalise to the same value as the plain path, or pins are never matched.
fn test_normalized_clone_source_matches_windows_drive_file_urls() {
$if windows {
path := @DIR
url := 'file:///' + path.replace('\\', '/')
normalized := normalized_clone_source(url)
assert normalized == normalized_clone_source(path)
assert normalized.all_after('file://').count(':') == 1, normalized
}
}
// On POSIX, `/a:/...` is an ordinary path, not a drive path. Stripping its slash
// would make it relative, so the same URL would normalise differently from each
// working directory.
fn test_normalized_clone_source_keeps_posix_paths_with_a_colon_after_one_character() {
$if !windows {
assert normalized_clone_source('file:///a:/repo') == 'file:///a:/repo'
}
}
+1 -5
View File
@@ -1,5 +1,5 @@
// vtest retry: 3
// vtest build: !windows
// vtest build: network? && !windows
module main
import os
@@ -9,10 +9,6 @@ import test_utils { cmd_ok_args }
const test_path = os.join_path(os.vtmp_dir(), 'vpm_outdated_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
os.mkdir_all(test_path)!
+11 -6
View File
@@ -59,6 +59,11 @@ fn override_project(directory string, deps []string, overrides []string) !string
return project
}
// lockfile_repo_path returns a repository path in the form the lockfile stores it: forward slashes.
fn lockfile_repo_path(path string) string {
return path.replace('\\', '/')
}
fn test_override_selects_source_manifest_dependencies_and_locked_revision() {
leaf := override_repo('leaf-source')!
leaf_one := override_tag(leaf, 'leaf', 'v1.0.0', '0.0.1', [], [])!
@@ -75,8 +80,8 @@ fn test_override_selects_source_manifest_dependencies_and_locked_revision() {
assert override_git(os.join_path(store, 'pkg'), ['rev-parse', 'HEAD']) == parent_two
assert override_git(os.join_path(store, 'leaf'), ['rev-parse', 'HEAD']) == leaf_one
assert os.read_file(os.join_path(store, 'leaf', 'marker.txt'))! == 'v1.0.0'
entry := read_lockfile(project)!.modules[lockfile_module_key(dep)]!
assert entry.requested == parent + '@v2.0.0'
entry := read_lockfile(project)!.modules[lockfile_module_key(lockfile_repo_path(dep))]!
assert entry.requested == lockfile_repo_path(parent) + '@v2.0.0'
assert entry.resolved == 'v2.0.0'
assert entry.revision == parent_two
override_tag(parent, 'pkg', 'v3.0.0', '0.0.1', [leaf + '@v2.0.0'], [])!
@@ -98,7 +103,7 @@ fn test_override_selects_source_manifest_dependencies_and_locked_revision() {
cmd_ok_args(@LOCATION, [override_vpm_exe, 'install', '-f'])
assert os.read_file(os.join_path(fresh, 'pkg', 'marker.txt'))! == 'v3.0.0'
assert os.read_file(os.join_path(fresh, 'leaf', 'marker.txt'))! == 'v2.0.0'
assert read_lockfile(project)!.modules[lockfile_module_key(dep)]!.requested == parent + '@v3.0.0'
assert read_lockfile(project)!.modules[lockfile_module_key(lockfile_repo_path(dep))]!.requested == lockfile_repo_path(parent) + '@v3.0.0'
}
fn test_override_range_resolves_and_locks_actual_tag() {
@@ -108,8 +113,8 @@ fn test_override_range_resolves_and_locks_actual_tag() {
project := override_project('range-project', [repo + '@missing-tag'], ['range_pkg: ^2.0.0'])!
test_utils.set_test_env(os.join_path(override_test_root, 'range-store'))
cmd_ok_args(@LOCATION, [override_vpm_exe, 'install'])
entry := read_lockfile(project)!.modules[repo]!
assert entry.requested == repo + '@^2.0.0'
entry := read_lockfile(project)!.modules[lockfile_repo_path(repo)]!
assert entry.requested == lockfile_repo_path(repo) + '@^2.0.0'
assert entry.resolved == 'v2.1.0'
assert entry.revision == selected
override_tag(repo, 'range_pkg', 'v2.2.0', '0.0.1', [], [])!
@@ -132,7 +137,7 @@ fn test_selector_override_selects_the_requiring_edges_source_before_min_v() {
cmd_ok_args(@LOCATION, [override_vpm_exe, 'install'])
assert override_git(os.join_path(store, 'c'), ['rev-parse', 'HEAD']) == selected
assert os.read_file(os.join_path(store, 'c', 'marker.txt'))! == 'v1.0.0'
assert read_lockfile(project)!.modules[leaf]!.requested == leaf + '@v1.0.0'
assert read_lockfile(project)!.modules[lockfile_repo_path(leaf)]!.requested == lockfile_repo_path(leaf) + '@v1.0.0'
other := override_repo('selector-other')!
override_tag(other, 'other', 'v1.0.0', '0.0.1', [leaf + '@v2.0.0'], [])!
override_project('selector-other-project', [other + '@v1.0.0'], ['requiring>c: v1.0.0'])!
+10 -1
View File
@@ -378,7 +378,16 @@ fn (m Module) existing_checkout_matches_source() bool {
fn normalized_clone_source(raw_source string) string {
raw := raw_source.trim_space()
local_path := os.expand_tilde_to_home(raw.trim_string_left('file://'))
mut local_path := os.expand_tilde_to_home(raw.trim_string_left('file://'))
// `file:///S:/x` keeps a slash before the drive letter. Drop it, or os.real_path
// resolves `/S:/x` against the current drive and gives `S:\S:\x`. Only Windows
// has drive letters: on POSIX `/a:/x` is an ordinary absolute path, so keep it.
$if windows {
if local_path.len > 2 && local_path[0] == `/` && local_path[1].is_letter()
&& local_path[2] == `:` {
local_path = local_path[1..]
}
}
if raw.starts_with('file://') || os.is_abs_path(local_path) || raw.starts_with('./')
|| raw.starts_with('../') || raw.starts_with('~/') || os.exists(local_path) {
return 'file://' + os.real_path(local_path)
+2 -5
View File
@@ -1,16 +1,13 @@
// vtest build: network?
// vtest retry: 3
import os
import rand
import test_utils
const vexe = os.quoted_path(@VEXE)
const vexe = @VEXE
const test_path = os.join_path(os.vtmp_dir(), 'vpm_remove_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
}
+1 -4
View File
@@ -1,13 +1,10 @@
// vtest build: network?
// vtest retry: 3
module main
import test_utils { cmd_ok_args }
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
unbuffer_stdout()
}
+2 -6
View File
@@ -1,17 +1,13 @@
// vtest build: !musl? && !sanitized_job?
// vtest build: network? && !musl? && !sanitized_job?
// vtest retry: 3
import os
import rand
import test_utils { cmd_fail_args, cmd_ok_args }
const v = os.quoted_path(@VEXE)
const v = @VEXE
const test_path = os.join_path(os.vtmp_dir(), 'vpm_update_test_${rand.ulid()}')
fn testsuite_begin() {
$if !network ? {
eprintln('> skipping ${@FILE}, when `-d network` is missing')
exit(0)
}
dump(test_path)
test_utils.set_test_env(test_path)
}
+55 -5
View File
@@ -26,12 +26,12 @@ const vcs_info = init_vcs_info() or {
exit(1)
}
fn init_vcs_info() !map[VCS]VCSInfo {
git_installed_raw_ver := parse_git_version(os.exec_opt(['git', '--version'])!.output) or { '' }
git_installed_ver := semver.from(git_installed_raw_ver)!
// git_clone_install_args returns the options `git clone` takes before the url and
// path, for the installed git `git_ver` on `os_name`.
fn git_clone_install_args(git_ver semver.Version, os_name string) ![]string {
git_shallow_submod_ver := semver.from('2.36.0')!
mut git_install_args := ['clone', '--recursive']
if os.user_os() != 'windows' {
if os_name != 'windows' {
// The variation of environment factors on windows is too high;
// the following options are known to work well on != windows,
// but can sometimes cause failures on windows for yet unknown reasons,
@@ -39,7 +39,7 @@ fn init_vcs_info() !map[VCS]VCSInfo {
// for more details, about why this is now allowed only on != windows platforms.
// Keep module blobs in the initial pack. Partial clones need another network
// fetch during checkout, which can stall installations such as `v install ui2`.
if git_installed_ver >= git_shallow_submod_ver {
if git_ver >= git_shallow_submod_ver {
git_install_args << '--shallow-submodules'
}
}
@@ -48,6 +48,13 @@ fn init_vcs_info() !map[VCS]VCSInfo {
// 13.6 MB with it, for an identical working tree.
git_install_args << '--depth'
git_install_args << '1'
return git_install_args
}
fn init_vcs_info() !map[VCS]VCSInfo {
git_installed_raw_ver := parse_git_version(os.exec_opt(['git', '--version'])!.output) or { '' }
git_installed_ver := semver.from(git_installed_raw_ver)!
git_install_args := git_clone_install_args(git_installed_ver, os.user_os())!
return {
VCS.git: VCSInfo{
dir: '.git'
@@ -74,6 +81,49 @@ fn init_vcs_info() !map[VCS]VCSInfo {
}
}
// git_config_count returns how many GIT_CONFIG_* entries the environment already
// holds, or none when GIT_CONFIG_COUNT is set to anything but a plain decimal count.
fn git_config_count() ?int {
text := os.getenv('GIT_CONFIG_COUNT')
if text == '' {
return 0
}
count := text.int()
if count < 0 || count == 2147483647 || count.str() != text {
return none
}
return count
}
// enable_git_longpaths sets core.longpaths for every git process vpm starts after
// main begins. The `git --version` call that initializes vcs_info runs before that, and
// reads no config, so it does not need the setting.
// Windows git keeps the 260-character path limit unless that setting is on, and
// vpm's module cache is deep enough to exceed it. The setting goes in two variables:
// GIT_CONFIG_COUNT, which git 2.31 and later read, and GIT_CONFIG_PARAMETERS, the
// variable `git -c` fills in, which every git version reads, so it also covers git
// before 2.31. Existing entries in both are kept, and the new setting is appended
// after them. A GIT_CONFIG_COUNT that is not a plain non-negative decimal is left
// alone and reported, since extending it would overwrite or hide the user's entries.
// The new entries come last, so a core.longpaths=false the user already set is
// overridden for the git processes vpm starts.
fn enable_git_longpaths() {
mut params := os.getenv('GIT_CONFIG_PARAMETERS')
if params != '' {
params += ' '
}
os.setenv('GIT_CONFIG_PARAMETERS', "${params}'core.longpaths=true'", true)
count := git_config_count() or {
vpm_warn('not adding core.longpaths to GIT_CONFIG_COUNT.',
details: 'GIT_CONFIG_COUNT is not a plain non-negative integer, so vpm leaves it alone. Git may exit with an error on it.'
)
return
}
os.setenv('GIT_CONFIG_KEY_${count}', 'core.longpaths', true)
os.setenv('GIT_CONFIG_VALUE_${count}', 'true', true)
os.setenv('GIT_CONFIG_COUNT', '${count + 1}', true)
}
fn (vcs VCS) clone_args(url string, version string, path string) ![]string {
if version.contains('\0') || version.contains('\r') || version.contains('\n') {
return error('version contains NUL, CR, or LF')
+96
View File
@@ -50,6 +50,102 @@ fn test_clone_args() {
assert !head_args.contains('--single-branch')
}
// save_git_config_env and restore_git_config_env keep these tests from leaking
// GIT_CONFIG_* variables, or the HOME and XDG_CONFIG_HOME config locations, into the
// rest of the test process.
fn save_git_config_env() map[string]string {
mut saved := map[string]string{}
for name in ['GIT_CONFIG_COUNT', 'GIT_CONFIG_KEY_0', 'GIT_CONFIG_VALUE_0', 'GIT_CONFIG_KEY_2',
'GIT_CONFIG_VALUE_2', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM',
'HOME', 'XDG_CONFIG_HOME'] {
saved[name] = os.getenv(name)
}
return saved
}
fn restore_git_config_env(saved map[string]string) {
for name, value in saved {
if value == '' {
os.unsetenv(name)
} else {
os.setenv(name, value, true)
}
}
}
// A count that cannot be extended must be left alone: rewriting it would overwrite
// the user's own config entries, or hide them from git. GIT_CONFIG_PARAMETERS is
// separate, so the setting still reaches git through it.
fn test_enable_git_longpaths_leaves_an_invalid_count_alone() {
saved := save_git_config_env()
defer {
restore_git_config_env(saved)
}
for invalid in ['-1', 'two', '2 ', '+2', '99999999999999999999', '2147483647'] {
os.unsetenv('GIT_CONFIG_KEY_0')
os.unsetenv('GIT_CONFIG_PARAMETERS')
os.setenv('GIT_CONFIG_COUNT', invalid, true)
enable_git_longpaths()
assert os.getenv('GIT_CONFIG_COUNT') == invalid, invalid
assert os.getenv('GIT_CONFIG_KEY_0') == '', invalid
assert os.getenv('GIT_CONFIG_PARAMETERS') == "'core.longpaths=true'", invalid
}
}
fn test_enable_git_longpaths_appends_after_existing_entries() {
saved := save_git_config_env()
defer {
restore_git_config_env(saved)
}
os.unsetenv('GIT_CONFIG_KEY_2')
os.setenv('GIT_CONFIG_COUNT', '2', true)
os.setenv('GIT_CONFIG_PARAMETERS', "'core.autocrlf=false'", true)
enable_git_longpaths()
assert os.getenv('GIT_CONFIG_COUNT') == '3'
assert os.getenv('GIT_CONFIG_KEY_2') == 'core.longpaths'
assert os.getenv('GIT_CONFIG_VALUE_2') == 'true'
assert os.getenv('GIT_CONFIG_PARAMETERS') == "'core.autocrlf=false' 'core.longpaths=true'"
os.unsetenv('GIT_CONFIG_COUNT')
os.unsetenv('GIT_CONFIG_KEY_0')
os.unsetenv('GIT_CONFIG_PARAMETERS')
enable_git_longpaths()
assert os.getenv('GIT_CONFIG_COUNT') == '1'
assert os.getenv('GIT_CONFIG_KEY_0') == 'core.longpaths'
assert os.getenv('GIT_CONFIG_PARAMETERS') == "'core.longpaths=true'"
}
// Git before 2.31 ignores GIT_CONFIG_COUNT, but every git version reads
// GIT_CONFIG_PARAMETERS. The COUNT entries are dropped after enabling, so only
// GIT_CONFIG_PARAMETERS can carry core.longpaths to git.
fn test_enable_git_longpaths_reaches_git_without_config_count() {
saved := save_git_config_env()
defer {
restore_git_config_env(saved)
}
root := os.join_path(os.vtmp_dir(), 'vpm_longpaths_${rand.ulid()}')
os.mkdir_all(root)!
defer {
os.rmdir_all(root) or {}
}
os.setenv('GIT_CONFIG_PARAMETERS', "'core.autocrlf=false'", true)
enable_git_longpaths()
os.unsetenv('GIT_CONFIG_COUNT')
os.unsetenv('GIT_CONFIG_KEY_0')
os.unsetenv('GIT_CONFIG_VALUE_0')
// Isolate git from the machine's own config, which may already set core.longpaths.
// GIT_CONFIG_GLOBAL is only read by git 2.32 and later, so HOME and XDG_CONFIG_HOME
// also move the user config into the test root for the older git this test is for.
empty_config := os.join_path(root, 'empty.gitconfig')
os.write_file(empty_config, '')!
os.setenv('HOME', root, true)
os.setenv('XDG_CONFIG_HOME', root, true)
os.setenv('GIT_CONFIG_NOSYSTEM', '1', true)
os.setenv('GIT_CONFIG_GLOBAL', empty_config, true)
res := os.exec(['git', '-C', root, 'config', '--get', 'core.longpaths'])
assert res.exit_code == 0, res.output
assert res.output.trim_space() == 'true'
}
fn test_git_clone_contains_historical_blobs() {
root := os.join_path(os.vtmp_dir(), 'vpm_clone_blobs_${rand.ulid()}')
repo := os.join_path(root, 'source')
+3
View File
@@ -21,6 +21,9 @@ const excluded_dirs = ['.cache', 'vlib']
fn main() {
unbuffer_stdout()
os.setenv(selected_server_url_env, '', true)
$if windows {
enable_git_longpaths()
}
// This tool is intended to be launched by the v frontend,
// which provides the path to V inside os.getenv('VEXE')
// args are: vpm [options] SUBCOMMAND module names
+4 -2
View File
@@ -2,6 +2,7 @@ module main
import os
import v.cmdexec
import v.pref
import v.skills
import v.util.version
import v.util.recompilation
@@ -218,7 +219,8 @@ fn tcc_cache_requests(root string, requests []cache_refresh.Request) []cache_ref
fn is_tcc_cache_compiler(path string, bundled string, cwd string) bool {
resolved := os.real_path(path)
name := os.file_name(resolved).to_lower_ascii()
if resolved == os.real_path(bundled) || name.contains('tcc') || name.contains('tinyc') {
// Same file-name rule as the launcher and driver, so `notcc` or `distcc` is not TinyCC by name.
if resolved == os.real_path(bundled) || pref.tinycc_file_name(name) {
return true
}
// Match the driver's effective compiler classification, including renamed
@@ -230,7 +232,7 @@ fn is_tcc_cache_compiler(path string, bundled string, cwd string) bool {
if os.user_os() in ['macos', 'ios'] && resolved == '/usr/bin/cc' { return false }
if !os.is_executable(path) { return false }
compiler_version := cmdexec.run_in(path, ['--version'], cwd).output.to_lower_ascii()
return compiler_version.contains('tiny c compiler') || compiler_version.contains('tcc version')
return pref.ccompiler_version_is_tinycc(compiler_version)
}
// skills_refresh_hint is what the report tells the user to run.
+21 -16
View File
@@ -56,9 +56,11 @@ fn rebundle(fake_vroot string, name string) ! {
// It returns the checkout, the home directory, the built tool, and a PATH
// holding only the stand-ins.
fn vup_report_fixture() !(string, string, string, string) {
root := os.join_path(os.vtmp_dir(), 'vup_report_${os.getpid()}')
mut root := os.join_path(os.vtmp_dir(), 'vup_report_${os.getpid()}')
os.rmdir_all(root) or {}
os.mkdir_all(root) or { panic(err) }
// Match the checkout's physical path when macOS exposes /tmp through /private/tmp.
root = os.real_path(root)
home := os.join_path(root, 'home')
os.mkdir_all(os.join_path(home, '.agents', 'skills')) or { panic(err) }
for name in ['alpha', 'beta', 'gamma'] {
@@ -154,20 +156,19 @@ fn test_vup_refreshes_only_tinycc_modes_even_when_already_updated() ! {
os.write_file(cache_log, '')!
result := run_vup(root, home, tool, bin_dir, ...options)!
assert result.exit_code == 0, result.output
assert result.output.count('Caching module crypto.sample...') == 4, result.output
assert result.output.count('> Refreshing module cache [') == 4, result.output
assert result.output.contains('> Refreshing module cache [1/4]'), result.output
assert result.output.contains('> Refreshing module cache [4/4]'), result.output
assert result.output.contains('> Module cache refresh complete: 4 build configurations checked.'), result.output
assert result.output.count('Caching module crypto.sample...') == 2, result.output
assert result.output.count('> Refreshing module cache [') == 2, result.output
assert result.output.contains('> Refreshing module cache [1/2]'), result.output
assert result.output.contains('> Refreshing module cache [2/2]'), result.output
assert result.output.contains('> Module cache refresh complete: 2 build configurations checked.'), result.output
assert result.output.contains('(normal program; -cc tcc).\nCaching module crypto.sample...'), result.output
assert result.output.contains('(production program; -cc tcc -prod).\nCaching module crypto.sample...'), result.output
assert result.output.contains('(normal tests; -cc tcc).\nCaching module crypto.sample...'), result.output
assert result.output.contains('(production tests; -cc tcc -prod).\nCaching module crypto.sample...'), result.output
// The production TinyCC records stay on disk from before the rule, and are not refreshed.
calls := os.read_file(cache_log)!.split_into_lines()
assert calls.len == 4, calls.str()
assert calls.len == 2, calls.str()
assert calls.all(it.starts_with('-cc tcc ')), calls.str()
assert calls.any(it.starts_with('-cc tcc -skip-running ')), calls.str()
assert calls.any(it.starts_with('-cc tcc -prod -skip-running ')), calls.str()
assert !calls.any(it.contains('-prod')), calls.str()
assert !calls.any(it.contains(' run ')), calls.str()
}
// A failed refresh is visible and fails v up, while other modules still refresh.
@@ -180,7 +181,7 @@ fn test_vup_refreshes_only_tinycc_modes_even_when_already_updated() ! {
assert failure.exit_code != 0, failure.output
assert failure.output.contains('refreshing cached vlib modules failed'), failure.output
assert !failure.output.contains('> Module cache refresh complete:'), failure.output
assert os.read_file(cache_log)!.split_into_lines().len == 4
assert os.read_file(cache_log)!.split_into_lines().len == 2
}
fn test_vup_resolves_relative_cache_paths_before_entering_checkout() ! {
@@ -268,7 +269,8 @@ fn test_vup_recognizes_tinycc_paths_aliases_and_default_records() ! {
write_executable(wrapper, '#!/bin/sh\necho probe >> ' + os.quoted_path(probes) +
'\necho "tcc version 0.9.27"\n')!
other := os.join_path(bin_dir, 'other-compiler')
write_executable(other, '#!/bin/sh\necho "clang version 18.0.0"\n')!
write_executable(other, '#!/bin/sh\necho "clang version 19.1.7"\n' +
'echo "InstalledDir: /opt/tinycc-tcc version-compatible-sdk/bin"\n')!
relative := os.path_rel(os.getwd(), bundled)!
variants := [['-cc', 'tinyc'], ['-cc', bundled], ['-cc', alias], ['-cc', relative], []string{},
['-cc', os.join_path(bin_dir, 'cc')], ['-cc'], ['-cc', renamed], ['-cc', copied],
@@ -305,7 +307,8 @@ fn test_vup_recognizes_tinycc_paths_aliases_and_default_records() ! {
result := run_vup(root, home, tool, bin_dir)!
assert result.exit_code == 0, result.output
calls := os.read_file(log)!.split_into_lines()
assert calls.len == 9, calls.str()
// The production wrapper resolves to TinyCC, so it is skipped with the other tcc+prod records.
assert calls.len == 8, calls.str()
assert calls.any(it.starts_with('-cc tinyc ')), calls.str()
assert calls.any(it.starts_with('-cc ${bundled} ')), calls.str()
assert calls.any(it.starts_with('-cc ${alias} ')), calls.str()
@@ -313,16 +316,18 @@ fn test_vup_recognizes_tinycc_paths_aliases_and_default_records() ! {
assert calls.any(it.starts_with('-cc ${renamed} ')), calls.str()
assert calls.any(it.starts_with('-cc ${copied} ')), calls.str()
assert calls.any(it.starts_with('-cc ${wrapper} -skip-running ')), calls.str()
assert calls.any(it.starts_with('-cc ${wrapper} -prod ')), calls.str()
assert !calls.any(it.starts_with('-cc ${wrapper} -prod ')), calls.str()
assert calls.filter(it.starts_with('-cc ${context_wrapper} ')).len == 1, calls.str()
assert !calls.any(it.contains('-cc ${os.join_path(bin_dir, 'cc')} ')), calls.str()
assert !calls.any(it.contains('-cc ${other} ')), calls.str()
assert os.read_file(probes)!.split_into_lines() == ['probe']
// vup probes the wrapper once for its cache selection, and the refresh filter probes the
// production record again before skipping it.
assert os.read_file(probes)!.split_into_lines() == ['probe', 'probe']
contexts := os.read_file(context_probes)!.split_into_lines()
assert contexts.len == 2, contexts.str()
assert os.join_path(root, 'tinycc_project') in contexts, contexts.str()
assert os.join_path(root, 'clang_project') in contexts, contexts.str()
assert result.output.contains('9 build configurations checked.'), result.output
assert result.output.contains('8 build configurations checked.'), result.output
}
fn test_vup_rebuilds_changed_tinycc_module_and_reuses_unrelated_module() ! {
+82
View File
@@ -678,6 +678,58 @@ fn run_launcher_test_process(executable string, args []string, work_dir string,
return result
}
fn test_prod_with_tinycc_is_refused_before_any_compiler_runs() {
dir := os.join_path(os.vtmp_dir(), 'v_prod_tinycc_${os.getpid()}')
os.mkdir_all(dir) or { panic(err) }
defer {
os.rmdir_all(dir) or {}
}
source := os.join_path(dir, 'main.v')
os.write_file(source, 'fn main() {\n\tprintln(1)\n}\n') or { panic(err) }
output := os.join_path(dir, 'main')
// `-old-compiler` must not reach the compatibility compiler with this pair either.
for args in [['-prod', '-cc', 'tcc'], ['-cc=tinyc', '-prod'],
['-old-compiler', '-prod', '-cc', 'tcc']] {
result := os.exec([@VEXE, ...args, '-o', output, source])
assert result.exit_code == 1, '${args}: ${result.output}'
assert result.output.contains('tcc is not allowed with -prod'), '${args}: ${result.output}'
}
}
fn test_prod_refuses_tinycc_under_another_name_once_it_resolves() {
bundled := os.join_path(os.dir(@VEXE), 'thirdparty', 'tcc', 'tcc.exe')
if !os.is_file(bundled) {
return
}
dir := os.join_path(os.vtmp_dir(), 'v_prod_tinycc_alias_${os.getpid()}')
os.mkdir_all(dir) or { panic(err) }
defer {
os.rmdir_all(dir) or {}
}
// The name has no `tcc`, so only the resolved compiler gives it away.
alias_path := os.join_path(dir, 'cc_alias.exe')
os.cp(bundled, alias_path) or { panic(err) }
source := os.join_path(dir, 'main.v')
os.write_file(source, 'fn main() {\n\tprintln(1)\n}\n') or { panic(err) }
// `-old-compiler` must refuse the alias too, before the compatibility compiler is started.
for args in [['-prod', '-cc', alias_path], ['-old-compiler', '-prod', '-cc', alias_path]] {
result := os.exec([@VEXE, ...args, '-o', os.join_path(dir, 'main'), source])
assert result.exit_code == 1, '${args}: ${result.output}'
assert result.output.contains('tcc is not allowed with -prod'), '${args}: ${result.output}'
assert !result.output.contains('retrying with'), '${args}: ${result.output}'
}
}
fn test_prod_with_tinycc_in_vflags_does_not_block_version_or_help() {
mut environment := os.environ()
environment['VFLAGS'] = '-prod -cc tcc'
for args in [['version'], ['help']] {
result := run_launcher_test_process(@VEXE, args, os.getwd(), environment)
assert result.exit_code == 0, '${args}: ${result.output}'
assert !result.output.contains('tcc is not allowed with -prod'), '${args}: ${result.output}'
}
}
fn test_external_tools_do_not_use_the_v1_fallback() {
$if !windows {
false_executable := os.find_abs_path_of_executable('false') or { return }
@@ -930,3 +982,33 @@ fn test_v1_fallback_rejects_copied_current_launcher() {
assert result.output.starts_with('V ${v_version} ')
assert !v1_fallback_has_expected_version(copy)
}
fn test_private_tool_builds_keep_tools_available_with_tinycc_production_flags() {
assert external_tool_compile_args('vfmt', ['-prod', '-cc', 'tcc']) == ['-cc', 'tcc']
assert external_tool_args_without_prod(['-prod', '-o', '-prod', '-cc', 'tcc']) == [
'-o',
'-prod',
'-cc',
'tcc',
]
assert external_tool_runtime_args('self', ['-prod', '-cc', 'tcc'], ['self']) == [
'-prod',
'-cc',
'tcc',
'self',
]
}
fn test_cold_formatter_cache_accepts_tinycc_production_environment() {
if !os.is_executable(os.join_path(os.dir(@VEXE), 'thirdparty', 'tcc', 'tcc.exe')) { return }
root := os.join_path(os.vtmp_dir(), 'launcher_prod_tcc_tool_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
mut environment := os.environ()
environment['VFLAGS'] = '-prod -cc tcc -gc none'
environment['VTOOLS_CACHE_DIR'] = os.join_path(root, 'tools')
environment['V_MACOS_V3_NO_FALLBACK'] = '1'
result := run_launcher_test_process(@VEXE, ['fmt', '--help'], os.getwd(), environment)
assert result.exit_code == 0, result.output
assert !result.output.contains('tcc is not allowed with -prod'), result.output
}
+35
View File
@@ -142,6 +142,16 @@ fn main() {
eprintln('unknown option `-usecache`; caching is enabled by default, use `-nocache` to disable it')
exit(1)
}
// TinyCC is not a -prod compiler. Refuse the pair before any compiler starts, including the
// compatibility compiler that `-old-compiler` selects.
if command !in external_commands
&& command !in ['version', '-version', '--version', 'help', '-h', '--help', 'new', 'init',
'install', 'link', 'list', 'outdated', 'remove', 'registry', 'search', 'show', 'unlink',
'update', 'upgrade', 'vlib-docs', 'get', 'interpret']
&& pref.tcc_with_prod_requested(compiler_args) {
eprintln('tcc is not allowed with -prod')
exit(1)
}
if command in ['version', '-version', '--version'] {
println('V ${v_version} ${@VCURRENTHASH}')
return
@@ -580,9 +590,34 @@ fn external_tool_compile_args(tool_name string, prefix_args []string) []string {
}
compile_args << ['-gc', 'none']
}
if pref.tcc_with_prod_requested(compile_args) {
compile_args = external_tool_args_without_prod(compile_args)
}
return compile_args
}
// external_tool_args_without_prod removes only the private helper's production option.
// Tool runtime arguments retain the caller's requested compiler configuration.
fn external_tool_args_without_prod(args []string) []string {
mut result := []string{cap: args.len}
mut value_follows := false
for i, arg in args {
if value_follows {
result << arg
value_follows = false
continue
}
if arg == '-prod' { continue }
result << arg
value_follows = if arg in ['-prof', '-profile'] {
v1_fallback_profile_option_consumes_value(args, i)
} else {
arg == '-cf' || pref.option_may_consume_value(arg)
}
}
return result
}
fn external_tool_args_without_target(args []string) []string {
mut result := []string{cap: args.len}
mut skip_target_value := false
+20
View File
@@ -0,0 +1,20 @@
# Wait for the program to enable raw mode before sending input to its terminal.
proc wait_for_raw_mode {timeout_seconds} {
global spawn_out
if {[info exists spawn_out(replica,name)]} {
set tty $spawn_out(replica,name)
} else {
set tty $spawn_out(slave,name)
}
set deadline [expr {[clock milliseconds] + 1000 * $timeout_seconds}]
while {[clock milliseconds] < $deadline} {
if {[catch {exec stty -a < $tty} mode]} {
return 0
}
if {[string match "*-icanon*" $mode]} {
return 1
}
after 25
}
return 0
}
+3
View File
@@ -2,9 +2,12 @@
# Allow a cold compiler/module-cache startup before the interactive prompt.
set timeout 10
source [file join [file dirname [info script]] raw_mode.tcl]
set v_root [exec sh -c "git rev-parse --show-toplevel"]
spawn $v_root/v run $v_root/examples/readline/readline.v
# Give compilation its own deadline; keystroke responses still use timeout above.
if {![wait_for_raw_mode 60]} { exit 1 }
send "a"
expect "got 97" {} timeout { exit 1 }
@@ -2,12 +2,15 @@
# Allow a cold compiler/module-cache startup before the interactive prompt.
set timeout 10
source [file join [file dirname [info script]] raw_mode.tcl]
set v_root [exec sh -c "git rev-parse --show-toplevel"]
# Use input arguments for send and expect.
set send_ [lindex $argv 0]
set expect_ [lindex $argv 1]
spawn $v_root/v run $v_root/examples/readline/readline.v
# Give compilation its own deadline; keystroke responses still use timeout above.
if {![wait_for_raw_mode 60]} { exit 1 }
send $send_
expect $expect_ {} timeout { exit 1 }
+32 -1
View File
@@ -473,6 +473,33 @@ and builds with bundled TinyCC on macOS and Linux. TinyCC builds and links its o
objects. Pass `-nocache` or `--no-cache` to disable caching. A first TinyCC build can be
faster without the cache, while later builds reuse the cached modules and executable.
The former `-usecache` flag is no longer accepted.
The C backend rejects `-prod` with TinyCC, including compiler aliases and renamed copies.
Use Clang or GCC for production builds. Help, formatter, and other tool commands remain available
with those flags. `v.pref.ccompiler_version_is_tinycc` recognizes TinyCC banner lines;
mentions in metadata such as Clang's `InstalledDir` do not identify TinyCC.
Compiler probes follow the launch path, including executable files in the working directory
before `PATH`. Windows explicit paths use the implicit `.exe` suffix and expand `%NAME%` variables;
renamed PE files with other extensions can also be queried.
Cached programs retain the element layouts required by fixed-array aliases and option payloads.
Cached standard-library headers retain their source classification so headerless C bindings keep
their complete layouts, including the Boehm GC stack type used by interface stringification.
Generated fixed-array map-key helpers declare their cached builtin callees before their definitions.
Inferred anonymous fixed arrays use their materialized element types, including nested arrays.
Literal-output programs still instantiate retained generic helpers with `-no-skip-unused`.
Merged imports in cached headers do not create parameter-name conflicts across source files.
Warm builds retain every `IError` implementer's `msg` and `code` methods and their helpers,
including calls made by dispatchers inside cached objects.
Selected option/result tuple signatures retain nested struct payload layouts, including signatures
discovered after cache demand collection, and declare them before their tuples and wrappers.
Shared wrappers retain their payload layouts even when only compile-time reflection visits them.
Recursive interface string helpers belong to the interface's declaration module, including generic
applications whose arguments name types from other modules.
Warm builds preserve generic interface parameter metadata after temporary compiler arenas are freed.
Translated programs regenerate their C prefixes after body edits so new runtime helpers are present;
their module and executable caches remain enabled.
Portable cross output keeps its embedded signal-handler installer local so cached builds can
also link the native helper object.
With `-v` or `-show-timings`, the compiler prints `Caching module <name>...` to standard error
before compiling a new module object, including during `run`. Ordinary builds and warm builds
print no cache creation notices; `-silent` suppresses them too.
@@ -481,7 +508,11 @@ This applies to SHA3 and other imported modules with TinyCC too.
It preserves import order for module C flags and reuses the same records on repeated updates.
Both original entries and temporary importers stop after module object validation,
before compiling program objects or linking an executable.
`v up` refreshes only TinyCC cache configurations, retaining recorded production and test modes.
`v up` refreshes only TinyCC cache configurations, retaining recorded test modes.
Production TinyCC records made by older compilers are skipped.
`v.pref.tcc_with_prod_requested_in` checks compiler wrappers in the record's working directory
and resolves relative compiler paths and `PATH` entries there without changing the caller's
directory or environment.
Caches for other C compilers are refreshed by the next build that uses that compiler.
Compiler updates validate fresh module interfaces and reuse compiled objects when the generated C,
C compiler, flags, and native inputs still match. Reused objects print no cache creation notices.
+14 -11
View File
@@ -162,29 +162,32 @@ fn test_dump_flags_after_cached_incbin_build_uses_arrays() {
assert !flags.contains('_v_embed_blob_'), flags
}
fn test_macos_tcc_build_keeps_the_array_form() {
fn assert_tcc_prod_build_rejected(name string) {
exe := os.join_path(incbin_workspace, name)
main_file := os.join_path(incbin_workspace, 'main.v')
build := os.exec([incbin_vexe, '-prod', '-cc', 'tcc', '-no-retry-compilation', '-o', exe,
main_file])
assert build.exit_code != 0, build.output
assert build.output.contains('tcc is not allowed with -prod'), build.output
assert !os.exists(exe + $if windows { '.exe' } $else { '' })
}
fn test_macos_tcc_prod_build_is_rejected_before_creating_output() {
$if macos {
bundled_tcc := os.join_path(@VEXEROOT, 'thirdparty', 'tcc', 'tcc.exe')
if !os.is_file(bundled_tcc) {
return
}
build, output := build_and_run('app_tcc', '-cc tcc -no-retry-compilation')
assert output == expected_output()
assert !build.output.contains('.S -o'), build.output
assert_tcc_prod_build_rejected('app_tcc_prod_rejected')
}
}
fn test_windows_tcc_build_keeps_the_array_form() {
fn test_windows_tcc_prod_build_is_rejected_before_creating_output() {
$if windows {
bundled_tcc := os.join_path(@VEXEROOT, 'thirdparty', 'tcc', 'tcc.exe')
if !os.is_file(bundled_tcc) {
return
}
// `-no-retry-compilation` turns off the retry that would hide the problem: TCC
// rejects the object the host assembler produced for the payload, so with the
// incbin path this build only succeeds through a fallback to that host.
build, output := build_and_run('app_tcc', '-cc tcc -no-retry-compilation')
assert output == expected_output()
assert !build.output.contains('.S -o'), build.output
assert_tcc_prod_build_rejected('app_tcc_prod_rejected')
}
}
+6 -14
View File
@@ -199,11 +199,6 @@ fn test_v_self_accepts_fastc_backend() {
assert v2_repeated_build.output.count('V self compiling') == 2, v2_repeated_build.output
assert os.is_executable(deep_self_output)
prod_build := cmdexec.run(@VEXE, ['self', '-silent', '-prod', '-b', 'fastc', '-o',
os.join_path(root, 'v prod')])
assert prod_build.exit_code != 0
assert prod_build.output.contains('does not support `-prod`'), prod_build.output
source := os.join_path(root, 'main.v')
write_fastc_test_source(source, 'module main\nfn main() { println(42) }\n')
program := os.join_path(root, 'program')
@@ -277,8 +272,9 @@ fn test_fastc_backend_parses_directly_to_c_without_ast_fallback() {
os.rmdir_all(root) or {}
}
v3_bin := os.join_path(root, 'v3')
build := cmdexec.run(@VEXE, ['-gc', 'none', '-path', '${fastc_backend_vlib_dir}|@vlib|@vmodules',
'-o', v3_bin, fastc_backend_v3_source])
// A standalone vlib/v/v.v compiler includes the fastc backend only when it is built with it.
build := cmdexec.run(@VEXE, ['-gc', 'none', '-compile-backend', 'fastc', '-path',
'${fastc_backend_vlib_dir}|@vlib|@vmodules', '-o', v3_bin, fastc_backend_v3_source])
assert build.exit_code == 0, build.output
collision_source := os.join_path(root, 'collision.v')
@@ -311,8 +307,9 @@ fn main() {
}
")
valid_binary := os.join_path(root, 'valid')
valid_compile := cmdexec.run(v3_bin, ['-macos-v3-compat-c99', '-b', 'fastc', '-o', valid_binary,
valid_source])
// Step lines are printed only with -show-timings (see the driver's bench setup).
valid_compile := cmdexec.run(v3_bin, ['-macos-v3-compat-c99', '-show-timings', '-b', 'fastc',
'-o', valid_binary, valid_source])
assert valid_compile.exit_code == 0, valid_compile.output
assert valid_compile.output.contains('fastc parse+gen'), valid_compile.output
assert !valid_compile.output.contains(' check'), valid_compile.output
@@ -778,11 +775,6 @@ fn main() {
}
for invocation in [
UnsupportedFastCInvocation{
args: ['-silent', '-prod', '-b', 'fastc', '-o', os.join_path(root, 'prod'),
valid_source]
expected: 'fastc parser does not support `-prod`'
},
UnsupportedFastCInvocation{
args: ['-silent', '-b', 'fastc', '-d', 'no_main', '-o', no_main_output, valid_source]
expected: 'fastc parser does not support `-d no_main`'
+78
View File
@@ -0,0 +1,78 @@
import os
// The invocations below stop before any C is generated, so they run on every host.
// `-prod` refuses TinyCC, and FastC refuses `-prod` in both `v` and `v self`.
fn prod_refusal_root(name string) string {
root := os.join_path(os.vtmp_dir(), 'v_prod_refusal_${name}_${os.getpid()}')
os.rmdir_all(root) or {}
os.mkdir_all(root) or { panic(err) }
os.write_file(os.join_path(root, 'main.v'), 'module main\nfn main() { println(42) }\n') or {
panic(err)
}
return root
}
fn test_prod_refuses_tinycc_before_any_compiler_runs() {
root := prod_refusal_root('tinycc')
defer {
os.rmdir_all(root) or {}
}
source := os.join_path(root, 'main.v')
for args in [['-prod', '-cc', 'tcc'], ['-cc=tinyc', '-prod'],
['-old-compiler', '-prod', '-cc', 'tcc']] {
result := os.exec([@VEXE, ...args, '-o', os.join_path(root, 'main'), source])
assert result.exit_code == 1, '${args}: ${result.output}'
assert result.output.contains('tcc is not allowed with -prod'), '${args}: ${result.output}'
assert (os.ls(root) or { []string{} }) == ['main.v'], '${args}'
}
}
fn test_prod_refuses_fastc_before_it_writes_output() {
root := prod_refusal_root('fastc')
defer {
os.rmdir_all(root) or {}
}
source := os.join_path(root, 'main.v')
for output in ['prod', 'prod.c'] {
result := os.exec([@VEXE, '-silent', '-prod', '-b', 'fastc', '-o', os.join_path(root, output),
source])
assert result.exit_code != 0, result.output
assert result.output.contains('fastc parser does not support `-prod`'), result.output
assert (os.ls(root) or { []string{} }) == ['main.v'], output
}
}
fn test_self_refuses_prod_with_fastc() {
root := prod_refusal_root('self')
defer {
os.rmdir_all(root) or {}
}
result := os.exec([@VEXE, 'self', '-silent', '-prod', '-b', 'fastc', '-o',
os.join_path(root, 'v prod')])
assert result.exit_code != 0, result.output
assert result.output.contains('does not support `-prod`'), result.output
assert (os.ls(root) or { []string{} }) == ['main.v'], result.output
}
fn test_prod_tinycc_flags_allow_information_commands() {
for command in ['version', 'help'] {
result := os.exec([@VEXE, '-prod', '-cc', 'tcc', command])
assert result.exit_code == 0, result.output
assert !result.output.contains('tcc is not allowed with -prod'), result.output
}
}
fn test_old_compiler_prod_refuses_a_tinycc_alias() {
$if windows {
return
}
root := prod_refusal_root('alias')
defer { os.rmdir_all(root) or {} }
alias := os.join_path(root, 'cc')
os.write_file(alias, '#!/bin/sh\nprintf "tcc version 0.9.27\\n"\n')!
os.chmod(alias, 0o700)!
result := os.exec([@VEXE, '-old-compiler', '-prod', '-cc', alias, os.join_path(root, 'main.v')])
assert result.exit_code == 1, result.output
assert result.output.contains('tcc is not allowed with -prod'), result.output
}
+55 -3
View File
@@ -135,7 +135,7 @@ fn test_module_cache_refresh_reads_only_project_flags_in_refresh_children() ! {
assert v3_cache_refresh_project_flags()! == []cache_refresh.FlagGroup{}
}
fn test_cached_tcc_main_command_preserves_production_optimization() ! {
fn test_cached_tcc_main_command_refuses_production_mode() ! {
compiler := v3_driver_test_executable()
if !os.is_executable(os.join_path(os.dir(compiler), 'thirdparty', 'tcc', 'tcc.exe')) { return }
root := os.join_path(os.vtmp_dir(), 'vup_tcc_optimization_${os.getpid()}')
@@ -154,7 +154,14 @@ fn test_cached_tcc_main_command_preserves_production_optimization() ! {
for name in ['VFLAGS', 'CFLAGS', 'LDFLAGS', 'V3_CACHE_REFRESH'] { os.unsetenv(name) }
source := os.join_path(root, 'main.v')
os.write_file(source, 'import math\nfn main() { println(math.abs(-3.0)) }\n')!
for mode in [[]string{}, ['-prod'], ['-prod', '-no-prod-options']] {
// TinyCC is not a -prod compiler, so the same build with -prod stops before compiling.
for mode in [['-prod'], ['-prod', '-no-prod-options']] {
refused := os.exec([compiler, '-cc', 'tcc', '-gc', 'none', '-showcc', ...mode, '-o',
os.join_path(root, 'program'), source])
assert refused.exit_code == 1, refused.output
assert refused.output.contains('tcc is not allowed with -prod'), refused.output
}
for mode in [[]string{}] {
result := os.exec([compiler, '-cc', 'tcc', '-gc', 'none', '-showcc', ...mode, '-o',
os.join_path(root, 'program'), source])
assert result.exit_code == 0, result.output
@@ -553,7 +560,8 @@ fn test_v3_bundled_tcc_probe_eligibility() {
host_target: windows_target
target: windows_target
})
// An explicit `-cc tcc` still wins with -prod on Windows.
// The probe still finds the bundled TinyCC for an explicit `-cc tcc` with -prod on Windows;
// the driver then refuses that build (see v3_tcc_prod_rejected).
assert v3_should_probe_bundled_tcc(V3BundledTccProbeOptions{
...base
is_prod: true
@@ -565,6 +573,50 @@ fn test_v3_bundled_tcc_probe_eligibility() {
})
}
fn test_effective_c_compiler_name_does_not_read_tinycc_from_names_that_only_contain_tcc() {
root := os.join_path(os.vtmp_dir(), 'v3_notcc_driver_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer {
os.rmdir_all(root) or {}
}
// The V executable is a real compiler driver that is not TinyCC.
not_tinycc := os.join_path(root, 'distcc.exe')
os.cp(@VEXE, not_tinycc) or { panic(err) }
assert effective_c_compiler_name(not_tinycc, pref.host_target()) != 'tinyc'
}
fn test_v3_tcc_prod_rejected_only_for_a_prod_c_build_that_resolves_to_tinycc() {
assert v3_tcc_prod_rejected(true, 'c', 'tinyc', 'tcc')
assert !v3_tcc_prod_rejected(true, 'c', 'gcc', 'gcc')
assert !v3_tcc_prod_rejected(true, 'c', 'clang', 'clang')
assert !v3_tcc_prod_rejected(true, 'arm64', 'tinyc', 'tcc')
assert !v3_tcc_prod_rejected(false, 'c', 'tinyc', 'tcc')
}
fn test_v3_tcc_prod_rejected_checks_actual_banner_for_known_compiler_names() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v3_prod_compiler_banner_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer { os.rmdir_all(root) or {} }
for name in ['clang-alias', 'gcc-alias'] {
alias := os.join_path(root, name)
os.write_file(alias, '#!/bin/sh\nprintf "tcc version 0.9.27\\n"\n')!
os.chmod(alias, 0o700)!
effective := effective_c_compiler_name(alias, pref.host_target())
assert effective != 'tinyc'
assert v3_tcc_prod_rejected(true, 'c', effective, alias)
assert !v3_tcc_prod_rejected(false, 'c', effective, alias)
assert !v3_tcc_prod_rejected(true, 'arm64', effective, alias)
}
clang := os.join_path(root, 'cc')
os.write_file(clang, '#!/bin/sh\nprintf "clang version 19.1.7\\nInstalledDir: /opt/tinycc-tcc version-compatible-sdk/bin\\n"\n')!
os.chmod(clang, 0o700)!
assert effective_c_compiler_name(clang, pref.host_target()) == 'clang'
assert !v3_tcc_prod_rejected(true, 'c', 'clang', clang)
}
fn test_v3_windows_prod_msvc_ready_needs_a_matching_developer_environment() {
ready := V3WindowsProdToolchain{
cl: 'C:/VS/bin/cl.exe'
+32 -2
View File
@@ -6680,6 +6680,20 @@ fn incremental_top_level_nodes(a &flat.FlatAst) []bool {
return result
}
fn program_has_translated_module_attributes(a &flat.FlatAst) bool {
for node in a.nodes {
if node.kind != .directive || !node.value.starts_with('@attributes:') {
continue
}
idx := node.value.all_after('@attributes:').int()
if idx >= 0 && idx < a.nodes.len && a.nodes[idx].kind == .module_decl
&& node.generic_params().any(it.all_before(':').trim_space() == 'translated') {
return true
}
}
return false
}
fn incremental_declaration_attribute_signatures(a &flat.FlatAst) map[int]string {
mut result := map[int]string{}
for node in a.nodes {
@@ -7675,6 +7689,7 @@ fn promote_scoped_type_metadata(mut tc types.TypeChecker) {
tc.enum_fields = clone_string_list_map(tc.enum_fields)
tc.flag_enums = clone_string_bool_map(tc.flag_enums)
tc.interface_names = clone_string_bool_map(tc.interface_names)
tc.interface_generic_params = clone_string_list_map(tc.interface_generic_params)
tc.interface_fields = clone_struct_field_map(tc.interface_fields)
tc.interface_embeds = clone_string_list_map(tc.interface_embeds)
tc.interface_abstract_methods = clone_string_list_map(tc.interface_abstract_methods)
@@ -7970,7 +7985,7 @@ fn effective_c_compiler_name(compiler string, target pref.Target) string {
compiler_path := os.find_abs_path_of_executable(compiler) or { compiler }
resolved_path := os.real_path(compiler_path)
name := os.file_name(resolved_path).to_lower_ascii()
if name.contains('tcc') || name.contains('tinyc') {
if pref.tinycc_file_name(name) {
return 'tinyc'
}
if name.contains('clang') {
@@ -7995,7 +8010,7 @@ fn effective_c_compiler_name(compiler string, target pref.Target) string {
}
compiler_result := cmdexec.run(compiler_path, ['--version'])
version := compiler_result.output.to_lower_ascii()
if version.contains('tiny c compiler') || version.contains('tcc version') {
if pref.ccompiler_version_is_tinycc(version) {
return 'tinyc'
}
if version.contains('clang') {
@@ -8027,6 +8042,14 @@ fn v3_select_implicit_c_compiler(c_compiler string, c_compiler_explicit bool, im
return c_compiler
}
// v3_tcc_prod_rejected reports whether a `-prod` C build would compile with TinyCC.
// Probe the actual compiler for production builds because its name may suggest Clang or GCC.
// FastC refuses `-prod` on its own, earlier.
fn v3_tcc_prod_rejected(is_prod bool, backend string, effective_c_compiler string, c_compiler string) bool {
return is_prod && backend == 'c'
&& (effective_c_compiler == 'tinyc' || pref.ccompiler_is_tinycc(c_compiler))
}
// V3CCompilerSelection is the C compiler that receives a compilation's first
// build attempt, and the name (`tinyc`, `gcc`, `clang`, ...) the generated code
// and the `// vtest build:` facts describe it by.
@@ -11437,6 +11460,10 @@ pub fn run(args []string) {
c_compiler = selection.c_compiler
use_implicit_tcc_semantics := selection.use_implicit_tcc_semantics
effective_c_compiler := selection.effective_c_compiler
if v3_tcc_prod_rejected(is_prod, backend, effective_c_compiler, c_compiler) {
eprintln('tcc is not allowed with -prod')
exit(1)
}
if effective_c_compiler == 'msvc' && target.os != 'windows' && !output_cross_c {
eprintln('MSVC requires a Windows target; cannot compile target `${target.os}`')
exit(1)
@@ -12624,7 +12651,10 @@ pub fn run(args []string) {
}
use_macos_dev_program_cache := backend == 'c' && program_cache_enabled && !is_prod && !is_shared
&& !is_selfhost && prefs.normalized_target_os() == 'macos'
// Translated body edits can require new integer-formatting helpers and map
// declarations that the prior prefix did not contain. Rebuild that prefix.
incremental_cache_enabled := use_macos_dev_program_cache
&& !translated_mode && !program_has_translated_module_attributes(a)
&& os.getenv('V3_CACHE_DISABLE_INCREMENTAL') != '1'
mut generic_cache_inputs_ready := false
mut incremental_snapshot := V3IncrementalSnapshot{}
+32
View File
@@ -728,6 +728,38 @@ fn test_clone_after_transform_keeps_cached_vlib_flag_ownership() {
assert flags == []
}
fn test_scoped_type_metadata_preserves_generic_interface_parameters() {
$if !prealloc {
return
}
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.interface_names['Store'] = true
tc.fn_ret_types['Store[T].get'] = types.Type(types.int_)
scope := prealloc_scope_begin_for_v3()
tc.interface_generic_params['Store'.clone()] = ['T'.clone()]
prealloc_scope_leave_for_v3(scope)
promote_scoped_type_metadata(mut tc)
params := tc.interface_generic_params['Store']
assert !scoped_value_owned(scope, params.data)
assert !scoped_value_owned(scope, params[0].str)
prealloc_scope_free_for_v3(scope)
key := tc.interface_method_signature_key('Store', 'get') or { panic('missing interface method') }
assert key == 'Store[T].get'
}
fn test_translated_module_attributes_are_detected_for_prefix_regeneration() {
mut a := flat.FlatAst.new()
id := a.add_node(flat.Node{ kind: .module_decl, value: 'main' })
assert !program_has_translated_module_attributes(&a)
a.add_node(flat.Node{
kind: .directive
value: '@attributes:${int(id)}'
payload: flat.node_payload(['translated'])
})
assert program_has_translated_module_attributes(&a)
}
fn test_scoped_ast_storage_copy_preserves_persistent_text_and_promotes_retiring_payloads() {
$if !prealloc {
return
+13
View File
@@ -407,6 +407,19 @@ fn (mut g FlatGen) finish_cache_declaration_demand(const_code string) {
g.cache_collect_declaration_refs(payload)
}
mut seen := map[string]bool{}
// Fixed-array typedefs are emitted even when their only use is an optional
// payload or an alias. Their element layouts must survive declaration pruning.
for _, info in g.collect_fixed_array_typedefs_needed() {
g.cache_require_declaration_type(info.arr, mut seen)
}
// Shared wrappers store their payload by value, including wrappers collected
// from fields that were only visited by compile-time reflection.
old_module := g.tc.cur_module
for _, info in g.shared_type_names {
g.tc.cur_module = info.module
g.cache_require_declaration_type(g.tc.parse_type(info.inner), mut seen)
}
g.tc.cur_module = old_module
for typ in g.multi_return_types {
g.cache_require_declaration_type(typ, mut seen)
}
+126
View File
@@ -3,6 +3,68 @@ module c
import v.flat
import v.types
fn test_final_cached_signatures_declare_tuple_payloads_before_wrappers() {
for is_result in [false, true] {
mut a := flat.FlatAst.new()
fn_id := a.add_node(flat.Node{ kind: .fn_decl, value: 'pair' })
mut tc := types.TypeChecker.new(&a)
tc.structs['dep.Box'] = [types.StructField{
name: 'child'
typ: types.Struct{ name: 'dep.Child' }
}]
tc.structs['dep.Child'] = [types.StructField{
name: 'value'
typ: types.int_
}]
tc.structs['dep.Unused'] = []types.StructField{}
payload := types.Type(types.MultiReturn{
types: [types.Type(types.Struct{ name: 'dep.Box' }), types.Type(types.string_)]
})
tc.fn_ret_types['dep.pair'] = if is_result {
types.Type(types.ResultType{ base_type: payload })
} else {
types.Type(types.OptionType{ base_type: payload })
}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cache_decl_demand = true
g.decl_types_ready = true
g.optional_types_ready = true
g.multi_return_types_ready = true
g.fn_gen_items = [
FlatFnGenItem{ node_id: fn_id, module: 'dep', file: 'dep.v', c_name: 'dep__pair' },
]
g.gen_type_declaration_block()
output := g.sb.str()
child_position := output.index('struct dep__Child {') or { -1 }
box_position := output.index('struct dep__Box {') or { -1 }
tuple_position := output.index('struct multi_return_dep__Box_string {') or { -1 }
wrapper := if is_result { '__v_result' } else { '__v_option' }
wrapper_position := output.index('typedef struct ${wrapper}_multi_return_dep__Box_string') or {
-1
}
assert child_position >= 0, output
assert box_position > child_position, output
assert tuple_position > box_position, output
assert wrapper_position > tuple_position, output
assert !output.contains('dep__Unused'), output
}
}
fn test_cached_header_retains_its_vlib_source_classification() {
mut a := flat.FlatAst.new()
a.cached_header_sources['/cache/builtin.vh'] = '/repo/vlib/builtin/array.v'
a.cached_header_sources['/cache/user.vh'] = '/repo/project/user.v'
mut g := FlatGen.new()
g.a = &a
g.compiler_vroot = '/repo'
assert g.c_source_file_is_in_vlib('/cache/builtin.vh')
assert g.c_source_file_is_in_vlib('/repo/vlib/builtin/array.v')
assert !g.c_source_file_is_in_vlib('/cache/user.vh')
assert !g.c_source_file_is_in_vlib('/repo/project/user.v')
}
fn test_cache_declaration_references_ignore_literals_and_comments() {
mut g := FlatGen.new()
g.cache_collect_declaration_refs('needed("quoted \\\" ignored", \'x\'); /* hidden */ // hidden_too\nother_2;')
@@ -47,6 +109,70 @@ fn test_cache_declarations_include_nested_payloads_and_recursive_fields() {
assert 'sample.Unused' !in names
}
fn test_cache_demand_keeps_fixed_array_alias_element_layouts() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.structs['sample.Box'] = []types.StructField{}
tc.structs['sample.Unused'] = []types.StructField{}
tc.type_aliases['sample.Boxes'] = '[2]sample.Box'
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cache_decl_demand = true
g.finish_cache_declaration_demand('')
names := g.c_struct_decl_names()
assert 'sample.Box' in names, names.str()
assert 'sample.Unused' !in names, names.str()
}
fn test_cache_demand_keeps_shared_wrapper_payload_layouts() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.structs['sample.Box'] = []types.StructField{}
tc.struct_modules['sample.Box'] = 'sample'
tc.structs['sample.Unused'] = []types.StructField{}
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.cache_decl_demand = true
g.register_shared_type_name('sample.Box', 'sample')
g.finish_cache_declaration_demand('')
names := g.c_struct_decl_names()
assert 'sample.Box' in names, names.str()
assert 'sample.Unused' !in names, names.str()
}
fn test_cache_map_key_helpers_follow_their_builtin_prototypes() {
mut a := flat.FlatAst.new()
file_id := a.add_val(.file, 'builtin.vh')
module_id := a.add_val(.module_decl, 'builtin')
fn_id := a.add_node(flat.Node{
kind: .fn_decl
value: 'fast_string_eq'
typ: 'bool'
is_mut: true
})
mut tc := types.TypeChecker.new(&a)
tc.fn_ret_types['fast_string_eq'] = types.bool_
mut g := FlatGen.new()
g.a = &a
g.tc = &tc
g.top_level_node_ids = [int(file_id), int(module_id), int(fn_id)]
g.cache_decl_demand = true
g.fixed_array_map_key_types['Array_fixed_string_2'] = types.ArrayFixed{
elem_type: types.Type(types.string_)
len: 2
}
g.gen_pre_body_support_declarations()
source := g.sb.str()
prototype := source.index('bool fast_string_eq(') or { panic(source) }
definition := source.index('static u64 Array_fixed_string_2_map_key_hash(void* pkey) {') or {
panic(source)
}
assert prototype < definition, source
assert source.contains('fast_string_eq((*left)[i], (*right)[i])'), source
}
fn test_cache_signature_collection_omits_unused_function_payloads() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
+27 -2
View File
@@ -4038,9 +4038,19 @@ fn (mut g FlatGen) write_parallel_cc_unit_marker() {
fn (mut g FlatGen) gen_pre_body_support_declarations() {
g.fixed_array_map_key_forward_decls()
support_start := g.sb.len
g.fixed_array_map_key_definitions()
g.gen_ownership_recursive_drop_helpers()
g.cached_header_forward_decls()
if g.cache_decl_demand {
// Support helpers can be the only callers of cached builtin functions.
// Select and emit those prototypes before the helper definitions.
support := g.sb.cut_to(support_start)
g.cache_collect_declaration_refs(support)
g.cached_header_forward_decls()
g.write(support)
} else {
g.cached_header_forward_decls()
}
g.interface_method_forward_decls()
g.shared_dup_fns()
if !g.skip_enum_autostr {
@@ -4135,6 +4145,15 @@ fn (mut g FlatGen) gen_global_declaration_block() {
// immutable after cgen preparation. Parallel self-host cgen can build it on the
// caller while the persistent worker pool emits function bodies.
fn (mut g FlatGen) gen_type_declaration_block() {
// The final function selection can add wrapped tuple signatures after cache
// demand collection. Discover them before emitting their payload layouts.
g.collect_selected_declaration_signature_types()
if g.cache_decl_demand {
mut seen := map[string]bool{}
for typ in g.multi_return_types {
g.cache_require_declaration_type(typ, mut seen)
}
}
g.enum_decls()
g.type_forward_decls()
g.type_alias_decls(false)
@@ -5516,7 +5535,12 @@ fn (g &FlatGen) c_source_file_is_in_vlib(source_file string) bool {
if root.len == 0 {
return false
}
return source_file.replace('\\', '/').starts_with('${root}/vlib/')
original_file := if !isnil(g.a) {
g.a.cached_header_sources[source_file] or { source_file }
} else {
source_file
}
return original_file.replace('\\', '/').starts_with('${root}/vlib/')
}
// note_c_include_directive records that a file (and, for user code, its module)
@@ -19251,6 +19275,7 @@ fn (mut g FlatGen) portable_native_runtime_helpers() {
.replace('defined(V_GCBOEHM_NATIVE)', 'defined(CUSTOM_DEFINE_gcboehm)')
.replace('#include <gc.h>', '#include <gc/gc.h>')
.replace('#include "segfault_handler_nix.h"', signal_header)
.replace('#define V_SEGFAULT_HANDLER_SCOPE\n', '#define V_SEGFAULT_HANDLER_SCOPE static\n')
.replace('void* v_thread_', 'static void* v_thread_')
.replace('void v_thread_', 'static void v_thread_'))
g.writeln('#endif')
@@ -39,7 +39,9 @@ fn main() {
mut direct := log.ThreadSafeLog{}
assert direct.get_level() == .debug
log.use_stdout()
assert log.get_level() == .debug
slevel := \$d("log_default_level", "info")
expected := log.level_from_tag(slevel.to_upper()) or { panic("invalid log level") }
assert log.get_level() == expected
log.info("logger-defaults-ok")
}
'
+8
View File
@@ -57,6 +57,14 @@ fn main() {
}
')!
// Cached cross builds also link native helper objects; the embedded installer
// must not export a second copy of the native signal-handler symbol.
for _ in 0 .. 2 {
cached := cmdexec.run('env', ['V3CACHE=${os.join_path(root, 'cache')}', @VEXE, '-cc', cc,
'-gc', 'none', '-prealloc', '-os', 'cross', 'run', source])
assert cached.exit_code == 0, cached.output
assert cached.output.trim_space() == 'portable', cached.output
}
generated := cmdexec.run(@VEXE, ['-gc', 'none', '-prealloc', '-nocache', '-os', 'cross', '-o',
c_source, source])
assert generated.exit_code == 0, generated.output
+18 -12
View File
@@ -7572,26 +7572,32 @@ fn (mut g FlatGen) gen_decl_assign(node flat.Node) {
}
if node.typ.len > 0 {
decl_type := g.tc.parse_resolution_type(node.typ)
if _ := array_like_type(decl_type) {
rhs_v_type = decl_type
} else if _ := array_fixed_type(decl_type) {
rhs_v_type = decl_type
if !g.type_contains_generic_placeholder(decl_type) {
if _ := array_like_type(decl_type) {
rhs_v_type = decl_type
} else if _ := array_fixed_type(decl_type) {
rhs_v_type = decl_type
}
}
}
if lhs.typ.len > 0 {
lhs_decl_type := g.parse_node_type(&lhs)
if _ := array_like_type(lhs_decl_type) {
rhs_v_type = lhs_decl_type
} else if _ := array_fixed_type(lhs_decl_type) {
rhs_v_type = lhs_decl_type
if !g.type_contains_generic_placeholder(lhs_decl_type) {
if _ := array_like_type(lhs_decl_type) {
rhs_v_type = lhs_decl_type
} else if _ := array_fixed_type(lhs_decl_type) {
rhs_v_type = lhs_decl_type
}
}
}
if lhs.kind == .ident {
lhs_type := g.usable_expr_type(lhs_id)
if _ := array_like_type(lhs_type) {
rhs_v_type = lhs_type
} else if _ := array_fixed_type(lhs_type) {
rhs_v_type = lhs_type
if !g.type_contains_generic_placeholder(lhs_type) {
if _ := array_like_type(lhs_type) {
rhs_v_type = lhs_type
} else if _ := array_fixed_type(lhs_type) {
rhs_v_type = lhs_type
}
}
}
if arr := array_like_type(rhs_v_type) {
+4
View File
@@ -1834,6 +1834,10 @@ fn generate_source_pieces(input_sources []FastcSourceFile, module_aliases map[st
mut global_sources := map[string]string{}
generic_method_sources := fastc_collect_generic_and_declaration_indexes(mut sources, prefs, mut declared_types, mut declared_kinds, mut enum_flags, mut params_structs, mut type_source_paths, mut type_sources, mut constants, mut public_constants, mut constant_sources, mut constant_spans, mut global_sources, mut globals, mut public_globals)!
timer.mark('declaration_indexes')
// builtin declares IError as `pub struct C.IError`, which the index does not record, so
// signatures such as `fn f() IError` in selfhost sources would be undeclared. The arm64 path
// records the same intrinsic error interface.
declared_types['IError'] = true
// The type declarations depend only on the declaration index, so they are
// rendered on a worker while the signatures are collected below; the
// composite typedefs that precede them need both and are rendered after.
+5
View File
@@ -141,6 +141,8 @@ NB: the build flags are shared with the run command too:
Compile the executable in production mode, where most optimizations are enabled.
Note that most V warnings turn to errors, if you pass -prod, so you will have
to fix them first.
TinyCC is not a -prod compiler: `-cc tcc`, or any compiler that resolves to TinyCC,
stops with `tcc is not allowed with -prod`. `-b fastc` is not supported with `-prod` either.
Note: gcc versions > 12.0 often have issues with inlining, that manifest with `-prod`.
To work around them, you can use `-prod -cflags -fno-inline-small-functions` .
@@ -377,3 +379,6 @@ For WebAssembly-specific build flags, use `v help build-wasm`.
See also:
`v help run` for documentation regarding `v run`.
Production C builds reject TinyCC, including executable aliases. This restriction does not
apply to help/version commands or non-C backends.
+3 -2
View File
@@ -36,8 +36,9 @@ Ownership dispatch preserves V3_PRIMARY_VEXE independently of the helper's VEXE.
Each module being cached is printed to stdout as `Caching module <name>...`.
Before each refresh build, progress shows the cache record index and total,
normal or production mode, program or test mode, and its original compiler flags.
For example: `> Refreshing module cache [1/4] (production tests; -cc tcc -prod).`
Equivalent builds and unavailable inputs are skipped, so progress indices can jump.
For example: `> Refreshing module cache [1/2] (normal program; -cc tcc).`
Equivalent builds, unavailable inputs and production TinyCC builds are skipped, so progress
indices can jump. TinyCC is not a `-prod` compiler.
The final summary reports how many distinct build configurations were checked.
The same module can be cached separately for different TinyCC build modes.
This also runs when the compiler itself is already updated. A cache refresh
+4 -4
View File
@@ -579,7 +579,7 @@ fn main() {
assert compilation.output.contains('VirtualProtect(ptr, size, PAGE_READWRITE, &tmp);')
}
fn test_windows_tcc_boehm_prod_does_not_emit_gc_remove_roots() {
fn test_windows_tcc_prod_is_refused_before_c_output() {
os.chdir(vroot) or {}
cc := windows_tcc_ccompiler_for_coutput_test()
if cc == '' {
@@ -601,10 +601,10 @@ fn main() {
defer {
os.rm(test_source) or {}
}
cmd := '${os.quoted_path(vexe)} -o - -os windows -cc ${cc} -prod ${os.quoted_path(test_source)}'
// TinyCC is not a -prod compiler, so the compilation stops before it emits any C.
compilation := os.exec([vexe, '-o', '-', '-os', 'windows', '-cc', cc, '-prod', test_source])
ensure_compilation_succeeded(compilation, cmd)
assert !compilation.output.contains('GC_remove_roots(')
assert compilation.exit_code != 0, compilation.output
assert compilation.output.contains('tcc is not allowed with -prod'), compilation.output
}
fn windows_tcc_ccompiler_for_coutput_test() string {
+54
View File
@@ -1,9 +1,63 @@
module markused
import os
import v.flat
import v.parser
import v.pref
import v.types
import v.token
fn test_warm_cache_keeps_ierror_methods_and_their_helpers() {
root := os.join_path(os.vtmp_dir(), 'cached_ierror_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
builtin_file := os.join_path(root, 'builtin.vh')
error_file := os.join_path(root, 'errors.v')
main_file := os.join_path(root, 'main.v')
os.write_file(builtin_file, 'module builtin\npub interface IError {\nmsg() string\ncode() int\n}\n')!
os.write_file(error_file, 'module errors
pub struct TestError {}
fn message_helper() string { return "message" }
fn code_helper() int { return 7 }
fn (e TestError) msg() string { return message_helper() }
fn (e TestError) code() int { return code_helper() }
fn unused_helper() {}
')!
os.write_file(main_file, 'import errors\nfn main() { _ := errors.TestError{} }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_files([builtin_file, error_file, main_file])
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.check_semantics()
assert tc.errors.len == 0, tc.errors.str()
used := mark_used_for_cache(a, &tc, []string{}, map[string]bool{})
assert used['errors.TestError.msg']
assert used['errors.TestError.code']
assert used['errors.message_helper']
assert used['errors.code_helper']
assert !used['errors.unused_helper']
}
fn test_all_functions_keeps_generic_calls_outside_literal_main() {
root := os.join_path(os.vtmp_dir(), 'all_fn_literal_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
builtin_file := os.join_path(root, 'builtin.v')
main_file := os.join_path(root, 'main.v')
os.write_file(builtin_file, 'module builtin\nfn generic_helper[T](value T) {}\nfn wrapper() { generic_helper(1) }\nfn println(value string) {}\n')!
os.write_file(main_file, 'fn main() { println("literal") }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_files([builtin_file, main_file])
mut tc := types.TypeChecker.new(a)
tc.collect(a)
tc.diagnostic_files[main_file] = true
assert is_trivial_literal_output_program(a, tc.diagnostic_files)
used, uses_generics := mark_all_used_with_generic_usage(a, &tc, []string{})
assert uses_generics
assert used['wrapper']
assert used['generic_helper']
}
fn test_collection_alias_methods_keep_declared_and_inherited_precedence() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
+14 -1
View File
@@ -280,7 +280,8 @@ pub fn reachable_const_exprs(a &flat.FlatAst, tc &types.TypeChecker, root_ids []
fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files map[string]bool, cache_modules map[string]bool, cache_mode bool, detect_generics bool, all_functions bool, allow_trivial_literal_output bool, prepared &PreparedMarkusedDecls, library_body_checker voidptr, library_check_parallel bool) (map[string]bool, bool) {
mut mu_sw := time.new_stopwatch()
trivial_literal_output := allow_trivial_literal_output && !cache_mode && cache_modules.len == 0
&& test_files.len == 0 && is_trivial_literal_output_program(a, tc.diagnostic_files)
&& test_files.len == 0 && !all_functions
&& is_trivial_literal_output_program(a, tc.diagnostic_files)
// An exact literal-output program has no user expressions or declarations that
// can instantiate a generic. Skip generic indexes and per-node generic checks,
// just as the known non-generic self-host path does.
@@ -551,6 +552,18 @@ fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files
}
queue << 'main'
used['main'] = true
if cache_mode {
// Cached IError dispatchers name every implementer's methods, even when
// their callers have no body in the warm AST. Cgen emits these methods,
// so retain their transformations and the helpers their bodies call.
for impl in tc.ierror_impl_names() {
for method in ['msg', 'code'] {
for alias in interface_implementer_method_aliases(impl, method, tc) {
enqueue(alias, mut used, mut queue)
}
}
}
}
$if ownership ? {
// Ownership cleanup and clone lowering emits direct C references to these
// process-wide IError sentinels. They have no source-AST reference for the
+206
View File
@@ -29,3 +29,209 @@ fn test_ccompiler_can_assemble_rejects_a_tcc_alias() {
found := find_system_assembler() or { panic('the fake Clang was not found') }
assert found == alias_path
}
fn test_tcc_with_prod_requested_needs_both_prod_and_a_tinycc_name() {
assert tcc_with_prod_requested(['-prod', '-cc', 'tcc', 'main.v'])
assert tcc_with_prod_requested(['-cc', 'C:/tools/tcc.exe', '-prod', 'main.v'])
assert tcc_with_prod_requested(['-cc=tinyc', '-prod', 'main.v'])
assert tcc_with_prod_requested(['-prod', '-cc', 'TCC.EXE', 'main.v'])
assert !tcc_with_prod_requested(['-cc', 'tcc', 'main.v'])
assert !tcc_with_prod_requested(['-prod', '-cc', 'clang', 'main.v'])
assert !tcc_with_prod_requested(['-prod', '-cc'])
assert !tcc_with_prod_requested(['-prod', 'main.v'])
// The last `-cc` wins, so a `VFLAGS=-cc tcc` can be overridden on the command line.
assert !tcc_with_prod_requested(['-cc', 'tcc', '-prod', '-cc', 'gcc', 'main.v'])
assert tcc_with_prod_requested(['-cc', 'gcc', '-prod', '-cc', 'tcc', 'main.v'])
}
fn test_tcc_prod_detection_does_not_match_unrelated_names_or_backends() {
assert !tcc_with_prod_requested(['-prod', '-cc', 'distcc'])
assert !tcc_with_prod_requested(['-prod', '-cc', 'notcc'])
assert !tcc_with_prod_requested(['-prod', '-cc', 'tcc', '-b', 'arm64'])
assert !tcc_with_prod_requested(['-prod', '-cc', 'tcc', '-b', 'fastc'])
assert tcc_with_prod_requested(['-prod', '-cc', 'tcc', '-b', 'arm64', '-b', 'c'])
assert !tcc_with_prod_requested(['-cc', '-prod'])
}
fn test_tcc_prod_detection_checks_compiler_aliases() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v_pref_prod_alias_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
alias := os.join_path(root, 'cc')
os.write_file(alias, '#!/bin/sh\nprintf "tcc version 0.9.27\\n"\n')!
os.chmod(alias, 0o700)!
assert tcc_with_prod_requested(['-prod', '-cc', alias])
os.write_file(alias, '#!/bin/sh\nprintf "clang version 18.0.0\\n"\n')!
assert !tcc_with_prod_requested(['-prod', '-cc', alias])
os.write_file(alias, '#!/bin/sh\nprintf "clang version 19.1.7\\nInstalledDir: /opt/tinycc-tcc version-compatible-sdk/bin\\n"\n')!
assert !ccompiler_is_tinycc(alias)
assert !tcc_with_prod_requested(['-prod', '-cc', alias])
assert ccompiler_can_assemble(alias)
}
fn test_ccompiler_version_is_tinycc_recognizes_banner_lines_only() {
for banner in ['tcc version 0.9.27 (arm64 Darwin)', 'TinyCC version 0.9.27',
'Tiny C Compiler 0.9.27', 'Tiny C Compiler - Copyright (C) 2001-2026',
'warning: wrapper\n tcc version 0.9.27\n', 'TinyCC'] {
assert ccompiler_version_is_tinycc(banner), banner
}
for version in ['clang version 19.1.7\nInstalledDir: /opt/tinycc-compatible-sdk/bin',
'clang version 19.1.7\nInstalledDir: /opt/tcc version-compatible-sdk/bin',
'gcc version 14.0\nConfigured with: --prefix=/opt/tiny c compiler-sdk',
'clang version 19.1.7 (supports TinyCC-compatible inputs)', 'notcc version 1.0'] {
assert !ccompiler_version_is_tinycc(version), version
}
}
fn test_tinycc_file_name_matches_tcc_names_only() {
for name in ['tcc', 'tcc.exe', 'TCC.EXE', 'tinyc', 'tinycc', 'x86_64-w64-mingw32-tcc'] {
assert tinycc_file_name(name), name
}
for name in ['distcc', 'notcc', 'notcc.exe', 'gcc', 'clang', 'cc'] {
assert !tinycc_file_name(name), name
}
}
fn test_tcc_with_prod_requested_finds_tinycc_under_another_name() {
bundled := os.join_path(@VEXEROOT, 'thirdparty', 'tcc', 'tcc.exe')
if !os.is_file(bundled) {
return
}
root := os.join_path(os.vtmp_dir(), 'v_pref_tinycc_alias_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer {
os.rmdir_all(root) or {}
}
alias_path := os.join_path(root, 'cc_alias.exe')
os.cp(bundled, alias_path) or { panic(err) }
assert tcc_with_prod_requested(['-prod', '-cc', alias_path])
assert !tcc_with_prod_requested(['-cc', alias_path])
}
fn test_tcc_with_prod_requested_ignores_other_executables_with_tcc_in_their_name() {
root := os.join_path(os.vtmp_dir(), 'v_pref_notcc_${os.getpid()}')
os.mkdir_all(root) or { panic(err) }
defer {
os.rmdir_all(root) or {}
}
// The V executable is a real compiler driver that is not TinyCC.
not_tinycc := os.join_path(root, 'notcc.exe')
os.cp(@VEXE, not_tinycc) or { panic(err) }
assert !tcc_with_prod_requested(['-prod', '-cc', not_tinycc])
assert !tcc_with_prod_requested(['-prod', '-cc', 'distcc'])
}
fn test_tcc_prod_detection_skips_option_values() {
assert !tcc_with_prod_requested(['-cc', 'tcc', '-o', '-prod', 'main.v'])
assert !tcc_with_prod_requested(['-prod', '-cc', 'clang', '-o', '-cc=tcc', 'main.v'])
assert tcc_with_prod_requested(['-prod', '-cc', 'tcc', '-o', '-b=arm64', 'main.v'])
assert tcc_with_prod_requested(['-profile', '-prod', '-cc', 'tcc', 'main.v'])
assert tcc_with_prod_requested(['-profile', 'report.txt', '-prod', '-cc', 'tcc', 'main.v'])
}
fn test_tcc_prod_detection_uses_recorded_wrapper_directory_without_chdir() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v_pref_cwd_${os.getpid()}')
project := os.join_path(root, 'tinycc-project')
os.mkdir_all(project)!
defer { os.rmdir_all(root) or {} }
wrapper := os.join_path(root, 'wrapper')
os.write_file(wrapper, '#!/bin/sh\ncase "$PWD" in\n*/tinycc-project) echo "tcc version 0.9.27" ;;\n*) echo "clang version 19.1.7" ;;\nesac\n')!
os.chmod(wrapper, 0o700)!
before := os.getwd()
for args in [['-prod', '-cc', wrapper], ['-prod', '-cc', '../wrapper'],
['-prod', '-cc=../wrapper']] {
assert tcc_with_prod_requested_in(args, project)
}
assert !tcc_with_prod_requested_in(['-prod', '-cc', wrapper], root)
assert !tcc_with_prod_requested_in(['-cc', wrapper], project)
assert !tcc_with_prod_requested_in(['-cc', wrapper, '-o', '-prod'], project)
assert os.getwd() == before
bare := 'pref_context_cc_${os.getpid()}'
os.cp(wrapper, os.join_path(project, bare))!
os.chmod(os.join_path(project, bare), 0o700)!
saved_path := os.getenv('PATH')
defer { os.setenv('PATH', saved_path, true) }
for prefix in ['.', ''] {
os.setenv('PATH', prefix + os.path_delimiter + saved_path, true)
assert tcc_with_prod_requested_in(['-prod', '-cc', bare], project)
assert !tcc_with_prod_requested_in(['-prod', '-cc', bare], root)
assert os.getenv('PATH') == prefix + os.path_delimiter + saved_path
}
os.mkdir_all(os.join_path(project, 'bin'))!
os.mv(os.join_path(project, bare), os.join_path(project, 'bin', bare))!
os.setenv('PATH', 'bin' + os.path_delimiter + saved_path, true)
assert tcc_with_prod_requested_in(['-prod', '-cc', bare], project)
assert os.getwd() == before
}
fn test_tcc_prod_detection_prefers_launchable_local_compiler_before_path() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'v_pref_local_cc_${os.getpid()}')
project := os.join_path(root, 'project')
bin := os.join_path(root, 'bin')
os.mkdir_all(project)!
os.mkdir_all(bin)!
saved_path := os.getenv('PATH')
defer {
os.setenv('PATH', saved_path, true)
os.rmdir_all(root) or {}
}
local := os.join_path(project, 'clang')
search := os.join_path(bin, 'clang')
os.write_file(local, '#!/bin/sh\necho "tcc version 0.9.27"\n')!
os.write_file(search, '#!/bin/sh\necho "clang version 19.1.7"\n')!
os.chmod(local, 0o700)!
os.chmod(search, 0o700)!
os.setenv('PATH', bin + os.path_delimiter + saved_path, true)
before := os.getwd()
assert ccompiler_path_in('clang', project) == local
assert tcc_with_prod_requested_in(['-prod', '-cc', 'clang'], project)
assert !tcc_with_prod_requested_in(['-prod', '-cc', 'clang'], root)
os.write_file(local, '#!/bin/sh\necho "clang version 19.1.7"\n')!
os.write_file(search, '#!/bin/sh\necho "tcc version 0.9.27"\n')!
assert !tcc_with_prod_requested_in(['-prod', '-cc', 'clang'], project)
assert tcc_with_prod_requested_in(['-prod', '-cc', 'clang'], root)
assert os.getwd() == before
}
fn test_ccompiler_windows_explicit_path_uses_exe_not_com_or_cmd() {
root := os.join_path(os.vtmp_dir(), 'v_pref_windows_paths_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
path := os.join_path(root, 'clang')
for suffix in ['', '.com', '.exe', '.cmd', '.bin'] {
os.write_file(path + suffix, 'dummy executable-path fixture')!
os.chmod(path + suffix, if suffix in ['.com', '.exe', '.cmd'] { 0o700 } else { 0o600 })!
}
assert ccompiler_windows_executable_path(path) == path + '.exe'
assert ccompiler_windows_executable_path(path + '.bin') == path + '.bin'
os.rm(path + '.exe')!
assert ccompiler_windows_executable_path(path) == path
os.rm(path)!
assert ccompiler_windows_executable_path(path) == path
$if windows {
assert os.real_path(ccompiler_path_in('./clang.bin', root)) == os.real_path(path + '.bin')
}
}
fn test_ccompiler_windows_percent_expansion_matches_launch_spelling() {
name := 'V_PREF_COMPILER_SDK_${os.getpid()}'
missing := 'V_PREF_COMPILER_UNSET_${os.getpid()}'
previous := os.getenv(name)
defer {
if previous == '' { os.unsetenv(name) } else { os.setenv(name, previous, true) }
}
os.setenv(name, 'C:/compiler sdk', true)
assert ccompiler_windows_expand_env_vars('%${name}%/clang.exe') == 'C:/compiler sdk/clang.exe'
assert ccompiler_windows_expand_env_vars('%${missing}%/clang.exe') == '%${missing}%/clang.exe'
assert ccompiler_windows_expand_env_vars('%%/clang.exe') == '%%/clang.exe'
assert ccompiler_windows_expand_env_vars('%${name}/clang.exe') == '%${name}/clang.exe'
}
+191 -3
View File
@@ -1,6 +1,7 @@
module pref
import os
import strings
import time
import v.cmdexec
@@ -1195,7 +1196,7 @@ pub fn normalized_arch(target_arch string) string {
// out an alias for tcc.
pub fn ccompiler_can_assemble(ccompiler string) bool {
real_name := os.file_name(os.real_path(ccompiler)).to_lower_ascii()
if real_name.contains('tcc') || real_name.contains('tinyc') || real_name.contains('msvc')
if tinycc_file_name(real_name) || real_name.contains('msvc')
|| real_name in ['cl', 'cl.exe'] {
return false
}
@@ -1203,8 +1204,7 @@ pub fn ccompiler_can_assemble(ccompiler string) bool {
for version_flag in ['--version', '-v'] {
res := os.exec([ccompiler, '${version_flag}'])
output := res.output.to_lower_ascii()
if output.contains('tiny c compiler') || output.contains('tinycc')
|| output.contains('\ntcc') || output.starts_with('tcc')
if ccompiler_version_is_tinycc(output)
|| output.contains('microsoft c/c++') || output.contains('msvc') {
return false
}
@@ -1216,6 +1216,194 @@ pub fn ccompiler_can_assemble(ccompiler string) bool {
return false
}
// tcc_with_prod_requested reports whether a production C build selects TinyCC.
// Compiler option values are skipped and the last compiler/backend options win.
// Executable aliases follow ccompiler_is_tinycc in the caller's working directory.
pub fn tcc_with_prod_requested(args []string) bool {
return tcc_with_prod_requested_in(args, '')
}
// tcc_with_prod_requested_in reports whether a production C build selects TinyCC in cwd.
// Relative compiler paths and wrapper version queries use cwd without changing the caller's
// working directory. Compiler option values are skipped and the last compiler/backend win.
pub fn tcc_with_prod_requested_in(args []string, cwd string) bool {
mut prod := false
mut ccompiler := ''
mut backend := 'c'
mut i := 0
for i < args.len {
arg := args[i]
if arg == '-prod' {
prod = true
} else if arg in ['-cc', '-b', '-backend'] && i + 1 < args.len {
i++
if arg == '-cc' { ccompiler = args[i] } else { backend = args[i] }
} else if arg.starts_with('-cc=') {
ccompiler = arg['-cc='.len..]
} else if arg.starts_with('-b=') {
backend = arg['-b='.len..]
} else if arg.starts_with('-backend=') {
backend = arg['-backend='.len..]
} else if arg in ['-prof', '-profile'] {
if prod_profile_consumes_value(args, i) { i++ }
} else if arg == '-cf' || option_may_consume_value(arg) {
i++
}
i++
}
return prod && backend == 'c' && ccompiler != '' && ccompiler_is_tinycc_in(ccompiler, cwd)
}
// prod_profile_consumes_value follows the launcher's optional profiling output rule.
fn prod_profile_consumes_value(args []string, index int) bool {
next := args[index + 1] or { return false }
if next == '-' { return true }
if next.starts_with('-') || next in ['run', 'build', 'test', 'doc']
|| next.ends_with('.v') || next.ends_with('.vv') || next.ends_with('.vsh') || os.is_dir(next) {
return false
}
return args[index + 2..].any(!it.starts_with('-'))
}
// ccompiler_is_tinycc reports whether `ccompiler` is TinyCC. It resolves the path and any
// symlink, then checks the file name with tinycc_file_name. Any other executable is asked for
// its version banner, so a copy of tcc under another name is still recognised. Mentions of
// TinyCC in metadata such as an installed directory do not identify a compiler. A name that
// does not resolve to an executable is judged by its name alone.
// Local launchable files precede PATH. Windows also permits loadable PE files with other extensions.
pub fn ccompiler_is_tinycc(ccompiler string) bool {
return ccompiler_is_tinycc_in(ccompiler, os.getwd())
}
fn ccompiler_is_tinycc_in(ccompiler string, cwd string) bool {
work_folder := if cwd == '' { os.getwd() } else { cwd }
path := ccompiler_path_in(ccompiler, work_folder)
if path == '' {
mut name := ccompiler
$if windows {
name = ccompiler_windows_expand_env_vars(name)
}
return tinycc_file_name(os.file_name(name))
}
resolved := os.real_path(path)
if tinycc_file_name(os.file_name(resolved)) {
return true
}
if !os.is_file(resolved) {
return false
}
$if !windows {
if !os.is_executable(resolved) { return false }
}
for flag in ['--version', '-v'] {
version := cmdexec.run_in(resolved, [flag], work_folder).output
if ccompiler_version_is_tinycc(version) {
return true
}
}
return false
}
// ccompiler_path_in resolves explicit paths and PATH entries in the requested directory.
// Match cmdexec's local-file and explicit-path rules before searching PATH.
fn ccompiler_path_in(ccompiler string, cwd string) string {
mut program := ccompiler
$if windows {
program = ccompiler_windows_expand_env_vars(program)
}
path := if os.is_abs_path(program) { program } else { os.join_path(cwd, program) }
if os.is_file(path) && os.is_executable(path) { return path }
mut explicit_path := os.is_abs_path(program) || program.contains('/')
$if windows {
explicit_path = explicit_path || program.contains('\\')
}
if explicit_path {
$if windows {
return ccompiler_windows_executable_path(path)
} $else {
return path
}
}
mut suffixes := ['']
$if windows {
suffixes = ['.com', '.exe', '.bat', '.cmd', '']
if os.file_ext(program).to_lower_ascii() in suffixes && os.file_ext(program) != '' {
suffixes = ['']
}
}
for suffix in suffixes {
for entry in os.getenv('PATH').split(os.path_delimiter) {
directory := if os.is_abs_path(entry) { entry } else { os.join_path(cwd, entry) }
candidate := os.join_path(directory, program + suffix)
if os.is_file(candidate) && os.is_executable(candidate) { return candidate }
}
}
return ''
}
// Explicit Windows paths have CreateProcessW's single implicit `.exe` suffix.
// Nonstandard extensions can still contain a loadable PE image.
fn ccompiler_windows_executable_path(candidate string) string {
if os.is_file(candidate) && os.is_executable(candidate) { return candidate }
if os.file_ext(candidate) != '' { return candidate }
probed := candidate + '.exe'
if os.is_file(probed) && os.is_executable(probed) { return probed }
return candidate
}
// Keep the same single-pass percent expansion as cmdexec's Windows launch path.
fn ccompiler_windows_expand_env_vars(text string) string {
if !text.contains('%') { return text }
parts := text.split('%')
mut out := strings.new_builder(text.len)
for i in 0 .. parts.len {
if i % 2 == 0 {
out.write_string(parts[i])
continue
}
if i == parts.len - 1 {
out.write_string('%')
out.write_string(parts[i])
continue
}
name := parts[i]
value := if name == '' { '' } else { os.getenv(name) }
if value != '' {
out.write_string(value)
continue
}
out.write_string('%')
out.write_string(name)
out.write_string('%')
}
return out.str()
}
// ccompiler_version_is_tinycc reports whether output contains a TinyCC version or branding
// banner line. Paths and other metadata containing the compiler's name are ignored.
pub fn ccompiler_version_is_tinycc(output string) bool {
for line in output.to_lower_ascii().split_into_lines() {
banner := line.trim_space()
if banner in ['tcc', 'tinycc', 'tiny c compiler']
|| banner.starts_with('tcc version ') || banner.starts_with('tinycc version ')
|| banner.starts_with('tiny c compiler ') {
return true
}
}
return false
}
// tinycc_file_name reports whether `file_name` is tcc or tinyc, with an optional `.exe` and an
// optional target prefix such as `x86_64-w64-mingw32-tcc`. A name that only contains tcc, such
// as `distcc` or `notcc`, is not TinyCC.
pub fn tinycc_file_name(file_name string) bool {
mut name := file_name.to_lower_ascii()
if name.ends_with('.exe') {
name = name[..name.len - '.exe'.len]
}
return name in ['tcc', 'tinyc', 'tinycc'] || name.ends_with('-tcc') || name.ends_with('-tinycc')
}
// find_system_assembler returns a GCC or Clang compatible compiler driver found
// on PATH, one that can assemble a preprocessed `.S` file, or none.
pub fn find_system_assembler() ?string {
@@ -59,7 +59,7 @@ fn test_empty_array_literal_before_logical_and_in_conditions() {
else { 100 }
}
assert hits == 3
assert 1 in [1] && flag
assert 1 in [1, 2] && flag
assert 1 !in [] && flag
}
@@ -98,8 +98,8 @@ fn test_comments_separate_logical_and_after_array_literals() {
assert a == [] && // separator
flag
assert a == [] /* separator */
&& flag
&& flag
fixed := [1]!
assert fixed == [1]! /* separator */
&& flag
&& flag
}
@@ -7,5 +7,5 @@ fn test_calling_functions_with_map_initializations_containing_arrays() {
'users': []string{}
'groups': []string{}
}, escape_unicode: true)
assert result == '{"users":[],"groups":[]}'
assert result == '{"groups":[],"users":[]}'
}
+69 -3
View File
@@ -238,7 +238,7 @@ fn test_default_cache_keeps_the_closure_runtime_when_module_bodies_are_not_parse
}
}
fn test_tcc_retries_programs_that_cannot_use_cached_declarations() {
fn test_tcc_uses_cached_declarations_for_anonymous_and_fixed_arrays() {
$if !linux && !macos {
return
}
@@ -257,10 +257,13 @@ fn test_tcc_retries_programs_that_cannot_use_cached_declarations() {
os.setenv('V3CACHE', os.join_path(root, 'cache${i}'), true)
for fixture in fixtures {
os.write_file(main_file, os.read_file(os.join_path(@VEXEROOT, 'vlib/v/tests', fixture))!)!
for _ in 0 .. 2 {
for iteration in 0 .. 2 {
output := build(root, flags, main_file, 'program')
assert output.contains('the bundled TinyCC could not compile or link the cached program'), output
assert !output.contains('the bundled TinyCC could not compile or link the cached program'), output
assert !output.contains('implicit tcc could not be used'), output
if iteration == 1 {
assert_reused_modules(output)
}
_ := run_built(root, 'program')
}
}
@@ -859,6 +862,69 @@ exec ${os.quoted_path(cc)} "$@"
return saved
}
fn test_warm_tuple_signatures_retain_nested_struct_payload_layouts() {
$if windows {
return
}
for flags in [['-cc', 'cc'], ['-cc', 'tcc']] {
root := new_project('module_cache_tuple_payload_${flags[1]}')
mut saved := pin_module_cache(os.join_path(root, 'cache'))
saved << save_env('VMODULES')
if flags[1] == 'cc' { saved << capture_cached_program_c(root) }
os.setenv('VMODULES', os.join_path(root, 'modules'), true)
defer {
for env in saved { env.restore() }
os.rmdir_all(root) or {}
}
os.mkdir_all(os.join_path(root, 'modules', 'cachetuple'))!
os.write_file(os.join_path(root, 'modules', 'cachetuple', 'cachetuple.v'), 'module cachetuple
pub struct Child {
pub:
value int
}
pub struct Box {
pub:
child Child
}
pub struct Unused { value int }
pub fn marker() int { return 1 }
pub fn optional_pair() ?(Box, string) { return Box{ child: Child{ value: 41 } }, "option" }
pub fn result_pair() !(Box, string) { return Box{ child: Child{ value: 42 } }, "result" }
')!
main_file := os.join_path(root, 'main.v')
os.write_file(main_file, 'import cachetuple\nfn main() { println(cachetuple.marker()) }\n')!
build(root, flags, main_file, 'cold')
assert run_built(root, 'cold') == '1'
published := module_objects(root)
program := 'import cachetuple
fn main() {
option_box, option_label := cachetuple.optional_pair() or { panic("none") }
result_box, result_label := cachetuple.result_pair() or { panic(err) }
println("\${option_box.child.value} \${option_label} \${result_box.child.value} \${result_label}")
}
'
for i in 0 .. 2 {
os.write_file(main_file, program + '\nfn added_${i}() {}\n')!
mut warm_flags := flags.clone()
if i == 1 { warm_flags << '-no-parallel' }
warm := build(root, warm_flags, main_file, 'warm')
assert_reused_modules(warm)
assert module_objects(root) == published
assert run_built(root, 'warm') == '41 option 42 result'
if flags[1] == 'cc' {
output := os.read_file(os.join_path(root, 'program.c'))!
child_position := output.index('struct cachetuple__Child {') or { -1 }
box_position := output.index('struct cachetuple__Box {') or { -1 }
tuple_position := output.index('struct multi_return_cachetuple__Box_string {') or { -1 }
assert child_position >= 0, output
assert box_position > child_position, output
assert tuple_position > box_position, output
assert !output.contains('struct cachetuple__Unused {'), output
}
}
}
}
fn test_unused_cached_option_and_result_constants_do_not_pull_in_payload_declarations() {
$if windows {
return
@@ -25,7 +25,7 @@ fn test_recursive_struct_equality_compares_content_at_every_depth() {
assert a != different
assert !(a == different)
assert [a] == [b]
assert a in [b]
assert a in [different, b]
left_map := {
'node': a
}
+12
View File
@@ -4,6 +4,18 @@ import v.flat
import v.token
import v.types
fn test_anonymous_struct_placeholders_inside_fixed_arrays_are_unresolved() {
for skip_generics in [false, true] {
t := Transformer{ skip_generics: skip_generics }
for typ in ['[2]struct', '[2][3]struct', '[][2]struct', '?[2]struct', 'map[string][2]struct'] {
assert t.generic_arg_is_unresolved(typ), typ
}
for typ in ['[2]int', '[2][3]string', '[][2]int', '?[2]int'] {
assert !t.generic_arg_is_unresolved(typ), typ
}
}
}
fn add_inferred_anonymous_struct(mut a flat.FlatAst, file_id int) (flat.NodeId, flat.NodeId) {
pos := token.new_pos(file_id, 0)
value_id := a.add_node(flat.Node{
+7 -1
View File
@@ -1128,7 +1128,13 @@ fn (mut t Transformer) transform_array_literal_for_type(id flat.NodeId, node fla
}
fn (mut t Transformer) transform_fixed_array_literal_for_type(_id flat.NodeId, node flat.Node, target_type string) ?flat.NodeId {
fixed_type := t.normalize_type_alias(target_type)
mut fixed_type := t.normalize_type_alias(target_type)
if t.generic_arg_is_unresolved(fixed_type) {
elem_type := t.array_literal_elem_type(node)
if decl_type_is_usable(elem_type) && !t.generic_arg_is_unresolved(elem_type) {
fixed_type = '[${fixed_array_len_text(fixed_type)}]${elem_type}'
}
}
if !t.is_fixed_array_type(fixed_type) {
return none
}
+35 -2
View File
@@ -6715,13 +6715,46 @@ fn auto_str_helper_name(aggregate string) string {
}
fn (t &Transformer) auto_str_helper_owner_module(aggregate string) string {
base_name := aggregate.all_before('[')
if !isnil(t.tc) {
if module_name := t.tc.struct_modules[aggregate] {
return module_name
}
if module_name := t.tc.type_alias_modules[aggregate] {
return module_name
}
// Bare interfaces belong to main or builtin, even when a library
// specialization requests their recursive stringification helper.
if !base_name.contains('.') {
for node in t.a.nodes {
if node.kind != .interface_decl || node.value != base_name {
continue
}
if source := t.a.source_files[node.pos.id] {
// Generated helper contexts can revisit a source file in a foreign
// namespace. The parsed file's module remains its declaration owner.
for file_node in t.a.nodes {
if file_node.kind == .file && file_node.children_count > 0
&& (file_node.value == source.name
|| t.a.real_source_path(file_node.value) == t.a.real_source_path(source.name)) {
module_name := t.file_module_name(file_node)
if module_name in ['', 'main', 'builtin'] {
return if module_name.len > 0 { module_name } else { 'main' }
}
}
}
module_name := t.tc.file_modules[source.name] or {
t.tc.file_modules[t.a.real_source_path(source.name)] or { continue }
}
if module_name in ['', 'main', 'builtin'] {
return module_name
}
}
}
}
}
if !aggregate.starts_with('C.') && aggregate.contains('.') {
return aggregate.all_before_last('.')
if !base_name.starts_with('C.') && base_name.contains('.') {
return base_name.all_before_last('.')
}
return if t.cur_module.len > 0 { t.cur_module } else { 'main' }
}
+41
View File
@@ -1,6 +1,7 @@
module transform
import v.flat
import v.token
import v.types
fn literal_binding_test_node(mut a flat.FlatAst, kind flat.NodeKind, children []flat.NodeId) flat.NodeId {
@@ -837,6 +838,46 @@ fn test_auto_str_helper_call_uses_type_owner_module() {
assert a.nodes[helper_idx - 1].value == 'token'
}
fn test_auto_str_helper_uses_interface_declaration_module() {
for owner in ['main', 'builtin', 'view'] {
mut a := flat.FlatAst.new()
file := '${owner}.v'
a.source_files[1] = token.File.unindexed(file, 1)
name := if owner == 'view' { 'view.View' } else { 'View' }
module_id := a.add_val(.module_decl, owner)
decl_id := a.add_node(flat.Node{
kind: .interface_decl
value: 'View'
pos: token.new_pos(1, 0)
})
file_children := a.children.len
a.children << module_id
a.children << decl_id
a.add_node(flat.Node{
kind: .file
value: file
children_start: i32(file_children)
children_count: 2
})
mut tc := types.TypeChecker.new(&a)
module_path := if owner == 'main' { a.real_source_path(file) } else { file }
tc.file_modules[module_path] = 'datatypes'
mut t := Transformer{
a: &a
tc: &tc
cur_module: 'datatypes'
cur_file: 'datatypes/linked_list.v'
}
value := t.make_ident('view')
t.stringify_stack << 'Wrapper'
for suffix in ['', '[int]', '[pkg.Item]'] {
application := name + suffix
t.request_auto_str_helper(value, application)
assert t.auto_str_types[application].helper_module == owner, application
}
}
}
fn test_default_clone_helper_drops_owned_rvalue_after_saving_clone() {
mut a := flat.FlatAst.new()
mut t := Transformer{
+6
View File
@@ -15673,6 +15673,12 @@ fn (t &Transformer) generic_arg_is_unresolved_uncached(arg string) bool {
if clean.starts_with('[]') {
return t.generic_arg_is_unresolved(clean[2..])
}
if clean.starts_with('[') {
bracket_end := generic_matching_bracket(clean, 0)
if bracket_end < clean.len {
return t.generic_arg_is_unresolved(clean[bracket_end + 1..])
}
}
if clean.starts_with('map[') {
bracket_end := generic_matching_bracket(clean, 3)
if bracket_end < clean.len {
+5
View File
@@ -372,6 +372,11 @@ fn (mut tc TypeChecker) check_import_symbol_conflict(id flat.NodeId, name string
}
fn (mut tc TypeChecker) check_import_symbol_conflict_at(id flat.NodeId, name string, pos token.Pos) {
// A cached header combines imports from different source files. Its names
// were already checked in their original file scopes before being cached.
if is_module_cache_header(tc.cur_file) {
return
}
if name == '' || name == '_' || !tc.has_active_import(name) {
return
}
+20
View File
@@ -5,6 +5,26 @@ import v.flat
import v.parser
import v.pref
fn test_cached_header_parameter_does_not_conflict_with_merged_imports() {
root := os.join_path(os.vtmp_dir(), 'v3_module_cache_import_params_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
for file_name in ['sample.v', 'sample.vh'] {
path := os.join_path(root, file_name)
os.write_file(path, 'module sample\nimport math.bits\npub fn prime(bits int) int { return bits }\n')!
mut p := parser.Parser.new(pref.new_preferences())
a := p.parse_file(path)
mut tc := TypeChecker.new(a)
tc.collect(a)
tc.check_semantics()
if file_name.ends_with('.vh') {
assert tc.errors.len == 0, tc.errors.str()
} else {
assert tc.errors.any(it.msg == 'duplicate of an import symbol `bits`'), tc.errors.str()
}
}
}
fn test_cached_module_header_uses_original_source_identity() {
root := os.join_path(os.vtmp_dir(), 'v3_cached_module_identity_${os.getpid()}')
source := os.join_path(root, 'app', 'html', 'html.v')
+11 -1
View File
@@ -6,6 +6,7 @@ import strconv
import strings
import time
import v.cmdexec
import v.pref
import v.tempname
const record_prefix = '.vup_refresh_'
@@ -17,6 +18,12 @@ const object_record_format = 'vup-cache-refresh-3'
const cwd_record_format = 'vup-cache-refresh-2'
const legacy_record_format = 'vup-cache-refresh-1'
// request_uses_prod_tcc applies the production restriction using the recorded compiler
// paths and working directory, including context-sensitive compiler wrappers.
fn request_uses_prod_tcc(request Request) bool {
return pref.tcc_with_prod_requested_in(request.args, request.cwd)
}
// FlagGroup preserves the ordering and deduplication of a project's C directives.
pub struct FlagGroup {
pub:
@@ -550,7 +557,10 @@ pub fn collect(vroot string, vexe string) ![]Request {
// Live entries whose project source identity changed are skipped until rebuilt and recorded.
// Legacy headers are marked migrated only after successful refreshes of their variants.
// Disk-full failures stop remaining builds; child output is forwarded without buffering it all.
pub fn refresh(vexe string, requests []Request) ! {
// TinyCC production builds are never refreshed: the compiler refuses `-cc tcc` with `-prod`,
// and records made before that rule still exist.
pub fn refresh(vexe string, all_requests []Request) ! {
requests := all_requests.filter(!request_uses_prod_tcc(it))
if requests.len == 0 { return }
dir := os.join_path(os.vtmp_dir(), 'vup_cache_refresh_${tempname.unique_token()}')
os.mkdir_all(dir)!
@@ -701,6 +701,8 @@ fn test_refresh_rebuilds_changed_vlib_dependencies_for_each_compiler_and_mode()
}
for mode in modes {
for prod in [false, true] {
// TinyCC is not a -prod compiler, so there is no production TinyCC build to check.
if compiler == 'tcc' && prod { continue }
mut flags := ['-cc', compiler, '-gc', 'none', '-path', 'modules/../modules|@vlib',
'-file-list', '../shared support.v,../implementers']
if prod { flags << '-prod' }
@@ -1015,3 +1017,52 @@ fn test_refresh_replays_and_clears_test_assert_display_modes() ! {
assert os.read_file(log)!.split_into_lines() == ['1', 'unset']
assert os.getenv('VTEST_SHOW_ASSERTS') == 'ambient'
}
fn test_refresh_skips_production_tinycc_alias_before_starting_builds() {
$if windows {
return
}
root := os.join_path(os.vtmp_dir(), 'cache_refresh_prod_tcc_${os.getpid()}')
os.mkdir_all(root)!
defer { os.rmdir_all(root) or {} }
alias := os.join_path(root, 'cc')
os.write_file(alias, '#!/bin/sh\nprintf "tcc version 0.9.27\\n"\n')!
os.chmod(alias, 0o700)!
request := Request{ args: ['-prod', '-cc', './cc'], cwd: root, module: 'builtin' }
assert request_uses_prod_tcc(request)
equals_request := Request{ ...request, args: ['-prod', '-cc=./cc'] }
assert request_uses_prod_tcc(equals_request)
refresh(os.join_path(root, 'missing-v'), [request])!
project := os.join_path(root, 'tinycc-project')
os.mkdir_all(project)!
os.mkdir_all(os.join_path(root, 'vlib', 'builtin'))!
wrapper := os.join_path(root, 'context-wrapper')
os.write_file(wrapper, '#!/bin/sh\ncase "$PWD" in\n*/tinycc-project) echo "tcc version 0.9.27" ;;\n*) echo "clang version 19.1.7" ;;\nesac\n')!
os.chmod(wrapper, 0o700)!
marker := os.join_path(root, 'refresh-started')
fake_v := os.join_path(root, 'fake-v')
os.write_file(fake_v, '#!/bin/sh\necho started > ' + os.quoted_path(marker) + '\nexit 91\n')!
os.chmod(fake_v, 0o700)!
before := os.getwd()
for args in [['-prod', '-cc', wrapper], ['-prod', '-cc', '../context-wrapper'],
['-prod', '-cc=../context-wrapper']] {
context_request := Request{ ...request, args: args, cwd: project }
assert request_uses_prod_tcc(context_request)
refresh(fake_v, [context_request])!
assert !os.exists(marker)
}
assert !request_uses_prod_tcc(Request{ ...request, args: ['-prod', '-cc', wrapper] })
assert os.getwd() == before
bare := 'cache_context_cc_${os.getpid()}'
os.cp(wrapper, os.join_path(project, bare))!
os.chmod(os.join_path(project, bare), 0o700)!
saved_path := os.getenv('PATH')
defer { os.setenv('PATH', saved_path, true) }
os.setenv('PATH', '.' + os.path_delimiter + saved_path, true)
context_request := Request{ ...request, args: ['-prod', '-cc', bare], cwd: project }
assert request_uses_prod_tcc(context_request)
refresh(fake_v, [context_request])!
assert !os.exists(marker)
assert os.getenv('PATH') == '.' + os.path_delimiter + saved_path
assert os.getwd() == before
}