Brian Robert Callahan

academic, developer, with an eye towards a brighter techno-social life



[prev]
[next]

2021-06-27
Write an OpenBSD port with me: The TIC-80 fantasy game console

Let's write an OpenBSD port together. Earlier today, I became aware of the TIC-80 tiny computer/fantasy video game console. That sounds incredibly cool and I have been hoping for something like this ever since I learned about the PICO-8 fantasy console. Unfortunately, the PICO-8 is not open source but the TIC-80 is.

Follow along with me as I write an OpenBSD port for the TIC-80. What appears to be a simple port ended up being a quite interesting challenge.

The setup

If you do not already have the ports tree set up, follow the Porter's Handbook for setup instructions. I am going to presume that you already have the ports tree set up, or are going to set it up with help from the documentation.

Once you're set up, we will need to create a working directory for our new port. Fortunately, OpenBSD ports has a workspace that allows the creation of new ports while keeping the official ports tree clean of the new ports we are working on. We'll make a new directory: /usr/ports/mystuff. Anything under this directory will be treated as if it was in the real ports tree. It helps us keep things organized while these ports are not yet in the real ports tree.

The work-in-progress ports under /usr/ports/mystuff need to be organized like the real ports tree. I have decided that our new TIC-80 port should live in the games category and the port name should be tic80, as that is the way the final binary is written and I think it is generally a good idea to name the port and package the same as the binary that the user will run after installing the package. So let's create our new port's working directory: /usr/ports/mystuff/games/tic80. We'll be working from within that directory, so cd into the newly created directory. Finally, we will need a starting Makefile. There is a template in /usr/ports/infrastructure/templates/Makefile.template that you can copy into our new directory. I start off by copying the Makefile of another port, but that might not be the best approach if you are new to porting. In any case, once you have a starting Makefile, our setup is complete.

Learning about our new port

We need to do a little bit of reconnaissance on our new port. Reading through the TIC-80 website, I found their GitHub repository on the learn page. There are a couple of things I am looking for as I browse the repository: how to fetch the source tarball which in this case appears to be an autogenerated tarball, the license which happens to be the MIT license, a list of dependencies which for this project I could not easily find, and any clues as to the build system. I found a CMakeLists.txt file in the repository, which tells me that the build system is CMake, a standard and well-supported build system on OpenBSD. We are ready to begin writing our port Makefile. Based on the information we have gathered, our port Makefile should look like this:

# $OpenBSD$

COMMENT =	open source fantasy video game console (TIC-80)
PKGNAME =	${DISTNAME:S/TIC-80/tic80/}
CATEGORIES =	games x11

GH_ACCOUNT =	nesbox
GH_PROJECT =	TIC-80
GH_TAGNAME =	v0.80.1344

HOMEPAGE =	https://tic80.com/
MAINTAINER =	Brian Callahan <bcallah@openbsd.org>

# MIT
PERMIT_PACKAGE =	Yes

MODULES =	devel/cmake

CONFIGURE_ARGS =	-DBUILD_PRO=ON

.include <bsd.port.mk>

Let's talk about what we have so far.

