From 89e79125a08a7dbc1882ce356270682836ac0ebb Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 16:40:24 -0700 Subject: [PATCH 001/100] Use SimpleNamespace for typed records --- pinch.py | 65 +++++++++++++++++++++++++++++++++++--------------------- test.sh | 2 +- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/pinch.py b/pinch.py index f974d1c..cdc0ba8 100644 --- a/pinch.py +++ b/pinch.py @@ -6,12 +6,12 @@ import os import os.path import shutil import tempfile +import types import urllib.parse import urllib.request import xml.dom.minidom from typing import ( - Any, Dict, Iterable, List, @@ -20,6 +20,22 @@ from typing import ( ) +class InfoTableEntry(types.SimpleNamespace): + content: bytes + digest: str + file: str + size: int + url: str + + +class Info(types.SimpleNamespace): + channel_html: bytes + forwarded_url: str + git_revision: str + table: Dict[str, InfoTableEntry] + url: str + + class VerificationError(Exception): pass @@ -86,74 +102,75 @@ def compare(a: str, return filecmp.cmpfiles(a, b, files, shallow=False) -def fetch(v: Verification, channel_url: str) -> Dict[str, Any]: - info: Dict[str, Any] = {'url': channel_url} +def fetch(v: Verification, channel_url: str) -> Info: + info = Info() + info.url = channel_url v.status('Fetching channel') request = urllib.request.urlopen( 'https://channels.nixos.org/nixos-20.03', timeout=10) - info['channel_html'] = request.read() - info['forwarded_url'] = request.geturl() + info.channel_html = request.read() + info.forwarded_url = request.geturl() v.result(request.status == 200) - v.check('Got forwarded', info['url'] != info['forwarded_url']) + v.check('Got forwarded', info.url != info.forwarded_url) return info -def parse(v: Verification, info: Dict[str, Any]) -> None: +def parse(v: Verification, info: Info) -> None: v.status('Parsing channel description as XML') - d = xml.dom.minidom.parseString(info['channel_html']) + d = xml.dom.minidom.parseString(info.channel_html) v.ok() v.status('Extracting git commit') git_commit_node = d.getElementsByTagName('tt')[0] - info['git_commit'] = git_commit_node.firstChild.nodeValue + info.git_commit = git_commit_node.firstChild.nodeValue v.ok() v.status('Verifying git commit label') v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') v.status('Parsing table') - info['table'] = {} + info.table = {} for row in d.getElementsByTagName('tr')[1:]: name = row.childNodes[0].firstChild.firstChild.nodeValue url = row.childNodes[0].firstChild.getAttribute('href') size = int(row.childNodes[1].firstChild.nodeValue) digest = row.childNodes[2].firstChild.firstChild.nodeValue - info['table'][name] = {'url': url, 'digest': digest, 'size': size} + info.table[name] = InfoTableEntry(url=url, digest=digest, size=size) v.ok() -def fetch_resources(v: Verification, info: Dict[str, Any]) -> None: +def fetch_resources(v: Verification, info: Info) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: - fields = info['table'][resource] + fields = info.table[resource] v.status('Fetching resource "%s"' % resource) - url = urllib.parse.urljoin(info['forwarded_url'], fields['url']) + url = urllib.parse.urljoin(info.forwarded_url, fields.url) request = urllib.request.urlopen(url, timeout=10) - if fields['size'] < 4096: - fields['content'] = request.read() + if fields.size < 4096: + fields.content = request.read() else: with tempfile.NamedTemporaryFile(suffix='.nixexprs.tar.xz', delete=False) as tmp_file: shutil.copyfileobj(request, tmp_file) - fields['file'] = tmp_file.name + fields.file = tmp_file.name v.result(request.status == 200) v.status('Verifying digest for "%s"' % resource) - if fields['size'] < 4096: - actual_hash = hashlib.sha256(fields['content']).hexdigest() + if fields.size < 4096: + actual_hash = hashlib.sha256(fields.content).hexdigest() else: hasher = hashlib.sha256() - with open(fields['file'], 'rb') as f: + with open(fields.file, 'rb') as f: # pylint: disable=cell-var-from-loop for block in iter(lambda: f.read(4096), b''): hasher.update(block) actual_hash = hasher.hexdigest() - v.result(actual_hash == fields['digest']) + v.result(actual_hash == fields.digest) v.check('Verifying git commit on main page matches git commit in table', - info['table']['git-revision']['content'].decode() == info['git_commit']) + info.table['git-revision'].content.decode() == info.git_commit) -def extract_channel(v: Verification, info: Dict[str, Any]) -> None: +def extract_channel(v: Verification, info: Info) -> None: with tempfile.TemporaryDirectory() as d: v.status('Extracting nixexprs.tar.xz') - shutil.unpack_archive(info['table']['nixexprs.tar.xz']['file'], d) + shutil.unpack_archive(info.table['nixexprs.tar.xz'].file, d) v.ok() v.status('Removing temporary directory') v.ok() diff --git a/test.sh b/test.sh index cea63ba..779a65b 100755 --- a/test.sh +++ b/test.sh @@ -8,7 +8,7 @@ find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 -find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d invalid-name,missing-docstring +find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d invalid-name,missing-docstring,too-few-public-methods formatting_needs_fixing=$( find . -name '*.py' -print0 | -- 2.44.1 From 73bec7e8e5b260f38f7a907ad1b5e17457596dbf Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 17:12:03 -0700 Subject: [PATCH 002/100] Fetch with nix-prefetch-url for the caching --- pinch.py | 85 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/pinch.py b/pinch.py index cdc0ba8..dfdedac 100644 --- a/pinch.py +++ b/pinch.py @@ -5,6 +5,7 @@ import operator import os import os.path import shutil +import subprocess import tempfile import types import urllib.parse @@ -15,14 +16,17 @@ from typing import ( Dict, Iterable, List, + NewType, Sequence, Tuple, ) +Digest16 = NewType('Digest16', str) +Digest32 = NewType('Digest32', str) + class InfoTableEntry(types.SimpleNamespace): - content: bytes - digest: str + digest: Digest16 file: str size: int url: str @@ -115,7 +119,7 @@ def fetch(v: Verification, channel_url: str) -> Info: return info -def parse(v: Verification, info: Info) -> None: +def parse_table(v: Verification, info: Info) -> None: v.status('Parsing channel description as XML') d = xml.dom.minidom.parseString(info.channel_html) v.ok() @@ -133,38 +137,63 @@ def parse(v: Verification, info: Info) -> None: name = row.childNodes[0].firstChild.firstChild.nodeValue url = row.childNodes[0].firstChild.getAttribute('href') size = int(row.childNodes[1].firstChild.nodeValue) - digest = row.childNodes[2].firstChild.firstChild.nodeValue + digest = Digest16(row.childNodes[2].firstChild.firstChild.nodeValue) info.table[name] = InfoTableEntry(url=url, digest=digest, size=size) v.ok() -def fetch_resources(v: Verification, info: Info) -> None: +def digest_file(filename: str) -> Digest16: + hasher = hashlib.sha256() + with open(filename, 'rb') as f: + # pylint: disable=cell-var-from-loop + for block in iter(lambda: f.read(4096), b''): + hasher.update(block) + return Digest16(hasher.hexdigest()) + + +def to_Digest16(v: Verification, digest32: Digest32) -> Digest16: + v.status('Converting digest to base16') + process = subprocess.run( + ['nix', 'to-base16', '--type', 'sha256', digest32], capture_output=True) + v.result(process.returncode == 0) + return Digest16(process.stdout.decode().strip()) + + +def to_Digest32(v: Verification, digest16: Digest16) -> Digest32: + v.status('Converting digest to base32') + process = subprocess.run( + ['nix', 'to-base32', '--type', 'sha256', digest16], capture_output=True) + v.result(process.returncode == 0) + return Digest32(process.stdout.decode().strip()) + + +def fetch_with_nix_prefetch_url( + v: Verification, + url: str, + digest: Digest16) -> str: + v.status('Fetching %s' % url) + process = subprocess.run( + ['nix-prefetch-url', '--print-path', url, digest], capture_output=True) + v.result(process.returncode == 0) + prefetch_digest, path, empty = process.stdout.decode().split('\n') + assert empty == '' + v.check("Verifying nix-prefetch-url's digest", + to_Digest16(v, Digest32(prefetch_digest)) == digest) + v.status("Verifying file digest") + file_digest = digest_file(path) + v.result(file_digest == digest) + return path + +def fetch_resources(v: Verification, info: Info) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: fields = info.table[resource] - v.status('Fetching resource "%s"' % resource) url = urllib.parse.urljoin(info.forwarded_url, fields.url) - request = urllib.request.urlopen(url, timeout=10) - if fields.size < 4096: - fields.content = request.read() - else: - with tempfile.NamedTemporaryFile(suffix='.nixexprs.tar.xz', delete=False) as tmp_file: - shutil.copyfileobj(request, tmp_file) - fields.file = tmp_file.name - v.result(request.status == 200) - v.status('Verifying digest for "%s"' % resource) - if fields.size < 4096: - actual_hash = hashlib.sha256(fields.content).hexdigest() - else: - hasher = hashlib.sha256() - with open(fields.file, 'rb') as f: - # pylint: disable=cell-var-from-loop - for block in iter(lambda: f.read(4096), b''): - hasher.update(block) - actual_hash = hasher.hexdigest() - v.result(actual_hash == fields.digest) - v.check('Verifying git commit on main page matches git commit in table', - info.table['git-revision'].content.decode() == info.git_commit) + fields.file = fetch_with_nix_prefetch_url(v, url, fields.digest) + v.status('Verifying git commit on main page matches git commit in table') + v.result( + open( + info.table['git-revision'].file).read(999) == info.git_commit) def extract_channel(v: Verification, info: Info) -> None: @@ -179,7 +208,7 @@ def extract_channel(v: Verification, info: Info) -> None: def main() -> None: v = Verification() info = fetch(v, 'https://channels.nixos.org/nixos-20.03') - parse(v, info) + parse_table(v, info) fetch_resources(v, info) extract_channel(v, info) print(info) -- 2.44.1 From 3e6421c4c43619da3b8b2501f262c86987f6d1cf Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 17:36:38 -0700 Subject: [PATCH 003/100] Extract release name & print out what's been extracted or being worked on --- pinch.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index dfdedac..f22a1c3 100644 --- a/pinch.py +++ b/pinch.py @@ -36,6 +36,7 @@ class Info(types.SimpleNamespace): channel_html: bytes forwarded_url: str git_revision: str + release_name: str table: Dict[str, InfoTableEntry] url: str @@ -124,9 +125,18 @@ def parse_table(v: Verification, info: Info) -> None: d = xml.dom.minidom.parseString(info.channel_html) v.ok() - v.status('Extracting git commit') + v.status('Extracting release name:') + title_name = d.getElementsByTagName( + 'title')[0].firstChild.nodeValue.split()[2] + h1_name = d.getElementsByTagName('h1')[0].firstChild.nodeValue.split()[2] + v.status(title_name) + v.result(title_name == h1_name) + info.release_name = title_name + + v.status('Extracting git commit:') git_commit_node = d.getElementsByTagName('tt')[0] info.git_commit = git_commit_node.firstChild.nodeValue + v.status(info.git_commit) v.ok() v.status('Verifying git commit label') v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') @@ -198,7 +208,7 @@ def fetch_resources(v: Verification, info: Info) -> None: def extract_channel(v: Verification, info: Info) -> None: with tempfile.TemporaryDirectory() as d: - v.status('Extracting nixexprs.tar.xz') + v.status('Extracting %s' % info.table['nixexprs.tar.xz'].file) shutil.unpack_archive(info.table['nixexprs.tar.xz'].file, d) v.ok() v.status('Removing temporary directory') -- 2.44.1 From 2cd3371417115c615b96bd22b50db202b4c6c67a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 17:37:51 -0700 Subject: [PATCH 004/100] Rename top-level procedures --- pinch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pinch.py b/pinch.py index f22a1c3..f0f0422 100644 --- a/pinch.py +++ b/pinch.py @@ -120,7 +120,7 @@ def fetch(v: Verification, channel_url: str) -> Info: return info -def parse_table(v: Verification, info: Info) -> None: +def parse_channel(v: Verification, info: Info) -> None: v.status('Parsing channel description as XML') d = xml.dom.minidom.parseString(info.channel_html) v.ok() @@ -206,7 +206,7 @@ def fetch_resources(v: Verification, info: Info) -> None: info.table['git-revision'].file).read(999) == info.git_commit) -def extract_channel(v: Verification, info: Info) -> None: +def check_channel_contents(v: Verification, info: Info) -> None: with tempfile.TemporaryDirectory() as d: v.status('Extracting %s' % info.table['nixexprs.tar.xz'].file) shutil.unpack_archive(info.table['nixexprs.tar.xz'].file, d) @@ -218,9 +218,9 @@ def extract_channel(v: Verification, info: Info) -> None: def main() -> None: v = Verification() info = fetch(v, 'https://channels.nixos.org/nixos-20.03') - parse_table(v, info) + parse_channel(v, info) fetch_resources(v, info) - extract_channel(v, info) + check_channel_contents(v, info) print(info) -- 2.44.1 From 72d3478ad2577fcb6a8d2ffdaff2beb8e8579dd7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 17:46:09 -0700 Subject: [PATCH 005/100] Rename Info -> Channel --- pinch.py | 58 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pinch.py b/pinch.py index f0f0422..89e053b 100644 --- a/pinch.py +++ b/pinch.py @@ -25,19 +25,19 @@ Digest16 = NewType('Digest16', str) Digest32 = NewType('Digest32', str) -class InfoTableEntry(types.SimpleNamespace): +class ChannelTableEntry(types.SimpleNamespace): digest: Digest16 file: str size: int url: str -class Info(types.SimpleNamespace): +class Channel(types.SimpleNamespace): channel_html: bytes forwarded_url: str git_revision: str release_name: str - table: Dict[str, InfoTableEntry] + table: Dict[str, ChannelTableEntry] url: str @@ -107,22 +107,22 @@ def compare(a: str, return filecmp.cmpfiles(a, b, files, shallow=False) -def fetch(v: Verification, channel_url: str) -> Info: - info = Info() - info.url = channel_url +def fetch(v: Verification, channel_url: str) -> Channel: + channel = Channel() + channel.url = channel_url v.status('Fetching channel') request = urllib.request.urlopen( 'https://channels.nixos.org/nixos-20.03', timeout=10) - info.channel_html = request.read() - info.forwarded_url = request.geturl() + channel.channel_html = request.read() + channel.forwarded_url = request.geturl() v.result(request.status == 200) - v.check('Got forwarded', info.url != info.forwarded_url) - return info + v.check('Got forwarded', channel.url != channel.forwarded_url) + return channel -def parse_channel(v: Verification, info: Info) -> None: +def parse_channel(v: Verification, channel: Channel) -> None: v.status('Parsing channel description as XML') - d = xml.dom.minidom.parseString(info.channel_html) + d = xml.dom.minidom.parseString(channel.channel_html) v.ok() v.status('Extracting release name:') @@ -131,24 +131,24 @@ def parse_channel(v: Verification, info: Info) -> None: h1_name = d.getElementsByTagName('h1')[0].firstChild.nodeValue.split()[2] v.status(title_name) v.result(title_name == h1_name) - info.release_name = title_name + channel.release_name = title_name v.status('Extracting git commit:') git_commit_node = d.getElementsByTagName('tt')[0] - info.git_commit = git_commit_node.firstChild.nodeValue - v.status(info.git_commit) + channel.git_commit = git_commit_node.firstChild.nodeValue + v.status(channel.git_commit) v.ok() v.status('Verifying git commit label') v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') v.status('Parsing table') - info.table = {} + channel.table = {} for row in d.getElementsByTagName('tr')[1:]: name = row.childNodes[0].firstChild.firstChild.nodeValue url = row.childNodes[0].firstChild.getAttribute('href') size = int(row.childNodes[1].firstChild.nodeValue) digest = Digest16(row.childNodes[2].firstChild.firstChild.nodeValue) - info.table[name] = InfoTableEntry(url=url, digest=digest, size=size) + channel.table[name] = ChannelTableEntry(url=url, digest=digest, size=size) v.ok() @@ -195,21 +195,21 @@ def fetch_with_nix_prefetch_url( return path -def fetch_resources(v: Verification, info: Info) -> None: +def fetch_resources(v: Verification, channel: Channel) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: - fields = info.table[resource] - url = urllib.parse.urljoin(info.forwarded_url, fields.url) + fields = channel.table[resource] + url = urllib.parse.urljoin(channel.forwarded_url, fields.url) fields.file = fetch_with_nix_prefetch_url(v, url, fields.digest) v.status('Verifying git commit on main page matches git commit in table') v.result( open( - info.table['git-revision'].file).read(999) == info.git_commit) + channel.table['git-revision'].file).read(999) == channel.git_commit) -def check_channel_contents(v: Verification, info: Info) -> None: +def check_channel_contents(v: Verification, channel: Channel) -> None: with tempfile.TemporaryDirectory() as d: - v.status('Extracting %s' % info.table['nixexprs.tar.xz'].file) - shutil.unpack_archive(info.table['nixexprs.tar.xz'].file, d) + v.status('Extracting %s' % channel.table['nixexprs.tar.xz'].file) + shutil.unpack_archive(channel.table['nixexprs.tar.xz'].file, d) v.ok() v.status('Removing temporary directory') v.ok() @@ -217,11 +217,11 @@ def check_channel_contents(v: Verification, info: Info) -> None: def main() -> None: v = Verification() - info = fetch(v, 'https://channels.nixos.org/nixos-20.03') - parse_channel(v, info) - fetch_resources(v, info) - check_channel_contents(v, info) - print(info) + channel = fetch(v, 'https://channels.nixos.org/nixos-20.03') + parse_channel(v, channel) + fetch_resources(v, channel) + check_channel_contents(v, channel) + print(channel) main() -- 2.44.1 From ca2c3eddd11811b0c494763e3d99614e9f6c62e7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 8 Apr 2020 17:48:32 -0700 Subject: [PATCH 006/100] main creates the Channel --- pinch.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pinch.py b/pinch.py index 89e053b..a8b6103 100644 --- a/pinch.py +++ b/pinch.py @@ -107,17 +107,13 @@ def compare(a: str, return filecmp.cmpfiles(a, b, files, shallow=False) -def fetch(v: Verification, channel_url: str) -> Channel: - channel = Channel() - channel.url = channel_url +def fetch(v: Verification, channel: Channel) -> None: v.status('Fetching channel') - request = urllib.request.urlopen( - 'https://channels.nixos.org/nixos-20.03', timeout=10) + request = urllib.request.urlopen(channel.url, timeout=10) channel.channel_html = request.read() channel.forwarded_url = request.geturl() v.result(request.status == 200) v.check('Got forwarded', channel.url != channel.forwarded_url) - return channel def parse_channel(v: Verification, channel: Channel) -> None: @@ -217,7 +213,8 @@ def check_channel_contents(v: Verification, channel: Channel) -> None: def main() -> None: v = Verification() - channel = fetch(v, 'https://channels.nixos.org/nixos-20.03') + channel = Channel(url='https://channels.nixos.org/nixos-20.03') + fetch(v, channel) parse_channel(v, channel) fetch_resources(v, channel) check_channel_contents(v, channel) -- 2.44.1 From dc038df02de3df877d535f1c978ad7537eaf70a8 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 15:56:45 -0700 Subject: [PATCH 007/100] Compare channel tarball with git --- pinch.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++----- test.sh | 2 +- 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/pinch.py b/pinch.py index a8b6103..e00bfd8 100644 --- a/pinch.py +++ b/pinch.py @@ -17,7 +17,6 @@ from typing import ( Iterable, List, NewType, - Sequence, Tuple, ) @@ -35,6 +34,9 @@ class ChannelTableEntry(types.SimpleNamespace): class Channel(types.SimpleNamespace): channel_html: bytes forwarded_url: str + git_cachedir: str + git_ref: str + git_repo: str git_revision: str release_name: str table: Dict[str, ChannelTableEntry] @@ -76,10 +78,7 @@ class Verification: self.result(True) -def compare(a: str, - b: str) -> Tuple[Sequence[str], - Sequence[str], - Sequence[str]]: +def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: def throw(error: OSError) -> None: raise error @@ -144,10 +143,15 @@ def parse_channel(v: Verification, channel: Channel) -> None: url = row.childNodes[0].firstChild.getAttribute('href') size = int(row.childNodes[1].firstChild.nodeValue) digest = Digest16(row.childNodes[2].firstChild.firstChild.nodeValue) - channel.table[name] = ChannelTableEntry(url=url, digest=digest, size=size) + channel.table[name] = ChannelTableEntry( + url=url, digest=digest, size=size) v.ok() +def digest_string(s: bytes) -> Digest16: + return Digest16(hashlib.sha256(s).hexdigest()) + + def digest_file(filename: str) -> Digest16: hasher = hashlib.sha256() with open(filename, 'rb') as f: @@ -202,21 +206,121 @@ def fetch_resources(v: Verification, channel: Channel) -> None: channel.table['git-revision'].file).read(999) == channel.git_commit) +def git_fetch(v: Verification, channel: Channel) -> None: + # It would be nice if we could share the nix git cache, but as of the time + # of writing it is transitioning from gitv2 (deprecated) to gitv3 (not ready + # yet), and trying to straddle them both is too far into nix implementation + # details for my comfort. So we re-implement here half of nix.fetchGit. + # :( + + # TODO: Consider using pyxdg to find this path. + channel.git_cachedir = os.path.expanduser( + '~/.cache/nix-pin-channel/git/%s' % + digest_string( + channel.url.encode())) + if not os.path.exists(channel.git_cachedir): + v.status("Initializing git repo") + process = subprocess.run( + ['git', 'init', '--bare', channel.git_cachedir]) + v.result(process.returncode == 0) + + v.status('Checking if we already have this rev:') + process = subprocess.run( + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_commit]) + if process.returncode == 0: + v.status('yes') + if process.returncode == 1: + v.status('no') + v.result(process.returncode == 0 or process.returncode == 1) + if process.returncode == 1: + v.status('Fetching ref "%s"' % channel.git_ref) + # We don't use --force here because we want to abort and freak out if forced + # updates are happening. + process = subprocess.run(['git', + '-C', + channel.git_cachedir, + 'fetch', + channel.git_repo, + '%s:%s' % (channel.git_ref, + channel.git_ref)]) + v.result(process.returncode == 0) + v.status('Verifying that fetch retrieved this rev') + process = subprocess.run( + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_commit]) + v.result(process.returncode == 0) + + v.status('Verifying rev is an ancestor of ref') + process = subprocess.run(['git', + '-C', + channel.git_cachedir, + 'merge-base', + '--is-ancestor', + channel.git_commit, + channel.git_ref]) + v.result(process.returncode == 0) + + def check_channel_contents(v: Verification, channel: Channel) -> None: - with tempfile.TemporaryDirectory() as d: - v.status('Extracting %s' % channel.table['nixexprs.tar.xz'].file) - shutil.unpack_archive(channel.table['nixexprs.tar.xz'].file, d) + with tempfile.TemporaryDirectory() as channel_contents, \ + tempfile.TemporaryDirectory() as git_contents: + v.status('Extracting tarball %s' % + channel.table['nixexprs.tar.xz'].file) + shutil.unpack_archive( + channel.table['nixexprs.tar.xz'].file, + channel_contents) + v.ok() + v.status('Checking out corresponding git revision') + git = subprocess.Popen(['git', + '-C', + channel.git_cachedir, + 'archive', + channel.git_commit], + stdout=subprocess.PIPE) + tar = subprocess.Popen( + ['tar', 'x', '-C', git_contents, '-f', '-'], stdin=git.stdout) + git.stdout.close() + tar.wait() + git.wait() + v.result(git.returncode == 0 and tar.returncode == 0) + v.status('Comparing channel tarball with git checkout') + match, mismatch, errors = compare(os.path.join( + channel_contents, channel.release_name), git_contents) v.ok() - v.status('Removing temporary directory') + v.check('%d files match' % len(match), len(match) > 0) + v.check('%d files differ' % len(mismatch), len(mismatch) == 0) + expected_errors = [ + '.git-revision', + '.version-suffix', + 'nixpkgs', + 'programs.sqlite', + 'svn-revision'] + benign_errors = [] + for ee in expected_errors: + if ee in errors: + errors.remove(ee) + benign_errors.append(ee) + v.check( + '%d unexpected incomparable files' % + len(errors), + len(errors) == 0) + v.check( + '(%d of %d expected incomparable files)' % + (len(benign_errors), + len(expected_errors)), + len(benign_errors) == len(expected_errors)) + v.status('Removing temporary directories') v.ok() def main() -> None: v = Verification() - channel = Channel(url='https://channels.nixos.org/nixos-20.03') + channel = Channel(url='https://channels.nixos.org/nixos-20.03', + git_repo='https://github.com/NixOS/nixpkgs.git', + git_ref='nixos-20.03') fetch(v, channel) parse_channel(v, channel) fetch_resources(v, channel) + git_fetch(v, channel) check_channel_contents(v, channel) print(channel) diff --git a/test.sh b/test.sh index 779a65b..edab8d8 100755 --- a/test.sh +++ b/test.sh @@ -8,7 +8,7 @@ find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 -find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d invalid-name,missing-docstring,too-few-public-methods +find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,too-few-public-methods formatting_needs_fixing=$( find . -name '*.py' -print0 | -- 2.44.1 From 925c801b763927b3fd3f9071fa017c638d656fe4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 16:16:33 -0700 Subject: [PATCH 008/100] Break up check_channel_contents --- pinch.py | 109 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/pinch.py b/pinch.py index e00bfd8..1a97f53 100644 --- a/pinch.py +++ b/pinch.py @@ -260,54 +260,73 @@ def git_fetch(v: Verification, channel: Channel) -> None: v.result(process.returncode == 0) +def compare_tarball_and_git( + v: Verification, + channel: Channel, + channel_contents: str, + git_contents: str) -> None: + v.status('Comparing channel tarball with git checkout') + match, mismatch, errors = compare(os.path.join( + channel_contents, channel.release_name), git_contents) + v.ok() + v.check('%d files match' % len(match), len(match) > 0) + v.check('%d files differ' % len(mismatch), len(mismatch) == 0) + expected_errors = [ + '.git-revision', + '.version-suffix', + 'nixpkgs', + 'programs.sqlite', + 'svn-revision'] + benign_errors = [] + for ee in expected_errors: + if ee in errors: + errors.remove(ee) + benign_errors.append(ee) + v.check( + '%d unexpected incomparable files' % + len(errors), + len(errors) == 0) + v.check( + '(%d of %d expected incomparable files)' % + (len(benign_errors), + len(expected_errors)), + len(benign_errors) == len(expected_errors)) + + +def extract_tarball(v: Verification, channel: Channel, dest: str) -> None: + v.status('Extracting tarball %s' % + channel.table['nixexprs.tar.xz'].file) + shutil.unpack_archive( + channel.table['nixexprs.tar.xz'].file, + dest) + v.ok() + + +def git_checkout(v: Verification, channel: Channel, dest: str) -> None: + v.status('Checking out corresponding git revision') + git = subprocess.Popen(['git', + '-C', + channel.git_cachedir, + 'archive', + channel.git_commit], + stdout=subprocess.PIPE) + tar = subprocess.Popen( + ['tar', 'x', '-C', dest, '-f', '-'], stdin=git.stdout) + git.stdout.close() + tar.wait() + git.wait() + v.result(git.returncode == 0 and tar.returncode == 0) + + def check_channel_contents(v: Verification, channel: Channel) -> None: with tempfile.TemporaryDirectory() as channel_contents, \ tempfile.TemporaryDirectory() as git_contents: - v.status('Extracting tarball %s' % - channel.table['nixexprs.tar.xz'].file) - shutil.unpack_archive( - channel.table['nixexprs.tar.xz'].file, - channel_contents) - v.ok() - v.status('Checking out corresponding git revision') - git = subprocess.Popen(['git', - '-C', - channel.git_cachedir, - 'archive', - channel.git_commit], - stdout=subprocess.PIPE) - tar = subprocess.Popen( - ['tar', 'x', '-C', git_contents, '-f', '-'], stdin=git.stdout) - git.stdout.close() - tar.wait() - git.wait() - v.result(git.returncode == 0 and tar.returncode == 0) - v.status('Comparing channel tarball with git checkout') - match, mismatch, errors = compare(os.path.join( - channel_contents, channel.release_name), git_contents) - v.ok() - v.check('%d files match' % len(match), len(match) > 0) - v.check('%d files differ' % len(mismatch), len(mismatch) == 0) - expected_errors = [ - '.git-revision', - '.version-suffix', - 'nixpkgs', - 'programs.sqlite', - 'svn-revision'] - benign_errors = [] - for ee in expected_errors: - if ee in errors: - errors.remove(ee) - benign_errors.append(ee) - v.check( - '%d unexpected incomparable files' % - len(errors), - len(errors) == 0) - v.check( - '(%d of %d expected incomparable files)' % - (len(benign_errors), - len(expected_errors)), - len(benign_errors) == len(expected_errors)) + + extract_tarball(v, channel, channel_contents) + git_checkout(v, channel, git_contents) + + compare_tarball_and_git(v, channel, channel_contents, git_contents) + v.status('Removing temporary directories') v.ok() -- 2.44.1 From f9cd7bdc50fad6c24ac226a6532ecfb81d78cbe2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 16:33:22 -0700 Subject: [PATCH 009/100] Verify channel tarball metadata --- pinch.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pinch.py b/pinch.py index 1a97f53..c77d891 100644 --- a/pinch.py +++ b/pinch.py @@ -318,11 +318,37 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: v.result(git.returncode == 0 and tar.returncode == 0) +def check_channel_metadata( + v: Verification, + channel: Channel, + channel_contents: str) -> None: + v.status('Verifying git commit in channel tarball') + v.result( + open( + os.path.join( + channel_contents, + channel.release_name, + '.git-revision')).read(999) == channel.git_commit) + + v.status( + 'Verifying version-suffix is a suffix of release name %s:' % + channel.release_name) + version_suffix = open( + os.path.join( + channel_contents, + channel.release_name, + '.version-suffix')).read(999) + v.status(version_suffix) + v.result(channel.release_name.endswith(version_suffix)) + + def check_channel_contents(v: Verification, channel: Channel) -> None: with tempfile.TemporaryDirectory() as channel_contents, \ tempfile.TemporaryDirectory() as git_contents: extract_tarball(v, channel, channel_contents) + check_channel_metadata(v, channel, channel_contents) + git_checkout(v, channel, git_contents) compare_tarball_and_git(v, channel, channel_contents, git_contents) -- 2.44.1 From f15e458d6371d1074d0d91cf7cf9a5312a467b80 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 17:48:02 -0700 Subject: [PATCH 010/100] Get configuration from config file --- channels | 4 ++++ pinch.py | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 channels diff --git a/channels b/channels new file mode 100644 index 0000000..d90c172 --- /dev/null +++ b/channels @@ -0,0 +1,4 @@ +[nixos] +url = https://channels.nixos.org/nixos-20.03 +git_repo = https://github.com/NixOS/nixpkgs.git +git_ref = nixos-20.03 diff --git a/pinch.py b/pinch.py index c77d891..1dbd667 100644 --- a/pinch.py +++ b/pinch.py @@ -1,3 +1,4 @@ +import configparser import filecmp import functools import hashlib @@ -6,6 +7,7 @@ import os import os.path import shutil import subprocess +import sys import tempfile import types import urllib.parse @@ -357,11 +359,11 @@ def check_channel_contents(v: Verification, channel: Channel) -> None: v.ok() -def main() -> None: +def main(argv: List[str]) -> None: v = Verification() - channel = Channel(url='https://channels.nixos.org/nixos-20.03', - git_repo='https://github.com/NixOS/nixpkgs.git', - git_ref='nixos-20.03') + config = configparser.ConfigParser() + config.read_file(open(argv[1]), argv[1]) + channel = Channel(**dict(config['nixos'].items())) fetch(v, channel) parse_channel(v, channel) fetch_resources(v, channel) @@ -370,4 +372,4 @@ def main() -> None: print(channel) -main() +main(sys.argv) -- 2.44.1 From 61aaf7990f91c97019b68a12595a167086fcc8e2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 17:48:34 -0700 Subject: [PATCH 011/100] Rename git_commit -> git_revision --- pinch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pinch.py b/pinch.py index 1dbd667..ec6e8b5 100644 --- a/pinch.py +++ b/pinch.py @@ -132,8 +132,8 @@ def parse_channel(v: Verification, channel: Channel) -> None: v.status('Extracting git commit:') git_commit_node = d.getElementsByTagName('tt')[0] - channel.git_commit = git_commit_node.firstChild.nodeValue - v.status(channel.git_commit) + channel.git_revision = git_commit_node.firstChild.nodeValue + v.status(channel.git_revision) v.ok() v.status('Verifying git commit label') v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') @@ -205,7 +205,7 @@ def fetch_resources(v: Verification, channel: Channel) -> None: v.status('Verifying git commit on main page matches git commit in table') v.result( open( - channel.table['git-revision'].file).read(999) == channel.git_commit) + channel.table['git-revision'].file).read(999) == channel.git_revision) def git_fetch(v: Verification, channel: Channel) -> None: @@ -228,7 +228,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: v.status('Checking if we already have this rev:') process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_commit]) + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) if process.returncode == 0: v.status('yes') if process.returncode == 1: @@ -248,7 +248,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: v.result(process.returncode == 0) v.status('Verifying that fetch retrieved this rev') process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_commit]) + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) v.result(process.returncode == 0) v.status('Verifying rev is an ancestor of ref') @@ -257,7 +257,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: channel.git_cachedir, 'merge-base', '--is-ancestor', - channel.git_commit, + channel.git_revision, channel.git_ref]) v.result(process.returncode == 0) @@ -310,7 +310,7 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: '-C', channel.git_cachedir, 'archive', - channel.git_commit], + channel.git_revision], stdout=subprocess.PIPE) tar = subprocess.Popen( ['tar', 'x', '-C', dest, '-f', '-'], stdin=git.stdout) @@ -330,7 +330,7 @@ def check_channel_metadata( os.path.join( channel_contents, channel.release_name, - '.git-revision')).read(999) == channel.git_commit) + '.git-revision')).read(999) == channel.git_revision) v.status( 'Verifying version-suffix is a suffix of release name %s:' % -- 2.44.1 From 5cfa8e119c86e2bf7c315ed1b82a10b7770fcfac Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 17:59:39 -0700 Subject: [PATCH 012/100] Support multiple channels --- pinch.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pinch.py b/pinch.py index ec6e8b5..0eafb90 100644 --- a/pinch.py +++ b/pinch.py @@ -363,13 +363,14 @@ def main(argv: List[str]) -> None: v = Verification() config = configparser.ConfigParser() config.read_file(open(argv[1]), argv[1]) - channel = Channel(**dict(config['nixos'].items())) - fetch(v, channel) - parse_channel(v, channel) - fetch_resources(v, channel) - git_fetch(v, channel) - check_channel_contents(v, channel) - print(channel) + for section in config.sections(): + channel = Channel(**dict(config[section].items())) + fetch(v, channel) + parse_channel(v, channel) + fetch_resources(v, channel) + git_fetch(v, channel) + check_channel_contents(v, channel) + print(channel) main(sys.argv) -- 2.44.1 From e434d96dbe26e90d2e881f052bd0c28b2a04ffc7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 20:21:15 -0700 Subject: [PATCH 013/100] Write back to config file --- pinch.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pinch.py b/pinch.py index 0eafb90..dd70718 100644 --- a/pinch.py +++ b/pinch.py @@ -27,6 +27,7 @@ Digest32 = NewType('Digest32', str) class ChannelTableEntry(types.SimpleNamespace): + absolute_url: str digest: Digest16 file: str size: int @@ -200,8 +201,10 @@ def fetch_with_nix_prefetch_url( def fetch_resources(v: Verification, channel: Channel) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: fields = channel.table[resource] - url = urllib.parse.urljoin(channel.forwarded_url, fields.url) - fields.file = fetch_with_nix_prefetch_url(v, url, fields.digest) + fields.absolute_url = urllib.parse.urljoin( + channel.forwarded_url, fields.url) + fields.file = fetch_with_nix_prefetch_url( + v, fields.absolute_url, fields.digest) v.status('Verifying git commit on main page matches git commit in table') v.result( open( @@ -370,7 +373,11 @@ def main(argv: List[str]) -> None: fetch_resources(v, channel) git_fetch(v, channel) check_channel_contents(v, channel) - print(channel) + config[section]['git_rev'] = channel.git_revision + config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url + config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest + with open(argv[1], 'w') as configfile: + config.write(configfile) main(sys.argv) -- 2.44.1 From 038f00114125f80eb24b500ed4001881b785460a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 20:22:31 -0700 Subject: [PATCH 014/100] .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7f7a42 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.mypy_cache -- 2.44.1 From b17def3f6b3c3f002d1bf7ef7d838110374d015d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 20:35:24 -0700 Subject: [PATCH 015/100] Rename config field: url -> channel_url --- channels | 2 +- pinch.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/channels b/channels index d90c172..3f48dcc 100644 --- a/channels +++ b/channels @@ -1,4 +1,4 @@ [nixos] -url = https://channels.nixos.org/nixos-20.03 +channel_url = https://channels.nixos.org/nixos-20.03 git_repo = https://github.com/NixOS/nixpkgs.git git_ref = nixos-20.03 diff --git a/pinch.py b/pinch.py index dd70718..e3b6882 100644 --- a/pinch.py +++ b/pinch.py @@ -36,6 +36,7 @@ class ChannelTableEntry(types.SimpleNamespace): class Channel(types.SimpleNamespace): channel_html: bytes + channel_url: str forwarded_url: str git_cachedir: str git_ref: str @@ -43,7 +44,6 @@ class Channel(types.SimpleNamespace): git_revision: str release_name: str table: Dict[str, ChannelTableEntry] - url: str class VerificationError(Exception): @@ -111,11 +111,11 @@ def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: def fetch(v: Verification, channel: Channel) -> None: v.status('Fetching channel') - request = urllib.request.urlopen(channel.url, timeout=10) + request = urllib.request.urlopen(channel.channel_url, timeout=10) channel.channel_html = request.read() channel.forwarded_url = request.geturl() v.result(request.status == 200) - v.check('Got forwarded', channel.url != channel.forwarded_url) + v.check('Got forwarded', channel.channel_url != channel.forwarded_url) def parse_channel(v: Verification, channel: Channel) -> None: @@ -222,7 +222,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: channel.git_cachedir = os.path.expanduser( '~/.cache/nix-pin-channel/git/%s' % digest_string( - channel.url.encode())) + channel.git_repo.encode())) if not os.path.exists(channel.git_cachedir): v.status("Initializing git repo") process = subprocess.run( -- 2.44.1 From 8fca6c28975315a042f0544b42cb4a15d8dfa192 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 21:44:49 -0700 Subject: [PATCH 016/100] Support non-hydra, plain ol' git channels --- pinch.py | 76 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/pinch.py b/pinch.py index e3b6882..80e631b 100644 --- a/pinch.py +++ b/pinch.py @@ -42,6 +42,7 @@ class Channel(types.SimpleNamespace): git_ref: str git_repo: str git_revision: str + old_git_revision: str release_name: str table: Dict[str, ChannelTableEntry] @@ -229,16 +230,20 @@ def git_fetch(v: Verification, channel: Channel) -> None: ['git', 'init', '--bare', channel.git_cachedir]) v.result(process.returncode == 0) - v.status('Checking if we already have this rev:') - process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) - if process.returncode == 0: - v.status('yes') - if process.returncode == 1: - v.status('no') - v.result(process.returncode == 0 or process.returncode == 1) - if process.returncode == 1: - v.status('Fetching ref "%s"' % channel.git_ref) + have_rev = False + if hasattr(channel, 'git_revision'): + v.status('Checking if we already have this rev:') + process = subprocess.run( + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) + if process.returncode == 0: + v.status('yes') + if process.returncode == 1: + v.status('no') + v.result(process.returncode == 0 or process.returncode == 1) + have_rev = process.returncode == 0 + + if not have_rev: + v.status('Fetching ref "%s" from %s' % (channel.git_ref, channel.git_repo)) # We don't use --force here because we want to abort and freak out if forced # updates are happening. process = subprocess.run(['git', @@ -249,10 +254,19 @@ def git_fetch(v: Verification, channel: Channel) -> None: '%s:%s' % (channel.git_ref, channel.git_ref)]) v.result(process.returncode == 0) - v.status('Verifying that fetch retrieved this rev') - process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) - v.result(process.returncode == 0) + if hasattr(channel, 'git_revision'): + v.status('Verifying that fetch retrieved this rev') + process = subprocess.run( + ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) + v.result(process.returncode == 0) + + if not hasattr(channel, 'git_revision'): + channel.git_revision = open( + os.path.join( + channel.git_cachedir, + 'refs', + 'heads', + channel.git_ref)).read(999).strip() v.status('Verifying rev is an ancestor of ref') process = subprocess.run(['git', @@ -362,20 +376,36 @@ def check_channel_contents(v: Verification, channel: Channel) -> None: v.ok() +def pin_channel(v: Verification, channel: Channel) -> None: + fetch(v, channel) + parse_channel(v, channel) + fetch_resources(v, channel) + git_fetch(v, channel) + check_channel_contents(v, channel) + + +def make_channel(conf: configparser.SectionProxy) -> Channel: + channel = Channel(**dict(conf.items())) + if hasattr(channel, 'git_revision'): + channel.old_git_revision = channel.git_revision + del channel.git_revision + return channel + + def main(argv: List[str]) -> None: v = Verification() config = configparser.ConfigParser() config.read_file(open(argv[1]), argv[1]) for section in config.sections(): - channel = Channel(**dict(config[section].items())) - fetch(v, channel) - parse_channel(v, channel) - fetch_resources(v, channel) - git_fetch(v, channel) - check_channel_contents(v, channel) - config[section]['git_rev'] = channel.git_revision - config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url - config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest + channel = make_channel(config[section]) + if 'channel_url' in config[section]: + pin_channel(v, channel) + config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url + config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest + else: + git_fetch(v, channel) + config[section]['git_revision'] = channel.git_revision + with open(argv[1], 'w') as configfile: config.write(configfile) -- 2.44.1 From 7d889b120b4b280ea0bf5daaa546dec067c112e9 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 21:44:56 -0700 Subject: [PATCH 017/100] Verify old rev is an ancestor of new rev --- pinch.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pinch.py b/pinch.py index 80e631b..774f5f1 100644 --- a/pinch.py +++ b/pinch.py @@ -278,6 +278,17 @@ def git_fetch(v: Verification, channel: Channel) -> None: channel.git_ref]) v.result(process.returncode == 0) + if hasattr(channel, 'old_git_revision'): + v.status('Verifying rev is an ancestor of previous rev %s' % channel.old_git_revision) + process = subprocess.run(['git', + '-C', + channel.git_cachedir, + 'merge-base', + '--is-ancestor', + channel.old_git_revision, + channel.git_revision]) + v.result(process.returncode == 0) + def compare_tarball_and_git( v: Verification, -- 2.44.1 From 17d42dd500b06196b70af99e05e9d0fffaf3f38e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 22:55:01 -0700 Subject: [PATCH 018/100] Formatting --- pinch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 774f5f1..b3f65c6 100644 --- a/pinch.py +++ b/pinch.py @@ -243,7 +243,9 @@ def git_fetch(v: Verification, channel: Channel) -> None: have_rev = process.returncode == 0 if not have_rev: - v.status('Fetching ref "%s" from %s' % (channel.git_ref, channel.git_repo)) + v.status( + 'Fetching ref "%s" from %s' % + (channel.git_ref, channel.git_repo)) # We don't use --force here because we want to abort and freak out if forced # updates are happening. process = subprocess.run(['git', @@ -279,7 +281,9 @@ def git_fetch(v: Verification, channel: Channel) -> None: v.result(process.returncode == 0) if hasattr(channel, 'old_git_revision'): - v.status('Verifying rev is an ancestor of previous rev %s' % channel.old_git_revision) + v.status( + 'Verifying rev is an ancestor of previous rev %s' % + channel.old_git_revision) process = subprocess.run(['git', '-C', channel.git_cachedir, -- 2.44.1 From 0e5e611d13ab114528838b4ab50b0bf417303877 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 9 Apr 2020 22:57:26 -0700 Subject: [PATCH 019/100] Sub-command --- pinch.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pinch.py b/pinch.py index b3f65c6..71496a3 100644 --- a/pinch.py +++ b/pinch.py @@ -1,3 +1,4 @@ +import argparse import configparser import filecmp import functools @@ -7,7 +8,6 @@ import os import os.path import shutil import subprocess -import sys import tempfile import types import urllib.parse @@ -407,10 +407,10 @@ def make_channel(conf: configparser.SectionProxy) -> Channel: return channel -def main(argv: List[str]) -> None: +def pin(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() - config.read_file(open(argv[1]), argv[1]) + config.read_file(open(args.channels_file), args.channels_file) for section in config.sections(): channel = make_channel(config[section]) if 'channel_url' in config[section]: @@ -421,8 +421,18 @@ def main(argv: List[str]) -> None: git_fetch(v, channel) config[section]['git_revision'] = channel.git_revision - with open(argv[1], 'w') as configfile: + with open(args.channels_file, 'w') as configfile: config.write(configfile) -main(sys.argv) +def main() -> None: + parser = argparse.ArgumentParser(prog='pinch') + subparsers = parser.add_subparsers(dest='mode', required=True) + parser_pin = subparsers.add_parser('pin') + parser_pin.add_argument('channels_file', type=str) + parser_pin.set_defaults(func=pin) + args = parser.parse_args() + args.func(args) + + +main() -- 2.44.1 From ebf95bd304bb9ef75c559be16a2d1176122db644 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 00:10:42 -0700 Subject: [PATCH 020/100] Record release name --- pinch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pinch.py b/pinch.py index 71496a3..6ad2cdc 100644 --- a/pinch.py +++ b/pinch.py @@ -415,6 +415,7 @@ def pin(args: argparse.Namespace) -> None: channel = make_channel(config[section]) if 'channel_url' in config[section]: pin_channel(v, channel) + config[section]['name'] = channel.release_name config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest else: -- 2.44.1 From e3cae769b843054130c54356da96d02ebb8ca895 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 00:45:45 -0700 Subject: [PATCH 021/100] Name git pins --- channels | 5 +++++ pinch.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/channels b/channels index 3f48dcc..dc1f754 100644 --- a/channels +++ b/channels @@ -2,3 +2,8 @@ channel_url = https://channels.nixos.org/nixos-20.03 git_repo = https://github.com/NixOS/nixpkgs.git git_ref = nixos-20.03 + +[nixos-hardware] +git_repo = https://github.com/NixOS/nixos-hardware.git +git_ref = master + diff --git a/pinch.py b/pinch.py index 6ad2cdc..ae2c7ec 100644 --- a/pinch.py +++ b/pinch.py @@ -399,6 +399,22 @@ def pin_channel(v: Verification, channel: Channel) -> None: check_channel_contents(v, channel) +def git_revision_name(v: Verification, channel: Channel) -> str: + v.status('Getting commit date') + process = subprocess.run(['git', + '-C', + channel.git_cachedir, + 'lo', + '-n1', + '--format=%ct-%h', + '--abbrev=11', + channel.git_revision], + capture_output=True) + v.result(process.returncode == 0 and process.stdout != '') + return '%s-%s' % (os.path.basename(channel.git_repo), + process.stdout.decode().strip()) + + def make_channel(conf: configparser.SectionProxy) -> Channel: channel = Channel(**dict(conf.items())) if hasattr(channel, 'git_revision'): @@ -420,6 +436,7 @@ def pin(args: argparse.Namespace) -> None: config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest else: git_fetch(v, channel) + config[section]['name'] = git_revision_name(v, channel) config[section]['git_revision'] = channel.git_revision with open(args.channels_file, 'w') as configfile: -- 2.44.1 From 9836141c70e29986f4f2664ad6fb1670eac5f40f Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 13:38:23 -0700 Subject: [PATCH 022/100] git_cachedir without Channel --- pinch.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pinch.py b/pinch.py index ae2c7ec..3e725e1 100644 --- a/pinch.py +++ b/pinch.py @@ -38,7 +38,6 @@ class Channel(types.SimpleNamespace): channel_html: bytes channel_url: str forwarded_url: str - git_cachedir: str git_ref: str git_repo: str git_revision: str @@ -211,6 +210,10 @@ def fetch_resources(v: Verification, channel: Channel) -> None: open( channel.table['git-revision'].file).read(999) == channel.git_revision) +def git_cachedir(git_repo: str) -> str: + # TODO: Consider using pyxdg to find this path. + return os.path.expanduser('~/.cache/nix-pin-channel/git/%s' % digest_string(git_repo.encode())) + def git_fetch(v: Verification, channel: Channel) -> None: # It would be nice if we could share the nix git cache, but as of the time @@ -219,22 +222,18 @@ def git_fetch(v: Verification, channel: Channel) -> None: # details for my comfort. So we re-implement here half of nix.fetchGit. # :( - # TODO: Consider using pyxdg to find this path. - channel.git_cachedir = os.path.expanduser( - '~/.cache/nix-pin-channel/git/%s' % - digest_string( - channel.git_repo.encode())) - if not os.path.exists(channel.git_cachedir): + cachedir = git_cachedir(channel.git_repo) + if not os.path.exists(cachedir): v.status("Initializing git repo") process = subprocess.run( - ['git', 'init', '--bare', channel.git_cachedir]) + ['git', 'init', '--bare', cachedir]) v.result(process.returncode == 0) have_rev = False if hasattr(channel, 'git_revision'): v.status('Checking if we already have this rev:') process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) + ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) if process.returncode == 0: v.status('yes') if process.returncode == 1: @@ -250,7 +249,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: # updates are happening. process = subprocess.run(['git', '-C', - channel.git_cachedir, + cachedir, 'fetch', channel.git_repo, '%s:%s' % (channel.git_ref, @@ -259,13 +258,13 @@ def git_fetch(v: Verification, channel: Channel) -> None: if hasattr(channel, 'git_revision'): v.status('Verifying that fetch retrieved this rev') process = subprocess.run( - ['git', '-C', channel.git_cachedir, 'cat-file', '-e', channel.git_revision]) + ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) v.result(process.returncode == 0) if not hasattr(channel, 'git_revision'): channel.git_revision = open( os.path.join( - channel.git_cachedir, + cachedir, 'refs', 'heads', channel.git_ref)).read(999).strip() @@ -273,7 +272,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: v.status('Verifying rev is an ancestor of ref') process = subprocess.run(['git', '-C', - channel.git_cachedir, + cachedir, 'merge-base', '--is-ancestor', channel.git_revision, @@ -286,7 +285,7 @@ def git_fetch(v: Verification, channel: Channel) -> None: channel.old_git_revision) process = subprocess.run(['git', '-C', - channel.git_cachedir, + cachedir, 'merge-base', '--is-ancestor', channel.old_git_revision, @@ -340,7 +339,7 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: v.status('Checking out corresponding git revision') git = subprocess.Popen(['git', '-C', - channel.git_cachedir, + git_cachedir(channel.git_repo), 'archive', channel.git_revision], stdout=subprocess.PIPE) @@ -403,7 +402,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: v.status('Getting commit date') process = subprocess.run(['git', '-C', - channel.git_cachedir, + git_cachedir(channel.git_repo), 'lo', '-n1', '--format=%ct-%h', -- 2.44.1 From 971d3659a4f2ba4e3daeba142668809529ff65c1 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 15:29:38 -0700 Subject: [PATCH 023/100] Split ensure_git_rev_available out from git_fetch --- pinch.py | 115 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/pinch.py b/pinch.py index 3e725e1..0f8f00a 100644 --- a/pinch.py +++ b/pinch.py @@ -210,9 +210,39 @@ def fetch_resources(v: Verification, channel: Channel) -> None: open( channel.table['git-revision'].file).read(999) == channel.git_revision) + def git_cachedir(git_repo: str) -> str: # TODO: Consider using pyxdg to find this path. - return os.path.expanduser('~/.cache/nix-pin-channel/git/%s' % digest_string(git_repo.encode())) + return os.path.expanduser( + '~/.cache/nix-pin-channel/git/%s' % + digest_string( + git_repo.encode())) + + +def verify_git_ancestry(v: Verification, channel: Channel) -> None: + cachedir = git_cachedir(channel.git_repo) + v.status('Verifying rev is an ancestor of ref') + process = subprocess.run(['git', + '-C', + cachedir, + 'merge-base', + '--is-ancestor', + channel.git_revision, + channel.git_ref]) + v.result(process.returncode == 0) + + if hasattr(channel, 'old_git_revision'): + v.status( + 'Verifying rev is an ancestor of previous rev %s' % + channel.old_git_revision) + process = subprocess.run(['git', + '-C', + cachedir, + 'merge-base', + '--is-ancestor', + channel.old_git_revision, + channel.git_revision]) + v.result(process.returncode == 0) def git_fetch(v: Verification, channel: Channel) -> None: @@ -229,39 +259,24 @@ def git_fetch(v: Verification, channel: Channel) -> None: ['git', 'init', '--bare', cachedir]) v.result(process.returncode == 0) - have_rev = False + v.status('Fetching ref "%s" from %s' % (channel.git_ref, channel.git_repo)) + # We don't use --force here because we want to abort and freak out if forced + # updates are happening. + process = subprocess.run(['git', + '-C', + cachedir, + 'fetch', + channel.git_repo, + '%s:%s' % (channel.git_ref, + channel.git_ref)]) + v.result(process.returncode == 0) + if hasattr(channel, 'git_revision'): - v.status('Checking if we already have this rev:') + v.status('Verifying that fetch retrieved this rev') process = subprocess.run( ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) - if process.returncode == 0: - v.status('yes') - if process.returncode == 1: - v.status('no') - v.result(process.returncode == 0 or process.returncode == 1) - have_rev = process.returncode == 0 - - if not have_rev: - v.status( - 'Fetching ref "%s" from %s' % - (channel.git_ref, channel.git_repo)) - # We don't use --force here because we want to abort and freak out if forced - # updates are happening. - process = subprocess.run(['git', - '-C', - cachedir, - 'fetch', - channel.git_repo, - '%s:%s' % (channel.git_ref, - channel.git_ref)]) v.result(process.returncode == 0) - if hasattr(channel, 'git_revision'): - v.status('Verifying that fetch retrieved this rev') - process = subprocess.run( - ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) - v.result(process.returncode == 0) - - if not hasattr(channel, 'git_revision'): + else: channel.git_revision = open( os.path.join( cachedir, @@ -269,28 +284,24 @@ def git_fetch(v: Verification, channel: Channel) -> None: 'heads', channel.git_ref)).read(999).strip() - v.status('Verifying rev is an ancestor of ref') - process = subprocess.run(['git', - '-C', - cachedir, - 'merge-base', - '--is-ancestor', - channel.git_revision, - channel.git_ref]) - v.result(process.returncode == 0) + verify_git_ancestry(v, channel) - if hasattr(channel, 'old_git_revision'): - v.status( - 'Verifying rev is an ancestor of previous rev %s' % - channel.old_git_revision) - process = subprocess.run(['git', - '-C', - cachedir, - 'merge-base', - '--is-ancestor', - channel.old_git_revision, - channel.git_revision]) - v.result(process.returncode == 0) + +def ensure_git_rev_available(v: Verification, channel: Channel) -> None: + cachedir = git_cachedir(channel.git_repo) + if os.path.exists(cachedir): + v.status('Checking if we already have this rev:') + process = subprocess.run( + ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) + if process.returncode == 0: + v.status('yes') + if process.returncode == 1: + v.status('no') + v.result(process.returncode == 0 or process.returncode == 1) + if process.returncode == 0: + verify_git_ancestry(v, channel) + return + git_fetch(v, channel) def compare_tarball_and_git( @@ -394,7 +405,7 @@ def pin_channel(v: Verification, channel: Channel) -> None: fetch(v, channel) parse_channel(v, channel) fetch_resources(v, channel) - git_fetch(v, channel) + ensure_git_rev_available(v, channel) check_channel_contents(v, channel) -- 2.44.1 From 736c25ebc1f9814c84429ed9dd16cd23104576f5 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 16:40:01 -0700 Subject: [PATCH 024/100] Update channels from pins --- pinch.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/pinch.py b/pinch.py index 0f8f00a..5bb4e95 100644 --- a/pinch.py +++ b/pinch.py @@ -2,6 +2,7 @@ import argparse import configparser import filecmp import functools +import getpass import hashlib import operator import os @@ -362,6 +363,33 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: v.result(git.returncode == 0 and tar.returncode == 0) +def git_get_tarball(v: Verification, channel: Channel) -> str: + with tempfile.TemporaryDirectory() as output_dir: + output_filename = os.path.join( + output_dir, channel.release_name + '.tar.xz') + with open(output_filename, 'w') as output_file: + v.status( + 'Generating tarball for git revision %s' % + channel.git_revision) + git = subprocess.Popen(['git', + '-C', + git_cachedir(channel.git_repo), + 'archive', + '--prefix=%s/' % channel.release_name, + channel.git_revision], + stdout=subprocess.PIPE) + xz = subprocess.Popen(['xz'], stdin=git.stdout, stdout=output_file) + xz.wait() + git.wait() + v.result(git.returncode == 0 and xz.returncode == 0) + + v.status('Putting tarball in Nix store') + process = subprocess.run( + ['nix-store', '--add', output_filename], capture_output=True) + v.result(process.returncode == 0) + return process.stdout.decode().strip() + + def check_channel_metadata( v: Verification, channel: Channel, @@ -425,40 +453,73 @@ def git_revision_name(v: Verification, channel: Channel) -> str: process.stdout.decode().strip()) -def make_channel(conf: configparser.SectionProxy) -> Channel: - channel = Channel(**dict(conf.items())) - if hasattr(channel, 'git_revision'): - channel.old_git_revision = channel.git_revision - del channel.git_revision - return channel - - def pin(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() config.read_file(open(args.channels_file), args.channels_file) for section in config.sections(): - channel = make_channel(config[section]) + + channel = Channel(**dict(config[section].items())) + if hasattr(channel, 'git_revision'): + channel.old_git_revision = channel.git_revision + del channel.git_revision + if 'channel_url' in config[section]: pin_channel(v, channel) - config[section]['name'] = channel.release_name + config[section]['release_name'] = channel.release_name config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest else: git_fetch(v, channel) - config[section]['name'] = git_revision_name(v, channel) + config[section]['release_name'] = git_revision_name(v, channel) config[section]['git_revision'] = channel.git_revision with open(args.channels_file, 'w') as configfile: config.write(configfile) +def update(args: argparse.Namespace) -> None: + v = Verification() + config = configparser.ConfigParser() + config.read_file(open(args.channels_file), args.channels_file) + exprs = [] + for section in config.sections(): + if 'channel_url' in config[section]: + tarball = fetch_with_nix_prefetch_url( + v, config[section]['tarball_url'], Digest16( + config[section]['tarball_sha256'])) + else: + channel = Channel(**dict(config[section].items())) + ensure_git_rev_available(v, channel) + tarball = git_get_tarball(v, channel) + exprs.append( + 'f: f { name = "%s"; channelName = "%s"; src = builtins.storePath "%s"; }' % + (config[section]['release_name'], section, tarball)) + v.status('Installing channels with nix-env') + process = subprocess.run( + [ + 'nix-env', + '--profile', + '/nix/var/nix/profiles/per-user/%s/channels' % + getpass.getuser(), + '--show-trace', + '--file', + '', + '--install', + '--from-expression'] + + exprs) + v.result(process.returncode == 0) + + def main() -> None: parser = argparse.ArgumentParser(prog='pinch') subparsers = parser.add_subparsers(dest='mode', required=True) parser_pin = subparsers.add_parser('pin') parser_pin.add_argument('channels_file', type=str) parser_pin.set_defaults(func=pin) + parser_update = subparsers.add_parser('update') + parser_update.add_argument('channels_file', type=str) + parser_update.set_defaults(func=update) args = parser.parse_args() args.func(args) -- 2.44.1 From 597bd03a184f89d9f46271dfb18da72812f8a8b7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 17:00:17 -0700 Subject: [PATCH 025/100] README --- README | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..b0c1ebc --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +# Pinch + +PIN CHannels - a simple drop-in replacement for `nix-channel --update`. + +Example usage, being invoked on the example "channels" file included here: + + $ python3 pinch.py pin channels + $ python3 pinch.py update channels + + +The first "pin" command will add these fields to the file: + + [nixos] + channel_url = https://channels.nixos.org/nixos-20.03 + git_repo = https://github.com/NixOS/nixpkgs.git + git_ref = nixos-20.03 + +release_name = nixos-20.03beta1155.29eddfc36d7 + +tarball_url = https://releases.nixos.org/nixos/20.03/nixos-20.03beta1155.29eddfc36d7/nixexprs.tar.xz + +tarball_sha256 = 9c1d182af2af64e5e8799e256a4a6dc1fed324ba06cb5f76c938dc63b64f0959 + +git_revision = 29eddfc36d720dcc4822581175217543b387b1e8 + + [nixos-hardware] + git_repo = https://github.com/NixOS/nixos-hardware.git + git_ref = master + +release_name = nixos-hardware.git-1585241157-edb7199b5c4 + +git_revision = edb7199b5c4f1db34a7253d4cabf6cf690521a92 + +The second "update" command applies these changes to your nix channels, like `nix-channel --update` does. + +Advantages over nix-channel: + + * Deploy the exact same channel content to multiple machines. + * Store your pin file in revision control for more powerful rollback mechanism than `nix-channel --rollback`. + * Chanel contents are verified by hash before being installed. -- 2.44.1 From 4fdb3625fc47889c73bb4af21da1a67dcb185a65 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 10 Apr 2020 23:17:22 -0700 Subject: [PATCH 026/100] This tool is named "pinch" now --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 5bb4e95..6bd7dd3 100644 --- a/pinch.py +++ b/pinch.py @@ -215,7 +215,7 @@ def fetch_resources(v: Verification, channel: Channel) -> None: def git_cachedir(git_repo: str) -> str: # TODO: Consider using pyxdg to find this path. return os.path.expanduser( - '~/.cache/nix-pin-channel/git/%s' % + '~/.cache/pinch/git/%s' % digest_string( git_repo.encode())) -- 2.44.1 From 988531533047eac7a381e103cb59301ec165de0e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sun, 12 Apr 2020 09:54:57 -0700 Subject: [PATCH 027/100] Specify channels to pin on command line No specified channels -> pin all channels (which is an unfortunate interface for scripting. Sorry). --- pinch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pinch.py b/pinch.py index 6bd7dd3..8bda816 100644 --- a/pinch.py +++ b/pinch.py @@ -458,6 +458,8 @@ def pin(args: argparse.Namespace) -> None: config = configparser.ConfigParser() config.read_file(open(args.channels_file), args.channels_file) for section in config.sections(): + if args.channels and section not in args.channels: + continue channel = Channel(**dict(config[section].items())) if hasattr(channel, 'git_revision'): @@ -516,6 +518,7 @@ def main() -> None: subparsers = parser.add_subparsers(dest='mode', required=True) parser_pin = subparsers.add_parser('pin') parser_pin.add_argument('channels_file', type=str) + parser_pin.add_argument('channels', type=str, nargs='*') parser_pin.set_defaults(func=pin) parser_update = subparsers.add_parser('update') parser_update.add_argument('channels_file', type=str) -- 2.44.1 From 7fcc18a2db40653e0d782528674ca1740e70830c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 18 May 2020 16:09:06 -0700 Subject: [PATCH 028/100] Exclude subprocess-run-check We check status explicitly. --- test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.sh b/test.sh index edab8d8..3a35523 100755 --- a/test.sh +++ b/test.sh @@ -8,7 +8,7 @@ find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 -find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,too-few-public-methods +find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,subprocess-run-check,too-few-public-methods formatting_needs_fixing=$( find . -name '*.py' -print0 | -- 2.44.1 From 9a78329ed18525c690e7d8b556bb891934f77517 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 18 May 2020 16:19:15 -0700 Subject: [PATCH 029/100] Add --dry_run flag --- pinch.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pinch.py b/pinch.py index 8bda816..4a6158a 100644 --- a/pinch.py +++ b/pinch.py @@ -7,6 +7,7 @@ import hashlib import operator import os import os.path +import shlex import shutil import subprocess import tempfile @@ -497,20 +498,22 @@ def update(args: argparse.Namespace) -> None: exprs.append( 'f: f { name = "%s"; channelName = "%s"; src = builtins.storePath "%s"; }' % (config[section]['release_name'], section, tarball)) - v.status('Installing channels with nix-env') - process = subprocess.run( - [ - 'nix-env', - '--profile', - '/nix/var/nix/profiles/per-user/%s/channels' % - getpass.getuser(), - '--show-trace', - '--file', - '', - '--install', - '--from-expression'] + - exprs) - v.result(process.returncode == 0) + command = [ + 'nix-env', + '--profile', + '/nix/var/nix/profiles/per-user/%s/channels' % + getpass.getuser(), + '--show-trace', + '--file', + '', + '--install', + '--from-expression'] + exprs + if args.dry_run: + print(' '.join(map(shlex.quote, command))) + else: + v.status('Installing channels with nix-env') + process = subprocess.run(command) + v.result(process.returncode == 0) def main() -> None: @@ -521,6 +524,7 @@ def main() -> None: parser_pin.add_argument('channels', type=str, nargs='*') parser_pin.set_defaults(func=pin) parser_update = subparsers.add_parser('update') + parser_update.add_argument('--dry-run', action='store_true') parser_update.add_argument('channels_file', type=str) parser_update.set_defaults(func=update) args = parser.parse_args() -- 2.44.1 From 9d7844bb9474c2431f268061dffed0c5c49ddc7d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 18 May 2020 16:21:33 -0700 Subject: [PATCH 030/100] Status chatter goes to stderr --- pinch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 4a6158a..46e5af3 100644 --- a/pinch.py +++ b/pinch.py @@ -10,6 +10,7 @@ import os.path import shlex import shutil import subprocess +import sys import tempfile import types import urllib.parse @@ -58,7 +59,7 @@ class Verification: self.line_length = 0 def status(self, s: str) -> None: - print(s, end=' ', flush=True) + print(s, end=' ', file=sys.stderr, flush=True) self.line_length += 1 + len(s) # Unicode?? @staticmethod @@ -70,7 +71,7 @@ class Verification: length = len(message) cols = shutil.get_terminal_size().columns pad = (cols - (self.line_length + length)) % cols - print(' ' * pad + self._color(message, color)) + print(' ' * pad + self._color(message, color), file=sys.stderr) self.line_length = 0 if not r: raise VerificationError() -- 2.44.1 From 49d58bdf61c8ee7da0de30a3c806e3964f583831 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 18 May 2020 23:05:59 -0700 Subject: [PATCH 031/100] Test --- test.sh | 4 ++++ tests/core.sh | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100755 tests/core.sh diff --git a/test.sh b/test.sh index 3a35523..9823458 100755 --- a/test.sh +++ b/test.sh @@ -6,6 +6,10 @@ PARALLELISM=4 find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports +for test in tests/*;do + "$test" +done + find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,subprocess-run-check,too-few-public-methods diff --git a/tests/core.sh b/tests/core.sh new file mode 100755 index 0000000..bb64701 --- /dev/null +++ b/tests/core.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +repo_dir="`mktemp -d`" +repo="$repo_dir/repo" +git init "$repo" +( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf="`mktemp`" +cat > "$conf" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi -- 2.44.1 From 17906b2786e4c5e57a66874fd78c9cf70e074677 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 18 May 2020 23:55:27 -0700 Subject: [PATCH 032/100] Aliases --- pinch.py | 27 ++++++++++++++++++++++----- tests/alias.sh | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100755 tests/alias.sh diff --git a/pinch.py b/pinch.py index 46e5af3..b147100 100644 --- a/pinch.py +++ b/pinch.py @@ -38,6 +38,7 @@ class ChannelTableEntry(types.SimpleNamespace): class Channel(types.SimpleNamespace): + alias_of: str channel_html: bytes channel_url: str forwarded_url: str @@ -464,6 +465,11 @@ def pin(args: argparse.Namespace) -> None: continue channel = Channel(**dict(config[section].items())) + + if hasattr(channel, 'alias_of'): + assert not hasattr(channel, 'git_repo') + continue + if hasattr(channel, 'git_revision'): channel.old_git_revision = channel.git_revision del channel.git_revision @@ -486,8 +492,13 @@ def update(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() config.read_file(open(args.channels_file), args.channels_file) - exprs = [] + exprs = {} for section in config.sections(): + + if 'alias_of' in config[section]: + assert 'git_repo' not in config[section] + continue + if 'channel_url' in config[section]: tarball = fetch_with_nix_prefetch_url( v, config[section]['tarball_url'], Digest16( @@ -496,9 +507,15 @@ def update(args: argparse.Namespace) -> None: channel = Channel(**dict(config[section].items())) ensure_git_rev_available(v, channel) tarball = git_get_tarball(v, channel) - exprs.append( - 'f: f { name = "%s"; channelName = "%s"; src = builtins.storePath "%s"; }' % - (config[section]['release_name'], section, tarball)) + + exprs[section] = ( + 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % + (config[section]['release_name'], tarball)) + + for section in config.sections(): + if 'alias_of' in config[section]: + exprs[section] = exprs[str(config[section]['alias_of'])] + command = [ 'nix-env', '--profile', @@ -508,7 +525,7 @@ def update(args: argparse.Namespace) -> None: '--file', '', '--install', - '--from-expression'] + exprs + '--from-expression'] + [exprs[name] % name for name in sorted(exprs.keys())] if args.dry_run: print(' '.join(map(shlex.quote, command))) else: diff --git a/tests/alias.sh b/tests/alias.sh new file mode 100755 index 0000000..5bc611b --- /dev/null +++ b/tests/alias.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +repo_dir="`mktemp -d`" +repo="$repo_dir/repo" +git init "$repo" +( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf="`mktemp`" +cat > "$conf" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi -- 2.44.1 From 01ba0eb2f8683ddb6f892c7adc98626cdba0d236 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 19 May 2020 00:07:12 -0700 Subject: [PATCH 033/100] Update merges multiple files --- pinch.py | 59 ++++++++++++++++++++++++------------------- tests/multi-update.sh | 41 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 26 deletions(-) create mode 100755 tests/multi-update.sh diff --git a/pinch.py b/pinch.py index b147100..b044faf 100644 --- a/pinch.py +++ b/pinch.py @@ -456,10 +456,15 @@ def git_revision_name(v: Verification, channel: Channel) -> str: process.stdout.decode().strip()) +def read_config(filename: str) -> configparser.ConfigParser: + config = configparser.ConfigParser() + config.read_file(open(filename), filename) + return config + + def pin(args: argparse.Namespace) -> None: v = Verification() - config = configparser.ConfigParser() - config.read_file(open(args.channels_file), args.channels_file) + config = read_config(args.channels_file) for section in config.sections(): if args.channels and section not in args.channels: continue @@ -491,30 +496,32 @@ def pin(args: argparse.Namespace) -> None: def update(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() - config.read_file(open(args.channels_file), args.channels_file) exprs = {} - for section in config.sections(): - - if 'alias_of' in config[section]: - assert 'git_repo' not in config[section] - continue - - if 'channel_url' in config[section]: - tarball = fetch_with_nix_prefetch_url( - v, config[section]['tarball_url'], Digest16( - config[section]['tarball_sha256'])) - else: - channel = Channel(**dict(config[section].items())) - ensure_git_rev_available(v, channel) - tarball = git_get_tarball(v, channel) - - exprs[section] = ( - 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % - (config[section]['release_name'], tarball)) - - for section in config.sections(): - if 'alias_of' in config[section]: - exprs[section] = exprs[str(config[section]['alias_of'])] + configs = [read_config(filename) for filename in args.channels_file] + for config in configs: + for section in config.sections(): + + if 'alias_of' in config[section]: + assert 'git_repo' not in config[section] + continue + + if 'channel_url' in config[section]: + tarball = fetch_with_nix_prefetch_url( + v, config[section]['tarball_url'], Digest16( + config[section]['tarball_sha256'])) + else: + channel = Channel(**dict(config[section].items())) + ensure_git_rev_available(v, channel) + tarball = git_get_tarball(v, channel) + + exprs[section] = ( + 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % + (config[section]['release_name'], tarball)) + + for config in configs: + for section in config.sections(): + if 'alias_of' in config[section]: + exprs[section] = exprs[str(config[section]['alias_of'])] command = [ 'nix-env', @@ -543,7 +550,7 @@ def main() -> None: parser_pin.set_defaults(func=pin) parser_update = subparsers.add_parser('update') parser_update.add_argument('--dry-run', action='store_true') - parser_update.add_argument('channels_file', type=str) + parser_update.add_argument('channels_file', type=str, nargs='+') parser_update.set_defaults(func=update) args = parser.parse_args() args.func(args) diff --git a/tests/multi-update.sh b/tests/multi-update.sh new file mode 100755 index 0000000..e7e5495 --- /dev/null +++ b/tests/multi-update.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +repo_dir="`mktemp -d`" +repo="$repo_dir/repo" +git init "$repo" +( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf1="`mktemp`" +cat > "$conf1" < "$conf2" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi -- 2.44.1 From ab7ebb2f956159d846cf9eb18d78130bd5821aaf Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 19 May 2020 00:20:09 -0700 Subject: [PATCH 034/100] Reject duplicate channel names --- pinch.py | 4 ++++ tests/reject-duplicates.sh | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100755 tests/reject-duplicates.sh diff --git a/pinch.py b/pinch.py index b044faf..787555f 100644 --- a/pinch.py +++ b/pinch.py @@ -514,6 +514,8 @@ def update(args: argparse.Namespace) -> None: ensure_git_rev_available(v, channel) tarball = git_get_tarball(v, channel) + if section in exprs: + raise Exception('Duplicate channel "%s"' % section) exprs[section] = ( 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % (config[section]['release_name'], tarball)) @@ -521,6 +523,8 @@ def update(args: argparse.Namespace) -> None: for config in configs: for section in config.sections(): if 'alias_of' in config[section]: + if section in exprs: + raise Exception('Duplicate channel "%s"' % section) exprs[section] = exprs[str(config[section]['alias_of'])] command = [ diff --git a/tests/reject-duplicates.sh b/tests/reject-duplicates.sh new file mode 100755 index 0000000..34e12c7 --- /dev/null +++ b/tests/reject-duplicates.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +repo_dir1="`mktemp -d`" +repo1="$repo_dir1/repo" +git init "$repo1" +( + cd "$repo1" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +repo_dir2="`mktemp -d`" +repo2="$repo_dir2/repo" +git init "$repo2" +( + cd "$repo2" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf1="`mktemp`" +cat > "$conf1" < "$conf2" < Date: Fri, 22 May 2020 16:54:41 -0700 Subject: [PATCH 035/100] Add missing type annotation --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 787555f..ad49334 100644 --- a/pinch.py +++ b/pinch.py @@ -496,7 +496,7 @@ def pin(args: argparse.Namespace) -> None: def update(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() - exprs = {} + exprs: Dict[str, str] = {} configs = [read_config(filename) for filename in args.channels_file] for config in configs: for section in config.sections(): -- 2.44.1 From 001b650db2f8a9e141634d63fa89250c1088f3c2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 16:55:04 -0700 Subject: [PATCH 036/100] set -e in test scripts --- tests/alias.sh | 2 ++ tests/core.sh | 2 ++ tests/multi-update.sh | 2 ++ tests/reject-duplicates.sh | 2 ++ 4 files changed, 8 insertions(+) diff --git a/tests/alias.sh b/tests/alias.sh index 5bc611b..c721340 100755 --- a/tests/alias.sh +++ b/tests/alias.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + repo_dir="`mktemp -d`" repo="$repo_dir/repo" git init "$repo" diff --git a/tests/core.sh b/tests/core.sh index bb64701..385ae2b 100755 --- a/tests/core.sh +++ b/tests/core.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + repo_dir="`mktemp -d`" repo="$repo_dir/repo" git init "$repo" diff --git a/tests/multi-update.sh b/tests/multi-update.sh index e7e5495..24c22bc 100755 --- a/tests/multi-update.sh +++ b/tests/multi-update.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + repo_dir="`mktemp -d`" repo="$repo_dir/repo" git init "$repo" diff --git a/tests/reject-duplicates.sh b/tests/reject-duplicates.sh index 34e12c7..d30bc40 100755 --- a/tests/reject-duplicates.sh +++ b/tests/reject-duplicates.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + repo_dir1="`mktemp -d`" repo1="$repo_dir1/repo" git init "$repo1" -- 2.44.1 From db3da89c7a9f14eec5aa400acfd3c888daca8385 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 16:57:54 -0700 Subject: [PATCH 037/100] More tests: pin-twice, reject-nonancestor --- tests/pin-twice.sh | 45 +++++++++++++++++++++++++++++++++++++ tests/reject-nonancestor.sh | 38 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100755 tests/pin-twice.sh create mode 100755 tests/reject-nonancestor.sh diff --git a/tests/pin-twice.sh b/tests/pin-twice.sh new file mode 100755 index 0000000..b17af2c --- /dev/null +++ b/tests/pin-twice.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +set -e + +repo_dir="`mktemp -d`" +repo="$repo_dir/repo" +git init "$repo" +( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf="`mktemp`" +cat > "$conf" < other-file + git add other-file + git commit -m 'Second commit message' +) + +python3 ./pinch.py pin "$conf" + +actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` + +rm -rf "$repo_dir" "$conf" + +expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi diff --git a/tests/reject-nonancestor.sh b/tests/reject-nonancestor.sh new file mode 100755 index 0000000..b8e944a --- /dev/null +++ b/tests/reject-nonancestor.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +repo_dir="`mktemp -d`" +repo="$repo_dir/repo" +git init "$repo" +( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' +) + +conf="`mktemp`" +cat > "$conf" < other-file + git add other-file + git commit --amend -m 'Amended commit message' +) + +if python3 ./pinch.py pin "$conf";then + echo "FAIL: non-ancestor commit should be rejected" + exit 1 +else + echo PASS +fi + +rm -rf "$repo_dir" "$conf" -- 2.44.1 From 4bc82b98659d9222b1a2b25d472f0e5923a0ed52 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 17:07:01 -0700 Subject: [PATCH 038/100] shell.nix --- shell.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 shell.nix diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b408ea4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import { } }: +pkgs.mkShell rec { + doCheck = true; + buildInputs = with pkgs; [ python3 ]; + checkInputs = with pkgs; [ + mypy + python3Packages.autopep8 + python3Packages.pylint + ]; +} -- 2.44.1 From de68382af99d04266b2ca33967ff6ca4a3c87947 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 17:23:22 -0700 Subject: [PATCH 039/100] Appease new mypy 0.761 -> 0.770 --- pinch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index ad49334..a7d5b65 100644 --- a/pinch.py +++ b/pinch.py @@ -360,7 +360,8 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: stdout=subprocess.PIPE) tar = subprocess.Popen( ['tar', 'x', '-C', dest, '-f', '-'], stdin=git.stdout) - git.stdout.close() + if git.stdout: + git.stdout.close() tar.wait() git.wait() v.result(git.returncode == 0 and tar.returncode == 0) @@ -451,7 +452,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: '--abbrev=11', channel.git_revision], capture_output=True) - v.result(process.returncode == 0 and process.stdout != '') + v.result(process.returncode == 0 and process.stdout != b'') return '%s-%s' % (os.path.basename(channel.git_repo), process.stdout.decode().strip()) -- 2.44.1 From 26125a281f6733d08ffb488668b052ee9c20061a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 17:25:24 -0700 Subject: [PATCH 040/100] Use xdg packge to find XDG cache dir --- pinch.py | 9 ++++++--- shell.nix | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pinch.py b/pinch.py index a7d5b65..1827281 100644 --- a/pinch.py +++ b/pinch.py @@ -25,6 +25,9 @@ from typing import ( Tuple, ) +import xdg + + Digest16 = NewType('Digest16', str) Digest32 = NewType('Digest32', str) @@ -216,9 +219,9 @@ def fetch_resources(v: Verification, channel: Channel) -> None: def git_cachedir(git_repo: str) -> str: - # TODO: Consider using pyxdg to find this path. - return os.path.expanduser( - '~/.cache/pinch/git/%s' % + return os.path.join( + xdg.XDG_CACHE_HOME, + 'pinch/git', digest_string( git_repo.encode())) diff --git a/shell.nix b/shell.nix index b408ea4..11e4f03 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,7 @@ { pkgs ? import { } }: pkgs.mkShell rec { doCheck = true; - buildInputs = with pkgs; [ python3 ]; + buildInputs = with pkgs; [ (python3.withPackages (ps: with ps; [ xdg ])) ]; checkInputs = with pkgs; [ mypy python3Packages.autopep8 -- 2.44.1 From a2290fb189bee1da5e985d785e044d1681401ff7 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 17:55:41 -0700 Subject: [PATCH 041/100] Factor out test setup --- test.sh | 4 ++- tests/alias.sh | 23 ++++------------ tests/core.sh | 21 ++------------ tests/lib/test-setup.sh | 26 ++++++++++++++++++ tests/multi-update.sh | 28 +++++-------------- tests/pin-twice.sh | 21 ++------------ tests/reject-duplicates.sh | 55 ++++++++++++------------------------- tests/reject-nonancestor.sh | 21 ++------------ 8 files changed, 67 insertions(+), 132 deletions(-) create mode 100644 tests/lib/test-setup.sh diff --git a/test.sh b/test.sh index 9823458..9485418 100755 --- a/test.sh +++ b/test.sh @@ -7,7 +7,9 @@ PARALLELISM=4 find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports for test in tests/*;do - "$test" + if [ ! -d "$test" ];then + "$test" + fi done find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 diff --git a/tests/alias.sh b/tests/alias.sh index c721340..5083999 100755 --- a/tests/alias.sh +++ b/tests/alias.sh @@ -1,23 +1,10 @@ #!/bin/sh -set -e - -repo_dir="`mktemp -d`" -repo="$repo_dir/repo" -git init "$repo" -( - cd "$repo" - echo Contents > test-file - git add test-file - git commit -m 'Commit message' -) - -conf="`mktemp`" -cat > "$conf" <> "$conf" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' diff --git a/tests/core.sh b/tests/core.sh index 385ae2b..da0d991 100755 --- a/tests/core.sh +++ b/tests/core.sh @@ -1,29 +1,14 @@ #!/bin/sh -set -e +. ./tests/lib/test-setup.sh -repo_dir="`mktemp -d`" -repo="$repo_dir/repo" -git init "$repo" -( - cd "$repo" - echo Contents > test-file - git add test-file - git commit -m 'Commit message' -) - -conf="`mktemp`" -cat > "$conf" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' diff --git a/tests/lib/test-setup.sh b/tests/lib/test-setup.sh new file mode 100644 index 0000000..12cf525 --- /dev/null +++ b/tests/lib/test-setup.sh @@ -0,0 +1,26 @@ +set -e + +foo_setup() { + + repo_dir="`mktemp -d`" + repo="$repo_dir/repo" + git init "$repo" + ( + cd "$repo" + echo Contents > test-file + git add test-file + git commit -m 'Commit message' + ) + + conf="`mktemp`" + cat > "$conf" < test-file - git add test-file - git commit -m 'Commit message' -) - -conf1="`mktemp`" -cat > "$conf1" < "$conf2" < "$conf2" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' diff --git a/tests/pin-twice.sh b/tests/pin-twice.sh index b17af2c..5f51cc0 100755 --- a/tests/pin-twice.sh +++ b/tests/pin-twice.sh @@ -1,23 +1,8 @@ #!/bin/sh -set -e +. ./tests/lib/test-setup.sh -repo_dir="`mktemp -d`" -repo="$repo_dir/repo" -git init "$repo" -( - cd "$repo" - echo Contents > test-file - git add test-file - git commit -m 'Commit message' -) - -conf="`mktemp`" -cat > "$conf" <'\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' diff --git a/tests/reject-duplicates.sh b/tests/reject-duplicates.sh index d30bc40..8a630b0 100755 --- a/tests/reject-duplicates.sh +++ b/tests/reject-duplicates.sh @@ -1,50 +1,29 @@ #!/bin/sh -set -e - -repo_dir1="`mktemp -d`" -repo1="$repo_dir1/repo" -git init "$repo1" -( - cd "$repo1" - echo Contents > test-file - git add test-file - git commit -m 'Commit message' -) - -repo_dir2="`mktemp -d`" -repo2="$repo_dir2/repo" -git init "$repo2" -( - cd "$repo2" - echo Contents > test-file - git add test-file - git commit -m 'Commit message' -) - -conf1="`mktemp`" -cat > "$conf1" < "$conf2" < test-file - git add test-file - git commit -m 'Commit message' -) - -conf="`mktemp`" -cat > "$conf" < Date: Fri, 22 May 2020 18:07:37 -0700 Subject: [PATCH 042/100] tests: Depend upon /bin/sh supporting traps --- tests/alias.sh | 2 -- tests/core.sh | 2 -- tests/lib/test-setup.sh | 7 +++++-- tests/multi-update.sh | 1 - tests/pin-twice.sh | 2 -- tests/reject-duplicates.sh | 8 +------- tests/reject-nonancestor.sh | 2 -- 7 files changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/alias.sh b/tests/alias.sh index 5083999..368fc1e 100755 --- a/tests/alias.sh +++ b/tests/alias.sh @@ -13,8 +13,6 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -foo_cleanup - expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then diff --git a/tests/core.sh b/tests/core.sh index da0d991..5ef652e 100755 --- a/tests/core.sh +++ b/tests/core.sh @@ -8,8 +8,6 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -foo_cleanup - expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then diff --git a/tests/lib/test-setup.sh b/tests/lib/test-setup.sh index 12cf525..4ccc8d9 100644 --- a/tests/lib/test-setup.sh +++ b/tests/lib/test-setup.sh @@ -21,6 +21,9 @@ EOF } -foo_cleanup() { - rm -rf "$repo_dir" "$conf" +test_cleanup() { + if [ "$repo_dir" ];then rm -rf "$repo_dir"; fi + if [ "$conf" ];then rm "$conf"; fi } + +trap test_cleanup EXIT INT TERM diff --git a/tests/multi-update.sh b/tests/multi-update.sh index d6a6d54..74899ff 100755 --- a/tests/multi-update.sh +++ b/tests/multi-update.sh @@ -15,7 +15,6 @@ python3 ./pinch.py pin "$conf2" actual_env_command=`python3 ./pinch.py update --dry-run "$conf" "$conf2"` -foo_cleanup rm -rf "$conf2" expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' diff --git a/tests/pin-twice.sh b/tests/pin-twice.sh index 5f51cc0..dec02f0 100755 --- a/tests/pin-twice.sh +++ b/tests/pin-twice.sh @@ -17,8 +17,6 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -foo_cleanup - expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then diff --git a/tests/reject-duplicates.sh b/tests/reject-duplicates.sh index 8a630b0..a6978f8 100755 --- a/tests/reject-duplicates.sh +++ b/tests/reject-duplicates.sh @@ -20,10 +20,4 @@ else echo PASS fi -foo_cleanup - -repo_dir=$repo_dir1 -repo=$repo1 -conf=$conf1 - -foo_cleanup +rm -rf "$repo_dir1" "$repo1" "$conf1" diff --git a/tests/reject-nonancestor.sh b/tests/reject-nonancestor.sh index 5608761..f8cc2e9 100755 --- a/tests/reject-nonancestor.sh +++ b/tests/reject-nonancestor.sh @@ -19,5 +19,3 @@ if python3 ./pinch.py pin "$conf";then else echo PASS fi - -foo_cleanup -- 2.44.1 From 78c89e6ef13aea1cb76ef6bc52b943a7e1a03347 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 18:26:41 -0700 Subject: [PATCH 043/100] Use separate cache dir for test isolation --- tests/lib/test-setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/lib/test-setup.sh b/tests/lib/test-setup.sh index 4ccc8d9..97fec64 100644 --- a/tests/lib/test-setup.sh +++ b/tests/lib/test-setup.sh @@ -1,5 +1,8 @@ set -e +cache_dir=$(mktemp -d) +export XDG_CACHE_HOME=$cache_dir + foo_setup() { repo_dir="`mktemp -d`" @@ -24,6 +27,7 @@ EOF test_cleanup() { if [ "$repo_dir" ];then rm -rf "$repo_dir"; fi if [ "$conf" ];then rm "$conf"; fi + if [ "$cache_dir" ];then rm -rf "$cache_dir"; fi } trap test_cleanup EXIT INT TERM -- 2.44.1 From 0e515222756a52b6f6e617a5dba149d7422546d4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 22 May 2020 21:15:18 -0700 Subject: [PATCH 044/100] More tests around rejecting force pushes --- tests/reject-amend.sh | 21 +++++++++++++++++++++ tests/reject-force-push.sh | 26 ++++++++++++++++++++++++++ tests/reject-nonancestor.sh | 2 ++ tests/reject-nonancestor2.sh | 25 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100755 tests/reject-amend.sh create mode 100755 tests/reject-force-push.sh create mode 100755 tests/reject-nonancestor2.sh diff --git a/tests/reject-amend.sh b/tests/reject-amend.sh new file mode 100755 index 0000000..f8cc2e9 --- /dev/null +++ b/tests/reject-amend.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +python3 ./pinch.py pin "$conf" + +( + cd "$repo" + echo Other contents > other-file + git add other-file + git commit --amend -m 'Amended commit message' +) + +if python3 ./pinch.py pin "$conf";then + echo "FAIL: non-ancestor commit should be rejected" + exit 1 +else + echo PASS +fi diff --git a/tests/reject-force-push.sh b/tests/reject-force-push.sh new file mode 100755 index 0000000..4a0614f --- /dev/null +++ b/tests/reject-force-push.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup +conf_bak=$(mktemp) +cp "$conf" "$conf_bak" + +python3 ./pinch.py pin "$conf" + +( + cd "$repo" + echo Other contents > other-file + git add other-file + git commit --amend -m 'Amended commit message' +) + +mv "$conf_bak" "$conf" + +if python3 ./pinch.py pin "$conf";then + echo "FAIL: non-ancestor commit should be rejected" + exit 1 +else + echo PASS +fi + diff --git a/tests/reject-nonancestor.sh b/tests/reject-nonancestor.sh index f8cc2e9..44f2a82 100755 --- a/tests/reject-nonancestor.sh +++ b/tests/reject-nonancestor.sh @@ -13,6 +13,8 @@ python3 ./pinch.py pin "$conf" git commit --amend -m 'Amended commit message' ) +rm -rf "$cache_dir"/* + if python3 ./pinch.py pin "$conf";then echo "FAIL: non-ancestor commit should be rejected" exit 1 diff --git a/tests/reject-nonancestor2.sh b/tests/reject-nonancestor2.sh new file mode 100755 index 0000000..924d229 --- /dev/null +++ b/tests/reject-nonancestor2.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +( + cd "$repo" + git checkout -b historical + git checkout master + echo Other contents > other-file + git add other-file + git commit -m 'Second commit message' +) + +python3 ./pinch.py pin "$conf" + +sed -i 's/master/historical/' "$conf" + +if python3 ./pinch.py pin "$conf";then + echo "FAIL: non-ancestor commit should be rejected" + exit 1 +else + echo PASS +fi -- 2.44.1 From f00c16b30250a85d379d18c0d9d31e9446d83bb4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 28 May 2020 23:16:50 -0700 Subject: [PATCH 045/100] Verify update fails if run before pin --- tests/reject-partially-unpinned.sh | 22 ++++++++++++++++++++++ tests/reject-unpinned.sh | 12 ++++++++++++ 2 files changed, 34 insertions(+) create mode 100755 tests/reject-partially-unpinned.sh create mode 100755 tests/reject-unpinned.sh diff --git a/tests/reject-partially-unpinned.sh b/tests/reject-partially-unpinned.sh new file mode 100755 index 0000000..5fc61fd --- /dev/null +++ b/tests/reject-partially-unpinned.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +python3 ./pinch.py pin "$conf" + +cat >> "$conf" < Date: Thu, 28 May 2020 23:22:07 -0700 Subject: [PATCH 046/100] Better error message for running "update" before "pin" --- pinch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pinch.py b/pinch.py index 1827281..f84440c 100644 --- a/pinch.py +++ b/pinch.py @@ -509,6 +509,11 @@ def update(args: argparse.Namespace) -> None: assert 'git_repo' not in config[section] continue + if 'git_repo' not in config[section] or 'release_name' not in config[section]: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + if 'channel_url' in config[section]: tarball = fetch_with_nix_prefetch_url( v, config[section]['tarball_url'], Digest16( -- 2.44.1 From 10ddbaff0197f0ef93a910d7ea3f9f7d0ebac3a4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 28 May 2020 23:52:01 -0700 Subject: [PATCH 047/100] Factor out fetch_channel Appeases pylint's "Too many branches" --- pinch.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/pinch.py b/pinch.py index f84440c..10481e4 100644 --- a/pinch.py +++ b/pinch.py @@ -497,6 +497,25 @@ def pin(args: argparse.Namespace) -> None: config.write(configfile) +def fetch_channel( + v: Verification, + section: str, + conf: configparser.SectionProxy) -> str: + if 'git_repo' not in conf or 'release_name' not in conf: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + + if 'channel_url' in conf: + return fetch_with_nix_prefetch_url( + v, conf['tarball_url'], Digest16( + conf['tarball_sha256'])) + + channel = Channel(**dict(conf.items())) + ensure_git_rev_available(v, channel) + return git_get_tarball(v, channel) + + def update(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() @@ -504,25 +523,10 @@ def update(args: argparse.Namespace) -> None: configs = [read_config(filename) for filename in args.channels_file] for config in configs: for section in config.sections(): - if 'alias_of' in config[section]: assert 'git_repo' not in config[section] continue - - if 'git_repo' not in config[section] or 'release_name' not in config[section]: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) - - if 'channel_url' in config[section]: - tarball = fetch_with_nix_prefetch_url( - v, config[section]['tarball_url'], Digest16( - config[section]['tarball_sha256'])) - else: - channel = Channel(**dict(config[section].items())) - ensure_git_rev_available(v, channel) - tarball = git_get_tarball(v, channel) - + tarball = fetch_channel(v, section, config[section]) if section in exprs: raise Exception('Duplicate channel "%s"' % section) exprs[section] = ( -- 2.44.1 From d2d9efeffd9867660166926c6ac3262b8d22e111 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 28 May 2020 23:52:53 -0700 Subject: [PATCH 048/100] Run tests on commit --- git-pre-commit-hook | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 git-pre-commit-hook diff --git a/git-pre-commit-hook b/git-pre-commit-hook new file mode 100755 index 0000000..7db6818 --- /dev/null +++ b/git-pre-commit-hook @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Copy me to .git/hooks/pre-commit + +set -e + +cleanup() { + if [[ "$D" && -e "$D" ]];then + rm -rf "$D" + fi +} +trap cleanup EXIT + +D=$(mktemp -d) +[[ "$D" && -d "$D" ]] + +git checkout-index --prefix="$D/" -a +pushd "$D" + +nix-shell --run ./test.sh + +popd -- 2.44.1 From eb0c6f1b9ad97954d3ee229428ac84d8f33be546 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 2 Jun 2020 11:17:17 -0700 Subject: [PATCH 049/100] Cache nix store paths of generated tarballs This eliminates the slow "Generating tarball for git revision" step for tarballs that have already been generated and are already in the nix store. --- pinch.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/pinch.py b/pinch.py index 10481e4..732a91f 100644 --- a/pinch.py +++ b/pinch.py @@ -222,8 +222,17 @@ def git_cachedir(git_repo: str) -> str: return os.path.join( xdg.XDG_CACHE_HOME, 'pinch/git', - digest_string( - git_repo.encode())) + digest_string(git_repo.encode())) + + +def tarball_cache_file(channel: Channel) -> str: + return os.path.join( + xdg.XDG_CACHE_HOME, + 'pinch/git-tarball', + '%s-%s-%s' % + (digest_string(channel.git_repo.encode()), + channel.git_revision, + channel.release_name)) def verify_git_ancestry(v: Verification, channel: Channel) -> None: @@ -371,6 +380,12 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: def git_get_tarball(v: Verification, channel: Channel) -> str: + cache_file = tarball_cache_file(channel) + if os.path.exists(cache_file): + cached_tarball = open(cache_file).read(9999) + if os.path.exists(cached_tarball): + return cached_tarball + with tempfile.TemporaryDirectory() as output_dir: output_filename = os.path.join( output_dir, channel.release_name + '.tar.xz') @@ -394,7 +409,11 @@ def git_get_tarball(v: Verification, channel: Channel) -> str: process = subprocess.run( ['nix-store', '--add', output_filename], capture_output=True) v.result(process.returncode == 0) - return process.stdout.decode().strip() + store_tarball = process.stdout.decode().strip() + + os.makedirs(os.path.dirname(cache_file), exist_ok=True) + open(cache_file, 'w').write(store_tarball) + return store_tarball def check_channel_metadata( -- 2.44.1 From 3603dde2e015c340cac1a2f1a0f3d35d44c51c98 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 2 Jun 2020 11:37:37 -0700 Subject: [PATCH 050/100] Don't depend upon xdg It's not yet available in a stable release branch of nixpkgs. We don't want to force folks to use unstable to use pinch. Revert this commit after 20.09 is released and everyone's had time to upgrade. --- pinch.py | 12 +++++++++++- shell.nix | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 732a91f..003ee70 100644 --- a/pinch.py +++ b/pinch.py @@ -25,7 +25,17 @@ from typing import ( Tuple, ) -import xdg +# Use xdg module when it's less painful to have as a dependency + + +class XDG(types.SimpleNamespace): + XDG_CACHE_HOME: str + + +xdg = XDG( + XDG_CACHE_HOME=os.getenv( + 'XDG_CACHE_HOME', + os.path.expanduser('~/.cache'))) Digest16 = NewType('Digest16', str) diff --git a/shell.nix b/shell.nix index 11e4f03..b408ea4 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,7 @@ { pkgs ? import { } }: pkgs.mkShell rec { doCheck = true; - buildInputs = with pkgs; [ (python3.withPackages (ps: with ps; [ xdg ])) ]; + buildInputs = with pkgs; [ python3 ]; checkInputs = with pkgs; [ mypy python3Packages.autopep8 -- 2.44.1 From bed3218277c11d0e6092683f0a2d00225f84d8ae Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:24:49 -0700 Subject: [PATCH 051/100] Spell "log" in "git log" correctly MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This only ever worked because I have a git alias "lo". 😅 Hooray for proper test environment isolation. --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 003ee70..24e132a 100644 --- a/pinch.py +++ b/pinch.py @@ -478,7 +478,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: process = subprocess.run(['git', '-C', git_cachedir(channel.git_repo), - 'lo', + 'log', '-n1', '--format=%ct-%h', '--abbrev=11', -- 2.44.1 From dd1026fe9c1e4b3b4097d5a9589597b64ca0b55b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:29:20 -0700 Subject: [PATCH 052/100] Handle 0 terminal width --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 24e132a..5d93c26 100644 --- a/pinch.py +++ b/pinch.py @@ -83,7 +83,7 @@ class Verification: def result(self, r: bool) -> None: message, color = {True: ('OK ', 92), False: ('FAIL', 91)}[r] length = len(message) - cols = shutil.get_terminal_size().columns + cols = shutil.get_terminal_size().columns or 80 pad = (cols - (self.line_length + length)) % cols print(' ' * pad + self._color(message, color), file=sys.stderr) self.line_length = 0 -- 2.44.1 From 1b48018c3d40a9f70ac98aa3d3112fc5d8cb2201 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:30:20 -0700 Subject: [PATCH 053/100] Set git user in test harness This allows git to run in a minimal test environment without git aborting with "*** Please tell me who you are." --- tests/lib/test-setup.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/lib/test-setup.sh b/tests/lib/test-setup.sh index 97fec64..35a4e73 100644 --- a/tests/lib/test-setup.sh +++ b/tests/lib/test-setup.sh @@ -1,5 +1,10 @@ set -e +export GIT_AUTHOR_NAME=automation +export GIT_COMMITTER_NAME=automation +export GIT_AUTHOR_EMAIL=auto@mati.on +export GIT_COMMITTER_EMAIL=auto@mati.on + cache_dir=$(mktemp -d) export XDG_CACHE_HOME=$cache_dir -- 2.44.1 From 4af9966c73666ed77a7a4c0dc498bfba1e853070 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:33:18 -0700 Subject: [PATCH 054/100] Tests use a private /nix/store This allows the tests to run inside a nix build. Otherwise, it fails with error: creating directory '/nix/var': Permission denied Bonus: This avoids polluting the real store with test artifacts. See also https://github.com/NixOS/nix/issues/13 --- tests/alias.sh | 2 +- tests/core.sh | 2 +- tests/lib/test-setup.sh | 7 +++++++ tests/multi-update.sh | 2 +- tests/pin-twice.sh | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/alias.sh b/tests/alias.sh index 368fc1e..928ceab 100755 --- a/tests/alias.sh +++ b/tests/alias.sh @@ -13,7 +13,7 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' +expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then echo PASS diff --git a/tests/core.sh b/tests/core.sh index 5ef652e..5d11707 100755 --- a/tests/core.sh +++ b/tests/core.sh @@ -8,7 +8,7 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' +expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then echo PASS diff --git a/tests/lib/test-setup.sh b/tests/lib/test-setup.sh index 35a4e73..66d13eb 100644 --- a/tests/lib/test-setup.sh +++ b/tests/lib/test-setup.sh @@ -8,6 +8,11 @@ export GIT_COMMITTER_EMAIL=auto@mati.on cache_dir=$(mktemp -d) export XDG_CACHE_HOME=$cache_dir +nix_store=$(mktemp -d) +nix_state=$(mktemp -d) +export NIX_STORE_DIR=$nix_store +export NIX_STATE_DIR=$nix_state + foo_setup() { repo_dir="`mktemp -d`" @@ -33,6 +38,8 @@ test_cleanup() { if [ "$repo_dir" ];then rm -rf "$repo_dir"; fi if [ "$conf" ];then rm "$conf"; fi if [ "$cache_dir" ];then rm -rf "$cache_dir"; fi + if [ "$nix_store" ];then rm -rf "$nix_store"; fi + if [ "$nix_state" ];then rm -rf "$nix_state"; fi } trap test_cleanup EXIT INT TERM diff --git a/tests/multi-update.sh b/tests/multi-update.sh index 74899ff..cd6e90b 100755 --- a/tests/multi-update.sh +++ b/tests/multi-update.sh @@ -17,7 +17,7 @@ actual_env_command=`python3 ./pinch.py update --dry-run "$conf" "$conf2"` rm -rf "$conf2" -expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' +expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "bar"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\'' '\''f: f \{ name = "\1"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then echo PASS diff --git a/tests/pin-twice.sh b/tests/pin-twice.sh index dec02f0..ee50174 100755 --- a/tests/pin-twice.sh +++ b/tests/pin-twice.sh @@ -17,7 +17,7 @@ python3 ./pinch.py pin "$conf" actual_env_command=`python3 ./pinch.py update --dry-run "$conf"` -expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "/nix/store/.{32}-\1.tar.xz"; \}'\''$' +expected_env_command_RE='^nix-env --profile /nix/var/nix/profiles/per-user/[^/]+/channels --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then echo PASS -- 2.44.1 From ba596fc0a680ccf113b880d72ab873b56c13f530 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:38:20 -0700 Subject: [PATCH 055/100] Don't hide stderr Only capture stdout; allow stderr to leak out to the terminal. This allows diagnosis of errors from subprocesses. --- pinch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pinch.py b/pinch.py index 5d93c26..b761313 100644 --- a/pinch.py +++ b/pinch.py @@ -184,7 +184,7 @@ def digest_file(filename: str) -> Digest16: def to_Digest16(v: Verification, digest32: Digest32) -> Digest16: v.status('Converting digest to base16') process = subprocess.run( - ['nix', 'to-base16', '--type', 'sha256', digest32], capture_output=True) + ['nix', 'to-base16', '--type', 'sha256', digest32], stdout=subprocess.PIPE) v.result(process.returncode == 0) return Digest16(process.stdout.decode().strip()) @@ -192,7 +192,7 @@ def to_Digest16(v: Verification, digest32: Digest32) -> Digest16: def to_Digest32(v: Verification, digest16: Digest16) -> Digest32: v.status('Converting digest to base32') process = subprocess.run( - ['nix', 'to-base32', '--type', 'sha256', digest16], capture_output=True) + ['nix', 'to-base32', '--type', 'sha256', digest16], stdout=subprocess.PIPE) v.result(process.returncode == 0) return Digest32(process.stdout.decode().strip()) @@ -203,7 +203,7 @@ def fetch_with_nix_prefetch_url( digest: Digest16) -> str: v.status('Fetching %s' % url) process = subprocess.run( - ['nix-prefetch-url', '--print-path', url, digest], capture_output=True) + ['nix-prefetch-url', '--print-path', url, digest], stdout=subprocess.PIPE) v.result(process.returncode == 0) prefetch_digest, path, empty = process.stdout.decode().split('\n') assert empty == '' @@ -417,7 +417,7 @@ def git_get_tarball(v: Verification, channel: Channel) -> str: v.status('Putting tarball in Nix store') process = subprocess.run( - ['nix-store', '--add', output_filename], capture_output=True) + ['nix-store', '--add', output_filename], stdout=subprocess.PIPE) v.result(process.returncode == 0) store_tarball = process.stdout.decode().strip() @@ -483,7 +483,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: '--format=%ct-%h', '--abbrev=11', channel.git_revision], - capture_output=True) + stdout=subprocess.PIPE) v.result(process.returncode == 0 and process.stdout != b'') return '%s-%s' % (os.path.basename(channel.git_repo), process.stdout.decode().strip()) -- 2.44.1 From b5964ec35e259f36550d6e8c7fa60412db05a049 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 13:41:49 -0700 Subject: [PATCH 056/100] Build like a real Python package --- default.nix | 11 +++++++++++ pinch.py | 3 ++- setup.py | 8 ++++++++ shell.nix | 10 ---------- 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 default.nix create mode 100644 setup.py delete mode 100644 shell.nix diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..942ef10 --- /dev/null +++ b/default.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } }: +pkgs.python3Packages.callPackage +({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: + buildPythonPackage rec { + pname = "pinch"; + version = "1.3"; + src = lib.cleanSource ./.; + checkInputs = [ nix git autopep8 mypy pylint ]; + doCheck = true; + checkPhase = "./test.sh"; + }) { } diff --git a/pinch.py b/pinch.py index b761313..5f25015 100644 --- a/pinch.py +++ b/pinch.py @@ -602,4 +602,5 @@ def main() -> None: args.func(args) -main() +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f8647e6 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup + +setup( + name="pinch", + version="1.4", + py_modules=['pinch'], + entry_points={"console_scripts": ["pinch = pinch:main"]}, +) diff --git a/shell.nix b/shell.nix deleted file mode 100644 index b408ea4..0000000 --- a/shell.nix +++ /dev/null @@ -1,10 +0,0 @@ -{ pkgs ? import { } }: -pkgs.mkShell rec { - doCheck = true; - buildInputs = with pkgs; [ python3 ]; - checkInputs = with pkgs; [ - mypy - python3Packages.autopep8 - python3Packages.pylint - ]; -} -- 2.44.1 From b365b52416a5c3d79b96ca9401b5031db4bddba2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 16:21:18 -0700 Subject: [PATCH 057/100] Omit build directories from mypy run --- test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 9485418..5df81bf 100755 --- a/test.sh +++ b/test.sh @@ -4,7 +4,8 @@ set -e PARALLELISM=4 -find . -name '*.py' -print0 | xargs -0 mypy --strict --ignore-missing-imports +find . -name build -prune -o -name dist -prune -o -name '*.py' -print0 | + xargs -0 mypy --strict --ignore-missing-imports for test in tests/*;do if [ ! -d "$test" ];then -- 2.44.1 From 420bd8c9c431223f9299477dc885d2c9d9f077c5 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 11 Jun 2020 16:38:34 -0700 Subject: [PATCH 058/100] Separate test and lint Don't lint in package build. Lint in git pre-commit hook. --- default.nix | 4 ++-- git-pre-commit-hook | 2 +- test.sh | 24 ++++++++++++++---------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/default.nix b/default.nix index 942ef10..f8d040a 100644 --- a/default.nix +++ b/default.nix @@ -1,11 +1,11 @@ -{ pkgs ? import { } }: +{ pkgs ? import { }, lint ? false }: pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; version = "1.3"; src = lib.cleanSource ./.; - checkInputs = [ nix git autopep8 mypy pylint ]; + checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; checkPhase = "./test.sh"; }) { } diff --git a/git-pre-commit-hook b/git-pre-commit-hook index 7db6818..85b4445 100755 --- a/git-pre-commit-hook +++ b/git-pre-commit-hook @@ -17,6 +17,6 @@ D=$(mktemp -d) git checkout-index --prefix="$D/" -a pushd "$D" -nix-shell --run ./test.sh +nix-shell --arg lint true --run './test.sh lint' popd diff --git a/test.sh b/test.sh index 5df81bf..16d9c05 100755 --- a/test.sh +++ b/test.sh @@ -15,14 +15,18 @@ done find . -name '*_test.py' -print0 | xargs -0 -r -n1 python3 -find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,subprocess-run-check,too-few-public-methods - -formatting_needs_fixing=$( - find . -name '*.py' -print0 | - xargs -P "$PARALLELISM" -0 -n1 autopep8 --diff -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -) -if [[ "$formatting_needs_fixing" ]];then - echo "Formatting needs fixing:" - echo "$formatting_needs_fixing" - exit 1 +if [ "$1" = lint ];then + + find . -name '*.py' -print0 | xargs -0 pylint --reports=n --persistent=n --ignore-imports=y -d fixme,invalid-name,missing-docstring,subprocess-run-check,too-few-public-methods + + formatting_needs_fixing=$( + find . -name '*.py' -print0 | + xargs -P "$PARALLELISM" -0 -n1 autopep8 --diff -aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + ) + if [[ "$formatting_needs_fixing" ]];then + echo "Formatting needs fixing:" + echo "$formatting_needs_fixing" + exit 1 + fi + fi -- 2.44.1 From 88af5903f7d0c6490a236167b6e07d831fc67a62 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 02:16:39 -0700 Subject: [PATCH 059/100] Support "log.showSignature = true" Don't let "log.showSignature = true" in global git config cause signature check output to appear in the release name. Bad excuses for not including a test: * Would have to add GPG as a dependency. * Generating a GPG key during the test would be slow. * Making a local "global" git config in the test would be troublesome. --- pinch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pinch.py b/pinch.py index 5f25015..4c8a51f 100644 --- a/pinch.py +++ b/pinch.py @@ -482,6 +482,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: '-n1', '--format=%ct-%h', '--abbrev=11', + '--no-show-signature', channel.git_revision], stdout=subprocess.PIPE) v.result(process.returncode == 0 and process.stdout != b'') -- 2.44.1 From 7c4de64c46013216cfef1d7fb34878b317a11ab6 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 16:57:47 -0700 Subject: [PATCH 060/100] Support multiple versions of mypy 0.701 needs the type-ignore directives 0.761 needs --no-warn-unused-ignores so it doesn't freak out about the type-ignore directives for 0.701. --- pinch.py | 6 +++--- test.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pinch.py b/pinch.py index 4c8a51f..47f4e34 100644 --- a/pinch.py +++ b/pinch.py @@ -131,7 +131,7 @@ def fetch(v: Verification, channel: Channel) -> None: request = urllib.request.urlopen(channel.channel_url, timeout=10) channel.channel_html = request.read() channel.forwarded_url = request.geturl() - v.result(request.status == 200) + v.result(request.status == 200) # type: ignore # (for old mypy) v.check('Got forwarded', channel.channel_url != channel.forwarded_url) @@ -212,7 +212,7 @@ def fetch_with_nix_prefetch_url( v.status("Verifying file digest") file_digest = digest_file(path) v.result(file_digest == digest) - return path + return path # type: ignore # (for old mypy) def fetch_resources(v: Verification, channel: Channel) -> None: @@ -423,7 +423,7 @@ def git_get_tarball(v: Verification, channel: Channel) -> str: os.makedirs(os.path.dirname(cache_file), exist_ok=True) open(cache_file, 'w').write(store_tarball) - return store_tarball + return store_tarball # type: ignore # (for old mypy) def check_channel_metadata( diff --git a/test.sh b/test.sh index 16d9c05..fed0065 100755 --- a/test.sh +++ b/test.sh @@ -5,7 +5,7 @@ set -e PARALLELISM=4 find . -name build -prune -o -name dist -prune -o -name '*.py' -print0 | - xargs -0 mypy --strict --ignore-missing-imports + xargs -0 mypy --strict --ignore-missing-imports --no-warn-unused-ignores for test in tests/*;do if [ ! -d "$test" ];then -- 2.44.1 From 6a17c53ee6bcb74bbf32ca4a616ceadb24701534 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 17:02:04 -0700 Subject: [PATCH 061/100] Release: 1.5.1 --- default.nix | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index f8d040a..c2c751c 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; - version = "1.3"; + version = "1.5.1"; src = lib.cleanSource ./.; checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; diff --git a/setup.py b/setup.py index f8647e6..b7d3a26 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pinch", - version="1.4", + version="1.5.1", py_modules=['pinch'], entry_points={"console_scripts": ["pinch = pinch:main"]}, ) -- 2.44.1 From d44818a9da861ef3f69968ad23337ad362145e92 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:10:08 -0700 Subject: [PATCH 062/100] Introduce SearchPath type --- pinch.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 47f4e34..0c0103b 100644 --- a/pinch.py +++ b/pinch.py @@ -50,7 +50,11 @@ class ChannelTableEntry(types.SimpleNamespace): url: str -class Channel(types.SimpleNamespace): +class SearchPath(types.SimpleNamespace): + release_name: str + + +class Channel(SearchPath): alias_of: str channel_html: bytes channel_url: str @@ -59,7 +63,6 @@ class Channel(types.SimpleNamespace): git_repo: str git_revision: str old_git_revision: str - release_name: str table: Dict[str, ChannelTableEntry] -- 2.44.1 From f8f5b12546942d8d801f3cda70c798d4a4e4ae13 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:19:28 -0700 Subject: [PATCH 063/100] Introduce read_search_path() --- pinch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 0c0103b..cc653da 100644 --- a/pinch.py +++ b/pinch.py @@ -493,6 +493,10 @@ def git_revision_name(v: Verification, channel: Channel) -> str: process.stdout.decode().strip()) +def read_search_path(conf: configparser.SectionProxy) -> Channel: + return Channel(**dict(conf.items())) + + def read_config(filename: str) -> configparser.ConfigParser: config = configparser.ConfigParser() config.read_file(open(filename), filename) @@ -506,7 +510,7 @@ def pin(args: argparse.Namespace) -> None: if args.channels and section not in args.channels: continue - channel = Channel(**dict(config[section].items())) + channel = read_search_path(config[section]) if hasattr(channel, 'alias_of'): assert not hasattr(channel, 'git_repo') @@ -544,7 +548,7 @@ def fetch_channel( v, conf['tarball_url'], Digest16( conf['tarball_sha256'])) - channel = Channel(**dict(conf.items())) + channel = read_search_path(conf) ensure_git_rev_available(v, channel) return git_get_tarball(v, channel) -- 2.44.1 From faff8642d1b68a819a41186ec720a0f00f43f712 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:28:27 -0700 Subject: [PATCH 064/100] Re-order definitions --- pinch.py | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pinch.py b/pinch.py index cc653da..a8af6a6 100644 --- a/pinch.py +++ b/pinch.py @@ -38,34 +38,6 @@ xdg = XDG( os.path.expanduser('~/.cache'))) -Digest16 = NewType('Digest16', str) -Digest32 = NewType('Digest32', str) - - -class ChannelTableEntry(types.SimpleNamespace): - absolute_url: str - digest: Digest16 - file: str - size: int - url: str - - -class SearchPath(types.SimpleNamespace): - release_name: str - - -class Channel(SearchPath): - alias_of: str - channel_html: bytes - channel_url: str - forwarded_url: str - git_ref: str - git_repo: str - git_revision: str - old_git_revision: str - table: Dict[str, ChannelTableEntry] - - class VerificationError(Exception): pass @@ -101,6 +73,34 @@ class Verification: self.result(True) +Digest16 = NewType('Digest16', str) +Digest32 = NewType('Digest32', str) + + +class ChannelTableEntry(types.SimpleNamespace): + absolute_url: str + digest: Digest16 + file: str + size: int + url: str + + +class SearchPath(types.SimpleNamespace): + release_name: str + + +class Channel(SearchPath): + alias_of: str + channel_html: bytes + channel_url: str + forwarded_url: str + git_ref: str + git_repo: str + git_revision: str + old_git_revision: str + table: Dict[str, ChannelTableEntry] + + def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: def throw(error: OSError) -> None: -- 2.44.1 From 3aa393bbc0cd6dd203cd69024112afc62d521581 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:29:55 -0700 Subject: [PATCH 065/100] Make pin() a Channel method --- pinch.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/pinch.py b/pinch.py index a8af6a6..d346a14 100644 --- a/pinch.py +++ b/pinch.py @@ -100,6 +100,25 @@ class Channel(SearchPath): old_git_revision: str table: Dict[str, ChannelTableEntry] + def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + if hasattr(self, 'alias_of'): + assert not hasattr(self, 'git_repo') + return + + if hasattr(self, 'git_revision'): + self.old_git_revision = self.git_revision + del self.git_revision + + if 'channel_url' in conf: + pin_channel(v, self) + conf['release_name'] = self.release_name + conf['tarball_url'] = self.table['nixexprs.tar.xz'].absolute_url + conf['tarball_sha256'] = self.table['nixexprs.tar.xz'].digest + else: + git_fetch(v, self) + conf['release_name'] = git_revision_name(v, self) + conf['git_revision'] = self.git_revision + def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: @@ -512,23 +531,7 @@ def pin(args: argparse.Namespace) -> None: channel = read_search_path(config[section]) - if hasattr(channel, 'alias_of'): - assert not hasattr(channel, 'git_repo') - continue - - if hasattr(channel, 'git_revision'): - channel.old_git_revision = channel.git_revision - del channel.git_revision - - if 'channel_url' in config[section]: - pin_channel(v, channel) - config[section]['release_name'] = channel.release_name - config[section]['tarball_url'] = channel.table['nixexprs.tar.xz'].absolute_url - config[section]['tarball_sha256'] = channel.table['nixexprs.tar.xz'].digest - else: - git_fetch(v, channel) - config[section]['release_name'] = git_revision_name(v, channel) - config[section]['git_revision'] = channel.git_revision + channel.pin(v, config[section]) with open(args.channels_file, 'w') as configfile: config.write(configfile) -- 2.44.1 From 93d2dafe3922ef5230ae41f759563fa5c3aa180e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:43:06 -0700 Subject: [PATCH 066/100] Make fetch_channel a Channel method --- pinch.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/pinch.py b/pinch.py index d346a14..632994b 100644 --- a/pinch.py +++ b/pinch.py @@ -119,6 +119,21 @@ class Channel(SearchPath): conf['release_name'] = git_revision_name(v, self) conf['git_revision'] = self.git_revision + def fetch(self, v: Verification, section: str, + conf: configparser.SectionProxy) -> str: + if 'git_repo' not in conf or 'release_name' not in conf: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + + if 'channel_url' in conf: + return fetch_with_nix_prefetch_url( + v, conf['tarball_url'], Digest16( + conf['tarball_sha256'])) + + ensure_git_rev_available(v, self) + return git_get_tarball(v, self) + def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: @@ -537,25 +552,6 @@ def pin(args: argparse.Namespace) -> None: config.write(configfile) -def fetch_channel( - v: Verification, - section: str, - conf: configparser.SectionProxy) -> str: - if 'git_repo' not in conf or 'release_name' not in conf: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) - - if 'channel_url' in conf: - return fetch_with_nix_prefetch_url( - v, conf['tarball_url'], Digest16( - conf['tarball_sha256'])) - - channel = read_search_path(conf) - ensure_git_rev_available(v, channel) - return git_get_tarball(v, channel) - - def update(args: argparse.Namespace) -> None: v = Verification() config = configparser.ConfigParser() @@ -566,7 +562,8 @@ def update(args: argparse.Namespace) -> None: if 'alias_of' in config[section]: assert 'git_repo' not in config[section] continue - tarball = fetch_channel(v, section, config[section]) + sp = read_search_path(config[section]) + tarball = sp.fetch(v, section, config[section]) if section in exprs: raise Exception('Duplicate channel "%s"' % section) exprs[section] = ( -- 2.44.1 From 411b705ecc2b1210dfce0bb43ea5900b71b6281d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Fri, 12 Jun 2020 23:45:56 -0700 Subject: [PATCH 067/100] read_search_path returns a SearchPath --- pinch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pinch.py b/pinch.py index 632994b..7049b69 100644 --- a/pinch.py +++ b/pinch.py @@ -527,7 +527,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: process.stdout.decode().strip()) -def read_search_path(conf: configparser.SectionProxy) -> Channel: +def read_search_path(conf: configparser.SectionProxy) -> SearchPath: return Channel(**dict(conf.items())) @@ -544,9 +544,9 @@ def pin(args: argparse.Namespace) -> None: if args.channels and section not in args.channels: continue - channel = read_search_path(config[section]) + sp = read_search_path(config[section]) - channel.pin(v, config[section]) + sp.pin(v, config[section]) with open(args.channels_file, 'w') as configfile: config.write(configfile) -- 2.44.1 From 4603b1a7c9e6463deba8c974e4181bd5ce2be9ef Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 00:05:43 -0700 Subject: [PATCH 068/100] Duplicate searchpath checking in one place --- pinch.py | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/pinch.py b/pinch.py index 7049b69..2698226 100644 --- a/pinch.py +++ b/pinch.py @@ -537,6 +537,18 @@ def read_config(filename: str) -> configparser.ConfigParser: return config +def read_config_files( + filenames: Iterable[str]) -> Dict[str, configparser.SectionProxy]: + merged_config: Dict[str, configparser.SectionProxy] = {} + for file in filenames: + config = read_config(file) + for section in config.sections(): + if section in merged_config: + raise Exception('Duplicate channel "%s"' % section) + merged_config[section] = config[section] + return merged_config + + def pin(args: argparse.Namespace) -> None: v = Verification() config = read_config(args.channels_file) @@ -554,28 +566,21 @@ def pin(args: argparse.Namespace) -> None: def update(args: argparse.Namespace) -> None: v = Verification() - config = configparser.ConfigParser() exprs: Dict[str, str] = {} - configs = [read_config(filename) for filename in args.channels_file] - for config in configs: - for section in config.sections(): - if 'alias_of' in config[section]: - assert 'git_repo' not in config[section] - continue - sp = read_search_path(config[section]) - tarball = sp.fetch(v, section, config[section]) - if section in exprs: - raise Exception('Duplicate channel "%s"' % section) - exprs[section] = ( - 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % - (config[section]['release_name'], tarball)) - - for config in configs: - for section in config.sections(): - if 'alias_of' in config[section]: - if section in exprs: - raise Exception('Duplicate channel "%s"' % section) - exprs[section] = exprs[str(config[section]['alias_of'])] + config = read_config_files(args.channels_file) + for section in config: + if 'alias_of' in config[section]: + assert 'git_repo' not in config[section] + continue + sp = read_search_path(config[section]) + tarball = sp.fetch(v, section, config[section]) + exprs[section] = ( + 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % + (config[section]['release_name'], tarball)) + + for section in config: + if 'alias_of' in config[section]: + exprs[section] = exprs[str(config[section]['alias_of'])] command = [ 'nix-env', -- 2.44.1 From 2fa9cbea8f21396556223dbf07e01e6af87ff5e8 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 00:20:09 -0700 Subject: [PATCH 069/100] Split AliasSearchPath out of Channel --- pinch.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pinch.py b/pinch.py index 2698226..34c63f1 100644 --- a/pinch.py +++ b/pinch.py @@ -1,3 +1,4 @@ +from abc import ABC, abstractmethod import argparse import configparser import filecmp @@ -85,12 +86,22 @@ class ChannelTableEntry(types.SimpleNamespace): url: str -class SearchPath(types.SimpleNamespace): +class SearchPath(types.SimpleNamespace, ABC): release_name: str + @abstractmethod + def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + pass -class Channel(SearchPath): + +class AliasSearchPath(SearchPath): alias_of: str + + def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + assert not hasattr(self, 'git_repo') + + +class Channel(SearchPath): channel_html: bytes channel_url: str forwarded_url: str @@ -101,10 +112,6 @@ class Channel(SearchPath): table: Dict[str, ChannelTableEntry] def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: - if hasattr(self, 'alias_of'): - assert not hasattr(self, 'git_repo') - return - if hasattr(self, 'git_revision'): self.old_git_revision = self.git_revision del self.git_revision @@ -528,6 +535,8 @@ def git_revision_name(v: Verification, channel: Channel) -> str: def read_search_path(conf: configparser.SectionProxy) -> SearchPath: + if 'alias_of' in conf: + return AliasSearchPath(**dict(conf.items())) return Channel(**dict(conf.items())) -- 2.44.1 From b896966bd140bc9c3c965c70597517ede83d5b86 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 00:24:24 -0700 Subject: [PATCH 070/100] Use type rather than field presence --- pinch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pinch.py b/pinch.py index 34c63f1..bfc6ac6 100644 --- a/pinch.py +++ b/pinch.py @@ -578,10 +578,10 @@ def update(args: argparse.Namespace) -> None: exprs: Dict[str, str] = {} config = read_config_files(args.channels_file) for section in config: - if 'alias_of' in config[section]: + sp = read_search_path(config[section]) + if isinstance(sp, AliasSearchPath): assert 'git_repo' not in config[section] continue - sp = read_search_path(config[section]) tarball = sp.fetch(v, section, config[section]) exprs[section] = ( 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % -- 2.44.1 From 7d2c278fa53b6c76eff793b1b38d588bb4cb884d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 09:24:41 -0700 Subject: [PATCH 071/100] Rename Channel -> TarrableSearchPath --- pinch.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/pinch.py b/pinch.py index bfc6ac6..662b425 100644 --- a/pinch.py +++ b/pinch.py @@ -101,7 +101,7 @@ class AliasSearchPath(SearchPath): assert not hasattr(self, 'git_repo') -class Channel(SearchPath): +class TarrableSearchPath(SearchPath): channel_html: bytes channel_url: str forwarded_url: str @@ -170,7 +170,7 @@ def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: return filecmp.cmpfiles(a, b, files, shallow=False) -def fetch(v: Verification, channel: Channel) -> None: +def fetch(v: Verification, channel: TarrableSearchPath) -> None: v.status('Fetching channel') request = urllib.request.urlopen(channel.channel_url, timeout=10) channel.channel_html = request.read() @@ -179,7 +179,7 @@ def fetch(v: Verification, channel: Channel) -> None: v.check('Got forwarded', channel.channel_url != channel.forwarded_url) -def parse_channel(v: Verification, channel: Channel) -> None: +def parse_channel(v: Verification, channel: TarrableSearchPath) -> None: v.status('Parsing channel description as XML') d = xml.dom.minidom.parseString(channel.channel_html) v.ok() @@ -259,7 +259,7 @@ def fetch_with_nix_prefetch_url( return path # type: ignore # (for old mypy) -def fetch_resources(v: Verification, channel: Channel) -> None: +def fetch_resources(v: Verification, channel: TarrableSearchPath) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: fields = channel.table[resource] fields.absolute_url = urllib.parse.urljoin( @@ -279,7 +279,7 @@ def git_cachedir(git_repo: str) -> str: digest_string(git_repo.encode())) -def tarball_cache_file(channel: Channel) -> str: +def tarball_cache_file(channel: TarrableSearchPath) -> str: return os.path.join( xdg.XDG_CACHE_HOME, 'pinch/git-tarball', @@ -289,7 +289,7 @@ def tarball_cache_file(channel: Channel) -> str: channel.release_name)) -def verify_git_ancestry(v: Verification, channel: Channel) -> None: +def verify_git_ancestry(v: Verification, channel: TarrableSearchPath) -> None: cachedir = git_cachedir(channel.git_repo) v.status('Verifying rev is an ancestor of ref') process = subprocess.run(['git', @@ -315,7 +315,7 @@ def verify_git_ancestry(v: Verification, channel: Channel) -> None: v.result(process.returncode == 0) -def git_fetch(v: Verification, channel: Channel) -> None: +def git_fetch(v: Verification, channel: TarrableSearchPath) -> None: # It would be nice if we could share the nix git cache, but as of the time # of writing it is transitioning from gitv2 (deprecated) to gitv3 (not ready # yet), and trying to straddle them both is too far into nix implementation @@ -357,7 +357,9 @@ def git_fetch(v: Verification, channel: Channel) -> None: verify_git_ancestry(v, channel) -def ensure_git_rev_available(v: Verification, channel: Channel) -> None: +def ensure_git_rev_available( + v: Verification, + channel: TarrableSearchPath) -> None: cachedir = git_cachedir(channel.git_repo) if os.path.exists(cachedir): v.status('Checking if we already have this rev:') @@ -376,7 +378,7 @@ def ensure_git_rev_available(v: Verification, channel: Channel) -> None: def compare_tarball_and_git( v: Verification, - channel: Channel, + channel: TarrableSearchPath, channel_contents: str, git_contents: str) -> None: v.status('Comparing channel tarball with git checkout') @@ -407,7 +409,10 @@ def compare_tarball_and_git( len(benign_errors) == len(expected_errors)) -def extract_tarball(v: Verification, channel: Channel, dest: str) -> None: +def extract_tarball( + v: Verification, + channel: TarrableSearchPath, + dest: str) -> None: v.status('Extracting tarball %s' % channel.table['nixexprs.tar.xz'].file) shutil.unpack_archive( @@ -416,7 +421,10 @@ def extract_tarball(v: Verification, channel: Channel, dest: str) -> None: v.ok() -def git_checkout(v: Verification, channel: Channel, dest: str) -> None: +def git_checkout( + v: Verification, + channel: TarrableSearchPath, + dest: str) -> None: v.status('Checking out corresponding git revision') git = subprocess.Popen(['git', '-C', @@ -433,7 +441,7 @@ def git_checkout(v: Verification, channel: Channel, dest: str) -> None: v.result(git.returncode == 0 and tar.returncode == 0) -def git_get_tarball(v: Verification, channel: Channel) -> str: +def git_get_tarball(v: Verification, channel: TarrableSearchPath) -> str: cache_file = tarball_cache_file(channel) if os.path.exists(cache_file): cached_tarball = open(cache_file).read(9999) @@ -472,7 +480,7 @@ def git_get_tarball(v: Verification, channel: Channel) -> str: def check_channel_metadata( v: Verification, - channel: Channel, + channel: TarrableSearchPath, channel_contents: str) -> None: v.status('Verifying git commit in channel tarball') v.result( @@ -494,7 +502,9 @@ def check_channel_metadata( v.result(channel.release_name.endswith(version_suffix)) -def check_channel_contents(v: Verification, channel: Channel) -> None: +def check_channel_contents( + v: Verification, + channel: TarrableSearchPath) -> None: with tempfile.TemporaryDirectory() as channel_contents, \ tempfile.TemporaryDirectory() as git_contents: @@ -509,7 +519,7 @@ def check_channel_contents(v: Verification, channel: Channel) -> None: v.ok() -def pin_channel(v: Verification, channel: Channel) -> None: +def pin_channel(v: Verification, channel: TarrableSearchPath) -> None: fetch(v, channel) parse_channel(v, channel) fetch_resources(v, channel) @@ -517,7 +527,7 @@ def pin_channel(v: Verification, channel: Channel) -> None: check_channel_contents(v, channel) -def git_revision_name(v: Verification, channel: Channel) -> str: +def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: v.status('Getting commit date') process = subprocess.run(['git', '-C', @@ -537,7 +547,7 @@ def git_revision_name(v: Verification, channel: Channel) -> str: def read_search_path(conf: configparser.SectionProxy) -> SearchPath: if 'alias_of' in conf: return AliasSearchPath(**dict(conf.items())) - return Channel(**dict(conf.items())) + return TarrableSearchPath(**dict(conf.items())) def read_config(filename: str) -> configparser.ConfigParser: -- 2.44.1 From 4a82be40d532c3916b54453b9e238b09721b7994 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 09:27:41 -0700 Subject: [PATCH 072/100] Introduce types {Git,Channel}SearchPath --- pinch.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 662b425..b76c8da 100644 --- a/pinch.py +++ b/pinch.py @@ -142,6 +142,14 @@ class TarrableSearchPath(SearchPath): return git_get_tarball(v, self) +class GitSearchPath(TarrableSearchPath): + pass + + +class ChannelSearchPath(TarrableSearchPath): + pass + + def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: def throw(error: OSError) -> None: @@ -547,7 +555,9 @@ def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: def read_search_path(conf: configparser.SectionProxy) -> SearchPath: if 'alias_of' in conf: return AliasSearchPath(**dict(conf.items())) - return TarrableSearchPath(**dict(conf.items())) + if 'channel_url' in conf: + return ChannelSearchPath(**dict(conf.items())) + return GitSearchPath(**dict(conf.items())) def read_config(filename: str) -> configparser.ConfigParser: -- 2.44.1 From a67cfec99a43c3a437aeab3257552c035ebab5bf Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 09:46:44 -0700 Subject: [PATCH 073/100] Polymorphic pin() --- pinch.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pinch.py b/pinch.py index b76c8da..a90ec3b 100644 --- a/pinch.py +++ b/pinch.py @@ -101,7 +101,9 @@ class AliasSearchPath(SearchPath): assert not hasattr(self, 'git_repo') -class TarrableSearchPath(SearchPath): +# (This lint-disable is for pylint bug https://github.com/PyCQA/pylint/issues/179 +# which is fixed in pylint 2.5.) +class TarrableSearchPath(SearchPath, ABC): # pylint: disable=abstract-method channel_html: bytes channel_url: str forwarded_url: str @@ -111,21 +113,6 @@ class TarrableSearchPath(SearchPath): old_git_revision: str table: Dict[str, ChannelTableEntry] - def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: - if hasattr(self, 'git_revision'): - self.old_git_revision = self.git_revision - del self.git_revision - - if 'channel_url' in conf: - pin_channel(v, self) - conf['release_name'] = self.release_name - conf['tarball_url'] = self.table['nixexprs.tar.xz'].absolute_url - conf['tarball_sha256'] = self.table['nixexprs.tar.xz'].digest - else: - git_fetch(v, self) - conf['release_name'] = git_revision_name(v, self) - conf['git_revision'] = self.git_revision - def fetch(self, v: Verification, section: str, conf: configparser.SectionProxy) -> str: if 'git_repo' not in conf or 'release_name' not in conf: @@ -143,11 +130,27 @@ class TarrableSearchPath(SearchPath): class GitSearchPath(TarrableSearchPath): - pass + def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + if hasattr(self, 'git_revision'): + self.old_git_revision = self.git_revision + del self.git_revision + + git_fetch(v, self) + conf['release_name'] = git_revision_name(v, self) + conf['git_revision'] = self.git_revision class ChannelSearchPath(TarrableSearchPath): - pass + def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + if hasattr(self, 'git_revision'): + self.old_git_revision = self.git_revision + del self.git_revision + + pin_channel(v, self) + conf['release_name'] = self.release_name + conf['tarball_url'] = self.table['nixexprs.tar.xz'].absolute_url + conf['tarball_sha256'] = self.table['nixexprs.tar.xz'].digest + conf['git_revision'] = self.git_revision def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: -- 2.44.1 From b3ea39b882d1f48781edff1863181274753b32b5 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Sat, 13 Jun 2020 09:56:32 -0700 Subject: [PATCH 074/100] polymorphic fetch() --- pinch.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pinch.py b/pinch.py index a90ec3b..58f7a32 100644 --- a/pinch.py +++ b/pinch.py @@ -113,21 +113,6 @@ class TarrableSearchPath(SearchPath, ABC): # pylint: disable=abstract-method old_git_revision: str table: Dict[str, ChannelTableEntry] - def fetch(self, v: Verification, section: str, - conf: configparser.SectionProxy) -> str: - if 'git_repo' not in conf or 'release_name' not in conf: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) - - if 'channel_url' in conf: - return fetch_with_nix_prefetch_url( - v, conf['tarball_url'], Digest16( - conf['tarball_sha256'])) - - ensure_git_rev_available(v, self) - return git_get_tarball(v, self) - class GitSearchPath(TarrableSearchPath): def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: @@ -139,6 +124,16 @@ class GitSearchPath(TarrableSearchPath): conf['release_name'] = git_revision_name(v, self) conf['git_revision'] = self.git_revision + def fetch(self, v: Verification, section: str, + conf: configparser.SectionProxy) -> str: + if 'git_repo' not in conf or 'release_name' not in conf: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + + ensure_git_rev_available(v, self) + return git_get_tarball(v, self) + class ChannelSearchPath(TarrableSearchPath): def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: @@ -152,6 +147,19 @@ class ChannelSearchPath(TarrableSearchPath): conf['tarball_sha256'] = self.table['nixexprs.tar.xz'].digest conf['git_revision'] = self.git_revision + # Lint TODO: Put tarball_url and tarball_sha256 in ChannelSearchPath + # pylint: disable=no-self-use + def fetch(self, v: Verification, section: str, + conf: configparser.SectionProxy) -> str: + if 'git_repo' not in conf or 'release_name' not in conf: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + + return fetch_with_nix_prefetch_url( + v, conf['tarball_url'], Digest16( + conf['tarball_sha256'])) + def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: -- 2.44.1 From 0a4ff8dde934f9921371da9600e473dd090a7425 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 11:19:26 -0700 Subject: [PATCH 075/100] Keep config mutability at top level --- pinch.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/pinch.py b/pinch.py index 58f7a32..f5fbb48 100644 --- a/pinch.py +++ b/pinch.py @@ -22,8 +22,10 @@ from typing import ( Dict, Iterable, List, + NamedTuple, NewType, Tuple, + Union, ) # Use xdg module when it's less painful to have as a dependency @@ -86,19 +88,38 @@ class ChannelTableEntry(types.SimpleNamespace): url: str -class SearchPath(types.SimpleNamespace, ABC): +class AliasPin(NamedTuple): + pass + + +class GitPin(NamedTuple): + git_revision: str release_name: str + +class ChannelPin(NamedTuple): + git_revision: str + release_name: str + tarball_url: str + tarball_sha256: str + + +Pin = Union[AliasPin, GitPin, ChannelPin] + + +class SearchPath(types.SimpleNamespace, ABC): + @abstractmethod - def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + def pin(self, v: Verification) -> Pin: pass class AliasSearchPath(SearchPath): alias_of: str - def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + def pin(self, v: Verification) -> AliasPin: assert not hasattr(self, 'git_repo') + return AliasPin() # (This lint-disable is for pylint bug https://github.com/PyCQA/pylint/issues/179 @@ -109,20 +130,19 @@ class TarrableSearchPath(SearchPath, ABC): # pylint: disable=abstract-method forwarded_url: str git_ref: str git_repo: str - git_revision: str old_git_revision: str table: Dict[str, ChannelTableEntry] class GitSearchPath(TarrableSearchPath): - def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + def pin(self, v: Verification) -> GitPin: if hasattr(self, 'git_revision'): self.old_git_revision = self.git_revision del self.git_revision git_fetch(v, self) - conf['release_name'] = git_revision_name(v, self) - conf['git_revision'] = self.git_revision + return GitPin(release_name=git_revision_name(v, self), + git_revision=self.git_revision) def fetch(self, v: Verification, section: str, conf: configparser.SectionProxy) -> str: @@ -136,16 +156,17 @@ class GitSearchPath(TarrableSearchPath): class ChannelSearchPath(TarrableSearchPath): - def pin(self, v: Verification, conf: configparser.SectionProxy) -> None: + def pin(self, v: Verification) -> ChannelPin: if hasattr(self, 'git_revision'): self.old_git_revision = self.git_revision del self.git_revision pin_channel(v, self) - conf['release_name'] = self.release_name - conf['tarball_url'] = self.table['nixexprs.tar.xz'].absolute_url - conf['tarball_sha256'] = self.table['nixexprs.tar.xz'].digest - conf['git_revision'] = self.git_revision + return ChannelPin( + release_name=self.release_name, + tarball_url=self.table['nixexprs.tar.xz'].absolute_url, + tarball_sha256=self.table['nixexprs.tar.xz'].digest, + git_revision=self.git_revision) # Lint TODO: Put tarball_url and tarball_sha256 in ChannelSearchPath # pylint: disable=no-self-use @@ -598,7 +619,7 @@ def pin(args: argparse.Namespace) -> None: sp = read_search_path(config[section]) - sp.pin(v, config[section]) + config[section].update(sp.pin(v)._asdict()) with open(args.channels_file, 'w') as configfile: config.write(configfile) -- 2.44.1 From 7f4c3ace15d9dc80a366743e5f7a38ec7b2af201 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 11:38:12 -0700 Subject: [PATCH 076/100] Require type to be specified in config --- channels | 2 ++ default.nix | 2 +- pinch.py | 13 ++++++++----- setup.py | 2 +- tests/alias.sh | 1 + tests/lib/test-setup.sh | 1 + tests/multi-update.sh | 1 + tests/reject-partially-unpinned.sh | 1 + 8 files changed, 16 insertions(+), 7 deletions(-) diff --git a/channels b/channels index dc1f754..a4b984d 100644 --- a/channels +++ b/channels @@ -1,9 +1,11 @@ [nixos] +type = channel channel_url = https://channels.nixos.org/nixos-20.03 git_repo = https://github.com/NixOS/nixpkgs.git git_ref = nixos-20.03 [nixos-hardware] +type = git git_repo = https://github.com/NixOS/nixos-hardware.git git_ref = master diff --git a/default.nix b/default.nix index c2c751c..3209685 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; - version = "1.5.1"; + version = "2.0.0-pre"; src = lib.cleanSource ./.; checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; diff --git a/pinch.py b/pinch.py index f5fbb48..ea380a3 100644 --- a/pinch.py +++ b/pinch.py @@ -22,9 +22,11 @@ from typing import ( Dict, Iterable, List, + Mapping, NamedTuple, NewType, Tuple, + Type, Union, ) @@ -585,11 +587,12 @@ def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: def read_search_path(conf: configparser.SectionProxy) -> SearchPath: - if 'alias_of' in conf: - return AliasSearchPath(**dict(conf.items())) - if 'channel_url' in conf: - return ChannelSearchPath(**dict(conf.items())) - return GitSearchPath(**dict(conf.items())) + mapping: Mapping[str, Type[SearchPath]] = { + 'alias': AliasSearchPath, + 'channel': ChannelSearchPath, + 'git': GitSearchPath, + } + return mapping[conf['type']](**dict(conf.items())) def read_config(filename: str) -> configparser.ConfigParser: diff --git a/setup.py b/setup.py index b7d3a26..e5d1ebe 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pinch", - version="1.5.1", + version="2.0.0-pre", py_modules=['pinch'], entry_points={"console_scripts": ["pinch = pinch:main"]}, ) diff --git a/tests/alias.sh b/tests/alias.sh index 928ceab..dce5747 100755 --- a/tests/alias.sh +++ b/tests/alias.sh @@ -6,6 +6,7 @@ foo_setup cat >> "$conf" < "$conf" < "$conf2" <> "$conf" < Date: Mon, 15 Jun 2020 12:30:15 -0700 Subject: [PATCH 077/100] Move pin_channel() into ChannelSearchPath.pin() --- pinch.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pinch.py b/pinch.py index ea380a3..78c6d52 100644 --- a/pinch.py +++ b/pinch.py @@ -163,7 +163,11 @@ class ChannelSearchPath(TarrableSearchPath): self.old_git_revision = self.git_revision del self.git_revision - pin_channel(v, self) + fetch(v, self) + parse_channel(v, self) + fetch_resources(v, self) + ensure_git_rev_available(v, self) + check_channel_contents(v, self) return ChannelPin( release_name=self.release_name, tarball_url=self.table['nixexprs.tar.xz'].absolute_url, @@ -561,14 +565,6 @@ def check_channel_contents( v.ok() -def pin_channel(v: Verification, channel: TarrableSearchPath) -> None: - fetch(v, channel) - parse_channel(v, channel) - fetch_resources(v, channel) - ensure_git_rev_available(v, channel) - check_channel_contents(v, channel) - - def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: v.status('Getting commit date') process = subprocess.run(['git', -- 2.44.1 From 4aaa88e0a2069e14830f07f1ad3190d6d13b43a0 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 12:36:25 -0700 Subject: [PATCH 078/100] fetch_resource is just for Channels --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 78c6d52..d8bf75c 100644 --- a/pinch.py +++ b/pinch.py @@ -305,7 +305,7 @@ def fetch_with_nix_prefetch_url( return path # type: ignore # (for old mypy) -def fetch_resources(v: Verification, channel: TarrableSearchPath) -> None: +def fetch_resources(v: Verification, channel: ChannelSearchPath) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: fields = channel.table[resource] fields.absolute_url = urllib.parse.urljoin( -- 2.44.1 From 41b87c9c5ed19c095f2d51c9180c826378bb2df6 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 12:52:05 -0700 Subject: [PATCH 079/100] Rename top-level command functions --- pinch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pinch.py b/pinch.py index d8bf75c..6f3f256 100644 --- a/pinch.py +++ b/pinch.py @@ -609,7 +609,7 @@ def read_config_files( return merged_config -def pin(args: argparse.Namespace) -> None: +def pinCommand(args: argparse.Namespace) -> None: v = Verification() config = read_config(args.channels_file) for section in config.sections(): @@ -624,7 +624,7 @@ def pin(args: argparse.Namespace) -> None: config.write(configfile) -def update(args: argparse.Namespace) -> None: +def updateCommand(args: argparse.Namespace) -> None: v = Verification() exprs: Dict[str, str] = {} config = read_config_files(args.channels_file) @@ -666,11 +666,11 @@ def main() -> None: parser_pin = subparsers.add_parser('pin') parser_pin.add_argument('channels_file', type=str) parser_pin.add_argument('channels', type=str, nargs='*') - parser_pin.set_defaults(func=pin) + parser_pin.set_defaults(func=pinCommand) parser_update = subparsers.add_parser('update') parser_update.add_argument('--dry-run', action='store_true') parser_update.add_argument('channels_file', type=str, nargs='+') - parser_update.set_defaults(func=update) + parser_update.set_defaults(func=updateCommand) args = parser.parse_args() args.func(args) -- 2.44.1 From 55ae4ff693dee4323804eae4232141fb2e524c7e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 12:55:30 -0700 Subject: [PATCH 080/100] Fix not-pinned check --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index 6f3f256..4c2341f 100644 --- a/pinch.py +++ b/pinch.py @@ -148,7 +148,7 @@ class GitSearchPath(TarrableSearchPath): def fetch(self, v: Verification, section: str, conf: configparser.SectionProxy) -> str: - if 'git_repo' not in conf or 'release_name' not in conf: + if 'git_revision' not in conf or 'release_name' not in conf: raise Exception( 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % section) -- 2.44.1 From 9343cf4818bc2b3772e46842b6703b2e9cd99e4d Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 12:57:35 -0700 Subject: [PATCH 081/100] Start pulling release_name and git_revision out of SearchPath --- pinch.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/pinch.py b/pinch.py index 4c2341f..5e6aab9 100644 --- a/pinch.py +++ b/pinch.py @@ -152,9 +152,12 @@ class GitSearchPath(TarrableSearchPath): raise Exception( 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % section) + the_pin = GitPin( + release_name=conf['release_name'], + git_revision=conf['git_revision']) - ensure_git_rev_available(v, self) - return git_get_tarball(v, self) + ensure_git_rev_available(v, self, the_pin) + return git_get_tarball(v, self, the_pin) class ChannelSearchPath(TarrableSearchPath): @@ -164,9 +167,9 @@ class ChannelSearchPath(TarrableSearchPath): del self.git_revision fetch(v, self) - parse_channel(v, self) - fetch_resources(v, self) - ensure_git_rev_available(v, self) + new_gitpin = parse_channel(v, self) + fetch_resources(v, self, new_gitpin) + ensure_git_rev_available(v, self, new_gitpin) check_channel_contents(v, self) return ChannelPin( release_name=self.release_name, @@ -225,7 +228,7 @@ def fetch(v: Verification, channel: TarrableSearchPath) -> None: v.check('Got forwarded', channel.channel_url != channel.forwarded_url) -def parse_channel(v: Verification, channel: TarrableSearchPath) -> None: +def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: v.status('Parsing channel description as XML') d = xml.dom.minidom.parseString(channel.channel_html) v.ok() @@ -256,6 +259,7 @@ def parse_channel(v: Verification, channel: TarrableSearchPath) -> None: channel.table[name] = ChannelTableEntry( url=url, digest=digest, size=size) v.ok() + return GitPin(release_name=title_name, git_revision=channel.git_revision) def digest_string(s: bytes) -> Digest16: @@ -305,7 +309,10 @@ def fetch_with_nix_prefetch_url( return path # type: ignore # (for old mypy) -def fetch_resources(v: Verification, channel: ChannelSearchPath) -> None: +def fetch_resources( + v: Verification, + channel: ChannelSearchPath, + pin: GitPin) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: fields = channel.table[resource] fields.absolute_url = urllib.parse.urljoin( @@ -315,7 +322,7 @@ def fetch_resources(v: Verification, channel: ChannelSearchPath) -> None: v.status('Verifying git commit on main page matches git commit in table') v.result( open( - channel.table['git-revision'].file).read(999) == channel.git_revision) + channel.table['git-revision'].file).read(999) == pin.git_revision) def git_cachedir(git_repo: str) -> str: @@ -405,12 +412,13 @@ def git_fetch(v: Verification, channel: TarrableSearchPath) -> None: def ensure_git_rev_available( v: Verification, - channel: TarrableSearchPath) -> None: + channel: TarrableSearchPath, + pin: GitPin) -> None: cachedir = git_cachedir(channel.git_repo) if os.path.exists(cachedir): v.status('Checking if we already have this rev:') process = subprocess.run( - ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) + ['git', '-C', cachedir, 'cat-file', '-e', pin.git_revision]) if process.returncode == 0: v.status('yes') if process.returncode == 1: @@ -487,7 +495,10 @@ def git_checkout( v.result(git.returncode == 0 and tar.returncode == 0) -def git_get_tarball(v: Verification, channel: TarrableSearchPath) -> str: +def git_get_tarball( + v: Verification, + channel: TarrableSearchPath, + pin: GitPin) -> str: cache_file = tarball_cache_file(channel) if os.path.exists(cache_file): cached_tarball = open(cache_file).read(9999) @@ -496,17 +507,17 @@ def git_get_tarball(v: Verification, channel: TarrableSearchPath) -> str: with tempfile.TemporaryDirectory() as output_dir: output_filename = os.path.join( - output_dir, channel.release_name + '.tar.xz') + output_dir, pin.release_name + '.tar.xz') with open(output_filename, 'w') as output_file: v.status( 'Generating tarball for git revision %s' % - channel.git_revision) + pin.git_revision) git = subprocess.Popen(['git', '-C', git_cachedir(channel.git_repo), 'archive', - '--prefix=%s/' % channel.release_name, - channel.git_revision], + '--prefix=%s/' % pin.release_name, + pin.git_revision], stdout=subprocess.PIPE) xz = subprocess.Popen(['xz'], stdin=git.stdout, stdout=output_file) xz.wait() -- 2.44.1 From d7cfdb226f4463b96a3ce50f93142e4b6c0d8420 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 13:29:01 -0700 Subject: [PATCH 082/100] Continue pulling release_name and git_revision out of SearchPath --- pinch.py | 76 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/pinch.py b/pinch.py index 5e6aab9..1b11709 100644 --- a/pinch.py +++ b/pinch.py @@ -25,6 +25,7 @@ from typing import ( Mapping, NamedTuple, NewType, + Optional, Tuple, Type, Union, @@ -132,19 +133,19 @@ class TarrableSearchPath(SearchPath, ABC): # pylint: disable=abstract-method forwarded_url: str git_ref: str git_repo: str - old_git_revision: str table: Dict[str, ChannelTableEntry] class GitSearchPath(TarrableSearchPath): def pin(self, v: Verification) -> GitPin: + old_revision = ( + self.git_revision if hasattr(self, 'git_revision') else None) if hasattr(self, 'git_revision'): - self.old_git_revision = self.git_revision del self.git_revision - git_fetch(v, self) - return GitPin(release_name=git_revision_name(v, self), - git_revision=self.git_revision) + new_revision = git_fetch(v, self, None, old_revision) + return GitPin(release_name=git_revision_name(v, self, new_revision), + git_revision=new_revision) def fetch(self, v: Verification, section: str, conf: configparser.SectionProxy) -> str: @@ -156,20 +157,21 @@ class GitSearchPath(TarrableSearchPath): release_name=conf['release_name'], git_revision=conf['git_revision']) - ensure_git_rev_available(v, self, the_pin) + ensure_git_rev_available(v, self, the_pin, None) return git_get_tarball(v, self, the_pin) class ChannelSearchPath(TarrableSearchPath): def pin(self, v: Verification) -> ChannelPin: + old_revision = ( + self.git_revision if hasattr(self, 'git_revision') else None) if hasattr(self, 'git_revision'): - self.old_git_revision = self.git_revision del self.git_revision fetch(v, self) new_gitpin = parse_channel(v, self) fetch_resources(v, self, new_gitpin) - ensure_git_rev_available(v, self, new_gitpin) + ensure_git_rev_available(v, self, new_gitpin, old_revision) check_channel_contents(v, self) return ChannelPin( release_name=self.release_name, @@ -342,7 +344,11 @@ def tarball_cache_file(channel: TarrableSearchPath) -> str: channel.release_name)) -def verify_git_ancestry(v: Verification, channel: TarrableSearchPath) -> None: +def verify_git_ancestry( + v: Verification, + channel: TarrableSearchPath, + new_revision: str, + old_revision: Optional[str]) -> None: cachedir = git_cachedir(channel.git_repo) v.status('Verifying rev is an ancestor of ref') process = subprocess.run(['git', @@ -350,25 +356,29 @@ def verify_git_ancestry(v: Verification, channel: TarrableSearchPath) -> None: cachedir, 'merge-base', '--is-ancestor', - channel.git_revision, + new_revision, channel.git_ref]) v.result(process.returncode == 0) - if hasattr(channel, 'old_git_revision'): + if old_revision is not None: v.status( 'Verifying rev is an ancestor of previous rev %s' % - channel.old_git_revision) + old_revision) process = subprocess.run(['git', '-C', cachedir, 'merge-base', '--is-ancestor', - channel.old_git_revision, - channel.git_revision]) + old_revision, + new_revision]) v.result(process.returncode == 0) -def git_fetch(v: Verification, channel: TarrableSearchPath) -> None: +def git_fetch( + v: Verification, + channel: TarrableSearchPath, + desired_revision: Optional[str], + old_revision: Optional[str]) -> str: # It would be nice if we could share the nix git cache, but as of the time # of writing it is transitioning from gitv2 (deprecated) to gitv3 (not ready # yet), and trying to straddle them both is too far into nix implementation @@ -394,26 +404,29 @@ def git_fetch(v: Verification, channel: TarrableSearchPath) -> None: channel.git_ref)]) v.result(process.returncode == 0) - if hasattr(channel, 'git_revision'): + if desired_revision is not None: v.status('Verifying that fetch retrieved this rev') process = subprocess.run( - ['git', '-C', cachedir, 'cat-file', '-e', channel.git_revision]) + ['git', '-C', cachedir, 'cat-file', '-e', desired_revision]) v.result(process.returncode == 0) - else: - channel.git_revision = open( - os.path.join( - cachedir, - 'refs', - 'heads', - channel.git_ref)).read(999).strip() - verify_git_ancestry(v, channel) + new_revision = open( + os.path.join( + cachedir, + 'refs', + 'heads', + channel.git_ref)).read(999).strip() + + verify_git_ancestry(v, channel, new_revision, old_revision) + + return new_revision def ensure_git_rev_available( v: Verification, channel: TarrableSearchPath, - pin: GitPin) -> None: + pin: GitPin, + old_revision: Optional[str]) -> None: cachedir = git_cachedir(channel.git_repo) if os.path.exists(cachedir): v.status('Checking if we already have this rev:') @@ -425,9 +438,9 @@ def ensure_git_rev_available( v.status('no') v.result(process.returncode == 0 or process.returncode == 1) if process.returncode == 0: - verify_git_ancestry(v, channel) + verify_git_ancestry(v, channel, pin.git_revision, old_revision) return - git_fetch(v, channel) + git_fetch(v, channel, pin.git_revision, old_revision) def compare_tarball_and_git( @@ -576,7 +589,10 @@ def check_channel_contents( v.ok() -def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: +def git_revision_name( + v: Verification, + channel: TarrableSearchPath, + git_revision: str) -> str: v.status('Getting commit date') process = subprocess.run(['git', '-C', @@ -586,7 +602,7 @@ def git_revision_name(v: Verification, channel: TarrableSearchPath) -> str: '--format=%ct-%h', '--abbrev=11', '--no-show-signature', - channel.git_revision], + git_revision], stdout=subprocess.PIPE) v.result(process.returncode == 0 and process.stdout != b'') return '%s-%s' % (os.path.basename(channel.git_repo), -- 2.44.1 From b17278e340d8524cfe0eb933d21b64013ef9f31a Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 13:35:03 -0700 Subject: [PATCH 083/100] Continue pulling release_name and git_revision out of SearchPath --- pinch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pinch.py b/pinch.py index 1b11709..916739d 100644 --- a/pinch.py +++ b/pinch.py @@ -174,7 +174,7 @@ class ChannelSearchPath(TarrableSearchPath): ensure_git_rev_available(v, self, new_gitpin, old_revision) check_channel_contents(v, self) return ChannelPin( - release_name=self.release_name, + release_name=new_gitpin.release_name, tarball_url=self.table['nixexprs.tar.xz'].absolute_url, tarball_sha256=self.table['nixexprs.tar.xz'].digest, git_revision=self.git_revision) @@ -241,7 +241,6 @@ def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: h1_name = d.getElementsByTagName('h1')[0].firstChild.nodeValue.split()[2] v.status(title_name) v.result(title_name == h1_name) - channel.release_name = title_name v.status('Extracting git commit:') git_commit_node = d.getElementsByTagName('tt')[0] @@ -334,14 +333,14 @@ def git_cachedir(git_repo: str) -> str: digest_string(git_repo.encode())) -def tarball_cache_file(channel: TarrableSearchPath) -> str: +def tarball_cache_file(channel: TarrableSearchPath, pin: GitPin) -> str: return os.path.join( xdg.XDG_CACHE_HOME, 'pinch/git-tarball', '%s-%s-%s' % (digest_string(channel.git_repo.encode()), - channel.git_revision, - channel.release_name)) + pin.git_revision, + pin.release_name)) def verify_git_ancestry( @@ -512,7 +511,7 @@ def git_get_tarball( v: Verification, channel: TarrableSearchPath, pin: GitPin) -> str: - cache_file = tarball_cache_file(channel) + cache_file = tarball_cache_file(channel, pin) if os.path.exists(cache_file): cached_tarball = open(cache_file).read(9999) if os.path.exists(cached_tarball): -- 2.44.1 From a72fdca967a7ef8c996757b0992297d2d5c1004c Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 13:42:44 -0700 Subject: [PATCH 084/100] Continue pulling release_name and git_revision out of SearchPath --- pinch.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pinch.py b/pinch.py index 916739d..d4728fe 100644 --- a/pinch.py +++ b/pinch.py @@ -172,7 +172,7 @@ class ChannelSearchPath(TarrableSearchPath): new_gitpin = parse_channel(v, self) fetch_resources(v, self, new_gitpin) ensure_git_rev_available(v, self, new_gitpin, old_revision) - check_channel_contents(v, self) + check_channel_contents(v, self, new_gitpin) return ChannelPin( release_name=new_gitpin.release_name, tarball_url=self.table['nixexprs.tar.xz'].absolute_url, @@ -444,12 +444,12 @@ def ensure_git_rev_available( def compare_tarball_and_git( v: Verification, - channel: TarrableSearchPath, + pin: GitPin, channel_contents: str, git_contents: str) -> None: v.status('Comparing channel tarball with git checkout') match, mismatch, errors = compare(os.path.join( - channel_contents, channel.release_name), git_contents) + channel_contents, pin.release_name), git_contents) v.ok() v.check('%d files match' % len(match), len(match) > 0) v.check('%d files differ' % len(mismatch), len(mismatch) == 0) @@ -549,40 +549,41 @@ def git_get_tarball( def check_channel_metadata( v: Verification, - channel: TarrableSearchPath, + pin: GitPin, channel_contents: str) -> None: v.status('Verifying git commit in channel tarball') v.result( open( os.path.join( channel_contents, - channel.release_name, - '.git-revision')).read(999) == channel.git_revision) + pin.release_name, + '.git-revision')).read(999) == pin.git_revision) v.status( 'Verifying version-suffix is a suffix of release name %s:' % - channel.release_name) + pin.release_name) version_suffix = open( os.path.join( channel_contents, - channel.release_name, + pin.release_name, '.version-suffix')).read(999) v.status(version_suffix) - v.result(channel.release_name.endswith(version_suffix)) + v.result(pin.release_name.endswith(version_suffix)) def check_channel_contents( v: Verification, - channel: TarrableSearchPath) -> None: + channel: TarrableSearchPath, + pin: GitPin) -> None: with tempfile.TemporaryDirectory() as channel_contents, \ tempfile.TemporaryDirectory() as git_contents: extract_tarball(v, channel, channel_contents) - check_channel_metadata(v, channel, channel_contents) + check_channel_metadata(v, pin, channel_contents) git_checkout(v, channel, git_contents) - compare_tarball_and_git(v, channel, channel_contents, git_contents) + compare_tarball_and_git(v, pin, channel_contents, git_contents) v.status('Removing temporary directories') v.ok() -- 2.44.1 From 3258ff2c4690f84d827b466d03e47042750774df Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Mon, 15 Jun 2020 13:46:34 -0700 Subject: [PATCH 085/100] Continue pulling release_name and git_revision out of SearchPath --- pinch.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pinch.py b/pinch.py index d4728fe..f4fd0b5 100644 --- a/pinch.py +++ b/pinch.py @@ -177,7 +177,7 @@ class ChannelSearchPath(TarrableSearchPath): release_name=new_gitpin.release_name, tarball_url=self.table['nixexprs.tar.xz'].absolute_url, tarball_sha256=self.table['nixexprs.tar.xz'].digest, - git_revision=self.git_revision) + git_revision=new_gitpin.git_revision) # Lint TODO: Put tarball_url and tarball_sha256 in ChannelSearchPath # pylint: disable=no-self-use @@ -244,8 +244,8 @@ def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: v.status('Extracting git commit:') git_commit_node = d.getElementsByTagName('tt')[0] - channel.git_revision = git_commit_node.firstChild.nodeValue - v.status(channel.git_revision) + git_revision = git_commit_node.firstChild.nodeValue + v.status(git_revision) v.ok() v.status('Verifying git commit label') v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') @@ -260,7 +260,7 @@ def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: channel.table[name] = ChannelTableEntry( url=url, digest=digest, size=size) v.ok() - return GitPin(release_name=title_name, git_revision=channel.git_revision) + return GitPin(release_name=title_name, git_revision=git_revision) def digest_string(s: bytes) -> Digest16: @@ -490,13 +490,14 @@ def extract_tarball( def git_checkout( v: Verification, channel: TarrableSearchPath, + pin: GitPin, dest: str) -> None: v.status('Checking out corresponding git revision') git = subprocess.Popen(['git', '-C', git_cachedir(channel.git_repo), 'archive', - channel.git_revision], + pin.git_revision], stdout=subprocess.PIPE) tar = subprocess.Popen( ['tar', 'x', '-C', dest, '-f', '-'], stdin=git.stdout) @@ -581,7 +582,7 @@ def check_channel_contents( extract_tarball(v, channel, channel_contents) check_channel_metadata(v, pin, channel_contents) - git_checkout(v, channel, git_contents) + git_checkout(v, channel, pin, git_contents) compare_tarball_and_git(v, pin, channel_contents, git_contents) -- 2.44.1 From 9f936f16fa5cec8d983cd2ed4006f6806a67e234 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 16 Jun 2020 01:29:38 -0700 Subject: [PATCH 086/100] Prefer more type-safe NamedTuple over SimpleNamespace --- pinch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinch.py b/pinch.py index f4fd0b5..8fe7df1 100644 --- a/pinch.py +++ b/pinch.py @@ -34,7 +34,7 @@ from typing import ( # Use xdg module when it's less painful to have as a dependency -class XDG(types.SimpleNamespace): +class XDG(NamedTuple): XDG_CACHE_HOME: str -- 2.44.1 From 567a67835a75aac5522261e1cdccfed319a8087b Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 16 Jun 2020 04:17:27 -0700 Subject: [PATCH 087/100] Immutable SearchPaths --- pinch.py | 187 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 95 insertions(+), 92 deletions(-) diff --git a/pinch.py b/pinch.py index 8fe7df1..e1d8d3b 100644 --- a/pinch.py +++ b/pinch.py @@ -1,4 +1,3 @@ -from abc import ABC, abstractmethod import argparse import configparser import filecmp @@ -26,8 +25,10 @@ from typing import ( NamedTuple, NewType, Optional, + Set, Tuple, Type, + TypeVar, Union, ) @@ -110,87 +111,64 @@ class ChannelPin(NamedTuple): Pin = Union[AliasPin, GitPin, ChannelPin] -class SearchPath(types.SimpleNamespace, ABC): - - @abstractmethod - def pin(self, v: Verification) -> Pin: - pass - - -class AliasSearchPath(SearchPath): +class AliasSearchPath(NamedTuple): alias_of: str - def pin(self, v: Verification) -> AliasPin: - assert not hasattr(self, 'git_repo') + # pylint: disable=no-self-use + def pin(self, _: Verification, __: Optional[Pin]) -> AliasPin: return AliasPin() -# (This lint-disable is for pylint bug https://github.com/PyCQA/pylint/issues/179 -# which is fixed in pylint 2.5.) -class TarrableSearchPath(SearchPath, ABC): # pylint: disable=abstract-method - channel_html: bytes - channel_url: str - forwarded_url: str +class GitSearchPath(NamedTuple): git_ref: str git_repo: str - table: Dict[str, ChannelTableEntry] - -class GitSearchPath(TarrableSearchPath): - def pin(self, v: Verification) -> GitPin: - old_revision = ( - self.git_revision if hasattr(self, 'git_revision') else None) - if hasattr(self, 'git_revision'): - del self.git_revision + def pin(self, v: Verification, old_pin: Optional[Pin]) -> GitPin: + if old_pin is not None: + assert isinstance(old_pin, GitPin) + old_revision = old_pin.git_revision if old_pin is not None else None new_revision = git_fetch(v, self, None, old_revision) return GitPin(release_name=git_revision_name(v, self, new_revision), git_revision=new_revision) - def fetch(self, v: Verification, section: str, - conf: configparser.SectionProxy) -> str: - if 'git_revision' not in conf or 'release_name' not in conf: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) - the_pin = GitPin( - release_name=conf['release_name'], - git_revision=conf['git_revision']) + def fetch(self, v: Verification, pin: Pin) -> str: + assert isinstance(pin, GitPin) + ensure_git_rev_available(v, self, pin, None) + return git_get_tarball(v, self, pin) - ensure_git_rev_available(v, self, the_pin, None) - return git_get_tarball(v, self, the_pin) +class ChannelSearchPath(NamedTuple): + channel_url: str + git_ref: str + git_repo: str -class ChannelSearchPath(TarrableSearchPath): - def pin(self, v: Verification) -> ChannelPin: - old_revision = ( - self.git_revision if hasattr(self, 'git_revision') else None) - if hasattr(self, 'git_revision'): - del self.git_revision + def pin(self, v: Verification, old_pin: Optional[Pin]) -> ChannelPin: + if old_pin is not None: + assert isinstance(old_pin, ChannelPin) + old_revision = old_pin.git_revision if old_pin is not None else None - fetch(v, self) - new_gitpin = parse_channel(v, self) - fetch_resources(v, self, new_gitpin) + channel_html, forwarded_url = fetch_channel(v, self) + table, new_gitpin = parse_channel(v, channel_html) + fetch_resources(v, new_gitpin, forwarded_url, table) ensure_git_rev_available(v, self, new_gitpin, old_revision) - check_channel_contents(v, self, new_gitpin) + check_channel_contents(v, self, table, new_gitpin) return ChannelPin( release_name=new_gitpin.release_name, - tarball_url=self.table['nixexprs.tar.xz'].absolute_url, - tarball_sha256=self.table['nixexprs.tar.xz'].digest, + tarball_url=table['nixexprs.tar.xz'].absolute_url, + tarball_sha256=table['nixexprs.tar.xz'].digest, git_revision=new_gitpin.git_revision) - # Lint TODO: Put tarball_url and tarball_sha256 in ChannelSearchPath # pylint: disable=no-self-use - def fetch(self, v: Verification, section: str, - conf: configparser.SectionProxy) -> str: - if 'git_repo' not in conf or 'release_name' not in conf: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) + def fetch(self, v: Verification, pin: Pin) -> str: + assert isinstance(pin, ChannelPin) return fetch_with_nix_prefetch_url( - v, conf['tarball_url'], Digest16( - conf['tarball_sha256'])) + v, pin.tarball_url, Digest16(pin.tarball_sha256)) + + +SearchPath = Union[AliasSearchPath, GitSearchPath, ChannelSearchPath] +TarrableSearchPath = Union[GitSearchPath, ChannelSearchPath] def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: @@ -221,18 +199,21 @@ def compare(a: str, b: str) -> Tuple[List[str], List[str], List[str]]: return filecmp.cmpfiles(a, b, files, shallow=False) -def fetch(v: Verification, channel: TarrableSearchPath) -> None: +def fetch_channel( + v: Verification, channel: ChannelSearchPath) -> Tuple[str, str]: v.status('Fetching channel') request = urllib.request.urlopen(channel.channel_url, timeout=10) - channel.channel_html = request.read() - channel.forwarded_url = request.geturl() + channel_html = request.read() + forwarded_url = request.geturl() v.result(request.status == 200) # type: ignore # (for old mypy) - v.check('Got forwarded', channel.channel_url != channel.forwarded_url) + v.check('Got forwarded', channel.channel_url != forwarded_url) + return channel_html, forwarded_url -def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: +def parse_channel(v: Verification, channel_html: str) \ + -> Tuple[Dict[str, ChannelTableEntry], GitPin]: v.status('Parsing channel description as XML') - d = xml.dom.minidom.parseString(channel.channel_html) + d = xml.dom.minidom.parseString(channel_html) v.ok() v.status('Extracting release name:') @@ -251,16 +232,15 @@ def parse_channel(v: Verification, channel: TarrableSearchPath) -> GitPin: v.result(git_commit_node.previousSibling.nodeValue == 'Git commit ') v.status('Parsing table') - channel.table = {} + table: Dict[str, ChannelTableEntry] = {} for row in d.getElementsByTagName('tr')[1:]: name = row.childNodes[0].firstChild.firstChild.nodeValue url = row.childNodes[0].firstChild.getAttribute('href') size = int(row.childNodes[1].firstChild.nodeValue) digest = Digest16(row.childNodes[2].firstChild.firstChild.nodeValue) - channel.table[name] = ChannelTableEntry( - url=url, digest=digest, size=size) + table[name] = ChannelTableEntry(url=url, digest=digest, size=size) v.ok() - return GitPin(release_name=title_name, git_revision=git_revision) + return table, GitPin(release_name=title_name, git_revision=git_revision) def digest_string(s: bytes) -> Digest16: @@ -312,18 +292,16 @@ def fetch_with_nix_prefetch_url( def fetch_resources( v: Verification, - channel: ChannelSearchPath, - pin: GitPin) -> None: + pin: GitPin, + forwarded_url: str, + table: Dict[str, ChannelTableEntry]) -> None: for resource in ['git-revision', 'nixexprs.tar.xz']: - fields = channel.table[resource] - fields.absolute_url = urllib.parse.urljoin( - channel.forwarded_url, fields.url) + fields = table[resource] + fields.absolute_url = urllib.parse.urljoin(forwarded_url, fields.url) fields.file = fetch_with_nix_prefetch_url( v, fields.absolute_url, fields.digest) v.status('Verifying git commit on main page matches git commit in table') - v.result( - open( - channel.table['git-revision'].file).read(999) == pin.git_revision) + v.result(open(table['git-revision'].file).read(999) == pin.git_revision) def git_cachedir(git_repo: str) -> str: @@ -477,13 +455,10 @@ def compare_tarball_and_git( def extract_tarball( v: Verification, - channel: TarrableSearchPath, + table: Dict[str, ChannelTableEntry], dest: str) -> None: - v.status('Extracting tarball %s' % - channel.table['nixexprs.tar.xz'].file) - shutil.unpack_archive( - channel.table['nixexprs.tar.xz'].file, - dest) + v.status('Extracting tarball %s' % table['nixexprs.tar.xz'].file) + shutil.unpack_archive(table['nixexprs.tar.xz'].file, dest) v.ok() @@ -575,11 +550,12 @@ def check_channel_metadata( def check_channel_contents( v: Verification, channel: TarrableSearchPath, + table: Dict[str, ChannelTableEntry], pin: GitPin) -> None: with tempfile.TemporaryDirectory() as channel_contents, \ tempfile.TemporaryDirectory() as git_contents: - extract_tarball(v, channel, channel_contents) + extract_tarball(v, table, channel_contents) check_channel_metadata(v, pin, channel_contents) git_checkout(v, channel, pin, git_contents) @@ -610,13 +586,36 @@ def git_revision_name( process.stdout.decode().strip()) -def read_search_path(conf: configparser.SectionProxy) -> SearchPath: - mapping: Mapping[str, Type[SearchPath]] = { - 'alias': AliasSearchPath, - 'channel': ChannelSearchPath, - 'git': GitSearchPath, +K = TypeVar('K') +V = TypeVar('V') + + +def filter_dict(d: Dict[K, V], fields: Set[K] + ) -> Tuple[Dict[K, V], Dict[K, V]]: + selected: Dict[K, V] = {} + remaining: Dict[K, V] = {} + for k, v in d.items(): + if k in fields: + selected[k] = v + else: + remaining[k] = v + return selected, remaining + + +def read_search_path( + conf: configparser.SectionProxy) -> Tuple[SearchPath, Optional[Pin]]: + mapping: Mapping[str, Tuple[Type[SearchPath], Type[Pin]]] = { + 'alias': (AliasSearchPath, AliasPin), + 'channel': (ChannelSearchPath, ChannelPin), + 'git': (GitSearchPath, GitPin), } - return mapping[conf['type']](**dict(conf.items())) + SP, P = mapping[conf['type']] + _, all_fields = filter_dict(dict(conf.items()), set(['type'])) + pin_fields, remaining_fields = filter_dict(all_fields, set(P._fields)) + # Error suppression works around https://github.com/python/mypy/issues/9007 + pin_present = pin_fields != {} or P._fields == () + pin = P(**pin_fields) if pin_present else None # type:ignore[call-arg] + return SP(**remaining_fields), pin def read_config(filename: str) -> configparser.ConfigParser: @@ -644,9 +643,9 @@ def pinCommand(args: argparse.Namespace) -> None: if args.channels and section not in args.channels: continue - sp = read_search_path(config[section]) + sp, old_pin = read_search_path(config[section]) - config[section].update(sp.pin(v)._asdict()) + config[section].update(sp.pin(v, old_pin)._asdict()) with open(args.channels_file, 'w') as configfile: config.write(configfile) @@ -657,11 +656,15 @@ def updateCommand(args: argparse.Namespace) -> None: exprs: Dict[str, str] = {} config = read_config_files(args.channels_file) for section in config: - sp = read_search_path(config[section]) + sp, pin = read_search_path(config[section]) + if pin is None: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) if isinstance(sp, AliasSearchPath): assert 'git_repo' not in config[section] continue - tarball = sp.fetch(v, section, config[section]) + tarball = sp.fetch(v, pin) exprs[section] = ( 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % (config[section]['release_name'], tarball)) -- 2.44.1 From b3bdc793dc1624c220ddb7be1dc92ad2b1578a8e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 10:39:45 -0700 Subject: [PATCH 088/100] Remove assert that's now handled by types --- pinch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pinch.py b/pinch.py index e1d8d3b..266daff 100644 --- a/pinch.py +++ b/pinch.py @@ -662,7 +662,6 @@ def updateCommand(args: argparse.Namespace) -> None: 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % section) if isinstance(sp, AliasSearchPath): - assert 'git_repo' not in config[section] continue tarball = sp.fetch(v, pin) exprs[section] = ( -- 2.44.1 From d815b199fa1d94ba00cd415a1310e80abc57e3b9 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 10:40:57 -0700 Subject: [PATCH 089/100] Rename read_search_path -> read_config_section --- pinch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pinch.py b/pinch.py index 266daff..125fa63 100644 --- a/pinch.py +++ b/pinch.py @@ -602,7 +602,7 @@ def filter_dict(d: Dict[K, V], fields: Set[K] return selected, remaining -def read_search_path( +def read_config_section( conf: configparser.SectionProxy) -> Tuple[SearchPath, Optional[Pin]]: mapping: Mapping[str, Tuple[Type[SearchPath], Type[Pin]]] = { 'alias': (AliasSearchPath, AliasPin), @@ -643,7 +643,7 @@ def pinCommand(args: argparse.Namespace) -> None: if args.channels and section not in args.channels: continue - sp, old_pin = read_search_path(config[section]) + sp, old_pin = read_config_section(config[section]) config[section].update(sp.pin(v, old_pin)._asdict()) @@ -656,7 +656,7 @@ def updateCommand(args: argparse.Namespace) -> None: exprs: Dict[str, str] = {} config = read_config_files(args.channels_file) for section in config: - sp, pin = read_search_path(config[section]) + sp, pin = read_config_section(config[section]) if pin is None: raise Exception( 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % -- 2.44.1 From e8bd4979a02d7bb9e8fb7d978fd0e986d7ee60d1 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 10:48:37 -0700 Subject: [PATCH 090/100] Factor out pin check --- pinch.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pinch.py b/pinch.py index 125fa63..1959f45 100644 --- a/pinch.py +++ b/pinch.py @@ -618,6 +618,16 @@ def read_config_section( return SP(**remaining_fields), pin +def read_pinned_config_section( + section: str, conf: configparser.SectionProxy) -> Tuple[SearchPath, Pin]: + sp, pin = read_config_section(conf) + if pin is None: + raise Exception( + 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % + section) + return sp, pin + + def read_config(filename: str) -> configparser.ConfigParser: config = configparser.ConfigParser() config.read_file(open(filename), filename) @@ -656,11 +666,7 @@ def updateCommand(args: argparse.Namespace) -> None: exprs: Dict[str, str] = {} config = read_config_files(args.channels_file) for section in config: - sp, pin = read_config_section(config[section]) - if pin is None: - raise Exception( - 'Cannot update unpinned channel "%s" (Run "pin" before "update")' % - section) + sp, pin = read_pinned_config_section(section, config[section]) if isinstance(sp, AliasSearchPath): continue tarball = sp.fetch(v, pin) -- 2.44.1 From 9d2c406b0eb069369289350f419b73a2e610a0a4 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 11:50:13 -0700 Subject: [PATCH 091/100] Process alias and non-alias configs separately --- pinch.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pinch.py b/pinch.py index 1959f45..9b845e3 100644 --- a/pinch.py +++ b/pinch.py @@ -18,6 +18,7 @@ import urllib.request import xml.dom.minidom from typing import ( + Callable, Dict, Iterable, List, @@ -590,18 +591,23 @@ K = TypeVar('K') V = TypeVar('V') -def filter_dict(d: Dict[K, V], fields: Set[K] - ) -> Tuple[Dict[K, V], Dict[K, V]]: +def partition_dict(pred: Callable[[K, V], bool], + d: Dict[K, V]) -> Tuple[Dict[K, V], Dict[K, V]]: selected: Dict[K, V] = {} remaining: Dict[K, V] = {} for k, v in d.items(): - if k in fields: + if pred(k, v): selected[k] = v else: remaining[k] = v return selected, remaining +def filter_dict(d: Dict[K, V], fields: Set[K] + ) -> Tuple[Dict[K, V], Dict[K, V]]: + return partition_dict(lambda k, v: k in fields, d) + + def read_config_section( conf: configparser.SectionProxy) -> Tuple[SearchPath, Optional[Pin]]: mapping: Mapping[str, Tuple[Type[SearchPath], Type[Pin]]] = { @@ -664,19 +670,24 @@ def pinCommand(args: argparse.Namespace) -> None: def updateCommand(args: argparse.Namespace) -> None: v = Verification() exprs: Dict[str, str] = {} - config = read_config_files(args.channels_file) - for section in config: - sp, pin = read_pinned_config_section(section, config[section]) - if isinstance(sp, AliasSearchPath): - continue + config = { + section: read_pinned_config_section(section, conf) for section, + conf in read_config_files( + args.channels_file).items()} + alias, nonalias = partition_dict( + lambda k, v: isinstance(v[0], AliasSearchPath), config) + + for section, (sp, pin) in nonalias.items(): + assert not isinstance(sp, AliasSearchPath) # mypy can't see through + assert not isinstance(pin, AliasPin) # partition_dict() tarball = sp.fetch(v, pin) exprs[section] = ( 'f: f { name = "%s"; channelName = "%%s"; src = builtins.storePath "%s"; }' % - (config[section]['release_name'], tarball)) + (pin.release_name, tarball)) - for section in config: - if 'alias_of' in config[section]: - exprs[section] = exprs[str(config[section]['alias_of'])] + for section, (sp, pin) in alias.items(): + assert isinstance(sp, AliasSearchPath) # For mypy + exprs[section] = exprs[sp.alias_of] command = [ 'nix-env', -- 2.44.1 From 75e95e09d162331bfa20e8eeca5c5695d295d6c2 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 14:03:43 -0700 Subject: [PATCH 092/100] Verify unknown config fields raise errors --- tests/reject-unknown-field.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 tests/reject-unknown-field.sh diff --git a/tests/reject-unknown-field.sh b/tests/reject-unknown-field.sh new file mode 100755 index 0000000..dfe069c --- /dev/null +++ b/tests/reject-unknown-field.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +echo "whatisthis = I don't even" >> "$conf" + +if python3 ./pinch.py pin "$conf";then + echo "FAIL: Config with unknown field should be rejected" + exit 1 +else + echo PASS +fi -- 2.44.1 From 0afcdb2a33c630ba4b2f882c4fad670e06ee3d98 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 16:23:26 -0700 Subject: [PATCH 093/100] Add symlink SearchPath type --- pinch.py | 45 ++++++++++++++++++++++++++++++++++++++------- tests/symlink.sh | 25 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100755 tests/symlink.sh diff --git a/pinch.py b/pinch.py index 9b845e3..37d37a7 100644 --- a/pinch.py +++ b/pinch.py @@ -11,6 +11,7 @@ import shlex import shutil import subprocess import sys +import tarfile import tempfile import types import urllib.parse @@ -97,6 +98,12 @@ class AliasPin(NamedTuple): pass +class SymlinkPin(NamedTuple): + @property + def release_name(self) -> str: + return 'link' + + class GitPin(NamedTuple): git_revision: str release_name: str @@ -109,7 +116,15 @@ class ChannelPin(NamedTuple): tarball_sha256: str -Pin = Union[AliasPin, GitPin, ChannelPin] +Pin = Union[AliasPin, SymlinkPin, GitPin, ChannelPin] + + +def copy_to_nix_store(v: Verification, filename: str) -> str: + v.status('Putting tarball in Nix store') + process = subprocess.run( + ['nix-store', '--add', filename], stdout=subprocess.PIPE) + v.result(process.returncode == 0) + return process.stdout.decode().strip() class AliasSearchPath(NamedTuple): @@ -120,6 +135,22 @@ class AliasSearchPath(NamedTuple): return AliasPin() +class SymlinkSearchPath(NamedTuple): + path: str + + # pylint: disable=no-self-use + def pin(self, _: Verification, __: Optional[Pin]) -> SymlinkPin: + return SymlinkPin() + + def fetch(self, v: Verification, _: Pin) -> str: + with tempfile.TemporaryDirectory() as td: + archive_filename = os.path.join(td, 'link.tar.gz') + os.symlink(self.path, os.path.join(td, 'link')) + with tarfile.open(archive_filename, mode='x:gz') as t: + t.add(os.path.join(td, 'link'), arcname='link') + return copy_to_nix_store(v, archive_filename) + + class GitSearchPath(NamedTuple): git_ref: str git_repo: str @@ -168,7 +199,10 @@ class ChannelSearchPath(NamedTuple): v, pin.tarball_url, Digest16(pin.tarball_sha256)) -SearchPath = Union[AliasSearchPath, GitSearchPath, ChannelSearchPath] +SearchPath = Union[AliasSearchPath, + SymlinkSearchPath, + GitSearchPath, + ChannelSearchPath] TarrableSearchPath = Union[GitSearchPath, ChannelSearchPath] @@ -513,11 +547,7 @@ def git_get_tarball( git.wait() v.result(git.returncode == 0 and xz.returncode == 0) - v.status('Putting tarball in Nix store') - process = subprocess.run( - ['nix-store', '--add', output_filename], stdout=subprocess.PIPE) - v.result(process.returncode == 0) - store_tarball = process.stdout.decode().strip() + store_tarball = copy_to_nix_store(v, output_filename) os.makedirs(os.path.dirname(cache_file), exist_ok=True) open(cache_file, 'w').write(store_tarball) @@ -614,6 +644,7 @@ def read_config_section( 'alias': (AliasSearchPath, AliasPin), 'channel': (ChannelSearchPath, ChannelPin), 'git': (GitSearchPath, GitPin), + 'symlink': (SymlinkSearchPath, SymlinkPin), } SP, P = mapping[conf['type']] _, all_fields = filter_dict(dict(conf.items()), set(['type'])) diff --git a/tests/symlink.sh b/tests/symlink.sh new file mode 100755 index 0000000..7f4c354 --- /dev/null +++ b/tests/symlink.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +cat >> "$conf" <'\'' --install --from-expression '\''f: f \{ name = "link"; channelName = "bar"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-link.tar.gz"; \}'\'' '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi -- 2.44.1 From 530104d72eadd4a2700a5f8ba6d80dd7fed66d4e Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 23:41:31 -0700 Subject: [PATCH 094/100] Support old mypy version 0.701 --- pinch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pinch.py b/pinch.py index 37d37a7..b5e3b81 100644 --- a/pinch.py +++ b/pinch.py @@ -124,7 +124,7 @@ def copy_to_nix_store(v: Verification, filename: str) -> str: process = subprocess.run( ['nix-store', '--add', filename], stdout=subprocess.PIPE) v.result(process.returncode == 0) - return process.stdout.decode().strip() + return process.stdout.decode().strip() # type: ignore # (for old mypy) class AliasSearchPath(NamedTuple): @@ -238,7 +238,7 @@ def fetch_channel( v: Verification, channel: ChannelSearchPath) -> Tuple[str, str]: v.status('Fetching channel') request = urllib.request.urlopen(channel.channel_url, timeout=10) - channel_html = request.read() + channel_html = request.read().decode() forwarded_url = request.geturl() v.result(request.status == 200) # type: ignore # (for old mypy) v.check('Got forwarded', channel.channel_url != forwarded_url) @@ -651,7 +651,7 @@ def read_config_section( pin_fields, remaining_fields = filter_dict(all_fields, set(P._fields)) # Error suppression works around https://github.com/python/mypy/issues/9007 pin_present = pin_fields != {} or P._fields == () - pin = P(**pin_fields) if pin_present else None # type:ignore[call-arg] + pin = P(**pin_fields) if pin_present else None # type: ignore return SP(**remaining_fields), pin -- 2.44.1 From a085d7e37f649b0aae8d22e4dfb448049827b989 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Wed, 17 Jun 2020 23:48:31 -0700 Subject: [PATCH 095/100] Release 2.0.0 --- default.nix | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 3209685..a50b21e 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; - version = "2.0.0-pre"; + version = "2.0.0"; src = lib.cleanSource ./.; checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; diff --git a/setup.py b/setup.py index e5d1ebe..a201941 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pinch", - version="2.0.0-pre", + version="2.0.0", py_modules=['pinch'], entry_points={"console_scripts": ["pinch = pinch:main"]}, ) -- 2.44.1 From 9ee71b8de80b57ed5851203b581ac09b3792d2c8 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 18 Jun 2020 01:14:18 -0700 Subject: [PATCH 096/100] Start on 2.1.0 --- default.nix | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index a50b21e..9e2a5f1 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; - version = "2.0.0"; + version = "2.1.0-pre"; src = lib.cleanSource ./.; checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; diff --git a/setup.py b/setup.py index a201941..342ed97 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pinch", - version="2.0.0", + version="2.1.0-pre", py_modules=['pinch'], entry_points={"console_scripts": ["pinch = pinch:main"]}, ) -- 2.44.1 From 96063a5164fcd9e6ec72c1912d41d77856fa980f Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 18 Jun 2020 01:19:53 -0700 Subject: [PATCH 097/100] Factor out symlink_archive() --- pinch.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pinch.py b/pinch.py index b5e3b81..8c9e976 100644 --- a/pinch.py +++ b/pinch.py @@ -127,6 +127,15 @@ def copy_to_nix_store(v: Verification, filename: str) -> str: return process.stdout.decode().strip() # type: ignore # (for old mypy) +def symlink_archive(v: Verification, path: str) -> str: + with tempfile.TemporaryDirectory() as td: + archive_filename = os.path.join(td, 'link.tar.gz') + os.symlink(path, os.path.join(td, 'link')) + with tarfile.open(archive_filename, mode='x:gz') as t: + t.add(os.path.join(td, 'link'), arcname='link') + return copy_to_nix_store(v, archive_filename) + + class AliasSearchPath(NamedTuple): alias_of: str @@ -143,12 +152,7 @@ class SymlinkSearchPath(NamedTuple): return SymlinkPin() def fetch(self, v: Verification, _: Pin) -> str: - with tempfile.TemporaryDirectory() as td: - archive_filename = os.path.join(td, 'link.tar.gz') - os.symlink(self.path, os.path.join(td, 'link')) - with tarfile.open(archive_filename, mode='x:gz') as t: - t.add(os.path.join(td, 'link'), arcname='link') - return copy_to_nix_store(v, archive_filename) + return symlink_archive(v, self.path) class GitSearchPath(NamedTuple): -- 2.44.1 From 9a21f5897e6be8839d01a54107fdc7224a8a4133 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Thu, 18 Jun 2020 09:41:44 -0700 Subject: [PATCH 098/100] Begin keeping a changelog As recommended by https://keepachangelog.com/ --- Changelog | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Changelog diff --git a/Changelog b/Changelog new file mode 100644 index 0000000..a2ffedf --- /dev/null +++ b/Changelog @@ -0,0 +1,23 @@ +## [Unreleased] + +## [2.0.0] - 2020-06-17 +### Changed +- Config sections must now specify their type explicitly + +### Added +- type=symlink search paths + + +## [1.5.1] - 2020-06-17 +### Fixed +- Support old mypy version 0.701 (by working around its bugs) +- Version numbers are now semver + + +## [1.5] - 2020-06-12 +### Fixed +- Support log.showSignature + + +## [1.4] - 2020-06-11 +First usable version -- 2.44.1 From 9e8ed0ed099aec5085d4eb3dc809f92d797ec932 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 30 Jun 2020 13:30:13 -0700 Subject: [PATCH 099/100] Specify profile path with --profile --- Changelog | 2 ++ pinch.py | 5 +++-- tests/profile.sh | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100755 tests/profile.sh diff --git a/Changelog b/Changelog index a2ffedf..6c0cdf8 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,6 @@ ## [Unreleased] +### Added +- Specify profile path with --profile ## [2.0.0] - 2020-06-17 ### Changed diff --git a/pinch.py b/pinch.py index 8c9e976..a7f3449 100644 --- a/pinch.py +++ b/pinch.py @@ -727,8 +727,7 @@ def updateCommand(args: argparse.Namespace) -> None: command = [ 'nix-env', '--profile', - '/nix/var/nix/profiles/per-user/%s/channels' % - getpass.getuser(), + args.profile, '--show-trace', '--file', '', @@ -751,6 +750,8 @@ def main() -> None: parser_pin.set_defaults(func=pinCommand) parser_update = subparsers.add_parser('update') parser_update.add_argument('--dry-run', action='store_true') + parser_update.add_argument('--profile', default=( + '/nix/var/nix/profiles/per-user/%s/channels' % getpass.getuser())) parser_update.add_argument('channels_file', type=str, nargs='+') parser_update.set_defaults(func=updateCommand) args = parser.parse_args() diff --git a/tests/profile.sh b/tests/profile.sh new file mode 100755 index 0000000..00ceb27 --- /dev/null +++ b/tests/profile.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +. ./tests/lib/test-setup.sh + +foo_setup + +python3 ./pinch.py pin "$conf" + +actual_env_command=`python3 ./pinch.py update --dry-run --profile /path/to/profile "$conf"` + +expected_env_command_RE='^nix-env --profile /path/to/profile --show-trace --file '\'''\'' --install --from-expression '\''f: f \{ name = "(repo-[0-9]{10}-[0-9a-f]{11})"; channelName = "foo"; src = builtins.storePath "'"$NIX_STORE_DIR"'/.{32}-\1.tar.xz"; \}'\''$' + +if echo "$actual_env_command" | egrep "$expected_env_command_RE" > /dev/null;then + echo PASS +else + echo "Output: $actual_env_command" + echo "does not match RE: $expected_env_command_RE" + exit 1 +fi -- 2.44.1 From 8e3f804c6b0dc1bf0e7b1d8634a95ee178b7c309 Mon Sep 17 00:00:00 2001 From: Scott Worley Date: Tue, 30 Jun 2020 13:34:31 -0700 Subject: [PATCH 100/100] Release 2.1.0 --- Changelog | 4 ++++ default.nix | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 6c0cdf8..11afedc 100644 --- a/Changelog +++ b/Changelog @@ -1,7 +1,11 @@ ## [Unreleased] + + +## [2.1.0] - 2020-06-30 ### Added - Specify profile path with --profile + ## [2.0.0] - 2020-06-17 ### Changed - Config sections must now specify their type explicitly diff --git a/default.nix b/default.nix index 9e2a5f1..dcd9c66 100644 --- a/default.nix +++ b/default.nix @@ -3,7 +3,7 @@ pkgs.python3Packages.callPackage ({ lib, buildPythonPackage, nix, git, autopep8, mypy, pylint, }: buildPythonPackage rec { pname = "pinch"; - version = "2.1.0-pre"; + version = "2.1.0"; src = lib.cleanSource ./.; checkInputs = [ nix git mypy ] ++ lib.optionals lint [ autopep8 pylint ]; doCheck = true; diff --git a/setup.py b/setup.py index 342ed97..a460921 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pinch", - version="2.1.0-pre", + version="2.1.0", py_modules=['pinch'], entry_points={"console_scripts": ["pinch = pinch:main"]}, ) -- 2.44.1