le module cx_freeze

Ce module est téléchargeable sur http://sourceforge.net/projects/cx-freeze/

Les principaux avantages de ce module est:

  • support de python 3
  • création possible d’un installateur
  • création possible d’un service (uniquement avec python2 ... dommage)

le principe est assez simple:

  • on génére un fichier setup.py qui contient les informations de compilation
  • on génère l’exe

Création d’un exécutable ou d’un installateur

python setup.py build
  • ou bien l’installateur
python setup.py bdist_msi

un exemple simple de fichier setup.py

import sys
from cx_Freeze import setup, Executable

setup(  name = "foo",
        version = "0.1",
        description = "My application!",
        executables = [Executable("foo.py")])

il faut parfois rajouter des modules externes et gérer les guis

import sys
from cx_Freeze import setup, Executable

# Dependencies are automatically detected, but it might need fine tuning.
build_exe_options = {"packages": ["os"], "excludes": ["tkinter"]}

# GUI applications require a different base on Windows (the default is for a
# console application).
base = None
if sys.platform == "win32":
    base = "Win32GUI"

setup(  name = "guifoo",
        version = "0.1",
        description = "My GUI application!",
        options = {"build_exe": build_exe_options},
        executables = [Executable("guifoo.py", base=base)])

Concernant les dépendances, il faut noter que les dépendances aux format egg ne sont pas bien pris en charge (ex colorconsole, ...).

On obtient alors une liste de module en erreur

j’ai construit un script qui permet de décompresser à la voler ces egg afin de les intégrer correctement

import sys
import shutil
import os
import os.path
import zipfile
from cx_Freeze import setup, Executable

def manage_eggs(eggs, eggs_unzip_path):
    if len(eggs) > 0:
        if os.path.isdir(eggs_unzip_path):
            shutil.rmtree(eggs_unzip_path)
        os.mkdir(eggs_unzip_path)
        list_eggs = []
        for k in sys.path:
            if os.path.isfile(k) and os.path.splitext(k)[1] == '.egg':
                list_eggs.append(k)
        if len(list_eggs) == 0:
            raise RuntimeError("not found module %s" % (','.join(eggs)))
        for i in eggs:
            egg = [item for item in list_eggs if item.find(i) > 0 ]
            if len(egg) == 0:
                raise RuntimeError("not found module %s" % (','.join(eggs)))
            with zipfile.ZipFile(egg[0], "r") as z:
                z.extractall(eggs_unzip_path)
        sys.path.insert(1,eggs_unzip_path)

eggs=['colorconsole']
eggs_unzip_path = 'libeggs'

manage_eggs(eggs, os.path.join(os.path.split(__file__)[0],eggs_unzip_path))

setup(
    name='Glances',
    version='1.7.2',
    description="A cross-platform curses-based monitoring tool",
    long_description=open('README.rst').read(),
    author='Nicolas Hennion',
    author_email='nicolas@nicolargo.com',
    url='https://github.com/nicolargo/glances',
    license="LGPL",
    keywords="cli curses monitoring system",
    executables = [Executable("glances/glances.py")]
)

if len(eggs) > 0:
    shutil.rmtree(eggs_unzip_path)

Il faut regarder du côté de la documentation pour trouver plein d’argument très utile

http://cx-freeze.readthedocs.org/en/latest/distutils.html#distutils

Il est aussi possible d’avori des problèmes de compilation si le code utilise par exemple

a = os.path.dirname(os.path.realpath(__file__))

il faut modifier le code ainsi

def we_are_frozen():
    # All of the modules are built-in to the interpreter, e.g., by py2exe
    return hasattr(sys, "frozen")

def module_path():
    encoding = sys.getfilesystemencoding()
    if we_are_frozen():
        return os.path.dirname(os.path.realpath(unicode(sys.executable, encoding)))
    return os.path.dirname(os.path.realpath(unicode(__file__, encoding)))
a = module_path()

Il est aussi possible de créer par le msi des raccourcit dans le dossier Programmes de windows

Voiçi un exemple concret

import sys
import shutil
import os
import os.path
import zipfile
from cx_Freeze import *

