From d4fa0cabcf1ae9efba31ab8a203fca069d74df0f Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:49:50 -0700 Subject: [PATCH 1/8] improve rp6502_xram --- src/paint/paint.c | 52 +++--- src/paint/xram.h | 17 +- tools/rp6502.cmake | 391 +++++++++++++++++++++++++++++++++------------ 3 files changed, 324 insertions(+), 136 deletions(-) diff --git a/src/paint/paint.c b/src/paint/paint.c index a5eaf0d..0f2ee04 100644 --- a/src/paint/paint.c +++ b/src/paint/paint.c @@ -70,8 +70,8 @@ static void setup_bitmap(unsigned config, int width, int height, unsigned data) // The point of the arrow is one pixel in from the corner of its image. static void move_pointer(int x, int y) { - xram0_struct_set(XRAM_POINTER_CONFIG, mode3_config_t, x_pos_px, x - 1); - xram0_struct_set(XRAM_POINTER_CONFIG, mode3_config_t, y_pos_px, y - 1); + xram0_struct_set(XRAM_CONFIG_POINTER, mode3_config_t, x_pos_px, x - 1); + xram0_struct_set(XRAM_CONFIG_POINTER, mode3_config_t, y_pos_px, y - 1); } static void draw_pointer(void) @@ -86,7 +86,7 @@ static void draw_pointer(void) }; // clang-format on unsigned i; - RIA.addr0 = XRAM_POINTER_DATA; + RIA.addr0 = XRAM_DATA_POINTER; RIA.step0 = 1; for (i = 0; i < sizeof(image); i++) RIA.rw0 = image[i]; @@ -113,7 +113,7 @@ static void mouse_sample(void) VIA.ifr = 0x40; // acknowledge timer 1 - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, x); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, x); RIA.step0 = 1; count = RIA.rw0; raw_x += (int8_t)(count - mouse_last_x); @@ -155,8 +155,8 @@ static void mouse_init(void) // in 8 ms. unsigned period = ria_attr_get(RIA_ATTR_PHI2_KHZ) * 8 - 2; - xreg_ria_mouse(XRAM_MOU_DATA); - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, x); + xreg_ria_mouse(XRAM_DATA_MOU); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, x); RIA.step0 = 1; mouse_last_x = RIA.rw0; mouse_last_y = RIA.rw0; @@ -183,7 +183,7 @@ static uint8_t mouse_read(int *x, int *y) *x = mouse_x; *y = mouse_y; CLI(); - RIA.addr0 = XRAM_MOU_DATA + offsetof(mouse_t, buttons); + RIA.addr0 = XRAM_DATA_MOU + offsetof(mouse_t, buttons); return RIA.rw0 & (MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT); } @@ -195,16 +195,12 @@ static uint8_t mouse_read(int *x, int *y) // the host can draw a cursor, as the emulator can for a mouse, the program // hides its own pointer and asks for a crosshair. -#define TABLET_CONTROL (XRAM_TAB_DATA + offsetof(tablet_t, control)) -#define TABLET_STATUS (XRAM_TAB_DATA + offsetof(tablet_t, status)) -#define TABLET_CONTACT (XRAM_TAB_DATA + offsetof(tablet_t, contact)) - static int tablet_x, tablet_y; static bool host_cursor; static void tablet_init(void) { - xreg_ria_tablet(XRAM_TAB_DATA); + xreg_ria_tablet(XRAM_DATA_TAB); } static uint8_t tablet_read(int *x, int *y) @@ -213,12 +209,12 @@ static uint8_t tablet_read(int *x, int *y) bool offered; int tries; - RIA.addr0 = TABLET_STATUS; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, status); offered = RIA.rw0 & TABLET_STATUS_HOST_CURSOR; if (offered != host_cursor) { host_cursor = offered; - RIA.addr0 = TABLET_CONTROL; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, control); RIA.rw0 = host_cursor ? TABLET_CURSOR_CROSSHAIR : TABLET_CURSOR_OFF; } @@ -226,7 +222,7 @@ static uint8_t tablet_read(int *x, int *y) // every window zero, so the contact is read a second time. for (tries = 0; tries < 2; tries++) { - RIA.addr0 = TABLET_CONTACT; + RIA.addr0 = XRAM_DATA_TAB + offsetof(tablet_t, contact); RIA.step0 = 1; flags = RIA.rw0; x0 = RIA.rw0; @@ -266,7 +262,7 @@ static uint8_t tablet_read(int *x, int *y) static void erase_picture(void) { unsigned i; - RIA.addr0 = XRAM_PICTURE_DATA; + RIA.addr0 = XRAM_DATA_PICTURE; RIA.step0 = 1; for (i = 0; i < CANVAS_WIDTH / 2 * (unsigned)CANVAS_HEIGHT; i++) RIA.rw0 = 0; @@ -276,7 +272,7 @@ static void erase_picture(void) // program starts, and this one, a file to put it back. static void load_logo(void) { - unsigned addr = XRAM_PICTURE_DATA; + unsigned addr = XRAM_DATA_PICTURE; int fd = open("ROM:logo", O_RDONLY); int count; while ((count = read_xram(addr, 0x7FFF, fd)) > 0) @@ -289,7 +285,7 @@ static void draw_pixel(int x, int y) { uint8_t pair; RIA.step0 = 0; - RIA.addr0 = XRAM_PICTURE_DATA + (unsigned)y * (CANVAS_WIDTH / 2) + x / 2; + RIA.addr0 = XRAM_DATA_PICTURE + (unsigned)y * (CANVAS_WIDTH / 2) + x / 2; pair = RIA.rw0; if (x & 1) RIA.rw0 = (pair & 0xF0) | draw_color; @@ -337,7 +333,7 @@ static void draw_picker_box(uint8_t shade, int x1, int y1, int x2, int y2) RIA.step0 = 1; for (y = y1; y <= y2; y++) { - RIA.addr0 = XRAM_PICKER_DATA + PICKER_WIDTH * y + x1; + RIA.addr0 = XRAM_DATA_PICKER + PICKER_WIDTH * y + x1; for (x = x1; x <= x2; x++) RIA.rw0 = shade; } @@ -382,8 +378,8 @@ static void move_picker(int x, int y) { picker_x = clamp(x, 0, CANVAS_WIDTH - PICKER_WIDTH); picker_y = clamp(y, 0, CANVAS_HEIGHT - PICKER_HEIGHT); - xram0_struct_set(XRAM_PICKER_CONFIG, mode3_config_t, x_pos_px, picker_x); - xram0_struct_set(XRAM_PICKER_CONFIG, mode3_config_t, y_pos_px, picker_y); + xram0_struct_set(XRAM_CONFIG_PICKER, mode3_config_t, x_pos_px, picker_x); + xram0_struct_set(XRAM_CONFIG_PICKER, mode3_config_t, y_pos_px, picker_y); } static int picker_pick(int x, int y) @@ -470,10 +466,10 @@ int main(int argc, char *argv[]) load_logo(); - xreg_vga_canvas(CANVAS_320X240); - setup_bitmap(XRAM_PICTURE_CONFIG, CANVAS_WIDTH, CANVAS_HEIGHT, XRAM_PICTURE_DATA); - setup_bitmap(XRAM_PICKER_CONFIG, PICKER_WIDTH, PICKER_HEIGHT, XRAM_PICKER_DATA); - setup_bitmap(XRAM_POINTER_CONFIG, POINTER_SIZE, POINTER_SIZE, XRAM_POINTER_DATA); + xreg_vga_canvas(1); // 320x240 + setup_bitmap(XRAM_CONFIG_PICTURE, CANVAS_WIDTH, CANVAS_HEIGHT, XRAM_DATA_PICTURE); + setup_bitmap(XRAM_CONFIG_PICKER, PICKER_WIDTH, PICKER_HEIGHT, XRAM_DATA_PICKER); + setup_bitmap(XRAM_CONFIG_POINTER, POINTER_SIZE, POINTER_SIZE, XRAM_DATA_POINTER); draw_picker(); move_picker((CANVAS_WIDTH - PICKER_WIDTH) / 2, 0); @@ -481,9 +477,9 @@ int main(int argc, char *argv[]) set_color(RIGHT, 0); draw_pointer(); - xreg_vga_mode3(MODE3_4BPP, XRAM_PICTURE_CONFIG, 0); // plane 0 - xreg_vga_mode3(MODE3_8BPP, XRAM_PICKER_CONFIG, 1); // plane 1 - xreg_vga_mode3(MODE3_8BPP, XRAM_POINTER_CONFIG, 2); // plane 2 + xreg_vga_mode3(2, XRAM_CONFIG_PICTURE, 0); // 4 bits per pixel, plane 0 + xreg_vga_mode3(3, XRAM_CONFIG_PICKER, 1); // 8 bits per pixel, plane 1 + xreg_vga_mode3(3, XRAM_CONFIG_POINTER, 2); // 8 bits per pixel, plane 2 if (use_mouse) mouse_init(); diff --git a/src/paint/xram.h b/src/paint/xram.h index 5395edd..eb81934 100644 --- a/src/paint/xram.h +++ b/src/paint/xram.h @@ -115,13 +115,14 @@ typedef struct uint8_t pointer[POINTER_SIZE * POINTER_SIZE]; } xram_layout_t; -#define XRAM_PICTURE_CONFIG offsetof(xram_layout_t, picture_config) -#define XRAM_PICKER_CONFIG offsetof(xram_layout_t, picker_config) -#define XRAM_POINTER_CONFIG offsetof(xram_layout_t, pointer_config) -#define XRAM_TAB_DATA offsetof(xram_layout_t, tab) -#define XRAM_MOU_DATA offsetof(xram_layout_t, mou) -#define XRAM_PICTURE_DATA offsetof(xram_layout_t, picture) -#define XRAM_PICKER_DATA offsetof(xram_layout_t, picker) -#define XRAM_POINTER_DATA offsetof(xram_layout_t, pointer) +#define XRAM_CONFIG_PICTURE offsetof(xram_layout_t, picture_config) +#define XRAM_CONFIG_PICKER offsetof(xram_layout_t, picker_config) +#define XRAM_CONFIG_POINTER offsetof(xram_layout_t, pointer_config) + +#define XRAM_DATA_TAB offsetof(xram_layout_t, tab) +#define XRAM_DATA_MOU offsetof(xram_layout_t, mou) +#define XRAM_DATA_PICTURE offsetof(xram_layout_t, picture) +#define XRAM_DATA_PICKER offsetof(xram_layout_t, picker) +#define XRAM_DATA_POINTER offsetof(xram_layout_t, pointer) #endif diff --git a/tools/rp6502.cmake b/tools/rp6502.cmake index d8d2c3e..4db0501 100644 --- a/tools/rp6502.cmake +++ b/tools/rp6502.cmake @@ -382,6 +382,12 @@ function(rp6502_executable name) VERBATIM ) add_custom_target(${name}_rp6502 ALL DEPENDS "${rom_file}") + # A layout that fails its checks must not produce a ROM. + get_directory_property(xram_checks RP6502_XRAM_CHECKS) + if (xram_checks) + add_dependencies(${name}_rp6502 ${xram_checks}) + endif() + set_property(DIRECTORY APPEND PROPERTY RP6502_ROM_TARGETS "${name}_rp6502") # Mark that rp6502_executable has been called for this target set_property(TARGET ${name} PROPERTY RP6502_EXECUTABLE_CALLED TRUE) endfunction() @@ -391,23 +397,74 @@ endfunction() # RP6502 Asset ROM # ^^^^^^^^^^^^^^^^ # -# rp6502_asset( address in_file) +# rp6502_asset(
) # # If the address is numeric, the in_file will be loaded into # RAM ($0-FFFF) or XRAM ($10000-1FFFF) when the ROM is loaded. # Non-numeric addresses become filenames that can be opened # with "ROM:filename" from a micro filesystem in the ROM. -# A name defined by rp6502_xram() is the XRAM address it stands for. +# Writing the address as RAM() or XRAM() checks that it is in range, +# and XRAM() sets the bit that tells XRAM from RAM, so an offset from +# rp6502_xram() loads into XRAM. Inside the parentheses, is a number +# or the name of a CMake variable. # -function(rp6502_asset name addr in_file) +function(rp6502_asset name) get_target_property(executable_called ${name} RP6502_EXECUTABLE_CALLED) if (executable_called) message(FATAL_ERROR "rp6502_asset(${name} ...) must be registered BEFORE calling rp6502_executable()." ) endif() - if (DEFINED ${addr}) - set(addr "${${addr}}") + # CMake gives every parenthesis to a command as an argument of its own, + # so RAM() arrives here as four arguments and nothing named RAM or + # XRAM is ever defined. + set(args ${ARGN}) + list(LENGTH args argc) + list(GET args 0 addr) + if (addr STREQUAL "RAM" OR addr STREQUAL "XRAM") + set(form "${addr}") + if (NOT argc EQUAL 5) + message(FATAL_ERROR "rp6502_asset(${name} ${form}(
) )") + endif() + list(GET args 1 opened) + list(GET args 3 closed) + if (NOT opened STREQUAL "(" OR NOT closed STREQUAL ")") + message(FATAL_ERROR "rp6502_asset(${name} ${form}(
) )") + endif() + list(GET args 2 value) + if (DEFINED ${value}) + set(value "${${value}}") + endif() + set(written "${value}") + # A leading $ is how a 6502 program writes hex, which rp6502.py + # takes as well. + string(REGEX REPLACE "^\\$" "0x" value "${value}") + if (NOT value MATCHES "^[-+]?(0[xX][0-9a-fA-F]+|[0-9]+)$") + message(FATAL_ERROR + "rp6502_asset(${name} ${form}(...)): ${written} is not a number.") + endif() + if (form STREQUAL "RAM") + set(limit 65535) + set(ends "0xFFFF") + else() + set(limit 131071) + set(ends "0x1FFFF") + endif() + math(EXPR value "(${value})") + if (value LESS 0 OR value GREATER ${limit}) + message(FATAL_ERROR + "rp6502_asset(${name} ${form}(...)): ${written} is outside ${form}, which ends at ${ends}.") + endif() + if (form STREQUAL "XRAM") + math(EXPR value "${value} | 0x10000") + endif() + math(EXPR addr "${value}" OUTPUT_FORMAT HEXADECIMAL) + list(GET args 4 in_file) + else() + if (NOT argc EQUAL 2) + message(FATAL_ERROR "rp6502_asset(
)") + endif() + list(GET args 1 in_file) endif() get_filename_component(src_file "${in_file}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") file(RELATIVE_PATH rel_path "${CMAKE_SOURCE_DIR}" "${src_file}") @@ -434,104 +491,184 @@ function(rp6502_asset name addr in_file) ) endfunction() -# Give CMake the XRAM addresses a header defines. +# Give CMake the addresses a header defines. # # RP6502 XRAM Layout # ^^^^^^^^^^^^^^^^^^ # # rp6502_xram(
[]) # -# Reads ``#define NAME offsetof(...)`` lines from ``
`` whose NAME -# matches ```` and sets each NAME as a variable holding the XRAM -# address it stands for. The names then work as rp6502_asset() addresses. +# Reads the ``#define`` lines of ``
`` whose name matches +# ```` and sets each name as a variable holding the value the +# header computes. The names then work as rp6502_asset() addresses. # Names matching ```` are exempt from 16-bit alignment. +# A commented out define, a define with no value, and a function-like +# macro are all skipped. +# +# The layout is checked while the project builds rather than while it +# configures, so a header that will not compile still leaves a configured +# project behind, and every problem is reported by the compiler against +# the line in the header. # function(rp6502_xram header regex) set(unaligned "${ARGV2}") get_filename_component(header_file "${header}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") get_filename_component(header_name "${header_file}" NAME) + get_filename_component(header_dir "${header_file}" DIRECTORY) get_filename_component(stem "${header_file}" NAME_WE) + get_directory_property(rom_targets RP6502_ROM_TARGETS) + if (rom_targets) + message(FATAL_ERROR + "rp6502_xram(${header}) must be registered BEFORE calling rp6502_executable()." + ) + endif() # Editing the layout has to configure again, since these values are read # at configure time. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${header_file}") - file(READ "${header_file}" text) - # A definition may continue onto the next line. - string(REGEX REPLACE "\\\\[ \t]*\r?\n" " " text "${text}") - # CMake's ^ is the start of the input, not of a line, so each definition is - # found with the newline ahead of it. - string(REGEX MATCHALL - "\n[ \t]*#[ \t]*define[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]+offsetof[ \t]*\\([^,]+," - defines "\n${text}") + # Read the header a line at a time. file(STRINGS), and every idiom that + # escapes the text and splits it, join a line ending in a backslash to + # the line after it, which moves every line number that follows. + # Nothing here reads comments or conditionals, because each name is + # written out under an #ifdef and the preprocessor settles what exists. + file(READ "${header_file}" rest) + string(REPLACE "\r\n" "\n" rest "${rest}") set(names) - set(types) - foreach(define IN LISTS defines) - string(REGEX MATCH - "define[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+offsetof[ \t]*\\(([^,]+)," - ignored "${define}") - set(name "${CMAKE_MATCH_1}") - string(STRIP "${CMAKE_MATCH_2}" type) - if (name MATCHES "^(${regex})$") - list(APPEND names "${name}") - list(APPEND types "${type}") + set(lines) + set(values) + set(lineno 0) + set(pending "") + set(pending_line 0) + while(TRUE) + string(FIND "${rest}" "\n" pos) + if (pos LESS 0) + set(line "${rest}") + set(rest "") + set(last TRUE) + else() + string(SUBSTRING "${rest}" 0 ${pos} line) + math(EXPR pos "${pos}+1") + string(SUBSTRING "${rest}" ${pos} -1 rest) + set(last FALSE) + endif() + math(EXPR lineno "${lineno}+1") + + # A continued definition is read under the number of its first line. + if (pending STREQUAL "") + set(cur "${line}") + set(cur_line ${lineno}) + else() + set(cur "${pending}${line}") + set(cur_line ${pending_line}) + endif() + if (cur MATCHES "\\\\$") + string(REGEX REPLACE "\\\\$" " " pending "${cur}") + set(pending_line ${cur_line}) + else() + set(pending "") + if (cur MATCHES "^[ \t]*#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)([^A-Za-z0-9_(].*)$") + set(name "${CMAKE_MATCH_1}") + string(STRIP "${CMAKE_MATCH_2}" value) + # An include guard has no value and only integers are carried. + if (NOT value STREQUAL "" AND NOT value MATCHES "^[\"']" + AND name MATCHES "^(${regex})$") + list(APPEND names "${name}") + list(APPEND lines "${cur_line}") + list(APPEND values "${value}") + endif() + endif() + endif() + if (last) + break() + endif() + endwhile() + + # Every structure an offsetof names is probed below, whatever else is + # written around it, and the first name that uses one carries its line. + set(probes) + set(probe_lines) + set(probe_guards) + foreach(name value line IN ZIP_LISTS names values lines) + if (value MATCHES "offsetof[ \t]*\\(([^,]+),") + string(STRIP "${CMAKE_MATCH_1}" type) + list(FIND probes "${type}" index) + if (index LESS 0) + list(APPEND probes "${type}") + list(APPEND probe_lines "${line}") + list(APPEND probe_guards "defined(${name})") + else() + # Any one of them being defined is enough to probe with. + list(GET probe_guards ${index} guard) + list(REMOVE_AT probe_guards ${index}) + list(INSERT probe_guards ${index} "${guard} || defined(${name})") + endif() endif() endforeach() - set(distinct_types ${types}) - if (distinct_types) - list(REMOVE_DUPLICATES distinct_types) - endif() - # A program that prints the addresses. Everything is unsigned long, so - # neither compiler's 16 bit size_t truncates what it prints or compares. - # Included by name with -I below, because cc65 cannot find a quoted - # include given as an absolute path. + set(dir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${stem}.xram") + string(REPLACE "\\" "/" header_c "${header_file}") + + # A program that prints the values. Everything is unsigned long, so + # neither compiler's 16 bit size_t truncates what it prints. Included by + # name with -I below, because cc65 cannot find a quoted include given as + # an absolute path. set(stub "#include \n#include \"${header_name}\"\n\nint main(void)\n{\n") - list(LENGTH distinct_types type_count) - if (type_count) - string(APPEND stub " int too_large[${type_count}] = {0};\n") - endif() - string(APPEND stub " int bad = 0;\n\n") - foreach(name type IN ZIP_LISTS names types) - list(FIND distinct_types "${type}" type_index) + foreach(name line IN ZIP_LISTS names lines) string(APPEND stub - " printf(\"${name} 0x%lX\\n\", 0x10000UL + (unsigned long)${name});\n" - " if ((unsigned long)${name} >= (unsigned long)sizeof(${type}))\n" - " too_large[${type_index}] = 1;\n") + "#ifdef ${name}\n" + "#line ${line} \"${header_c}\"\n" + " printf(\"${name} 0x%lX\\n\", (unsigned long)${name});\n" + "#endif\n") + endforeach() + string(APPEND stub " return 0;\n}\n") + file(WRITE "${dir}/stub.c" "${stub}") + + # A program of assertions, compiled but never run, so the compiler + # reports a bad layout against the line in the header. Every name gets a + # line of its own, since a name with no assertion would let a define + # that will not compile through. Each declaration is one line, because + # cc65 reports the line it finished reading rather than the one it + # started on. + set(check "#include \n#include \"${header_name}\"\n\n") + string(APPEND check + "/* size_t is 16 bits, so a structure larger than 64K wraps instead of\n" + " being refused. An array of one is refused outright, because the\n" + " size of an array is checked against the largest object the target\n" + " allows and not against the truncated sizeof. */\n") + foreach(type line guard IN ZIP_LISTS probes probe_lines probe_guards) + string(MAKE_C_IDENTIFIER "${type}" probe) + string(APPEND check + "\n#if ${guard}\n#line ${line} \"${header_c}\"\n" + "extern ${type} rp6502_xram_${probe}_fits[1];\n#endif\n") + endforeach() + # An offset and arithmetic between offsets are size_t, so 16 bits, and + # can never trip this. A number that does not fit is a long and does. + foreach(name line IN ZIP_LISTS names lines) + string(APPEND check + "\n#ifdef ${name}\n#line ${line} \"${header_c}\"\n" + "_Static_assert((${name}) < 0x10000L," + " \"${name} overflows 16 bits.\");\n") if (NOT unaligned OR NOT name MATCHES "^(${unaligned})$") - string(APPEND stub - " if ((unsigned long)${name} & 1UL)\n" - " {\n" - " printf(\"${header_name}: ${name} is unaligned at $%lX.\"\n" - " \" To allow, use the []\"\n" - " \" in rp6502_xram.\\n\", (unsigned long)${name});\n" - " bad = 1;\n" - " }\n") + string(APPEND check + "#line ${line} \"${header_c}\"\n" + "_Static_assert(!((${name}) & 1), \"${name} is unaligned." + " To allow, use the [] in rp6502_xram.\");\n") endif() + string(APPEND check "#endif\n") endforeach() - foreach(type IN LISTS distinct_types) - list(FIND distinct_types "${type}" type_index) - # The words cc65 uses when it rejects the same layout itself. - string(APPEND stub - " if (too_large[${type_index}])\n" - " {\n" - " printf(\"${header_name}: Error: Size of '${type}'\"\n" - " \" is too large\\n\");\n" - " bad = 1;\n" - " }\n") - endforeach() - string(APPEND stub "\n return bad;\n}\n") - set(dir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${stem}.xram") - file(WRITE "${dir}/stub.c" "${stub}") + file(WRITE "${dir}/check.c" "${check}") - # cc65's CMAKE_C_COMPILER is a wrapper around cl65 for the IDE. - set(compiler "${CMAKE_C_COMPILER}") - if (CC65_C_COMPILER) - set(compiler "${CC65_C_COMPILER}") + # cc65's CMAKE_C_COMPILER is a wrapper around cl65 that puts diagnostics + # in the form an IDE matches, so both programs are built through it. + set(compiler_args) + if (CMAKE_C_COMPILER_ARG1) + separate_arguments(compiler_args NATIVE_COMMAND "${CMAKE_C_COMPILER_ARG1}") endif() - get_filename_component(header_dir "${header_file}" DIRECTORY) separate_arguments(flags NATIVE_COMMAND "${CMAKE_C_FLAGS}") + + set(failed FALSE) execute_process( - COMMAND "${compiler}" ${flags} -I "${header_dir}" + COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" -o "${dir}/stub" "${dir}/stub.c" WORKING_DIRECTORY "${dir}" RESULT_VARIABLE result @@ -539,43 +676,97 @@ function(rp6502_xram header regex) ERROR_VARIABLE output ) if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + set(failed TRUE) endif() - rp6502_default_address(load_addr) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process( - COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" - -a "${load_addr}" -r "${load_addr}" - -o "${dir}/stub.rp6502" create "${dir}/stub" - RESULT_VARIABLE result - OUTPUT_VARIABLE output - ERROR_VARIABLE output - ) - if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + if (NOT failed) + rp6502_default_address(load_addr) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" + -a "${load_addr}" -r "${load_addr}" + -o "${dir}/stub.rp6502" create "${dir}/stub" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + if (NOT result EQUAL 0) + set(failed TRUE) + endif() endif() - execute_process( - COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" - -c "${RP6502_PROJECT_DIR}/.rp6502" - execute "${dir}/stub.rp6502" - TIMEOUT 60 - RESULT_VARIABLE result - OUTPUT_VARIABLE output - ERROR_VARIABLE output - ) - if (NOT result EQUAL 0) - message(FATAL_ERROR "rp6502_xram(${header})\n${output}") + if (NOT failed) + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" + -c "${RP6502_PROJECT_DIR}/.rp6502" + execute "${dir}/stub.rp6502" + TIMEOUT 60 + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + if (NOT result EQUAL 0) + set(failed TRUE) + endif() endif() + # A failure here is the header's, and the build reports it in full, so + # the configure finishes with every address at zero rather than leaving + # the project unconfigured. string(REPLACE "\r" "" output "${output}") foreach(name IN LISTS names) - if (NOT output MATCHES "(^|\n)${name} (0x[0-9A-Fa-f]+)") - message(FATAL_ERROR "rp6502_xram(${header}) found no address for ${name}\n${output}") + if (failed) + set(${name} 0 PARENT_SCOPE) + elseif (output MATCHES "(^|\n)${name} (0x[0-9A-Fa-f]+)") + # Out of range is zeroed, so it reaches rp6502_asset() as nothing + # while the build reports it against the header. + set(found "${CMAKE_MATCH_2}") + math(EXPR numeric "${found}") + if (numeric GREATER 65535) + set(found 0) + endif() + set(${name} "${found}" PARENT_SCOPE) + else() + # The preprocessor skipped it, so it is not a name at all. + unset(${name} PARENT_SCOPE) endif() - set(${name} "${CMAKE_MATCH_2}" PARENT_SCOPE) endforeach() + # A header that will not compile is reported by the compile below, but a + # tool that did not run leaves a header that compiles and every address + # at zero, which would otherwise build a ROM that loads everything over + # the start of XRAM. So the reason is carried to the build and fails it. + set(unread) + if (failed) + message(STATUS "rp6502_xram(${header}) read no addresses; the build reports why.") + file(WRITE "${dir}/unread.txt" + "rp6502_xram(${header}) read no addresses, so every name is zero.\n${output}\n") + set(unread + COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/unread.txt" + COMMAND "${CMAKE_COMMAND}" -E false) + endif() + + # Target names are global, and one header can be shared by two + # directories, so the name carries where it was called from. + file(RELATIVE_PATH id "${CMAKE_SOURCE_DIR}" "${header_file}") + if (id MATCHES "^\\.\\.") + set(id "${header_file}") + endif() + string(MAKE_C_IDENTIFIER "${id}" id) + string(MD5 hash "${CMAKE_CURRENT_BINARY_DIR}|${header_file}") + string(SUBSTRING "${hash}" 0 8 hash) + set(target "rp6502_xram_${id}_${hash}") + add_custom_command( + OUTPUT "${dir}/check.stamp" + DEPENDS "${header_file}" "${dir}/check.c" + COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" + -c -o "${dir}/check.o" "${dir}/check.c" + ${unread} + COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/check.stamp" + COMMENT "Checking ${header_name}" + VERBATIM + ) + add_custom_target(${target} ALL DEPENDS "${dir}/check.stamp") + set_property(DIRECTORY APPEND PROPERTY RP6502_XRAM_CHECKS "${target}") endfunction() # Declare files as byproducts of building . From 7198a90e35aeccc15b677309473730c35d1d57ae Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:15:41 -0700 Subject: [PATCH 2/8] ship example ROMs from CI Every commit to main builds the examples with cc65 from the official repository and publishes rp6502--examples.zip, holding one ROM per example and a README saying what they are. The generated programs rp6502_xram() leaves in the build tree are named for the function now. One of them was a plain stub.rp6502, which reads like a ROM anyone would ship. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 103 +++++++++++++++++++++++++++++++++++++++ src/README.txt | 7 +++ tools/rp6502.cmake | 24 ++++----- 3 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 src/README.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..21b31dc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + # A commit to main gets a group of its own. Sharing one group would let a + # third push cancel the second while it waits, and that commit would ship + # no release without anything failing. Pull requests still collapse. + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + examples: + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + # From source, the way the SDK tells everyone to install it. bin/cl65 + # finds its own ../target, so there is nothing to install afterwards. + - name: Build cc65 + run: | + set -euo pipefail + git clone --depth 1 https://github.com/cc65/cc65 "$RUNNER_TEMP/cc65" + make -C "$RUNNER_TEMP/cc65" -j"$(nproc)" + echo "$RUNNER_TEMP/cc65/bin" >> "$GITHUB_PATH" + + # rp6502_xram() runs a program on the emulator while CMake configures. + # These two variables skip the tool download, so the examples build + # against the tools this repository holds rather than the newest ones. + - name: Fetch the emulator + run: | + set -euo pipefail + cmake -DRP6502_TOOLS_RELOADED=TRUE -DRP6502_TOOLS_FETCHED=TRUE \ + -P tools/rp6502.cmake + # A download that fails is a notice rather than an error, and the + # build would go on to report a header that is not at fault. + test -x tools/rp6502-emu + + - name: Build every example + run: | + set -euo pipefail + cmake --preset cc65/Release + cmake --build --preset cc65/Release + + - name: Collect the ROMs + id: pack + run: | + set -euo pipefail + sha=$(git rev-parse --short HEAD) + name="rp6502-${sha}-examples" + mkdir "$name" + cp src/README.txt "$name/" + # Only the target ROMs. CMakeFiles holds directories with the same + # suffix, and the program rp6502_xram() runs while CMake configures + # is a .rp6502 of its own. + find build/cc65/release/src -mindepth 2 -maxdepth 2 -type f \ + -name '*.rp6502' -exec cp -t "$name" {} + + # One ROM per example, so a target that quietly stopped producing + # one fails the release rather than leaving a gap in it. + want=$(grep -c '^add_subdirectory(src/' CMakeLists.txt) + got=$(find "$name" -name '*.rp6502' | wc -l) + if [ "$got" -ne "$want" ]; then + echo "packed ${got} ROMs, expected ${want}" >&2 + exit 1 + fi + ls -l "$name" + cmake -E tar cf "${name}.zip" --format=zip "$name" + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + echo "name=${name}" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@v7 + with: + name: ${{ steps.pack.outputs.name }} + path: ${{ steps.pack.outputs.name }}.zip + + - name: Publish the release + if: github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v3 + with: + tag_name: build-${{ steps.pack.outputs.sha }} + # Without this the tag is created on whatever the default branch + # points at when the release is published, not on what was built. + target_commitish: ${{ github.sha }} + name: Examples ${{ steps.pack.outputs.sha }} + files: ${{ steps.pack.outputs.name }}.zip + fail_on_unmatched_files: true + body: | + Example ROMs for the Picocomputer 6502, built with cc65. + + Unzip and run a `.rp6502` in the emulator, or load one on a + Picocomputer. + + https://picocomputer.github.io diff --git a/src/README.txt b/src/README.txt new file mode 100644 index 0000000..4440bef --- /dev/null +++ b/src/README.txt @@ -0,0 +1,7 @@ +RP6502 example ROMs + +Each .rp6502 file is one program for the Picocomputer 6502. Run it in the +emulator, or load it on a Picocomputer from the monitor. + +Documentation https://picocomputer.github.io +Source https://github.com/picocomputer/examples diff --git a/tools/rp6502.cmake b/tools/rp6502.cmake index 4db0501..c184707 100644 --- a/tools/rp6502.cmake +++ b/tools/rp6502.cmake @@ -621,7 +621,7 @@ function(rp6502_xram header regex) "#endif\n") endforeach() string(APPEND stub " return 0;\n}\n") - file(WRITE "${dir}/stub.c" "${stub}") + file(WRITE "${dir}/rp6502_xram_stub.c" "${stub}") # A program of assertions, compiled but never run, so the compiler # reports a bad layout against the line in the header. Every name gets a @@ -656,7 +656,7 @@ function(rp6502_xram header regex) endif() string(APPEND check "#endif\n") endforeach() - file(WRITE "${dir}/check.c" "${check}") + file(WRITE "${dir}/rp6502_xram_check.c" "${check}") # cc65's CMAKE_C_COMPILER is a wrapper around cl65 that puts diagnostics # in the form an IDE matches, so both programs are built through it. @@ -669,7 +669,7 @@ function(rp6502_xram header regex) set(failed FALSE) execute_process( COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" - -o "${dir}/stub" "${dir}/stub.c" + -o "${dir}/rp6502_xram_stub" "${dir}/rp6502_xram_stub.c" WORKING_DIRECTORY "${dir}" RESULT_VARIABLE result OUTPUT_VARIABLE output @@ -685,7 +685,7 @@ function(rp6502_xram header regex) execute_process( COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" -a "${load_addr}" -r "${load_addr}" - -o "${dir}/stub.rp6502" create "${dir}/stub" + -o "${dir}/rp6502_xram_stub.rp6502" create "${dir}/rp6502_xram_stub" RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output @@ -699,7 +699,7 @@ function(rp6502_xram header regex) execute_process( COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" -c "${RP6502_PROJECT_DIR}/.rp6502" - execute "${dir}/stub.rp6502" + execute "${dir}/rp6502_xram_stub.rp6502" TIMEOUT 60 RESULT_VARIABLE result OUTPUT_VARIABLE output @@ -738,10 +738,10 @@ function(rp6502_xram header regex) set(unread) if (failed) message(STATUS "rp6502_xram(${header}) read no addresses; the build reports why.") - file(WRITE "${dir}/unread.txt" + file(WRITE "${dir}/rp6502_xram_unread.txt" "rp6502_xram(${header}) read no addresses, so every name is zero.\n${output}\n") set(unread - COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/unread.txt" + COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/rp6502_xram_unread.txt" COMMAND "${CMAKE_COMMAND}" -E false) endif() @@ -756,16 +756,16 @@ function(rp6502_xram header regex) string(SUBSTRING "${hash}" 0 8 hash) set(target "rp6502_xram_${id}_${hash}") add_custom_command( - OUTPUT "${dir}/check.stamp" - DEPENDS "${header_file}" "${dir}/check.c" + OUTPUT "${dir}/rp6502_xram_check.stamp" + DEPENDS "${header_file}" "${dir}/rp6502_xram_check.c" COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" - -c -o "${dir}/check.o" "${dir}/check.c" + -c -o "${dir}/rp6502_xram_check.o" "${dir}/rp6502_xram_check.c" ${unread} - COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/check.stamp" + COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/rp6502_xram_check.stamp" COMMENT "Checking ${header_name}" VERBATIM ) - add_custom_target(${target} ALL DEPENDS "${dir}/check.stamp") + add_custom_target(${target} ALL DEPENDS "${dir}/rp6502_xram_check.stamp") set_property(DIRECTORY APPEND PROPERTY RP6502_XRAM_CHECKS "${target}") endfunction() From 832b2ca75c2cd6a8638687c78b457d8a853e621f Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:20:26 -0700 Subject: [PATCH 3/8] shorten the generated build filenames They sit in CMakeFiles/
.xram, which already says whose they are, so xram_stub.c reads better than rp6502_xram_stub.c. The extern in the check program and the custom target keep the longer prefix, because both of those names are shared with code and targets we do not own. Co-Authored-By: Claude Opus 5 (1M context) --- tools/rp6502.cmake | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/rp6502.cmake b/tools/rp6502.cmake index c184707..663da94 100644 --- a/tools/rp6502.cmake +++ b/tools/rp6502.cmake @@ -621,7 +621,7 @@ function(rp6502_xram header regex) "#endif\n") endforeach() string(APPEND stub " return 0;\n}\n") - file(WRITE "${dir}/rp6502_xram_stub.c" "${stub}") + file(WRITE "${dir}/xram_stub.c" "${stub}") # A program of assertions, compiled but never run, so the compiler # reports a bad layout against the line in the header. Every name gets a @@ -656,7 +656,7 @@ function(rp6502_xram header regex) endif() string(APPEND check "#endif\n") endforeach() - file(WRITE "${dir}/rp6502_xram_check.c" "${check}") + file(WRITE "${dir}/xram_check.c" "${check}") # cc65's CMAKE_C_COMPILER is a wrapper around cl65 that puts diagnostics # in the form an IDE matches, so both programs are built through it. @@ -669,7 +669,7 @@ function(rp6502_xram header regex) set(failed FALSE) execute_process( COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" - -o "${dir}/rp6502_xram_stub" "${dir}/rp6502_xram_stub.c" + -o "${dir}/xram_stub" "${dir}/xram_stub.c" WORKING_DIRECTORY "${dir}" RESULT_VARIABLE result OUTPUT_VARIABLE output @@ -685,7 +685,7 @@ function(rp6502_xram header regex) execute_process( COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" -a "${load_addr}" -r "${load_addr}" - -o "${dir}/rp6502_xram_stub.rp6502" create "${dir}/rp6502_xram_stub" + -o "${dir}/xram_stub.rp6502" create "${dir}/xram_stub" RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output @@ -699,7 +699,7 @@ function(rp6502_xram header regex) execute_process( COMMAND "${Python3_EXECUTABLE}" "${RP6502_TOOLS_DIR}/rp6502.py" -c "${RP6502_PROJECT_DIR}/.rp6502" - execute "${dir}/rp6502_xram_stub.rp6502" + execute "${dir}/xram_stub.rp6502" TIMEOUT 60 RESULT_VARIABLE result OUTPUT_VARIABLE output @@ -738,10 +738,10 @@ function(rp6502_xram header regex) set(unread) if (failed) message(STATUS "rp6502_xram(${header}) read no addresses; the build reports why.") - file(WRITE "${dir}/rp6502_xram_unread.txt" + file(WRITE "${dir}/xram_unread.txt" "rp6502_xram(${header}) read no addresses, so every name is zero.\n${output}\n") set(unread - COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/rp6502_xram_unread.txt" + COMMAND "${CMAKE_COMMAND}" -E cat "${dir}/xram_unread.txt" COMMAND "${CMAKE_COMMAND}" -E false) endif() @@ -756,16 +756,16 @@ function(rp6502_xram header regex) string(SUBSTRING "${hash}" 0 8 hash) set(target "rp6502_xram_${id}_${hash}") add_custom_command( - OUTPUT "${dir}/rp6502_xram_check.stamp" - DEPENDS "${header_file}" "${dir}/rp6502_xram_check.c" + OUTPUT "${dir}/xram_check.stamp" + DEPENDS "${header_file}" "${dir}/xram_check.c" COMMAND "${CMAKE_C_COMPILER}" ${compiler_args} ${flags} -I "${header_dir}" - -c -o "${dir}/rp6502_xram_check.o" "${dir}/rp6502_xram_check.c" + -c -o "${dir}/xram_check.o" "${dir}/xram_check.c" ${unread} - COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/rp6502_xram_check.stamp" + COMMAND "${CMAKE_COMMAND}" -E touch "${dir}/xram_check.stamp" COMMENT "Checking ${header_name}" VERBATIM ) - add_custom_target(${target} ALL DEPENDS "${dir}/rp6502_xram_check.stamp") + add_custom_target(${target} ALL DEPENDS "${dir}/xram_check.stamp") set_property(DIRECTORY APPEND PROPERTY RP6502_XRAM_CHECKS "${target}") endfunction() From 2608d520c5932d452e6d601205f2acf9523a92d0 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:21:42 -0700 Subject: [PATCH 4/8] put the ROMs at the root of the zip The archive was built from outside the staging directory, so everything landed one folder down. GitHub zips an artifact on the way out as well, so uploading the zip gave a zip holding a zip; the artifact is now the directory. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21b31dc..302ed7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,14 +74,18 @@ jobs: exit 1 fi ls -l "$name" - cmake -E tar cf "${name}.zip" --format=zip "$name" + # Zipped from inside, so the ROMs are at the root of the archive + # rather than one folder down. + ( cd "$name" && cmake -E tar cf "../${name}.zip" --format=zip . ) echo "sha=${sha}" >> "$GITHUB_OUTPUT" echo "name=${name}" >> "$GITHUB_OUTPUT" + # The directory, not the zip. Uploading the zip would be downloaded as + # a zip holding a zip. - uses: actions/upload-artifact@v7 with: name: ${{ steps.pack.outputs.name }} - path: ${{ steps.pack.outputs.name }}.zip + path: ${{ steps.pack.outputs.name }}/ - name: Publish the release if: github.ref == 'refs/heads/main' From e5149a9f65dd3ded610c4f4c4600c845a1adb96c Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:24:12 -0700 Subject: [PATCH 5/8] name the probe in the underscore namespace The declaration shares a translation unit with the layout header, and a leading underscore at file scope is the one namespace a name there cannot take from the header. rp6502_ stays on the CMake target, where the names are ours and global. Co-Authored-By: Claude Opus 5 (1M context) --- tools/rp6502.cmake | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/rp6502.cmake b/tools/rp6502.cmake index 663da94..2c040ee 100644 --- a/tools/rp6502.cmake +++ b/tools/rp6502.cmake @@ -635,11 +635,13 @@ function(rp6502_xram header regex) " being refused. An array of one is refused outright, because the\n" " size of an array is checked against the largest object the target\n" " allows and not against the truncated sizeof. */\n") + # The leading underscore is the namespace C keeps for file scope, which + # is the one place a name here cannot collide with the header's own. foreach(type line guard IN ZIP_LISTS probes probe_lines probe_guards) string(MAKE_C_IDENTIFIER "${type}" probe) string(APPEND check "\n#if ${guard}\n#line ${line} \"${header_c}\"\n" - "extern ${type} rp6502_xram_${probe}_fits[1];\n#endif\n") + "extern ${type} _xram_fits_${probe}[1];\n#endif\n") endforeach() # An offset and arithmetic between offsets are size_t, so 16 bits, and # can never trip this. A number that does not fit is a long and does. From e4d466e95ec395602f2bbaab656effc185f11a35 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:25:53 -0700 Subject: [PATCH 6/8] drop the build artifact GitHub appends .zip when an artifact is downloaded, so the name it lists can never carry the extension without doubling it. The release asset is the one that ships and it is named for what it is, so the artifact was only a second thing with almost the same name. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 302ed7b..15225d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,13 +80,6 @@ jobs: echo "sha=${sha}" >> "$GITHUB_OUTPUT" echo "name=${name}" >> "$GITHUB_OUTPUT" - # The directory, not the zip. Uploading the zip would be downloaded as - # a zip holding a zip. - - uses: actions/upload-artifact@v7 - with: - name: ${{ steps.pack.outputs.name }} - path: ${{ steps.pack.outputs.name }}/ - - name: Publish the release if: github.ref == 'refs/heads/main' uses: softprops/action-gh-release@v3 From bc1dedd8fe500d146671b1296098745cba5a8f42 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:33:00 -0700 Subject: [PATCH 7/8] keep a build to look at on a pull request A pull request publishes nothing, so the artifact is the only way to open what it built. It is named for the run rather than the commit, because a pull request's HEAD is a merge commit nobody can look up, and because two downloads called rp6502--examples.zip should not be the same name for a release and a trial build. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15225d4..6895b44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,14 @@ jobs: run: | set -euo pipefail sha=$(git rev-parse --short HEAD) - name="rp6502-${sha}-examples" + # A release is named for the commit it ships. A pull request ships + # nothing, and its HEAD is a merge commit nobody can look up, so it + # is named for the run instead. + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + name="rp6502-${sha}-examples" + else + name="rp6502-${GITHUB_RUN_ID}-examples" + fi mkdir "$name" cp src/README.txt "$name/" # Only the target ROMs. CMakeFiles holds directories with the same @@ -80,6 +87,14 @@ jobs: echo "sha=${sha}" >> "$GITHUB_OUTPUT" echo "name=${name}" >> "$GITHUB_OUTPUT" + # Only where nothing is released. GitHub appends .zip when an artifact + # is downloaded, so uploading the directory lands it as .zip. + - uses: actions/upload-artifact@v7 + if: github.ref != 'refs/heads/main' + with: + name: ${{ steps.pack.outputs.name }} + path: ${{ steps.pack.outputs.name }}/ + - name: Publish the release if: github.ref == 'refs/heads/main' uses: softprops/action-gh-release@v3 From b20e51dd52f905b5c112ab2e8360f3868b4fcc72 Mon Sep 17 00:00:00 2001 From: rumbledethumps <16963588+rumbledethumps@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:38:12 -0700 Subject: [PATCH 8/8] name the pull request artifact with .zip GitHub serves an artifact download under the artifact's own name, adding .zip only when it is not already there, so the name may carry it and the file still arrives as one archive of loose ROMs. This is what rp6502 does with its own build artifacts. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6895b44..2d0ac19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,12 +87,13 @@ jobs: echo "sha=${sha}" >> "$GITHUB_OUTPUT" echo "name=${name}" >> "$GITHUB_OUTPUT" - # Only where nothing is released. GitHub appends .zip when an artifact - # is downloaded, so uploading the directory lands it as .zip. + # Only where nothing is released. The name carries .zip because that is + # the filename the download is served with, and the directory is what + # goes in it, so the one archive holds the ROMs and nothing else. - uses: actions/upload-artifact@v7 if: github.ref != 'refs/heads/main' with: - name: ${{ steps.pack.outputs.name }} + name: ${{ steps.pack.outputs.name }}.zip path: ${{ steps.pack.outputs.name }}/ - name: Publish the release