trying to make standalone game w/ Lectrote

Okay, I’ve succeeded. I took David C’s 2017 script, put it alongside the current (May 2022) version of makedist.py from Zarf’s Lectrote project, and hacked the latter using the former to produce a script with which you can create the win32 Lectrote builds of games on a PC, from the command line, today. (The bold isn’t for ego purposes, it’s to make clear what this does at a glance for future readers.)

NOTE: The script as is is tailored to only produce the win32 builds, using a PC. I can’t vouch for its ability to produce any other builds by any means. This is because I hacked it to work; it’s not a pure translation of Zarf’s bash script to the command line environment. More knowledgable people might be able to do that. I use this script to produce the Windows versions of a Lectrote-wrapped game on a PC, while I go produce the Linux and Mac versions on a Mac. ALSO!.. this script fails at the zip stage, at least on my PC. But that doesn’t matter – at that point, it’s already built the game. You can zip the files up with any zip utility.

I’m not allowed to upload a python suffix file so I’ll paste the script in the details pane below.

Summary
#!/usr/bin/env python3

# Usage: python3 makedist.py [--game yourdir] [win32]
#
# This script copies the working files (everything needed to run Lectrote)
# into prebuilt Electron app packages. Fetch these from
#    https://github.com/atom/electron/releases
# and unzip them into a "dist" directory.

import sys
import os, os.path
import optparse
import shutil
import json
import subprocess

all_packages = [
    'darwin-x64',
    'darwin-arm64',
    'darwin-univ',
    'linux-x64',
    'linux-arm64',
    'win32-ia32',
    'win32-x64',
    'win32-arm64',
]

popt = optparse.OptionParser()

popt.add_option('-b', '--build',
                action='store_true', dest='makedist',
                help='build dist directories')
popt.add_option('-z', '--zip',
                action='store_true', dest='makezip',
                help='turn dist directories into zip files')
popt.add_option('-n', '--none',
                action='store_true', dest='makenothing',
                help='do nothing except look at the arguments')
popt.add_option('-g', '--game', '--gamedir',
                action='store', dest='gamedir',
                help='directory for game-specific files')
popt.add_option('--macsign',
                action='store', dest='macsign',
                help='Apple Developer cert name')
popt.add_option('-v', '--version',
                action='store', dest='buildversion',
                default='1',
                help='build version (default 1)')

(opts, args) = popt.parse_args()


appfiles = [
    './package.json',
    './main.js',
    './apphooks.js',
    './formats.js',
    './play.html',
    './prefs.html',
    './prefs.js',
    './fonts.js',
    './about.html',
    './if-card.html',
    './if-card.js',
    './fonts.css',
    './play.css',
    './el-glkote.css',
    './font',  # all files
    './icon-128.png',
    './icon-tray.ico',
    './docicon-glulx.ico',
    './docicon-zcode.ico',
    './docicon-hugo.ico',
    './docicon-tads.ico',
    './docicon-json.ico',
    './quixe/lib/elkote.min.js',
    './quixe/lib/jquery-1.12.4.min.js',
    './quixe/lib/quixe.min.js',
    './quixe/media/waiting.gif',
    {
        'key': 'ifvms',
        'files': [
            './zplay.html',
            './ifvms/zvm.min.js',
            './ifvms/zvm_dispatch.min.js',
            './ifvms/gi_load.min.js',
            './ifvms/zvm.css',
            './ifvms/package.json',
        ]
    },
    {
        'key': 'emglken',
        'files': [
            './emglkenplay.html',
            'emglken/gi_load.min.js',
            'emglken/git-core.wasm',
            'emglken/git.js',
            'emglken/glulxe-core.wasm',
            'emglken/glulxe.js',
            'emglken/hugo-core.wasm',
            'emglken/hugo.js',
            'emglken/tads-core.wasm',
            'emglken/tads.js',
            'emglken/versions.json',
        ]
    },
    {
        'key': 'inkjs',
        'files': [
            './inkplay.html',
            './inkplay.js',
            './inkjs/ink.min.js',
            './inkjs/ink-130.min.js',
            './inkjs/ink-146.min.js',
            './inkjs/ink-160.min.js',
            './inkjs/package.json',
        ]
    },
]

