trying to make standalone game w/ Lectrote

Thanks for figuring this out, this is really good work.

Indeed. I just stopped looking at it after I got it to work on my Mac, so I’m happy it’s now usable by more people.

Success!

One glitch: though I ran “npm install --save-dev bestzip” successfully (or at least with no error messages), when I run makedist.py I’m getting this error at the very end:

Zipping up: dist\Anchorhead-win32-ia32 to Anchorhead-1.0.0-win32-ia32 (unwrapped) 'bestzip' is not recognized as an internal or external command, operable program or batch file. Zipping up: dist\Anchorhead-win32-x64 to Anchorhead-1.0.0-win32-x64 (unwrapped) 'bestzip' is not recognized as an internal or external command, operable program or batch file.

However, I’m assuming that bestzip’s job is just to zip up everything in the dist directories, which I can certainly do on my own, manually. Is this assumption correct? Is there anything else that bestzip is meant to do (e.g., does it create a self-extracting executable rather than just a ZIP file, or create registry entries, or something like that?)

npm install -g bestzip

sorry - should be global - and you might have to reopen your command window

although you can just zip the dist… folder yourself using the built in windows Send to Compressed Folder right-click menu item

There are some tools to turn an Electron app into a Windows installer. If I poke around the gulp tasks, I might look into that too.

Well creating an installer is easy, once you have the dist folders…

REF: github.com/electron/windows-installer

  • create buildexe.js file based on code snippet below in your /dist folder
    NOTE: The .exe has to match what’s in the dist folder
  • npm install -g electron-winstaller
  • from command line (I’d recommend as Administrator):
    node buildexe.js
  • results will be in dist/release including msi and exe installers (and a nuget installer, but who cares)

As noted in the doc, if you plan to sell your app, you should purchase a code-signing cert and sign the msi/exe before distributing

buildexe.js

var winInstaller = require('electron-winstaller');

resultPromise = winInstaller.createWindowsInstaller({
    appDirectory: './shadow-win32-ia32',
    outputDirectory: './release',
    authors: 'Ian Finley & Jon Ingold',
    exe: 'shadow.exe'
  });

resultPromise.then(() => console.log("It worked!"), (e) => console.log(`No dice: ${e.message}`));

Hi guys, I’m trying to build this too, and I got this error in Win 10 x64:

[code]npm WARN optional SKIPPING OPTIONAL DEPENDENCY: appdmg@^0.4.0 (node_modules\appdmg):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for appdmg@0.4.5: wanted {“os”:“darwin”,“arch”:“any”} (current: {“os”:“win32”,“arch”:“x64”})
npm ERR! Windows_NT 10.0.15063
npm ERR! argv “C:\Program Files\nodejs\node.exe” “C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js” “install”
npm ERR! node v6.11.4
npm ERR! npm v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! lectrote@1.2.9 preinstall: run-script-os
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the lectrote@1.2.9 preinstall script ‘run-script-os’.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the lectrote package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR! run-script-os
npm ERR! You can get information on how to open an issue for this project with:
npm ERR! npm bugs lectrote
npm ERR! Or if that isn’t available, you can get their info via:
npm ERR! npm owner ls lectrote
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR! E:\Aventuras\lectrote-lectrote-1.2.9\npm-debug.log[/code]

I’m not in a hurry for this, and I though in just abandon. Because this error seems like a more primordial problem than the failure to import Quixe and such. So, I have no idea how to proceed. My problem doesn’t seem equal to any of this threads. Anyway, I’ve attached the debug file.
npm-debug.log (27.7 KB)

Did you update your make file with the code I posted (on this thread) for Windows? The out-of-the-box script doesn’t run on Windows.

Yes, I think. Well, I’m still trying to install Lectrote using npm.

I’ve modified the package.json file with this:

 "scripts": {
    "start": "electron .",
    "preinstall": "run-script-os",
    "preinstall:macos": "if [ -f quixe/LICENSE ]; then echo Quixe already installed; elif [ -d .git ]; then git submodule init; git submodule update; else git clone https://github.com/erkyrath/quixe.git; fi",
    "preinstall:darwin": "if [ -f quixe/LICENSE ]; then echo Quixe already installed; elif [ -d .git ]; then git submodule init; git submodule update; else git clone https://github.com/erkyrath/quixe.git; fi",
	"preinstall:x64": "if EXISTS quixe/LICENSE ( echo Quixe already installed) else (if EXISTS .git ( git submodule init; git submodule update; ) else (git clone https://github.com/erkyrath/quixe.git; fi)",
    "preinstall:win32": "if EXISTS quixe/LICENSE ( echo Quixe already installed) else (if EXISTS .git ( git submodule init; git submodule update; ) else (git clone https://github.com/erkyrath/quixe.git; fi)"
  },

But I get the error:


npm ERR! Failed at the lectrote@1.2.9 preinstall script 'run-script-os'.

But finally managed to compile the thing using the command “npm install darwin x64 win32”.

Now, let’s try to package the game.

Nah, it is of no use. I’ve made all changes in the scripts you mentioned in this thread. The main problem I’m getting is that dist directory keeps getting empty. npm install runs without problems, but fails to fill dist with the proper files, and so, the packager fails.

I will soon be trying the techniques David has shared here to bundle a PC Lectrote app on a PC. I’m just warming up the topic, since the odds are I’ll run into hurdles and be wanting to ask folks about them.

It’s been bizarre circumstances getting to this point. I had a 2010 Mac that was old enough to run Wine and therefore could make the PC version of the game on a Mac. Separately, I had a 2013 Mac laptop that was too new to run Wine, but was able to build the Mac versions.

Now, after the death of my 2010 Mac, I have a new iMac, running Monterey, that is too new for Wine, but can make all the other Lectrote versions. And I’m turning to a PC laptop to try to build for the PC.

-Wade

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