public inbox for cygwin-apps-cvs@sourceware.org
help / color / mirror / Atom feed
From: Jon TURNEY <jturney@sourceware.org>
To: cygwin-apps-cvs@sourceware.org
Subject: [calm - Cygwin server-side packaging maintenance script] branch master, updated. 20200220-9-g44c3b47
Date: Mon,  9 Mar 2020 23:23:06 +0000 (GMT)	[thread overview]
Message-ID: <20200309232306.6D504395307B@sourceware.org> (raw)




https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=44c3b473d6365332a6a388f9c6da94e30e9c7dea

commit 44c3b473d6365332a6a388f9c6da94e30e9c7dea
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Mar 9 21:40:41 2020 +0000

    Don't include hostname in email subject if it's uninteresting

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=4bdb8295f8c92128dbc65405eb71341866a0cd5d

commit 4bdb8295f8c92128dbc65405eb71341866a0cd5d
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Mar 2 12:09:50 2020 +0000

    Avoid an exception if no previous .ini file exists

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=4fd321e9367aac6d49f8e3d344a1eab560c0fcf7

commit 4fd321e9367aac6d49f8e3d344a1eab560c0fcf7
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Thu Feb 27 12:11:34 2020 +0000

    Add option to specify gpg key(s) to use for signing setup.ini
    
    If the option is absent, gpg will use it's default (the first key found
    in the secret keyring, in the absence of specific configuration).

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=77d127441c4ee2c3174be1a79e3f0f8d1523dab7

commit 77d127441c4ee2c3174be1a79e3f0f8d1523dab7
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Sun Mar 1 20:03:21 2020 +0000

    Log output from compression and signing subprocesses

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=9b473bd66e10194e0d59782362b1a7ba90bee083

commit 9b473bd66e10194e0d59782362b1a7ba90bee083
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Sun Mar 1 19:58:21 2020 +0000

    Just compute the compressed filename once

https://sourceware.org/git/gitweb.cgi?p=cygwin-apps/calm.git;h=f411dd352a92d728f50626e4af5132a54bd6bf94