rootfiles = [
    './LICENSE',    
    './LICENSES-FONTS.txt',
]

win32_rootfiles = [
    'LICENSE',
    'LICENSES-FONTS.txt',
]

def install(resourcedir, pkg):
    if not os.path.isdir(resourcedir):
        raise Exception('path does not exist: ' + resourcedir)
    appdir = resourcedir
    print('Installing to: ' + appdir)

    soleterp = pkg.get('lectroteSoleInterpreter')

    appfilesused = []
    for val in appfiles:
        if type(val) is dict:
            if soleterp and val['key'] != soleterp:
                continue
            for filename in val['files']:
                appfilesused.append(filename)
        else:
            appfilesused.append(val)
    
    os.makedirs(appdir, exist_ok=True)
    qdir = os.path.join(appdir, 'quixe')
    os.makedirs(qdir, exist_ok=True)
    os.makedirs(os.path.join(qdir, 'lib'), exist_ok=True)
    os.makedirs(os.path.join(qdir, 'media'), exist_ok=True)
    zvmdir = os.path.join(appdir, 'ifvms')
    os.makedirs(zvmdir, exist_ok=True)
    emglkendir = os.path.join(appdir, 'emglken')
    os.makedirs(emglkendir, exist_ok=True)
    inkdir = os.path.join(appdir, 'inkjs')
    os.makedirs(inkdir, exist_ok=True)
# SECTION BETWEEN HERE AND NEXT COMMENT IS A REPLACEMENT FROM DAVID
    for filename in appfilesused:
        srcfilename = filename
        if opts.gamedir:
            val = os.path.join(opts.gamedir, filename)
            if os.path.exists(val):
                srcfilename = val

        tgtfilename = os.path.join(appdir, filename)
        if not os.path.isdir(filename):
            if not os.path.exists(tgtfilename):
                shutil.copyfile(srcfilename, tgtfilename)
        else:
            subdirname = os.path.join(appdir, filename)
            os.makedirs(subdirname, exist_ok=True)
            for subfile in os.listdir(srcfilename):
                tgtfilename = os.path.join(subdirname, subfile)
                if not os.path.exists(tgtfilename):
                    shutil.copyfile(os.path.join(srcfilename, subfile), tgtfilename)

    extrafiles = pkg.get('lectroteExtraFiles')
    if opts.gamedir and extrafiles:
        gamedir = os.path.basename(opts.gamedir)
        os.makedirs(gamedir, exist_ok=True)
        for filename in extrafiles:
            srcfilename = os.path.join(opts.gamedir, filename)
            if not os.path.isdir(filename):
                tgtfilename = os.path.join(gamedir, filename)
                if not os.path.exists(tgtfilename):
                    shutil.copyfile(srcfilename, tgtfilename)
            else:
                subdirname = os.path.join(gamedir, filename)
                os.makedirs(subdirname, exist_ok=True)
                for subfile in os.listdir(srcfilename):
                    tgtfilename = os.path.join(subdirname, subfile)
                    if not os.path.exists(tgtfilename):
                        shutil.copyfile(os.path.join(srcfilename, subfile), tgtfilename)

def builddir(dir, pack, pkg):
    (platform, dummy, arch) = pack.partition('-')
# DAVID REPLACEMENT ENDS HERE
    cmd = 'node_modules/.bin/electron-packager'
#DAVID
    if platform == 'win32':
        cmd = 'node_modules\.bin\electron-packager'
