1#!/usr/bin/env python3
2#
3# This work is licensed under the GNU GPLv2 or later.
4# See the COPYING file in the top-level directory.
5
6
7import sys
8if sys.version_info.major < 3:
9    print("virt-manager is python3 only. Run this as ./setup.py")
10    sys.exit(1)
11
12import glob
13import os
14from pathlib import Path
15import subprocess
16
17import distutils
18import distutils.command.build
19import distutils.command.install
20import distutils.command.install_data
21import distutils.command.install_egg_info
22import distutils.dist
23import distutils.log
24import distutils.sysconfig
25
26
27sysprefix = distutils.sysconfig.get_config_var("prefix")
28
29
30def _import_buildconfig():
31    # A bit of crazyness to import the buildconfig file without importing
32    # the rest of virtinst, so the build process doesn't require all the
33    # runtime deps to be installed
34    import warnings
35
36    # 'imp' is deprecated. We use it elsewhere though too. Deal with using
37    # the modern replacement when we replace all usage
38    with warnings.catch_warnings():
39        warnings.filterwarnings("ignore", category=DeprecationWarning)
40        import imp
41        buildconfig = imp.load_source('buildconfig', 'virtinst/buildconfig.py')
42        if "libvirt" in sys.modules:
43            raise RuntimeError("Found libvirt in sys.modules. setup.py should "
44                    "not import virtinst.")
45        return buildconfig.BuildConfig
46
47
48BuildConfig = _import_buildconfig()
49
50
51# pylint: disable=attribute-defined-outside-init
52
53_desktop_files = [
54    ("share/applications", ["data/virt-manager.desktop.in"]),
55]
56_appdata_files = [
57    ("share/metainfo", ["data/virt-manager.appdata.xml.in"]),
58]
59
60
61class my_build_i18n(distutils.command.build.build):
62    """
63    Add our desktop files to the list, saves us having to track setup.cfg
64    """
65    user_options = [
66        ('merge-po', 'm', 'merge po files against template'),
67    ]
68
69    def initialize_options(self):
70        self.merge_po = False
71    def finalize_options(self):
72        pass
73
74    def run(self):
75        po_dir = "po"
76        if self.merge_po:
77            pot_file = os.path.join("po", "virt-manager.pot")
78            for po_file in glob.glob("%s/*.po" % po_dir):
79                cmd = ["msgmerge", "--previous", "-o", po_file, po_file, pot_file]
80                self.spawn(cmd)
81
82        max_po_mtime = 0
83        for po_file in glob.glob("%s/*.po" % po_dir):
84            lang = os.path.basename(po_file[:-3])
85            mo_dir = os.path.join("build", "mo", lang, "LC_MESSAGES")
86            mo_file = os.path.join(mo_dir, "virt-manager.mo")
87            if not os.path.exists(mo_dir):
88                os.makedirs(mo_dir)
89
90            cmd = ["msgfmt", po_file, "-o", mo_file]
91            po_mtime = os.path.getmtime(po_file)
92            mo_mtime = (os.path.exists(mo_file) and
93                        os.path.getmtime(mo_file)) or 0
94            if po_mtime > max_po_mtime:
95                max_po_mtime = po_mtime
96            if po_mtime > mo_mtime:
97                self.spawn(cmd)
98
99            targetpath = os.path.join("share/locale", lang, "LC_MESSAGES")
100            self.distribution.data_files.append((targetpath, (mo_file,)))
101
102        # Merge .in with translations using gettext
103        for (file_set, switch) in [(_appdata_files, "--xml"),
104                                   (_desktop_files, "--desktop")]:
105            for (target, files) in file_set:
106                build_target = os.path.join("build", target)
107                if not os.path.exists(build_target):
108                    os.makedirs(build_target)
109
110                files_merged = []
111                for f in files:
112                    if f.endswith(".in"):
113                        file_merged = os.path.basename(f[:-3])
114                    else:
115                        file_merged = os.path.basename(f)
116
117                    file_merged = os.path.join(build_target, file_merged)
118                    cmd = ["msgfmt", switch, "--template", f, "-d", po_dir,
119                           "-o", file_merged]
120                    mtime_merged = (os.path.exists(file_merged) and
121                                    os.path.getmtime(file_merged)) or 0
122                    mtime_file = os.path.getmtime(f)
123                    if (mtime_merged < max_po_mtime or
124                        mtime_merged < mtime_file):
125                        # Only build if output is older than input (.po,.in)
126                        self.spawn(cmd)
127                    files_merged.append(file_merged)
128                self.distribution.data_files.append((target, files_merged))
129
130
131class my_build(distutils.command.build.build):
132    def _make_bin_wrappers(self):
133        template = """#!/usr/bin/env python3
134
135import os
136import sys
137sys.path.insert(0, "%(sharepath)s")
138from %(pkgname)s import %(filename)s
139
140%(filename)s.runcli()
141"""
142        if not os.path.exists("build"):
143            os.mkdir("build")
144        sharepath = os.path.join(BuildConfig.prefix, "share", "virt-manager")
145
146        def make_script(pkgname, filename, toolname):
147            assert os.path.exists(pkgname + "/" + filename + ".py")
148            content = template % {
149                "sharepath": sharepath,
150                "pkgname": pkgname,
151                "filename": filename}
152
153            newpath = os.path.abspath(os.path.join("build", toolname))
154            print("Generating %s" % newpath)
155            open(newpath, "w").write(content)
156
157        make_script("virtinst", "virtinstall", "virt-install")
158        make_script("virtinst", "virtclone", "virt-clone")
159        make_script("virtinst", "virtxml", "virt-xml")
160        make_script("virtManager", "virtmanager", "virt-manager")
161
162
163    def _make_man_pages(self):
164        from distutils.spawn import find_executable
165        rstbin = find_executable("rst2man")
166        if not rstbin:
167            rstbin = find_executable("rst2man.py")
168        if not rstbin:
169            sys.exit("Didn't find rst2man or rst2man.py")
170
171        for path in glob.glob("man/*.rst"):
172            base = os.path.basename(path)
173            appname = os.path.splitext(base)[0]
174            newpath = os.path.join(os.path.dirname(path),
175                                   appname + ".1")
176
177            print("Generating %s" % newpath)
178            out = subprocess.check_output([rstbin, "--strict", path])
179            open(newpath, "wb").write(out)
180
181            self.distribution.data_files.append(
182                ('share/man/man1', (newpath,)))
183
184    def _build_icons(self):
185        for size in glob.glob(os.path.join("data/icons", "*")):
186            for category in glob.glob(os.path.join(size, "*")):
187                icons = []
188                for icon in glob.glob(os.path.join(category, "*")):
189                    icons.append(icon)
190                if not icons:
191                    continue
192
193                category = os.path.basename(category)
194                dest = ("share/icons/hicolor/%s/%s" %
195                        (os.path.basename(size), category))
196                if category != "apps":
197                    dest = dest.replace("share/", "share/virt-manager/")
198
199                self.distribution.data_files.append((dest, icons))
200
201
202    def _make_bash_completion_files(self):
203        scripts = ["virt-install", "virt-clone", "virt-xml"]
204        srcfile = "data/bash-completion.sh.in"
205        builddir = "build/bash-completion/"
206        if not os.path.exists(builddir):
207            os.makedirs(builddir)
208
209        instpaths = []
210        for script in scripts:
211            genfile = os.path.join(builddir, script)
212            print("Generating %s" % genfile)
213            src = open(srcfile, "r")
214            dst = open(genfile, "w")
215            dst.write(src.read().replace("::SCRIPTNAME::", script))
216            dst.close()
217            instpaths.append(genfile)
218
219        bashdir = "share/bash-completion/completions/"
220        self.distribution.data_files.append((bashdir, instpaths))
221
222
223    def run(self):
224        self._make_bin_wrappers()
225        self._make_man_pages()
226        self._build_icons()
227        self._make_bash_completion_files()
228
229        self.run_command("build_i18n")
230        distutils.command.build.build.run(self)
231
232
233class my_egg_info(distutils.command.install_egg_info.install_egg_info):
234    """
235    Disable egg_info installation, seems pointless for a non-library
236    """
237    def run(self):
238        pass
239
240
241class my_install(distutils.command.install.install):
242    """
243    Error if we weren't 'configure'd with the correct install prefix
244    """
245    def finalize_options(self):
246        if self.prefix is None:
247            if BuildConfig.prefix != sysprefix:
248                print("Using configured prefix=%s instead of sysprefix=%s" % (
249                    BuildConfig.prefix, sysprefix))
250                self.prefix = BuildConfig.prefix
251            else:
252                print("Using sysprefix=%s" % sysprefix)
253                self.prefix = sysprefix
254
255        elif self.prefix != BuildConfig.prefix:
256            print("Install prefix=%s doesn't match configure prefix=%s\n"
257                  "Pass matching --prefix to 'setup.py configure'" %
258                  (self.prefix, BuildConfig.prefix))
259            sys.exit(1)
260
261        distutils.command.install.install.finalize_options(self)
262
263
264class my_install_data(distutils.command.install_data.install_data):
265    def run(self):
266        distutils.command.install_data.install_data.run(self)
267
268        if not self.distribution.no_update_icon_cache:
269            distutils.log.info("running gtk-update-icon-cache")
270            icon_path = os.path.join(self.install_dir, "share/icons/hicolor")
271            self.spawn(["gtk-update-icon-cache", "-q", "-t", icon_path])
272
273        if not self.distribution.no_compile_schemas:
274            distutils.log.info("compiling gsettings schemas")
275            gschema_install = os.path.join(self.install_dir,
276                "share/glib-2.0/schemas")
277            self.spawn(["glib-compile-schemas", gschema_install])
278
279
280###################
281# Custom commands #
282###################
283
284class my_rpm(distutils.core.Command):
285    user_options = []
286    description = "Build RPMs and output to the source directory."
287
288    def initialize_options(self):
289        pass
290    def finalize_options(self):
291        pass
292
293    def run(self):
294        self.run_command('sdist')
295        srcdir = os.path.dirname(__file__)
296        cmd = [
297            "rpmbuild", "-ta",
298            "--define", "_rpmdir %s" % srcdir,
299            "--define", "_srcrpmdir %s" % srcdir,
300            "--define", "_specdir /tmp",
301            "dist/virt-manager-%s.tar.gz" % BuildConfig.version,
302        ]
303        subprocess.check_call(cmd)
304
305
306class configure(distutils.core.Command):
307    user_options = [
308        ("prefix=", None, "installation prefix"),
309        ("default-graphics=", None,
310         "Default graphics type (spice or vnc) (default=spice)"),
311        ("default-hvs=", None,
312         "Comma separated list of hypervisors shown in 'Open Connection' "
313         "wizard. (default=all hvs)"),
314
315    ]
316    description = "Configure the build, similar to ./configure"
317
318    def finalize_options(self):
319        pass
320
321    def initialize_options(self):
322        self.prefix = sysprefix
323        self.default_graphics = None
324        self.default_hvs = None
325
326
327    def run(self):
328        template = ""
329        template += "[config]\n"
330        template += "prefix = %s\n" % self.prefix
331        if self.default_graphics is not None:
332            template += "default_graphics = %s\n" % self.default_graphics
333        if self.default_hvs is not None:
334            template += "default_hvs = %s\n" % self.default_hvs
335
336        open(BuildConfig.cfgpath, "w").write(template)
337        print("Generated %s" % BuildConfig.cfgpath)
338
339
340class TestCommand(distutils.core.Command):
341    user_options = []
342    description = "DEPRECATED: Use `pytest`. See CONTRIBUTING.md"
343    def finalize_options(self):
344        pass
345    def initialize_options(self):
346        pass
347    def run(self):
348        sys.exit("ERROR: `test` is deprecated. Call `pytest` instead. "
349                 "See CONTRIBUTING.md for more info.")
350
351
352class CheckPylint(distutils.core.Command):
353    user_options = [
354        ("jobs=", "j", "use multiple processes to speed up Pylint"),
355    ]
356    description = "Check code using pylint and pycodestyle"
357
358    def initialize_options(self):
359        self.jobs = None
360
361    def finalize_options(self):
362        if self.jobs:
363            self.jobs = int(self.jobs)
364
365    def run(self):
366        import pylint.lint
367        import pycodestyle
368
369        lintfiles = ["setup.py", "virtinst", "virtManager", "tests"]
370
371        spellfiles = lintfiles[:]
372        spellfiles += list(glob.glob("*.md"))
373        spellfiles += list(glob.glob("man/*.rst"))
374        spellfiles += ["data/virt-manager.appdata.xml.in",
375                       "data/virt-manager.desktop.in",
376                       "data/org.virt-manager.virt-manager.gschema.xml",
377                       "virt-manager.spec"]
378        spellfiles.remove("NEWS.md")
379
380        try:
381            import codespell_lib
382            # pylint: disable=protected-access
383            print("running codespell")
384            codespell_lib._codespell.main(
385                '-I', 'tests/data/codespell_dict.txt',
386                '--skip', '*.pyc,*.iso,*.xml', *spellfiles)
387        except ImportError:
388            print("codespell is not installed. skipping...")
389        except Exception as e:
390            print("Error running codespell: %s" % e)
391
392        output_format = sys.stdout.isatty() and "colorized" or "text"
393
394        print("running pycodestyle")
395        style_guide = pycodestyle.StyleGuide(
396            config_file='setup.cfg',
397            format="pylint",
398            paths=lintfiles,
399        )
400        report = style_guide.check_files()
401        if style_guide.options.count:
402            sys.stderr.write(str(report.total_errors) + '\n')
403
404        print("running pylint")
405        pylint_opts = [
406            "--rcfile", ".pylintrc",
407            "--output-format=%s" % output_format,
408        ]
409        if self.jobs:
410            pylint_opts += ["--jobs=%d" % self.jobs]
411
412        pylint.lint.Run(lintfiles + pylint_opts)
413
414
415class VMMDistribution(distutils.dist.Distribution):
416    global_options = distutils.dist.Distribution.global_options + [
417        ("no-update-icon-cache", None, "Don't run gtk-update-icon-cache"),
418        ("no-compile-schemas", None, "Don't compile gsettings schemas"),
419    ]
420
421    def __init__(self, *args, **kwargs):
422        self.no_update_icon_cache = True
423        self.no_compile_schemas = True
424        distutils.dist.Distribution.__init__(self, *args, **kwargs)
425
426
427class ExtractMessages(distutils.core.Command):
428    user_options = [
429    ]
430    description = "Extract the translation messages"
431
432    def initialize_options(self):
433        pass
434
435    def finalize_options(self):
436        pass
437
438    def run(self):
439        bug_address = "https://github.com/virt-manager/virt-manager/issues"
440        potfile = "po/virt-manager.pot"
441        xgettext_args = [
442            "xgettext",
443            "--add-comments=translators",
444            "--msgid-bugs-address=" + bug_address,
445            "--package-name=virt-manager",
446            "--output=" + potfile,
447            "--sort-by-file",
448            "--join-existing",
449        ]
450
451        # Truncate .pot file to ensure it exists
452        open(potfile, "w").write("")
453
454        # First extract the messages from the AppStream sources,
455        # creating the template
456        appdata_files = [f for sublist in _appdata_files for f in sublist[1]]
457        cmd = xgettext_args + appdata_files
458        self.spawn(cmd)
459
460        # Extract the messages from the desktop files
461        desktop_files = [f for sublist in _desktop_files for f in sublist[1]]
462        cmd = xgettext_args + ["--language=Desktop"] + desktop_files
463        self.spawn(cmd)
464
465        # Extract the messages from the Python sources
466        py_sources = list(Path("virtManager").rglob("*.py"))
467        py_sources += list(Path("virtinst").rglob("*.py"))
468        py_sources = [str(src) for src in py_sources]
469        cmd = xgettext_args + ["--language=Python"] + py_sources
470        self.spawn(cmd)
471
472        # Extract the messages from the Glade UI files
473        ui_files = list(Path(".").rglob("*.ui"))
474        ui_files = [str(src) for src in ui_files]
475        cmd = xgettext_args + ["--language=Glade"] + ui_files
476        self.spawn(cmd)
477
478
479distutils.core.setup(
480    name="virt-manager",
481    version=BuildConfig.version,
482    author="Cole Robinson",
483    author_email="virt-tools-list@redhat.com",
484    url="http://virt-manager.org",
485    license="GPLv2+",
486
487    # These wrappers are generated in our custom build command
488    scripts=([
489        "build/virt-manager",
490        "build/virt-clone",
491        "build/virt-install",
492        "build/virt-xml"]),
493
494    data_files=[
495        ("share/virt-manager/ui", glob.glob("ui/*.ui")),
496
497        ("man/man1", [
498            "man/virt-manager.1",
499            "man/virt-install.1",
500            "man/virt-clone.1",
501            "man/virt-xml.1"
502        ]),
503
504        ("share/virt-manager/virtManager", glob.glob("virtManager/*.py")),
505        ("share/virt-manager/virtManager/details",
506            glob.glob("virtManager/details/*.py")),
507        ("share/virt-manager/virtManager/device",
508            glob.glob("virtManager/device/*.py")),
509        ("share/virt-manager/virtManager/lib",
510            glob.glob("virtManager/lib/*.py")),
511        ("share/virt-manager/virtManager/object",
512            glob.glob("virtManager/object/*.py")),
513        ("share/virt-manager/virtinst",
514            glob.glob("virtinst/*.py") + glob.glob("virtinst/build.cfg")),
515        ("share/virt-manager/virtinst/devices",
516            glob.glob("virtinst/devices/*.py")),
517        ("share/virt-manager/virtinst/domain",
518            glob.glob("virtinst/domain/*.py")),
519        ("share/virt-manager/virtinst/install",
520            glob.glob("virtinst/install/*.py")),
521    ],
522
523    cmdclass={
524        'build': my_build,
525        'build_i18n': my_build_i18n,
526
527        'install': my_install,
528        'install_data': my_install_data,
529
530        'configure': configure,
531
532        'pylint': CheckPylint,
533        'rpm': my_rpm,
534        'test': TestCommand,
535
536        'extract_messages': ExtractMessages,
537    },
538
539    distclass=VMMDistribution,
540)
541