def manage_eggs(eggs, eggs_unzip_path):
    if len(eggs) > 0:
        if os.path.isdir(eggs_unzip_path):
            shutil.rmtree(eggs_unzip_path)
        os.mkdir(eggs_unzip_path)
        list_eggs = []
        for k in sys.path:
            if os.path.isfile(k) and os.path.splitext(k)[1] == '.egg':
                list_eggs.append(k)
        if len(list_eggs) == 0:
            raise RuntimeError("not found module %s" % (','.join(eggs)))
        for i in eggs:
            egg = [item for item in list_eggs if item.find(i) > 0 ]
            if len(egg) == 0:
                raise RuntimeError("not found module %s" % (','.join(eggs)))
            with zipfile.ZipFile(egg[0], "r") as z:
                z.extractall(eggs_unzip_path)
        sys.path.insert(1,eggs_unzip_path)

eggs=['colorconsole']
eggs_unzip_path = 'libeggs'

manage_eggs(eggs, os.path.join(os.path.split(__file__)[0],eggs_unzip_path))


directory_table = [
    ("ProgramMenuFolder",       # Directory_
     "TARGETDIR",               # Directory_Parent
     ".:Programs",              # DefaultDir
     ),
    ("GlancesMenuFolder",       # Directory_
     "ProgramMenuFolder",       # Directory_Parent
     "Glances",                 # DefaultDir
     )
    ]

shortcut_table = [
    ("GlancesExe",             # Shortcut
     "GlancesMenuFolder",      # Directory_
     "glances",                # Name
     "TARGETDIR",              # Component_
     "[TARGETDIR]glances.exe", # Target
     None,                     # Arguments
     None,                     # Description
     None,                     # Hotkey
     None,                     # Icon
     None,                     # IconIndex
     None,                     # ShowCmd
     'TARGETDIR'               # WkDir
     ),
    ("GlancesExeServer",       # Shortcut
     "GlancesMenuFolder",      # Directory_
     "glances server",         # Name
     "TARGETDIR",              # Component_
     "[TARGETDIR]glances.exe", # Target
     "-s",                     # Arguments
     None,                     # Description
     None,                     # Hotkey
     None,                     # Icon
     None,                     # IconIndex
     None,                     # ShowCmd
     'TARGETDIR'               # WkDir
     )
    ]

msi_data = {"Directory": directory_table,
            "Shortcut": shortcut_table}
bdist_msi_options = {'data': msi_data}

setup(
    name='Glances',
    version='1.7.2',
    description="A cross-platform curses-based monitoring tool",
    long_description=open('README.rst').read(),
    author='Nicolas Hennion',
    author_email='nicolas@nicolargo.com',
    url='https://github.com/nicolargo/glances',
    license="LGPL",
    keywords="cli curses monitoring system",
    options = {
        "bdist_msi": bdist_msi_options,
        'build_exe': {'icon':'glances.ico'}
    },
    executables = [Executable("glances/glances.py",
                    icon = "glances.ico",)]
)

if len(eggs) > 0:
    shutil.rmtree(eggs_unzip_path)

Note

il existe comme dossier spécifique windows pour les raccourcits: DesktopFolder, StartupFolder,StartMenuFolder,ProgramMenuFolder, SystemFolder

Pour voir ce qu’il est possible d’ajouter dans un MSI vous pouvez utiliser MSI_Analyser et le msi d’apache par exemple

Création d’un service