#DAVID
    args = [
        cmd, 'tempapp', product_name,
        '--app-version', product_version,
        '--build-version', opts.buildversion,
        '--arch='+arch, '--platform='+platform,
        '--out', 'dist',
        '--overwrite'
        ]

    if platform == 'darwin' and arch == 'univ':
        cmd = 'node'
        args = [ cmd, 'tools/makemacuni.js', dir, product_name ]
        if opts.macsign:
            args = args + [
                '--osx-sign.entitlements', 'resources/mac-app.entitlements',
                '--osx-sign.entitlements-inherit', 'resources/mac-app.entitlements',
                '--osx-sign.identity', opts.macsign,
                '--osx-sign.hardenedRuntime', 'true',
            ]
    
    elif platform == 'darwin':
        appid = 'com.eblong.lectrote'
        if opts.gamedir:
            appid = pkg.get('lectroteMacAppID')
            if not appid:
                raise Exception('Mac package must set lectroteMacAppID')
            if appid == 'com.eblong.lectrote':
                raise Exception('lectroteMacAppID must not be com.eblong.lectrote')

        iconpath = 'resources/appicon-mac.icns'
        if opts.gamedir and os.path.exists(os.path.join(opts.gamedir, 'resources/appicon-mac.icns')):
            iconpath = os.path.join(opts.gamedir, 'resources/appicon-mac.icns')
        
        args = args + [
            '--app-bundle-id='+appid,
            '--app-category-type=public.app-category.games',
            '--icon='+iconpath,
            '--extra-resource=resources/icon-glulx.icns',
            '--extra-resource=resources/icon-zcode.icns',
            '--extra-resource=resources/icon-hugo.icns',
            '--extra-resource=resources/icon-tads.icns',
            '--extra-resource=resources/icon-blorb.icns',
            '--extra-resource=resources/icon-gblorb.icns',
            '--extra-resource=resources/icon-zblorb.icns',
            '--extra-resource=resources/icon-glksave.icns',
            '--extra-resource=resources/icon-glkdata.icns',
            '--extra-resource=resources/icon-json.icns',
            '--extend-info', 'resources/Add-Info.plist',
            ]
        if opts.macsign:
            args = args + [
                '--osx-sign.entitlements', 'resources/mac-app.entitlements',
                '--osx-sign.entitlements-inherit', 'resources/mac-app.entitlements',
                '--osx-sign.identity', opts.macsign,
                '--osx-sign.hardenedRuntime', 'true',
            ]

    if platform == 'win32':
        iconpath = 'resources/appicon-win.ico'
        if opts.gamedir and os.path.exists(os.path.join(opts.gamedir, 'resources/appicon-win.ico')):
            iconpath = os.path.join(opts.gamedir, 'resources/appicon-win.ico')

        filedesc = 'Interactive Fiction Interpreter'
        if opts.gamedir and pkg.get('description'):
            filedesc = pkg.get('description')

        if not opts.gamedir:
            companyname = 'Zarfhome Software'
        else:
            companyname = pkg.get('lectroteCompanyName')
        if companyname:
            args.append('--win32metadata.CompanyName='+companyname)

        if not opts.gamedir:
            copyright = 'Copyright 2016 by Andrew Plotkin'
        else:
            copyright = pkg.get('lectroteCopyright')
        if copyright:
            args.append('--app-copyright='+copyright)
        
        args = args + [
            '--win32metadata.InternalName='+product_name,
            '--win32metadata.ProductName='+product_name,
            '--win32metadata.OriginalFilename='+product_name+'.exe',
            '--win32metadata.FileDescription='+filedesc,
            '--icon='+iconpath,
            ]
#DAVID
        rootfiles = win32_rootfiles

    res = subprocess.call(args, shell=True)
#DAVID
    if res:
        raise Exception('electron-packager failed')

    for filename in rootfiles:
        shutil.copyfile(filename, os.path.join(dir, filename))
    val = os.path.join(dir, 'version')
    if os.path.exists(val):
        os.unlink(val)
    
