Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add spack-python script to output diff in tree format #138

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
180 changes: 180 additions & 0 deletions lib/scripts/altdiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import spack.cmd
import spack.environment as ev
import spack.traverse as traverse
from llnl.util.tty.color import cwrite

import argparse
import sys


def diff_specs(spec_a, spec_b, truncate=False):
def highlight(element):
cwrite("@R{%s}" % str(element))

def _write(s):
print(s, end="")

def _variant_str(v):
if isinstance(v, spack.variant.BoolValuedVariant):
return str(v)
else:
return " " + str(v)

class VariantsComparator:
def __init__(self, spec, truncate):
self.variants = spec.variants
self.truncate = truncate

def compare(self, other_spec):
if not self.variants:
return
for k, v in self.variants.items():
if k not in other_spec.variants:
highlight(_variant_str(v))
elif not v.satisfies(other_spec.variants[k]):
highlight(_variant_str(v))
elif not self.truncate:
_write(_variant_str(v))
# else: there is no difference and truncate=True

class VersionComparator:
def __init__(self, spec, truncate):
self.version = spec.version
self.truncate = truncate

def compare(self, other_spec):
other_version = other_spec.version

if not self.version.satisfies(other_version):
highlight(f"@{self.version}")
elif not self.truncate:
_write(f"@{self.version}")

class CompilerComparator:
def __init__(self, spec, truncate):
self.compiler = spec.compiler
self.truncate = truncate

def compare(self, other_spec):
other_cmp = other_spec.compiler
if self.compiler.name != other_cmp.name:
highlight(f"%{self.compiler}")
else:
if not self.compiler.version.satisfies(other_cmp.version):
_write(f"%{self.compiler.name}")
highlight("@{self.compiler.version}")
elif not self.truncate:
_write(f"%{self.compiler}")

class DepsComparator:
def __init__(self, spec, newline_cb):
self.spec = spec
self.newline_cb = newline_cb

def compare(self, other_spec):
self_deps = set(x.name for x in self.spec.dependencies())
other_deps = set(x.name for x in other_spec.dependencies())
extra = list(sorted(self_deps - other_deps))
if extra:
self.newline_cb()
highlight(f"-> [{' '.join(extra)}]")

class ArchComparator:
def __init__(self, spec, truncate):
self.arch = spec.architecture
self.truncate = truncate

def _component_wise_diff(self, x, y, separator):
pairs = list(zip(x, y))
size = len(pairs)
for i, (xi, yi) in enumerate(pairs):
if xi == yi:
_write(xi)
else:
highlight(f"{xi}/{yi}")
# I want to put `separator` "between" components. Since I'm
# writing as I go, I need to handle this fenceposting issue
# manually
if i < size - 1:
_write(separator)

def compare(self, other_spec):
this = [self.arch.platform, self.arch.os, str(self.arch.target)]
other_arch = other_spec.architecture
other = [other_arch.platform, other_arch.os, str(other_arch.target)]
if this != other:
_write(" arch=")
self._component_wise_diff(this, other, "-")
elif not self.truncate:
_write(f" arch={self.arch}")

class NewlineWithDepthIndent:
def __init__(self):
self.depth = 0

def __call__(self):
print()
_write(" " * self.depth)

nl_cb = NewlineWithDepthIndent()

def decompose(spec):
return [
VersionComparator(spec, truncate),
CompilerComparator(spec, truncate),
VariantsComparator(spec, truncate),
ArchComparator(spec, truncate),
DepsComparator(spec, nl_cb),
]

for depth, dep_spec in traverse.traverse_tree(
[spec_a], deptype=("link", "run"), depth_first=True
):
indent = " " * depth
nl_cb.depth = depth
node = dep_spec.spec
_write(indent)
if node.name in spec_b:
_write(node.name)

comparators = decompose(node)
for c in comparators:
c.compare(spec_b[node.name])
else:
highlight(node.name)
print() # New line


def main():
env = ev.active_environment()

parser = argparse.ArgumentParser(description="diff two specs")

parser.add_argument(
"-t",
"--truncate",
action="store_true",
help="don't show most details unless they are different",
)

parser.add_argument("specs", nargs=argparse.REMAINDER, help="two specs to compare")

args = parser.parse_args()

specs = []
for spec in spack.cmd.parse_specs(args.specs):
# If the spec has a hash, check it before disambiguating
spec.replace_hash()
if spec.concrete:
specs.append(spec)
else:
specs.append(spack.cmd.disambiguate_spec(spec, env))

if len(specs) != 2:
raise Exception("Need two specs")

diff_specs(specs[0], specs[1], truncate=args.truncate)


if __name__ == "__main__":
main()
Loading