Skip to content

Conversation

@drpaneas
Copy link

Here is a summary of what this does:

Auto Submodule Initiatilazation

The project requires some tools which are not present/part of the repository.
But it fetches them via git-submodules, which complicates a bit a few things.
Using the CMake build, this complication goes away, at it automatically handles these tools for you.

Trigger condition: if(PVSNESLIB_AUTO_SUBMODULES) # Default: ON

You have the option to opt-out of course, but by default CMake will take care of setting up all the submodules for you.

  • When enabled: Runs automatically on every CMake configure
  • When disabled: User must manage submodules manually (e.g. cmake -DPVSNESLIB_AUTO_SUBMODULES=OFF to disable)

The whole automation is basically running just one simple command:

execute_process(
    COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)

In other words, it initializes any submodules if they do not exist, and if they exists it makes sure they are updated to the version exact commit hash recorded in superproject. And yes, it does that recursively as well.

Now this works because the project is embedding submodules by pointing to specific commits, not branches! This is important. See:

$ git ls-tree HEAD | grep commit
160000 commit a1b2c3d4... compiler/tcc
160000 commit e5f6g7h8... compiler/wla-dx

This is important, because it makes the whole thing to work with just one command: cmake -B build. So you don't have to explicitely remember to "do sth with submodules". They "just work" during the build.

Auto Dependrency Management

The project has a complex dependency chain:

# MANUAL dependency chain - breaks if any step fails
all: clean
	$(MAKE) -C $(COMPILER_PATH)          # Step 1
	$(MAKE) -C $(COMPILER_PATH) install  # Step 2  
	$(MAKE) -C $(TOOLS_PATH)             # Step 3
	$(MAKE) -C $(TOOLS_PATH) install     # Step 4
	$(MAKE) -C $(PVSNESLIB_PATH)         # Step 5
	$(MAKE) -C $(SNES_EXAMPLES_PATH)     # Step 6

now imagine what is happening if you are in the middle of this and not very familiar with all of these steps. Wouldn't be great if you didn't have to care at all? Here's is where CMake comes, as it defines an order:

# Add subdirectories in dependency order (matching existing Makefile)
add_subdirectory(compiler)
add_subdirectory(tools)
add_subdirectory(pvsneslib)

if(PVSNESLIB_BUILD_EXAMPLES)
    add_subdirectory(snes-examples)
endif()

And if something fails, you see why:

# Compiler dependency check
if(EXISTS ${TCC_DIR}/configure)
    ExternalProject_Add(tcc_compiler ...)  # Build if available
else()
    # Create helpful dummy target instead of failing silently
    add_custom_target(tcc_compiler
        COMMAND ${CMAKE_COMMAND} -E echo "Error: TCC submodule not initialized"
        COMMAND ${CMAKE_COMMAND} -E false
    )
endif()

e.g. let's say you try you build the minimum stuff, but submodules (for some reason, you might have manually deleted them) are not there and you have disabled the automation during build:

cmake --build build --target minimal

What happens is:

  1. Detects missing submodules
  2. Provides clear error: "Please run: git submodule update --init --recursive"
  3. Fails with helpful message instead of cryptic errors

But it's very uncommon for a failure to happen due to the order of things, since each target has explicitely defined dependency (e.g. if you select, for some reason, to build only the tools):

# Tools depend on compiler
if(TARGET pvsneslib_compiler)
    add_dependencies(pvsneslib_tools pvsneslib_compiler)
endif()

# High-level targets with smart dependencies
add_custom_target(minimal
    DEPENDS compiler tools library    # Builds in order automatically
)

In that case, building only the tools cmake --build build --target tools

What happens is that:

  1. CMake analyzes: "tools needs compiler"
  2. Automatically builds compiler first
  3. Then builds tools
  4. Skips library and examples (not requested)

Easy builds

Building the project itself

If you intend to contribute to the the project itself or build and study its examples, then this is super easy now.

$ git clone https://github.com/alekmaul/pvsneslib.git
$ cd pvsneslib  
$ cmake -B build # No environment variable needed
$ cmake --build build --target <put-your-target>

There are many targets, but the 3 main ones are: minimal, examples_only, everything

But there are others as well, use guide to see them all:

