diff --git a/direct/src/showutil/FreezeTool.py b/direct/src/showutil/FreezeTool.py index b9cdf1a34b..a639d78689 100644 --- a/direct/src/showutil/FreezeTool.py +++ b/direct/src/showutil/FreezeTool.py @@ -754,16 +754,16 @@ class Freezer: co = self.mf.replace_paths_in_code(module.__code__) module.__code__ = co; - def __addPyc(self, multifile, filename, code): + def __addPyc(self, multifile, filename, code, compressionLevel): if code: data = imp.get_magic() + '\0\0\0\0' + \ marshal.dumps(code) stream = StringStream(data) - multifile.addSubfile(filename, stream, 0) + multifile.addSubfile(filename, stream, compressionLevel) multifile.flush() - def __addPythonDirs(self, multifile, moduleDirs, dirnames): + def __addPythonDirs(self, multifile, moduleDirs, dirnames, compressionLevel): """ Adds all of the names on dirnames as a module directory. """ if not dirnames: return @@ -785,17 +785,18 @@ class Freezer: else: filename += '.pyo' code = compile('', moduleName, 'exec') - self.__addPyc(multifile, filename, code) + self.__addPyc(multifile, filename, code, compressionLevel) moduleDirs[str] = True - self.__addPythonDirs(multifile, moduleDirs, dirnames[:-1]) + self.__addPythonDirs(multifile, moduleDirs, dirnames[:-1], compressionLevel) - def __addPythonFile(self, multifile, moduleDirs, moduleName, mdef): + def __addPythonFile(self, multifile, moduleDirs, moduleName, mdef, + compressionLevel): """ Adds the named module to the multifile as a .pyc file. """ # First, split the module into its subdirectory names. dirnames = moduleName.split('.') - self.__addPythonDirs(multifile, moduleDirs, dirnames[:-1]) + self.__addPythonDirs(multifile, moduleDirs, dirnames[:-1], compressionLevel) filename = '/'.join(dirnames) @@ -823,7 +824,7 @@ class Freezer: if self.storePythonSource: if sourceFilename and sourceFilename.exists(): filename += '.py' - multifile.addSubfile(filename, sourceFilename, 0) + multifile.addSubfile(filename, sourceFilename, compressionLevel) return # If we can't find the source file, add the compiled pyc instead. @@ -844,16 +845,17 @@ class Freezer: source = source + '\n' code = compile(source, sourceFilename.cStr(), 'exec') - self.__addPyc(multifile, filename, code) + self.__addPyc(multifile, filename, code, compressionLevel) - def addToMultifile(self, multifile): + def addToMultifile(self, multifile, compressionLevel = 0): """ After a call to done(), this stores all of the accumulated python code into the indicated Multifile. """ moduleDirs = {} for moduleName, mdef in self.getModuleDefs(): if mdef.token != self.MTForbid: - self.__addPythonFile(multifile, moduleDirs, moduleName, mdef) + self.__addPythonFile(multifile, moduleDirs, moduleName, mdef, + compressionLevel) def writeMultifile(self, mfname): """ After a call to done(), this stores all of the accumulated diff --git a/direct/src/showutil/Packager.py b/direct/src/showutil/Packager.py index e87a4496e8..eab13475af 100644 --- a/direct/src/showutil/Packager.py +++ b/direct/src/showutil/Packager.py @@ -28,11 +28,13 @@ class Packager: notify = directNotify.newCategory("Packager") class PackFile: - def __init__(self, filename, newName = None, deleteTemp = False): + def __init__(self, filename, newName = None, deleteTemp = False, + extract = False): assert isinstance(filename, Filename) self.filename = filename self.newName = newName self.deleteTemp = deleteTemp + self.extract = extract class Package: def __init__(self, packageName, packager): @@ -44,6 +46,8 @@ class Packager: self.files = [] self.compressionLevel = 0 self.importedMapsDir = 'imported_maps' + self.mainModule = None + self.requires = [] # This is the set of files and modules, already included # by required packages, that we can skip. @@ -148,12 +152,23 @@ class Packager: # Any other file. self.addComponent(file) + if not self.mainModule and self.p3dApplication: + message = 'No main_module specified for application %s' % (self.packageName) + raise PackagerError, message + if self.mainModule: + if self.mainModule not in self.freezer.modules: + self.freezer.addModule(self.mainModule) + # Pick up any unfrozen Python files. self.freezer.done() # Add known module names. self.moduleNames = {} for moduleName in self.freezer.getAllModuleNames(): + if moduleName == '__main__': + # Ignore this special case. + continue + self.moduleNames[moduleName] = True xmodule = TiXmlElement('module') @@ -161,7 +176,9 @@ class Packager: self.components.append(xmodule) if not self.dryRun: - self.freezer.addToMultifile(self.multifile) + self.freezer.addToMultifile(self.multifile, self.compressionLevel) + if self.p3dApplication: + self.makeP3dInfo() self.multifile.repack() self.multifile.close() @@ -176,6 +193,43 @@ class Packager: if file.deleteTemp: file.filename.unlink() + def makeP3dInfo(self): + """ Makes the p3d_info.xml file that defines the + application startup parameters and such. """ + + doc = TiXmlDocument() + decl = TiXmlDeclaration("1.0", "utf-8", "") + doc.InsertEndChild(decl) + + xpackage = TiXmlElement('package') + xpackage.SetAttribute('name', self.packageName) + if self.platform: + xpackage.SetAttribute('platform', self.platform) + if self.version: + xpackage.SetAttribute('version', self.version) + + xpackage.SetAttribute('main_module', self.mainModule) + + for package in self.requires: + xrequires = TiXmlElement('requires') + xrequires.SetAttribute('name', package.packageName) + if package.platform: + xrequires.SetAttribute('platform', package.platform) + if package.version: + xrequires.SetAttribute('version', package.version) + xpackage.InsertEndChild(xrequires) + + doc.InsertEndChild(xpackage) + + # Write the xml file to a temporary file on disk, so we + # can add it to the multifile. + filename = Filename.temporary('', 'p3d_', '.xml') + doc.SaveFile(filename.toOsSpecific()) + self.multifile.addSubfile('p3d_info.xml', filename, self.compressionLevel) + self.multifile.flush() + filename.unlink() + + def compressMultifile(self): """ Compresses the .mf file into an .mf.pz file. """ @@ -435,8 +489,14 @@ class Packager: ext = Filename(file.newName).getExtension() if ext in self.packager.uncompressibleExtensions: compressible = False + + extract = file.extract if ext in self.packager.extractExtensions: extract = True + + if ext in self.packager.platformSpecificExtensions: + if not self.platform: + self.platform = PandaSystem.getPlatform() compressionLevel = 0 if compressible: @@ -451,6 +511,19 @@ class Packager: xcomponent.SetAttribute('filename', file.newName) self.components.append(xcomponent) + def requirePackage(self, package): + """ Indicates a dependency on the given package. """ + + if package in self.requires: + # Already on the list. + return + + self.requires.append(package) + for filename in package.targetFilenames.keys(): + self.skipFilenames[filename] = True + for moduleName in package.moduleNames.keys(): + self.skipModules[moduleName] = True + def __init__(self): # The following are config settings that the caller may adjust @@ -513,6 +586,9 @@ class Packager: # Files that should be extracted to disk. self.extractExtensions = [ 'dll', 'so', 'dylib', 'exe' ] + # Files that indicate a platform dependency. + self.platformSpecificExtensions = [ 'dll', 'so', 'dylib', 'exe' ] + # Binary files that are considered uncompressible, and are # copied without compression. self.uncompressibleExtensions = [ 'mp3', 'ogg' ] @@ -633,30 +709,40 @@ class Packager: return packageList - def parse_setenv(self, lineList): + def parse_set(self, lineList): """ - setenv variable value + set variable=value """ try: - command, variable, value = lineList + command, assign = lineList except ValueError: raise ArgumentNumber + + try: + variable, value = assign.split('=', 1) + except ValueError: + raise PackagerError, 'Equals sign required in assignment' - value = ExecutionEnvironment.expandString(value) + variable = variable.strip() + value = ExecutionEnvironment.expandString(value.strip()) ExecutionEnvironment.setEnvironmentVariable(variable, value) def parse_begin_package(self, lineList): """ - begin_package packageName + begin_package packageName [version=v] """ - + + args = self.parseArgs(lineList, ['version']) + try: command, packageName = lineList except ValueError: raise ArgumentNumber - self.beginPackage(packageName) + version = args.get('version', None) + + self.beginPackage(packageName, version = version, p3dApplication = False) def parse_end_package(self, lineList): """ @@ -668,11 +754,23 @@ class Packager: except ValueError: raise ArgumentError - self.endPackage(packageName) + self.endPackage(packageName, p3dApplication = False) - def parse_require(self, lineList): + def parse_begin_p3d(self, lineList): """ - require packageName + begin_p3d appName + """ + + try: + command, packageName = lineList + except ValueError: + raise ArgumentNumber + + self.beginPackage(packageName, p3dApplication = True) + + def parse_end_p3d(self, lineList): + """ + end_p3d appName """ try: @@ -680,12 +778,22 @@ class Packager: except ValueError: raise ArgumentError - package = self.findPackage(packageName) - if not package: - message = "Unknown package %s" % (packageName) - raise PackagerError, message + self.endPackage(packageName, p3dApplication = True) - self.require(package) + def parse_require(self, lineList): + """ + require packageName [version=v] + """ + + args = self.parseArgs(lineList, ['version']) + + try: + command, packageName = lineList + except ValueError: + raise ArgumentError + + version = args.get('version', None) + self.require(packageName, version = version) def parse_module(self, lineList): """ @@ -703,6 +811,18 @@ class Packager: self.module(moduleName, newName = newName) + def parse_main_module(self, lineList): + """ + main_module moduleName + """ + + try: + command, moduleName = lineList + except ValueError: + raise ArgumentError + + self.mainModule(moduleName) + def parse_freeze_exe(self, lineList): """ freeze_exe path/to/basename @@ -760,8 +880,31 @@ class Packager: raise ArgumentError self.dir(dirname, newDir = newDir) + + def parseArgs(self, lineList, argList): + args = {} + + while len(lineList) > 1: + arg = lineList[-1] + if '=' not in arg: + return args + + parameter, value = arg.split('=', 1) + parameter = parameter.strip() + value = value.strip() + if parameter not in argList: + message = 'Unknown parameter %s' % (parameter) + raise PackagerError, message + if parameter in args: + message = 'Duplicate parameter %s' % (parameter) + raise PackagerError, message + + args[parameter] = value + + del lineList[-1] + - def beginPackage(self, packageName): + def beginPackage(self, packageName, version = None, p3dApplication = False): """ Begins a new package specification. packageName is the basename of the package. Follow this with a number of calls to file() etc., and close the package with endPackage(). """ @@ -769,12 +912,27 @@ class Packager: if self.currentPackage: raise PackagerError, 'unmatched end_package %s' % (self.currentPackage.packageName) + # A special case for the Panda3D package. We enforce that the + # version number matches what we've been compiled with. + if packageName == 'panda3d': + if version is None: + version = PandaSystem.getPackageVersionString() + else: + if version != PandaSystem.getPackageVersionString(): + message = 'mismatched Panda3D version: requested %s, but Panda3D is built as %s' % (version, PandaSystem.getPackageVersionString()) + raise PackageError, message + package = self.Package(packageName, self) + package.version = version + package.p3dApplication = p3dApplication + if package.p3dApplication: + # Default compression level for an app. + package.compressionLevel = 6 package.dryRun = self.dryRun self.currentPackage = package - def endPackage(self, packageName): + def endPackage(self, packageName, p3dApplication = False): """ Closes a package specification. This actually generates the package file. The packageName must match the previous call to beginPackage(). """ @@ -784,6 +942,11 @@ class Packager: if self.currentPackage.packageName != packageName: raise PackagerError, 'end_package %s where %s expected' % ( packageName, self.currentPackage.packageName) + if self.currentPackage.p3dApplication != p3dApplication: + if p3dApplication: + raise PackagerError, 'end_p3d where end_package expected' + else: + raise PackagerError, 'end_package where end_p3d expected' package = self.currentPackage package.close() @@ -807,105 +970,49 @@ class Packager: # Look on the searchlist. for path in self.installSearch: - packageDir = Filename(path, packageName) - if packageDir.isDirectory(): - # Hey, this package appears to exist! - package = self.scanPackageDir(packageDir, packageName, version) - if package: - self.packages[(packageName, version)] = package - return package + package = self.scanPackageDir(path, packageName, version, self.platform) + if package: + self.packages[(packageName, version)] = package + return package + package = self.scanPackageDir(path, packageName, version, None) + if package: + self.packages[(packageName, version)] = package + return package return None - def scanPackageDir(self, packageDir, packageName, packageVersion): + def scanPackageDir(self, rootDir, packageName, version, platform): """ Scans a directory on disk, looking for _import.xml files that match the indicated packageName and option version. If a suitable xml file is found, reads it and returns the assocated Package definition. """ - - bestVersion = None - bestPackage = None - - # First, look for a platform-specific file. - platformDir = Filename(packageDir, self.platform) - prefix = '%s_%s_' % (packageName, self.platform) - if packageVersion: - # Do we have a specific version request? - basename = prefix + '%s_import.xml' % (packageVersion) - filename = Filename(platformDir, basename) - if filename.exists(): - # Got it! - package = self.readPackageImportDescFile(filename) - if package: - return package - else: - # Look for a generic version request. Get the highest - # version available. - files = vfs.scanDirectory(platformDir) or [] - for file in files: - filename = file.getFilename() - basename = filename.getBasename() - if not basename.startswith(prefix) or \ - not basename.endswith('_import.xml'): - continue - package = self.readPackageImportDescFile(filename) - if not package: - continue - - parts = basename.split('_') - if len(parts) == 3: - # No version number. - if bestVersion is None: - bestPackage = package - - elif len(parts) == 4: - # Got a version number. - version = self.parseVersionForCompare(parts[2]) - if bestVersion > version: - bestVersion = version - bestPackage = package - - # Didn't find a suitable platform-specific file, so look for a - # platform-nonspecific one. - prefix = '%s_' % (packageName) - if packageVersion: - # Do we have a specific version request? - basename = prefix + '%s_import.xml' % (packageVersion) - filename = Filename(packageDir, basename) - if filename.exists(): - # Got it! - package = self.readPackageImportDescFile(filename) - if package: - return package - else: - # Look for a generic version request. Get the highest - # version available. - files = vfs.scanDirectory(packageDir) or [] - for file in files: - filename = file.getFilename() - basename = filename.getBasename() - if not basename.startswith(prefix) or \ - not basename.endswith('_import.xml'): - continue - package = self.readPackageImportDescFile(filename) - if not package: - continue + packageDir = Filename(rootDir, packageName) + basename = packageName - parts = basename.split('_') - if len(parts) == 2: - # No version number. - if bestVersion is None: - bestPackage = package + if platform: + packageDir = Filename(packageDir, platform) + basename += '_%s' % (platform) - elif len(parts) == 3: - # Got a version number. - version = self.parseVersionForCompare(parts[1]) - if bestVersion > version: - bestVersion = version - bestPackage = package + if version: + packageDir = Filename(packageDir, version) + basename += '_%s' % (version) - return bestPackage + basename += '_import.xml' + filename = Filename(packageDir, basename) + if filename.exists(): + # It exists in the nested directory. + package = self.readPackageImportDescFile(filename) + if package: + return package + filename = Filename(rootDir, basename) + if filename.exists(): + # It exists in the root directory. + package = self.readPackageImportDescFile(filename) + if package: + return package + + return None def readPackageImportDescFile(self, filename): """ Reads the named xml file as a Package, and returns it if @@ -917,26 +1024,51 @@ class Packager: return None - - def parseVersionForCompare(self, version): - """ Given a formatted version string, breaks it up into a - tuple, grouped so that putting the tuples into sorted order - will put the order numbers in increasing order. """ - - # TODO. For now, we just use a dumb string compare. - return (version,) - - def require(self, package): - """ Indicates a dependency on the indicated package. + def require(self, packageName, version = None): + """ Indicates a dependency on the named package, supplied as + name. Attempts to install this package will implicitly install the named package also. Files already included in the named package will be omitted from this one. """ - for filename in package.targetFilenames.keys(): - self.currentPackage.skipFilenames[filename] = True - for moduleName in package.moduleNames.keys(): - self.currentPackage.skipModules[moduleName] = True + # A special case for the Panda3D package. We enforce that the + # version number matches what we've been compiled with. + if packageName == 'panda3d': + if version is None: + version = PandaSystem.getPackageVersionString() + + package = self.findPackage(packageName, version = version) + if not package: + message = "Unknown package %s" % (packageName) + raise PackagerError, message + + self.requirePackage(package) + + def requirePackage(self, package): + """ Indicates a dependency on the indicated package, supplied + as a Package object. + + Attempts to install this package will implicitly install the + named package also. Files already included in the named + package will be omitted from this one. """ + + # A special case for the Panda3D package. We enforce that the + # version number matches what we've been compiled with. + if package.packageName == 'panda3d': + if package.version != PandaSystem.getPackageVersionString(): + if not PandaSystem.getPackageVersionString(): + # We haven't been compiled with any particular + # version of Panda. This is a warning, not an + # error. + print "Warning: requiring panda3d version %s, which may or may not match the current build of Panda. Recommend that you use only the official Panda3D build for making distributable applications to ensure compatibility." % (package.version) + else: + # This particular version of Panda doesn't match + # the requested version. Again, a warning, not an + # error. + print "Warning: requiring panda3d version %s, which does not match the current build of Panda, which is version %s." % (package, PandaSystem.getPackageVersionString()) + + self.currentPackage.requirePackage(package) def module(self, moduleName, newName = None): """ Adds the indicated Python module to the current package. """ @@ -946,6 +1078,19 @@ class Packager: self.currentPackage.freezer.addModule(moduleName, newName = newName) + def mainModule(self, moduleName, newName = None): + """ Names the indicated module as the "main" module of the + application or exe. """ + + if not self.currentPackage: + raise OutsideOfPackageError + + if self.currentPackage.mainModule and self.currentPackage.mainModule != moduleName: + self.notify.warning("Replacing main_module %s with %s" % ( + self.currentPackage.mainModule, moduleName)) + + self.currentPackage.mainModule = moduleName + def freeze(self, filename, compileToExe = False): """ Freezes all of the current Python code into either an executable (if compileToExe is true) or a dynamic library (if @@ -958,6 +1103,19 @@ class Packager: package = self.currentPackage freezer = package.freezer + + if package.mainModule and not compileToExe: + self.notify.warning("Ignoring main_module for dll %s" % (filename)) + package.mainModule = None + if not package.mainModule and compileToExe: + message = "No main_module specified for exe %s" % (filename) + raise PackagerError, message + + if package.mainModule: + if package.mainModule not in freezer.modules: + freezer.addModule(package.mainModule, newName = '__main__') + else: + freezer.modules['__main__'] = freezer.modules[package.mainModule] freezer.done() if not package.dryRun: @@ -966,12 +1124,16 @@ class Packager: if '/' in basename: dirname, basename = filename.rsplit('/', 1) dirname += '/' + basename = freezer.generateCode(basename, compileToExe = compileToExe) - package.files.append(self.PackFile(Filename(basename), newName = dirname + basename, deleteTemp = True)) + package.files.append(self.PackFile(Filename(basename), newName = dirname + basename, deleteTemp = True, extract = True)) + if not package.platform: + package.platform = PandaSystem.getPlatform() # Reset the freezer for more Python files. freezer.reset() + package.mainModule = None def file(self, filename, newNameOrDir = None): diff --git a/direct/src/showutil/runp3d.py b/direct/src/showutil/runp3d.py index 8a38263440..4e25fa6fff 100755 --- a/direct/src/showutil/runp3d.py +++ b/direct/src/showutil/runp3d.py @@ -22,7 +22,7 @@ See pack3d.py for a script that generates these p3d files. import sys from direct.showbase import VFSImporter from direct.showbase.DirectObject import DirectObject -from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties +from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties, readXmlStream from direct.stdpy import file from direct.task.TaskManagerGlobal import taskMgr from direct.showbase.MessengerGlobal import messenger @@ -192,7 +192,25 @@ class AppRunner(DirectObject): # Hang a hook so we know when the window is actually opened. self.acceptOnce('window-event', self.windowEvent) - import main + # Look for the startup Python file. This may be a magic + # filename (like "__main__", or any filename that contains + # invalid module characters), so we can't just import it + # directly; instead, we go through the low-level importer. + + # If there's no p3d_info.xml file, we look for "main". + moduleName = 'main' + if self.p3dPackage: + mainName = self.p3dPackage.Attribute('main_module') + if mainName: + moduleName = mainName + + v = VFSImporter.VFSImporter(MultifileRoot) + loader = v.find_module(moduleName) + if not loader: + message = "No %s.py found in application." % (mainName) + raise StandardError, message + + main = loader.load_module(moduleName) if hasattr(main, 'main') and callable(main.main): main.main() @@ -233,31 +251,6 @@ class AppRunner(DirectObject): # Now go load the applet. fname = Filename.fromOsSpecific(p3dFilename) - if not p3dFilename: - # If we didn't get a literal filename, we have to download it - # from the URL. TODO: make this a smarter temporary filename? - fname = Filename.temporary('', 'p3d_') - fname.setExtension('p3d') - p3dFilename = fname.toOsSpecific() - src = self.tokenDict.get('src', None) - if not src: - raise ArgumentError, "No Panda app specified." - - http = HTTPClient.getGlobalPtr() - hc = http.getDocument(src) - if not hc.downloadToFile(fname): - fname.unlink() - raise ArgumentError, "Couldn't download %s" % (src) - - # Set a hook on sys.exit to delete the temporary file. - oldexitfunc = getattr(sys, 'exitfunc', None) - def deleteTempFile(fname = fname, oldexitfunc = oldexitfunc): - fname.unlink() - if oldexitfunc: - oldexitfunc() - - sys.exitfunc = deleteTempFile - vfs = VirtualFileSystem.getGlobalPtr() if not vfs.exists(fname): @@ -285,6 +278,19 @@ class AppRunner(DirectObject): data = open(pathname, 'r').read() loadPrcFileData(pathname, data) + # Now load the p3dInfo file. + self.p3dInfo = None + self.p3dPackage = None + i = mf.findSubfile('p3d_info.xml') + if i >= 0: + stream = mf.openReadSubfile(i) + self.p3dInfo = readXmlStream(stream) + mf.closeReadSubfile(stream) + if self.p3dInfo: + p3dPackage = self.p3dInfo.FirstChild('package') + if p3dPackage: + self.p3dPackage = p3dPackage.ToElement() + self.gotP3DFilename = True # Send this call to the main thread; don't call it directly.