Il faut pour cela avoir sur sa machine le module Cx_Logging (http://cx-logging.sourceforge.net/)

il faut aussi avoir le fichier cx_Thread (modifié par mes soins)

"""Defines methods for managing threads, events and queues."""

#import cx_Exceptions
import cx_Logging
import sys
import thread
import time

class Thread(object):
    """Base class for threads which is a little more lightweight than the
       threading module exports and allows for finer control."""

    def __init__(self, function, *args, **keywordArgs):
        self.function = function
        self.args = args
        self.keywordArgs = keywordArgs
        self.started = False
        self.stopped = False
        self.errorObj = None
        self.event = None
        self.name = None

    def OnThreadEnd(self):
        """Called when the thread is ended. Override in child classes."""
        cx_Logging.Info("thread %r ending", self.name)

    def OnThreadStart(self):
        """Called when the thread is started. Override in child classes."""
        cx_Logging.Info("thread %r starting", self.name)

    def Start(self, loggingState = None):
        """Start the thread."""
        self.started = True
        self.stopped = False
        self.errorObj = None
        thread.start_new_thread(self._Run, (loggingState,))

    def _Run(self, loggingState):
        """Execute the function associated with the thread."""
        try:
            if loggingState is not None:
                cx_Logging.SetLoggingState(loggingState)
            self.OnThreadStart()
            try:
                self.function(*self.args, **self.keywordArgs)
            except:
                #self.errorObj = cx_Exceptions.GetExceptionInfo(*sys.exc_info())
                #cx_Logging.LogException(self.errorObj)
                cx_Logging.Error("Thread %r terminating", self.name)
        finally:
            self.stopped = True
            self.OnThreadEnd()
            if self.event:
                self.event.Set()


class Event(object):
    """Event class which permits synchronization between threads."""

    def __init__(self):
        self.lock = thread.allocate_lock()
        self.isSet = False
        self.waiters = []

    def Clear(self):
        """Clear the flag."""
        self.lock.acquire()
        self.isSet = False
        self.lock.release()

    def Set(self):
        """Set the flag and notify all waiters of the event."""
        self.lock.acquire()
        self.isSet = True
        if self.waiters:
            for waiter in self.waiters:
                waiter.release()
            self.waiters = []
            self.isSet = False
        self.lock.release()

    def Wait(self):
        """Wait for the flag to be set and immediately reset it."""
        self.lock.acquire()
        if self.isSet:
            self.isSet = False
            self.lock.release()
        else:
            waiterLock = thread.allocate_lock()
            waiterLock.acquire()
            self.waiters.append(waiterLock)
            self.lock.release()
            waiterLock.acquire()


class Queue(object):
    """Light weight implementation of stacks and queues."""

    def __init__(self):
        self.lock = thread.allocate_lock()
        self.queueEvent = Event()
        self.items = []

    def QueueItem(self, item):
        """Add an item to end of the list of items (for queues)."""
        self.lock.acquire()
        self.items.append(item)
        self.lock.release()
        self.queueEvent.Set()

    def PopItem(self, returnNoneIfEmpty=False):
        """Get the next item from the beginning of the list of items,
           optionally returning None if nothing is found."""
        self.lock.acquire()
        while not self.items:
            self.lock.release()
            if returnNoneIfEmpty:
                return None
            self.queueEvent.Wait()
            self.lock.acquire()
        item = self.items.pop(0)
        self.lock.release()
        return item

    def PushItem(self, item):
        """Add an item to the beginning of the list of items (for stacks)."""
        self.lock.acquire()
        self.items.insert(0, item)
        self.lock.release()
        self.queueEvent.Set()


class ResourcePool(object):
    """Implements a pool of resources."""

    def __init__(self, maxResources, newResourceFunc):
        self.lock = thread.allocate_lock()
        self.poolEvent = Event()
        self.freeResources = []
        self.busyResources = []
        self.maxResources = maxResources
        self.newResourceFunc = newResourceFunc

    def Destroy(self):
        """Destroy the resource pool, this blocks until all resources are
           returned to the pool for destruction."""
        self.lock.acquire()
        self.freeResources = []
        self.maxResources = 0
        self.lock.release()
        while self.busyResources:
            self.poolEvent.Wait()

    def Get(self):
        """Gets a resource form the pool, creating new resources as necessary.
           The calling thread will block until a resource is available, if
           necessary."""
        resource = None
        self.lock.acquire()
        while resource is None:
            try:
                if self.freeResources:
                    resource = self.freeResources.pop()
                elif len(self.busyResources) < self.maxResources:
                    resource = self.newResourceFunc()
                elif not self.maxResources:
                    raise "No resources not available."
                else:
                    self.lock.release()
                    self.poolEvent.Wait()
                    self.lock.acquire()
            except:
                if self.lock.locked():
                    self.lock.release()
                raise
        self.busyResources.append(resource)
    self.lock.release()
    return resource

def Put(self, resource, addToFreeList = True):
    """Put a resource back into the pool."""
    self.lock.acquire()
    try:
        index = self.busyResources.index(resource)
        del self.busyResources[index]
        if self.maxResources and addToFreeList:
            self.freeResources.append(resource)
    finally:
        self.lock.release()
    self.poolEvent.Set()


class Timer(Thread):
    """Operates a timer."""

    def __init__(self, timeInSeconds):
        Thread.__init__(self, time.sleep, timeInSeconds)

il faut par la suite 3 fichiers:

  • Config.py
  • ServiceHandler.py (notre service)
  • setup.py

config.py

#------------------------------------------------------------------------------
# Config.py
#   This file defines information about the service. The first four
# attributes are expected to be defined and if they are not an exception will
# be thrown when attempting to create the service:
#
#   NAME
#       the name to call the service with one %s place holder that will be used
#       to identify the service further.
#
#   DISPLAY_NAME
#       the value to use as the display name for the service with one %s place
#       holder that will be used to identify the service further.
#
#   MODULE_NAME
#       the name of the module implementing the service.
#
#   CLASS_NAME
#       the name of the class within the module implementing the service. This
#       class should accept no parameters in the constructor. It should have a
#       method called "Initialize" which will accept the configuration file
#       name. It should also have a method called "Run" which will be called
#       with no parameters when the service is started. It should also have a
#       method called "Stop" which will be called with no parameters when the
#       service is stopped using the service control GUI.
#
#   DESCRIPTION
#       the description of the service (optional)
#
#   AUTO_START
#       whether the service should be started automatically (optional)
#
#   SESSION_CHANGES
#       whether the service should monitor session changes (optional). If
#       True, session changes will call the method "SessionChanged" with the
#       parameters sessionId and eventTypeId.
#------------------------------------------------------------------------------

NAME = "cx_FreezeSampleService%s"
DISPLAY_NAME = "cx_Freeze Sample Service - %s"
MODULE_NAME = "ServiceHandler"
CLASS_NAME = "Handler"
DESCRIPTION = "Sample service description"
AUTO_START = False
SESSION_CHANGES = False

ServiceHandler.py

"""
Implements a simple service using cx_Freeze.

This sample makes use of cx_PyGenLib (http://cx-pygenlib.sourceforge.net) and
cx_Logging (http://cx-logging.sourceforge.net).

See below for more information on what methods must be implemented and how they
are called.
"""

import cx_Logging
import cx_Threads
import sys

class Handler(object):

    # no parameters are permitted; all configuration should be placed in the
    # configuration file and handled in the Initialize() method
    def __init__(self):
        cx_Logging.Info("creating handler instance")
        self.stopEvent = cx_Threads.Event()

    # called when the service is starting
    def Initialize(self, configFileName):
        cx_Logging.Info("initializing: config file name is %r", configFileName)

    # called when the service is starting immediately after Initialize()
    # use this to perform the work of the service; don't forget to set or check
    # for the stop event or the service GUI will not respond to requests to
    # stop the service
    def Run(self):
        cx_Logging.Info("running service....")
        self.stopEvent.Wait()

    # called when the service is being stopped by the service manager GUI
    def Stop(self):
        cx_Logging.Info("stopping service...")
        self.stopEvent.Set()
# A simple setup script for creating a Windows service. See the comments in the
# Config.py and ServiceHandler.py files for more information on how to set this
# up.
#
# Installing the service is done with the option --install <Name> and
# uninstalling the service is done with the option --uninstall <Name>. The
# value for <Name> is intended to differentiate between different invocations
# of the same service code -- for example for accessing different databases or
# using different configuration files.

from cx_Freeze import setup, Executable

buildOptions = dict(includes = ["ServiceHandler"])
executable = Executable("Config.py", base = "Win32Service",
        targetName = "cx_FreezeSampleService.exe")

setup(
        name = "cx_FreezeSampleService",
        version = "0.1",
        description = "Sample cx_Freeze Windows serice",
        executables = [executable],
        options = dict(build_exe = buildOptions))

pour génerer l’executable

python setup.py build

par la suite pour installer le service (en mode Administrateur)

cx_FreezeSampleService.exe --install tata

pour le désinstaller

cx_FreezeSampleService.exe --uninstall tata

Warning

si lors du lancement du msi vous avez un message comme quoi le msi n’est pas “reading” il faut ajouter au repertoire du msi tout les droits pour tout le monde (il s’agit d’un problème de création de dossier temporaire qui bloque l’installation)