build_helpers.py 14.9 KB
Newer Older
1
# This program is free software: you can redistribute it and/or modify
2
# it under the terms of the GNU General Public License as published by
3 4 5 6 7 8 9 10
# the Free Software Foundation, either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
11
# You should have received a copy of the GNU General Public License
12
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
13 14 15 16 17 18

# Copied and adapted from the DistUtilsExtra project
# Created by Sebastian Heinlein and Martin Pitt
# Copyright Canonical Ltd.

# Modified by Kai Willadsen for the Meld project
19
# Copyright (C) 2013-2014 Kai Willadsen <kai.willadsen@gmail.com>
20

21
import distutils.cmd
22
import distutils.command.build
23
import distutils.command.build_py
24
import distutils.command.install
25
import distutils.command.install_data
26
import distutils.dir_util
27
import distutils.dist
28 29
import glob
import os.path
30
import platform
31
import sys
32 33
from distutils.log import info

34

35
def has_help(self):
36
    return "build_help" in self.distribution.cmdclass and os.name != 'nt'
37 38


39
def has_icons(self):
40
    return "build_icons" in self.distribution.cmdclass
41 42


43
def has_i18n(self):
44
    return "build_i18n" in self.distribution.cmdclass and os.name != 'nt'
45

Kai Willadsen's avatar
Kai Willadsen committed
46

47
def has_data(self):
48
    return "build_data" in self.distribution.cmdclass
49 50 51 52 53 54 55 56


distutils.command.build.build.sub_commands.extend([
    ("build_i18n", has_i18n),
    ("build_icons", has_icons),
    ("build_help", has_help),
    ("build_data", has_data),
])
Kai Willadsen's avatar
Kai Willadsen committed
57 58


59 60 61
class MeldDistribution(distutils.dist.Distribution):
    global_options = distutils.dist.Distribution.global_options + [
        ("no-update-icon-cache", None, "Don't run gtk-update-icon-cache"),
62
        ("no-compile-schemas", None, "Don't compile gsettings schemas"),
63 64 65 66
    ]

    def __init__(self, *args, **kwargs):
        self.no_update_icon_cache = False
67
        self.no_compile_schemas = False
68
        super().__init__(*args, **kwargs)
69 70


Kai Willadsen's avatar
Kai Willadsen committed
71 72 73
class build_data(distutils.cmd.Command):

    gschemas = [
74
        ('share/glib-2.0/schemas', ['data/org.gnome.meld.gschema.xml'])
Kai Willadsen's avatar
Kai Willadsen committed
75 76
    ]

77 78 79 80
    frozen_gschemas = [
        ('share/meld', ['data/gschemas.compiled']),
    ]

81 82 83 84 85
    # FIXME: This is way too much hard coding, but I really hope
    # it also doesn't last that long.
    resource_source = "meld/resources/meld.gresource.xml"
    resource_target = "org.gnome.meld.gresource"

Kai Willadsen's avatar
Kai Willadsen committed
86 87 88 89 90 91 92
    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def get_data_files(self):
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
        data_files = []

        build_path = os.path.join('build', 'data')
        if not os.path.exists(build_path):
            os.makedirs(build_path)

        info("compiling gresources")
        resource_dir = os.path.dirname(self.resource_source)
        target = os.path.join(build_path, self.resource_target)
        self.spawn([
            "glib-compile-resources",
            "--target={}".format(target),
            "--sourcedir={}".format(resource_dir),
            self.resource_source,
        ])

        data_files.append(('share/meld', [target]))

111
        if os.name == 'nt':
112
            gschemas = self.frozen_gschemas
113
        else:
114 115 116 117
            gschemas = self.gschemas
        data_files.extend(gschemas)

        return data_files
Kai Willadsen's avatar
Kai Willadsen committed
118 119 120 121

    def run(self):
        data_files = self.distribution.data_files
        data_files.extend(self.get_data_files())
122 123


124 125 126 127 128 129 130 131 132 133 134 135 136 137
class build_help(distutils.cmd.Command):

    help_dir = 'help'

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def get_data_files(self):
        data_files = []
        name = self.distribution.metadata.name

