From 1ef48846a9b57b2f3f15360dadcaaadf80ff89e1 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:11 -0700 Subject: [PATCH 1/8] rzx: include where the GSList declarations need it rzx.h declares functions returning GSList* but includes only libspectrum.h. Since 2003 libspectrum's public header has deliberately not pulled in -- upstream commit "Don't include in libspectrum.h; makes it easy to include from other projects" -- so every consumer that uses a glib type is expected to include itself, as event.h, keyboard.c, tape.c and friends already do under HAVE_LIB_GLIB. rzx.h never got that include. The omission has been latent in upstream Fuse for years, masked by include ordering (most translation units reach rzx.h only after something else has already pulled in glib). It surfaces on a real-GLib libspectrum build (as required by the GTK UI) via a unit that reaches rzx.h first -- e.g. peripherals/joystick.c -- which then fails with "unknown type name 'GSList'". Add the guarded include, following the established convention. --- rzx.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rzx.h b/rzx.h index 0bfa5326..fad1976f 100644 --- a/rzx.h +++ b/rzx.h @@ -24,6 +24,10 @@ #ifndef FUSE_RZX_H #define FUSE_RZX_H +#ifdef HAVE_LIB_GLIB +#include +#endif /* #ifdef HAVE_LIB_GLIB */ + #include "libspectrum.h" /* The offset used to get the count of instructions from the R register */ From 1f6a46083ba09f48f744b92ae22894ebbacb5f0a Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:11 -0700 Subject: [PATCH 2/8] ui/sdl: restore the ui/ui.h include in the shared joystick code sdljoystick.c is shared with the GTK build via a textual #include of the .c file. Its include of "ui.h" -- upstream spells it "ui/ui.h", as the very next line already does for "ui/uijoystick.h" -- resolves against the source file's own directory, ui/sdl/, where no ui.h exists, so the GTK build fails with "ui.h: No such file or directory". Restore the upstream include path. --- ui/sdl/sdljoystick.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/sdl/sdljoystick.c b/ui/sdl/sdljoystick.c index db2a0592..8fccf5bd 100644 --- a/ui/sdl/sdljoystick.c +++ b/ui/sdl/sdljoystick.c @@ -32,7 +32,7 @@ #include "input.h" #include "sdljoystick.h" #include "settings.h" -#include "ui.h" +#include "ui/ui.h" #include "ui/uijoystick.h" static SDL_Joystick *joystick1 = NULL; From 43a89c543de258950634477cbd1ecd6b9d99de89 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:11 -0700 Subject: [PATCH 3/8] cocoa: add a UI_COCOA macro to identify the macOS UI build FuseX defines a per-UI macro for every user interface it builds (UI_WIN32, UI_GTK, UI_SDL, ...) except the Cocoa app, whose Xcode target sets no UI macro at all. That left shared core code unable to tell the Cocoa build apart from the portable autotools builds, so it leaned on "not Win32" as a proxy for "is Cocoa" -- which silently swept in the SDL/GTK/Xlib builds and broke them. Define UI_COCOA in the Cocoa build's config.h so those call sites can name the Cocoa build positively, and convert the first of them: machine.c gated its SetEmulationHz stub on __APPLE__, which excluded the stub from autotools builds on macOS as well and left them unable to link, the real implementation being in fusepb/FuseMenus.m. It also enables the two commits that follow. --- fusepb/config.h | 3 +++ machine.c | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fusepb/config.h b/fusepb/config.h index 483aa62f..799fc1e2 100644 --- a/fusepb/config.h +++ b/fusepb/config.h @@ -151,6 +151,9 @@ /* Defined if svgalib UI in use */ /* #undef UI_SVGA */ +/* Defined if the Cocoa (macOS) UI is in use */ +#define UI_COCOA 1 + /* Defined if Win32 UI in use */ /* #undef UI_WIN32 */ diff --git a/machine.c b/machine.c index 11175768..135f6c5c 100644 --- a/machine.c +++ b/machine.c @@ -493,10 +493,12 @@ machine_register_startup( void ) NULL, machine_end ); } -#ifndef __APPLE__ +/* The Cocoa app implements this in fusepb/FuseMenus.m; every other build, + macOS autotools included, needs the stub. */ +#ifndef UI_COCOA void SetEmulationHz( float hz ) { (void)hz; } -#endif +#endif /* #ifndef UI_COCOA */ From 5579f5f6ac332fd55a8aef12f5070516e1467c8b Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:12 -0700 Subject: [PATCH 4/8] fuse: keep main() for the portable UIs, old_main() only for Cocoa FuseX renamed the emulator entry from upstream's main() to old_main() for every non-Win32 build, so its Cocoa app could supply its own main(). That also stripped main() from the autotools UIs (SDL, GTK, Xlib, ...), which then failed to link with "undefined reference to main". Select old_main() via UI_COCOA rather than "everything that is not Win32": Win32 keeps fuse_main() (called from WinMain), the Cocoa build keeps old_main() (unused there, as Cocoa drives the emulator itself), and every other build gets main() as upstream intended. --- fuse.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fuse.c b/fuse.c index 9db40d68..933e2434 100644 --- a/fuse.c +++ b/fuse.c @@ -171,9 +171,15 @@ static int parse_nonoption_args( int argc, char **argv, int first_arg, static int do_start_files( start_files_t *start_files ); #ifdef UI_WIN32 +/* The Win32 UI supplies WinMain(), which calls this */ int fuse_main(int argc, char **argv) -#else +#elif defined UI_COCOA +/* fusepb/main.m supplies main() and Emulator.m calls fuse_init() itself, so + this entry point goes unused; it is renamed only to avoid colliding with + the one in main.m */ int old_main(int argc, char **argv) +#else +int main(int argc, char **argv) #endif { int r = 0; From 9c547a7e5193b804bebb10eb65ad5b065ea7dfe2 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:12 -0700 Subject: [PATCH 5/8] scaler: wire the 32-bit scalers into every build except Cocoa Upstream references the 32-bit scaler routines (scaler_Normal1x_32, ...) directly in the scaler table. FuseX wrapped that column in a SCALER32() macro that expands to the real routine only under _WIN32 and to NULL otherwise, because its Cocoa Xcode project compiles only scalers16.o and the _32 symbols do not exist there. The autotools build always links scalers32.o, and FuseX's GTK display renders at 32bpp and calls scaler_proc32, so on Linux that NULL was a guaranteed crash the moment a frame was drawn. Gate the macro on UI_COCOA instead: the real routines are used everywhere they are compiled, and only the 16bpp-only Cocoa build (which never calls scaler_proc32) keeps NULL. Also stub uidisplay_set_next_hotswap_reason in scalerexpandtest, alongside the ui_error and uidisplay_hotswap_gfx_mode stubs already there. The test links ui/scaler/scaler.c without ui.c, and FuseX's hotswap call in scaler_select_scaler left it undefined; since the test is in noinst_PROGRAMS, that broke plain "make" on every autotools platform. --- ui/scaler/scaler.c | 10 +++++++--- unittests/scalerexpandtest.c | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ui/scaler/scaler.c b/ui/scaler/scaler.c index bd7603f8..6c6daa18 100644 --- a/ui/scaler/scaler.c +++ b/ui/scaler/scaler.c @@ -54,10 +54,14 @@ struct scaler_info { }; -#ifdef _WIN32 -#define SCALER32( name ) name##_32 -#else +/* Every build links scalers32.o (the 32-bit scaler routines) except the Cocoa + app, whose Xcode project compiles only the 16-bit variant. Wire the 32-bit + routines into the table everywhere they exist; the Cocoa build renders at + 16bpp and never uses them. */ +#ifdef UI_COCOA #define SCALER32( name ) NULL +#else +#define SCALER32( name ) name##_32 #endif /* The expander functions */ diff --git a/unittests/scalerexpandtest.c b/unittests/scalerexpandtest.c index 8197256e..039786ea 100644 --- a/unittests/scalerexpandtest.c +++ b/unittests/scalerexpandtest.c @@ -14,6 +14,7 @@ #include "settings.h" #include "ui/scaler/scaler.h" #include "ui/ui.h" +#include "ui/uidisplay.h" #include "utils.h" settings_info settings_current; @@ -30,6 +31,11 @@ uidisplay_hotswap_gfx_mode( void ) return 0; } +void +uidisplay_set_next_hotswap_reason( uidisplay_hotswap_reason reason GCC_UNUSED ) +{ +} + char* utils_safe_strdup( const char *src GCC_UNUSED ) { From 35c1419ad9859467fe9ac50009d6b0d5c3eb94d6 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:12 -0700 Subject: [PATCH 6/8] settings: emit portable C by default, Objective-C under --cocoa Upstream Fuse ships a single, portable settings generator (settings.pl, libxml2/INI backed). FuseX rewrote it to emit Objective-C backed by NSUserDefaults for the Cocoa app, which cannot compile anywhere else -- the autotools build's settings.c pulled in and failed immediately. Rather than fork a third, parallel generator for Linux, teach the existing settings.pl both dialects behind a --cocoa flag: with it, the Objective-C used by the Cocoa app (fusepb now passes --cocoa); without it, portable C for the autotools builds (the default the Makefile.am rule already invokes). The parts driven by settings.dat alone -- the defaults table, settings_init, the getopt_long option set, settings_copy, settings_get_rom_setting and settings_set_string -- are emitted once and shared by both dialects; only the config file I/O, the string ownership rules and the Cocoa-only ROM array bridging are emitted per mode. The Cocoa output changes only in the #line directives the shared sections now emit and in --no-banner moving from 444 to 256. The portable build gains --no-banner, which until now existed only in the Cocoa output. The portable read_config_file also keeps upstream 1.9.1's behaviour of parsing into a scratch copy and warning rather than failing: an unreadable or unparseable config file leaves the defaults in place instead of aborting startup. The portable INI reader recognises the settings that only the Cocoa build stores, rather than reporting them as unknown, and bounds that report to the offending line, since utils_read_file does not NUL terminate the buffer. --no-banner takes the reserved getopt_long value 256, leaving the generated options to run from 257 where they cannot collide with it. --- fusepb/Makefile | 2 +- settings.pl | 786 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 763 insertions(+), 25 deletions(-) diff --git a/fusepb/Makefile b/fusepb/Makefile index 60323610..1086688f 100644 --- a/fusepb/Makefile +++ b/fusepb/Makefile @@ -9,7 +9,7 @@ settings.h: ../settings.dat settings-header.pl cd .. && perl -Iperl fusepb/settings-header.pl settings.dat > fusepb/settings.h settings.m: ../settings.dat ../settings.pl - cd .. && perl -Iperl settings.pl settings.dat > fusepb/settings.m + cd .. && perl -Iperl settings.pl --cocoa settings.dat > fusepb/settings.m options_cocoa.h: ../perl/cpp-perl.pl config.h ../ui/cocoa/options-cocoa-header.pl ../ui/options.dat ../perl/Fuse.pm ../perl/Fuse/Dialog.pm cd .. && perl perl/cpp-perl.pl fusepb/config.h ui/options.dat | perl -Iperl ui/cocoa/options-cocoa-header.pl - public > fusepb/$@.tmp && mv fusepb/$@.tmp fusepb/$@ diff --git a/settings.pl b/settings.pl index 63b16406..73cb0f8d 100644 --- a/settings.pl +++ b/settings.pl @@ -1,6 +1,7 @@ #!/usr/bin/perl -w -# settings.pl: generate settings.c from settings.dat +# settings.pl: generate the configuration-settings implementation from +# settings.dat # Copyright (c) 2002-2018 Philip Kendall, BogDan Vatra, Alistair Cree # This program is free software; you can redistribute it and/or modify @@ -21,12 +22,30 @@ # E-mail: philip-fuse@shadowmagic.org.uk +# This generator has two modes. With --cocoa it emits the Objective-C +# implementation used by the Cocoa (macOS) app, backed by NSUserDefaults. +# Without it, it emits portable C backed by libxml2 (with an INI fallback), +# used by the autotools builds (SDL, GTK, Xlib, framebuffer, ...). +# +# Everything that depends on settings.dat alone rather than on the storage +# backend is emitted once and shared: the settings_default table, settings_init, +# the getopt_long option set, settings_copy, settings_get_rom_setting and +# settings_set_string. Only the config file I/O, the string ownership rules +# and the Cocoa-only ROM array bridging are emitted per mode. + use strict; use Fuse; sub hashline ($) { '#line ', $_[0] + 1, '"', __FILE__, "\"\n" } +my $cocoa = 0; +my @files; +foreach( @ARGV ) { + if( $_ eq '--cocoa' ) { $cocoa = 1; } else { push @files, $_; } +} +@ARGV = @files; + my %options; my %fileAssoc = ( 'snapshot' => 1, 'tape_file' => 1, 'record_file' => 1, @@ -63,10 +82,19 @@ configfile => $configfile, funcn => $funcn }; } -print Fuse::GPL( 'settings.m: Handling configuration settings', - '2002 Philip Kendall, Fredrick Meunier' ); +# Each emitter below writes one chunk of the generated implementation. Those +# whose output depends on the storage backend dispatch on $cocoa internally so +# that the driver at the foot of this file stays a flat sequence. -print << 'CODE'; +sub emit_gpl { + my $ext = $cocoa ? 'm' : 'c'; + print Fuse::GPL( "settings.$ext: Handling configuration settings", + '2002 Philip Kendall, Fredrick Meunier' ); +} + +sub emit_prologue { + if( $cocoa ) { + print << 'CODE'; /* This file is autogenerated from settings.dat by settings.pl. Do not edit unless you know what will happen! */ @@ -109,28 +137,113 @@ /* The default settings of options, etc */ settings_info settings_default = { CODE + } else { + print hashline( __LINE__ ), << 'CODE'; + +/* This file is autogenerated from settings.dat by settings.pl. + Do not edit unless you know what will happen! */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_GETOPT_LONG /* Did our libc include getopt_long? */ +#include +#elif defined AMIGA || defined __MORPHOS__ /* #ifdef HAVE_GETOPT_LONG */ +/* The platform uses GNU getopt, but not getopt_long, so we get + symbol clashes on this platform. Just use getopt */ +#else /* #ifdef HAVE_GETOPT_LONG */ +#include "compat.h" /* If not, use ours */ +#endif /* #ifdef HAVE_GETOPT_LONG */ + +#ifdef HAVE_LIB_XML2 +#include +#include +#endif /* #ifdef HAVE_LIB_XML2 */ + +#include "fuse.h" +#include "infrastructure/startup_manager.h" +#include "machine.h" +#include "settings.h" +#include "spectrum.h" +#include "ui/ui.h" +#include "utils.h" + +/* The name of our configuration file */ +#ifdef WIN32 +#define CONFIG_FILE_NAME "fuse.cfg" +#define FALLBACK_CONFIG_FILE_NAME CONFIG_FILE_NAME +#else /* #ifdef WIN32 */ +#define CONFIG_FILE_NAME "fuserc" +#define FALLBACK_CONFIG_FILE_NAME ".fuserc" +#endif /* #ifdef WIN32 */ + +/* The current settings of options, etc */ +settings_info settings_current; + +/* The default settings of options, etc */ +settings_info settings_default = { +CODE + } +} +# The settings_default initialiser. Field order must match the struct emitted +# by settings-header.pl, so the trailing three members are spelled out here. +sub emit_defaults_table { foreach my $name ( sort keys %options ) { - next if $options{$name}->{type} eq 'null'; - next if $options{$name}->{type} eq 'nsarray'; - if( $options{$name}->{type} eq 'string' ) { + my $type = $options{$name}->{type}; + next if $type eq 'null'; + next if $type eq 'nsarray'; + next if $type eq 'nsdictionary'; + if( $type eq 'string' ) { print " /* $name */ (char *)$options{$name}->{default},\n"; - } else { + } else { print " /* $name */ $options{$name}->{default},\n"; } } - print " /* cocoa */ NULL,\n"; -print << 'CODE'; + print " /* cocoa */ NULL,\n" if $cocoa; + + print << 'CODE'; /* show_help */ 0, /* show_version */ 0, /* no_banner */ 0, }; +CODE +} + +sub emit_forward_declarations { + print "\n"; + + unless( $cocoa ) { + print << 'CODE'; +static int read_config_file( settings_info *settings ); + +#ifdef HAVE_LIB_XML2 +static int parse_xml( xmlDocPtr doc, settings_info *settings ); +#else /* #ifdef HAVE_LIB_XML2 */ +static int parse_ini( utils_file *file, settings_info *settings ); +#endif /* #ifdef HAVE_LIB_XML2 */ +CODE + print "\n"; + } + print << 'CODE'; static int settings_command_line( settings_info *settings, int *first_arg, int argc, char **argv ); static void settings_copy_internal( settings_info *dest, settings_info *src ); +CODE +} + +sub emit_settings_init { + print << 'CODE'; /* Called on emulator startup */ int @@ -138,6 +251,11 @@ { int error; +CODE + + print " settings_defaults( &settings_current );\n\n" unless $cocoa; + + print << 'CODE'; error = read_config_file( &settings_current ); if( error ) return error; @@ -146,7 +264,24 @@ return 0; } +CODE +} +sub emit_settings_defaults { + print "\n"; + + unless( $cocoa ) { + print << 'CODE'; +/* Fill the settings structure with sensible defaults */ +void settings_defaults( settings_info *settings ) +{ + settings_copy_internal( settings, &settings_default ); +} +CODE + return; + } + + print << 'CODE'; /* Fill the settings structure with sensible defaults */ void settings_defaults( settings_info *settings ) { @@ -202,6 +337,15 @@ [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:defaultValues]; } +CODE +} + +sub emit_config_io { + $cocoa ? emit_config_io_cocoa() : emit_config_io_portable(); +} + +sub emit_config_io_cocoa { + print << 'CODE'; /* Read options from the config file */ @@ -283,7 +427,7 @@ } } -print << 'CODE'; + print << 'CODE'; return 0; } @@ -342,6 +486,463 @@ return 0; } +CODE +} + +sub emit_config_io_portable { + print hashline( __LINE__ ), << 'CODE'; + +#ifdef HAVE_LIB_XML2 + +/* Read options from the config file (if libxml2 is available) */ + +static int +read_config_file( settings_info *settings ) +{ + const char *cfgdir; char path[ PATH_MAX ]; + settings_info new_settings; + + xmlDocPtr doc; + + cfgdir = compat_get_config_path(); if( !cfgdir ) return 1; + + snprintf( path, PATH_MAX, "%s/%s", cfgdir, CONFIG_FILE_NAME ); + + /* See if the file exists; if doesn't, it's not a problem */ + if( !compat_file_exists( path ) ) { + /* Look for the config file in the fallback location, if there is one */ + const char *fallbackdir = compat_get_fallback_config_path(); + if( !fallbackdir ) return 0; + snprintf( path, PATH_MAX, "%s/%s", fallbackdir, FALLBACK_CONFIG_FILE_NAME ); + if( !compat_file_exists( path ) ) return 0; + } + + memset( &new_settings, 0, sizeof( new_settings ) ); + settings_copy_internal( &new_settings, settings ); + + doc = xmlReadFile( path, NULL, 0 ); + if( !doc ) { + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; + } + + if( parse_xml( doc, &new_settings ) ) { + xmlFreeDoc( doc ); + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; + } + + xmlFreeDoc( doc ); + + settings_copy_internal( settings, &new_settings ); + settings_free( &new_settings ); + + return 0; +} + +static int +parse_xml( xmlDocPtr doc, settings_info *settings ) +{ + xmlNodePtr node; + xmlChar *xmlstring; + + node = xmlDocGetRootElement( doc ); + if( xmlStrcmp( node->name, (const xmlChar*)"settings" ) ) { + ui_error( UI_ERROR_ERROR, "config file's root node is not 'settings'" ); + return 1; + } + + node = node->xmlChildrenNode; + while( node ) { + +CODE + +foreach my $name ( sort keys %options ) { + + my $type = $options{$name}->{type}; + + next if $command_line_only{$name}; + + if( $type eq 'boolean' or $type eq 'numeric' ) { + + print << "CODE"; + if( !strcmp( (const char*)node->name, "$options{$name}->{configfile}" ) ) { + xmlstring = xmlNodeListGetString( doc, node->xmlChildrenNode, 1 ); + if( xmlstring ) { + settings->$name = atoi( (char*)xmlstring ); + xmlFree( xmlstring ); + } + } else +CODE + + } elsif( $type eq 'string' ) { + + print << "CODE"; + if( !strcmp( (const char*)node->name, "$options{$name}->{configfile}" ) ) { + xmlstring = xmlNodeListGetString( doc, node->xmlChildrenNode, 1 ); + if( xmlstring ) { + libspectrum_free( settings->$name ); + settings->$name = utils_safe_strdup( (char*)xmlstring ); + xmlFree( xmlstring ); + } + } else +CODE + + } elsif( $type eq 'null' or $type eq 'nsarray' or $type eq 'nsdictionary' ) { + + print << "CODE"; + if( !strcmp( (const char*)node->name, "$options{$name}->{configfile}" ) ) { + /* Do nothing */ + } else +CODE + + } else { + die "Unknown setting type `$type'"; + } +} + +print hashline( __LINE__ ), << 'CODE'; + if( !strcmp( (const char*)node->name, "text" ) ) { + /* Do nothing */ + } else { + ui_error( UI_ERROR_WARNING, "Unknown setting '%s' in config file", + node->name ); + } + + node = node->next; + } + + return 0; +} + +int +settings_write_config( settings_info *settings ) +{ + const char *cfgdir; char path[ PATH_MAX ], buffer[80]; + + xmlDocPtr doc; xmlNodePtr root; + + cfgdir = compat_get_config_path(); if( !cfgdir ) return 1; + + snprintf( path, PATH_MAX, "%s/%s", cfgdir, CONFIG_FILE_NAME ); + + /* Create the XML document */ + doc = xmlNewDoc( (const xmlChar*)"1.0" ); + + root = xmlNewNode( NULL, (const xmlChar*)"settings" ); + xmlDocSetRootElement( doc, root ); +CODE + +foreach my $name ( sort keys %options ) { + + my $type = $options{$name}->{type}; + + next if $command_line_only{$name}; + + if( $type eq 'boolean' ) { + + print " xmlNewTextChild( root, NULL, (const xmlChar*)\"$options{$name}->{configfile}\", (const xmlChar*)(settings->$name ? \"1\" : \"0\") );\n"; + + } elsif( $type eq 'string' ) { + print << "CODE"; + if( settings->$name ) + xmlNewTextChild( root, NULL, (const xmlChar*)"$options{$name}->{configfile}", (const xmlChar*)settings->$name ); +CODE + } elsif( $type eq 'numeric' ) { + print << "CODE"; + snprintf( buffer, 80, "%d", settings->$name ); + xmlNewTextChild( root, NULL, (const xmlChar*)"$options{$name}->{configfile}", (const xmlChar*)buffer ); +CODE + } elsif( $type eq 'null' or $type eq 'nsarray' or $type eq 'nsdictionary' ) { + # Do nothing + } else { + die "Unknown setting type `$type'"; + } +} + + print hashline( __LINE__ ), << 'CODE'; + + xmlSaveFormatFile( path, doc, 1 ); + + xmlFreeDoc( doc ); + + return 0; +} + +#else /* #ifdef HAVE_LIB_XML2 */ + +/* Read options from the config file as ini file (if libxml2 is not available) */ + +static int +read_config_file( settings_info *settings ) +{ + const char *cfgdir; char path[ PATH_MAX ]; + int error; + settings_info new_settings; + + utils_file file; + + cfgdir = compat_get_config_path(); if( !cfgdir ) return 1; + + snprintf( path, PATH_MAX, "%s/%s", cfgdir, CONFIG_FILE_NAME ); + + /* See if the file exists; if doesn't, it's not a problem */ + if( !compat_file_exists( path ) ) { + /* Look for the config file in the fallback location, if there is one */ + const char *fallbackdir = compat_get_fallback_config_path(); + if( !fallbackdir ) return 0; + snprintf( path, PATH_MAX, "%s/%s", fallbackdir, FALLBACK_CONFIG_FILE_NAME ); + if( !compat_file_exists( path ) ) return 0; + } + + memset( &new_settings, 0, sizeof( new_settings ) ); + settings_copy_internal( &new_settings, settings ); + + error = utils_read_file( path, &file ); + if( error ) { + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; + } + + if( parse_ini( &file, &new_settings ) ) { + utils_close_file( &file ); + settings_free( &new_settings ); + ui_error( UI_ERROR_WARNING, + "ignoring unrecognised config file '%s'; using defaults", path ); + return 0; + } + + utils_close_file( &file ); + + settings_copy_internal( settings, &new_settings ); + settings_free( &new_settings ); + + return 0; +} + +static int +settings_var( settings_info *settings, unsigned char *name, unsigned char *last, + int **val_int, char ***val_char, unsigned char **next ) +{ + unsigned char* cpos; + size_t n; + + *val_int = NULL; + *val_char = NULL; + + *next = name; + while( name < last && ( *name == ' ' || *name == '\t' || *name == '\r' || + *name == '\n' ) ) { + *next = ++name; /* seek to first char */ + } + cpos = name; + + while( cpos < last && ( *cpos != '=' && *cpos != ' ' && *cpos != '\t' && + *cpos != '\r' && *cpos != '\n' ) ) cpos++; + *next = cpos; + n = cpos - name; /* length of name */ + + while( *next < last && **next != '=' ) { /* search for '=' */ + if( **next != ' ' && **next != '\t' && **next != '\r' && **next != '\n' ) + return 1; /* error in value */ + (*next)++; + } + if( *next < last) (*next)++; /* set after '=' */ +/* ui_error( UI_ERROR_WARNING, "Config: (%5s): ", name ); */ + +CODE +foreach my $name ( sort keys %options ) { + next if $command_line_only{$name}; + my $type = $options{$name}->{type}; + my $len = length $options{$name}->{configfile}; + + print << "CODE"; + if( n == $len && !strncmp( (const char *)name, "$options{$name}->{configfile}", n ) ) { +CODE + print " *val_int = \&settings->$name;\n" if( $type eq 'boolean' or $type eq 'numeric' ); + print " *val_char = \&settings->$name;\n" if( $type eq 'string' ); + print " /* Recognised but not stored */\n" + if( $type eq 'null' or $type eq 'nsarray' or $type eq 'nsdictionary' ); + print << "CODE"; + return 0; + } +CODE +} + print << "CODE"; + return 1; +} + +static int +parse_ini( utils_file *file, settings_info *settings ) +{ + unsigned char *cpos, *cpos_new; + int *val_int; + char **val_char; + + cpos = file->buffer; + + /* Read until the end of file */ + while( cpos < file->buffer + file->length ) { + if( settings_var( settings, cpos, file->buffer + file->length, &val_int, + &val_char, &cpos_new ) ) { + /* error in name or something else ... The buffer is not NUL terminated, + so bound the report to the offending line. */ + unsigned char *eol = cpos; + while( eol < file->buffer + file->length && + *eol != '\\r' && *eol != '\\n' ) eol++; + ui_error( UI_ERROR_WARNING, + "Unknown and/or invalid setting '%.*s' in config file", + (int)( eol - cpos ), (const char *)cpos ); + cpos = cpos_new + 1; + continue; + } + cpos = cpos_new; + if( val_int ) { + *val_int = atoi( (char *)cpos ); + while( cpos < file->buffer + file->length && + ( *cpos != '\\0' && *cpos != '\\r' && *cpos != '\\n' ) ) cpos++; + } else if( val_char ) { + char *value = (char *)cpos; + size_t n = 0; + while( cpos < file->buffer + file->length && + ( *cpos != '\\0' && *cpos != '\\r' && *cpos != '\\n' ) ) cpos++; + n = (char *)cpos - value; + if( n > 0 ) { + if( *val_char != NULL ) { + libspectrum_free( *val_char ); + *val_char = NULL; + } + *val_char = libspectrum_new( char, n + 1 ); + (*val_char)[n] = '\\0'; + memcpy( *val_char, value, n ); + } + } else { + /* Recognised, but this build does not store it: skip the value, or + the parser resumes mid-value and loses the next setting. */ + while( cpos < file->buffer + file->length && + ( *cpos != '\\0' && *cpos != '\\r' && *cpos != '\\n' ) ) cpos++; + } + /* skip 'new line' like chars */ + while( ( cpos < ( file->buffer + file->length ) ) && + ( *cpos == '\\r' || *cpos == '\\n' ) ) cpos++; + +CODE +print hashline( __LINE__ ), << 'CODE'; + } + + return 0; +} + +static int +settings_file_write( compat_fd fd, const char *buffer, size_t length ) +{ + return compat_file_write( fd, (const unsigned char *)buffer, length ); +} + +static int +settings_string_write( compat_fd doc, const char* name, const char* config ) +{ + if( config != NULL && + ( settings_file_write( doc, name, strlen( name ) ) || + settings_file_write( doc, "=", 1 ) || + settings_file_write( doc, config, strlen( config ) ) || + settings_file_write( doc, FUSE_EOL, strlen( FUSE_EOL ) ) ) ) + return 1; + return 0; +} + +static int +settings_boolean_write( compat_fd doc, const char* name, int config ) +{ + return settings_string_write( doc, name, config ? "1" : "0" ); +} + +static int +settings_numeric_write( compat_fd doc, const char* name, int config ) +{ + char buffer[80]; + snprintf( buffer, sizeof( buffer ), "%d", config ); + return settings_string_write( doc, name, buffer ); +} + +int +settings_write_config( settings_info *settings ) +{ + const char *cfgdir; char path[ PATH_MAX ]; + + compat_fd doc; + + cfgdir = compat_get_config_path(); if( !cfgdir ) return 1; + + snprintf( path, PATH_MAX, "%s/%s", cfgdir, CONFIG_FILE_NAME ); + + doc = compat_file_open( path, 1 ); + if( doc == COMPAT_FILE_OPEN_FAILED ) { + ui_error( UI_ERROR_ERROR, "couldn't open `%s' for writing: %s\n", + path, strerror( errno ) ); + return 1; + } + +CODE + +foreach my $name ( sort keys %options ) { + + my $type = $options{$name}->{type}; + + next if $command_line_only{$name}; + + if( $type eq 'boolean' ) { + + print << "CODE"; + if( settings_boolean_write( doc, "$options{$name}->{configfile}", + settings->$name ) ) + goto error; +CODE + + } elsif( $type eq 'string' ) { + print << "CODE"; + if( settings_string_write( doc, "$options{$name}->{configfile}", + settings->$name ) ) + goto error; +CODE + + } elsif( $type eq 'numeric' ) { + print << "CODE"; + if( settings_numeric_write( doc, "$options{$name}->{configfile}", + settings->$name ) ) + goto error; +CODE + + } elsif( $type eq 'null' or $type eq 'nsarray' or $type eq 'nsdictionary' ) { + # Do nothing + } else { + die "Unknown setting type `$type'"; + } +} + + print hashline( __LINE__ ), << 'CODE'; + + compat_file_close( doc ); + + return 0; +error: + compat_file_close( doc ); + + return 1; +} + +#endif /* #ifdef HAVE_LIB_XML2 */ +CODE +} + +sub emit_command_line { + print hashline( __LINE__ ), << 'CODE'; /* Read options from the command line */ static int @@ -359,7 +960,10 @@ CODE -my $fake_short_option = 256; +# getopt_long values above 255 cannot clash with a short option character. +# 256 is reserved for --no-banner; generated options take 257 upwards. +my $no_banner_short = 256; +my $fake_short_option = 257; foreach my $name ( sort keys %options ) { @@ -389,11 +993,15 @@ } } -print << 'CODE'; +print hashline( __LINE__ ), << 'CODE'; { "help", 0, NULL, 'h' }, { "version", 0, NULL, 'V' }, - { "no-banner", 0, NULL, 444 }, +CODE + +print " { \"no-banner\", 0, NULL, $no_banner_short },\n"; + +print hashline( __LINE__ ), << 'CODE'; { 0, 0, 0, 0 } /* End marker: DO NOT REMOVE */ }; @@ -418,7 +1026,7 @@ CODE -$fake_short_option = 256; +$fake_short_option = 257; foreach my $name ( sort keys %options ) { @@ -444,11 +1052,15 @@ } } -print << 'CODE'; +print hashline( __LINE__ ), << 'CODE'; case 'h': settings->show_help = 1; break; case 'V': settings->show_version = 1; break; - case 444: settings->no_banner = 1; break; +CODE + +print " case $no_banner_short: settings->no_banner = 1; break;\n"; + +print hashline( __LINE__ ), << 'CODE'; case ':': case '?': @@ -467,6 +1079,12 @@ return 0; } +CODE +} + +sub emit_copy_internal { + if( $cocoa ) { + print << 'CODE'; /* Copy one settings object to another */ static void @@ -502,15 +1120,49 @@ CODE } } + } else { + print hashline( __LINE__ ), << 'CODE'; + +/* Copy one settings object to another */ +static void +settings_copy_internal( settings_info *dest, settings_info *src ) +{ + settings_free( dest ); + +CODE + +foreach my $name ( sort keys %options ) { + + my $type = $options{$name}->{type}; + + if( $type eq 'boolean' or $type eq 'numeric' ) { + print " dest->$name = src->$name;\n"; + } elsif( $type eq 'string' ) { + print << "CODE"; + dest->$name = NULL; + if( src->$name ) { + dest->$name = utils_safe_strdup( src->$name ); + } +CODE + } +} + } -print << 'CODE'; + print << 'CODE'; +} +CODE } +sub emit_free { + print << 'CODE'; + int settings_free( settings_info *settings ) { CODE + if( $cocoa ) { + foreach my $name ( sort keys %options ) { if( $options{$name}->{type} eq 'string' ) { print " if( settings->$name ) {\n"; @@ -525,13 +1177,32 @@ } } -print << 'CODE'; + print << 'CODE'; if( settings->cocoa ) free( settings->cocoa ); settings->cocoa = NULL; return 0; } +CODE + } else { + +foreach my $name ( sort keys %options ) { + if( $options{$name}->{type} eq 'string' ) { + print " if( settings->$name ) libspectrum_free( settings->$name );\n"; + } +} + + print hashline( __LINE__ ), << 'CODE'; + + return 0; +} +CODE + } +} + +sub emit_copy { + print << 'CODE'; /* Copy one settings object to another */ void settings_copy( settings_info *dest, settings_info *src ) @@ -539,6 +1210,11 @@ settings_defaults( dest ); settings_copy_internal( dest, src ); } +CODE +} + +sub emit_rom_setting { + print << 'CODE'; char ** settings_get_rom_setting( settings_info *settings, size_t which, @@ -608,6 +1284,11 @@ } } } +CODE +} + +sub emit_set_string { + print << 'CODE'; void settings_set_string( char **string_setting, const char *value ) @@ -620,8 +1301,12 @@ *string_setting = utils_safe_strdup( value ); } CODE +} -print << 'CODE'; +# Cocoa bindings expect the per-machine ROM settings as an NSArray of +# dictionaries; these two functions bridge between that and settings_info. +sub emit_rom_arrays { + print << 'CODE'; /* Comparison function to sort the machineroms array */ NSInteger @@ -673,7 +1358,7 @@ print "\n"; } -print << 'CODE'; + print << 'CODE'; /* We assume that we got all machines in the array, this should always be true*/ [machineroms sortUsingFunction:machineroms_compare context:nil]; @@ -682,7 +1367,7 @@ } CODE -print << 'CODE'; + print << 'CODE'; /* Fill the settings structure from the supplied NSMutableArray as above */ void @@ -712,9 +1397,12 @@ print " }\n"; print " }\n"; print "}\n"; -print "\n"; +} + +sub emit_end { + if( $cocoa ) { + print << 'CODE'; -print << 'CODE'; static void settings_end( void ) { @@ -739,6 +1427,28 @@ settings_free( &settings_current ); } +CODE + } else { + print hashline( __LINE__ ), << 'CODE'; + +static void +settings_end( void ) +{ + if( settings_current.autosave_settings ) + settings_write_config( &settings_current ); + + settings_free( &settings_current ); + +#ifdef HAVE_LIB_XML2 + xmlCleanupParser(); +#endif /* #ifdef HAVE_LIB_XML2 */ +} +CODE + } +} + +sub emit_register_startup { + print << 'CODE'; void settings_register_startup( void ) @@ -746,10 +1456,20 @@ /* settings_init not yet managed by the startup manager */ startup_manager_module dependencies[] = { +CODE + + if( $cocoa ) { + print << 'CODE'; /* Fuse for OS X requires that settings_end is called before memory is deallocated as settings need to look up machine names etc */ STARTUP_MANAGER_MODULE_MEMORY, /* STARTUP_MANAGER_MODULE_SETUID, */ +CODE + } else { + print " STARTUP_MANAGER_MODULE_SETUID,\n"; + } + + print << 'CODE'; }; startup_manager_register( STARTUP_MANAGER_MODULE_SETTINGS_END, dependencies, ARRAY_SIZE( dependencies ), NULL, NULL, @@ -757,3 +1477,21 @@ } CODE +} + +emit_gpl(); +emit_prologue(); +emit_defaults_table(); +emit_forward_declarations(); +emit_settings_init(); +emit_settings_defaults(); +emit_config_io(); +emit_command_line(); +emit_copy_internal(); +emit_free(); +emit_copy(); +emit_rom_setting(); +emit_set_string(); +emit_rom_arrays() if $cocoa; +emit_end(); +emit_register_startup(); From 1f26dc6f216b612e2bc31a38b627467090eac6e4 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:12 -0700 Subject: [PATCH 7/8] menu: gate the WinSparkle updater item and fix the debug-log callback Two FuseX menu additions never accounted for the widget/GTK UIs: * "Help/Check for Updates..." is wired to menu_help_check_for_updates, which only exists in the Win32 WinSparkle backend. The widget and GTK builds failed to link. Gate the menu entry to UI_WIN32 (the Cocoa app drives updates from its own XIB menus, not menu_data.dat). * menu_machine_debuglog did (void)action; but the GTK MENU_CALLBACK signature has no action parameter, so the GTK build failed to compile. Drop the line -- other no-op callbacks (e.g. menu_machine_nmi) already leave the body empty. --- menu.c | 1 - menu_data.dat | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/menu.c b/menu.c index e13e7980..9ed635ff 100644 --- a/menu.c +++ b/menu.c @@ -454,7 +454,6 @@ MENU_CALLBACK( menu_machine_profiler_stop ) #if !defined( UI_WIN32 ) MENU_CALLBACK( menu_machine_debuglog ) { - (void)action; } #endif /* !defined( UI_WIN32 ) */ diff --git a/menu_data.dat b/menu_data.dat index b9a88e5b..4b57bb08 100644 --- a/menu_data.dat +++ b/menu_data.dat @@ -520,6 +520,8 @@ Media/IDE/ZXMMC/_Commit, Item,, menu_media_ide_commit,, 9 Media/IDE/ZXMMC/_Eject, Item,, menu_media_ide_eject,, 9 _Help, Branch +#ifdef UI_WIN32 Help/_Check for Updates..., Item,, menu_help_check_for_updates +#endif /* #ifdef UI_WIN32 */ Help/_Keyboard..., Item Help/_About..., Item From 153ee311654ef26f8522700d54f2c68e013b1190 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 25 Jul 2026 10:25:12 -0700 Subject: [PATCH 8/8] peripherals/fs: build the xfs RAM engine without Spectranet The whole xfs filesystem lived behind BUILD_SPECTRANET, but the GDB vfile debugger feature -- compiled unconditionally and used by gdbserver.c -- depends on the RAM-backed xfs engine. With Spectranet off (the default when pthreads/sockets are unavailable) the link failed on xfs_ram_engine and xfs_reset. Split xfs: the core (xfs.c, xfs_worker.c, xfs_fs.c) is now always built, while only the network engines (xfs_https.c) and their HTTP/TLS/SSH stack stay Spectranet-only. The http/https branches of the mount dispatcher in xfs.c are guarded by BUILD_SPECTRANET so it references those engines only when they are compiled. --- peripherals/Makefile.am | 11 ++++++++--- peripherals/fs/xfs.c | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/peripherals/Makefile.am b/peripherals/Makefile.am index 7428ba9e..e2ab6a8d 100644 --- a/peripherals/Makefile.am +++ b/peripherals/Makefile.am @@ -65,6 +65,14 @@ if BUILD_SPECCYBOOT fuse_SOURCES += peripherals/nic/enc28j60.c endif +# The xfs core (RAM-backed filesystem engine) is always built: the GDB vfile +# debugger feature, which is compiled unconditionally, depends on it. Only the +# network engines (xfs_https.c) and their HTTP/TLS/SSH stack are Spectranet-only. +fuse_SOURCES += \ + peripherals/fs/xfs.c \ + peripherals/fs/xfs_worker.c \ + peripherals/fs/xfs_fs.c + if BUILD_SPECTRANET fuse_SOURCES += \ peripherals/flash/am29f010.c \ @@ -84,9 +92,6 @@ fuse_SOURCES += \ peripherals/nic/engines/parson.c \ peripherals/nic/spectranext_controller.c \ peripherals/nic/spectranext_stdout.c \ - peripherals/fs/xfs.c \ - peripherals/fs/xfs_worker.c \ - peripherals/fs/xfs_fs.c \ peripherals/fs/xfs_https.c \ peripherals/http/httpc.c \ peripherals/http/http_sck.c \ diff --git a/peripherals/fs/xfs.c b/peripherals/fs/xfs.c index 44ec9150..126b233f 100644 --- a/peripherals/fs/xfs.c +++ b/peripherals/fs/xfs.c @@ -105,6 +105,9 @@ void xfs_handle_mount(volatile struct xfs_registers_t* registers) return; } } +#ifdef BUILD_SPECTRANET + /* The http/https engines live in xfs_https.c, which is built only with + Spectranet as it needs the HTTP client and the mbedTLS stack. */ else if (strcmp(protocol, "https") == 0) { XFS_DEBUG("xfs: mount https hostname='%s' path='%s' mount_point=%d\n", hostname, path, mount_point); @@ -115,6 +118,7 @@ void xfs_handle_mount(volatile struct xfs_registers_t* registers) XFS_DEBUG("xfs: mount http hostname='%s' path='%s' mount_point=%d\n", hostname, path, mount_point); engine = &http_engine; } +#endif /* #ifdef BUILD_SPECTRANET */ else { XFS_DEBUG("xfs: mount failed: unknown protocol '%s' mount_point=%d\n", protocol, mount_point);