Getting Started

Alright, let's cut to the chase and get a blank project going.

Install

  1. Install Nim if you haven't already, and make sure that it is accessible from the command line (PATH or something).
  2. Download SDCC, either the snapshots or the GBDK-2020 distribution. Extract it somewhere.

Setup

We're going to use Atlas, the workspace manager that is bundled with Nim starting from version 2.0. It doesn't have the most conventional way of managing things—but it works I guess.

For the following, a Unix-like shell will be assumed; adjust the commands as needed.

mkdir gbnim-workspace
cd gbnim-workspace
atlas init

This will initialize an Atlas workspace, a common environment in which every Jibby project would reside.

mkdir Blank
cd Blank
atlas use 'https://github.com/zoomten/jibby#head'

This initializes a project within the workspace that uses the Jibby library. As the package is currently "unpublished", the command to "use" this package currently points to the source repo.

And yes, the folder name is capitalized. This is because, in the workspace folder just up above, the jibby library will be present alongside the project, instead of being in a "dependencies" folder or something like that. I told you it's not very conventional, though this may change… This naming is not a requirement, although it may help you differentiate projects you made from dependencies pulled in from somewhere else.

Either way, two files will be generated:

  1. Blank.nimble: the traditional Nimble package file, which is used to tell Atlas what the project needs.
  2. nim.cfg: The file which contains what command line parameters to be automatically added when the Nim compiler is invoked.

Compiler tasks

However, we are not going to be invoking the Nim compiler in the usual way. Instead we are going to create tasks that we can invoke with the compiler instead. Create a new file, config.nims (and yes, it must be named that way), which will contain:

when defined(nimsuggest):
  import system/nimscript

from std/os import `/`, absolutePath
from std/strutils import join
const
  srcPath = "src"
  romName = "blank"

import jibby/helper/scriptConfig
if projectPath().absolutePath() == thisDir() / srcPath / romName:
  precompileTools()
  setupToolchain()
  patchCompiler()
  switch "listCmd"

task build, "Build the ROM":
  selfExec(
    (
      @["compile"] & makeArgs() & @["-o:" & romName & ".gb"] &
      @[srcPath / romName]
    ).join(" ")
  )

task clean, "Clean build artifacts":
  for i in [".gb", ".ihx", ".map", ".noi", ".sym"]:
    rmFile(romName & i)
  rmDir(".tools")

Aight, let's break this down.

when defined(nimsuggest):
  import system/nimscript

So this bit is for the IDEs to not freak out when given this file. When this file is executed by the Nim compiler, it will automatically import this. Hopefully this issue will be fixed some time.

const
  srcPath = "src"
  romName = "blank"

Here we define two constants: our source folder relative to this Blank folder, and the name of the source file (and corresponding ROM) within that folder.

import jibby/helper/scriptConfig
if projectPath().absolutePath() == thisDir() / srcPath / romName:
  precompileTools()
  setupToolchain()
  patchCompiler()
  switch "listCmd"

Here we set the additional stuff to do only when the compiler is given the file src/blank.nim. We do this to ensure that when the compiler processes files that get imported from that main file, we don't do these again.

  1. precompileTools: See scriptConfig: precompileTools. As the name implies, it will precompile the compile and link wrappers needed to correctly compile the ROM, see the former's link for why this is needed. These wrappers will then be compiled under a hidden .tools directory.
  2. setupToolchain: See scriptConfig: setupToolchain. This will configure the Nim compiler to set what I think are "optimal" settings for the Game Boy platform.
  3. patchCompiler: See scriptConfig: patchCompiler. This will redirect one of Nim's system files to a custom one provided by Jibby. At the time of writing, this will be the copyMem and related functions.
  4. switch "listCmd": This one is optional, but it will show exactly what commands are being called by the Nim compiler.
task build, "Build the ROM":
  selfExec(
    (
      @["compile"] & makeArgs() & @["-o:" & romName & ".gb"] &
      @[srcPath / romName]
    ).join(" ")
  )

Here we define a build command. This is really just a quick shortcut to invoke nim compile -o:blank.gb src/blank.nim. Of course, blank.gb and src/blank.nim originating from the romName and srcPath variables we defined earlier. makeArgs() here attempts to pass whatever relevant defines you have set to the compiler process spawned here, in order for the tools to process them. See scriptConfig: makeArgs for more details.

task clean, "Clean build artifacts":
  for i in [".gb", ".ihx", ".map", ".noi", ".sym"]:
    rmFile(romName & i)
  rmDir(".tools")

The clean command here does pretty much what it says on the tin.

Nim compiler configuration

You can add this to the nim.cfg file, so you don't need to specify GBDK_ROOT manually in your shell:

--putEnv:GBDK_ROOT:"/root/gbdk"

Obviously, you need to point this to the absolute path where you extracted SDCC or GBDK to.

There is an additional rule here, namely directly beneath whatever you set GBDK_ROOT there must exist a bin folder containing all the SDCC programs, and an include folder containing the SDCC .h files. If you have downloaded the snapshots, the includes will be found in share/include, and you need to move this folder up.

Sources

So that's the configuration. But we can't build anything if we don't have a source file, can we? Fortunately, since this is a blank program, we can do this easily.

Create a folder called src, and then inside that folder create blank.nim:

import jibby/runtime/init
import jibby/runtime/vblank

Finally, in the same folder, create panicoverride.nim:

proc panic(s: string) =
  discard

proc rawoutput(s: string) =
  discard

The reason why this extra file is needed is because setupToolchain() earlier set the compiler's OS target to standalone. As a result, the compiler wants to include a specific file called panicoverride.nim located near the file it's compiling, to provide those two procs that it needs for this target.

Build

Alright, let's try building:

nim build

If everything went right, you should end up with a lot of files:

  1. blank.gb: The ROM!
  2. blank.sym: A plain-text symbol file that can be processed by emulators like BGB, SameBoy, or Emulicious to make the resulting assembly code a little easier to parse.
  3. blank.map: A plain-text file that tells you exactly where every section is, how big are they, and what object files were linked into the ROM.
  4. blank.ihx: An Intel Hex representation of the ROM, automatically generated by the SDLD linker, from whence the final .gb file came.
  5. blank.noi: The NoICE symbols automatically generated by the SDLD linker, from whence the .sym file came.
  6. .tools/: A folder containing the wrappers that were needed to compile the ROM.

All of those files can be cleaned automatically by doing:

nim clean

Because the build and clean commands are something that was defined earlier, you can change it to fit your specific needs.

But for now, go ahead and inspect the ROM in an emulator!