Source

fanstatic / fanstatic / compiler.py

import subprocess 
import os.path

from fanstatic import MINIFIED, DEBUG
from fanstatic.processor import Processor

class CompilerException(Exception):
    """
    The path could not be compiled.
    """


class Compiler(Processor):
    """
    A Compiler compiles resources of source_extension to target_extension.

    The filename of the target of the compilation contains the name of the compiler.
    """
    target_extension = None

    minified = False
    debug = False

    @classmethod
    def target_path(cls, path, mode=None):
        """
        Return the target path based on the compiler and path.
        Works for both relative paths and absolute paths.
        """
        directory, filename = os.path.split(path)
        base, _ = os.path.splitext(filename)
        suffix = ''
        if mode == MINIFIED and cls.minified:
            suffix = '-min'
        elif mode == DEBUG and cls.debug:
            suffix = '-debug'
        return os.path.join(directory, '%s%s.%s' % (base, suffix, cls.target_extension))


class SASS(Compiler):
    source_extension = 'scss'
    target_extension = 'css'

    minified = True
    debug = True

    @staticmethod
    def available():
        try:
            check = subprocess.check_call('which sass', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except subprocess.CalledProcessError:
            return False
        return not bool(check)

    @classmethod
    def __call__(cls, path, mode=None):
        # From the sass command line utility:
        # -t, --style NAME   Output style. Can be nested (default), compact, compressed, or expanded.
        options = ''
        if mode == DEBUG:
            options += '-t expanded'
        elif mode == MINIFIED:
            options += '-t compressed'

        cmd = ['sass', options, path, cls.target_path(path, mode=mode)]
        p = subprocess.Popen(' '.join(cmd), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
            raise CompilerException(p.stderr.read())

class CoffeeScript(Compiler):
    source_extension = 'coffee'
    target_extension = 'js'

    @staticmethod
    def available():
        try:
            check = subprocess.check_call('which coffee', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except subprocess.CalledProcessError:
            return False
        return not bool(check)

    @classmethod
    def __call__(cls, path, mode=None):
        cmd = ['coffee', '--bare', '--compile', '--print', path]
        
        p = subprocess.Popen(' '.join(cmd), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
            raise CompilerException(p.stderr.read())

        f = open(cls.target_path(path, mode=mode), 'wb')
        try:
            f.write(p.stdout.read())
        finally:
            f.close()


class LessCSS(Compiler):
    source_extension = 'less'
    target_extension = 'css'

    minified = True

    @staticmethod
    def available():
        try:
            check = subprocess.check_call('which lessc', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        except subprocess.CalledProcessError:
            return False
        return not bool(check)

    @classmethod
    def __call__(cls, path, mode=None):
        cmd = ['lessc', path]
        if mode == MINIFIED:
            # To output minified CSS, simply pass the -x option.
            cmd.insert(1, '-x')

        p = subprocess.Popen(' '.join(cmd), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
            raise CompilerException(p.stderr.read())

        f = open(cls.target_path(path, mode=mode), 'wb')
        try:
            f.write(p.stdout.read())
        finally:
            f.close()