The COMMENT needs to be 60 characters or less. I chose to append (TIC-80) at the end of the COMMENT because I had the space and it catches users who search for either TIC-80 or tic80. Because the DISTFILE will be ${GH_PROJECT}-${GH_TAGNAME:S/^v//}, that is TIC-80-0.80.1344, but because I want the package name to be tic80, I use a simple sed substitution to set the PKGNAME to tic80-0.80.1344. I set two CATEGORIES for this port: games as the primary CATEGORY and x11 for the secondary CATEGORY. Because we saw that CMakeLists.txt file in the repository, we know we are using CMake, and as such we need to add CMake to the list of MODULES and the ports tree will do the right thing setting up and using CMake for the build.

Now we can fetch the source tarball with: $ make makesum. This will create a distinfo file for us as well.

Configuring for the first time

Let's see what happens if we start the build process in earnest. That is done with: $ make configure.

A couple of problems emerge here. First, it fails because it is looking for dependencies in the vendor directory and also it appears that git is being run, and I don't want that either. So I will need to create a patch to remove git from being executed. I will also need to figure out what is going on with the vendor directory.

Dealing with embedded dependencies

It turns out that TIC-80 wants to statically link all of the libraries it depends on into the binary. And it also wants to build all those dependencies itself as part of the build process. That isn't going to work for us. We have a number of these dependencies in the ports tree already, and some even in base, and we should use those versions over what TIC-80 wants to carry with it.

It also means that we will need to create our own tarball, unfortunately. Let's start by cloning the git repository with the --recursive flag to bring in all those dependencies as well. Note that I will be cloning and using the tip of the TIC-80 tree for the port now.

Now let's look through the list of dependencies that live in the vendor directory to see what we can remove and what needs to stay. There are a couple things here that definitely need to go: SDL2, libpng, and zlib. There are some other things that could go but as they are small and generally not patched in the ports tree, I left them in. We might consider using the ports versions for everything when moving to the real ports tree.

Once removed, I also remove the .git directory as well since it's not useful for ports and is usually quite large. I'll then tar and gzip what's left and toss it on the NYC*BUG mirror for distribution.

Updating the port Makefile

We can now update the port Makefile to use the tarball we created:

# $OpenBSD$

V =		0.90.1678-dev
COMMENT =	open source fantasy video game console (TIC-80)
DISTNAME =	TIC-80-${V}
PKGNAME =	${DISTNAME:S/TIC-80/tic80/}
CATEGORIES =	games x11

HOMEPAGE =	https://tic80.com/
MAINTAINER =	Brian Callahan <bcallah@openbsd.org>

# TIC-80 itself: MIT
# Built-in dependencies:
#   argparse: MIT
#   blip-buf: LGPLv2.1 only
#   dirent: MIT
#   duktape: MIT
#   fennel: MIT
#   giflib: MIT
#   http-parser: MIT
#   libuv: MIT
#   lpeg: MIT
#   lua: MIT
#   moonscript: MIT
#   sdl-gpu: MIT
#   sokol: Zlib
#   squirrel: MIT
#   wren: MIT
#   zip: Unlicense
PERMIT_PACKAGE =	Yes

MASTER_SITES =	https://mirrors.nycbug.org/pub/distfiles/

MODULES =	devel/cmake

CONFIGURE_ARGS =	-DBUILD_PRO=ON \
			-DVERSION_HASH=74fd7f5

.include <bsd.port.mk>

We will need to run: $ make clean=all && make clean=dist && make makesum. This will clear out the previous working directory, remove the old source tarball, and replace it with the new source tarball and update distinfo. Now let's re-run make configure.

Trying to configure, again

Now is the time where we will create the patch to remove git execution from CMake. I could have made this change directly to CMakeLists.txt when I was creating the tarball. However, I do not like or recommend that approach. You should instead let the ports tree handle any differences to upstream. This is because when it comes time to update the port, if you do not use the ports tree to manage your patches, you will need to remember the changes you made. I will definitely forget and I will waste a lot of time trying to remember what I did. Making the changes now allows the ports tree to remember those changes for me.

In addition to the removal of git execution, we also need to tell CMake to use our libraries from ports for those we removed. The final patch looks like this:

$OpenBSD$

Don't do git stuff.
Use SDL2 and curl from ports.

Index: CMakeLists.txt
--- CMakeLists.txt.orig
+++ CMakeLists.txt
@@ -10,39 +10,6 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
     set(VERSION_BUILD ".dbg" )
 endif()
 
-find_package(Git)
-if(Git_FOUND)
-    execute_process(
-        COMMAND ${GIT_EXECUTABLE} status
-        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
-        ERROR_VARIABLE RESULT_STRING
-        OUTPUT_STRIP_TRAILING_WHITESPACE
-    )
-
-    string(LENGTH "${RESULT_STRING}" LENGTH_RESULT_STRING)
-
-    if(${LENGTH_RESULT_STRING} EQUAL 0)
-
-        execute_process(
-            COMMAND ${GIT_EXECUTABLE} log -1 --format=%H
-            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
-            OUTPUT_VARIABLE GIT_COMMIT_HASH
-            OUTPUT_STRIP_TRAILING_WHITESPACE
-        )
-
-        string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH)
-        set(VERSION_HASH ${GIT_COMMIT_HASH} )
-
-        execute_process(
-            COMMAND ${GIT_EXECUTABLE} rev-list HEAD --count
-            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
-            OUTPUT_VARIABLE VERSION_REVISION
-            OUTPUT_STRIP_TRAILING_WHITESPACE
-        )
-
-    endif()
-endif()
-
 project(TIC-80 VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_REVISION} LANGUAGES C CXX)
 message("Building for target : ${CMAKE_SYSTEM_NAME}")
 
@@ -326,7 +293,7 @@ macro(MACRO_CORE SCRIPT DEFINE BUILD_DEPRECATED)
         squirrel 
         duktape 
         blipbuf 
-        zlib)
+        z)
 
     if(${BUILD_DEPRECATED})
         target_compile_definitions(tic80core${SCRIPT} PRIVATE DEPRECATED_CHUNKS)