$ cmake --build build --target guide
Displaying PVSnesLib essential targets

🎮 PVSnesLib - 3 Essential Targets
==================================

🔧 1. MINIMAL - Absolute minimum to write SNES programs:
   cmake --build build --target minimal
   Result: C compiler + assembler + tools + library
   Use this: To write your own SNES programs

🎮 2. EXAMPLES_ONLY - Build all example ROMs:
   cmake --build build --target examples_only
   Result: 60+ example SNES ROMs
   Use this: To learn from existing examples

🚀 3. EVERYTHING - Build absolutely everything:
   cmake --build build --target everything
   Result: Complete development environment + all examples
   Use this: For complete setup

📋 QUICK START:
   cmake -B build -DPVSNESLIB_BUILD_EXAMPLES=ON
   cmake --build build --target everything

📖 4. DOCUMENTATION - Generate API docs [requires Doxygen]:
   cmake --build build --target docs
   Result: HTML documentation in pvsneslib/docs/html/
   Use this: To browse the complete API reference

🎯 INDIVIDUAL COMPONENTS - if needed:
   compiler, tools, library, examples, hello_world, effects, etc.

Built target guide

Building your game

After having the project build somewhere in your disk, export a ENV variable to its location: export PVSNESLIB_HOME="/your/project/location"

Then:

mkdir $HOME/my_snes_game; cd $HOME/my_snes_game
mkdir src
vim src/main.c

In the src/main.c you write your game's logic.

#include <snes.h>

int main(void)
{
    u16 timer = 0;
    u8 color_state = 0;

    // Initialize console
    consoleInit();

    // Disable all backgrounds
    bgSetDisable(0);
    bgSetDisable(1);
    bgSetDisable(2);
    bgSetDisable(3);

    // Turn screen on
    setScreenOn();

    // Main loop: Blue for 5 seconds, then red for 5 seconds
    while (1)
    {
        timer++;

        // Change color every 5 seconds (300 frames at 60fps)
        if (timer >= 300)
        {
            timer = 0;
            color_state = !color_state;

            if (color_state)
            {
                setPaletteColor(0, RGB5(0, 0, 31)); // paint it blue
            }
            else
            {
                setPaletteColor(0, RGB5(31, 0, 0)); // paint it red
            }
        }

        // Wait for next frame
        WaitForVBlank();
    }

    return 0;
}

So it looks like this:

drpaneas@m2:~/my_snes_game% tree .
.
├── CMakeLists.txt
├── data.asm
├── hdr.asm
└── src
    └── main.c

Then create the hdr.asm. You can basically copy-paste this, but change only the NAME.
Pay attention to it, because it has to be exactly 21 characters at the .SNESHEADER section:

;==LoRom== Minimal SNES Test

.MEMORYMAP
  SLOTSIZE $8000
  DEFAULTSLOT 0
  SLOT 0 $8000
  SLOT 1 $0 $2000
  SLOT 2 $2000 $E000
  SLOT 3 $0 $10000
.ENDME

.ROMBANKSIZE $8000
.ROMBANKS 8

.SNESHEADER
  ID "SNES"
  NAME "MY SNES GAME         "
  SLOWROM
  LOROM
  CARTRIDGETYPE $00
  ROMSIZE $08
  SRAMSIZE $00
  COUNTRY $01
  LICENSEECODE $00
  VERSION $00
.ENDSNES

.SNESNATIVEVECTOR
  COP EmptyHandler
  BRK EmptyHandler
  ABORT EmptyHandler
  NMI VBlank
  IRQ EmptyHandler
.ENDNATIVEVECTOR

.SNESEMUVECTOR
  COP EmptyHandler
  ABORT EmptyHandler
  NMI EmptyHandler
  RESET tcc__start
  IRQBRK EmptyHandler
.ENDEMUVECTOR

Also create the data.asm even there are no graphics or audio assets:

.include "hdr.asm"
.section ".rodata1" superfree
.ends

and last but not least the CMakeLists.txt:

##################################
# -- this is boilerplate code -- #
##################################
cmake_minimum_required(VERSION 3.16)
project(MySNESGames)

# Require PVSNESLIB_HOME for external projects
if(NOT DEFINED ENV{PVSNESLIB_HOME})
    message(FATAL_ERROR "PVSNESLIB_HOME environment variable not set. Please set it to point to your PVSnesLib installation.")