def makezip(dir, unwrapped=False):
    prefix = product_name + '-'
    val = os.path.split(dir)[-1]
    if not val.startswith(prefix):
        raise Exception('path does not have the prefix')
    platform = val[len(prefix):]
#DAVID
    zipfile = product_name + '-' + product_version + '-' + val[len(prefix):]
#DAVID
    zipargs = '-q'
    if 'darwin' in zipfile:
        zipfile = zipfile.replace('darwin', 'macos')
        print('AppDMGing up: %s to %s' % (dir, zipfile))
        specfile = 'resources/pack-dmg-spec.json'
        if opts.gamedir and os.path.exists(os.path.join(opts.gamedir, specfile)):
            specfile = os.path.join(opts.gamedir, specfile)
        tmpspecfile = specfile.replace('/pack-dmg-spec.json', '/pack-dmg-spec-tmp.json')
        dat = open(specfile).read()
        dat = dat.replace('$PLATFORM$', platform)
        open(tmpspecfile, 'w').write(dat)
        subprocess.call('rm -f "dist/%s.dmg"; node_modules/.bin/appdmg "%s" "dist/%s.dmg"' % (zipfile, tmpspecfile, zipfile),
                        shell=True)
        return
    print('Zipping up: %s to %s (%s)' % (dir, zipfile, ('unwrapped' if unwrapped else 'wrapped')))
    if unwrapped:
        if 'win32' in zipfile:
            os.chdir('dist')
            os.chdir(zipdir)
            tgtfile = '..\%s.zip' % (zipfile)
            if os.path.exists(tgtfile):
                os.remove(tgtfile)
            subprocess.call('bestzip ..\%s.zip .\\' % (zipfile), shell=True)
            os.chdir('..\\')
            os.chdir('..\\')
    else:
        dirls = os.path.split(dir)
        subdir = dirls[-1]
        topdir = os.path.join(*os.path.split(dir)[0:-1])
        if 'win32' in zipfile:
            subprocess.call('cd "%s"; del /F "%s.zip"; bestzip "%s" -r "%s.zip" "%s"' % (topdir, zipfile, zipargs, zipfile, subdir), shell=True)

# Start work! First, read the version string out of package.json.

pkgfile = 'package.json'
if opts.gamedir and os.path.exists(os.path.join(opts.gamedir, 'package.json')):
    pkgfile = os.path.join(opts.gamedir, 'package.json')
fl = open(pkgfile)
pkg = json.load(fl)
fl.close()

product_version = pkg['version']
product_name = pkg['productName'];
print('%s version: %s' % (product_name, product_version,))
if product_name != 'Lectrote':
    print('%s version: %s' % ('Lectrote', pkg['lectroteVersion'],))

# Decide what distributions we're working on. ("packages" is a bit overloaded,
# sorry.)

packages = []
if not args:
    packages = all_packages
else:
    for pack in all_packages:
        for arg in args:
            if arg in pack:
                packages.append(pack)
                break

if not packages:
    raise Exception('no packages selected')
#DAVID
if not opts.gamedir:
    os.makedirs('tempapp', exist_ok=True)
    install('tempapp', pkg)

if opts.gamedir:
    os.makedirs(opts.gamedir, exist_ok=True)
    install(opts.gamedir, pkg)
#DAVID
os.makedirs('dist', exist_ok=True)

doall = not (opts.makedist or opts.makezip or opts.makenothing)

if doall or opts.makedist:
    for pack in packages:
        (platform, dummy, arch) = pack.partition('-')

        if platform == 'win32':
            dest = 'dist\%s-%s' % (product_name, pack,)
            builddir(dest, pack, pkg)

if doall or opts.makezip:
    for pack in packages:
        (platform, dummy, arch) = pack.partition('-')

        if platform == 'win32':
            zippath = 'dist\%s-%s' % (product_name, pack,)
            zipdir = '%s-%s' % (product_name, pack,)
            makezip(zippath, zipdir, unwrapped=('win32' in pack))

-Wade

2 Likes