@@ -368,9 +335,6 @@ if(BUILD_SDL AND NOT EMSCRIPTEN AND NOT RPI)
     endif()
     
     set(SDL_SHARED OFF CACHE BOOL "" FORCE)
-
-    add_subdirectory(${THIRDPARTY_DIR}/sdl2)
-
 endif()
 
 ################################
@@ -391,7 +355,7 @@ if(BUILD_SDL AND BUILD_PLAYER AND NOT RPI)
         target_link_options(player-sdl PRIVATE -static)
     endif()
 
-    target_link_libraries(player-sdl tic80core SDL2-static SDL2main)
+    target_link_libraries(player-sdl tic80core SDL2 SDL2main)
 endif()
 
 ################################
@@ -518,24 +482,6 @@ endif()
 
 if (NOT N3DS)
 
-set(ZLIB_DIR ${THIRDPARTY_DIR}/zlib)
-set(ZLIB_SRC 
-    ${ZLIB_DIR}/adler32.c
-    ${ZLIB_DIR}/compress.c
-    ${ZLIB_DIR}/crc32.c
-    ${ZLIB_DIR}/deflate.c
-    ${ZLIB_DIR}/inflate.c
-    ${ZLIB_DIR}/infback.c
-    ${ZLIB_DIR}/inftrees.c
-    ${ZLIB_DIR}/inffast.c
-    ${ZLIB_DIR}/trees.c
-    ${ZLIB_DIR}/uncompr.c
-    ${ZLIB_DIR}/zutil.c
-)
-
-add_library(zlib STATIC ${ZLIB_SRC})
-target_include_directories(zlib INTERFACE ${THIRDPARTY_DIR}/zlib)
-
 else ()
 
 add_library(zlib STATIC IMPORTED)
@@ -568,7 +514,7 @@ if(BUILD_DEMO_CARTS)
     target_link_libraries(prj2cart tic80core)
 
     add_executable(bin2txt ${TOOLS_DIR}/bin2txt.c)
-    target_link_libraries(bin2txt zlib)
+    target_link_libraries(bin2txt z)
 
     add_executable(xplode 
         ${TOOLS_DIR}/xplode.c 
@@ -655,9 +601,6 @@ if (USE_CURL)
     if(RPI)
         set(CURL_ZLIB OFF CACHE BOOL "" )
     endif()
-
-    add_subdirectory(${THIRDPARTY_DIR}/curl)
-
 endif()
 
 ################################
@@ -681,36 +624,8 @@ endif()
 # PNG
 ################################
 
-set(LIBPNG_DIR ${THIRDPARTY_DIR}/libpng)
-set(LIBPNG_SRC 
-    ${LIBPNG_DIR}/png.c
-    ${LIBPNG_DIR}/pngerror.c
-    ${LIBPNG_DIR}/pngget.c
-    ${LIBPNG_DIR}/pngmem.c
-    ${LIBPNG_DIR}/pngpread.c
-    ${LIBPNG_DIR}/pngread.c
-    ${LIBPNG_DIR}/pngrio.c
-    ${LIBPNG_DIR}/pngrtran.c
-    ${LIBPNG_DIR}/pngrutil.c
-    ${LIBPNG_DIR}/pngset.c
-    ${LIBPNG_DIR}/pngtrans.c
-    ${LIBPNG_DIR}/pngwio.c
-    ${LIBPNG_DIR}/pngwrite.c
-    ${LIBPNG_DIR}/pngwtran.c
-    ${LIBPNG_DIR}/pngwutil.c
-)
+target_link_libraries(tic80core${SCRIPT} png)
 
-configure_file(${LIBPNG_DIR}/scripts/pnglibconf.h.prebuilt ${CMAKE_CURRENT_BINARY_DIR}/pnglibconf.h)
-
-add_library(png STATIC ${LIBPNG_SRC})
-
-target_compile_definitions(png PRIVATE PNG_ARM_NEON_OPT=0)
-
-target_include_directories(png 
-    PUBLIC ${CMAKE_CURRENT_BINARY_DIR} 
-    PRIVATE ${THIRDPARTY_DIR}/zlib
-    INTERFACE ${THIRDPARTY_DIR}/libpng)
-
 ################################
 # TIC-80 studio
 ################################
@@ -759,7 +674,7 @@ target_include_directories(tic80studio PUBLIC ${CMAKE_
 target_link_libraries(tic80studio tic80core zip wave_writer argparse giflib png)
 
 if(USE_CURL)
-    target_link_libraries(tic80studio libcurl)
+    target_link_libraries(tic80studio curl)
 endif()
 
 if(USE_LIBUV)
@@ -846,7 +761,7 @@ if(ANDROID)
 endif()
 
 if(NOT EMSCRIPTEN)
-    target_link_libraries(sdlgpu SDL2-static)
+    target_link_libraries(sdlgpu SDL2)
 endif()
 
 endif()
@@ -914,7 +829,7 @@ if(BUILD_SDL)
         elseif(RPI)
             target_link_libraries(tic80 libSDL2.a bcm_host)
         else()
-            target_link_libraries(tic80 SDL2-static)
+            target_link_libraries(tic80 SDL2)
         endif()
     endif()
 
@@ -1058,7 +973,7 @@ if(BUILD_STUB)
             elseif(RPI)
                 target_link_libraries(tic80${SCRIPT} libSDL2.a bcm_host pthread dl)
             else()
-                target_link_libraries(tic80${SCRIPT} SDL2-static)
+                target_link_libraries(tic80${SCRIPT} SDL2)
             endif()
         endif()

However, because we are now using the ports version of major libraries, we need to teach CMake where those libraries and their headers live. CMake will not know where they are. We can take care of that by adding a pair of CONFIGURE_ARGS to the port Makefile. Since we are already editing the port Makefile, and we know we are using some libraries from ports, let's also register those as LIB_DEPENDS:

# $OpenBSD$

V =             0.90.1678-dev
COMMENT =       open source fantasy video game console (TIC-80)
DISTNAME =      TIC-80-${V}
PKGNAME =       ${DISTNAME:S/TIC-80/tic80/}
CATEGORIES =    games x11

HOMEPAGE =      https://tic80.com/
MAINTAINER =    Brian Callahan <bcallah@openbsd.org>

# TIC-80 itself: MIT
# Built-in dependencies:
#   argparse: MIT
#   blip-buf: LGPLv2.1 only
#   dirent: MIT
#   duktape: MIT
#   fennel: MIT
#   giflib: MIT
#   http-parser: MIT
#   libuv: MIT
#   lpeg: MIT
#   lua: MIT
#   moonscript: MIT
#   sdl-gpu: MIT
#   sokol: Zlib
#   squirrel: MIT
#   wren: MIT
#   zip: Unlicense
PERMIT_PACKAGE =        Yes

MASTER_SITES =  https://mirrors.nycbug.org/pub/distfiles/

MODULES =       devel/cmake

LIB_DEPENDS =	devel/sdl2 \
		graphics/libpng \
		net/curl

CONFIGURE_ARGS =        -DBUILD_PRO=ON \
			-DCMAKE_C_FLAGS="${CFLAGS} -I${LOCALBASE}/include/SDL2 -I${LOCALBASE}/include" \
			-DCMAKE_EXE_LINKER_FLAGS="${LDFLAGS} -L${LOCALBASE}/lib" \
                        -DVERSION_HASH=74fd7f5

.include <bsd.port.mk>

Finally, we are ready to build. That can be done with: $ make build.

Does it build?

Almost. Only one Linuxism to fix:

$OpenBSD$

Fix include header.

Index: src/studio/screens/console.c
--- src/studio/screens/console.c.orig
+++ src/studio/screens/console.c
@@ -38,7 +38,7 @@
 #include <string.h>
 
 #if !defined(__TIC_MACOSX__)
-#include <malloc.h>
+#include <stdlib.h>
 #endif
 
 #if defined (TIC_BUILD_WITH_LUA)

And with that, TIC-80 builds.

Generating the PLIST

CMake should know how to install everything correctly and in a format suitable for a package, so we can jump right to generating the PLIST with: $ make update-plist.

But I noticed that there are additional binaries to aid in TIC-80 cartridge creation that CMake does not install. We can install them manually via a post-install routine in the port Makefile. There is also a @tag update-desktop-database entry at the bottom of the PLIST, meaning we need to add a RUN_DEPENDS on devel/desktop-file-utils:

# $OpenBSD$

V =             0.90.1678-dev
COMMENT =       open source fantasy video game console (TIC-80)
DISTNAME =      TIC-80-${V}
PKGNAME =       ${DISTNAME:S/TIC-80/tic80/}
CATEGORIES =    games x11

HOMEPAGE =      https://tic80.com/
MAINTAINER =    Brian Callahan <bcallah@openbsd.org>

# TIC-80 itself: MIT
# Built-in dependencies:
#   argparse: MIT
#   blip-buf: LGPLv2.1 only
#   dirent: MIT
#   duktape: MIT
#   fennel: MIT
#   giflib: MIT
#   http-parser: MIT
#   libuv: MIT
#   lpeg: MIT
#   lua: MIT
#   moonscript: MIT
#   sdl-gpu: MIT
#   sokol: Zlib
#   squirrel: MIT
#   wren: MIT
#   zip: Unlicense
PERMIT_PACKAGE =        Yes

MASTER_SITES =  https://mirrors.nycbug.org/pub/distfiles/

MODULES =       devel/cmake

LIB_DEPENDS =   devel/sdl2 \
                graphics/libpng \
                net/curl

RUN_DEPENDS =	devel/desktop-file-utils

CONFIGURE_ARGS =        -DBUILD_PRO=ON \
                        -DCMAKE_C_FLAGS="${CFLAGS} -I${LOCALBASE}/include/SDL2 -I${LOCALBASE}/include" \
                        -DCMAKE_EXE_LINKER_FLAGS="${LDFLAGS} -L${LOCALBASE}/lib" \
                        -DVERSION_HASH=74fd7f5

# Install the other binaries.
post-install:
	${INSTALL_PROGRAM} \
		${WRKBUILD}/bin/{bin2txt,cart2prj,player-sdl,prj2cart,xplode} \
			${PREFIX}/bin

.include <bsd.port.mk>

We can regenerate our PLIST with: $ make clean=fake && make update-plist.

Wrapping up

We should now run: $ make test. We discover there are no tests. We must also run: $ make port-lib-depends-check. This will tell us if we have the correct LIB_DEPENDS and generate a WANTLIB for us. We copy and paste that WANTLIB into the port Makefile and add a marker saying there are no tests to run:

# $OpenBSD$

V =             0.90.1678-dev
COMMENT =       open source fantasy video game console (TIC-80)
DISTNAME =      TIC-80-${V}
PKGNAME =       ${DISTNAME:S/TIC-80/tic80/}
CATEGORIES =    games x11

HOMEPAGE =      https://tic80.com/
MAINTAINER =    Brian Callahan <bcallah@openbsd.org>

# TIC-80 itself: MIT
# Built-in dependencies:
#   argparse: MIT
#   blip-buf: LGPLv2.1 only
#   dirent: MIT
#   duktape: MIT
#   fennel: MIT
#   giflib: MIT
#   http-parser: MIT
#   libuv: MIT
#   lpeg: MIT
#   lua: MIT
#   moonscript: MIT
#   sdl-gpu: MIT
#   sokol: Zlib
#   squirrel: MIT
#   wren: MIT
#   zip: Unlicense
PERMIT_PACKAGE =        Yes

WANTLIB += ${COMPILER_LIBCXX} SDL2 c curl m png z

MASTER_SITES =  https://mirrors.nycbug.org/pub/distfiles/

MODULES =       devel/cmake

LIB_DEPENDS =   devel/sdl2 \
                graphics/libpng \
                net/curl

RUN_DEPENDS =   devel/desktop-file-utils

CONFIGURE_ARGS =        -DBUILD_PRO=ON \
                        -DCMAKE_C_FLAGS="${CFLAGS} -I${LOCALBASE}/include/SDL2 -I${LOCALBASE}/include" \
                        -DCMAKE_EXE_LINKER_FLAGS="${LDFLAGS} -L${LOCALBASE}/lib" \
                        -DVERSION_HASH=74fd7f5

NO_TEST =	Yes

# Install the other binaries.
post-install:
        ${INSTALL_PROGRAM} \
                ${WRKBUILD}/bin/{bin2txt,cart2prj,player-sdl,prj2cart,xplode} \
                        ${PREFIX}/bin

.include <bsd.port.mk>

Lastly, we need to write a DESCR, which I essentially copy and pasted from the TIC-80 home page:

TIC-80 is a fantasy computer for making, playing and sharing tiny games.

There are built-in tools for development: code, sprites, maps, sound
editors and the command line, which is enough to create a mini retro
game. You will get a cartridge file, which can be stored and played
locally and uploaded to the TIC-80 website to be played online.

Also, the game can be packed into a player that works on all popular
platforms and distributed as you wish. To make a retro styled game the
whole process of creation takes place under some technical limitations:
240x136 pixel display, 16 color palette, 256 8x8 color sprites and 4
channel sound.

Time to test

Our port is now finished. I posted it to openbsd-wip for testing. Please test and let me know how it works for you so that I can post this new port to ports@ and commit it to the ports tree.

An interesting port

I thought a port of TIC-80 would be relatively straightforward. But it turned out to have some difficult intricacies that I thought deserved a blog post so that others who face a similar situation can have a guide to solving these problems.

Top

RSS