tidy up requirements installation

This commit is contained in:
riscy 2024-09-02 10:38:26 -07:00
parent f90463188a
commit 6ee358727b

View file

@ -110,7 +110,7 @@ def check_containerized_build(recipe: str, elisp_dir: Path) -> None:
files[ii].parent.mkdir(parents=True, exist_ok=True) files[ii].parent.mkdir(parents=True, exist_ok=True)
# shutil.copy/copytree won't work here because file can be a file or a dir: # shutil.copy/copytree won't work here because file can be a file or a dir:
subprocess.run(['cp', '-r', str(elisp_dir / file), files[ii]], check=True) subprocess.run(['cp', '-r', str(elisp_dir / file), files[ii]], check=True)
_write_requirements(files, recipe) _write_requirements(files)
_note(f"<!-- Building container for {package_name(recipe)}... 🐳 -->") _note(f"<!-- Building container for {package_name(recipe)}... 🐳 -->")
run_env = dict(os.environ, DOCKER_OUTPUT='--quiet') # or --progress=plain run_env = dict(os.environ, DOCKER_OUTPUT='--quiet') # or --progress=plain
@ -233,25 +233,23 @@ def _main_file(files: list[Path], recipe: str) -> Path | None:
return None return None
def _write_requirements(files: list[Path], recipe: str) -> None: def _write_requirements(files: list[Path]) -> None:
"""Create a little elisp script that Docker will run as setup.""" """Create a little elisp script that Docker will run as setup."""
with open('_requirements.el', 'w', encoding='utf-8') as requirements_el: with Path('_requirements.el').open('w', encoding='utf-8') as requirements_el:
requirements_el.write( requirements_el.write(
f''' f";; {time.strftime('%Y-%m-%d')} ; helps to invalidate old Docker cache\n\n"
;; {time.strftime('%Y-%m-%d')} ; helps to invalidate old Docker cache + ";; NOTE: emacs --script <file.el> will set `load-file-name' to <file.el>\n"
;; NOTE: emacs --script <file.el> will set `load-file-name' to <file.el> + ";; which can disrupt the compilation of packages that use that variable:\n"
;; which can disrupt the compilation of packages that use that variable: + "(setq load-file-name nil)\n"
(setq load-file-name nil) + ";; (setq network-security-level 'low) ; expired certs last resort\n"
;; (setq network-security-level 'low) ; expired certs last resort + "(require 'package)\n"
(require 'package) + "(package-initialize)\n"
(package-initialize) + """(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))\n"""
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/")) + "(package-refresh-contents)\n"
(package-refresh-contents) + "(package-install 'pkg-info)\n"
(package-install 'pkg-info) + "(package-install 'package-lint)\n"
(package-install 'package-lint)
'''
) )
for req in requirements(files, recipe): for req in requirements(files):
req_, *version_maybe = req.split() req_, *version_maybe = req.split()
version = version_maybe[0].strip('"') if version_maybe else 'N/A' version = version_maybe[0].strip('"') if version_maybe else 'N/A'
if req_ == 'emacs': if req_ == 'emacs':
@ -265,64 +263,64 @@ def _write_requirements(files: list[Path], recipe: str) -> None:
) )
# always install the latest available version of the dependency. # always install the latest available version of the dependency.
requirements_el.write( requirements_el.write(
f''' f'\n(message "Installing {req_} {version} or later")\n'
(message "Installing {req_} {version}") + f"(ignore-errors (package-install (cadr (assq '{req_} package-archive-contents))))\n"
(ignore-errors (package-install (cadr (assq '{req_} package-archive-contents))))
'''
) )
def requirements(files: list[Path], recipe: str | None = None) -> set[str]: def requirements(files: list[Path]) -> set[str]:
"""Return (downcased) requirements given a listing of files. """Return (downcased) requirements given a listing of files.
If a recipe is given, use it to determine which file is the main file; If a recipe is given, use it to determine which file is the main file;
otherwise scan every .el file for requirements. otherwise scan every .el file for requirements.
""" """
reqs = [] reqs: set[str] = set()
if recipe:
main_file = _main_file(files, recipe)
if main_file:
files = [main_file]
for file_ in (f for f in files if f.is_file()): for file_ in (f for f in files if f.is_file()):
with file_.open(encoding='utf-8', errors='replace') as stream:
if file_.name.endswith('-pkg.el'): if file_.name.endswith('-pkg.el'):
with open(file_, encoding='utf-8', errors='replace') as pkg_el: reqs = reqs.union(_reqs_from_pkg_el(stream))
reqs.append(_reqs_from_pkg_el(pkg_el))
elif file_.name.endswith('.el'): elif file_.name.endswith('.el'):
with open(file_, encoding='utf-8', errors='replace') as el_file: reqs = reqs.union(_reqs_from_el_file(stream))
reqs.append(_reqs_from_el_file(el_file) or '') return reqs
reqs = sum((re.split('[()]', req) for req in reqs), [])
return {req.replace(')', '').strip().lower() for req in reqs if req.strip()}
def _reqs_from_pkg_el(pkg_el: TextIO) -> str: def _reqs_from_pkg_el(pkg_el: TextIO) -> set[str]:
"""Pull the requirements out of a -pkg.el file. """Pull the requirements out of a -pkg.el file.
>>> import io >>> import io
>>> _reqs_from_pkg_el(io.StringIO( >>> _reqs_from_pkg_el(io.StringIO(
... '''(define-package "x" "1.2" "A pkg." '(a (b "31.5")))''')) ... '''(define-package "x" "1.2" "A pkg." '(a (b "31.5")))'''))
'( a ( b "31.5" ) )' {'a', 'b "31.5"'}
""" """
# TODO: fails if EXTRA-PROPERTIES args were given to #'define-package # TODO: fails if EXTRA-PROPERTIES args were given to #'define-package
reqs = pkg_el.read() reqs = pkg_el.read()
reqs = ' '.join(_tokenize_expression(reqs)[5:-1]) reqs = ' '.join(_tokenize_expression(reqs)[5:-1])
reqs = reqs[reqs.find("' (") + 2 :] reqs = reqs[reqs.find("' (") + 2 :]
reqs = reqs[: reqs.find(') )') + 3] reqs = reqs[: reqs.find(') )') + 3]
return reqs return {
req.replace(')', '').strip().lower()
for req in re.split('[()]', reqs)
if req.strip()
}
def _reqs_from_el_file(el_file: TextIO) -> str | None: def _reqs_from_el_file(el_file: TextIO) -> set[str]:
"""Hacky function to pull the requirements out of an elisp file. """Hacky function to pull the requirements out of an elisp file.
>>> import io >>> import io
>>> _reqs_from_el_file(io.StringIO(';; package-requires: ((emacs "24.4"))')) >>> _reqs_from_el_file(io.StringIO(';; package-requires: ((emacs "24.4"))'))
'((emacs "24.4"))' {'emacs "24.4"'}
""" """
for line in el_file.readlines(): for line in el_file:
match = re.match(r'[; ]*Package-Requires[ ]*:[ ]*(.*)$', line, re.I) match = re.match(r'[; ]*Package-Requires[ ]*:[ ]*(.*)$', line, re.I)
if match: if match:
try: try:
_tokenize_expression(match.groups()[0]) _tokenize_expression(match.groups()[0])
except ValueError as err: except ValueError as err:
_fail(str(err)) _fail(str(err))
return match.groups()[0].strip() return {
return None req.replace(')', '').strip().lower()
for req in re.split('[()]', match.groups()[0])
if req.strip()
}
return set()
def _check_license_api(clone_address: str) -> bool: def _check_license_api(clone_address: str) -> bool:
@ -590,7 +588,7 @@ def _check_package_requires(recipe: str, elisp_dir: Path) -> None:
if not main_file: if not main_file:
_fail("- Can't check Package-Requires if there is no 'main' file") _fail("- Can't check Package-Requires if there is no 'main' file")
return return
main_file_requirements = requirements(files, recipe) main_file_requirements = requirements([main_file])
for file in files: for file in files:
file_requirements = requirements([file]) file_requirements = requirements([file])
if file_requirements - main_file_requirements > set(): if file_requirements - main_file_requirements > set():
@ -601,7 +599,7 @@ def _check_package_requires(recipe: str, elisp_dir: Path) -> None:
) )
compat = next((r for r in main_file_requirements if r.startswith('compat ')), None) compat = next((r for r in main_file_requirements if r.startswith('compat ')), None)
if compat: if compat:
_note(f"- This package depends on {compat}", CLR_INFO) _note(f"- Reviewer note: this package depends on {compat}", CLR_INFO)
def check_package_name(name: str) -> None: def check_package_name(name: str) -> None: