commands: add --update option to spack list

- Add a `--update FILE` option to `spack list`
- Output is written to the file only if any package is newer than the file
- Simplify the code in docs/conf.py using this new option
This commit is contained in:
Todd Gamblin 2019-05-20 14:25:03 -07:00
parent 6380f1917a
commit 3340d586c4
5 changed files with 171 additions and 96 deletions

View file

@ -54,8 +54,8 @@
os.environ['COLUMNS'] = '120' os.environ['COLUMNS'] = '120'
# Generate full package list if needed # Generate full package list if needed
subprocess.Popen( subprocess.call([
['spack', 'list', '--format=html', '--update=package_list.html']) 'spack', 'list', '--format=html', '--update=package_list.html'])
# Generate a command index if an update is needed # Generate a command index if an update is needed
subprocess.call([ subprocess.call([

View file

@ -9,6 +9,7 @@
import argparse import argparse
import cgi import cgi
import fnmatch import fnmatch
import os
import re import re
import sys import sys
import math import math
@ -46,6 +47,9 @@ def setup_parser(subparser):
subparser.add_argument( subparser.add_argument(
'--format', default='name_only', choices=formatters, '--format', default='name_only', choices=formatters,
help='format to be used to print the output [default: name_only]') help='format to be used to print the output [default: name_only]')
subparser.add_argument(
'--update', metavar='FILE', default=None, action='store',
help='write output to the specified file, if any package is newer')
arguments.add_common_arguments(subparser, ['tags']) arguments.add_common_arguments(subparser, ['tags'])
@ -90,11 +94,11 @@ def match(p, f):
@formatter @formatter
def name_only(pkgs): def name_only(pkgs, out):
indent = 0 indent = 0
if sys.stdout.isatty(): if out.isatty():
tty.msg("%d packages." % len(pkgs)) tty.msg("%d packages." % len(pkgs))
colify(pkgs, indent=indent) colify(pkgs, indent=indent, output=out)
def github_url(pkg): def github_url(pkg):
@ -123,64 +127,69 @@ def rows_for_ncols(elts, ncols):
@formatter @formatter
def rst(pkg_names): def rst(pkg_names, out):
"""Print out information on all packages in restructured text.""" """Print out information on all packages in restructured text."""
pkgs = [spack.repo.get(name) for name in pkg_names] pkgs = [spack.repo.get(name) for name in pkg_names]
print('.. _package-list:') out.write('.. _package-list:\n')
print() out.write('\n')
print('============') out.write('============\n')
print('Package List') out.write('Package List\n')
print('============') out.write('============\n')
print() out.write('\n')
print('This is a list of things you can install using Spack. It is') out.write('This is a list of things you can install using Spack. It is\n')
print('automatically generated based on the packages in the latest Spack') out.write(
print('release.') 'automatically generated based on the packages in the latest Spack\n')
print() out.write('release.\n')
print('Spack currently has %d mainline packages:' % len(pkgs)) out.write('\n')
print() out.write('Spack currently has %d mainline packages:\n' % len(pkgs))
print(rst_table('`%s`_' % p for p in pkg_names)) out.write('\n')
print() out.write(rst_table('`%s`_' % p for p in pkg_names))
out.write('\n')
out.write('\n')
# Output some text for each package. # Output some text for each package.
for pkg in pkgs: for pkg in pkgs:
print('-----') out.write('-----\n')
print() out.write('\n')
print('.. _%s:' % pkg.name) out.write('.. _%s:\n' % pkg.name)
print() out.write('\n')
# Must be at least 2 long, breaks for single letter packages like R. # Must be at least 2 long, breaks for single letter packages like R.
print('-' * max(len(pkg.name), 2)) out.write('-' * max(len(pkg.name), 2))
print(pkg.name) out.write('\n')
print('-' * max(len(pkg.name), 2)) out.write(pkg.name)
print() out.write('\n')
print('Homepage:') out.write('-' * max(len(pkg.name), 2))
print(' * `%s <%s>`__' % (cgi.escape(pkg.homepage), pkg.homepage)) out.write('\n\n')
print() out.write('Homepage:\n')
print('Spack package:') out.write(
print(' * `%s/package.py <%s>`__' % (pkg.name, github_url(pkg))) ' * `%s <%s>`__\n' % (cgi.escape(pkg.homepage), pkg.homepage))
print() out.write('\n')
out.write('Spack package:\n')
out.write(' * `%s/package.py <%s>`__\n' % (pkg.name, github_url(pkg)))
out.write('\n')
if pkg.versions: if pkg.versions:
print('Versions:') out.write('Versions:\n')
print(' ' + ', '.join(str(v) for v in out.write(' ' + ', '.join(str(v) for v in
reversed(sorted(pkg.versions)))) reversed(sorted(pkg.versions))))
print() out.write('\n\n')
for deptype in spack.dependency.all_deptypes: for deptype in spack.dependency.all_deptypes:
deps = pkg.dependencies_of_type(deptype) deps = pkg.dependencies_of_type(deptype)
if deps: if deps:
print('%s Dependencies' % deptype.capitalize()) out.write('%s Dependencies\n' % deptype.capitalize())
print(' ' + ', '.join('%s_' % d if d in pkg_names out.write(' ' + ', '.join('%s_' % d if d in pkg_names
else d for d in deps)) else d for d in deps))
print() out.write('\n\n')
print('Description:') out.write('Description:\n')
print(pkg.format_doc(indent=2)) out.write(pkg.format_doc(indent=2))
print() out.write('\n\n')
@formatter @formatter
def html(pkg_names): def html(pkg_names, out):
"""Print out information on all packages in Sphinx HTML. """Print out information on all packages in Sphinx HTML.
This is intended to be inlined directly into Sphinx documentation. This is intended to be inlined directly into Sphinx documentation.
@ -199,83 +208,90 @@ def html(pkg_names):
def head(n, span_id, title, anchor=None): def head(n, span_id, title, anchor=None):
if anchor is None: if anchor is None:
anchor = title anchor = title
print(('<span id="id%d"></span>' out.write(('<span id="id%d"></span>'
'<h1>%s<a class="headerlink" href="#%s" ' '<h1>%s<a class="headerlink" href="#%s" '
'title="Permalink to this headline">&para;</a>' 'title="Permalink to this headline">&para;</a>'
'</h1>') % (span_id, title, anchor)) '</h1>\n') % (span_id, title, anchor))
# Start with the number of packages, skipping the title and intro # Start with the number of packages, skipping the title and intro
# blurb, which we maintain in the RST file. # blurb, which we maintain in the RST file.
print('<p>') out.write('<p>\n')
print('Spack currently has %d mainline packages:' % len(pkgs)) out.write('Spack currently has %d mainline packages:\n' % len(pkgs))
print('</p>') out.write('</p>\n')
# Table of links to all packages # Table of links to all packages
print('<table border="1" class="docutils">') out.write('<table border="1" class="docutils">\n')
print('<tbody valign="top">') out.write('<tbody valign="top">\n')
for i, row in enumerate(rows_for_ncols(pkg_names, 3)): for i, row in enumerate(rows_for_ncols(pkg_names, 3)):
print('<tr class="row-odd">' if i % 2 == 0 else out.write('<tr class="row-odd">\n' if i % 2 == 0 else
'<tr class="row-even">') '<tr class="row-even">\n')
for name in row: for name in row:
print('<td>') out.write('<td>\n')
print('<a class="reference internal" href="#%s">%s</a></td>' out.write('<a class="reference internal" href="#%s">%s</a></td>\n'
% (name, name)) % (name, name))
print('</td>') out.write('</td>\n')
print('</tr>') out.write('</tr>\n')
print('</tbody>') out.write('</tbody>\n')
print('</table>') out.write('</table>\n')
print('<hr class="docutils"/>') out.write('<hr class="docutils"/>\n')
# Output some text for each package. # Output some text for each package.
for pkg in pkgs: for pkg in pkgs:
print('<div class="section" id="%s">' % pkg.name) out.write('<div class="section" id="%s">\n' % pkg.name)
head(2, span_id, pkg.name) head(2, span_id, pkg.name)
span_id += 1 span_id += 1
print('<dl class="docutils">') out.write('<dl class="docutils">\n')
print('<dt>Homepage:</dt>') out.write('<dt>Homepage:</dt>\n')
print('<dd><ul class="first last simple">') out.write('<dd><ul class="first last simple">\n')
print(('<li>' out.write(('<li>'
'<a class="reference external" href="%s">%s</a>' '<a class="reference external" href="%s">%s</a>'
'</li>') % (pkg.homepage, cgi.escape(pkg.homepage))) '</li>\n') % (pkg.homepage, cgi.escape(pkg.homepage)))
print('</ul></dd>') out.write('</ul></dd>\n')
print('<dt>Spack package:</dt>') out.write('<dt>Spack package:</dt>\n')
print('<dd><ul class="first last simple">') out.write('<dd><ul class="first last simple">\n')
print(('<li>' out.write(('<li>'
'<a class="reference external" href="%s">%s/package.py</a>' '<a class="reference external" href="%s">%s/package.py</a>'
'</li>') % (github_url(pkg), pkg.name)) '</li>\n') % (github_url(pkg), pkg.name))
print('</ul></dd>') out.write('</ul></dd>\n')
if pkg.versions: if pkg.versions:
print('<dt>Versions:</dt>') out.write('<dt>Versions:</dt>\n')
print('<dd>') out.write('<dd>\n')
print(', '.join(str(v) for v in reversed(sorted(pkg.versions)))) out.write(', '.join(
print('</dd>') str(v) for v in reversed(sorted(pkg.versions))))
out.write('\n')
out.write('</dd>\n')
for deptype in spack.dependency.all_deptypes: for deptype in spack.dependency.all_deptypes:
deps = pkg.dependencies_of_type(deptype) deps = pkg.dependencies_of_type(deptype)
if deps: if deps:
print('<dt>%s Dependencies:</dt>' % deptype.capitalize()) out.write('<dt>%s Dependencies:</dt>\n' % deptype.capitalize())
print('<dd>') out.write('<dd>\n')
print(', '.join( out.write(', '.join(
d if d not in pkg_names else d if d not in pkg_names else
'<a class="reference internal" href="#%s">%s</a>' % (d, d) '<a class="reference internal" href="#%s">%s</a>' % (d, d)
for d in deps)) for d in deps))
print('</dd>') out.write('\n')
out.write('</dd>\n')
print('<dt>Description:</dt>') out.write('<dt>Description:</dt>\n')
print('<dd>') out.write('<dd>\n')
print(cgi.escape(pkg.format_doc(indent=2))) out.write(cgi.escape(pkg.format_doc(indent=2)))
print('</dd>') out.write('\n')
print('</dl>') out.write('</dd>\n')
out.write('</dl>\n')
print('<hr class="docutils"/>') out.write('<hr class="docutils"/>\n')
print('</div>') out.write('</div>\n')
def list(parser, args): def list(parser, args):
# retrieve the formatter to use from args
formatter = formatters[args.format]
# Retrieve the names of all the packages # Retrieve the names of all the packages
pkgs = set(spack.repo.all_package_names()) pkgs = set(spack.repo.all_package_names())
# Filter the set appropriately # Filter the set appropriately
@ -288,5 +304,17 @@ def list(parser, args):
sorted_packages = set(sorted_packages) & packages_with_tags sorted_packages = set(sorted_packages) & packages_with_tags
sorted_packages = sorted(sorted_packages) sorted_packages = sorted(sorted_packages)
if args.update:
# change output stream if user asked for update
if os.path.exists(args.update):
if os.path.getmtime(args.update) > spack.repo.path.last_mtime():
tty.msg('File is up to date: %s' % args.update)
return
tty.msg('Updating file: %s' % args.update)
with open(args.update, 'w') as f:
formatter(sorted_packages, f)
else:
# Print to stdout # Print to stdout
formatters[args.format](sorted_packages) formatter(sorted_packages, sys.stdout)

View file

@ -183,6 +183,10 @@ def _create_new_cache(self):
return cache return cache
def last_mtime(self):
return max(
sinfo.st_mtime for sinfo in self._packages_to_stats.values())
def __getitem__(self, item): def __getitem__(self, item):
return self._packages_to_stats[item] return self._packages_to_stats[item]
@ -607,6 +611,10 @@ def load_module(self, fullname):
sys.modules[fullname] = module sys.modules[fullname] = module
return module return module
def last_mtime(self):
"""Time a package file in this repo was last updated."""
return max(repo.last_mtime() for repo in self.repos)
def repo_for_pkg(self, spec): def repo_for_pkg(self, spec):
"""Given a spec, get the repository for its package.""" """Given a spec, get the repository for its package."""
# We don't @_autospec this function b/c it's called very frequently # We don't @_autospec this function b/c it's called very frequently
@ -1018,6 +1026,10 @@ def exists(self, pkg_name):
"""Whether a package with the supplied name exists.""" """Whether a package with the supplied name exists."""
return pkg_name in self._pkg_checker return pkg_name in self._pkg_checker
def last_mtime(self):
"""Time a package file in this repo was last updated."""
return self._pkg_checker.last_mtime()
def is_virtual(self, pkg_name): def is_virtual(self, pkg_name):
"""True if the package with this name is virtual, False otherwise.""" """True if the package with this name is virtual, False otherwise."""
return self.provider_index.contains(pkg_name) return self.provider_index.contains(pkg_name)

View file

@ -59,3 +59,30 @@ def test_list_format_html():
assert '<div class="section" id="hdf5">' in output assert '<div class="section" id="hdf5">' in output
assert '<h1>hdf5' in output assert '<h1>hdf5' in output
def test_list_update(tmpdir):
update_file = tmpdir.join('output')
# not yet created when list is run
list('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read()
# created but older than any package
with update_file.open('w') as f:
f.write('empty\n')
update_file.setmtime(0)
list('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read() != 'empty\n'
# newer than any packages
with update_file.open('w') as f:
f.write('empty\n')
list('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read() == 'empty\n'

View file

@ -3,6 +3,7 @@
# #
# SPDX-License-Identifier: (Apache-2.0 OR MIT) # SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import pytest import pytest
import spack.repo import spack.repo
@ -56,3 +57,10 @@ def test_repo_pkg_with_unknown_namespace(repo_for_test):
def test_repo_unknown_pkg(repo_for_test): def test_repo_unknown_pkg(repo_for_test):
with pytest.raises(spack.repo.UnknownPackageError): with pytest.raises(spack.repo.UnknownPackageError):
repo_for_test.get('builtin.mock.nonexistentpackage') repo_for_test.get('builtin.mock.nonexistentpackage')
@pytest.mark.maybeslow
def test_repo_last_mtime():
latest_mtime = max(os.path.getmtime(p.module.__file__)
for p in spack.repo.path.all_packages())
assert spack.repo.path.last_mtime() == latest_mtime