138 139 140
        if "LINGUAS" in os.environ:
            self.selected_languages = os.environ["LINGUAS"].split()
        else:
141 142 143
            self.selected_languages = [
                d for d in os.listdir(self.help_dir) if os.path.isdir(d)
            ]
144

145 146 147
        if 'C' not in self.selected_languages:
            self.selected_languages.append('C')

148 149 150 151 152
        self.C_PAGES = glob.glob(os.path.join(self.help_dir, 'C', '*.page'))
        self.C_EXTRA = glob.glob(os.path.join(self.help_dir, 'C', '*.xml'))

        for lang in self.selected_languages:
            source_path = os.path.join(self.help_dir, lang)
153 154 155
            if not os.path.exists(source_path):
                continue

156 157 158 159 160 161 162 163 164 165 166
            build_path = os.path.join('build', self.help_dir, lang)
            if not os.path.exists(build_path):
                os.makedirs(build_path)

            if lang != 'C':
                po_file = os.path.join(source_path, lang + '.po')
                mo_file = os.path.join(build_path, lang + '.mo')

                msgfmt = ['msgfmt', po_file, '-o', mo_file]
                self.spawn(msgfmt)
                for page in self.C_PAGES:
167 168
                    itstool = [
                        'itstool', '-m', mo_file, '-o', build_path, page]
169 170
                    self.spawn(itstool)
                for extra in self.C_EXTRA:
171 172
                    extra_path = os.path.join(
                        build_path, os.path.basename(extra))
173 174 175 176 177 178 179 180 181 182
                    if os.path.exists(extra_path):
                        os.unlink(extra_path)
                    os.symlink(os.path.relpath(extra, source_path), extra_path)
            else:
                distutils.dir_util.copy_tree(source_path, build_path)

            xml_files = glob.glob('%s/*.xml' % build_path)
            mallard_files = glob.glob('%s/*.page' % build_path)
            path_help = os.path.join('share', 'help', lang, name)
            path_figures = os.path.join(path_help, 'figures')
183
            data_files.append((path_help, xml_files + mallard_files))
184 185 186
            figures = glob.glob('%s/figures/*.png' % build_path)
            if figures:
                data_files.append((path_figures, figures))
187 188

        return data_files
Kai Willadsen's avatar
Kai Willadsen committed
189

190 191 192
    def run(self):
        data_files = self.distribution.data_files
        data_files.extend(self.get_data_files())
193 194 195 196 197
        self.check_help()

    def check_help(self):
        for lang in self.selected_languages:
            build_path = os.path.join('build', self.help_dir, lang)
198 199 200
            if not os.path.exists(build_path):
                continue

201 202 203 204
            pages = [os.path.basename(p) for p in self.C_PAGES]
            for page in pages:
                page_path = os.path.join(build_path, page)
                if not os.path.exists(page_path):
205
                    info("skipping missing file %s", page_path)
206 207 208 209
                    continue
                lint = ['xmllint', '--noout', '--noent', '--path', build_path,
                        '--xinclude', page_path]
                self.spawn(lint)
210 211 212 213


class build_icons(distutils.cmd.Command):

Kai Willadsen's avatar
Kai Willadsen committed
214
    icon_dir = os.path.join("data", "icons")
215
    target = "share/icons"
216
    frozen_target = "share/meld/icons"
217 218 219 220 221 222 223 224

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
225
        target_dir = self.frozen_target if os.name == 'nt' else self.target
226 227 228 229 230
        data_files = self.distribution.data_files

        for theme in glob.glob(os.path.join(self.icon_dir, "*")):
            for size in glob.glob(os.path.join(theme, "*")):
                for category in glob.glob(os.path.join(size, "*")):
231 232
                    icons = (glob.glob(os.path.join(category, "*.png")) +
                             glob.glob(os.path.join(category, "*.svg")))
233 234
                    icons = [
                        icon for icon in icons if not os.path.islink(icon)]
