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