diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-01-30 09:59:20 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-01-30 09:59:20 +0000 |
commit | d54b4a40e8151e0bac395d94222085f264da25f8 (patch) | |
tree | 285939851c69cdafb9368ca23ff7c5497f069630 | |
parent | 84527e4bc62e69800cedbcca85452173173c4d29 (diff) | |
parent | 857e86ccd32a427cce5dd9df6402fd1d458c5c7a (diff) | |
download | ewww-d54b4a40e8151e0bac395d94222085f264da25f8.tar.gz |
Merge branch 'debian' into 'main'
Update stuff, fix stuff, add Debian packaging
See merge request larswirzenius/ewww!14
-rw-r--r-- | debian/cargo-checksum.json | 0 | ||||
-rw-r--r-- | debian/changelog | 6 | ||||
-rw-r--r-- | debian/compat | 2 | ||||
-rw-r--r-- | debian/control | 26 | ||||
-rw-r--r-- | debian/copyright | 23 | ||||
-rwxr-xr-x | debian/rules | 15 | ||||
-rw-r--r-- | debian/source/format | 1 | ||||
-rw-r--r-- | ewww.md | 16 | ||||
-rw-r--r-- | subplot/ewww.py | 42 | ||||
-rw-r--r-- | subplot/ewww.yaml | 8 | ||||
-rw-r--r-- | subplot/vendored/daemon.py | 256 | ||||
-rw-r--r-- | subplot/vendored/daemon.yaml | 37 | ||||
-rw-r--r-- | subplot/vendored/files.py | 158 | ||||
-rw-r--r-- | subplot/vendored/files.yaml | 62 | ||||
-rw-r--r-- | subplot/vendored/runcmd.py (renamed from subplot/runcmd.py) | 9 | ||||
-rw-r--r-- | subplot/vendored/runcmd.yaml (renamed from subplot/runcmd.yaml) | 0 |
16 files changed, 621 insertions, 40 deletions
diff --git a/debian/cargo-checksum.json b/debian/cargo-checksum.json new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/debian/cargo-checksum.json diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..fa45612 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6 @@ +ewww (0.1-1) unstable; urgency=low + + * Initial packaging. This is not intended to be uploaded to Debian, so + no closing of an ITP bug. + + -- Lars Wirzenius <liw@liw.fi> Sat, 28 Sep 2019 16:45:49 +0300 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..021ea30 --- /dev/null +++ b/debian/compat @@ -0,0 +1,2 @@ +10 + diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..91b0975 --- /dev/null +++ b/debian/control @@ -0,0 +1,26 @@ +Source: ewww +Maintainer: Lars Wirzenius <liw@liw.fi> +Section: www +Priority: optional +Standards-Version: 4.2.0 +Build-Depends: + debhelper (>= 10~), + build-essential, + dh-cargo, + daemonize, + git, + pkg-config, + python3, + python3-requests, + subplot, + texlive-fonts-recommended, + texlive-latex-base, + texlive-latex-recommended +Homepage: https://obnam.org + +Package: ewww +Architecture: any +Depends: ${misc:Depends}, ${shlibs:Depends} +Built-Using: ${cargo:Built-Using} +Description: simplistic web server for static content + Ewww serves static web content. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..89db566 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,23 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ewww +Upstream-Contact: Lars Wirzenius <liw@liw.fi> +Source: http://git.liw.fi/ewww + +Files: * +Copyright: 2020, Lars Wirzenius +License: GPL-3+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + . + On a Debian system, you can find a copy of GPL version 3 at + /usr/share/common-licenses/GPL-3 . diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..9385a1b --- /dev/null +++ b/debian/rules @@ -0,0 +1,15 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem cargo + +override_dh_auto_build: + true + +override_dh_auto_install: + cargo install --path=. --root=debian/ewww + rm -f debian/ewww/.crates.toml + rm -f debian/ewww/.crates2.json + +override_dh_auto_test: + ./check diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) @@ -92,7 +92,8 @@ for static content only. Every other method returns an error. ~~~scenario given a self-signed certificate as snakeoil.pem, using key snakeoil.key -and file webroot/foo.html with "this is your web page" +and directory webroot +and file webroot/foo.html from webpage.html and a running server using config file smoke.yaml when I request GET https://example.com/foo.html then I get status code 200 @@ -110,6 +111,10 @@ tls_cert: snakeoil.pem tls_key: snakeoil.key ~~~ +~~~{#webpage.html .file add-newline=no} +this is your web page +~~~ + ## Performance test ~~~scenario-disabled @@ -146,12 +151,15 @@ author: Lars Wirzenius template: python bindings: - subplot/ewww.yaml - - subplot/runcmd.yaml + - subplot/vendored/daemon.yaml + - subplot/vendored/files.yaml + - subplot/vendored/runcmd.yaml functions: - subplot/ewww.py - - subplot/daemon.py - subplot/http.py - - subplot/runcmd.py + - subplot/vendored/daemon.py + - subplot/vendored/files.py + - subplot/vendored/runcmd.py classes: - scenario-disabled ... diff --git a/subplot/ewww.py b/subplot/ewww.py index 43e5946..f1e02e3 100644 --- a/subplot/ewww.py +++ b/subplot/ewww.py @@ -5,8 +5,6 @@ import logging import os import random import shutil -import socket -import time import urllib.parse import yaml @@ -42,12 +40,11 @@ def fixme(*args, **kwargs): assert 0 -# Create a file. -def create_file(ctx, filename=None, content=None): - logging.debug(f"Creating file {filename} with {content}") - dirname = os.path.dirname(filename) - os.makedirs(dirname) - _write(filename, content) +# Create a directory. +def create_directory(ctx, dirname=None): + dirname = "./" + dirname + if not os.path.exists(dirname): + os.makedirs(dirname) # Copy test certificate from source tree, where it's been created previously by @@ -62,7 +59,8 @@ def copy_test_certificate(ctx, cert=None, key=None): # Start server using named configuration file. def start_server(ctx, filename=None): get_file = globals()["get_file"] - start_daemon = globals()["start_daemon"] + daemon_start_on_port = globals()["daemon_start_on_port"] + logging.debug(f"Starting ewww with config file {filename}") config = get_file(filename).decode("UTF-8") config = yaml.safe_load(config) @@ -72,32 +70,16 @@ def start_server(ctx, filename=None): config = yaml.safe_dump(config) _write(filename, config) - start_daemon(ctx, "ewww", [_binary("ewww"), filename]) - - if not port_open("localhost", port, 5.0): - stderr = open(ctx["daemon"]["ewww"]["stderr"]).read() - logging.debug(f"Stderr from daemon: {stderr!r}") - - -# Wait for a port to be open -def port_open(host, port, timeout): - logging.debug(f"Waiting for port localhost:{port} to be available") - started = time.time() - while time.time() < started + timeout: - try: - socket.create_connection((host, port), timeout=timeout) - return True - except socket.error: - pass - logging.error(f"Port localhost:{port} is not open") - return False + daemon_start_on_port( + ctx, path=_binary("ewww"), args=filename, name="ewww", port=port + ) # Stop previously started server. def stop_server(ctx): - stop_daemon = globals()["stop_daemon"] + daemon_stop = globals()["daemon_stop"] logging.debug("Stopping ewww") - stop_daemon(ctx, "ewww") + daemon_stop(ctx, name="ewww") # Make an HTTP request. diff --git a/subplot/ewww.yaml b/subplot/ewww.yaml index 375558d..7353863 100644 --- a/subplot/ewww.yaml +++ b/subplot/ewww.yaml @@ -5,12 +5,8 @@ function: start_server cleanup: stop_server -- given: "{count} files in {dirname}" - function: fixme - -- given: file (?P<filename>\S+) with "(?P<content>.*)" - regex: true - function: create_file +- given: directory {dirname} + function: create_directory - then: I am redirected to {location} function: fixme diff --git a/subplot/vendored/daemon.py b/subplot/vendored/daemon.py new file mode 100644 index 0000000..d436b4f --- /dev/null +++ b/subplot/vendored/daemon.py @@ -0,0 +1,256 @@ +import logging +import os +import signal +import socket +import subprocess +import time + + +# A helper function for testing lib/daemon itself. +def _daemon_shell_script(ctx, filename=None): + get_file = globals()["get_file"] + data = get_file(filename) + with open(filename, "wb") as f: + f.write(data) + os.chmod(filename, 0o755) + + +# Start a daemon that will open a port on localhost. +def daemon_start_on_port(ctx, path=None, args=None, name=None, port=None): + _daemon_start(ctx, path=path, args=args, name=name) + daemon_wait_for_port("localhost", port) + + +# Start a daemon after a little wait. This is used only for testing the +# port-waiting code. +def _daemon_start_soonish(ctx, path=None, args=None, name=None, port=None): + _daemon_start(ctx, path=os.path.abspath(path), args=args, name=name) + daemon = ctx.declare("_daemon") + + # Store the PID of the process we just started so that _daemon_stop_soonish + # can kill it during the cleanup phase. This works around the Subplot + # Python template not giving the step captures to cleanup functions. Note + # that this code assume at most one _soonish function is called. + daemon["_soonish"] = daemon[name]["pid"] + + try: + daemon_wait_for_port("localhost", port) + except Exception as e: + daemon["_start_error"] = repr(e) + + logging.info("pgrep: %r", _daemon_pgrep(path)) + + +def _daemon_stop_soonish(ctx, path=None, args=None, name=None, port=None): + ns = ctx.declare("_daemon") + pid = ns["_soonish"] + logging.debug(f"Stopping soonishly-started daemon, {pid}") + signo = signal.SIGKILL + try: + os.kill(pid, signo) + except ProcessLookupError: + logging.warning("Process did not actually exist (anymore?)") + + +# Start a daeamon, get its PID. Don't wait for a port or anything. This is +# meant for background processes that don't have port. Useful for testing the +# lib/daemon library of Subplot, but not much else. +def _daemon_start(ctx, path=None, args=None, name=None): + runcmd_run = globals()["runcmd_run"] + runcmd_exit_code_is = globals()["runcmd_exit_code_is"] + runcmd_get_exit_code = globals()["runcmd_get_exit_code"] + runcmd_get_stderr = globals()["runcmd_get_stderr"] + runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"] + + path = os.path.abspath(path) + argv = [path] + args.split() + + logging.debug(f"Starting daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" name={name}") + logging.debug(f" path={path}") + logging.debug(f" args={args}") + logging.debug(f" argv={argv}") + + ns = ctx.declare("_daemon") + + this = ns[name] = { + "pid-file": f"{name}.pid", + "stderr": f"{name}.stderr", + "stdout": f"{name}.stdout", + } + + # Debian installs `daemonize` to /usr/sbin, which isn't part of the minimal + # environment that Subplot sets up. So we add /usr/sbin to the PATH. + runcmd_prepend_to_path(ctx, "/usr/sbin") + runcmd_run( + ctx, + [ + "daemonize", + "-c", + os.getcwd(), + "-p", + this["pid-file"], + "-e", + this["stderr"], + "-o", + this["stdout"], + ] + + argv, + ) + + # Check that daemonize has exited OK. If it hasn't, it didn't start the + # background process at all. If so, log the stderr in case there was + # something useful there for debugging. + exit = runcmd_get_exit_code(ctx) + if exit != 0: + stderr = runcmd_get_stderr(ctx) + logging.error(f"daemon {name} stderr: {stderr}") + runcmd_exit_code_is(ctx, 0) + + # Get the pid of the background process, from the pid file created by + # daemonize. We don't need to wait for it, since we know daemonize already + # exited. If it isn't there now, it's won't appear later. + if not os.path.exists(this["pid-file"]): + raise Exception("daemonize didn't create a PID file") + + this["pid"] = _daemon_wait_for_pid(this["pid-file"], 10.0) + + logging.debug(f"Started daemon {name}") + logging.debug(f" pid={this['pid']}") + logging.debug(f" ctx={ctx.as_dict()}") + + +def _daemon_wait_for_pid(filename, timeout): + start = time.time() + while time.time() < start + timeout: + with open(filename) as f: + data = f.read().strip() + if data: + return int(data) + raise Exception("daemonize created a PID file without a PID") + + +def daemon_wait_for_port(host, port, timeout=5.0): + addr = (host, port) + until = time.time() + timeout + while True: + try: + s = socket.create_connection(addr, timeout=timeout) + s.close() + return + except socket.timeout: + logging.error(f"daemon did not respond at port {port} within {timeout} seconds") + raise + except socket.error as e: + logging.info(f"could not connect to daemon at {port}: {e}") + pass + if time.time() >= until: + logging.error(f"could not connect to daemon at {port} within {timeout} seconds") + raise ConnectionRefusedError() + # Sleep a bit to avoid consuming too much CPU while busy-waiting. + time.sleep(0.1) + + +# Stop a daemon. +def daemon_stop(ctx, path=None, args=None, name=None): + logging.debug(f"Stopping daemon {name}") + + ns = ctx.declare("_daemon") + logging.debug(f" ns={ns}") + pid = ns[name]["pid"] + signo = signal.SIGTERM + + this = ns[name] + data = open(this["stdout"]).read() + logging.debug(f"{name} stdout, before: {data!r}") + data = open(this["stderr"]).read() + logging.debug(f"{name} stderr, before: {data!r}") + + logging.debug(f"Terminating process {pid} with signal {signo}") + try: + os.kill(pid, signo) + except ProcessLookupError: + logging.warning("Process did not actually exist (anymore?)") + + while True: + try: + os.kill(pid, 0) + logging.debug(f"Daemon {name}, pid {pid} still exists") + time.sleep(1) + except ProcessLookupError: + break + logging.debug(f"Daemon {name} is gone") + + data = open(this["stdout"]).read() + logging.debug(f"{name} stdout, after: {data!r}") + data = open(this["stderr"]).read() + logging.debug(f"{name} stderr, after: {data!r}") + + +def daemon_no_such_process(ctx, args=None): + assert not _daemon_pgrep(args) + + +def daemon_process_exists(ctx, args=None): + assert _daemon_pgrep(args) + + +def _daemon_pgrep(pattern): + logging.info(f"checking if process exists: pattern={pattern}") + exit = subprocess.call(["pgrep", "-laf", pattern], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logging.info(f"exit code: {exit}") + return exit == 0 + + +def daemon_start_fails_with(ctx, message=None): + daemon = ctx.declare("_daemon") + error = daemon["_start_error"] + logging.debug(f"daemon_start_fails_with: error={error!r}") + logging.debug(f"daemon_start_fails_with: message={message!r}") + assert message.lower() in error.lower() + + +def daemon_get_stdout(ctx, name): + return _daemon_get_output(ctx, name, "stdout") + + +def daemon_get_stderr(ctx, name): + return _daemon_get_output(ctx, name, "stderr") + + +def _daemon_get_output(ctx, name, which): + ns = ctx.declare("_daemon") + this = ns[name] + filename = this[which] + data = open(filename).read() + logging.debug(f"Read {which} of daemon {name} from {filename}: {data!r}") + return data + + +def daemon_has_produced_output(ctx, name=None): + started = time.time() + timeout = 5.0 + while time.time() < started + timeout: + stdout = daemon_get_stdout(ctx, name) + stderr = daemon_get_stderr(ctx, name) + if stdout and stderr: + break + time.sleep(0.1) + + +def daemon_stdout_is(ctx, name=None, text=None): + daemon_get_stdout = globals()["daemon_get_stdout"] + _daemon_output_is(ctx, name, text, daemon_get_stdout) + + +def daemon_stderr_is(ctx, name=None, text=None): + daemon_get_stderr = globals()["daemon_get_stderr"] + _daemon_output_is(ctx, name, text, daemon_get_stderr) + + +def _daemon_output_is(ctx, name, text, getter): + assert_eq = globals()["assert_eq"] + text = bytes(text, "UTF-8").decode("unicode_escape") + output = getter(ctx, name) + assert_eq(text, output) diff --git a/subplot/vendored/daemon.yaml b/subplot/vendored/daemon.yaml new file mode 100644 index 0000000..4fab1f6 --- /dev/null +++ b/subplot/vendored/daemon.yaml @@ -0,0 +1,37 @@ +- given: there is no "{args:text}" process + function: daemon_no_such_process + +- given: a daemon helper shell script {filename} + function: _daemon_shell_script + +- when: I start "{path}{args:text}" as a background process as {name}, on port {port} + function: daemon_start_on_port + +- when: I try to start "{path}{args:text}" as {name}, on port {port} + function: _daemon_start_soonish + cleanup: _daemon_stop_soonish + +- when: I start "{path}{args:text}" as a background process as {name} + function: _daemon_start + +- when: I stop background process {name} + function: daemon_stop + +- when: daemon {name} has produced output + function: daemon_has_produced_output + +- then: a process "{args:text}" is running + function: daemon_process_exists + +- then: there is no "{args:text}" process + function: daemon_no_such_process + +- then: starting daemon fails with "{message:text}" + function: daemon_start_fails_with + +- then: daemon {name} stdout is "{text:text}" + function: daemon_stdout_is + +- then: daemon {name} stderr is "{text:text}" + function: daemon_stderr_is + diff --git a/subplot/vendored/files.py b/subplot/vendored/files.py new file mode 100644 index 0000000..ec37b9d --- /dev/null +++ b/subplot/vendored/files.py @@ -0,0 +1,158 @@ +import logging +import os +import re +import time + + +def files_create_from_embedded(ctx, filename=None): + files_create_from_embedded_with_other_name( + ctx, filename_on_disk=filename, embedded_filename=filename + ) + + +def files_create_from_embedded_with_other_name( + ctx, filename_on_disk=None, embedded_filename=None +): + get_file = globals()["get_file"] + with open(filename_on_disk, "wb") as f: + f.write(get_file(embedded_filename)) + + +def files_create_from_text(ctx, filename=None, text=None): + with open(filename, "w") as f: + f.write(text) + + +def files_file_exists(ctx, filename=None): + assert_eq = globals()["assert_eq"] + assert_eq(os.path.exists(filename), True) + + +def files_file_does_not_exist(ctx, filename=None): + assert_eq = globals()["assert_eq"] + assert_eq(os.path.exists(filename), False) + + +def files_only_these_exist(ctx, filenames=None): + assert_eq = globals()["assert_eq"] + filenames = filenames.replace(",", "").split() + assert_eq(set(os.listdir(".")), set(filenames)) + + +def files_file_contains(ctx, filename=None, data=None): + assert_eq = globals()["assert_eq"] + with open(filename, "rb") as f: + actual = f.read() + actual = actual.decode("UTF-8") + assert_eq(data in actual, True) + + +def files_file_matches_regex(ctx, filename=None, regex=None): + assert_eq = globals()["assert_eq"] + with open(filename) as f: + content = f.read() + m = re.search(regex, content) + if m is None: + logging.debug(f"files_file_matches_regex: no match") + logging.debug(f" filenamed: {filename}") + logging.debug(f" regex: {regex}") + logging.debug(f" content: {regex}") + logging.debug(f" match: {m}") + assert_eq(bool(m), True) + + +def files_match(ctx, filename1=None, filename2=None): + assert_eq = globals()["assert_eq"] + with open(filename1, "rb") as f: + data1 = f.read() + with open(filename2, "rb") as f: + data2 = f.read() + assert_eq(data1, data2) + + +def files_touch_with_timestamp( + ctx, + filename=None, + year=None, + month=None, + day=None, + hour=None, + minute=None, + second=None, +): + t = ( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), + -1, + -1, + -1, + ) + ts = time.mktime(t) + _files_touch(filename, ts) + + +def files_touch(ctx, filename=None): + _files_touch(filename, None) + + +def _files_touch(filename, ts): + if not os.path.exists(filename): + open(filename, "w").close() + times = None + if ts is not None: + times = (ts, ts) + os.utime(filename, times=times) + + +def files_mtime_is_recent(ctx, filename=None): + st = os.stat(filename) + age = abs(st.st_mtime - time.time()) + assert age < 1.0 + + +def files_mtime_is_ancient(ctx, filename=None): + st = os.stat(filename) + age = abs(st.st_mtime - time.time()) + year = 365 * 24 * 60 * 60 + required = 39 * year + logging.debug(f"ancient? mtime={st.st_mtime} age={age} required={required}") + assert age > required + + +def files_remember_metadata(ctx, filename=None): + meta = _files_remembered(ctx) + meta[filename] = _files_get_metadata(filename) + logging.debug("files_remember_metadata:") + logging.debug(f" meta: {meta}") + logging.debug(f" ctx: {ctx}") + + +# Check that current metadata of a file is as stored in the context. +def files_has_remembered_metadata(ctx, filename=None): + assert_eq = globals()["assert_eq"] + meta = _files_remembered(ctx) + logging.debug("files_has_remembered_metadata:") + logging.debug(f" meta: {meta}") + logging.debug(f" ctx: {ctx}") + assert_eq(meta[filename], _files_get_metadata(filename)) + + +def files_has_different_metadata(ctx, filename=None): + assert_ne = globals()["assert_ne"] + meta = _files_remembered(ctx) + assert_ne(meta[filename], _files_get_metadata(filename)) + + +def _files_remembered(ctx): + ns = ctx.declare("_files") + return ns.get("remembered-metadata", {}) + + +def _files_get_metadata(filename): + st = os.lstat(filename) + keys = ["st_dev", "st_gid", "st_ino", "st_mode", "st_mtime", "st_size", "st_uid"] + return {key: getattr(st, key) for key in keys} diff --git a/subplot/vendored/files.yaml b/subplot/vendored/files.yaml new file mode 100644 index 0000000..be69920 --- /dev/null +++ b/subplot/vendored/files.yaml @@ -0,0 +1,62 @@ +- given: file {filename} + function: files_create_from_embedded + types: + filename: file + +- given: file {filename_on_disk} from {embedded_filename} + function: files_create_from_embedded_with_other_name + types: + embedded_filename: file + +- given: file {filename} has modification time {year}-{month}-{day} {hour}:{minute}:{second} + function: files_touch_with_timestamp + +- when: I write "(?P<text>.*)" to file (?P<filename>\S+) + regex: true + function: files_create_from_text + +- when: I remember metadata for file {filename} + function: files_remember_metadata + +- when: I touch file {filename} + function: files_touch + +- then: file {filename} exists + function: files_file_exists + +- then: file {filename} does not exist + function: files_file_does_not_exist + +- then: only files (?P<filenames>.+) exist + function: files_only_these_exist + regex: true + +- then: file (?P<filename>\S+) contains "(?P<data>.*)" + regex: true + function: files_file_contains + +- then: file (?P<filename>\S+) matches regex /(?P<regex>.*)/ + regex: true + function: files_file_matches_regex + +- then: file (?P<filename>\S+) matches regex "(?P<regex>.*)" + regex: true + function: files_file_matches_regex + +- then: files {filename1} and {filename2} match + function: files_match + +- then: file {filename} has same metadata as before + function: files_has_remembered_metadata + +- then: file {filename} has different metadata from before + function: files_has_different_metadata + +- then: file {filename} has changed from before + function: files_has_different_metadata + +- then: file {filename} has a very recent modification time + function: files_mtime_is_recent + +- then: file {filename} has a very old modification time + function: files_mtime_is_ancient diff --git a/subplot/runcmd.py b/subplot/vendored/runcmd.py index 532b60b..a2564c6 100644 --- a/subplot/runcmd.py +++ b/subplot/vendored/runcmd.py @@ -49,7 +49,16 @@ def runcmd_get_argv(ctx): # ctx context. def runcmd_run(ctx, argv, **kwargs): ns = ctx.declare("_runcmd") + + # The Subplot Python template empties os.environ at startup, modulo a small + # number of variables with carefully chosen values. Here, we don't need to + # care about what those variables are, but we do need to not overwrite + # them, so we just add anything in the env keyword argument, if any, to + # os.environ. env = dict(os.environ) + for key, arg in kwargs.pop("env", {}).items(): + env[key] = arg + pp = ns.get("path-prefix") if pp: env["PATH"] = pp + ":" + env["PATH"] diff --git a/subplot/runcmd.yaml b/subplot/vendored/runcmd.yaml index 48dde90..48dde90 100644 --- a/subplot/runcmd.yaml +++ b/subplot/vendored/runcmd.yaml |