endif()

# Add PVSnesLib CMake modules to path
list(APPEND CMAKE_MODULE_PATH "$ENV{PVSNESLIB_HOME}/cmake")

# Include SNESGame helper functions
include(SNESGame)

###########################################################
# This is essentially the part you need to pay attention  #
###########################################################
add_snes_game(my_snes_game
    SOURCES src/main.c
    ASM_SOURCES hdr.asm data.asm
    ROM_NAME my_snes_game
)

So to build it:

$ cmake -B build
$ cmake --build build --target my_snes_game

And voila, you have SNES rom:

$ drpaneas@m2:~/my_snes_game% find . -iname "*.sfc"
./src/my_snes_game.sfc

Fixing macOS sed issue

In Linux, this works fine:

sed -i 's/old/new/' file.txt    # ✅ Works - edits file in-place

but in Darwin, it doesn't:

sed -i 's/old/new/' file.txt    # ❌ FAILS with error:
"sed: 1: "file.txt": invalid command code"

this is because macOS uses BSD verions of sed which requires a backup extension argument.
so in general, in the code this is fixed based on this pattern:

# BEFORE (Linux-only):
sed -i 'pattern' file

# AFTER (Cross-platform):
sed -i.bak 'pattern' file && rm -f file.bak
  • -i.bak: Creates backup with .bak extension (required by BSD sed)
  • && rm -f file.bak: Removes backup file after successful edit

So in the end we have same behavior on both Linux and macOS.

Release and versions

I think (and take this with a grain of salt) that there are 4 main places where the version is to be found:

  1. CMakeLists.txt: project(PVSnesLib VERSION 4.4.0) - You set it here
  2. libversion.h/libversion.h.in - template used from 1
  3. pvsneslib_version.txt/pvsneslib_version.txt.in - template used from 1
  4. Documentation (Doxyfile) - template used from 1

But there is also README of course, which has a hardcoded link there.
And also the changelog for devkitsnes/readme.txt.

For the readmes, the process will continue to be manual. No automation there, as it will over-complicate things for no good reason. Just remember to modify them per version bump.

Individual tools, will remain having their own processes and their own Makefiles.
That said, to update the new version you will have to do only this:

# Automatic (template-based):
1. Edit: project(PVSnesLib VERSION X.Y.Z) in CMakeLists.txt
2. # HTML documentation only (default)
cmake --build build --target docs
# or HTML + PDF manual:
cmake --build build --target docspdf


# Manual (release-specific):  
3. Update: README.md download links for new release
4. Add: devkitsnes/readme.txt changelog entry

But there is also a whole Release target that does all of these and more automatically for you. So let's pretend we want to bump the version up to 4.5.0. Here's the steps:

  1. Edit CMakeLists.txt and especially:
project(PVSnesLib VERSION 4.5.0 LANGUAGES C CXX)  # ← Only change needed
  1. Build the release target:
cmake --build build --target release

This will create these variables:

# CMake automatically creates from project() call:
PROJECT_VERSION = "4.5.0"           # Full version string
PROJECT_VERSION_MAJOR = 4           # Major number  
PROJECT_VERSION_MINOR = 5           # Minor number
PROJECT_VERSION_PATCH = 0           # Patch number

So the propagation chain will look like this:

CMakeLists.txt: project(PVSnesLib VERSION 4.5.0)
        ↓
CMake Variables: PROJECT_VERSION = "4.5.0"
        ↓
Release Target: pvsneslib_${PROJECT_VERSION}_64b_${PVSNESLIB_OS}.tar.gz
        ↓
Generated File: pvsneslib_4.5.0_64b_darwin.tar.gz

Indeed building created in my machine: ./release/pvsneslib_4.5.0_64b_darwin.tar.gz

I have an arm64 mac, so I fixed the CMake file and it detects the arch properly now, ./release/pvsneslib_4.5.0_arm64_darwin.tar.gz.

@drpaneas drpaneas force-pushed the panos branch 4 times, most recently from 2da7e26 to 7a4cd5d Compare September 30, 2025 13:54
Signed-off-by: Panagiotis Georgiadis <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant