Skip to content

Commit

Permalink
Code clean-up. Also fixed issue where freeze would fail if attempt tw…
Browse files Browse the repository at this point in the history
…o include same file twice via two different symbolic paths.
  • Loading branch information
cainesi committed Jul 25, 2020
1 parent a3a9cb3 commit 3f08428
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 40 deletions.
75 changes: 64 additions & 11 deletions cx_Freeze/darwintools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import subprocess
import stat
from typing import List, Dict, Optional, Set
from typing import List, Dict, Optional, Set, Iterable


# In a MachO file, need to deal specially with links that use @executable_path,
Expand All @@ -15,6 +15,9 @@
# linked MachO files leading the the current file, to determine which directories are
# included in the current rpath.

class DarwinException(Exception):
pass

def _isMachOFile(path: str) -> bool:
"""Determines whether the file is a Mach-O file."""
if not os.path.isfile(path):
Expand All @@ -27,9 +30,9 @@ def _isMachOFile(path: str) -> bool:
class MachOReference:
"""Represents a linking reference from MachO file to another file."""
def __init__(self, sourceFile: "DarwinFile", rawPath: str, resolvedPath: str):
self.sourceFile = sourceFile
self.rawPath = rawPath
self.resolvedPath = resolvedPath
self.sourceFile: "DarwinFile" = sourceFile
self.rawPath: str = rawPath
self.resolvedPath: str = resolvedPath

# isSystemFile is True if the target is a system file that will not be
# included in package
Expand All @@ -45,8 +48,6 @@ def setTargetFile(self, darwinFile: "DarwinFile"):
self.isCopied = True
return



class DarwinFile:
"""A DarwinFile object tracks a file referenced in the application, and record where it was
ultimately moved to in the application bundle. Should also save a copy of the DarwinFile
Expand Down Expand Up @@ -79,7 +80,7 @@ def __init__(self, originalObjectPath: str,

for rawPath, resolvedPath in self.libraryPathResolution.items():
if resolvedPath in self.machReferenceDict:
raise Exception("Dynamic libraries resolved to the same file?")
raise DarwinException("Dynamic libraries resolved to the same file?")
self.machReferenceDict[resolvedPath] = MachOReference(sourceFile=self,
rawPath=rawPath,
resolvedPath=resolvedPath)
Expand Down Expand Up @@ -119,21 +120,21 @@ def sourceDir(self) -> str:
def resolveLoader(self, path:str) -> Optional[str]:
if self.isLoaderPath(path=path):
return path.replace("@loader_path", self.sourceDir(), 1)
raise Exception("resolveLoader() called on bad path: {}".format(path))
raise DarwinException("resolveLoader() called on bad path: {}".format(path))


def resolveExecutable(self, path:str) -> str:
if self.isExecutablePath(path=path):
return path.replace("@executable_path", self.sourceDir(), 1)
raise Exception("resolveExecutable() called on bad path: {}".format(path))
raise DarwinException("resolveExecutable() called on bad path: {}".format(path))

def resolveRPath(self, path: str) -> str:
for rp in self.getRPath():
testPath = os.path.abspath( path.replace("@rpath", rp, 1) )
if _isMachOFile(testPath):
return testPath
pass
raise Exception("resolveRPath() failed to resolve path: {}".format(path))
raise DarwinException("resolveRPath() failed to resolve path: {}".format(path))

def getRPath(self) -> List[str]:
"""Returns the rpath in effect for this file."""
Expand Down Expand Up @@ -170,7 +171,7 @@ def resolvePath(self, path) -> str:
testPath = os.path.abspath( os.path.join(self.sourceDir(), path) )
if _isMachOFile(path=testPath):
return testPath
raise Exception("Could not resolve path: {}".format(path))
raise DarwinException("Could not resolve path: {}".format(path))

def resolveLibraryPaths(self):
for lc in self.loadCommands:
Expand Down Expand Up @@ -326,3 +327,55 @@ def changeLoadReference(fileName: str, oldReference: str, newReference: str,
os.chmod(fileName, original)
return

class DarwinFileTracker:
"""Object to track the DarwinFiles that have been added during a freeze."""

def __init__(self):
self._targetFileList: List[DarwinFile] = []
self._targetFileDict: Dict[str, DarwinFile] = {}
return

def __iter__(self) -> Iterable[DarwinFile]:
return iter(self._targetFileList)

def pathIsAlreadyCopiedTo(self, targetPath: str) -> bool:
"""Check if the given targetPath has already has a file copied to it."""
if targetPath in self._targetFileDict: return True
return False

def getDarwinFile(self, sourcePath: str, targetPath: str) -> DarwinFile:
"""Gets the DarwinFile for file copied from sourcePath to targetPath. If either (i) nothing,
or (ii) a different file has been copied to targetPath, raises a DarwinException."""

# check that the file has been copied to
if targetPath not in self._targetFileDict:
raise DarwinException(
"File \"{}\" already copied to, but no DarwinFile object found for it.".format(targetPath))

# check that the target file came from the specified source
targetDarwinFile: DarwinFile = self._targetFileDict[targetPath]
realSource = os.path.realpath(sourcePath)
targetRealSource = os.path.realpath(targetDarwinFile.originalObjectPath)
if realSource != targetRealSource:
exceptionString = \
"""Attempting to copy two files to "{}"
source 1: "{}" (real: "{}")
source 2: "{}" (real: "{}")
(This may be caused by including modules in the zip file that rely on binary libraries with the same name.)"""
exceptionString = exceptionString.format( targetPath,
targetDarwinFile.originalObjectPath, targetRealSource,
sourcePath, realSource )

raise DarwinException(exceptionString)
return targetDarwinFile


def addFile(self, targetPath:str, darwinFile: DarwinFile):
"""Record that a DarwinFile is being copied to a given path. If the same file has already been copied
to that path, do nothing. If a different file has been copied to that bath, raise a DarwinException."""
if self.pathIsAlreadyCopiedTo(targetPath=targetPath):
raise DarwinException("addFile() called with targetPath already copied to (targetPath=\"{}\")".format(targetPath))

self._targetFileList.append(darwinFile)
self._targetFileDict[targetPath] = darwinFile
return
27 changes: 10 additions & 17 deletions cx_Freeze/freezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
import sys
import sysconfig
import time
from typing import Optional
import zipfile

import cx_Freeze
from cx_Freeze.darwintools import DarwinFile, MachOReference
from cx_Freeze.darwintools import DarwinFile, MachOReference, DarwinFileTracker

__all__ = [ "ConfigError", "ConstantsModule", "Executable", "Freezer" ]

Expand Down Expand Up @@ -140,18 +141,9 @@ def _CopyFile(self, source, target, copyDependentFiles,
if sys.platform == "darwin" and (machOReference is not None):
# If file was already copied, and we are following a reference from a DarwinFile, then we need
# to tell the reference where the file was copied to.
if normalizedTarget not in self.darwinFileDict:
raise Exception("File \"{}\" already copied to, but no DarwinFile object found for it.".format(normalizedTarget))
assert (normalizedTarget in self.darwinFileDict)
# confirm that the file already copied to this location came from the same source
targetFile: DarwinFile = self.darwinFileDict[normalizedTarget]
machOReference.setTargetFile(darwinFile=targetFile)
if targetFile.originalObjectPath != normalizedSource:
raise Exception("Attempting to copy two files to \"{}\" (\"{}\" and \"{}\")".format(
normalizedTarget, normalizedSource, targetFile.originalObjectPath
))
assert( targetFile.originalObjectPath == normalizedSource )
#TODO: we should check that targetFile has the same source path as the reference specifies (otherwise, we have multiple files being copied to the same target...
targetDarwinFile = self.darwinTracker.getDarwinFile(sourcePath=normalizedSource,
targetPath=normalizedTarget)
machOReference.setTargetFile(darwinFile=targetDarwinFile)
return
if normalizedSource == normalizedTarget:
return
Expand All @@ -176,8 +168,7 @@ def _CopyFile(self, source, target, copyDependentFiles,
newDarwinFile.copyDestinationPath = normalizedTarget
if machOReference is not None:
machOReference.setTargetFile(darwinFile=newDarwinFile)
self.darwinFiles.append(newDarwinFile)
self.darwinFileDict[normalizedTarget] = newDarwinFile
self.darwinTracker.addFile(targetPath=normalizedTarget, darwinFile=newDarwinFile)
pass

if copyDependentFiles \
Expand Down Expand Up @@ -644,8 +635,10 @@ def Freeze(self):
self.filesCopied = {}
self.linkerWarnings = {}
self.msvcRuntimeDir = None
self.darwinFiles = []
self.darwinFileDict = {}

self.darwinTracker: Optional[DarwinFileTracker] = None
if sys.platform == "darwin":
self.darwinTracker = DarwinFileTracker()

self.finder = self._GetModuleFinder()
for executable in self.executables:
Expand Down
31 changes: 20 additions & 11 deletions cx_Freeze/macdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from cx_Freeze.common import normalize_to_list

from cx_Freeze.darwintools import changeLoadReference
from cx_Freeze.darwintools import changeLoadReference, DarwinFile, DarwinFileTracker

__all__ = ["bdist_dmg", "bdist_mac"]

Expand Down Expand Up @@ -178,14 +178,23 @@ def setRelativeReferencePaths(self, buildDir: str, binDir: str):
#TODO: Do an initial pass through the DarwinFiles to see if any references on DarwinFiles copied into the
# bundle that were not already set--in which case we set them?

for mf in self.darwinFiles:
relativeMFDest = os.path.relpath(mf.copyDestinationPath, buildDir)
filePathInBinDir = os.path.join(binDir, relativeMFDest)
for path, ref in mf.machReferenceDict.items():
if not ref.isCopied: continue
rawPath = ref.rawPath # this is the reference in the machO file that needs to be updated
targFile = ref.targetFile
absoluteDest = targFile.copyDestinationPath # this is the absolute in the build directory
for darwinFile in self.darwinTracker:
# get the relative path to darwinFile in build directory
relativeCopyDestination = os.path.relpath(darwinFile.copyDestinationPath, buildDir)
# figure out directory where it will go in binary directory
filePathInBinDir = os.path.join(binDir, relativeCopyDestination)

# for each file that this darwinFile references, update the reference as necessary
# if the file is copied into the binary package, change the refernce to be relative to
# @executable_path (so an .app bundle will work wherever it is moved)
for path, machORef in darwinFile.machReferenceDict.items():
if not machORef.isCopied:
# referenced file not copied -- assume this is a system file that will also be
# present on the user's machine, and do not change reference
continue
rawPath = machORef.rawPath # this is the reference in the machO file that needs to be updated
referencedDarwinFile: DarwinFile = machORef.targetFile
absoluteDest = referencedDarwinFile.copyDestinationPath # this is the absolute in the build directory
relativeDest = os.path.relpath(absoluteDest, buildDir)
exePath = "@executable_path/{}".format(relativeDest)
changeLoadReference(filePathInBinDir,oldReference=rawPath,newReference=exePath, VERBOSE=False)
Expand Down Expand Up @@ -242,7 +251,7 @@ def prepare_qt_app(self):
def run(self):
self.run_command('build')
build = self.get_finalized_command('build')
freezer = self.get_finalized_command('build_exe').freezer
freezer: "freezer.Freezer" = self.get_finalized_command('build_exe').freezer

# Define the paths within the application bundle
self.bundleDir = os.path.join(build.build_base,
Expand Down Expand Up @@ -289,7 +298,7 @@ def run(self):
self.execute(self.create_plist, ())

# Make all references to libraries relative
self.darwinFiles = freezer.darwinFiles
self.darwinTracker: DarwinFileTracker = freezer.darwinTracker
self.execute(self.setRelativeReferencePaths, (os.path.abspath(build.build_exe), os.path.abspath(self.binDir)))

# Make library references absolute if enabled
Expand Down
4 changes: 3 additions & 1 deletion cx_Freeze/samples/PyQt5_plugins/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ def _getInclude(pluginPath: str) -> Tuple[str,str]:
}

exe = Executable(script = "test_script.py")
exe2 = Executable(script = "test_script2.py")


setup( name = "Test application",
author = "[author]",
maintainer = "[maintainer]",
maintainer_email = "[email]",
options = {"build":build_options, "build_exe" : build_exe_options, "bdist_mac": bdist_mac_options,
"bdist_dmg": bdist_dmg_options},
executables = [exe]
executables = [exe, exe2]
)
2 changes: 2 additions & 0 deletions cx_Freeze/samples/PyQt5_plugins/test_script2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

print("bye")

0 comments on commit 3f08428

Please sign in to comment.