import common { Task, exec_args } import crypto.sha256 import os import runtime fn test_symlink() { exec_args(['v', 'symlink']) } fn test_cross_compilation() { exec_args(['v', '-o', 'hw', '-os', 'linux', 'examples/hello_world.v']) exec_args(['ls', '-la', 'hw']) exec_args(['file', 'hw']) exec_args(['v', '-d', 'use_openssl', '-o', 've', '-os', 'linux', 'examples/veb/veb_example.v']) exec_args(['ls', '-la', 've']) exec_args(['file', 've']) } fn build_with_cstrict() { exec_args(['v', '-cg', '-cstrict', '-o', 'vstrict1', 'cmd/v']) } fn all_code_is_formatted() { if common.is_github_job { exec_args(['env', 'VJOBS=1', 'v', '-silent', 'test-cleancode']) } else { vjobs := os.getenv_opt('VJOBS') or { '1' } exec_args(['env', 'VJOBS=' + '${vjobs}', 'v', '-progress', 'test-cleancode']) } } fn run_sanitizers() { common.exec_args_with_progress(['v', '-o', 'v2', 'cmd/v', '-cflags', '-fsanitize=undefined'], ['v2']) exec_args(['env', 'UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1', './v2', '-o', 'v.c', 'cmd/v']) } fn build_using_v() { exec_args(['v', '-o', 'v2', 'cmd/v']) exec_args(['./v2', '-o', 'v3', 'cmd/v']) } fn verify_v_test_works() { println(os.getenv('VFLAGS')) exec_args(['v', 'cmd/tools/test_if_v_test_system_works.v']) exec_args(['./cmd/tools/test_if_v_test_system_works']) } fn install_iconv() { // Skip Homebrew when iconv is already linkable for V on this machine. if os.system_args([os.join_path(@VEXEROOT, 'v'), '-silent', 'test', 'vlib/encoding/iconv/']) == 0 { return } exec_args(['sh', '-c', 'brew list --versions libiconv >/dev/null 2>&1 || brew install libiconv']) } fn test_pure_v_math_module() { exec_args(['v', '-silent', '-d', 'pure_v_math', '-exclude', '@vlib/math/*.c.v', 'test', 'vlib/math/']) } fn self_tests() { $if macos { // Metal frame callbacks stop when the display sleeps. Keep the real GPU // probes running, and release the assertion even if a failed command exits. mut display_guard := os.new_process('/usr/bin/caffeinate') display_guard.set_args(['-d', '-u', '-w', os.getpid().str()]) display_guard.run() defer { display_guard.signal_term() display_guard.wait() display_guard.close() } run_self_tests() } $else { run_self_tests() } } fn run_self_tests() { // Do not select the V1 compatibility compiler here. It is a separate V 0.5.2 // installation, so `test-self vlib` would resolve `vlib` under *its* VROOT and // test the release's own standard library instead of this repository's. // Individual files still fall back to it when the default compiler cannot // build them. // Exercise default caching throughout the standard library suite. Respect the // workflow's worker count as well as the aggregate runner's serial setting. if common.is_github_job { vjobs := os.getenv_opt('VJOBS') or { 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', 'vlib']) } } fn build_examples() { if common.is_github_job { exec_args(['v', '-no-memory-limit', 'build-examples']) } else { exec_args(['v', '-no-memory-limit', '-progress', 'build-examples']) } } fn build_examples_v_compiled_with_tcc() { exec_args(['v', '-o', 'vtcc', '-cc', 'tcc', 'cmd/v']) if common.is_github_job { exec_args(['./vtcc', '-no-memory-limit', 'build-examples']) } else { exec_args(['./vtcc', '-no-memory-limit', '-progress', 'build-examples']) } } // ownership_vexe builds, once per job, a V3 compiler with the ownership checker // compiled in. `-autofree` needs it: a standard V3 build rejects the flag. fn ownership_vexe() string { vexe := './vownership' if !os.exists(vexe) { exec_args(['v', '-d', 'ownership', '-o', 'vownership', 'cmd/v']) } return vexe } fn skip_ownership_autofree_test() bool { return common.is_github_job || os.getenv('VTEST_SKIP_OWNERSHIP') == '1' } fn build_hello_world_autofree() { if skip_ownership_autofree_test() { eprintln('> skipping ownership/autofree test') return } exec_args([ownership_vexe(), '-autofree', '-o', 'hello_world', 'examples/hello_world.v']) exec_args(['./hello_world']) } fn build_tetris_autofree() { if skip_ownership_autofree_test() { eprintln('> skipping ownership/autofree test') return } exec_args([ownership_vexe(), '-autofree', '-o', 'tetris', 'examples/tetris/tetris.v']) } fn build_blog_autofree() { if skip_ownership_autofree_test() { eprintln('> skipping ownership/autofree test') return } // `-autofree` still needs the V1 compatibility compiler, and the frozen V 0.5.2 // release behind it ships a vlib without `json2`, which the blog imports. Build // the tutorial with the default compiler until V3 ownership can run it; // build_tetris_autofree keeps the autofree path covered. exec_args(['v', '-o', 'blog', 'tutorials/building_a_simple_web_blog_with_veb/code/blog']) } fn build_examples_prod() { exec_args(['v', '-prod', 'examples/news_fetcher.v']) } fn v_doctor() { exec_args(['v', 'doctor']) } fn build_v_with_prealloc() { exec_args(['v', '-cg', '-cstrict', '-o', 'vstrict1', 'cmd/v']) exec_args(['./vstrict1', '-d', 'debug_malloc', '-d', 'debug_realloc', '-o', 'vdebug1', 'cmd/v']) exec_args(['./vstrict1', '-o', 'vprealloc', '-prealloc', 'cmd/v']) // TODO: fix prealloc on macos (the rwmutex implementation for shared maps there seems to require that mutexes are allocated by C.malloc directly, and segfaults for arbitrary memory addresses) // exec_args(['./vprealloc', 'run', 'examples/hello_world.v']) // exec_args(['./vprealloc', '-o', 'v3', 'cmd/v']) // exec_args(['./v3', '-o', 'v4', 'cmd/v']) } fn v_self_compilation_usecache() { $if !enable_usecache_test ? { eprintln('> ${@LOCATION} use `-d enable_usecache_test` in VFLAGS to enable this task') return } exec_args(['v', 'examples/hello_world.v']) exec_args(['./examples/hello_world']) exec_args(['v', '-o', 'v2', 'cmd/v']) exec_args(['./v2', '-o', 'v3', 'cmd/v']) exec_args(['./v3', 'version']) exec_args(['./v3', '-o', 'tetris', 'examples/tetris/tetris.v']) } fn v_self_compilation_parallel_cc() { exec_args(['v', '-o', 'vp', '-parallel-cc', 'cmd/v']) // exec_args(['./v2', '-o', 'v3', 'cmd/v']) exec_args(['./vp', 'version']) exec_args(['./vp', '-o', 'tetris', 'examples/tetris/tetris.v']) } fn test_password_input() { // Expect gives the child a pseudo-terminal, but non-interactive parent shells can // still export TERM=dumb, which makes os.input_password reject that usable PTY. if os.getenv('TERM') in ['', 'dumb'] { os.setenv('TERM', 'xterm', true) } exec_args(['v', '-silent', 'test', 'examples/password/']) } fn test_readline() { exec_args(['v', '-silent', 'test', 'examples/readline/']) } fn test_inline_assembly() { exec_args(['v', 'test', 'vlib/v/slow_tests/assembly']) } const ci_tasks = [ 'test_symlink', 'v_doctor', 'build_v_with_prealloc', 'test_cross_compilation', 'test_inline_assembly', 'build_with_cstrict', 'all_code_is_formatted', 'run_sanitizers', 'build_using_v', 'verify_v_test_works', 'install_iconv', 'test_pure_v_math_module', 'self_tests', 'build_examples', 'build_hello_world_autofree', 'build_tetris_autofree', 'build_blog_autofree', 'build_examples_prod', 'build_examples_v_compiled_with_tcc', 'v_self_compilation_parallel_cc', 'test_password_input', 'test_readline', ] // Keep progress across edits/rebuilds, but isolate users and checkout directories. fn ci_progress_path() string { checkout := sha256.hexhash(os.real_path(os.getwd())) return '/tmp/v-macos-ci-${os.getuid()}-${checkout}.progress' } fn ci_progress_contents(task_name string) string { // Record the whole ordered task list so changed plans restart safely. return 'macos-ci-v1\n${task_name}\n${ci_tasks.join('\n')}\n' } fn ci_resume_index(path string) !int { if !os.exists(path) { return -1 } if !os.is_file(path) { return error('CI progress path is not a file: ${path}') } saved := os.read_file(path)! for i, task_name in ci_tasks { if saved == ci_progress_contents(task_name) { return i } } eprintln('Ignoring invalid or outdated CI progress; restarting from the first task.') return -1 } fn save_ci_progress(path string, task_name string) ! { // Write privately, then rename on the same filesystem. An interrupted write // leaves the previous checkpoint intact, never a partially written cursor. if os.exists(path) && !os.is_file(path) { return error('CI progress path is not a file: ${path}') } tmp_dir := '${path}.${os.getpid()}.tmp' os.mkdir(tmp_dir, mode: 0o700)! defer { os.rmdir_all(tmp_dir) or {} } tmp_path := os.join_path(tmp_dir, 'progress') os.write_file(tmp_path, ci_progress_contents(task_name))! os.rename(tmp_path, path)! } // check_macho_signing runs the native signing tests without resume or test filters. fn check_macho_signing() { $if macos { vexe := os.getenv_opt('V_CI_VEXE') or { os.join_path_single(@VEXEROOT, 'v') } tcc := os.join_path(os.dir(os.real_path(vexe)), 'thirdparty', 'tcc', 'tcc.exe') if !os.is_executable(tcc) { eprintln('Mach-O signing check requires TinyCC at ${tcc}; run `make` first.') exit(1) } eprintln('Checking Mach-O signing before resuming CI tasks.') // Run the file directly so test filters and saved successes cannot skip it. exec_args(['env', 'VTEST_ONLY_FN=', 'v', '-silent', 'vlib/v/util/machosign/macho_sign_test.v']) } } // run_ci_tasks mirrors the active ci/macos_ci.vsh steps in // .github/workflows/macos_ci.yml. The generic `all` mode intentionally remains // exhaustive, including tasks that are currently disabled in the workflow. fn run_ci_tasks(reset bool) ! { // Match the GitHub Actions job environment that changes test behavior. os.setenv('CI', 'true', true) os.setenv('GITHUB_ACTIONS', 'true', true) os.setenv('GITHUB_JOB', 'clang-macos', true) os.setenv('RUNNER_OS', 'macOS', true) os.setenv('VFLAGS', '-cc clang', true) // Stop within test/build sessions too, without other files already running. os.setenv('VTEST_FAIL_FAST', '1', true) os.setenv('VJOBS', '1', true) os.setenv('VTEST_SHOW_LONGEST_BY_RUNTIME', '3', true) os.setenv('VTEST_SHOW_LONGEST_BY_COMPTIME', '3', true) os.setenv('VTEST_SHOW_LONGEST_BY_TOTALTIME', '3', true) os.setenv('VTEST_SKIP_OWNERSHIP', '1', true) os.setenv('V_MACOS_V3_NO_FALLBACK', '1', true) os.setenv('V_MACOS_MULTIWINDOW_TESTS', '0', true) check_macho_signing() progress_path := ci_progress_path() progress_dir := '${progress_path}.d' saved_index := if reset { -1 } else { ci_resume_index(progress_path)! } // No valid cursor means none of its finer-grained records may be reused. if saved_index < 0 && os.exists(progress_dir) { os.rmdir_all(progress_dir)! } if !os.exists(progress_dir) { os.mkdir(progress_dir, mode: 0o700)! } start := if saved_index < 0 { 0 } else { saved_index } os.unsetenv('VTEST_RESUME_OWNER') eprintln('CI progress: ${progress_path}') eprintln('Use `v run ci/macos_ci.vsh ci --reset` to restart from the first task.') if start > 0 { eprintln('Resuming at ${ci_tasks[start]}; skipping ${start} completed CI tasks.') } for i in start .. ci_tasks.len { task_name := ci_tasks[i] // Save BEFORE execution: a failure or interruption must retry this task. save_ci_progress(progress_path, task_name)! os.setenv('V_MACOS_CI_TASK_PROGRESS', os.join_path(progress_dir, task_name), true) eprintln('CI task ${i + 1}/${ci_tasks.len}: ${task_name}') exec_args(['v', 'run', 'ci/macos_ci.vsh', '${task_name}']) } os.rmdir_all(progress_dir)! os.rm(progress_path)! eprintln('CI tasks complete; progress cleared.') } const all_tasks = { 'test_symlink': Task{test_symlink, 'Test symlink'} 'test_cross_compilation': Task{test_cross_compilation, 'Test cross compilation to Linux'} 'build_with_cstrict': Task{build_with_cstrict, 'Build V with -cstrict'} 'all_code_is_formatted': Task{all_code_is_formatted, 'All code is formatted'} 'run_sanitizers': Task{run_sanitizers, 'Run sanitizers'} 'build_using_v': Task{build_using_v, 'Build V using V'} 'verify_v_test_works': Task{verify_v_test_works, 'Verify `v test` works'} 'install_iconv': Task{install_iconv, 'Install iconv for encoding.iconv'} 'test_pure_v_math_module': Task{test_pure_v_math_module, 'Test pure V math module'} 'self_tests': Task{self_tests, 'Self tests'} 'build_examples': Task{build_examples, 'Build examples'} 'build_hello_world_autofree': Task{build_hello_world_autofree, 'Build hello_world with -autofree'} 'build_tetris_autofree': Task{build_tetris_autofree, 'Build tetris with -autofree'} 'build_blog_autofree': Task{build_blog_autofree, 'Build blog tutorial with -autofree'} 'build_examples_prod': Task{build_examples_prod, 'Build examples with -prod'} 'build_examples_v_compiled_with_tcc': Task{build_examples_v_compiled_with_tcc, 'Build examples with V build with tcc'} 'v_doctor': Task{v_doctor, 'v doctor'} 'build_v_with_prealloc': Task{build_v_with_prealloc, 'Build V with prealloc'} 'v_self_compilation_usecache': Task{v_self_compilation_usecache, 'V self compilation with default caching'} 'v_self_compilation_parallel_cc': Task{v_self_compilation_parallel_cc, 'V self compilation with -parallel-cc'} 'test_password_input': Task{test_password_input, 'Test password input'} 'test_readline': Task{test_readline, 'Test readline'} 'test_inline_assembly': Task{test_inline_assembly, 'Test inline assembly'} } if os.args.len > 1 && os.args[1] == 'ci' { if os.args.len > 3 || (os.args.len == 3 && os.args[2] != '--reset') { eprintln('Usage: v run ci/macos_ci.vsh ci [--reset]') exit(1) } run_ci_tasks(os.args.len == 3) or { eprintln('Could not update CI progress: ${err.msg()}') exit(1) } exit(0) } common.run(all_tasks)