From fa32217a4f540c9d5b1dc319de84615b19722fc5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 28 May 2026 18:36:33 +0300 Subject: [PATCH] builder: move C error uploads out of compiler --- cmd/tools/vbug-report-send.v | 102 +++++++++++++++++ cmd/v/v.v | 1 + vlib/v/builder/c_error_report.v | 159 +++++++++++++++++++-------- vlib/v/builder/c_error_report_test.v | 20 +++- 4 files changed, 235 insertions(+), 47 deletions(-) create mode 100644 cmd/tools/vbug-report-send.v diff --git a/cmd/tools/vbug-report-send.v b/cmd/tools/vbug-report-send.v new file mode 100644 index 000000000..e114fe9e2 --- /dev/null +++ b/cmd/tools/vbug-report-send.v @@ -0,0 +1,102 @@ +module main + +import flag +import json +import net.http +import os +import time + +struct CreateBugReportResponse { +pub: + id string + delete_url string + delete_token string + message string +} + +struct SendConfig { + report_url string + report_file string +} + +fn args_without_command() []string { + args := os.args#[1..] + if args.len > 0 && args[0] == 'bug-report-send' { + return args[1..] + } + return args +} + +fn config_from_args() !SendConfig { + mut fp := flag.new_flag_parser(args_without_command()) + fp.application('v bug-report-send') + fp.version('0.0.1') + fp.description('Send a V compiler bug report JSON payload.') + fp.arguments_description('') + show_help := fp.bool('help', `h`, false, 'Show this help screen.') + report_url := fp.string('url', 0, '', 'Bug report endpoint URL.') + report_file := fp.string('file', `f`, '', 'JSON report file to send.') + if show_help { + println(fp.usage()) + exit(0) + } + remaining := fp.finalize()! + if remaining.len > 0 { + return error('unexpected arguments: ${remaining.join(' ')}') + } + if report_url == '' { + return error('missing required -url') + } + if report_file == '' { + return error('missing required -file') + } + return SendConfig{ + report_url: report_url.trim_space().trim_right('/') + report_file: report_file + } +} + +fn send_bug_report(config SendConfig) !CreateBugReportResponse { + report_json := os.read_file(config.report_file)! + mut header := http.new_header(key: .content_type, value: 'application/json') + header.set(.accept, 'application/json') + response := http.fetch( + method: .post + url: config.report_url + data: report_json + 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(CreateBugReportResponse, response.body)! +} + +fn main() { + config := config_from_args() or { + eprintln('v bug-report-send: ${err}') + exit(1) + } + response := send_bug_report(config) or { + eprintln('v bug-report-send: ${err}') + exit(1) + } + println('Sent C compiler bug report to ${config.report_url}.') + if response.id != '' { + println('Report id: ${response.id}') + } + delete_url := if response.delete_url != '' { + response.delete_url + } else if response.id != '' && response.delete_token != '' { + '${config.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)}') + } +} diff --git a/cmd/v/v.v b/cmd/v/v.v index 27cbc0310..5f0041ee8 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -19,6 +19,7 @@ const external_tools = [ 'bin2v', 'bug', 'bug-report', + 'bug-report-send', 'build-examples', 'build-tools', 'build-vbinaries', diff --git a/vlib/v/builder/c_error_report.v b/vlib/v/builder/c_error_report.v index 0dfa5d1e0..5c1bd780b 100644 --- a/vlib/v/builder/c_error_report.v +++ b/vlib/v/builder/c_error_report.v @@ -1,9 +1,8 @@ module builder -import json -import net.http import os -import time +import strings +import v.pref import v.util.version const default_c_error_bug_report_url = 'https://vlang.io/bug-report' @@ -39,39 +38,19 @@ pub: 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 { + tool_output := 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}') + if tool_output != '' { + println(tool_output) } 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)) } @@ -126,26 +105,118 @@ fn c_error_bug_report_url(flag_url string) string { 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}') +fn send_c_error_bug_report(report CErrorBugReport, report_url string) !string { + report_path := os.join_path(os.vtmp_dir(), 'v-c-error-report-${os.getpid()}.json') + os.write_file(report_path, c_error_bug_report_json(report))! + defer { + os.rm(report_path) or {} + } + cmd := '${os.quoted_path(pref.vexe_path())} bug-report-send --url ${os.quoted_path(report_url)} --file ${os.quoted_path(report_path)}' + res := os.execute(cmd) + if res.exit_code != 0 { + return error(res.output.trim_space()) + } + return res.output.trim_right('\r\n') +} + +fn c_error_bug_report_json(report CErrorBugReport) string { + mut b := strings.new_builder(1024 + report.c_error.len) + b.write_u8(`{`) + write_json_string_field(mut b, 'kind', report.kind, false) + write_json_string_field(mut b, 'v_version', report.v_version, true) + write_json_string_field(mut b, 'target_os', report.target_os, true) + write_json_string_field(mut b, 'target_backend', report.target_backend, true) + write_json_string_field(mut b, 'ccompiler', report.ccompiler, true) + write_json_string_field(mut b, 'c_error', report.c_error, true) + write_json_string_field(mut b, 'c_file', report.c_file, true) + write_json_int_field(mut b, 'c_line', report.c_line, true) + write_json_report_lines_field(mut b, 'c_context', report.c_context, true) + write_json_string_field(mut b, 'v_file', report.v_file, true) + write_json_int_field(mut b, 'v_line', report.v_line, true) + write_json_report_lines_field(mut b, 'v_context', report.v_context, true) + b.write_u8(`}`) + return b.str() +} + +fn write_json_string_field(mut b strings.Builder, name string, value string, needs_comma bool) { + write_json_field_name(mut b, name, needs_comma) + write_json_string(mut b, value) +} + +fn write_json_int_field(mut b strings.Builder, name string, value int, needs_comma bool) { + write_json_field_name(mut b, name, needs_comma) + b.write_string(value.str()) +} + +fn write_json_report_lines_field(mut b strings.Builder, name string, lines []CErrorReportLine, needs_comma bool) { + write_json_field_name(mut b, name, needs_comma) + b.write_u8(`[`) + for idx, line in lines { + if idx > 0 { + b.write_u8(`,`) + } + b.write_u8(`{`) + write_json_int_field(mut b, 'line', line.line, false) + write_json_string_field(mut b, 'text', line.text, true) + b.write_u8(`}`) + } + b.write_u8(`]`) +} + +fn write_json_field_name(mut b strings.Builder, name string, needs_comma bool) { + if needs_comma { + b.write_u8(`,`) + } + write_json_string(mut b, name) + b.write_u8(`:`) +} + +fn write_json_string(mut b strings.Builder, value string) { + b.write_u8(`"`) + for ch in value.bytes() { + match ch { + `"` { + b.write_string('\\"') + } + `\\` { + b.write_string('\\\\') + } + `\b` { + b.write_string('\\b') + } + `\f` { + b.write_string('\\f') + } + `\n` { + b.write_string('\\n') + } + `\r` { + b.write_string('\\r') + } + `\t` { + b.write_string('\\t') + } + else { + if ch < 0x20 { + write_json_control_escape(mut b, ch) + } else { + b.write_u8(ch) + } + } + } } - return json.decode(CErrorBugReportResponse, response.body)! + b.write_u8(`"`) +} + +fn write_json_control_escape(mut b strings.Builder, ch u8) { + hex := '0123456789abcdef' + b.write_string('\\u00') + b.write_u8(hex[ch >> 4]) + b.write_u8(hex[ch & 0x0f]) } 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 { + if max_body_bytes <= 0 || c_error_bug_report_json(report).len <= max_body_bytes { return report } if bounded := report_with_bounded_c_error(report, max_body_bytes, report.c_context, @@ -175,7 +246,7 @@ fn report_with_bounded_c_error(report CErrorBugReport, max_body_bytes int, c_con c_context: c_context v_context: v_context } - if json.encode(min_report).len > max_body_bytes { + if c_error_bug_report_json(min_report).len > max_body_bytes { return none } mut low := 0 @@ -189,7 +260,7 @@ fn report_with_bounded_c_error(report CErrorBugReport, max_body_bytes int, c_con c_context: c_context v_context: v_context } - if json.encode(candidate).len <= max_body_bytes { + if c_error_bug_report_json(candidate).len <= max_body_bytes { best = candidate low = mid + 1 } else { diff --git a/vlib/v/builder/c_error_report_test.v b/vlib/v/builder/c_error_report_test.v index b79ffb7f7..a71dbf710 100644 --- a/vlib/v/builder/c_error_report_test.v +++ b/vlib/v/builder/c_error_report_test.v @@ -1,7 +1,5 @@ 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 { @@ -116,7 +114,7 @@ fn test_bounded_c_error_bug_report_keeps_encoded_body_under_limit() { ] } bounded := bounded_c_error_bug_report(report, 4096) - encoded := json.encode(bounded) + encoded := c_error_bug_report_json(bounded) assert encoded.len <= 4096 assert bounded.c_error.len < report.c_error.len assert bounded.c_context[0].line == 12 @@ -125,6 +123,22 @@ fn test_bounded_c_error_bug_report_keeps_encoded_body_under_limit() { assert bounded.v_context[0].text.len < report.v_context[0].text.len } +fn test_c_error_bug_report_json_escapes_strings() { + report := CErrorBugReport{ + kind: 'v-c-compiler-error' + v_version: 'V "test"\n' + c_context: [ + CErrorReportLine{ + line: 1 + text: 'tab\tslash\\' + }, + ] + } + encoded := c_error_bug_report_json(report) + assert encoded.contains('"v_version":"V \\"test\\"\\n"') + assert encoded.contains('"text":"tab\\tslash\\\\"') +} + fn test_truncated_report_text_preserves_start_and_end_when_space_allows() { text := 'start-' + 'x'.repeat(100) + '-end' truncated := truncated_report_text(text, 80) -- 2.39.5