<commit_before>import os.path
import shutil
import subprocess
import tempfile
import sys

from collections import defaultdict, OrderedDict
from textwrap import indent, dedent

from .. import get_logger
from .base import *
from .osxutil import *

logger = get_logger(__file__)

class OSXGenerator(Generator):
    def generate(self, base='.'):
        self.build_dir = os.path.abspath(base)

        o = OrderedDict()

        for name, layout in self._project.layouts.items():
            logger.info("Generating '%s'..." % name)
            o[name] = self.generate_xml(layout)

        if self.dry_run:
            logger.info("Dry run completed.")
            return

        if os.path.exists(self.build_dir):
            shutil.rmtree(self.build_dir)

        logger.info("Creating bundle...")
        bundle_path = self.create_bundle(self.build_dir)
        res_path = os.path.join(bundle_path, "Contents", "Resources")

        translations = defaultdict(dict)

        for name, data in o.items():
            layout = self._project.layouts[name]
            fn = layout.internal_name

            for locale, lname in layout.display_names.items():
                translations[locale][fn] = lname

            logger.debug("%s.keylayout -> bundle" % fn)
            with open(os.path.join(res_path, "%s.keylayout" % fn), 'w') as f:
                f.write(data)

            self.write_icon(res_path, layout)

        self.write_localisations(res_path, translations)

        logger.info("Creating installer...")
        pkg_path = self.create_installer(bundle_path)

        if self.is_release:
            logger.info("Signing installer...")
            self.sign_installer(pkg_path)

        logger.info("Done!")

    def generate_iconset(self, icon, output_fn):
        cmd_tmpl = "convert -resize {d}x{d} -background transparent -gravity center -extent {d}x{d} {src} {out}"

        files = (
            ("icon_16x16", 16),
            ("icon_16x16@2x", 32),
            ("icon_32x32", 32),
            ("icon_32x32@2x", 64)
        )

        iconset = tempfile.TemporaryDirectory(suffix=".iconset")

        for name, dimen in files:
            fn = "%s.png" % name
            cmd = cmd_tmpl.format(d=dimen, src=icon, out=os.path.join(iconset.name, fn))

            logger.info("Creating '%s.png' at size %dx%d" % (name, dimen, dimen))
            process = subprocess.Popen(cmd, shell=True,
                    stderr=subprocess.PIPE, stdout=subprocess.PIPE)
            out, err = process.communicate()
            if process.returncode != 0:
                logger.error(err.decode())
                logger.error("Application ended with error code %s." % process.returncode)
                # TODO throw exception instead.
                sys.exit(process.returncode)
        
        process = subprocess.Popen(["iconutil", "--convert", "icns",
                                    "--output", output_fn, iconset.name],
                                   stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        
        out, err = process.communicate()
        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % process.returncode)
            # TODO throw exception instead.
            sys.exit(process.returncode)
        
    def write_icon(self, res_path, layout):
        icon = self._project.target('osx').get('icon', None)
        if icon is None:
            logger.warning("no icon for layout '%s'." % layout.internal_name)
            return

        iconpath = self._project.relpath(icon)

        fn = os.path.join(res_path, "%s.icns" % layout.internal_name)
        self.generate_iconset(iconpath, fn)

    def write_localisations(self, res_path, translations):
        for locale, o in translations.items():
            path = os.path.join(res_path, "%s.lproj" % locale)
            os.makedirs(path)

            with open(os.path.join(path, "InfoPlist.strings"), 'w') as f:
                for name, lname in o.items():
                    f.write('"%s" = "%s";\n' % (name, lname))

    def create_bundle(self, path):
        bundle_path = os.path.join(path, "%s.bundle" % self._project.internal_name)
        bundle_name = self._project.target('osx').get('bundleName', None)

        if bundle_name is None:
            raise Exception("Target 'osx' has no 'bundleName' property. "
                "Please fix your project YAML file.")

        os.makedirs(os.path.join(bundle_path, 'Contents', 'Resources'),
            exist_ok=True)

        bundle_id = "%s.keyboardlayout.%s" % (
                self._project.target('osx')['packageId'],
                self._project.internal_name
            )

        target_tmpl = indent(dedent("""\
<key>KLInfo_%s</key>
<dict>
    <key>TISInputSourceID</key>
    <string>%s.%s</string>
    <key>TISIntendedLanguage</key>
    <string>%s</string>
</dict>"""), "        ")

        targets = []
        for name, layout in self._project.layouts.items():
            name = layout.internal_name
            bundle_chunk = name.lower().replace(' ', '')
            targets.append(target_tmpl % (name, bundle_id, bundle_chunk,
                layout.locale))

        with open(os.path.join(bundle_path, 'Contents', "Info.plist"), 'w') as f:
            f.write(dedent("""\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundleIdentifier</key>
        <string>%s</string>
        <key>CFBundleName</key>
        <string>%s</string>
        <key>CFBundleVersion</key>
        <string>%s</string>
%s
    </dict>
</plist>
                """) % (
                    bundle_id,
                    bundle_name,
                    self._project.build,
                    '\n'.join(targets)
                )
            )

        return bundle_path

    def create_installer(self, bundle):
        pkg_name = "%s.unsigned.pkg" % self._project.internal_name

        cmd = ['productbuild', '--component',
                bundle, '/Library/Keyboard Layouts',
                pkg_name]

        process = subprocess.Popen(cmd, cwd=self.build_dir,
                stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        out, err = process.communicate()

        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % (
                    process.returncode))
            sys.exit(process.returncode)
        
        return os.path.join(self.build_dir, pkg_name)
    
    def sign_installer(self, pkg_path):
        signed_path = "%s.pkg" % self._project.internal_name

        sign_id = self._project.target('osx').get("codeSignId", None)

        if sign_id is None:
            logger.error("No signing identify found; skipping.")
            return

        cmd = ['productsign', '--sign', sign_id, pkg_path, signed_path]
        process = subprocess.Popen(cmd, cwd=self.build_dir,
                stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        out, err = process.communicate()

        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % (
                    process.returncode))
            sys.exit(process.returncode)

    def generate_xml(self, layout):
        #name = layout.display_names[layout.locale]
        name = layout.internal_name
        out = OSXKeyLayout(name, random_id())
