From e93194b0eb97822be79c03a8de0fda3cc5e49bc9 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 15:53:51 +0300 Subject: [PATCH] builder: fix c error that should never happen during tcc retry (fixes #15191) --- vlib/os/os_windows.c.v | 30 ++++++++++++ vlib/v/builder/cc.v | 76 +++++++++++++++++++++++++----- vlib/v/builder/cc_tcc_retry_test.v | 22 +++++++++ 3 files changed, 117 insertions(+), 11 deletions(-) diff --git a/vlib/os/os_windows.c.v b/vlib/os/os_windows.c.v index bc46bf957..aa91a40d5 100644 --- a/vlib/os/os_windows.c.v +++ b/vlib/os/os_windows.c.v @@ -23,6 +23,9 @@ fn C.CreateSymbolicLinkW(&u16, &u16, u32) i32 // See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createhardlinkw fn C.CreateHardLinkW(&u16, &u16, C.SECURITY_ATTRIBUTES) i32 +// See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getshortpathnamew +fn C.GetShortPathNameW(&u16, &u16, u32) u32 + fn C._getpid() i32 const executable_suffixes = ['.exe', '.bat', '.cmd', ''] @@ -291,6 +294,33 @@ fn native_glob_pattern(pattern string, mut matches []string) ! { } } +// short_path returns the Windows DOS 8.3 short path when it is available. +// If the path does not exist, or if short names are unavailable, it returns the original path. +pub fn short_path(path string) string { + if path == '' { + return '' + } + normalized := path.replace('/', '\\') + mut short_buf := [max_path_buffer_size]u16{} + mut wpath := normalized.to_wide() + defer { + unsafe { free(voidptr(wpath)) } + } + short_len := C.GetShortPathNameW(wpath, &short_buf[0], max_path_buffer_size) + if short_len > 0 && short_len < u32(max_path_buffer_size) { + return unsafe { string_from_wide2(&short_buf[0], int(short_len)) } + } + parent := dir(normalized) + if parent == '' || parent == '.' || parent == normalized || !is_dir(parent) { + return path + } + short_parent := short_path(parent) + if short_parent == parent { + return path + } + return join_path_single(short_parent, file_name(normalized)) +} + pub fn utime(path string, actime i64, modtime i64) ! { mut u := C._utimbuf{actime, modtime} if C._utime(&char(path.str), voidptr(&u)) != 0 { diff --git a/vlib/v/builder/cc.v b/vlib/v/builder/cc.v index 51b3bb222..cc089bc50 100644 --- a/vlib/v/builder/cc.v +++ b/vlib/v/builder/cc.v @@ -366,6 +366,8 @@ pub mut: ldflags []string // `-labcd' from `v -ldflags "-labcd"` } +type WindowsPathResolver = fn (string) string + fn ccompiler_type_from_name_with_ok(ccompiler string) (pref.CompilerType, bool) { cc_file_name := os.file_name(ccompiler).to_lower_ascii() if cc_file_name.contains('tcc') || cc_file_name.contains('tinyc') { @@ -948,13 +950,64 @@ fn (mut v Builder) setup_output_name() { } pub fn (mut v Builder) tcc_quoted_path(p string) string { - if v.ccoptions.cc == .tcc && !v.pref.no_rsp { - // tcc has a bug, that prevents it from being able to parse names quoted with ' in .rsp files :-| - mut escaped := p.replace('\\', '\\\\') - escaped = escaped.replace('"', '\\"') - return '"${escaped}"' + return os.quoted_path(v.tcc_windows_path(p)) +} + +fn looks_like_windows_path(value string) bool { + return value.contains('\\') || value.contains('/') || (value.len > 1 && value[1] == `:`) +} + +fn rewrite_windows_path_arg(arg string, resolver WindowsPathResolver) string { + if arg == '' { + return '' + } + if start := arg.index('"') { + end := arg.last_index('"') or { -1 } + if end > start { + path := arg[start + 1..end] + if looks_like_windows_path(path) { + return arg[..start] + '"${resolver(path)}"' + arg[end + 1..] + } + } + } + for prefix in ['-I', '-L', '-B', '-o ', '-c '] { + if arg.starts_with(prefix) { + path := arg[prefix.len..].trim_space().trim('"') + if looks_like_windows_path(path) { + return prefix + '"${resolver(path)}"' + } + } } - return os.quoted_path(p) + trimmed := arg.trim_space().trim('"') + if !arg.starts_with('-') && looks_like_windows_path(trimmed) { + return '"${resolver(trimmed)}"' + } + return arg +} + +fn short_windows_path(path string) string { + $if windows { + return os.short_path(path) + } + return path +} + +fn (v &Builder) tcc_windows_path(p string) string { + $if windows { + if v.ccoptions.cc == .tcc { + return short_windows_path(p) + } + } + return p +} + +fn (v &Builder) tcc_windows_path_arg(arg string) string { + $if windows { + if v.ccoptions.cc == .tcc { + return rewrite_windows_path_arg(arg, short_windows_path) + } + } + return arg } fn (v &Builder) rsp_safe_arg(arg string) string { @@ -1216,7 +1269,8 @@ pub fn (mut v Builder) cc() { // all_args := v.all_args(v.ccoptions) v.dump_c_options(all_args) - rsp_args := all_args.map(v.rsp_safe_arg(it)) + mut rsp_args := all_args.map(v.rsp_safe_arg(it)) + rsp_args = rsp_args.map(v.tcc_windows_path_arg(it)) shell_args := rsp_args.join(' ') mut should_use_rsp := v.should_use_rsp(rsp_args) mut str_args := if !should_use_rsp { @@ -1236,9 +1290,9 @@ pub fn (mut v Builder) cc() { if should_use_rsp { response_file = '${v.out_name_c}.rsp' response_file_content = str_args.replace('\\', '\\\\') - rspexpr := '@${response_file}' - cmd = '${v.quote_compiler_name(ccompiler)} ${os.quoted_path(rspexpr)}' write_response_file(response_file, response_file_content) + rspexpr := '@${v.tcc_windows_path(response_file)}' + cmd = '${v.quote_compiler_name(ccompiler)} ${os.quoted_path(rspexpr)}' if !v.ccoptions.debug_mode { v.pref.cleanup_files << response_file } @@ -1813,7 +1867,7 @@ fn (mut v Builder) build_thirdparty_obj_file(mod string, path string, moduleflag os.chdir(v.pref.vroot) or {} cc_options := if source_kind == .asm { - '-o ${os.quoted_path(opath)} -c ${os.quoted_path(source_file)}' + '-o ${v.tcc_quoted_path(opath)} -c ${v.tcc_quoted_path(source_file)}' } else { mut all_options := []string{cap: 4} all_options << v.pref.third_party_option @@ -1821,7 +1875,7 @@ fn (mut v Builder) build_thirdparty_obj_file(mod string, path string, moduleflag all_options << '-o ${v.tcc_quoted_path(opath)}' all_options << '-c ${v.tcc_quoted_path(source_file)}' cpp_file := source_kind == .cpp - v.thirdparty_object_args(v.ccoptions, all_options, cpp_file).join(' ') + v.thirdparty_object_args(v.ccoptions, all_options, cpp_file).map(v.tcc_windows_path_arg(it)).join(' ') } // If the third party object file requires a CPP file compilation, switch to a CPP compiler diff --git a/vlib/v/builder/cc_tcc_retry_test.v b/vlib/v/builder/cc_tcc_retry_test.v index 60fe9e84c..505844941 100644 --- a/vlib/v/builder/cc_tcc_retry_test.v +++ b/vlib/v/builder/cc_tcc_retry_test.v @@ -36,3 +36,25 @@ fn test_is_tcc_compilation_failure_detects_tcc_alias_compiler() { } assert is_tcc_compilation_failure('cc', .unknown, '') } + +fn fake_windows_short_path(path string) string { + return path.replace(r'C:\Users\Léo', r'C:\Users\LEO~1') +} + +fn test_rewrite_windows_path_arg_rewrites_quoted_object_paths() { + arg := r'"C:\Users\Léo\.vmodules\.cache\bc\artifact.o"' + expected := r'"C:\Users\LEO~1\.vmodules\.cache\bc\artifact.o"' + assert rewrite_windows_path_arg(arg, fake_windows_short_path) == expected +} + +fn test_rewrite_windows_path_arg_rewrites_prefixed_paths() { + assert rewrite_windows_path_arg(r'-I"C:\Users\Léo\include"', fake_windows_short_path) == r'-I"C:\Users\LEO~1\include"' + assert rewrite_windows_path_arg(r'-L"C:\Users\Léo\lib"', fake_windows_short_path) == r'-L"C:\Users\LEO~1\lib"' + assert rewrite_windows_path_arg(r'-o "C:\Users\Léo\bin\tool.exe"', fake_windows_short_path) == r'-o "C:\Users\LEO~1\bin\tool.exe"' +} + +fn test_rewrite_windows_path_arg_leaves_non_paths_alone() { + for arg in ['-bt25', '-std=c99', '-D_DEFAULT_SOURCE'] { + assert rewrite_windows_path_arg(arg, fake_windows_short_path) == arg + } +} -- 2.39.5