235 236
                    if not icons:
                        continue
237 238 239
                    data_files.append(("%s/%s/%s/%s" %
                                       (target_dir,
                                        os.path.basename(theme),
240 241
                                        os.path.basename(size),
                                        os.path.basename(category)),
Kai Willadsen's avatar
Kai Willadsen committed
242
                                       icons))
243 244 245 246


class build_i18n(distutils.cmd.Command):

247
    bug_contact = None
248 249
    domain = "meld"
    po_dir = "po"
250
    merge_po = False
251

252 253
    # FIXME: It's ridiculous to specify these here, but I know of no other
    # way except magically extracting them from self.distribution.data_files
254
    desktop_files = [('share/applications', glob.glob("data/*.desktop.in"))]
255
    xml_files = [
256
        ('share/metainfo', glob.glob("data/*.appdata.xml.in")),
257 258 259 260
        ('share/mime/packages', glob.glob("data/mime/*.xml.in"))
    ]
    schemas_files = []
    key_files = []
261

262
    def initialize_options(self):
263
        pass
264 265

    def finalize_options(self):
266
        pass
267

268
    def _rebuild_po(self):
269 270 271 272 273 274
        # If there is a po/LINGUAS file, or the LINGUAS environment variable
        # is set, only compile the languages listed there.
        selected_languages = None
        linguas_file = os.path.join(self.po_dir, "LINGUAS")
        if "LINGUAS" in os.environ:
            selected_languages = os.environ["LINGUAS"].split()
275 276
        elif os.path.isfile(linguas_file):
            selected_languages = open(linguas_file).read().split()
277

278 279 280 281 282 283 284
        # If we're on Windows, assume we're building frozen and make a bunch
        # of insane assumptions.
        if os.name == 'nt':
            msgfmt = "C:\\Python27\\Tools\\i18n\\msgfmt"
        else:
            msgfmt = "msgfmt"

285 286
        # Update po(t) files and print a report
        # We have to change the working dir to the po dir for intltool
Kai Willadsen's avatar
Kai Willadsen committed
287 288 289 290
        cmd = [
            "intltool-update",
            (self.merge_po and "-r" or "-p"), "-g", self.domain
        ]
291 292 293 294 295 296 297
        wd = os.getcwd()
        os.chdir(self.po_dir)
        self.spawn(cmd)
        os.chdir(wd)
        max_po_mtime = 0
        for po_file in glob.glob("%s/*.po" % self.po_dir):
            lang = os.path.basename(po_file[:-3])
298
            if selected_languages and lang not in selected_languages:
299
                continue
Kai Willadsen's avatar
Kai Willadsen committed
300
            mo_dir = os.path.join("build", "mo", lang, "LC_MESSAGES")
301 302 303
            mo_file = os.path.join(mo_dir, "%s.mo" % self.domain)
            if not os.path.exists(mo_dir):
                os.makedirs(mo_dir)
304
            cmd = [msgfmt, po_file, "-o", mo_file]
305
            po_mtime = os.path.getmtime(po_file)
Kai Willadsen's avatar
Kai Willadsen committed
306 307
            mo_mtime = (
                os.path.exists(mo_file) and os.path.getmtime(mo_file) or 0)
308 309 310 311 312 313
            if po_mtime > max_po_mtime:
                max_po_mtime = po_mtime
            if po_mtime > mo_mtime:
                self.spawn(cmd)

            targetpath = os.path.join("share/locale", lang, "LC_MESSAGES")
314 315 316 317 318 319 320 321 322
            self.distribution.data_files.append((targetpath, (mo_file,)))
        self.max_po_mtime = max_po_mtime

    def run(self):
        if self.bug_contact is not None:
            os.environ["XGETTEXT_ARGS"] = "--msgid-bugs-address=%s " % \
                                          self.bug_contact

        self._rebuild_po()
323

324 325 326 327 328 329 330 331 332
        intltool_switches = [
            (self.xml_files, "-x"),
            (self.desktop_files, "-d"),
            (self.schemas_files, "-s"),
            (self.key_files, "-k"),
        ]

        for file_set, switch in intltool_switches:
            for target, files in file_set:
