Skip to content

Build System

Bjorn Stahl edited this page Feb 16, 2018 · 2 revisions

This page covers explicit details for developing and working with Arcan from a low level- technical perspective, with some hints towards the rationale for style choices along with explanations how the build system is structured.

Governing Principles

  • Minimize Dependencies - Should be able to link statically with reproducible builds. It should ultimately be possible to embed a packaged appl in a binary that could be signed, verified and executed as first step after a boot loader. No dependencies on funky IPC, init systems, file-system layout or networking. The more we can do with the least amount of code (own AND external deps) the better. Every introduced (rather than removed) dependency should be treated with heavy scrutiny as the cost in increased debugging surface, licensing liabilities, increased configuration management complexity and reduced control tend to be unseemly.

  • Portability helps Quality - Split OS specific binding into single file- single purpose- platform functions, put them in appropriate platform folder and add to corresponding cmake/CMakeLists.OS. OS specific #ifdefs should be reduced to one-liners or eliminated if at all possible.

  • Refactoring implies writing tests - Test coverage does not happen by itself, and code may well have been added that was proof of concept and experimental. When such code is to- / should be- refactored, fixate the behavior by external tests, ideally reachable from both a scripting context and a fuzzer.

  • Steer away from callbacks - They make it easier to write exploits (introducing unnecessary R/W mapped memory that may be end up attacker controllable), are harder to debug (partially broken stack, omitting frame-pointer etc. means debugger has to heuristically determine call path) and optimize. If execution flow has to follow a dynamic path, reduce the possibilities by adding a small (8-bit) indexed indirection in a read-only jump table where non-used indexes trigger monitoring / logging.

  • Separate Parsers - There is a turing- complete (Lua interpreter) parser available already, core engine code should ultimately need no- other ones and the current set will not be expanded upon. Any other tasks can- and should- be split to separate processes or be done as offline tasks by other programs.

  • Least- surprise, Execution Flow - The path to/from any specific function should span a minimal set of compilation units and indirections. Emphasize traceability from an entrypoint (e.g. main) to feature with minimal compilation and use-context considerations (#ifdef hell).

  • Single- Thread, Domain Specific Concurrency - Engine code should try to be thread- safe and reentrant whenever possible, but mixing asynchronous delivery, event- queues and multi-process code with multi-threading is a recipe for disaster.

Build System

The following pseudo-code gives a view about the typical path:

 1. Probe OS, Platform and mix in user specified options. Generate identification tags.
 2. Resolve dependencies, check external/git for specific dependencies that should be inlined statically.
 3. Build shared-memory interface, lockstep it to git revision.
 3. Resolve OS specific platform.
 4. Traverse down frameservers (src/frameservers/CMakeLists.txt).
 5. Traverse down hijack libraries.
 6. Build final targets (arcan, arcan_lwa, frameservers, libs)
 7. Install and forward to packaging systems.

Other steps, generating manpages, running tests etc. are kept manual for the time being. There is also a support script hidden in external/git/clone.sh to pull down gits of projects that should be built statically. Due to the number of external dependencies and external projects that may be included in any one build, emphasize building in parallel and investigate every time those fail (file-system and networking race conditions are painful).

Licensing

Depending on which build flags are used, the end- targets may end up with different licenses. Most of the engine, excluding the .Lua bindings (which are GPLv2 until that API is frozen), are BSD- style licenses. Complete static builds, however, are also reduced into GPLv2 due to LGPL contraints from the OpenAL-soft project.

Code Style

The following annotated nonsense- code attempts to illustrate most of the conventions in use here.

#include <stdint.h>
#include <stdbool.h>
#include <inttypes.h> 

/* 
 * Comments and macros are not indented, English, C style block only,
 * don't mix indentation level between nested languages.
 * 
 * Split at 80 width, linefeed and increment indent one step, no
 * space-padding to get some weird alignment. Assume 2- character tabstop
 * and tabs for indentation. Try to organize so overflown arguments
 * "grow" the block, e.g.
 * 
 * void myfun(int dont, int do, int this,
 *   int dude){}
 * 
 * vs 
 * 
 * void myfun(int do,
 *   int do, int this, int dude){}
 */
arcan_errc fun(char* args[], size_t n_args,
  struct dont_typedef_structs a, int max_three_tier, int tab_indent)
{
/* AVOID: multiple declarations, rather declare close to use and
 * pointer * goes with type not symbol */
 int* a, (* b);

/* AVOID: 'this is an iteration of argv-' kinds of comments,
 * RATHER: 'we ignore the first value in iteration as it contains
 * a user-supplied value' */
  for (size_t i=1; i < argc; i++){
  }

  switch(some.type){
  case ENUM_SOMEVAL:{
    int temporary = 15;
/* case labels do not increment indent level, define auto variables
 * in same scope as use */
  }
  break;
  }

/* 
 * it's ok to skip {} on one-liners, for multiple else cases,
 * use a single line ; to indicate that you have considered the
 * other possible values and they were found irrelevant here
 */
  if (some.type == ENUM_SOMEVAL)
    return ARCAN_ERRC_INVALID_ARGUMENT;
  else
    ; 

/* use designated initializers */
  struct some_type tv = {
   .memb_val = 0xdeadbeef;
  };

/* unscope convenience macros and use sparringly, don't indent macros */
#define SOMEMACRO(X, Y, Z) ( sqrtf((X)*(X) / (Y)*(Y) / (Z)*(Z) )
 SOMEMACRO(1, 2, 3);
#undef SOMEMACRO

 char dynstr[sizeof(struct some_type)];
 snprintf(dynstr, sizeof(dynstr), "VLAs when possible\n");

/* AVOID: malloc/free/realloc,
 * use the abstracted versions arcan_alloc_mem(size, type, hint, align)
 * arcan_alloc_memfill, etc.
 */

/* DO use inttypes- style type macros and explicit casting of
 * VA_ARGS members */
 printf("I am binky, the ptr is called %"PRIxPTR" and this %" INTu32" is an"
   " uint32_t \n", (uintptr_t) args, (uint32_t) tab_indent);

done:
  return ARCAN_OK;
}