commit f411dd352a92d728f50626e4af5132a54bd6bf94
Author: Jon Turney <jon.turney@dronecode.org.uk>
Date:   Mon Feb 24 17:49:38 2020 +0000

    Make --force work more sensibly when daemonized
    
    If daemonized, --force should force regeneration of static content in
    htdocs initially (in case the generation code has changed), but that
    static content should only by updated as needed on subsequent loops.
    
    Use open_amifc() so that mtimes aren't changed by forced regeneration of
    static content, if it hasn't actually changed.
    
    (This also atomically updates that content, just in case someone happens
    to read it while it's being updated)
    
    Regenerating package listing pages is expensive (since we have to read
    all of every tar archive to do so), so only do that with '--force --force'.


Diff:
---
 calm/calm.py     | 28 ++++++++++++++++++++-------
 calm/pkg2html.py | 34 +++++++++++++++-----------------
 calm/utils.py    | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 96 insertions(+), 25 deletions(-)

diff --git a/calm/calm.py b/calm/calm.py
index dd3e184..3fb0e62 100755
--- a/calm/calm.py
+++ b/calm/calm.py
@@ -385,6 +385,10 @@ def do_output(args, state):
     # update packages listings
     # XXX: perhaps we need a --[no]listing command line option to disable this from being run?
     pkg2html.update_package_listings(args, state.packages)
+    # if we are daemonized, allow force regeneration of static content in htdocs
+    # initially (in case the generation code has changed), but update that
+    # static content only as needed on subsequent loops
+    args.force = 0
 
     update_json = False
 
@@ -434,7 +438,8 @@ def do_output(args, state):
                     os.remove(tmpfile.name)
                 else:
                     # make a backup of the current setup.ini
-                    shutil.copy2(inifile, inifile + '.bak')
+                    if os.path.exists(inifile):
+                        shutil.copy2(inifile, inifile + '.bak')
 
                     # replace setup.ini
                     logging.info("moving %s to %s" % (tmpfile.name, inifile))
@@ -443,17 +448,19 @@ def do_output(args, state):
 
                     # compress and re-sign
                     for ext in ['.ini', '.bz2', '.xz']:
+                        extfile = os.path.join(basedir, 'setup' + ext)
                         try:
-                            os.remove(os.path.join(basedir, 'setup' + ext + '.sig'))
+                            os.remove(extfile + '.sig')
                         except FileNotFoundError:
                             pass
 
                         if ext == '.bz2':
-                            os.system('/usr/bin/bzip2 <%s >%s' % (inifile, os.path.splitext(inifile)[0] + ext))
+                            utils.system('/usr/bin/bzip2 <%s >%s' % (inifile, extfile))
                         elif ext == '.xz':
-                            os.system('/usr/bin/xz -6e <%s >%s' % (inifile, os.path.splitext(inifile)[0] + ext))
+                            utils.system('/usr/bin/xz -6e <%s >%s' % (inifile, extfile))
 
-                        os.system('/usr/bin/gpg --batch --yes -b </dev/null ' + os.path.join(basedir, 'setup' + ext))
+                        keys = ' '.join(['-u' + k for k in args.keys])
+                        utils.system('/usr/bin/gpg ' + keys + ' --batch --yes -b ' + extfile)
 
                     # arrange for checksums to be recomputed
                     for sumfile in ['md5.sum', 'sha512.sum']:
@@ -658,9 +665,10 @@ def main():
     parser = argparse.ArgumentParser(description='Upset replacement')
     parser.add_argument('-d', '--daemon', action='store', nargs='?', const=pidfile_default, help="daemonize (PIDFILE defaults to " + pidfile_default + ")", metavar='PIDFILE')
     parser.add_argument('--email', action='store', dest='email', nargs='?', const=common_constants.EMAILS, help="email output to maintainer and ADDRS (ADDRS defaults to '" + common_constants.EMAILS + "')", metavar='ADDRS')
-    parser.add_argument('--force', action='store_true', help="overwrite existing files")
+    parser.add_argument('--force', action='count', help="force regeneration of static htdocs content", default=0)
     parser.add_argument('--homedir', action='store', metavar='DIR', help="maintainer home directory (default: " + homedir_default + ")", default=homedir_default)
     parser.add_argument('--htdocs', action='store', metavar='DIR', help="htdocs output directory (default: " + htdocs_default + ")", default=htdocs_default)
+    parser.add_argument('--key', action='append', metavar='KEYID', help="key to use to sign setup.ini", default=[], dest='keys')
     parser.add_argument('--logdir', action='store', metavar='DIR', help="log directory (default: '" + logdir_default + "')", default=logdir_default)
     parser.add_argument('--orphanmaint', action='store', metavar='NAMES', help="orphan package maintainers (default: '" + orphanmaint_default + "')", default=orphanmaint_default)
     parser.add_argument('--pkglist', action='store', metavar='FILE', help="package maintainer list (default: " + pkglist_default + ")", default=pkglist_default)
@@ -679,7 +687,13 @@ def main():
         args.email = args.email.split(',')
 
     state = CalmState()
-    state.subject = 'calm%s: cygwin package upload report from %s' % (' [dry-run]' if args.dryrun else '', os.uname()[1])
+
+    host = os.uname()[1]
+    if 'sourceware.org' not in host:
+        host = ' from ' + host
+    else:
+        host = ''
+    state.subject = 'calm%s: cygwin package upload report%s' % (' [dry-run]' if args.dryrun else '', host)
 
     status = 0
     if args.daemon:
diff --git a/calm/pkg2html.py b/calm/pkg2html.py
index 110f03a..b6c5490 100755
--- a/calm/pkg2html.py
+++ b/calm/pkg2html.py
@@ -163,9 +163,8 @@ def update_package_listings(args, packages):
         # if listing files were added or removed, or it doesn't already exist,
         # or force, update the summary
         if p in update_summary or not os.path.exists(summary) or args.force:
-            logging.debug('writing %s' % summary)
             if not args.dryrun:
-                with open(summary, 'w') as f:
+                with utils.open_amifc(summary) as f:
                     os.fchmod(f.fileno(), 0o755)
 
                     pos = arch_packages(packages, p)
@@ -305,9 +304,17 @@ def update_package_listings(args, packages):
 #
 def write_packages_inc(args, packages, name, kind, includer):
     packages_inc = os.path.join(args.htdocs, name)
-    logging.debug('writing %s' % packages_inc)
     if not args.dryrun:
-        with open(packages_inc, 'w') as index:
+
+        def touch_including(changed):
+            if changed:
+                # touch the including file for the benefit of 'XBitHack full'
+                package_list = os.path.join(args.htdocs, includer)
+                if os.path.exists(package_list):
+                    logging.info("touching %s for the benefit of 'XBitHack full'" % (package_list))
+                    utils.touch(package_list)
+
+        with utils.open_amifc(packages_inc, cb=touch_including) as index:
             os.fchmod(index.fileno(), 0o644)
 
             # This list contains all packages in any arch. Source packages
@@ -371,11 +378,6 @@ def write_packages_inc(args, packages, name, kind, includer):
 
             print('</table>', file=index)
 
-        # touch the including file for the benefit of 'XBitHack full'
-        package_list = os.path.join(args.htdocs, includer)
-        if os.path.exists(package_list):
-            utils.touch(package_list)
-
 
 def write_arch_listing(args, packages, arch):
     update_summary = set()
@@ -393,9 +395,8 @@ def write_arch_listing(args, packages, arch):
 
     htaccess = os.path.join(base, '.htaccess')
     if not os.path.exists(htaccess) or args.force:
-        logging.debug('writing %s' % htaccess)
         if not args.dryrun:
-            with open(htaccess, 'w') as f:
+            with utils.open_amifc(htaccess) as f:
 
                 print('Redirect temp /packages/%s/index.html https://cygwin.com/packages/package_list.html' % (arch),
                       file=f)
@@ -413,9 +414,8 @@ def write_arch_listing(args, packages, arch):
 
         htaccess = os.path.join(dir, '.htaccess')
         if not os.path.exists(htaccess):
-            logging.debug('writing %s' % htaccess)
             if not args.dryrun or args.force:
-                with open(htaccess, 'w') as f:
+                with utils.open_amifc(htaccess) as f:
                     # We used to allow access to the directory listing as a
                     # crude way of listing the versions of the package available
                     # for which file lists were available. Redirect that index
@@ -444,16 +444,14 @@ def write_arch_listing(args, packages, arch):
             fver = re.sub(r'\.tar.*$', '', tn)
             listing = os.path.join(dir, fver)
 
-            # ... if it doesn't already exist, or force
-            if not os.path.exists(listing) or args.force:
-
-                logging.debug('writing %s' % listing)
+            # ... if it doesn't already exist, or --force --force
+            if not os.path.exists(listing) or (args.force > 1):
 
                 if not args.dryrun:
                     # versions are being added, so summary needs updating
                     update_summary.add(p)
 
-                    with open(listing, 'w') as f:
+                    with utils.open_amifc(listing) as f:
                         bv = packages[p].best_version
                         header = p + ": " + sdesc(packages[p], bv)
 
diff --git a/calm/utils.py b/calm/utils.py
index d6d6e1c..14bd517 100644
--- a/calm/utils.py
+++ b/calm/utils.py
@@ -25,8 +25,12 @@
 # utility functions
 #
 
+import filecmp
 import logging
 import os
+import subprocess
+
+from contextlib import contextmanager
 
 
 #
@@ -51,3 +55,58 @@ def makedirs(name):
         os.makedirs(name, exist_ok=True)
     except FileExistsError:
         pass
+
+
+#
+# a wrapper for open() which:
+#
+# - atomically changes the file contents (atomic)
+# - only touches the mtime if the file contents have changed (move-if-changed)
+#
+@contextmanager
+def open_amifc(filepath, mode='w', cb=None):
+    tmppath = filepath + '~'
+    while os.path.isfile(tmppath):
+        tmppath += '~'
+
+    try:
+        with open(tmppath, mode) as file:
+            logging.debug('writing %s for move-if-changed' % (tmppath))
+            yield file
+
+        changed = not os.path.exists(filepath) or not filecmp.cmp(tmppath, filepath, shallow=False)
+        if changed:
+            logging.info("writing %s" % (filepath))
+            os.rename(tmppath, filepath)
+        else:
+            logging.debug("unchanged %s" % (filepath))
+    finally:
+        try:
+            os.remove(tmppath)
+        except OSError:
+            pass
+
+    # notify callback if file was changed or not
+    if cb:
+        cb(changed)
+
+
+#
+# run a subprocess, logging it's output
+#
+# N.B. because we use shell=True, args should be a string to be supplied to 'sh
+# -c', not a list.
+#
+def system(args):
+    logging.debug(args)
+    try:
+        output = subprocess.check_output(args, shell=True,
+                                         stdin=subprocess.DEVNULL,
+                                         stderr=subprocess.STDOUT)
+    except subprocess.CalledProcessError as e:
+        for l in e.output.decode().splitlines():
+            logging.warning(l)
+        logging.warning('%s exited %d' % (args.split()[0], e.returncode))
+    else:
+        for l in output.decode().splitlines():
+            logging.info(l)



                 reply	other threads:[~2020-03-09 23:23 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20200309232306.6D504395307B@sourceware.org \
    --to=jturney@sourceware.org \
    --cc=cygwin-apps-cvs@sourceware.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).