From 0a5429e74232e24fba36dd029f83bd4c309803be Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 28 May 2026 02:05:54 +0300 Subject: [PATCH] builder: submit C compiler error reports (#27279) --- cmd/tools/vbug-report.v | 284 ++++++++++++++++ cmd/v/v.v | 1 + vlib/v/builder/c_error_report.v | 477 +++++++++++++++++++++++++++ vlib/v/builder/c_error_report_test.v | 135 ++++++++ vlib/v/builder/cc.v | 46 ++- vlib/v/builder/cc_tcc_retry_test.v | 28 ++ vlib/v/help/build/build-c.txt | 4 + vlib/v/help/other/other.txt | 2 + vlib/v/pref/pref.v | 5 + 9 files changed, 972 insertions(+), 10 deletions(-) create mode 100644 cmd/tools/vbug-report.v create mode 100644 vlib/v/builder/c_error_report.v create mode 100644 vlib/v/builder/c_error_report_test.v diff --git a/cmd/tools/vbug-report.v b/cmd/tools/vbug-report.v new file mode 100644 index 000000000..c36fa0c32 --- /dev/null +++ b/cmd/tools/vbug-report.v @@ -0,0 +1,284 @@ +module main + +import db.sqlite +import flag +import json +import os +import rand +import time +import veb + +const default_port = 8080 +const max_report_body_bytes = 256 * 1024 + +struct ReportLine { +pub: + line int + text string +} + +struct BugReport { +pub: + kind string + v_version string + target_os string + target_backend string + ccompiler string + c_error string + c_file string + c_line int + c_context []ReportLine + v_file string + v_line int + v_context []ReportLine +} + +struct CreateBugReportResponse { +pub: + id string + delete_url string + delete_token string + message string +} + +struct DeleteBugReportResponse { +pub: + id string + deleted bool + message string +} + +struct ServerConfig { + host string + port int + db_path string + public_url string +} + +pub struct App { +pub: + public_url string +pub mut: + db sqlite.DB +} + +pub struct Context { + veb.Context +} + +fn args_without_command() []string { + args := os.args#[1..] + if args.len > 0 && args[0] == 'bug-report' { + return args[1..] + } + return args +} + +fn db_path_from_env() string { + db_path := os.getenv('V_BUG_REPORT_DB') + if db_path != '' { + return db_path + } + report_dir := os.getenv('V_BUG_REPORT_DIR') + if report_dir != '' { + return os.join_path(report_dir, 'bug_reports.sqlite') + } + return os.join_path(os.getwd(), 'bug_reports.sqlite') +} + +fn report_host_from_env() string { + host := os.getenv('V_BUG_REPORT_HOST') + if host != '' { + return host + } + return 'localhost' +} + +fn report_port_from_env() int { + bug_report_port := os.getenv('V_BUG_REPORT_PORT') + if bug_report_port != '' { + return bug_report_port.int() + } + port := os.getenv('PORT') + if port != '' { + return port.int() + } + return default_port +} + +fn public_url_from_config(flag_url string, host string, port int) string { + url := if flag_url != '' { flag_url } else { os.getenv('V_BUG_REPORT_PUBLIC_URL') } + if url != '' { + return url.trim_space().trim_right('/') + } + public_host := if host == '' || host == '0.0.0.0' { 'localhost' } else { host } + return 'http://${public_host}:${port}/bug-report' +} + +fn config_from_args() !ServerConfig { + mut fp := flag.new_flag_parser(args_without_command()) + fp.application('v bug-report') + fp.version('0.0.1') + fp.description('Run the V compiler bug report receiver.') + fp.arguments_description('') + show_help := fp.bool('help', `h`, false, 'Show this help screen.') + host := fp.string('host', 0, report_host_from_env(), 'Host to listen on.') + port := fp.int('port', `p`, report_port_from_env(), 'Port to listen on.') + db_path := fp.string('db', 0, db_path_from_env(), 'SQLite database path.') + public_url_arg := fp.string('public-url', 0, '', 'Public /bug-report endpoint URL.') + if show_help { + println(fp.usage()) + exit(0) + } + remaining := fp.finalize()! + if remaining.len > 0 { + return error('unexpected arguments: ${remaining.join(' ')}') + } + return ServerConfig{ + host: host + port: port + db_path: db_path + public_url: public_url_from_config(public_url_arg, host, port) + } +} + +fn safe_id(id string) bool { + if id.len < 8 || id.len > 80 { + return false + } + for ch in id { + if !ch.is_letter() && !ch.is_digit() && ch != `-` && ch != `_` { + return false + } + } + return true +} + +fn new_report_id() string { + return rand.uuid_v4() +} + +fn (app App) delete_url(id string, token string) string { + return '${app.public_url}/${id}?token=${token}' +} + +fn delete_token_from_request(ctx Context) string { + if token := ctx.query['token'] { + return token + } + return ctx.get_custom_header('X-Delete-Token') or { '' } +} + +fn ensure_db_parent(db_path string) ! { + if db_path == ':memory:' || db_path.starts_with('file:') { + return + } + parent_dir := os.dir(db_path) + if parent_dir != '' && parent_dir != '.' { + os.mkdir_all(parent_dir)! + } +} + +fn connect_db(db_path string) !sqlite.DB { + ensure_db_parent(db_path)! + mut db := sqlite.connect_full(db_path, [.readwrite, .create, .fullmutex], '')! + db.busy_timeout(3000) + return db +} + +fn init_db(db sqlite.DB) ! { + db.exec('create table if not exists bug_reports ( + id text primary key, + delete_token text not null, + created_at text not null, + remote_ip text not null, + user_agent text not null, + report_json text not null + )')! + db.exec('create index if not exists idx_bug_reports_created_at + on bug_reports(created_at)')! +} + +// create stores a compiler bug report and returns its deletion token. +@['/bug-report'; post] +pub fn (mut app App) create(mut ctx Context) veb.Result { + if ctx.req.data.len == 0 { + return ctx.request_error('empty report body') + } + if ctx.req.data.len > max_report_body_bytes { + ctx.res.set_status(.bad_request) + return ctx.text('report body is too large') + } + report := json.decode(BugReport, ctx.req.data) or { + return ctx.request_error('invalid report JSON: ${err}') + } + if report.kind != 'v-c-compiler-error' { + return ctx.request_error('unsupported report kind') + } + id := new_report_id() + delete_token := rand.uuid_v4() + app.db.exec_param_many('insert into bug_reports ( + id, delete_token, created_at, remote_ip, user_agent, report_json + ) values (?, ?, ?, ?, ?, ?)', [ + id, + delete_token, + time.utc().format_rfc3339(), + ctx.ip(), + ctx.user_agent(), + json.encode(report), + ]) or { return ctx.server_error('could not store report') } + return ctx.json(CreateBugReportResponse{ + id: id + delete_url: app.delete_url(id, delete_token) + delete_token: delete_token + message: 'created' + }) +} + +// delete removes a compiler bug report when the deletion token matches. +@['/bug-report/:id'; delete] +pub fn (mut app App) delete(mut ctx Context, id string) veb.Result { + if !safe_id(id) { + return ctx.request_error('invalid report id') + } + rows := app.db.exec_param('select delete_token from bug_reports where id = ?', id) or { + return ctx.server_error('could not read report') + } + if rows.len == 0 { + return ctx.not_found() + } + if delete_token_from_request(ctx) != rows[0].vals[0] { + ctx.res.set_status(.forbidden) + return ctx.text('invalid delete token') + } + app.db.exec_param('delete from bug_reports where id = ?', id) or { + return ctx.server_error('could not delete report') + } + return ctx.json(DeleteBugReportResponse{ + id: id + deleted: true + message: 'deleted' + }) +} + +// healthz returns a basic readiness response for local checks. +@['/healthz'; get] +pub fn (mut app App) healthz(mut ctx Context) veb.Result { + return ctx.text('ok') +} + +fn main() { + config := config_from_args() or { + eprintln('v bug-report: ${err}') + exit(1) + } + mut db := connect_db(config.db_path) or { panic(err) } + init_db(db) or { panic(err) } + mut app := &App{ + public_url: config.public_url + db: db + } + veb.run_at[App, Context](mut app, host: config.host, port: config.port, family: .ip) or { + panic(err) + } +} diff --git a/cmd/v/v.v b/cmd/v/v.v index ca211d595..27cbc0310 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -18,6 +18,7 @@ const external_tools = [ 'ast', 'bin2v', 'bug', + 'bug-report', 'build-examples', 'build-tools', 'build-vbinaries', diff --git a/vlib/v/builder/c_error_report.v b/vlib/v/builder/c_error_report.v new file mode 100644 index 000000000..0dfa5d1e0 --- /dev/null +++ b/vlib/v/builder/c_error_report.v @@ -0,0 +1,477 @@ +module builder + +import json +import net.http +import os +import time +import v.util.version + +const default_c_error_bug_report_url = 'https://vlang.io/bug-report' +const c_error_context_radius = 5 +const c_error_bug_report_max_body_bytes = 256 * 1024 +const c_error_bug_report_truncation_notice = '\n... report truncated before upload ...\n' + +struct CErrorReportLine { +pub: + line int + text string +} + +struct CErrorReportLocation { +pub: + file string + line int +} + +struct CErrorBugReport { +pub: + kind string + v_version string + target_os string + target_backend string + ccompiler string + c_error string + c_file string + c_line int + c_context []CErrorReportLine + v_file string + v_line int + v_context []CErrorReportLine +} + +struct CErrorBugReportResponse { +pub: + id string + delete_url string + delete_token string + message string +} + +fn (mut v Builder) submit_c_error_bug_report(ccompiler string, c_output string) { + raw_report := v.new_c_error_bug_report(ccompiler, c_output) + report := bounded_c_error_bug_report(raw_report, c_error_bug_report_max_body_bytes) + report_url := c_error_bug_report_url(v.pref.c_error_bug_report_url) + response := send_c_error_bug_report(report, report_url) or { + eprintln('C compiler bug report was not sent to ${report_url}: ${err}') + return + } + println('================== C compiler bug report ==============') + println('Sent C compiler bug report to ${report_url}.') + if response.id != '' { + println('Report id: ${response.id}') + } + print_c_error_bug_report_context(report) + delete_url := if response.delete_url != '' { + response.delete_url + } else if response.id != '' && response.delete_token != '' { + '${report_url}/${response.id}?token=${response.delete_token}' + } else { + '' + } + if delete_url != '' { + println('Delete this report from the server with:') + println(' curl -X DELETE ${os.quoted_path(delete_url)}') + } + println('='.repeat('================== C compiler bug report =============='.len)) +} + +fn (mut v Builder) new_c_error_bug_report(ccompiler string, c_output string) CErrorBugReport { + c_source := os.read_file(v.out_name_c) or { '' } + c_lines := c_source.split_into_lines() + mut c_file := v.out_name_c + mut c_line := 0 + mut v_file := '' + mut v_line := 0 + if c_loc := c_error_location_for_generated_c(c_output, v.out_name_c) { + c_file = c_loc.file + c_line = c_loc.line + if v_loc := v_source_location_for_c_line(c_lines, c_line, v.out_name_c) { + v_file = v_loc.file + v_line = v_loc.line + } + } else if source_loc := first_error_source_location(c_output) { + v_file = source_loc.file + v_line = source_loc.line + if found_c_line := generated_c_line_for_source_location(c_lines, source_loc, v.out_name_c) { + c_line = found_c_line + } + } + v_source := if v_file != '' { os.read_file(v_file) or { '' } } else { '' } + return CErrorBugReport{ + kind: 'v-c-compiler-error' + v_version: version.full_v_version(true) + target_os: v.pref.os.str() + target_backend: v.pref.backend.str() + ccompiler: ccompiler + c_error: c_output + c_file: c_file + c_line: c_line + c_context: numbered_context_lines(c_lines, c_line, c_error_context_radius) + v_file: v_file + v_line: v_line + v_context: numbered_context_lines(v_source.split_into_lines(), v_line, + c_error_context_radius) + } +} + +fn c_error_bug_report_url(flag_url string) string { + trimmed_flag_url := flag_url.trim_space() + if trimmed_flag_url != '' { + return trimmed_flag_url.trim_right('/') + } + env_url := os.getenv('V_C_ERROR_BUG_REPORT_URL').trim_space() + if env_url != '' { + return env_url.trim_right('/') + } + return default_c_error_bug_report_url +} + +fn send_c_error_bug_report(report CErrorBugReport, report_url string) !CErrorBugReportResponse { + mut header := http.new_header(key: .content_type, value: 'application/json') + header.set(.accept, 'application/json') + response := http.fetch( + method: .post + url: report_url + data: json.encode(report) + header: header + max_retries: 1 + read_timeout: 3 * time.second + write_timeout: 3 * time.second + )! + if response.status_code < 200 || response.status_code >= 300 { + return error('server responded with HTTP ${response.status_code}') + } + return json.decode(CErrorBugReportResponse, response.body)! +} + +fn bounded_c_error_bug_report(report CErrorBugReport, max_body_bytes int) CErrorBugReport { + if max_body_bytes <= 0 || json.encode(report).len <= max_body_bytes { + return report + } + if bounded := report_with_bounded_c_error(report, max_body_bytes, report.c_context, + report.v_context) + { + return bounded + } + for context_text_bytes in [4096, 1024, 256, 80, 0] { + c_context := bounded_report_lines(report.c_context, context_text_bytes) + v_context := bounded_report_lines(report.v_context, context_text_bytes) + if bounded := report_with_bounded_c_error(report, max_body_bytes, c_context, v_context) { + return bounded + } + } + return CErrorBugReport{ + ...report + c_error: truncated_report_text(report.c_error, 0) + c_context: []CErrorReportLine{} + v_context: []CErrorReportLine{} + } +} + +fn report_with_bounded_c_error(report CErrorBugReport, max_body_bytes int, c_context []CErrorReportLine, v_context []CErrorReportLine) ?CErrorBugReport { + min_report := CErrorBugReport{ + ...report + c_error: truncated_report_text(report.c_error, 0) + c_context: c_context + v_context: v_context + } + if json.encode(min_report).len > max_body_bytes { + return none + } + mut low := 0 + mut high := report.c_error.len + mut best := min_report + for low <= high { + mid := (low + high) / 2 + candidate := CErrorBugReport{ + ...report + c_error: truncated_report_text(report.c_error, mid) + c_context: c_context + v_context: v_context + } + if json.encode(candidate).len <= max_body_bytes { + best = candidate + low = mid + 1 + } else { + high = mid - 1 + } + } + return best +} + +fn bounded_report_lines(lines []CErrorReportLine, max_text_bytes int) []CErrorReportLine { + mut bounded := []CErrorReportLine{cap: lines.len} + for report_line in lines { + bounded << CErrorReportLine{ + line: report_line.line + text: truncated_report_text(report_line.text, max_text_bytes) + } + } + return bounded +} + +fn truncated_report_text(text string, max_bytes int) string { + if max_bytes <= 0 { + return '' + } + if text.len <= max_bytes { + return text + } + if max_bytes <= c_error_bug_report_truncation_notice.len { + return text[..max_bytes] + } + kept_bytes := max_bytes - c_error_bug_report_truncation_notice.len + head_bytes := kept_bytes / 2 + tail_bytes := kept_bytes - head_bytes + return text[..head_bytes] + c_error_bug_report_truncation_notice + text[text.len - tail_bytes..] +} + +fn print_c_error_bug_report_context(report CErrorBugReport) { + println('Generated C lines sent from ${report.c_file}:${report.c_line}:') + print_report_lines(report.c_context, report.c_line) + if report.v_file != '' { + println('Corresponding V lines sent from ${report.v_file}:${report.v_line}:') + print_report_lines(report.v_context, report.v_line) + } else { + println('Corresponding V lines sent: no V source mapping was available.') + } +} + +fn print_report_lines(lines []CErrorReportLine, center int) { + if lines.len == 0 { + println(' (no source lines available)') + return + } + for line in lines { + prefix := if line.line == center { '>' } else { ' ' } + println('${prefix} ${line.line:6} | ${line.text}') + } +} + +fn numbered_context_lines(lines []string, center int, radius int) []CErrorReportLine { + if center <= 0 || lines.len == 0 { + return []CErrorReportLine{} + } + mut start := center - radius + if start < 1 { + start = 1 + } + mut end := center + radius + if end > lines.len { + end = lines.len + } + mut context := []CErrorReportLine{cap: end - start + 1} + for line_nr in start .. end + 1 { + context << CErrorReportLine{ + line: line_nr + text: lines[line_nr - 1] + } + } + return context +} + +fn c_error_location_for_generated_c(c_output string, generated_c_file string) ?CErrorReportLocation { + needles := c_error_generated_c_needles(generated_c_file) + for output_line in c_output.split_into_lines() { + if !output_line.to_lower_ascii().contains('error') { + continue + } + for needle in needles { + if loc := parse_error_location_after_needle(output_line, needle) { + return loc + } + } + } + return none +} + +fn c_error_generated_c_needles(generated_c_file string) []string { + mut needles := []string{} + for candidate in [generated_c_file, os.real_path(generated_c_file), + os.file_name(generated_c_file)] { + if candidate != '' && candidate !in needles { + needles << candidate + } + normalized := candidate.replace('\\', '/') + if normalized != '' && normalized !in needles { + needles << normalized + } + } + return needles +} + +fn parse_error_location_after_needle(output_line string, needle string) ?CErrorReportLocation { + idx := output_line.index(needle) or { return none } + after := output_line[idx + needle.len..] + if after.starts_with(':') { + line_nr := leading_int(after[1..]) + if line_nr > 0 { + return CErrorReportLocation{ + file: needle + line: line_nr + } + } + } + if after.starts_with('(') { + line_nr := leading_int(after[1..]) + if line_nr > 0 { + return CErrorReportLocation{ + file: needle + line: line_nr + } + } + } + return none +} + +fn first_error_source_location(c_output string) ?CErrorReportLocation { + for output_line in c_output.split_into_lines() { + if !output_line.to_lower_ascii().contains('error') { + continue + } + if loc := parse_colon_error_location(output_line) { + return loc + } + if loc := parse_msvc_error_location(output_line) { + return loc + } + } + return none +} + +fn parse_colon_error_location(output_line string) ?CErrorReportLocation { + parts := output_line.split(':') + if parts.len < 2 { + return none + } + for idx := 1; idx < parts.len; idx++ { + line_nr := parts[idx].int() + if line_nr <= 0 { + continue + } + file := parts[..idx].join(':') + if file == '' { + continue + } + return CErrorReportLocation{ + file: file + line: line_nr + } + } + return none +} + +fn parse_msvc_error_location(output_line string) ?CErrorReportLocation { + open_idx := output_line.index('(') or { return none } + close_rel_idx := output_line[open_idx + 1..].index(')') or { return none } + line_nr := leading_int(output_line[open_idx + 1..open_idx + 1 + close_rel_idx]) + if line_nr <= 0 { + return none + } + return CErrorReportLocation{ + file: output_line[..open_idx] + line: line_nr + } +} + +fn v_source_location_for_c_line(c_lines []string, c_line int, generated_c_file string) ?CErrorReportLocation { + if c_line <= 0 || c_lines.len == 0 { + return none + } + mut current := CErrorReportLocation{} + last_line := if c_line <= c_lines.len { c_line } else { c_lines.len } + for idx in 0 .. last_line { + if directive := parse_line_directive(c_lines[idx]) { + current = directive + continue + } + if idx + 1 == c_line && is_v_source_file(current.file) + && !same_path(current.file, generated_c_file) { + return current + } + if current.file != '' { + current = CErrorReportLocation{ + file: current.file + line: current.line + 1 + } + } + } + return none +} + +fn generated_c_line_for_source_location(c_lines []string, source CErrorReportLocation, generated_c_file string) ?int { + if source.file == '' || source.line <= 0 { + return none + } + mut current := CErrorReportLocation{} + mut fallback_line := 0 + for idx, line in c_lines { + if directive := parse_line_directive(line) { + current = directive + continue + } + if is_v_source_file(current.file) && !same_path(current.file, generated_c_file) + && same_path(current.file, source.file) && current.line == source.line { + if fallback_line == 0 { + fallback_line = idx + 1 + } + if line.trim_space() != '' { + return idx + 1 + } + } + if current.file != '' { + current = CErrorReportLocation{ + file: current.file + line: current.line + 1 + } + } + } + if fallback_line > 0 { + return fallback_line + } + return none +} + +fn parse_line_directive(line string) ?CErrorReportLocation { + trimmed := line.trim_space() + if !trimmed.starts_with('#line ') { + return none + } + rest := trimmed['#line '.len..].trim_space() + line_nr := leading_int(rest) + if line_nr <= 0 { + return none + } + first_quote_idx := rest.index('"') or { return none } + remaining := rest[first_quote_idx + 1..] + second_quote_idx := remaining.index('"') or { return none } + return CErrorReportLocation{ + file: remaining[..second_quote_idx] + line: line_nr + } +} + +fn leading_int(s string) int { + mut end := 0 + for end < s.len && s[end].is_digit() { + end++ + } + if end == 0 { + return 0 + } + return s[..end].int() +} + +fn is_v_source_file(path string) bool { + return path.ends_with('.v') || path.ends_with('.vv') || path.ends_with('.vsh') +} + +fn same_path(a string, b string) bool { + if a == b { + return true + } + normalized_a := a.replace('\\', '/') + normalized_b := b.replace('\\', '/') + return normalized_a == normalized_b + || os.real_path(a).replace('\\', '/') == os.real_path(b).replace('\\', '/') +} diff --git a/vlib/v/builder/c_error_report_test.v b/vlib/v/builder/c_error_report_test.v new file mode 100644 index 000000000..b79ffb7f7 --- /dev/null +++ b/vlib/v/builder/c_error_report_test.v @@ -0,0 +1,135 @@ +module builder + +import json + +fn test_c_error_location_for_generated_c_parses_gcc_output() { + loc := c_error_location_for_generated_c('/tmp/program.tmp.c:42:7: error: unknown type name', + '/tmp/program.tmp.c') or { + assert false + return + } + assert loc.line == 42 +} + +fn test_c_error_location_for_generated_c_parses_msvc_output() { + loc := c_error_location_for_generated_c('C:\\tmp\\program.tmp.c(19): error C2143: syntax error', + 'C:\\tmp\\program.tmp.c') or { + assert false + return + } + assert loc.line == 19 +} + +fn test_numbered_context_lines_returns_five_lines_each_side() { + lines := ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] + context := numbered_context_lines(lines, 6, 5) + assert context.len == 11 + assert context.first().line == 1 + assert context.last().line == 11 + assert context[5].line == 6 + assert context[5].text == '6' +} + +fn test_v_source_location_mapping_from_line_directives() { + c_lines := [ + '#line 10 "/tmp/source.v"', + 'int a = 1;', + 'int b = missing;', + '#line 999 "/tmp/program.tmp.c"', + 'int main(void) { return 0; }', + ] + loc := v_source_location_for_c_line(c_lines, 3, '/tmp/program.tmp.c') or { + assert false + return + } + assert loc.file == '/tmp/source.v' + assert loc.line == 11 + c_line := generated_c_line_for_source_location(c_lines, CErrorReportLocation{ + file: '/tmp/source.v' + line: 11 + }, '/tmp/program.tmp.c') or { + assert false + return + } + assert c_line == 3 +} + +fn test_generated_c_reset_line_is_not_reported_as_v_source() { + c_lines := [ + '#line 40 "/tmp/program.tmp.c"', + 'int generated = missing;', + ] + if _ := v_source_location_for_c_line(c_lines, 2, '/tmp/program.tmp.c') { + assert false + } +} + +fn test_generated_c_line_for_source_location_prefers_non_empty_line() { + c_lines := [ + '#line 5 "/tmp/source.v"', + 'void main__main(void) {', + '', + '#line 6 "/tmp/source.v"', + '{NoSuchType _ = ((NoSuchType){E_STRUCT});}', + '}', + ] + c_line := generated_c_line_for_source_location(c_lines, CErrorReportLocation{ + file: '/tmp/source.v' + line: 6 + }, '/tmp/program.tmp.c') or { + assert false + return + } + assert c_line == 5 +} + +fn test_c_error_bug_report_url_uses_override_without_trailing_slash() { + assert c_error_bug_report_url(' http://127.0.0.1:19090/bug-report/ ') == 'http://127.0.0.1:19090/bug-report' +} + +fn test_bounded_c_error_bug_report_keeps_encoded_body_under_limit() { + long_output := 'C compiler diagnostic '.repeat(12000) + long_c_line := 'generated C line '.repeat(1000) + long_v_line := 'source V line '.repeat(1000) + report := CErrorBugReport{ + kind: 'v-c-compiler-error' + v_version: 'V test' + target_os: 'linux' + target_backend: 'c' + ccompiler: 'cc' + c_error: long_output + c_file: '/tmp/program.tmp.c' + c_line: 12 + c_context: [ + CErrorReportLine{ + line: 12 + text: long_c_line + }, + ] + v_file: '/tmp/source.v' + v_line: 4 + v_context: [ + CErrorReportLine{ + line: 4 + text: long_v_line + }, + ] + } + bounded := bounded_c_error_bug_report(report, 4096) + encoded := json.encode(bounded) + assert encoded.len <= 4096 + assert bounded.c_error.len < report.c_error.len + assert bounded.c_context[0].line == 12 + assert bounded.c_context[0].text.len < report.c_context[0].text.len + assert bounded.v_context[0].line == 4 + assert bounded.v_context[0].text.len < report.v_context[0].text.len +} + +fn test_truncated_report_text_preserves_start_and_end_when_space_allows() { + text := 'start-' + 'x'.repeat(100) + '-end' + truncated := truncated_report_text(text, 80) + assert truncated.len <= 80 + assert truncated.starts_with('start-') + assert truncated.contains('report truncated before upload') + assert truncated.ends_with('-end') +} diff --git a/vlib/v/builder/cc.v b/vlib/v/builder/cc.v index adc8f412b..4f600c517 100644 --- a/vlib/v/builder/cc.v +++ b/vlib/v/builder/cc.v @@ -286,7 +286,35 @@ fn (mut v Builder) show_c_compiler_output(ccompiler string, res os.Result) { println('='.repeat(header.len)) } +struct CCompilerFailureOutput { + display_ccompiler string + display_res os.Result + report_ccompiler string + report_res os.Result +} + +fn c_compiler_failure_output(ccompiler string, res os.Result, tcc_output os.Result) CCompilerFailureOutput { + if res.exit_code != 0 && tcc_output.output != '' { + return CCompilerFailureOutput{ + display_ccompiler: 'tcc' + display_res: tcc_output + report_ccompiler: ccompiler + report_res: res + } + } + return CCompilerFailureOutput{ + display_ccompiler: ccompiler + display_res: res + report_ccompiler: ccompiler + report_res: res + } +} + fn (mut v Builder) post_process_c_compiler_output(ccompiler string, res os.Result) { + v.post_process_c_compiler_output_with_report(ccompiler, res, ccompiler, res) +} + +fn (mut v Builder) post_process_c_compiler_output_with_report(ccompiler string, res os.Result, report_ccompiler string, report_res os.Result) { if res.exit_code == 0 { if v.pref.reuse_tmpc { return @@ -360,6 +388,7 @@ fn (mut v Builder) post_process_c_compiler_output(ccompiler string, res os.Resul } } } + v.submit_c_error_bug_report(report_ccompiler, report_res.output) if v.pref.is_quiet { exit(1) } @@ -1608,16 +1637,13 @@ pub fn (mut v Builder) cc() { missing_compiler_info()) } } - if !v.pref.show_c_output { - // if tcc failed once, and the system C compiler has failed as well, - // print the tcc error instead since it may contain more useful information - // see https://discord.com/channels/592103645835821068/592115457029308427/811956304314761228 - if res.exit_code != 0 && tcc_output.output != '' { - v.post_process_c_compiler_output('tcc', tcc_output) - } else { - v.post_process_c_compiler_output(ccompiler, res) - } - } + // If tcc failed once, and the system C compiler has failed as well, + // print the tcc error instead since it may contain more useful information. + // Keep the uploaded bug report tied to the final compiler result. + // See https://discord.com/channels/592103645835821068/592115457029308427/811956304314761228 + failure_output := c_compiler_failure_output(ccompiler, res, tcc_output) + v.post_process_c_compiler_output_with_report(failure_output.display_ccompiler, + failure_output.display_res, failure_output.report_ccompiler, failure_output.report_res) // Print the C command if v.pref.is_verbose { println('${ccompiler}') diff --git a/vlib/v/builder/cc_tcc_retry_test.v b/vlib/v/builder/cc_tcc_retry_test.v index 505844941..7d9453d25 100644 --- a/vlib/v/builder/cc_tcc_retry_test.v +++ b/vlib/v/builder/cc_tcc_retry_test.v @@ -19,6 +19,34 @@ fn test_is_tcc_compilation_failure_detects_tcc_output() { assert !is_tcc_compilation_failure('cc', .unknown, 'clang: error: unsupported option') } +fn test_c_compiler_failure_output_reports_final_compiler_after_tcc_retry_fails() { + tcc_res := os.Result{ + exit_code: 1 + output: 'tcc: stale retry failure' + } + clang_res := os.Result{ + exit_code: 1 + output: 'clang: final generated C failure' + } + failure_output := c_compiler_failure_output('clang', clang_res, tcc_res) + assert failure_output.display_ccompiler == 'tcc' + assert failure_output.display_res.output == tcc_res.output + assert failure_output.report_ccompiler == 'clang' + assert failure_output.report_res.output == clang_res.output +} + +fn test_c_compiler_failure_output_reports_displayed_compiler_without_tcc_retry() { + clang_res := os.Result{ + exit_code: 1 + output: 'clang: final generated C failure' + } + failure_output := c_compiler_failure_output('clang', clang_res, os.Result{}) + assert failure_output.display_ccompiler == 'clang' + assert failure_output.display_res.output == clang_res.output + assert failure_output.report_ccompiler == 'clang' + assert failure_output.report_res.output == clang_res.output +} + fn test_is_tcc_compilation_failure_detects_tcc_alias_compiler() { if os.user_os() == 'windows' { return diff --git a/vlib/v/help/build/build-c.txt b/vlib/v/help/build/build-c.txt index 687f8259a..35271bde5 100644 --- a/vlib/v/help/build/build-c.txt +++ b/vlib/v/help/build/build-c.txt @@ -321,6 +321,10 @@ see also `v help build`. -show-c-output Prints the output, that your C compiler produced, while compiling your program. + -bug-report-url url + Override where automatic C compiler bug reports are submitted. + Useful with `v bug-report --host localhost --port 8080` for local testing. + -dump-c-flags file.txt Write all C flags into `file.txt`, one flag per line. If `file.txt` is `-`, write to stdout instead. diff --git a/vlib/v/help/other/other.txt b/vlib/v/help/other/other.txt index e391bbdd3..b9baa6d31 100644 --- a/vlib/v/help/other/other.txt +++ b/vlib/v/help/other/other.txt @@ -5,6 +5,8 @@ Other less frequently used commands supported by V include: bug Post an issue on the V's issue tracker, including the failing program, and some diagnostic information. + bug-report Run the local receiver for automatic C compiler bug reports. + bin2v Convert a binary file to a v source file, that can be later embedded in a module or program. diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 4e73b30fe..837cbc2d8 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -153,6 +153,7 @@ pub mut: show_callgraph bool // -show-callgraph, print the program callgraph, in a Graphviz DOT format to stdout show_depgraph bool // -show-depgraph, print the program module dependency graph, in a Graphviz DOT format to stdout show_unused_params bool = true // regular function params should report as unused by default. + c_error_bug_report_url string // `-bug-report-url url` - override the automatic C compiler bug report endpoint. dump_c_flags string // `-dump-c-flags file.txt` - let V store all C flags, passed to the backend C compiler in `file.txt`, one C flag/value per line. dump_modules string // `-dump-modules modules.txt` - let V store all V modules, that were used by the compiled program in `modules.txt`, one module per line. dump_files string // `-dump-files files.txt` - let V store all V or .template file paths, that were used by the compiled program in `files.txt`, one path per line. @@ -878,6 +879,10 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin '-show-depgraph' { res.show_depgraph = true } + '-bug-report-url' { + res.c_error_bug_report_url = cmdline.option(args[i..], arg, '') + i++ + } '-run-only' { res.run_only = cmdline.option(args[i..], arg, os.getenv('VTEST_ONLY_FN')).split_any(',') -- 2.39.5