blob: 14a75382d75a0bb9e1f00bac5982420b01af8e2b [file] [log] [blame]
Chris Matthewsf0d9e882018-01-11 23:55:23 +00001#!/usr/bin/python2.7
2"""
3Dependency manager for llvm CI builds.
4
5We have complex dependencies for some of our CI builds. This will serve
6as a system to help document and enforce them.
7
8Developer notes:
9
10- We are trying to keep package dependencies to a minimum in this project. So it
11does not require an installer. It should be able to be run as a stand alone script
12when checked out of VCS. So, don't import anything not in the Python 2.7
13standard library.
14
15"""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20from __future__ import unicode_literals
21
22import argparse
Chris Matthewsea57dc42018-01-31 22:37:34 +000023import collections
Chris Matthewsf0d9e882018-01-11 23:55:23 +000024import json
25import platform
26import re
27import subprocess
28
Chris Matthews5b8d4b22018-02-03 00:56:41 +000029import os
Chris Matthewsf0d9e882018-01-11 23:55:23 +000030
31try:
Chris Matthewsbcc3c052018-02-01 00:33:15 +000032 # noinspection PyUnresolvedReferences
33 from typing import List, Text, Union, Dict, Type, Optional # noqa
Chris Matthewsf0d9e882018-01-11 23:55:23 +000034except ImportError as e:
Chris Matthewsf0d9e882018-01-11 23:55:23 +000035 pass # Not really needed at runtime, so okay to not have installed.
36
37
38VERSION = '0.1'
39"""We have a built in version check, so we can require specific features and fixes."""
40
41
42class Version(object):
43 """Model a version number, which can be compared to another version number.
44
45 Keeps a nice looking text version around as well for printing.
46
47 This abstraction exists to make some of the more complex comparisons easier,
48 as well as collecting and printing versions.
49
50 In the future, we might want to have some different comparison,
51 for instance, 4.0 == 4.0.0 -> True.
52
53 """
54
55 def __init__(self, text):
56 """Create a version from a . separated version string."""
57 self.text = text
58 self.numeric = [int(d) for d in text.split(".")]
59
60 def __gt__(self, other):
61 """Compare the numeric representation of the version."""
62 return self.numeric.__gt__(other.numeric)
63
64 def __lt__(self, other):
65 """Compare the numeric representation of the version."""
66 return self.numeric.__lt__(other.numeric)
67
68 def __eq__(self, other):
69 """Compare the numeric representation of the version."""
70 return self.numeric.__eq__(other.numeric)
71
72 def __le__(self, other):
73 """Compare the numeric representation of the version."""
74 return self.numeric.__le__(other.numeric)
75
76 def __ge__(self, other):
77 """Compare the numeric representation of the version."""
78 return self.numeric.__ge__(other.numeric)
79
80 def __repr__(self):
81 """Print the original text representation of the Version."""
82 return "v{}".format(self.text)
83
84
85class Dependency(object):
86 """Dependency Abstract base class."""
87
88 def __init__(self, line, str_kind):
89 """Save line information.
90
91 :param line: A parsed Line object that contains the raw dependency deceleration.
92 :param str_kind: The determined kind of the Dependency.
93 """
94 # type: (Line, Text) -> object
95 self.line = line
96 self.str_kind = str_kind
97 self.installed_version = None
98
99 def parse(self):
100 """Read the input line and prepare to verify this dependency.
101
102 Raise a MalformedDependencyError if three is something wrong.
103
104 Should return nothing, but get the dependency ready for verification.
105 """
106 raise NotImplementedError()
107
108 def verify(self):
109 # type: () -> bool
110 """Determine if this dependency met.
111
112 :returns: True when the dependency is met, otherwise False.
113 """
114 raise NotImplementedError()
115
116 def inject(self):
117 """If possible, modify the system to meet the dependency."""
118 raise NotImplementedError()
119
120 def verify_and_act(self):
121 """Parse, then verify and trigger pass or fail.
122
123 Extract that out here, so we don't duplicate the logic in the subclasses.
124 """
125 met = self.verify()
126 if met:
127 self.verify_pass()
128 else:
129 self.verify_fail()
130
131 def verify_fail(self):
132 """When dependency is not met, raise an exception.
133
134 This is the default behavior; but I want the subclasses to be able
135 to change it.
136 """
137 raise MissingDependencyError(self, self.installed_version)
138
139 def verify_pass(self):
140 """Print a nice message that the dependency is met.
141
142 I'm not sure we even want to print this, but we will for now. It might
143 be to verbose. Subclasses should override this if wanted.
144 """
145 print("Dependency met", str(self))
146
147
148class MalformedDependency(Exception):
149 """Raised when parsing a dependency directive fails.
150
151 This is situations like the regexes not matching, or part of the dependency directive missing.
152
153 Should probably record more useful stuff, but Exception.message is set. So we can print it later.
154 """
155
156 pass
157
158
159def brew_cmd(command):
Chris Matthewsbcc3c052018-02-01 00:33:15 +0000160 # type: (List[Text]) -> List[Dict[Text, object]]
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000161 """Help run a brew command, and parse the output.
162
163 Brew has a json output option which we use. Run the command and parse the stdout
164 as json and return the result.
165 :param command: The brew command to execute, and parse the output of.
166 :return:
167 """
168 assert "--json=v1" in command, "Must pass JSON arg so we can parse the output."
169 out = subprocess.check_output(command)
170 brew_info = json.loads(out)
171 return brew_info
172
173
174class MissingDependencyError(Exception):
175 """Fail verification with one of these when we determine a dependency is not met.
176
177 For each dependency, we will print a useful message with the dependency line as well as the
178 reason it was not matched.
179 """
180
181 def __init__(self, dependency, installed=None):
182 # type: (Dependency, Optional[Text]) -> None
183 """Raise when a dependency is not met.
184
185 This exception can be printed as the error message.
186
187 :param dependency: The dependency that is not being met.
188 :param installed: what was found to be installed instead.
189 """
190 # type: (Dependency, Union[Text, Version]) -> None
191 super(MissingDependencyError, self).__init__()
192 self.dependency = dependency
193 self.installed = installed
194
195 def __str__(self):
196 """For now, we will just print these as our error message."""
197 return "missing dependency: {}, found {} installed, requested from {}".format(self.dependency,
198 self.installed,
199 self.dependency.line)
200
201
202def check_version(installed, operator, requested):
203 """Check that the installed version does the operator of the requested.
204
205 :param installed: The installed Version of the requirement.
206 :param operator: the text operator (==, <=, >=)
207 :param requested: The requested Version of the requirement.
208 :return: True if the requirement is satisfied.
209 """
210 # type: (Version, Text, Version) -> bool
211
212 dependency_met = False
213 if operator == "==" and installed == requested:
214 dependency_met = True
215 if operator == "<=" and installed <= requested:
216 dependency_met = True
217 if operator == ">=" and installed >= requested:
218 dependency_met = True
219 return dependency_met
220
221
222class ConMan(Dependency):
223 """Version self-check of this tool.
224
225 In case we introduce something in the future, the dep files can
226 be made to depend on a specific version of this tool. We will
227 increment the versions manually.
228
229 """
230
231 conman_re = re.compile(r'(?P<command>\w+)\s+(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
232 """For example: config_manager <= 0.1"""
233
234 def __init__(self, line, kind):
235 """Check that this tool is up to date."""
236 super(ConMan, self).__init__(line, kind)
237 self.command = None
238 self.operator = None
239 self.version = None
240 self.version_text = None
241 self.installed_version = None
242
243 def parse(self):
244 """Parse dependency."""
245 text = self.line.text
246 match = self.conman_re.match(text)
247 if not match:
248 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
249 self.line))
250 self.__dict__.update(match.groupdict())
251
252 self.version = Version(self.version_text)
253
254 def verify(self):
255 """Verify the version of this tool."""
256 self.installed_version = Version(VERSION)
257
258 return check_version(self.installed_version, self.operator, self.version)
259
260 def inject(self):
261 """Can't really do much here."""
262 pass
263
264 def __str__(self):
265 """Show as dependency and version."""
266 return "{} {}".format(self.str_kind, self.version)
267
268
269class HostOSVersion(Dependency):
270 """Use Python's platform module to get host OS information and verify.
271
272 Wew can only verify, but not inject for host OS version.
273 """
274
275 conman_re = re.compile(r'(?P<command>\w+)\s+(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
276
277 def __init__(self, line, kind):
278 """Parse and Verify host OS version using Python's platform module.
279
280 :param line: Line with teh Dependencies deceleration.
281 :param kind: the dependency kind that was detected by the parser.
282 """
283 # type: (Line, Text) -> None
284 super(HostOSVersion, self).__init__(line, kind)
285 self.command = None
286 self.operator = None
287 self.version = None
288 self.version_text = None
289 self.installed_version = None
290
291 def parse(self):
292 """Parse dependency."""
293 text = self.line.text
294 match = self.conman_re.match(text)
295 if not match:
296 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
297 self.line))
298 self.__dict__.update(match.groupdict())
299
300 self.version = Version(self.version_text)
301
302 def verify(self):
303 """Verify the request host OS version holds."""
304 self.installed_version = Version(platform.mac_ver()[0])
305
306 return check_version(self.installed_version, self.operator, self.version)
307
308 def inject(self):
309 """Can't change the host OS version, so not much to do here."""
310 pass
311
312 def __str__(self):
313 """For printing in error messages."""
314 return "{} {}".format(self.str_kind, self.version)
315
316
317class Brew(Dependency):
318 """Verify and Inject brew package dependencies."""
319
320 # brew <package> <operator> <version>. Operator may not have spaces around it.
321 brew_re = re.compile(r'(?P<command>\w+)\s+(?P<package>\w+)\s*(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
322
323 def __init__(self, line, kind):
324 # type: (Line, Text) -> None
325 """Parse and verify brew package is installed.
326
327 :param line: the Line with the deceleration of the dependency.
328 :param kind: the detected dependency kind.
329 """
330 super(Brew, self).__init__(line, kind)
331 self.command = None
332 self.operator = None
333 self.package = None
334 self.version = None
335 self.version_text = None
336 self.installed_version = None
337
338 def parse(self):
339 """Parse this dependency."""
340 text = self.line.text
341 match = self.brew_re.match(text)
342 if not match:
343 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
344 self.line))
345 self.__dict__.update(match.groupdict())
346
347 self.version = Version(self.version_text)
348
349 def verify(self):
350 """Verify the packages in brew match this dependency."""
Chris Matthews25d1da12018-02-01 01:21:23 +0000351 try:
352 brew_package_config = brew_cmd(['/usr/local/bin/brew', 'info', self.package, "--json=v1"])
353 except OSError:
354 raise MissingDependencyError(self, "Can't find brew command")
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000355 version = None
356 for brew_package in brew_package_config:
357 name = brew_package['name']
Chris Matthewscbaf8a82018-02-07 21:33:33 +0000358 linked_keg = brew_package["linked_keg"]
359
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000360 install_info = brew_package.get('installed')
361 for versions in install_info:
Chris Matthewscbaf8a82018-02-07 21:33:33 +0000362 if linked_keg == versions['version']:
363 version = versions['version']
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000364 if name == self.package:
365 break
366 if not version:
367 # The package is not installed at all.
368 raise MissingDependencyError(self, "nothing")
369 self.installed_version = Version(version)
370 return check_version(self.installed_version, self.operator, self.version)
371
372 def inject(self):
373 """Not implemented."""
374 raise NotImplementedError()
375
376 def __str__(self):
377 """Dependency kind, package and version, for printing in error messages."""
378 return "{} {} {}".format(self.str_kind, self.package, self.version)
379
380
Chris Matthewsea57dc42018-01-31 22:37:34 +0000381class Xcode(Dependency):
382 """Verify and Inject Xcode version dependencies."""
383
384 # xcode <operator> <version>. Operator may not have spaces around it.
385 xcode_dep_re = re.compile(r'(?P<command>\w+)\s+(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
386
387 def __init__(self, line, kind):
388 # type: (Line, Text) -> None
389 """Parse and xcode version installed.
390
391 :param line: the Line with the deceleration of the dependency.
392 :param kind: the detected dependency kind.
393 """
394 super(Xcode, self).__init__(line, kind)
395 self.command = None
396 self.operator = None
397 self.version = None
398 self.version_text = None
399 self.installed_version = None
400
401 def parse(self):
402 """Parse this dependency."""
403 text = self.line.text
404 match = self.xcode_dep_re.match(text)
405 if not match:
406 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
407 self.line))
408 self.__dict__.update(match.groupdict())
409
410 self.version = Version(self.version_text)
411
412 def verify(self):
413 """Verify the installed Xcode matches this dependency."""
414 installed_version_output = subprocess.check_output(['/usr/bin/xcrun', 'xcodebuild', "-version"])
415 installed_version_re = re.compile(r"^Xcode\s(?P<version_text>[\d/.]+)")
416 match = installed_version_re.match(installed_version_output)
417 if not match:
418 raise MissingDependencyError(self, "Did not find Xcode version in output:" + installed_version_output)
419 version = match.groupdict().get('version_text')
420 if not version:
421 # The package is not installed at all.
422 raise AssertionError("No version text found.")
423 self.installed_version = Version(version)
424 return check_version(self.installed_version, self.operator, self.version)
425
426 def inject(self):
427 """Not implemented."""
428 raise NotImplementedError()
429
430 def __str__(self):
431 """Dependency kind, package and version, for printing in error messages."""
432 return "{} {}".format(self.str_kind, self.version)
433
434
435class Sdk(Dependency):
436 """Verify and Inject Sdk version dependencies."""
437
438 # sdk <operator> <version>. Operator may not have spaces around it.
Chris Matthewsbcc3c052018-02-01 00:33:15 +0000439 sdk_dep_re = re.compile(r'(?P<command>\w+)\s+(?P<sdk>[\w/.]+)\s*'
440 r'(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
Chris Matthewsea57dc42018-01-31 22:37:34 +0000441
442 def __init__(self, line, kind):
443 # type: (Line, Text) -> None
444 """Parse and sdk version installed.
445
446 :param line: the Line with the deceleration of the dependency.
447 :param kind: the detected dependency kind.
448 """
449 super(Sdk, self).__init__(line, kind)
450 self.command = None
451 self.sdk = None
452 self.operator = None
453 self.version = None
454 self.version_text = None
455 self.installed_version = None
456
457 def parse(self):
458 """Parse this dependency."""
459 text = self.line.text
460 match = self.sdk_dep_re.match(text)
461 if not match:
462 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
463 self.line))
464 self.__dict__.update(match.groupdict())
465
466 self.version = Version(self.version_text)
467
468 def verify(self):
469 """Verify the installed Sdk matches this dependency."""
470 installed_version_output = subprocess.check_output(['/usr/bin/xcrun', 'xcodebuild', "-showsdks"])
471 installed_version_re = re.compile(r".*-sdk\s+(?P<sdk_text>\S+)")
472
473 matches = [installed_version_re.match(l).groupdict()['sdk_text']
474 for l in installed_version_output.split('\n') if installed_version_re.match(l)]
475
476 if not matches:
477 raise MissingDependencyError(self, "Did not find Sdk version in output:" + installed_version_output)
478
479 extract_version_names = re.compile(r'(?P<pre>\D*)(?P<version_text>[\d+/.]*)(?P<post>.*)')
480
481 sdks = [extract_version_names.match(sdk_text).groupdict()
482 for sdk_text in matches if extract_version_names.match(sdk_text)]
483
484 installed_sdks = collections.defaultdict(list)
485 for sdk in sdks:
486 name = sdk['pre']
487 if sdk.get('post'):
488 name += "." + sdk.get('post')
489 if sdk.get('version_text'):
490 version = Version(sdk['version_text'].rstrip('.'))
491 else:
492 continue
493 installed_sdks[name].append(version)
494
495 if self.sdk not in installed_sdks.keys():
Chris Matthewsbcc3c052018-02-01 00:33:15 +0000496 raise MissingDependencyError(self, "{} not found in installed SDKs.".format(self.sdk))
Chris Matthewsea57dc42018-01-31 22:37:34 +0000497
498 self.installed_version = installed_sdks[self.sdk]
499
500 satisfied = [check_version(s, self.operator, self.version) for s in self.installed_version]
501 return any(satisfied)
502
503 def inject(self):
504 """Not implemented."""
505 raise NotImplementedError()
506
507 def __str__(self):
508 """Dependency kind, package and version, for printing in error messages."""
509 return "{} {}".format(self.str_kind, self.version)
510
511
Chris Matthews6daa0802018-02-01 00:33:16 +0000512class Pip(Dependency):
513 """Verify and Inject pip package dependencies."""
514
515 # pip <package> <operator> <version>. Operator may not have spaces around it.
516 pip_re = re.compile(r'(?P<command>\w+)\s+(?P<package>\w+)\s*(?P<operator>>=|<=|==)\s*(?P<version_text>[\d.-_]+)')
517
518 def __init__(self, line, kind):
519 # type: (Line, Text) -> None
520 """Parse and verify pip package is installed.
521
522 :param line: the Line with the deceleration of the dependency.
523 :param kind: the detected dependency kind.
524 """
525 super(Pip, self).__init__(line, kind)
526 self.command = None
527 self.operator = None
528 self.package = None
529 self.version = None
530 self.version_text = None
531 self.installed_version = None
532
533 def parse(self):
534 """Parse this dependency."""
535 text = self.line.text
536 match = self.pip_re.match(text)
537 if not match:
538 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
539 self.line))
540 self.__dict__.update(match.groupdict())
541
542 self.version = Version(self.version_text)
543
544 def verify(self):
545 """Verify the packages in pip match this dependency."""
Chris Matthewsa6b83482018-02-02 21:24:00 +0000546
Chris Matthews6daa0802018-02-01 00:33:16 +0000547 try:
Chris Matthewsa6b83482018-02-02 21:24:00 +0000548 pip_version = subprocess.check_output(["/usr/bin/env", "python", "-m", "pip", "--version"])
549 pip_tokens = pip_version.split()
550 assert pip_tokens[0] == "pip"
551 pip_version = Version(pip_tokens[1])
552
553 if pip_version < Version("9.0.0"):
Chris Matthewsc754fa22018-02-07 23:47:26 +0000554 raise MissingDependencyError(self, "Version of pip too old.")
Chris Matthewsa6b83482018-02-02 21:24:00 +0000555
Chris Matthews6daa0802018-02-01 00:33:16 +0000556 pip_package_config = json.loads(subprocess.check_output(["/usr/bin/env",
557 "python", "-m", "pip", "list", "--format=json"]))
558 except (subprocess.CalledProcessError, OSError):
559 raise MissingDependencyError(self, "Cannot find pip")
560
561 installed = {p['name']: p['version'] for p in pip_package_config} # type: Dict[Text, Text]
562
563 package = installed.get(self.package)
564
565 if not package:
566 # The package is not installed at all.
567 raise MissingDependencyError(self, "not in package list")
568 self.installed_version = Version(package)
569 return check_version(self.installed_version, self.operator, self.version)
570
571 def inject(self):
572 """Not implemented."""
573 raise NotImplementedError()
574
575 def __str__(self):
576 """Dependency kind, package and version, for printing in error messages."""
577 return "{} {} {}".format(self.str_kind, self.package, self.version)
578
579
Chris Matthews803ed022018-06-05 20:43:04 +0000580class Device(Dependency):
581 """Verify correct device is attached to this machine."""
582
583 # device somelongudidstring.
584 # We will filter dashes if they are added.
585 device_re = re.compile(r'(?P<command>\w+)\s+(?P<udid>.*)')
586
587 def __init__(self, line, kind):
588 # type: (Line, Text) -> None
589 """Parse and verify device is attached.
590
591 :param line: the Line with the deceleration of the dependency.
592 :param kind: the detected dependency kind.
593 """
594 super(Device, self).__init__(line, kind)
595 self.command = None
596 self.udid = None
597 self.installed_version = None
598
599 def parse(self):
600 """Parse this dependency."""
601 text = self.line.text
602 match = self.device_re.match(text)
603 if not match:
604 raise MalformedDependency("Expression does not compile in {}: {}".format(self.__class__.__name__,
605 self.line))
606 self.__dict__.update(match.groupdict())
607 # Sometimes people put dashes in these, lets not compare with dashes.
608 self.udid = self.udid.replace("-", "")
609
610 def verify(self):
611 """Verify the device is attached."""
612
613 try:
614 instruments_output = subprocess.check_output(["xcrun", "instruments", "-s", "devices"]).decode("utf-8")
615 except (subprocess.CalledProcessError, OSError):
616 raise MissingDependencyError(self, "Cannot find instruments")
617 # Convert udids with dashes to without for comparison.
618 cleaned_instruments_output = instruments_output.replace(u"-", u"")
619 if self.udid not in cleaned_instruments_output:
620 # The device is not in instruments.
621 raise MissingDependencyError(self, "")
622 return True
623
624 def inject(self):
625 """Not implemented."""
626 raise NotImplementedError()
627
628 def __str__(self):
629 """Dependency kind, package and version, for printing in error messages."""
630 return "{} {} {}".format(self.str_kind, self.udid, "")
631
632
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000633dependencies_implementations = {'brew': Brew,
634 'os_version': HostOSVersion,
Chris Matthewsea57dc42018-01-31 22:37:34 +0000635 'config_manager': ConMan,
636 'xcode': Xcode,
637 'sdk': Sdk,
Chris Matthews6daa0802018-02-01 00:33:16 +0000638 'pip': Pip,
Chris Matthews803ed022018-06-05 20:43:04 +0000639 'device': Device,
Chris Matthewsea57dc42018-01-31 22:37:34 +0000640 }
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000641
642
643def dependency_factory(line):
644 """Given a line, create a concrete dependency for it.
645
646 :param line: The line with the dependency info
647 :return: Some subclass of Dependency, based on what was in the line.
648 """
649 # type: Text -> Dependency
650 kind = line.text.split()[0]
651 try:
652 return dependencies_implementations[kind](line, kind)
653 except KeyError:
654 raise MalformedDependency("Don't know about {} kind of dependency.".format(kind))
655
656
657class Line(object):
658 """A preprocessed line. Understands file and line number as well as comments."""
659
660 def __init__(self, filename, line_number, text, comment):
661 # type: (Text, int, Text, Text) -> None
662 """Raw Line information, split into the dependency deceleration and comment.
663
664 :param filename: the input filename.
665 :param line_number: the line number in the input file.
666 :param text: Non-comment part of the line.
667 :param comment: Text from the comment part of the line if any.
668 """
669 self.filename = filename
670 self.line_number = line_number
671 self.text = text
672 self.comment = comment
673
674 def __repr__(self):
675 """Reconstruct the line for pretty printing."""
Chris Matthews5b8d4b22018-02-03 00:56:41 +0000676 return "{}:{}: {}{}".format(os.path.basename(self.filename),
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000677 self.line_number,
678 self.text,
679 " # " + self.comment if self.comment else "")
680
681
682# For stripping comments out of lines.
683comment_re = re.compile(r'(?P<main_text>.*)#(?P<comment>.*)')
684
685
686# noinspection PyUnresolvedReferences
687def _parse_dep_file(lines, filename):
688 # type: (List[Text], Text) -> List[Line]
689 process_lines = []
690 for num, text in enumerate(lines):
691 if "#" in text:
692 bits = comment_re.match(text)
693 main_text = bits.groupdict().get('main_text')
694 comment = bits.groupdict().get('comment')
695 else:
696 main_text = text
697 comment = None
698 if main_text:
699 main_text = main_text.strip()
700 if comment:
701 comment = comment.strip()
702 process_lines.append(Line(filename, num, main_text, comment))
703
704 return process_lines
705
706
707def parse_dependencies(file_names):
708 """Program logic: read files, verify dependencies.
709
710 For each input file, read lines and create dependencies. Verify each dependency.
711
712 :param file_names: files to read dependencies from.
713 :return: The list of dependencies, each verified.
714 """
715 # type: (List[Text]) -> List[Type[Dependency]]
716 preprocessed_lines = []
717 for file_name in file_names:
718 with open(file_name, 'r') as f:
719 lines = f.readlines()
720 preprocessed_lines.extend(_parse_dep_file(lines, file_name))
721
722 dependencies = [dependency_factory(l) for l in preprocessed_lines if l.text]
723 [d.parse() for d in dependencies]
724 for d in dependencies:
725 try:
726 met = d.verify()
727 if met:
728 d.verify_pass()
729 else:
730 d.verify_fail()
731
732 except MissingDependencyError as exec_info:
733 print("Error:", exec_info)
734
735 return dependencies
736
737
738def main():
739 """Parse arguments and trigger dependency verification."""
740 parser = argparse.ArgumentParser(description='Verify and install dependencies.')
741 parser.add_argument('command', help="What to do.")
742
743 parser.add_argument('dependencies', nargs='+', help="Path to dependency files.")
744
745 args = parser.parse_args()
746
Chris Matthewsbd9d9bb2018-02-06 19:00:40 +0000747 full_file_paths = [os.path.abspath(path) for path in args.dependencies]
748
749 parse_dependencies(full_file_paths)
Chris Matthewsf0d9e882018-01-11 23:55:23 +0000750
751 return True
752
753
754if __name__ == '__main__':
755 main()