<commit_msg>Print proof that the pkg was signed<commit_after>import os.path
import shutil
import subprocess
import tempfile
import sys

from collections import defaultdict, OrderedDict
from textwrap import indent, dedent

from .. import get_logger
from .base import *
from .osxutil import *

logger = get_logger(__file__)

class OSXGenerator(Generator):
    def generate(self, base='.'):
        self.build_dir = os.path.abspath(base)

        o = OrderedDict()

        for name, layout in self._project.layouts.items():
            logger.info("Generating '%s'..." % name)
            o[name] = self.generate_xml(layout)

        if self.dry_run:
            logger.info("Dry run completed.")
            return

        if os.path.exists(self.build_dir):
            shutil.rmtree(self.build_dir)

        logger.info("Creating bundle...")
        bundle_path = self.create_bundle(self.build_dir)
        res_path = os.path.join(bundle_path, "Contents", "Resources")

        translations = defaultdict(dict)

        for name, data in o.items():
            layout = self._project.layouts[name]
            fn = layout.internal_name

            for locale, lname in layout.display_names.items():
                translations[locale][fn] = lname

            logger.debug("%s.keylayout -> bundle" % fn)
            with open(os.path.join(res_path, "%s.keylayout" % fn), 'w') as f:
                f.write(data)

            self.write_icon(res_path, layout)

        self.write_localisations(res_path, translations)

        logger.info("Creating installer...")
        pkg_path = self.create_installer(bundle_path)

        if self.is_release:
            logger.info("Signing installer...")
            self.sign_installer(pkg_path)

        logger.info("Done!")

    def generate_iconset(self, icon, output_fn):
        cmd_tmpl = "convert -resize {d}x{d} -background transparent -gravity center -extent {d}x{d} {src} {out}"

        files = (
            ("icon_16x16", 16),
            ("icon_16x16@2x", 32),
            ("icon_32x32", 32),
            ("icon_32x32@2x", 64)
        )

        iconset = tempfile.TemporaryDirectory(suffix=".iconset")

        for name, dimen in files:
            fn = "%s.png" % name
            cmd = cmd_tmpl.format(d=dimen, src=icon, out=os.path.join(iconset.name, fn))

            logger.info("Creating '%s.png' at size %dx%d" % (name, dimen, dimen))
            process = subprocess.Popen(cmd, shell=True,
                    stderr=subprocess.PIPE, stdout=subprocess.PIPE)
            out, err = process.communicate()
            if process.returncode != 0:
                logger.error(err.decode())
                logger.error("Application ended with error code %s." % process.returncode)
                # TODO throw exception instead.
                sys.exit(process.returncode)
        
        process = subprocess.Popen(["iconutil", "--convert", "icns",
                                    "--output", output_fn, iconset.name],
                                   stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        
        out, err = process.communicate()
        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % process.returncode)
            # TODO throw exception instead.
            sys.exit(process.returncode)
        
    def write_icon(self, res_path, layout):
        icon = self._project.target('osx').get('icon', None)
        if icon is None:
            logger.warning("no icon for layout '%s'." % layout.internal_name)
            return

        iconpath = self._project.relpath(icon)

        fn = os.path.join(res_path, "%s.icns" % layout.internal_name)
        self.generate_iconset(iconpath, fn)

    def write_localisations(self, res_path, translations):
        for locale, o in translations.items():
            path = os.path.join(res_path, "%s.lproj" % locale)
            os.makedirs(path)

            with open(os.path.join(path, "InfoPlist.strings"), 'w') as f:
                for name, lname in o.items():
                    f.write('"%s" = "%s";\n' % (name, lname))

    def create_bundle(self, path):
        bundle_path = os.path.join(path, "%s.bundle" % self._project.internal_name)
        bundle_name = self._project.target('osx').get('bundleName', None)

        if bundle_name is None:
            raise Exception("Target 'osx' has no 'bundleName' property. "
                "Please fix your project YAML file.")

        os.makedirs(os.path.join(bundle_path, 'Contents', 'Resources'),
            exist_ok=True)

        bundle_id = "%s.keyboardlayout.%s" % (
                self._project.target('osx')['packageId'],
                self._project.internal_name
            )

        target_tmpl = indent(dedent("""\
<key>KLInfo_%s</key>
<dict>
    <key>TISInputSourceID</key>
    <string>%s.%s</string>
    <key>TISIntendedLanguage</key>
    <string>%s</string>
</dict>"""), "        ")

        targets = []
        for name, layout in self._project.layouts.items():
            name = layout.internal_name
            bundle_chunk = name.lower().replace(' ', '')
            targets.append(target_tmpl % (name, bundle_id, bundle_chunk,
                layout.locale))

        with open(os.path.join(bundle_path, 'Contents', "Info.plist"), 'w') as f:
            f.write(dedent("""\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundleIdentifier</key>
        <string>%s</string>
        <key>CFBundleName</key>
        <string>%s</string>
        <key>CFBundleVersion</key>
        <string>%s</string>
%s
    </dict>
</plist>
                """) % (
                    bundle_id,
                    bundle_name,
                    self._project.build,
                    '\n'.join(targets)
                )
            )

        return bundle_path

    def create_installer(self, bundle):
        pkg_name = "%s.unsigned.pkg" % self._project.internal_name

        cmd = ['productbuild', '--component',
                bundle, '/Library/Keyboard Layouts',
                pkg_name]

        process = subprocess.Popen(cmd, cwd=self.build_dir,
                stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        out, err = process.communicate()

        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % (
                    process.returncode))
            sys.exit(process.returncode)
        
        return os.path.join(self.build_dir, pkg_name)
    
    def sign_installer(self, pkg_path):
        signed_path = "%s.pkg" % self._project.internal_name

        sign_id = self._project.target('osx').get("codeSignId", None)

        if sign_id is None:
            logger.error("No signing identify found; skipping.")
            return

        cmd = ['productsign', '--sign', sign_id, pkg_path, signed_path]
        process = subprocess.Popen(cmd, cwd=self.build_dir,
                stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        out, err = process.communicate()

        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % (
                    process.returncode))
            sys.exit(process.returncode)

        cmd = ['pkgutil', '--check-signature', signed_path]
        process = subprocess.Popen(cmd, cwd=self.build_dir,
                stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        out, err = process.communicate()

        # TODO: seriously need to deduplicate this popen dance... 
        if process.returncode != 0:
            logger.error(err.decode())
            logger.error("Application ended with error code %s." % (
                    process.returncode))
            sys.exit(process.returncode)

        logger.info(out.decode().strip())

    def generate_xml(self, layout):
        #name = layout.display_names[layout.locale]
        name = layout.internal_name
        out = OSXKeyLayout(name, random_id())

{'fraction_non_alphanumeric': 0.12985775971551944, 'fraction_numerical': 0.005016510033020066, 'mean_word_length': 2.0240015360983103, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 2, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2437452', 'n_tokens_mistral': 4840, 'n_tokens_neox': 4574, 'n_words': 941}