333
                build_target = os.path.join("build", target)
Kai Willadsen's avatar
Kai Willadsen committed
334
                if not os.path.exists(build_target):
335 336 337
                    os.makedirs(build_target)
                files_merged = []
                for file in files:
338 339 340
                    file_merged = os.path.basename(file)
                    if file_merged.endswith(".in"):
                        file_merged = file_merged[:-3]
341
                    file_merged = os.path.join(build_target, file_merged)
Kai Willadsen's avatar
Kai Willadsen committed
342
                    cmd = ["intltool-merge", switch, self.po_dir, file,
343
                           file_merged]
Kai Willadsen's avatar
Kai Willadsen committed
344 345
                    mtime_merged = (os.path.exists(file_merged) and
                                    os.path.getmtime(file_merged) or 0)
346
                    mtime_file = os.path.getmtime(file)
Kai Willadsen's avatar
Kai Willadsen committed
347 348
                    if (mtime_merged < self.max_po_mtime or
                            mtime_merged < mtime_file):
Kai Willadsen's avatar
Kai Willadsen committed
349
                        # Only build if output is older than input (.po,.in)
350 351
                        self.spawn(cmd)
                    files_merged.append(file_merged)
352
                self.distribution.data_files.append((target, files_merged))
353 354


355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
class build_py(distutils.command.build_py.build_py):
    """Insert real package installation locations into conf module

    Adapted from gottengeography
    """

    data_line = 'DATADIR = "%s"'
    locale_line = 'LOCALEDIR = "%s"'

    def build_module(self, module, module_file, package):

        if module_file == 'meld/conf.py':
            with open(module_file) as f:
                contents = f.read()

            try:
371 372
                options = self.distribution.get_option_dict('install')
                prefix = options['prefix'][1]
373
            except KeyError as e:
Kai Willadsen's avatar
Kai Willadsen committed
374
                print(e)
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
                prefix = sys.prefix

            datadir = os.path.join(prefix, 'share', 'meld')
            localedir = os.path.join(prefix, 'share', 'locale')

            start, end = 0, 0
            lines = contents.splitlines()
            for i, line in enumerate(lines):
                if line.startswith('# START'):
                    start = i
                elif line.startswith('# END'):
                    end = i

            if start and end:
                lines[start:end + 1] = [
                    self.data_line % datadir,
                    self.locale_line % localedir,
                ]

            module_file = module_file + "-installed"
            contents = "\n".join(lines)
            with open(module_file, 'w') as f:
                f.write(contents)

        distutils.command.build_py.build_py.build_module(
            self, module, module_file, package)


403 404 405
class install(distutils.command.install.install):

    def finalize_options(self):
406
        special_cases = ('debian', 'ubuntu', 'linuxmint')
407 408 409 410 411 412 413 414 415
        if (platform.system() == 'Linux' and
                platform.linux_distribution()[0].lower() in special_cases):
            # Maintain an explicit install-layout, but use deb by default
            specified_layout = getattr(self, 'install_layout', None)
            self.install_layout = specified_layout or 'deb'

        distutils.command.install.install.finalize_options(self)


416 417 418 419
class install_data(distutils.command.install_data.install_data):

    def run(self):
        distutils.command.install_data.install_data.run(self)
420 421 422 423 424 425 426

        if not self.distribution.no_update_icon_cache:
            # TODO: Generalise to non-hicolor icon themes
            info("running gtk-update-icon-cache")
            icon_path = os.path.join(self.install_dir, "share/icons/hicolor")
            self.spawn(["gtk-update-icon-cache", "-q", "-t", icon_path])

427 428
        if not self.distribution.no_compile_schemas:
            info("compiling gsettings schemas")
429 430 431
            gschema_path = build_data.gschemas[0][0]
            gschema_install = os.path.join(self.install_dir, gschema_path)
            self.spawn(["glib-compile-schemas", gschema_install])