#!/usr/bin/env python3 # Regenerates library/raylib.pl from raylib.h. # # ./util/gen_raylib.py > /tmp/raylib.pl # print # ./util/gen_raylib.py --in-place # write library/raylib.pl # ./util/gen_raylib.py --verify # check layouts against cc # # The header is found via RAYLIB_H, else the first of HEADERS that exists. # # Every RLAPI prototype becomes a use_foreign_module/2 entry, keeping # raylib's own name and its trailing comment. What cannot be expressed is # listed, with a reason, at the end of the generated file. # # STRUCTS ARE FLATTENED, and that is the whole reason this is a script # rather than a hand-written file. src/bif_ffi.c cannot marshal a nested # struct: a struct-typed field is packed at the wrong offset going in, and # the struct-return decoder gives up on one. So a Camera2D has to be # declared as six floats rather than two vector2s, and anything with # mixed field widths needs explicit padding entries at exactly the offsets # the C compiler pads at. That is mechanical, unreadable, and very easy to # get quietly wrong by hand - STRUCTS below is the readable form, and # layout() derives the padding. # # --verify emits a C program of offsetof/sizeof assertions for every # struct and runs it, so a raylib upgrade that moves a field fails loudly # instead of silently corrupting arguments. It has already earned that: # raylib 6.0 dropped FilePathList.capacity, which 5.x had. import re, os, sys, subprocess, tempfile HEADERS = [ '/opt/homebrew/include/raylib.h', '/usr/local/include/raylib.h', '/usr/include/raylib.h', ] OUT = 'library/raylib.pl' # Sizes and alignments are the LP64 ones every platform trealla's FFI # builds on shares; --verify is what proves that for the host in hand. SCALARS = { # name -> (size, align, foreign_struct tag) 'float': (4, 4, 'float'), 'double': (8, 8, 'double'), 'int': (4, 4, 'sint'), 'uint': (4, 4, 'uint'), 'bool': (1, 1, 'bool'), 'char': (1, 1, 'schar'), 'uchar': (1, 1, 'uint8'), 'ptr': (8, 8, 'ptr'), } # (field, type, array length or None), in declaration order. Types are # either a SCALARS key or another entry here. STRUCTS = { 'vector2': [('x','float',None),('y','float',None)], 'vector3': [('x','float',None),('y','float',None),('z','float',None)], 'vector4': [('x','float',None),('y','float',None),('z','float',None),('w','float',None)], 'quaternion': [('x','float',None),('y','float',None),('z','float',None),('w','float',None)], # raylib declares Matrix a row at a time but names the fields column-major. 'matrix': [('m%d'%i,'float',None) for i in (0,4,8,12, 1,5,9,13, 2,6,10,14, 3,7,11,15)], 'color': [('r','uchar',None),('g','uchar',None),('b','uchar',None),('a','uchar',None)], 'rectangle': [('x','float',None),('y','float',None),('width','float',None),('height','float',None)], 'image': [('data','ptr',None),('width','int',None),('height','int',None),('mipmaps','int',None),('format','int',None)], 'texture': [('id','uint',None),('width','int',None),('height','int',None),('mipmaps','int',None),('format','int',None)], 'rendertexture':[('id','uint',None),('texture','texture',None),('depth','texture',None)], 'npatchinfo': [('source','rectangle',None),('left','int',None),('top','int',None),('right','int',None),('bottom','int',None),('layout','int',None)], 'glyphinfo': [('value','int',None),('offsetX','int',None),('offsetY','int',None),('advanceX','int',None),('image','image',None)], 'font': [('baseSize','int',None),('glyphCount','int',None),('glyphPadding','int',None),('texture','texture',None),('recs','ptr',None),('glyphs','ptr',None)], 'camera3d': [('position','vector3',None),('target','vector3',None),('up','vector3',None),('fovy','float',None),('projection','int',None)], 'camera2d': [('offset','vector2',None),('target','vector2',None),('rotation','float',None),('zoom','float',None)], 'mesh': [('vertexCount','int',None),('triangleCount','int',None), ('vertices','ptr',None),('texcoords','ptr',None),('texcoords2','ptr',None),('normals','ptr',None), ('tangents','ptr',None),('colors','ptr',None),('indices','ptr',None), ('boneCount','int',None),('boneIndices','ptr',None),('boneWeights','ptr',None), ('animVertices','ptr',None),('animNormals','ptr',None), ('vaoId','uint',None),('vboId','ptr',None)], 'shader': [('id','uint',None),('locs','ptr',None)], 'materialmap': [('texture','texture',None),('color','color',None),('value','float',None)], 'material': [('shader','shader',None),('maps','ptr',None),('params','float',4)], 'transform': [('translation','vector3',None),('rotation','quaternion',None),('scale','vector3',None)], 'boneinfo': [('name','char',32),('parent','int',None)], 'modelskeleton':[('boneCount','int',None),('bones','ptr',None),('bindPose','ptr',None)], 'model': [('transform','matrix',None),('meshCount','int',None),('materialCount','int',None), ('meshes','ptr',None),('materials','ptr',None),('meshMaterial','ptr',None), ('skeleton','modelskeleton',None),('currentPose','ptr',None),('boneMatrices','ptr',None)], 'modelanimation':[('name','char',32),('boneCount','int',None),('keyframeCount','int',None),('keyframePoses','ptr',None)], 'ray': [('position','vector3',None),('direction','vector3',None)], 'raycollision': [('hit','bool',None),('distance','float',None),('point','vector3',None),('normal','vector3',None)], 'boundingbox': [('min','vector3',None),('max','vector3',None)], 'wave': [('frameCount','uint',None),('sampleRate','uint',None),('sampleSize','uint',None),('channels','uint',None),('data','ptr',None)], 'audiostream': [('buffer','ptr',None),('processor','ptr',None),('sampleRate','uint',None),('sampleSize','uint',None),('channels','uint',None)], 'sound': [('stream','audiostream',None),('frameCount','uint',None)], 'music': [('stream','audiostream',None),('frameCount','uint',None),('looping','bool',None),('ctxType','int',None),('ctxData','ptr',None)], 'vrdeviceinfo': [('hResolution','int',None),('vResolution','int',None),('hScreenSize','float',None),('vScreenSize','float',None), ('eyeToScreenDistance','float',None),('lensSeparationDistance','float',None),('interpupillaryDistance','float',None), ('lensDistortionValues','float',4),('chromaAbCorrection','float',4)], 'vrstereoconfig':[('projection','matrix',2),('viewOffset','matrix',2),('leftLensCenter','float',2),('rightLensCenter','float',2), ('leftScreenCenter','float',2),('rightScreenCenter','float',2),('scale','float',2),('scaleIn','float',2)], 'filepathlist': [('count','uint',None),('paths','ptr',None)], 'automationevent':[('frame','uint',None),('type','uint',None),('params','int',4)], 'automationeventlist':[('capacity','uint',None),('count','uint',None),('events','ptr',None)], # raymath.h only, for MatrixToFloatV / Vector3ToFloatV. 'float3': [('v','float',3)], 'float16': [('v','float',16)], } CNAME = { 'vector2':'Vector2','vector3':'Vector3','vector4':'Vector4','quaternion':'Quaternion','matrix':'Matrix', 'color':'Color','rectangle':'Rectangle','image':'Image','texture':'Texture','rendertexture':'RenderTexture', 'npatchinfo':'NPatchInfo','glyphinfo':'GlyphInfo','font':'Font','camera3d':'Camera3D','camera2d':'Camera2D', 'mesh':'Mesh','shader':'Shader','materialmap':'MaterialMap','material':'Material','transform':'Transform', 'boneinfo':'BoneInfo','modelskeleton':'ModelSkeleton','model':'Model','modelanimation':'ModelAnimation', 'ray':'Ray','raycollision':'RayCollision','boundingbox':'BoundingBox','wave':'Wave','audiostream':'AudioStream', 'sound':'Sound','music':'Music','vrdeviceinfo':'VrDeviceInfo','vrstereoconfig':'VrStereoConfig', 'filepathlist':'FilePathList','automationevent':'AutomationEvent','automationeventlist':'AutomationEventList', 'float3':'float3','float16':'float16', } # raylib typedefs these onto a struct already described above. ALIASES = {'texture2d':'texture','texturecubemap':'texture','rendertexture2d':'rendertexture','camera':'camera3d'} SCALAR_MAP = { 'void':'void', 'bool':'bool', 'int':'sint', 'unsigned int':'uint', 'char':'schar', 'unsigned char':'uint8', 'short':'sshort', 'unsigned short':'ushort', 'long':'slong', 'unsigned long':'ulong', 'float':'float', 'double':'double', } CALLBACKS = {'TraceLogCallback','LoadFileDataCallback','SaveFileDataCallback', 'LoadFileTextCallback','SaveFileTextCallback','AudioCallback'} # Must track src/bif_ffi.c. Both are bounds checked there, so going over # is a resource_error rather than a stray write - but a binding that can # never succeed is still not worth emitting. MAX_FFI_STRUCT_BYTES = 4096 MAX_FFI_RET_BYTES = 4096 DOC_COLUMN = 56 def header_path(): p = os.environ.get('RAYLIB_H') if p: return p for p in HEADERS: if os.path.exists(p): return p sys.exit("no raylib.h found; set RAYLIB_H") def raymath_path(raylib_h): """raymath.h, which ships beside raylib.h.""" p = os.environ.get('RAYMATH_H') or os.path.join(os.path.dirname(raylib_h), 'raymath.h') if not os.path.exists(p): sys.exit("no raymath.h beside %s; set RAYMATH_H" % raylib_h) return p # ---------------------------------------------------------------- layout def align_of(t): if t in SCALARS: return SCALARS[t][1] return max(align_of(ft) for _, ft, _ in STRUCTS[t]) def size_of(t): if t in SCALARS: return SCALARS[t][0] return layout(t)[1] def fields(t): """Every scalar in t, recursively, as (dotted path, type, offset).""" off = 0 for fname, ftype, alen in STRUCTS[t]: a = align_of(ftype) off = (off + a - 1) // a * a for i in range(1 if alen is None else alen): idx = '' if alen is None else '[%d]' % i if ftype in SCALARS: yield (fname + idx, ftype, off) off += SCALARS[ftype][0] else: for sub, st, so in fields(ftype): yield (fname + idx + '.' + sub, st, off + so) off += size_of(ftype) def layout(t): """(entries, size), entries being (tag, path) with padding as (tag, None).""" out, off = [], 0 for path, st, foff in fields(t): while off < foff: gap = foff - off for psz, ptag in ((4,'uint32'), (2,'uint16'), (1,'uint8')): if gap >= psz and off % psz == 0: out.append((ptag, None)) off += psz break else: out.append(('uint8', None)) off += 1 out.append((SCALARS[st][2], path)) off += SCALARS[st][0] a = align_of(t) return out, (off + a - 1) // a * a # ------------------------------------------------------------ headers def prototypes(path, marker='RLAPI '): """(module banner, sub-heading, prototype, trailing comment) per prototype. raylib.h groups under '... (Module: core)' banners with '... functions' sub-headings; raymath.h has only 'Module Functions Definition - X'. """ out, heading, banner = [], None, None for raw in open(path): line = raw.rstrip('\n') if re.match(r'^\s*//-{10,}', line): continue m = re.match(r'^\s*//\s*(.+?)\s*$', line) if m and not line.lstrip().startswith('// NOTE'): txt = m.group(1) if '(Module:' in txt: banner, heading = txt, None elif txt.startswith('Module Functions Definition - '): banner = txt.split(' - ', 1)[1] heading = None elif re.match(r'^[A-Z].*[Ff]unctions\s*$', txt): heading = txt continue if line.startswith(marker): body = line[len(marker):] doc = '' if '//' in body: body, doc = body.split('//', 1) doc = doc.strip() out.append((banner, heading, body.strip().rstrip(';').strip(), doc)) return out def enums(path): src = open(path).read() src = re.sub(r'//.*', '', src) src = re.sub(r'/\*.*?\*/', '', src, flags=re.S) out = [] for body, name in re.findall(r'typedef\s+enum\s*\{(.*?)\}\s*([A-Za-z_][A-Za-z0-9_]*)\s*;', src, flags=re.S): members, nxt = [], 0 for part in body.split(','): part = part.strip() if not part: continue if '=' in part: k, v = part.split('=', 1) k, val = k.strip(), int(v.strip(), 0) else: k, val = part, nxt members.append((k, val)) nxt = val + 1 out.append((name, members)) return out def colors(path): out = [] for line in open(path): m = re.match(r'^#define\s+([A-Z][A-Z0-9_]*)\s+CLITERAL\(Color\)\{\s*' r'([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\}', line) if m: out.append((m.group(1), tuple(int(m.group(i)) for i in (2,3,4,5)))) return out # ------------------------------------------------------------- bindings class Unsupported(Exception): pass def map_type(t, is_ret): """C type -> (foreign type, bytes of struct it costs).""" t = t.strip() if t == '...': raise Unsupported('varargs') base = t.replace('const', '').strip() if base.endswith('*'): inner = base[:-1].strip() # A string. Returns are never 'cstr': that tag makes bif_ffi.c # TPL_free() the pointer, which for raylib is a static buffer or # memory raylib owns. 'ccstr' reads it without freeing. if inner == 'char' and base.count('*') == 1: return ('ccstr' if is_ret else 'cstr'), 0 return 'ptr', 0 if base in CALLBACKS: raise Unsupported('callback') if base in SCALAR_MAP: return SCALAR_MAP[base], 0 k = ALIASES.get(base.lower(), base.lower()) if k in STRUCTS: return k, layout(k)[1] raise Unsupported('unknown type %r' % t) def parse(proto): m = re.match(r'^(.*?)\b([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)$', proto.strip()) ret, name, params = m.group(1).strip(), m.group(2), m.group(3).strip() args = [] if params not in ('void', ''): for p in params.split(','): p = p.strip() if p == '...': args.append('...') continue pm = re.match(r'^(.*?)\b([A-Za-z_][A-Za-z0-9_]*)(\[\])?$', p) args.append(pm.group(1).strip() + (' *' if pm.group(3) else '')) return ret, name, args def convert(proto): ret, name, cargs = parse(proto) ffi_args, total = [], 0 for a in cargs: t, n = map_type(a, False) ffi_args.append(t) total += n if total > MAX_FFI_STRUCT_BYTES: raise Unsupported('struct args total %d bytes > %d' % (total, MAX_FFI_STRUCT_BYTES)) rt, rn = map_type(ret, True) if rn > MAX_FFI_RET_BYTES: raise Unsupported('struct return %d bytes > %d' % (rn, MAX_FFI_RET_BYTES)) return name, ffi_args, rt, len(ffi_args) + (0 if rt == 'void' else 1) # --------------------------------------------------------------- output PREAMBLE = """\ % raylib and raymath bindings for Trealla Prolog. % % GENERATED by util/gen_raylib.py from raylib.h and raymath.h - edit that, % not this. Written against raylib 6.0. % % raymath is in here rather than in a module of its own because it is % header-only, ships with raylib, resolves out of the same libraylib.so, % shares the vector and matrix structs, and shares no function name with % raylib.h. Note that its symbols exist only because raylib was built % with RAYMATH_IMPLEMENTATION and default visibility; a raylib built % otherwise exports none of them, and because do_use_foreign_module % ignores a failed dlsym, that shows up as existence_error per call % rather than as a load failure. % % CALLING CONVENTION % % Bindings keep raylib's own CamelCase names, so they need quoting: % % 'InitWindow'(800, 450, "demo") % % A function returning non-void takes one extra argument, last, to % receive the result. C bools arrive as 0 or 1: % % 'GetScreenWidth'(W) % 'IsKeyDown'(Key, Down) % % A C string return arrives as an ATOM, not a Prolog string: % % ?- 'GetFileName'("/tmp/a/b.png", N). % N = 'b.png'. % % String returns are typed 'ccstr' throughout. The other string tag, % 'cstr', makes bif_ffi.c TPL_free() the returned pointer - which for % raylib is either a static internal buffer or memory raylib owns, so it % must not be used here. The cost is that the few functions returning % freshly allocated text (LoadFileText, TextReplace, TextInsert) leak it; % their Unload counterparts need a pointer this binding no longer has. % % A C float or double argument must be given a Prolog float, never an % integer - bif_ffi.c type-checks the cell tag, so 'DrawCircleV'(P, 20, C) % raises a type_error where 20.0 works. % % STRUCTS % % Structs pass and return as a list whose head is the struct name: % % [color, 255, 255, 255, 255] % [vector2, 3.0, 4.0] % % The lists are FLAT, never nested. A camera2d is six floats in one list, % not two vector2 sublists. This is forced by bif_ffi.c: a struct-typed % field is packed at the wrong offset on the way in, and the struct-return % decoder bails out on one entirely. So each struct below is declared as % its scalar fields in C layout order, with explicit padding fields where % the C compiler inserts padding. Every layout is checked field-by-field % against offsetof/sizeof by `./util/gen_raylib.py --verify`. % % Padding fields read as junk and are ignored on the way in; they are % marked '_' in the layout comment on each declaration. % % Struct field values are NOT type-checked, unlike scalar arguments, and % are read straight through the cell. An integer at or above 2^63 is a % bignum in trealla, and passes its pointer rather than its value - keep % struct fields inside int64 range. % % POINTERS % % A C pointer argument is 'ptr' and shows up as a plain integer address - % whatever a previous call returned. Nothing here allocates one for you, % so the Image*/Mesh*/Wave* mutators are only callable on a pointer raylib % itself handed back. % % CONSTANTS % % raylib_const/2 and raylib_color/2 carry the enum values and predefined % colours, so callers need no magic numbers: % % raylib_const('KEY_SPACE', K), % raylib_color('RAYWHITE', C) % % LOADING % % Where raylib sits outside the loader's default search path - Homebrew on % macOS, for one - a bare run reports % % Error: foreign module creation failed: libraylib.so, raylib % % which means dlopen, not a missing binding. Point the loader at it: % % DYLD_LIBRARY_PATH=/opt/homebrew/lib tpl ... %""" EPILOGUE = """\ % --------------------------------------------------------------- % Not bound. Everything raylib passes or returns by value is % reachable; what is left needs FFI machinery that does not exist. % % 'callback' % takes a C function pointer. The FFI cannot build a closure % that calls back into Prolog. % % 'varargs' % TextFormat and TraceLog are printf-style, which needs % ffi_prep_cif_var and a per-call signature. % % Struct size is no longer a reason. MAX_FFI_STRUCT_BYTES and % MAX_FFI_RET_BYTES in src/bif_ffi.c allow 4096 bytes of struct % arguments per call and 4096 bytes of returned struct, both bounds % checked; the largest thing raylib passes by value is Model, at 136 % bytes. They were 64 and 256, which put the whole model and mesh % API out of reach. % --------------------------------------------------------------- """ def generate(path, mpath): # raymath.h is folded into this same module; PREAMBLE says why. protos = prototypes(path) + [('raymath: ' + (b or 'misc'), h, p, d) for b, h, p, d in prototypes(mpath, 'RMAPI ')] bound, skipped = [], [] for banner, heading, proto, doc in protos: try: bound.append((banner, heading, doc) + convert(proto)) except Unsupported as e: skipped.append((parse(proto)[1], str(e))) used = set() for _, _, _, _, args, rt, _ in bound: for a in list(args) + [rt]: if a in STRUCTS: used.add(a) L = [] A = L.append A(PREAMBLE) A("% NOT BOUND") A("%") A("%% %d of the %d functions across both headers are left out - the %d" % (len(skipped), len(protos), sum(1 for _, w in skipped if w == 'callback'))) A("%% that take a C callback and the %d that are varargs, all of them in" % sum(1 for _, w in skipped if w == 'varargs')) A("%% raylib.h. All %d of raymath.h is bound. See the end of this file." % sum(1 for b, _, _, _, _, _, _ in bound if str(b).startswith('raymath'))) A("") # module + exports, grouped by the headers' module banners A(":- module(raylib, [") groups, order = {}, [] for banner, _, _, name, _, _, arity in bound: if banner not in groups: groups[banner] = [] order.append(banner) groups[banner].append("'%s'/%d" % (name, arity)) for b in order: A("\t%% %s" % (b or 'misc')) for it in groups[b]: A("\t%s," % it) A("") A("\t% constants") A("\traylib_const/2,") A("\traylib_color/2") A("\t]).") A("") # struct declarations A("% Struct layouts, flattened. The comment on each line names the") A("% fields in order; '_' is compiler padding.") A("") for name in STRUCTS: if name not in used: continue flat, total = layout(name) A("%% %s (%d bytes): %s" % (CNAME[name], total, ', '.join(p if p else '_' for _, p in flat))) A(":- foreign_struct(%s, [%s])." % (name, ','.join(t for t, _ in flat))) A("") aliased = [(a, t) for a, t in ALIASES.items() if t in used] if aliased: A("% raylib typedefs these to structs already declared above; the") A("% bindings below just use the underlying name.") for a, t in aliased: A("%% %s -> %s" % (a, t)) A("") # the bindings A(":- use_foreign_module('libraylib.so', [") cur_b = cur_h = object() for i, (banner, heading, doc, name, args, rt, _) in enumerate(bound): if banner != cur_b: if i: A("") A("\t%% ==== %s ====" % (banner or 'misc')) cur_b, cur_h = banner, object() if heading != cur_h: A("\t%% %s" % (heading or '')) cur_h = heading sig = "\t'%s'([%s], %s)%s" % (name, ','.join(args), rt, '' if i == len(bound)-1 else ',') A(sig.ljust(DOC_COLUMN) + ' % ' + doc if doc else sig) A("\t]).") A("") # constants A("% ---------------------------------------------------------------") A("% Constants, transcribed from the raylib enums and #defines so") A("% callers need not hardcode magic numbers.") A("% ---------------------------------------------------------------") A("") A("%% raylib_const(?Name, ?Value) is nondet.") A("%") A("% Enum constants, under their C names:") A("%") A("% ?- raylib_const('KEY_SPACE', K).") A("% K = 32.") A("") for ename, members in enums(path): A("%% %s" % ename) for k, v in members: A("raylib_const('%s', %d)." % (k, v)) A("") A("%% raylib_color(?Name, ?Color) is nondet.") A("%") A("% The predefined colours, as ready-to-pass color structs:") A("%") A("% ?- raylib_color('RAYWHITE', C).") A("% C = [color,245,245,245,255].") A("") for k, (r, g, b, a) in colors(path): A("raylib_color('%s', [color,%d,%d,%d,%d])." % (k, r, g, b, a)) A("") A(EPILOGUE) for name, why in sorted(skipped): A("%% %-28s %s" % (name, why)) A("") return '\n'.join(L) + '\n' # --------------------------------------------------------------- verify def verify(path): """Assert every computed offset and size against the C compiler.""" src = ['#include ', '#include ', '#include "%s"' % path, '#include "%s"' % raymath_path(path), 'int fails = 0;', '#define CK(S,F,E) do{ if(offsetof(S,F)!=(size_t)(E)){' 'printf("OFFSET %s.%s: cc=%zu here=%d\\n",#S,#F,offsetof(S,F),(int)(E));fails++;} }while(0)', '#define CKSZ(S,E) do{ if(sizeof(S)!=(size_t)(E)){' 'printf("SIZE %s: cc=%zu here=%d\\n",#S,sizeof(S),(int)(E));fails++;} }while(0)', 'int main(void) {'] n = 0 for name in STRUCTS: src.append('\tCKSZ(%s, %d);' % (CNAME[name], layout(name)[1])) for fpath, _, off in fields(name): src.append('\tCK(%s, %s, %d);' % (CNAME[name], fpath, off)) n += 1 src.append('\tif (!fails) printf("%%d struct layouts OK\\n", %d);' % len(STRUCTS)) src.append('\treturn fails ? 1 : 0;\n}') with tempfile.TemporaryDirectory() as d: c, exe = os.path.join(d, 'v.c'), os.path.join(d, 'v') open(c, 'w').write('\n'.join(src)) cc = os.environ.get('CC', 'cc') r = subprocess.run([cc, '-I' + os.path.dirname(path), '-o', exe, c], capture_output=True, text=True) if r.returncode: print(r.stderr, file=sys.stderr) return 1 r = subprocess.run([exe], capture_output=True, text=True) sys.stdout.write(r.stdout) print("%d fields checked" % n, file=sys.stderr) return r.returncode if __name__ == '__main__': path = header_path() if '--verify' in sys.argv: sys.exit(verify(path)) body = generate(path, raymath_path(path)) if '--in-place' in sys.argv: i = sys.argv.index('--in-place') target = sys.argv[i+1] if len(sys.argv) > i+1 else OUT old = open(target).read() if os.path.exists(target) else None # Unchanged output must not touch the file: library/%.c depends on # it, so a rewrite forces a rebuild of the embedded library. how = 'unchanged' if old == body else ('written' if old else 'created') if how != 'unchanged': open(target, 'w').write(body) print("%s: %s" % (target, how), file=sys.stderr) else: sys.stdout.write(body)