1776 lines
56 KiB
Python
1776 lines
56 KiB
Python
# Copyright 2004-2022 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation files
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
# This file contains code that manages the distribution of Ren'Py games
|
|
# and Ren'Py proper.
|
|
#
|
|
# In this module, all files and paths are stored in unicode. Full paths
|
|
# might include windows path separators (\), but archive paths and names we
|
|
# deal with/match against use the unix separator (/).
|
|
|
|
init python in distribute:
|
|
|
|
from store import config, persistent
|
|
import store.project as project
|
|
import store.interface as interface
|
|
import store.archiver as archiver
|
|
import store.updater as updater
|
|
import store as store
|
|
|
|
from change_icon import change_icons
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import subprocess
|
|
import hashlib
|
|
import struct
|
|
import collections
|
|
import os
|
|
import io
|
|
import re
|
|
import plistlib
|
|
import time
|
|
import shutil
|
|
|
|
def py(s):
|
|
"""
|
|
Formats a string with information about the python version.
|
|
"""
|
|
|
|
return s.format(
|
|
major=sys.version_info.major,
|
|
minor=sys.version_info.minor,
|
|
)
|
|
|
|
# Going from 7.4 to 7.5 or 8.0, the library directory changed.
|
|
RENPY_PATCH = py("""\
|
|
def change_renpy_executable():
|
|
import sys, os, renpy, site
|
|
|
|
if hasattr(site, "RENPY_PLATFORM") and hasattr(sys, "renpy_executable") and (renpy.linux or renpy.windows):
|
|
sys.renpy_executable = os.path.join(renpy.config.renpy_base, "lib", "py{major}-" + site.RENPY_PLATFORM, os.path.basename(sys.renpy_executable))
|
|
|
|
change_renpy_executable()
|
|
""")
|
|
|
|
match_cache = { }
|
|
|
|
def compile_match(pattern):
|
|
"""
|
|
Compiles a pattern for use with match.
|
|
"""
|
|
|
|
regexp = ""
|
|
|
|
while pattern:
|
|
if pattern.startswith("**"):
|
|
regexp += r'.*'
|
|
pattern = pattern[2:]
|
|
elif pattern[0] == "*":
|
|
regexp += r'[^/]*/?'
|
|
pattern = pattern[1:]
|
|
elif pattern[0] == '[':
|
|
regexp += r'['
|
|
pattern = pattern[1:]
|
|
|
|
while pattern and pattern[0] != ']':
|
|
regexp += pattern[0]
|
|
pattern = pattern[1:]
|
|
|
|
pattern = pattern[1:]
|
|
regexp += ']'
|
|
|
|
else:
|
|
regexp += re.escape(pattern[0])
|
|
pattern = pattern[1:]
|
|
|
|
regexp += "$"
|
|
|
|
return re.compile(regexp, re.I)
|
|
|
|
def match(s, pattern):
|
|
"""
|
|
Matches a glob-style pattern against s. Returns True if it matches,
|
|
and False otherwise.
|
|
|
|
** matches every character.
|
|
* matches every character but /.
|
|
[abc] matches a, b, or c.
|
|
|
|
Things are matched case-insensitively.
|
|
"""
|
|
|
|
regexp = match_cache.get(pattern, None)
|
|
if regexp is None:
|
|
regexp = compile_match(pattern)
|
|
match_cache[pattern] = regexp
|
|
|
|
if regexp.match(s):
|
|
return True
|
|
|
|
if regexp.match("/" + s):
|
|
return True
|
|
|
|
return False
|
|
|
|
def hash_file(fn):
|
|
"""
|
|
Returns the hash of `fn`.
|
|
"""
|
|
|
|
sha = hashlib.sha256()
|
|
|
|
with open(renpy.fsencode(fn), "rb") as f:
|
|
while True:
|
|
|
|
data = f.read(8 * 1024 * 1024)
|
|
|
|
if not data:
|
|
break
|
|
|
|
sha.update(data)
|
|
|
|
return sha.hexdigest()
|
|
|
|
class File(object):
|
|
"""
|
|
Represents a file that we can distribute.
|
|
|
|
self.name
|
|
The name of the file as it will be stored in the archives.
|
|
|
|
self.path
|
|
The path to the file on disk. None if it won't be stored
|
|
on disk.
|
|
|
|
self.directory
|
|
True if this is a directory.
|
|
|
|
self.executable
|
|
True if this is an executable that should be distributed
|
|
with the xbit set.
|
|
"""
|
|
|
|
def __init__(self, name, path, directory, executable):
|
|
self.name = name
|
|
self.path = path
|
|
self.directory = directory
|
|
self.executable = executable
|
|
|
|
def __repr__(self):
|
|
if self.directory:
|
|
extra = "dir"
|
|
elif self.executable:
|
|
extra = "x-bit"
|
|
else:
|
|
extra = ""
|
|
|
|
return "<File {!r} {!r} {}>".format(self.name, self.path, extra)
|
|
|
|
def copy(self):
|
|
return File(self.name, self.path, self.directory, self.executable)
|
|
|
|
def hash(self, hash, distributor):
|
|
"""
|
|
Update hash with information about this entry.
|
|
"""
|
|
|
|
key = (self.name, self.directory, self.executable)
|
|
|
|
hash.update(repr(key).encode("utf-8"))
|
|
|
|
if self.path is None:
|
|
return
|
|
|
|
if self.directory:
|
|
return
|
|
|
|
if self.name == "update/current.json":
|
|
return
|
|
|
|
if self.path in distributor.hash_cache:
|
|
digest = distributor.hash_cache[self.path]
|
|
else:
|
|
digest = hash_file(self.path)
|
|
|
|
hash.update(digest.encode("utf-8"))
|
|
|
|
def reprefix(self, old, new):
|
|
rv = self.copy()
|
|
|
|
if self.name.startswith(old):
|
|
rv.name = new + self.name[len(old):]
|
|
|
|
return rv
|
|
|
|
class FileList(list):
|
|
"""
|
|
This represents a list of files that we know about.
|
|
"""
|
|
|
|
def sort(self):
|
|
list.sort(self, key=lambda a : a.name)
|
|
|
|
def copy(self):
|
|
"""
|
|
Makes a deep copy of this file list.
|
|
"""
|
|
|
|
rv = FileList()
|
|
|
|
for i in self:
|
|
rv.append(i.copy())
|
|
|
|
return rv
|
|
|
|
def filter_empty(self):
|
|
"""
|
|
Makes a deep copy of this file list with empty directories
|
|
omitted.
|
|
"""
|
|
|
|
rv = FileList()
|
|
|
|
needed_dirs = set()
|
|
|
|
for i in reversed(self):
|
|
|
|
if (not i.directory) or (i.name in needed_dirs):
|
|
rv.insert(0, i.copy())
|
|
|
|
directory, _sep, _filename = i.name.rpartition("/")
|
|
needed_dirs.add(directory)
|
|
|
|
return rv
|
|
|
|
def add_missing_directories(self):
|
|
"""
|
|
Adds to this file list all directories that are needed by other
|
|
entries in this file list.
|
|
"""
|
|
|
|
rv = self.copy()
|
|
|
|
seen = set()
|
|
required = set()
|
|
|
|
for i in self:
|
|
seen.add(i.name)
|
|
|
|
name = i.name
|
|
|
|
while "/" in name:
|
|
name = name.rpartition("/")[0]
|
|
required.add(name)
|
|
|
|
for name in required - seen:
|
|
rv.append(File(name, None, True, False))
|
|
|
|
rv.sort()
|
|
|
|
return rv
|
|
|
|
|
|
@staticmethod
|
|
def merge(l):
|
|
"""
|
|
Merges a list of file lists into a single file list with no
|
|
duplicate entries.
|
|
"""
|
|
|
|
rv = FileList()
|
|
|
|
seen = set()
|
|
|
|
for fl in l:
|
|
for f in fl:
|
|
if f.name in seen:
|
|
continue
|
|
|
|
rv.append(f)
|
|
seen.add(f.name)
|
|
|
|
return rv
|
|
|
|
def prepend_directory(self, directory):
|
|
"""
|
|
Modifies this file list such that every file in it has `directory`
|
|
prepended.
|
|
"""
|
|
|
|
for i in self:
|
|
i.name = directory + "/" + i.name
|
|
|
|
self.insert(0, File(directory, None, True, False))
|
|
|
|
|
|
def mac_transform(self, app, documentation):
|
|
"""
|
|
Creates a new file list that has the mac transform applied to it.
|
|
|
|
The mac transform places all files that aren't already in <app> in
|
|
<app>/Contents/Resources/autorun. If it matches one of the documentation
|
|
patterns, then it appears both inside and outside of the app.
|
|
"""
|
|
|
|
rv = FileList()
|
|
|
|
for f in self:
|
|
|
|
# Already in the app.
|
|
if f.name == app or f.name.startswith(app + "/"):
|
|
rv.append(f)
|
|
continue
|
|
|
|
# If it's documentation, keep the file. (But also make
|
|
# a copy.)
|
|
for pattern in documentation:
|
|
if match(f.name, pattern):
|
|
rv.append(f)
|
|
|
|
if match("/" + f.name, pattern):
|
|
rv.append(f)
|
|
|
|
# Make a copy.
|
|
f = f.copy()
|
|
|
|
f.name = app + "/Contents/Resources/autorun/" + f.name
|
|
rv.append(f)
|
|
|
|
rv.append(File(app + "/Contents/Resources/autorun", None, True, False))
|
|
rv.sort()
|
|
|
|
return rv
|
|
|
|
def mac_lib_transform(self, app, duplicate):
|
|
"""
|
|
Creates a new file list that has lib/darwin-x86_64 and lib/pythonlib2.7
|
|
copied into the mac app, the latter iff it's not duplicated elsewhere or
|
|
duplicate is set.
|
|
"""
|
|
|
|
for f in list(self):
|
|
|
|
if f.name.startswith("lib/python") and (not duplicate):
|
|
name = app + "/Contents/Resources/" + f.name
|
|
|
|
elif f.name.startswith(py("lib/py{major}-mac-x86_64")):
|
|
name = app + "/Contents/MacOS/" + f.name[19:]
|
|
|
|
else:
|
|
continue
|
|
|
|
new = f.copy()
|
|
new.name = name
|
|
self.append(new)
|
|
|
|
if not duplicate:
|
|
self.remove(f)
|
|
|
|
self.sort()
|
|
|
|
|
|
def hash(self, distributor):
|
|
"""
|
|
Returns a hex digest representing this file list.
|
|
"""
|
|
|
|
sha = hashlib.sha256()
|
|
|
|
for f in sorted(self, key=lambda a : a.name):
|
|
f.hash(sha, distributor)
|
|
|
|
return sha.hexdigest()
|
|
|
|
def split_by_prefix(self, prefix):
|
|
"""
|
|
Returns two filelists, one that contains all the files starting with prefix,
|
|
and one tht contains all other files.
|
|
"""
|
|
|
|
yes = FileList()
|
|
no = FileList()
|
|
|
|
for f in self:
|
|
if f.name.startswith(prefix):
|
|
yes.append(f)
|
|
else:
|
|
no.append(f)
|
|
|
|
return yes, no
|
|
|
|
def reprefix(self, old, new):
|
|
"""
|
|
Returns a new file list with all the paths reprefixed.
|
|
"""
|
|
|
|
rv = FileList()
|
|
|
|
for f in self:
|
|
rv.append(f.reprefix(old, new))
|
|
|
|
return rv
|
|
|
|
|
|
class Distributor(object):
|
|
"""
|
|
This manages the process of building distributions.
|
|
"""
|
|
|
|
def __init__(self, project, destination=None, reporter=None, packages=None, build_update=True, open_directory=False, noarchive=False, packagedest=None, report_success=True, scan=True, macapp=None, force_format=None):
|
|
"""
|
|
Distributes `project`.
|
|
|
|
`destination`
|
|
The destination in which the distribution will be placed. If None,
|
|
uses a default location.
|
|
|
|
`reporter`
|
|
An object that's used to report status and progress to the user.
|
|
|
|
`packages`
|
|
If not None, a list of packages to distributed. If None, all
|
|
packages are distributed.
|
|
|
|
`build_update`
|
|
Will updates be built?
|
|
|
|
`open_directory`
|
|
If true, the directory containing the built files will be opened
|
|
if the build succeeds.
|
|
|
|
`noarchive`
|
|
If true, files will not be placed into archives.
|
|
|
|
`packagedest`
|
|
If given, gives the full path to the single package (without any
|
|
extensions).
|
|
|
|
`report_success`
|
|
If true, we report that the build succeeded.
|
|
|
|
`macapp`
|
|
If given, the path to a macapp that's used instead of
|
|
the macapp that's included with Ren'Py.
|
|
|
|
`force_format`
|
|
If given, forces the format of the distribution to be this.
|
|
"""
|
|
|
|
# A map from a package to a unique update version hash.
|
|
self.update_versions = { }
|
|
|
|
# Map from destination file with extension to (that file's hash,
|
|
# hash of the file list)
|
|
self.build_cache = { }
|
|
|
|
# A map from file to its hash.
|
|
self.hash_cache = { }
|
|
|
|
# A map from a list of file lists and formats to a single integrated
|
|
# file list with transforms applied.
|
|
self.file_list_cache = { }
|
|
|
|
# Status reporter.
|
|
self.reporter = reporter
|
|
|
|
if packagedest is not None:
|
|
if packages is None or len(packages) != 1:
|
|
raise Exception("Packagedest requires a single package be given.")
|
|
|
|
# Safety - prevents us from releasing a launcher that won't update.
|
|
if store.UPDATE_SIMULATE:
|
|
raise Exception("Cannot build distributions when UPDATE_SIMULATE is True.")
|
|
|
|
# The project we want to distribute.
|
|
self.project = project
|
|
|
|
# Logfile.
|
|
self.log = open(self.temp_filename("distribute.txt"), "w")
|
|
|
|
# The path to the mac app.
|
|
self.macapp = macapp
|
|
|
|
# Start by scanning the project, to get the data and build
|
|
# dictionaries.
|
|
data = project.data
|
|
|
|
if scan:
|
|
self.reporter.info(_("Scanning project files..."))
|
|
project.update_dump(force=True, gui=False, compile=project.data['force_recompile'])
|
|
|
|
if project.data['force_recompile']:
|
|
import compileall
|
|
|
|
compileall.compile_dir(
|
|
os.path.join(config.renpy_base, "renpy"),
|
|
ddir="renpy/",
|
|
force=True,
|
|
quiet=True,
|
|
)
|
|
|
|
if project.dump.get("error", False):
|
|
raise Exception("Could not get build data from the project. Please ensure the project runs.")
|
|
|
|
self.build = build = project.dump['build']
|
|
|
|
# Map from file list name to file list.
|
|
self.file_lists = collections.defaultdict(FileList)
|
|
|
|
self.base_name = build['directory_name']
|
|
self.executable_name = build['executable_name']
|
|
self.pretty_version = build['version']
|
|
|
|
if (" " in self.base_name) or (":" in self.base_name) or (";" in self.base_name):
|
|
reporter.info(_("Building distributions failed:\n\nThe build.directory_name variable may not include the space, colon, or semicolon characters."), pause=True)
|
|
self.log.close()
|
|
return
|
|
|
|
# The destination directory.
|
|
if destination is None:
|
|
destination = build["destination"]
|
|
parent = os.path.dirname(project.path)
|
|
self.destination = os.path.join(parent, destination)
|
|
else:
|
|
self.destination = destination
|
|
|
|
if not packagedest:
|
|
try:
|
|
os.makedirs(self.destination)
|
|
except Exception:
|
|
pass
|
|
|
|
self.load_build_cache()
|
|
|
|
self.packagedest = packagedest
|
|
|
|
self.include_update = build['include_update']
|
|
self.build_update = self.include_update and build_update
|
|
|
|
# The various executables, which change names based on self.executable_name.
|
|
self.app = self.executable_name + ".app"
|
|
self.exe = self.executable_name + ".exe"
|
|
self.exe32 = self.executable_name + "-32.exe"
|
|
self.sh = self.executable_name + ".sh"
|
|
self.py = self.executable_name + ".py"
|
|
|
|
self.documentation_patterns = build['documentation_patterns']
|
|
|
|
build_packages = [ ]
|
|
|
|
for i in build['packages']:
|
|
name = i['name']
|
|
|
|
if packages is None:
|
|
if not i['hidden']:
|
|
build_packages.append(i)
|
|
elif name in packages:
|
|
build_packages.append(i)
|
|
|
|
if not build_packages:
|
|
self.reporter.info(_("No packages are selected, so there's nothing to do."), pause=True)
|
|
self.log.close()
|
|
return
|
|
|
|
self.scan_and_classify(project.path, build["base_patterns"])
|
|
|
|
if noarchive:
|
|
self.ignore_archives(build['archives'])
|
|
else:
|
|
self.archive_files(build["archives"])
|
|
|
|
# Add Ren'Py.
|
|
self.reporter.info(_("Scanning Ren'Py files..."))
|
|
self.scan_and_classify(config.renpy_base, build["renpy_patterns"])
|
|
|
|
if build["_sdk_fonts"]:
|
|
for k in list(self.file_lists.keys()):
|
|
self.file_lists[k] = self.file_lists[k].reprefix("sdk-fonts", "game")
|
|
|
|
# Add Python (with the same name as our executables)
|
|
self.add_python()
|
|
|
|
# Build the mac app and windows exes.
|
|
self.add_mac_files()
|
|
self.add_windows_files()
|
|
self.add_main_py()
|
|
|
|
# Add the main.py.
|
|
self.add_main_py()
|
|
|
|
# Add generated/special files.
|
|
if build['renpy']:
|
|
self.add_renpy_distro_files()
|
|
else:
|
|
self.add_renpy_game_files()
|
|
|
|
# Assign the x-bit as necessary.
|
|
self.mark_executable()
|
|
|
|
# Merge file lists, as needed.
|
|
self.merge_file_lists()
|
|
|
|
# Rename the executable-like files.
|
|
self.rename()
|
|
|
|
# Sign the mac app once on Ren'Py.
|
|
if self.build["renpy"]:
|
|
fl = self.file_lists['binary']
|
|
app, rest = fl.split_by_prefix(self.app)
|
|
if app:
|
|
app = self.sign_app(app, macapp)
|
|
fl = FileList.merge([ app, rest ])
|
|
self.file_lists['binary'] = fl
|
|
else:
|
|
raise Exception("No mac app found.")
|
|
|
|
# The time of the update version.
|
|
self.update_version = int(time.time())
|
|
|
|
for p in build_packages:
|
|
|
|
formats = p["formats"]
|
|
if force_format is not None:
|
|
formats = [ force_format ]
|
|
|
|
for f in formats:
|
|
|
|
self.make_package(
|
|
p["name"],
|
|
f,
|
|
p["file_lists"],
|
|
dlc=p["dlc"])
|
|
|
|
if self.build_update and p["update"]:
|
|
self.make_package(
|
|
p["name"],
|
|
"update",
|
|
p["file_lists"],
|
|
dlc=p["dlc"])
|
|
|
|
wait_parallel_threads()
|
|
|
|
if self.build_update:
|
|
self.finish_updates(build_packages)
|
|
|
|
if not packagedest:
|
|
self.save_build_cache()
|
|
|
|
# Finish up.
|
|
self.log.close()
|
|
|
|
if report_success:
|
|
self.reporter.info(_("All packages have been built.\n\nDue to the presence of permission information, unpacking and repacking the Linux and Macintosh distributions on Windows is not supported."))
|
|
|
|
if open_directory:
|
|
renpy.run(store.OpenDirectory(self.destination, absolute=True))
|
|
|
|
def scan_and_classify(self, directory, patterns):
|
|
"""
|
|
Walks through the `directory`, finds files and directories that
|
|
match the pattern, and assigns them to the appropriate file list.
|
|
|
|
`patterns`
|
|
A list of pattern, file_list tuples. The pattern is a string
|
|
that is matched using match. File_list is either
|
|
a space-separated list of file lists to add the file to,
|
|
or None to ignore it.
|
|
|
|
Directories are matched with a trailing /, but added to the
|
|
file list with the trailing / removed.
|
|
"""
|
|
|
|
def walk(name, path):
|
|
|
|
# Ignore ASCII control characters, like (Icon\r on the mac).
|
|
if re.search('[\x00-\x19]', name):
|
|
return
|
|
|
|
is_dir = os.path.isdir(path)
|
|
|
|
if is_dir:
|
|
match_name = name + "/"
|
|
else:
|
|
match_name = name
|
|
|
|
for pattern, file_list in patterns:
|
|
|
|
if match(match_name, pattern):
|
|
|
|
# When we have ('test/**', None), avoid excluding test.
|
|
if (not file_list) and is_dir:
|
|
new_pattern = pattern.rstrip("*")
|
|
if (pattern != new_pattern) and match(match_name, new_pattern):
|
|
continue
|
|
|
|
break
|
|
|
|
else:
|
|
print(str(match_name), "doesn't match anything.", file=self.log)
|
|
|
|
pattern = None
|
|
file_list = None
|
|
|
|
print(str(match_name), "matches", str(pattern), "(" + str(file_list) + ").", file=self.log)
|
|
|
|
if file_list is None:
|
|
return
|
|
|
|
for fl in file_list:
|
|
f = File(name, path, is_dir, False)
|
|
self.file_lists[fl].append(f)
|
|
|
|
if is_dir:
|
|
|
|
for fn in os.listdir(path):
|
|
walk(
|
|
name + "/" + fn,
|
|
os.path.join(path, fn),
|
|
)
|
|
|
|
for fn in os.listdir(directory):
|
|
walk(fn, os.path.join(directory, fn))
|
|
|
|
def merge_file_lists(self):
|
|
"""
|
|
For each (old, new) in self.build['merge'], merge the old list
|
|
into the new list.
|
|
"""
|
|
|
|
for old, new in self.build['merge']:
|
|
self.file_lists[new] = FileList.merge([
|
|
self.file_lists[old],
|
|
self.file_lists[new]])
|
|
|
|
def rescan(self, oldlist, directory):
|
|
"""
|
|
Scans `directory`, and produces a filelist from it. Returns the
|
|
produced filelist.
|
|
|
|
`oldlist`
|
|
Is a filelist. If a file has the xbit set in the oldlist, it
|
|
has the xbit set in the new list.
|
|
"""
|
|
|
|
executable = set()
|
|
|
|
for f in oldlist:
|
|
if f.executable:
|
|
executable.add(f.name)
|
|
|
|
rv = FileList()
|
|
|
|
def walk(name, path):
|
|
|
|
# Ignore ASCII control characters, like (Icon\r on the mac).
|
|
if re.search('[\x00-\x19]', name):
|
|
return
|
|
|
|
is_dir = os.path.isdir(path)
|
|
|
|
f = File(name, path, is_dir, name in executable)
|
|
rv.append(f)
|
|
|
|
if is_dir:
|
|
|
|
for fn in os.listdir(path):
|
|
walk(
|
|
name + "/" + fn,
|
|
os.path.join(path, fn),
|
|
)
|
|
|
|
for fn in os.listdir(directory):
|
|
walk(fn, os.path.join(directory, fn))
|
|
|
|
return rv
|
|
|
|
def temp_filename(self, name):
|
|
self.project.make_tmp()
|
|
return os.path.join(self.project.tmp, name)
|
|
|
|
def add_file(self, file_list, name, path, executable=False):
|
|
"""
|
|
Adds a file to the file lists.
|
|
|
|
`file_list`
|
|
A space-separated list of file list names.
|
|
|
|
`name`
|
|
The name of the file to be added.
|
|
|
|
`path`
|
|
The path to that file on disk.
|
|
"""
|
|
|
|
if not os.path.exists(path):
|
|
raise Exception("{} does not exist.".format(path))
|
|
|
|
if isinstance(file_list, basestring):
|
|
file_list = file_list.split()
|
|
|
|
f = File(name, path, False, executable)
|
|
|
|
for fl in file_list:
|
|
self.file_lists[fl].append(f)
|
|
|
|
def add_directory(self, file_list, name):
|
|
"""
|
|
Adds an empty directory to the file lists.
|
|
"""
|
|
|
|
if isinstance(file_list, basestring):
|
|
file_list = file_list.split()
|
|
|
|
f = File(name, None, True, False)
|
|
|
|
for fl in file_list:
|
|
self.file_lists[fl].append(f)
|
|
|
|
|
|
def ignore_archives(self, archives):
|
|
"""
|
|
Ignore archiving commands by adding the files that would be in
|
|
archives into packages instead.
|
|
"""
|
|
|
|
for arcname, file_lists in archives:
|
|
if not self.file_lists[arcname]:
|
|
continue
|
|
|
|
for f in self.file_lists[arcname]:
|
|
for fl in file_lists:
|
|
self.file_lists[fl].append(f)
|
|
|
|
def archive_files(self, archives):
|
|
"""
|
|
Add files to archives.
|
|
"""
|
|
|
|
for arcname, file_list in archives:
|
|
|
|
if not self.file_lists[arcname]:
|
|
continue
|
|
|
|
arcfn = arcname + ".rpa"
|
|
arcpath = self.temp_filename(arcfn)
|
|
|
|
af = archiver.Archive(arcpath)
|
|
|
|
fll = len(self.file_lists[arcname])
|
|
|
|
for i, entry in enumerate(self.file_lists[arcname]):
|
|
|
|
if entry.directory:
|
|
continue
|
|
|
|
self.reporter.progress(_("Archiving files..."), i, fll)
|
|
|
|
name = "/".join(entry.name.split("/")[1:])
|
|
af.add(name, entry.path)
|
|
|
|
self.reporter.progress_done()
|
|
|
|
af.close()
|
|
|
|
self.add_file(file_list, "game/" + arcfn, arcpath)
|
|
|
|
def add_renpy_game_files(self):
|
|
"""
|
|
Add Ren'Py file to the game.
|
|
"""
|
|
|
|
LICENSE_TXT = os.path.join(config.renpy_base, "LICENSE.txt")
|
|
|
|
if os.path.exists(LICENSE_TXT):
|
|
self.add_file("renpy", "renpy/LICENSE.txt", LICENSE_TXT)
|
|
|
|
if self.build["script_version"]:
|
|
|
|
if (not os.path.exists(os.path.join(self.project.path, "game", "script_version.rpy"))) and \
|
|
(not os.path.exists(os.path.join(self.project.path, "game", "script_version.rpyc"))):
|
|
|
|
script_version_txt = self.temp_filename("script_version.txt")
|
|
|
|
with open(script_version_txt, "w") as f:
|
|
f.write(unicode(repr(renpy.renpy.version_tuple[:-1])))
|
|
|
|
self.add_file("all", "game/script_version.txt", script_version_txt)
|
|
|
|
def add_file_list_hash(self, list_name):
|
|
"""
|
|
Hashes a file list, then adds that file to the Ren'Py distribution.
|
|
"""
|
|
|
|
tfn = self.temp_filename(list_name + "_hash.txt")
|
|
|
|
with open(tfn, "w") as tf:
|
|
tf.write(self.file_lists[list_name].hash(self))
|
|
|
|
self.add_file("binary", "launcher/game/" + list_name + "_hash.txt", tfn)
|
|
self.add_file(list_name, list_name + "/hash.txt", tfn)
|
|
|
|
def add_renpy_distro_files(self):
|
|
"""
|
|
Add additional files to Ren'Py.
|
|
"""
|
|
|
|
self.add_file_list_hash("rapt")
|
|
self.add_file_list_hash("renios")
|
|
self.add_file_list_hash("web")
|
|
|
|
tmp_fn = self.temp_filename("renpy.py")
|
|
|
|
with open(os.path.join(config.renpy_base, "renpy.py"), "rb") as f:
|
|
data = f.read()
|
|
|
|
with open(tmp_fn, "wb") as f:
|
|
f.write(b"#!/usr/bin/env python3\n")
|
|
f.write(data)
|
|
|
|
self.add_file("source_only", "renpy.py", tmp_fn, True)
|
|
|
|
|
|
def write_plist(self):
|
|
|
|
display_name = self.build['display_name']
|
|
executable_name = self.executable_name
|
|
version = self.build['version']
|
|
|
|
plist = dict(
|
|
CFBundleDevelopmentRegion="English",
|
|
CFBundleDisplayName=display_name,
|
|
CFBundleExecutable=executable_name,
|
|
CFBundleIconFile="icon",
|
|
CFBundleInfoDictionaryVersion="6.0",
|
|
CFBundleName=display_name,
|
|
CFBundlePackageType="APPL",
|
|
CFBundleShortVersionString=version,
|
|
CFBundleVersion=time.strftime("%Y.%m%d.%H%M%S"),
|
|
LSApplicationCategoryType="public.app-category.simulation-games",
|
|
CFBundleDocumentTypes = [
|
|
{
|
|
"CFBundleTypeOSTypes" : [ "****", "fold", "disk" ],
|
|
"CFBundleTypeRole" : "Viewer",
|
|
},
|
|
],
|
|
UTExportedTypeDeclarations = [
|
|
{
|
|
"UTTypeConformsTo" : [ "public.python-script" ],
|
|
"UTTypeDescription" : "Ren'Py Script",
|
|
"UTTypeIdentifier" : "org.renpy.rpy",
|
|
"UTTypeTagSpecification" : { "public.filename-extension" : [ "rpy" ] }
|
|
},
|
|
],
|
|
NSHighResolutionCapable=True,
|
|
)
|
|
|
|
if self.build.get('allow_integrated_gpu', False):
|
|
plist["NSSupportsAutomaticGraphicsSwitching"] = True
|
|
|
|
plist.update(self.build.get("mac_info_plist", { }))
|
|
|
|
rv = self.temp_filename("Info.plist")
|
|
|
|
if PY2:
|
|
plistlib.writePlist(plist, rv)
|
|
else:
|
|
with open(rv, "wb") as f:
|
|
plistlib.dump(plist, f)
|
|
|
|
return rv
|
|
|
|
def add_python(self):
|
|
|
|
if self.build['renpy']:
|
|
windows = 'binary'
|
|
linux = 'binary'
|
|
linux_i686 = 'binary'
|
|
mac = 'binary'
|
|
raspi = 'linux_arm'
|
|
else:
|
|
windows = 'windows'
|
|
linux = 'linux'
|
|
linux_i686 = 'linux_i686'
|
|
mac = 'mac'
|
|
raspi = 'linux_arm'
|
|
|
|
prefix = py("lib/py{major}-")
|
|
|
|
if os.path.exists(linux_i686):
|
|
|
|
self.add_file(
|
|
linux_i686,
|
|
prefix + "linux-i686/" + self.executable_name,
|
|
os.path.join(config.renpy_base, prefix + "linux-i686/renpy"),
|
|
True)
|
|
|
|
self.add_file(
|
|
linux,
|
|
prefix + "linux-x86_64/" + self.executable_name,
|
|
os.path.join(config.renpy_base, prefix + "linux-x86_64/renpy"),
|
|
True)
|
|
|
|
armfn = os.path.join(config.renpy_base, prefix + "linux-armv7l/renpy")
|
|
|
|
if os.path.exists(armfn):
|
|
|
|
self.add_file(
|
|
raspi,
|
|
prefix + "linux-armv7l/" + self.executable_name,
|
|
armfn,
|
|
True)
|
|
|
|
aarch64fn = os.path.join(config.renpy_base, prefix + "linux-aarch64/renpy")
|
|
|
|
if os.path.exists(aarch64fn):
|
|
|
|
self.add_file(
|
|
raspi,
|
|
prefix + "linux-aarch64/" + self.executable_name,
|
|
aarch64fn,
|
|
True)
|
|
|
|
self.add_file(
|
|
mac,
|
|
prefix + "mac-x86_64/" + self.executable_name,
|
|
os.path.join(config.renpy_base, prefix + "mac-x86_64/renpy"),
|
|
True)
|
|
|
|
def add_mac_files(self):
|
|
"""
|
|
Add mac-specific files to the distro.
|
|
"""
|
|
|
|
if self.build['renpy']:
|
|
filelist = "binary"
|
|
else:
|
|
filelist = "mac"
|
|
|
|
contents = self.app + "/Contents"
|
|
|
|
self.add_directory(filelist, self.app)
|
|
self.add_directory(filelist, contents)
|
|
self.add_directory(filelist, contents + "/MacOS")
|
|
|
|
plist_fn = self.write_plist()
|
|
self.add_file(filelist, contents + "/Info.plist", plist_fn)
|
|
|
|
self.add_file(filelist,
|
|
contents + "/MacOS/" + self.executable_name,
|
|
os.path.join(config.renpy_base, py("lib/py{major}-mac-x86_64/renpy")))
|
|
|
|
|
|
custom_fn = os.path.join(self.project.path, "icon.icns")
|
|
default_fn = os.path.join(config.renpy_base, "launcher/icon.icns")
|
|
|
|
if os.path.exists(custom_fn):
|
|
icon_fn = custom_fn
|
|
else:
|
|
icon_fn = default_fn
|
|
|
|
resources = contents + "/Resources"
|
|
|
|
self.add_directory(filelist, resources)
|
|
self.add_file(filelist, resources + "/icon.icns", icon_fn)
|
|
|
|
if not self.build['renpy']:
|
|
self.add_directory(filelist, contents + "/MacOS/lib")
|
|
self.add_directory(filelist, contents + py("/MacOS/lib/py{major}-mac-x86_64"))
|
|
self.add_directory(filelist, contents + py("/Resources/lib/python{major}.{minor}"))
|
|
|
|
self.file_lists[filelist].mac_lib_transform(self.app, self.build['renpy'])
|
|
|
|
def add_windows_files(self):
|
|
"""
|
|
Adds windows-specific files.
|
|
"""
|
|
|
|
if self.build['renpy']:
|
|
windows = 'binary'
|
|
windows_i686 = 'binary'
|
|
else:
|
|
windows = 'windows'
|
|
windows_i686 = 'windows_i686'
|
|
|
|
|
|
icon_fn = os.path.join(self.project.path, "icon.ico")
|
|
|
|
|
|
def write_exe(src, dst, tmp, fl):
|
|
"""
|
|
Write the exe found at `src` (taken as relative to renpy-base)
|
|
as `dst` (in the distribution). `tmp` is the name of a tempfile
|
|
that is written if one is needed.
|
|
"""
|
|
|
|
if fl == "windows_i686":
|
|
should_change_icon = self.build["change_icon_i686"]
|
|
else:
|
|
should_change_icon = True
|
|
|
|
src = os.path.join(config.renpy_base, src)
|
|
tmp = self.temp_filename(tmp)
|
|
|
|
if should_change_icon and os.path.exists(icon_fn) and os.path.exists(src):
|
|
|
|
with open(tmp, "wb") as f:
|
|
f.write(change_icons(src, icon_fn))
|
|
|
|
else:
|
|
tmp = src
|
|
|
|
if os.path.exists(tmp):
|
|
self.add_file(fl, dst, tmp)
|
|
|
|
if PY2:
|
|
|
|
if self.build["include_i686"]:
|
|
write_exe("lib/py2-windows-i686/renpy.exe", self.exe32, self.exe32, windows_i686)
|
|
write_exe("lib/py2-windows-i686/pythonw.exe", "lib/py2-windows-i686/pythonw.exe", "pythonw-32.exe", windows_i686)
|
|
|
|
write_exe("lib/py2-windows-x86_64/renpy.exe", self.exe, self.exe, windows)
|
|
write_exe("lib/py2-windows-x86_64/pythonw.exe", "lib/py2-windows-x86_64/pythonw.exe", "pythonw-64.exe", windows)
|
|
|
|
else:
|
|
|
|
write_exe("lib/py3-windows-x86_64/renpy.exe", self.exe, self.exe, windows)
|
|
write_exe("lib/py3-windows-x86_64/pythonw.exe", "lib/py3-windows-x86_64/pythonw.exe", "pythonw-64.exe", windows)
|
|
|
|
|
|
def add_main_py(self):
|
|
if self.build['renpy']:
|
|
return
|
|
|
|
self.add_file("web", "main.py", os.path.join(config.renpy_base, "renpy.py"))
|
|
|
|
def mark_executable(self):
|
|
"""
|
|
Marks files as executable.
|
|
"""
|
|
|
|
for l in self.file_lists.values():
|
|
for f in l:
|
|
for pat in self.build['xbit_patterns']:
|
|
if match(f.name, pat):
|
|
f.executable = True
|
|
|
|
if match("/" + f.name, pat):
|
|
f.executable = True
|
|
|
|
def rename(self):
|
|
"""
|
|
Rename files in all lists to match the executable names.
|
|
"""
|
|
|
|
major_sh = py("renpy{major}.sh")
|
|
|
|
def rename_one(fn):
|
|
parts = fn.split('/')
|
|
p = parts[0]
|
|
|
|
if p == major_sh:
|
|
p = self.sh
|
|
elif p == "renpy.sh":
|
|
p = self.sh
|
|
elif p == "renpy.py":
|
|
p = self.py
|
|
|
|
parts[0] = p
|
|
return "/".join(parts)
|
|
|
|
for l in self.file_lists.values():
|
|
for f in l:
|
|
f.name = rename_one(f.name)
|
|
|
|
def run(self, message, command, **kwargs):
|
|
"""
|
|
Runs a command.
|
|
"""
|
|
|
|
self.reporter.info(message)
|
|
|
|
cmd = [ renpy.fsencode(i.format(**kwargs)) for i in command ]
|
|
|
|
# print("\"" + "\" \"".join(cmd) + "\"")
|
|
|
|
try:
|
|
import sys, os
|
|
isatty = os.isatty(sys.stdin.fileno())
|
|
except Exception:
|
|
isatty = False
|
|
|
|
if isatty:
|
|
subprocess.check_call(cmd)
|
|
else:
|
|
subprocess.check_call(cmd, stdout=self.log, stderr=subprocess.STDOUT)
|
|
|
|
|
|
def sign_app(self, fl, appzip):
|
|
"""
|
|
Signs the mac app contained in appzip.
|
|
"""
|
|
|
|
if self.macapp:
|
|
return self.rescan(fl, self.macapp)
|
|
|
|
identity = self.build.get('mac_identity', None)
|
|
|
|
if identity is None:
|
|
return fl
|
|
|
|
# Figure out where it goes.
|
|
if appzip:
|
|
dn = "sign.app-standalone"
|
|
else:
|
|
dn = "sign.app-crossplatform"
|
|
|
|
dn = self.temp_filename(dn)
|
|
|
|
if os.path.exists(dn):
|
|
shutil.rmtree(dn)
|
|
|
|
# Unpack the app.
|
|
pkg = DirectoryPackage(dn)
|
|
|
|
for i, f in enumerate(fl):
|
|
self.reporter.progress(_("Unpacking the Macintosh application for signing..."), i, len(fl))
|
|
|
|
if f.directory:
|
|
pkg.add_directory(f.name, f.path)
|
|
else:
|
|
pkg.add_file(f.name, f.path, f.executable)
|
|
|
|
pkg.close()
|
|
|
|
# Sign the mac app.
|
|
self.run(
|
|
_("Signing the Macintosh application...\n(This may take a long time.)"),
|
|
self.build["mac_codesign_command"],
|
|
identity=identity,
|
|
app=os.path.join(dn, self.app),
|
|
entitlements=os.path.join(config.gamedir, "entitlements.plist"),
|
|
)
|
|
|
|
# Rescan the signed app.
|
|
fl = self.rescan(fl, dn)
|
|
|
|
return fl
|
|
|
|
def make_dmg(self, volname, sourcedir, dmg):
|
|
"""
|
|
Packages `sourcedir` as a dmg.
|
|
"""
|
|
|
|
identity = self.build.get('mac_identity', None)
|
|
|
|
if identity is None:
|
|
identity = ''
|
|
|
|
self.run(
|
|
_("Creating the Macintosh DMG..."),
|
|
self.build["mac_create_dmg_command"],
|
|
identity=identity,
|
|
volname=volname,
|
|
sourcedir=sourcedir,
|
|
dmg=dmg,
|
|
)
|
|
|
|
if self.build.get("mac_codesign_dmg_command", None):
|
|
|
|
self.run(
|
|
_("Signing the Macintosh DMG..."),
|
|
self.build["mac_codesign_dmg_command"],
|
|
identity=identity,
|
|
volname=volname,
|
|
sourcedir=sourcedir,
|
|
dmg=dmg,
|
|
)
|
|
|
|
def workaround_mac_notarization(self, fl):
|
|
"""
|
|
This works around mac notarization by compressing the unsigned,
|
|
un-notarized, binaries in lib/py3-mac-x86_64.
|
|
"""
|
|
|
|
fl = fl.copy()
|
|
|
|
for f in fl:
|
|
if py("/lib/py{major}-mac-x86_64/") in f.name:
|
|
with open(f.path, "rb") as inf:
|
|
data = inf.read()
|
|
|
|
tempfile = self.temp_filename(os.path.basename(f.name) + ".macho")
|
|
|
|
with open(tempfile, "wb") as outf:
|
|
outf.write(b"RENPY" + data)
|
|
|
|
f.name += ".macho"
|
|
f.path = tempfile
|
|
|
|
return fl
|
|
|
|
def prepare_file_list(self, format, file_lists):
|
|
"""
|
|
Prepares a master list of files, given the format and file lists.
|
|
This also takes care of the mac transforms, and signing the app
|
|
if necessary.
|
|
"""
|
|
|
|
macapp = (format in { "app-zip", "app-directory", "app-dmg" })
|
|
key = (macapp, tuple(file_lists))
|
|
|
|
if key in self.file_list_cache:
|
|
return self.file_list_cache[key].copy()
|
|
|
|
fl = FileList.merge([ self.file_lists[i] for i in file_lists ])
|
|
fl = fl.copy()
|
|
fl.sort()
|
|
|
|
if self.build.get("exclude_empty_directories", True):
|
|
fl = fl.filter_empty()
|
|
|
|
fl = fl.add_missing_directories()
|
|
|
|
if macapp:
|
|
fl = fl.mac_transform(self.app, self.documentation_patterns)
|
|
|
|
if not self.build["renpy"]:
|
|
|
|
app, rest = fl.split_by_prefix(self.app)
|
|
|
|
if app:
|
|
app = self.sign_app(app, macapp)
|
|
|
|
fl = FileList.merge([ app, rest ])
|
|
|
|
self.file_list_cache[key] = fl
|
|
return fl.copy()
|
|
|
|
def make_package(self, variant, format, file_lists, dlc=False):
|
|
"""
|
|
Creates a package file in the projects directory.
|
|
|
|
`variant`
|
|
The name of the variant to package. This is appended to the base name to become
|
|
part of the file and directory names.
|
|
|
|
`format`
|
|
The format things will be packaged in. See the table of formats below.
|
|
`file_lists`
|
|
A string containing a space-separated list of file_lists to include in this
|
|
package.
|
|
|
|
`dlc`
|
|
True if we want to build a non-update file in DLC mode.
|
|
"""
|
|
filename = self.base_name + "-" + variant
|
|
path = os.path.join(self.destination, filename)
|
|
|
|
|
|
# A map from the name of the format, to the options that will be
|
|
# used with it. The fields are:
|
|
#
|
|
# - The extension used.
|
|
# - Is this a directory based format?
|
|
# - Should the directory be turned into a dmg?
|
|
# - Should a directory name be prepended?
|
|
|
|
FORMATS = {
|
|
"update" : (".update", False, False, False),
|
|
|
|
"tar.bz2" : (".tar.bz2", False, False, True),
|
|
"zip" : (".zip", False, False, True),
|
|
"directory" : ("", True, False, False),
|
|
"dmg" : ("-dmg", True, True, True),
|
|
|
|
"app-zip" : (".zip", False, False, False),
|
|
"app-directory" : ("-app", True, False, False),
|
|
"app-dmg" : ("-app-dmg", True, True, False),
|
|
|
|
"bare-tar.bz2" : (".tar.bz2", False, False, False),
|
|
"bare-zip" : (".zip", False, False, False),
|
|
}
|
|
|
|
if format not in FORMATS:
|
|
raise Exception("Format %r is unknown." % format)
|
|
|
|
ext, directory, dmg, prepend = FORMATS[format]
|
|
|
|
mac_identity = self.build.get('mac_identity', None)
|
|
|
|
if dmg and (mac_identity is None):
|
|
return
|
|
|
|
if self.packagedest:
|
|
path = self.packagedest
|
|
|
|
fl = self.prepare_file_list(format, file_lists)
|
|
|
|
# Write the update information.
|
|
update_files = [ ]
|
|
update_xbit = [ ]
|
|
update_directories = [ ]
|
|
|
|
for i in fl:
|
|
if not i.directory:
|
|
update_files.append(i.name)
|
|
else:
|
|
update_directories.append(i.name)
|
|
|
|
if i.executable:
|
|
update_xbit.append(i.name)
|
|
|
|
self.update_versions[variant] = fl.hash(self)
|
|
|
|
update = { variant : { "version" : self.update_versions[variant], "base_name" : self.base_name, "files" : update_files, "directories" : update_directories, "xbit" : update_xbit } }
|
|
|
|
update_fn = os.path.join(self.destination, filename + ".update.json")
|
|
|
|
if self.include_update and (variant not in [ 'ios', 'android', 'source']) and (not format.startswith("app-")):
|
|
|
|
with open(update_fn, "w") as f:
|
|
json.dump(update, f, indent=2)
|
|
|
|
if (not dlc) or (format == "update"):
|
|
fl.append(File("update", None, True, False))
|
|
fl.append(File("update/current.json", update_fn, False, False))
|
|
|
|
# If we're not an update file, prepend the directory.
|
|
if (not dlc) and prepend:
|
|
fl.prepend_directory(filename)
|
|
|
|
# The path to the DMG, if we're going to make one.
|
|
dmg_path = path + ".dmg"
|
|
|
|
full_filename = filename + ext
|
|
path += ext
|
|
|
|
if self.build['renpy']:
|
|
fl_hash = fl.hash(self)
|
|
else:
|
|
fl_hash = '<not building renpy>'
|
|
|
|
file_hash, old_fl_hash = self.build_cache.get(full_filename, ("", ""))
|
|
|
|
if (not directory) and (old_fl_hash == fl_hash) and not(self.build['renpy'] and (variant == "sdk")):
|
|
|
|
if file_hash:
|
|
self.build_cache[full_filename] = (file_hash, fl_hash)
|
|
|
|
return
|
|
|
|
def done():
|
|
"""
|
|
This is called when the build of the package is done, either
|
|
in this thread or a background thread.
|
|
"""
|
|
|
|
if self.include_update and not self.build_update and not dlc:
|
|
if os.path.exists(update_fn):
|
|
os.unlink(update_fn)
|
|
|
|
if not directory:
|
|
file_hash = hash_file(path)
|
|
else:
|
|
file_hash = ""
|
|
|
|
if file_hash:
|
|
self.build_cache[full_filename] = (file_hash, fl_hash)
|
|
|
|
if format == "tar.bz2" or format == "bare-tar.bz2":
|
|
pkg = TarPackage(path, "w:bz2")
|
|
elif format == "update":
|
|
pkg = UpdatePackage(path, filename, self.destination)
|
|
elif format == "zip" or format == "app-zip" or format == "bare-zip":
|
|
if self.build['renpy']:
|
|
pkg = ExternalZipPackage(path)
|
|
else:
|
|
pkg = ZipPackage(path)
|
|
elif dmg:
|
|
|
|
def make_dmg():
|
|
self.make_dmg(filename, path, dmg_path)
|
|
shutil.rmtree(path)
|
|
|
|
pkg = DMGPackage(path, make_dmg)
|
|
|
|
fl = self.workaround_mac_notarization(fl)
|
|
|
|
elif directory:
|
|
pkg = DirectoryPackage(path)
|
|
|
|
# If we want to build in parallel.
|
|
if self.build['renpy']:
|
|
pkg = ParallelPackage(pkg, done, variant + "." + format)
|
|
done = None
|
|
|
|
for i, f in enumerate(fl):
|
|
self.reporter.progress(_("Writing the [variant] [format] package."), i, len(fl), variant=variant, format=format)
|
|
|
|
if f.directory:
|
|
pkg.add_directory(f.name, f.path)
|
|
else:
|
|
pkg.add_file(f.name, f.path, f.executable)
|
|
|
|
self.reporter.progress_done()
|
|
|
|
|
|
if format == "update":
|
|
# Build the zsync file.
|
|
|
|
self.reporter.info(_("Making the [variant] update zsync file."), variant=variant)
|
|
|
|
pkg.close()
|
|
|
|
if done is not None:
|
|
done()
|
|
|
|
|
|
def finish_updates(self, packages):
|
|
"""
|
|
Indexes the updates, then removes the .update files.
|
|
"""
|
|
|
|
if not self.build_update:
|
|
return
|
|
|
|
index = { }
|
|
|
|
if self.build['renpy']:
|
|
index["monkeypatch"] = RENPY_PATCH
|
|
|
|
def add_variant(variant):
|
|
|
|
digest = self.build_cache[self.base_name + "-" + variant + ".update"][0]
|
|
|
|
sums_size = os.path.getsize(self.destination + "/" + self.base_name + "-" + variant + ".sums")
|
|
|
|
index[variant] = {
|
|
"version" : self.update_versions[variant],
|
|
"pretty_version" : self.pretty_version,
|
|
"digest" : digest,
|
|
"zsync_url" : self.base_name + "-" + variant + ".zsync",
|
|
"sums_url" : self.base_name + "-" + variant + ".sums",
|
|
"sums_size" : sums_size,
|
|
"json_url" : self.base_name + "-" + variant + ".update.json",
|
|
}
|
|
|
|
fn = renpy.fsencode(os.path.join(self.destination, self.base_name + "-" + variant + ".update"))
|
|
|
|
if os.path.exists(fn):
|
|
os.unlink(fn)
|
|
|
|
for p in packages:
|
|
if p["update"]:
|
|
add_variant(p["name"])
|
|
|
|
fn = renpy.fsencode(os.path.join(self.destination, "updates.json"))
|
|
with open(fn, "w") as f:
|
|
json.dump(index, f, indent=2)
|
|
|
|
|
|
def save_build_cache(self):
|
|
if not self.build['renpy']:
|
|
return
|
|
|
|
fn = renpy.fsencode(os.path.join(self.destination, ".build_cache"))
|
|
|
|
with open(fn, "w", encoding="utf-8") as f:
|
|
for k, v in self.build_cache.items():
|
|
l = "\t".join([k, v[0], v[1]]) + "\n"
|
|
f.write(l)
|
|
|
|
def load_build_cache(self):
|
|
if not self.build['renpy']:
|
|
return
|
|
|
|
fn = renpy.fsencode(os.path.join(self.destination, ".build_cache"))
|
|
|
|
if not os.path.exists(fn):
|
|
return
|
|
|
|
with open(fn, "rb") as f:
|
|
for l in f:
|
|
if not l:
|
|
continue
|
|
|
|
l = l.decode("utf-8").rstrip()
|
|
l = l.split("\t")
|
|
|
|
self.build_cache[l[0]] = (l[1], l[2])
|
|
|
|
os.unlink(fn)
|
|
|
|
def dump(self):
|
|
for k, v in sorted(self.file_lists.items()):
|
|
print()
|
|
print(k + ":")
|
|
|
|
v.sort()
|
|
|
|
for i in v:
|
|
print(" ", i.name, "xbit" if i.executable else "")
|
|
|
|
class GuiReporter(object):
|
|
"""
|
|
Displays progress using the gui.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# The time at which we should next report progress.
|
|
self.next_progress = 0
|
|
|
|
def info(self, what, pause=False, **kwargs):
|
|
if pause:
|
|
interface.info(what, **kwargs)
|
|
else:
|
|
interface.processing(what, **kwargs)
|
|
|
|
def progress(self, what, complete, total, **kwargs):
|
|
|
|
if (complete > 0) and (time.time() < self.next_progress):
|
|
return
|
|
|
|
interface.processing(what, _("Processed {b}[complete]{/b} of {b}[total]{/b} files."), complete=complete, total=total, **kwargs)
|
|
|
|
self.next_progress = time.time() + .05
|
|
|
|
def progress_done(self):
|
|
return
|
|
|
|
|
|
class TextReporter(object):
|
|
"""
|
|
Displays progress on the command line.
|
|
"""
|
|
|
|
def info(self, what, pause=False, **kwargs):
|
|
what = what.replace("[", "{")
|
|
what = what.replace("]", "}")
|
|
what = what.format(**kwargs)
|
|
print(what)
|
|
|
|
def progress(self, what, done, total, **kwargs):
|
|
what = what.replace("[", "{")
|
|
what = what.replace("]", "}")
|
|
what = what.format(**kwargs)
|
|
|
|
sys.stdout.write("\r{} - {} of {}".format(what, done + 1, total))
|
|
sys.stdout.flush()
|
|
|
|
def progress_done(self):
|
|
sys.stdout.write("\n")
|
|
|
|
|
|
def distribute_command():
|
|
ap = renpy.arguments.ArgumentParser()
|
|
ap.add_argument("--destination", "--dest", default=None, action="store", help="The directory where the packaged files should be placed.")
|
|
ap.add_argument("--packagedest", default=None, action="store", help="If given, gives the full path to the package file, without extensions." )
|
|
ap.add_argument("--no-update", default=True, action="store_false", dest="build_update", help="Prevents updates from being built.")
|
|
ap.add_argument("--package", action="append", help="If given, a package to build. Defaults to building all packages.")
|
|
ap.add_argument("--no-archive", action="store_true", help="If given, files will not be added to archives.")
|
|
ap.add_argument("--macapp", default=None, action="store", help="If given, the path to a signed and notarized mac app.")
|
|
ap.add_argument("--format", default=None, action="store", help="The format of package to build.")
|
|
|
|
ap.add_argument("project", help="The path to the project directory.")
|
|
|
|
args = ap.parse_args()
|
|
|
|
p = project.Project(args.project)
|
|
|
|
if args.package:
|
|
packages = args.package
|
|
else:
|
|
packages = None
|
|
|
|
Distributor(p, destination=args.destination, reporter=TextReporter(), packages=packages, build_update=args.build_update, noarchive=args.no_archive, packagedest=args.packagedest, macapp=args.macapp, force_format=args.format)
|
|
|
|
return False
|
|
|
|
renpy.arguments.register_command("distribute", distribute_command)
|
|
|
|
|
|
def update_old_game(project, reporter, compile):
|
|
if compile:
|
|
reporter.info(_("Recompiling all rpy files into rpyc files..."))
|
|
project.launch([ "compile", "--keep-orphan-rpyc" ], wait=True)
|
|
|
|
files = [fn + "c" for fn in project.script_files()
|
|
if fn.startswith("game/") and project.exists(fn + "c")]
|
|
len_files = len(files)
|
|
|
|
if not files:
|
|
return
|
|
|
|
TEMP_OLD_GAME_DIR = project.temp_filename("old-game")
|
|
if os.path.isdir(TEMP_OLD_GAME_DIR):
|
|
shutil.rmtree(TEMP_OLD_GAME_DIR)
|
|
|
|
for i, src in enumerate(files):
|
|
reporter.progress(_("Copying files..."), i, len_files)
|
|
dst = project.temp_filename("old-" + src)
|
|
try:
|
|
os.makedirs(os.path.dirname(dst))
|
|
except Exception:
|
|
pass
|
|
shutil.copyfile(os.path.join(project.path, src), dst)
|
|
|
|
reporter.progress_done()
|
|
|
|
OLD_GAME_DIR = os.path.join(project.path, "old-game")
|
|
if os.path.isdir(OLD_GAME_DIR):
|
|
shutil.rmtree(OLD_GAME_DIR)
|
|
|
|
shutil.copytree(TEMP_OLD_GAME_DIR, OLD_GAME_DIR)
|
|
|
|
def update_old_game_command():
|
|
ap = renpy.arguments.ArgumentParser("Back-ups all rpyc files into old-game directory.")
|
|
ap.add_argument("project", help="The path to the project directory.")
|
|
|
|
args = ap.parse_args()
|
|
|
|
update_old_game(project.Project(args.project), TextReporter(), True)
|
|
|
|
return False
|
|
|
|
renpy.arguments.register_command("update_old_game", update_old_game_command)
|
|
|
|
label distribute:
|
|
|
|
python hide:
|
|
|
|
data = project.current.data
|
|
d = distribute.Distributor(project.current,
|
|
reporter=distribute.GuiReporter(),
|
|
packages=data['packages'],
|
|
build_update=data['build_update'],
|
|
open_directory=True,
|
|
)
|
|
|
|
|
|
jump post_build
|
|
|
|
label update_old_game:
|
|
python hide:
|
|
distribute.update_old_game(project.current, distribute.GuiReporter(), False)
|
|
return
|