Unverified Commit 5051decf authored by LIN, Jian's avatar LIN, Jian Committed by GitHub
Browse files

emacs: improve update-from-overlay.py (#496276)

parents 5c01614f 555ae74a
Loading
Loading
Loading
Loading
+168 −153
Original line number Diff line number Diff line
#!/usr/bin/env nix-shell
#! nix-shell -i python3 -p nix git

# linter: ruff check update-from-overlay.py
# formatter: ruff format update-from-overlay.py
# type-checker: mypy update-from-overlay.py

import argparse
import contextlib
import dataclasses
@@ -10,245 +14,256 @@ import os
import pathlib
import shutil
import subprocess
import sys
import urllib.request


@dataclasses.dataclass
class EmacsOverlay:
    git_url : str = f'https://github.com/nix-community/emacs-overlay'
    raw_url : str = f'https://raw.githubusercontent.com/nix-community/emacs-overlay'
    git_url: str = "https://github.com/nix-community/emacs-overlay"
    raw_url: str = "https://raw.githubusercontent.com/nix-community/emacs-overlay"
    # The field declaration here is a hack around the error:
    # ValueError: mutable default <class 'dict'> for field elisp_packages_set is not allowed: use default_factory
    # See more at https://peps.python.org/pep-0557/#mutable-default-values
    elisp_packages_set : dict[str, dict] = dataclasses.field(default_factory = lambda: {
        'elpa': {
            'location': 'repos/elpa',
            'basename': 'elpa-generated.nix',
            'nix_attrs': ['elpaPackages']
    elisp_packages_set: dict[str, dict] = dataclasses.field(
        default_factory=lambda: {
            "elpa": {
                "location": "repos/elpa",
                "basename": "elpa-generated.nix",
                "nix_attrs": ["elpaPackages"],
            },
        'elpa-devel': {
            'location': 'repos/elpa',
            'basename': 'elpa-devel-generated.nix',
            'nix_attrs': ['elpaDevelPackages']
            "elpa-devel": {
                "location": "repos/elpa",
                "basename": "elpa-devel-generated.nix",
                "nix_attrs": ["elpaDevelPackages"],
            },
        'melpa': {
            'location': 'repos/melpa',
            'basename': 'recipes-archive-melpa.json',
            'nix_attrs': ['melpaPackages',
                          'melpaStablePackages']
            "melpa": {
                "location": "repos/melpa",
                "basename": "recipes-archive-melpa.json",
                "nix_attrs": ["melpaPackages", "melpaStablePackages"],
            },
        'nongnu': {
            'location': 'repos/nongnu',
            'basename': 'nongnu-generated.nix',
            'nix_attrs': ['nongnuPackages']
            "nongnu": {
                "location": "repos/nongnu",
                "basename": "nongnu-generated.nix",
                "nix_attrs": ["nongnuPackages"],
            },
        'nongnu-devel': {
            'location': 'repos/nongnu',
            'basename': 'nongnu-devel-generated.nix',
            'nix_attrs': ['nongnuDevelPackages']
            "nongnu-devel": {
                "location": "repos/nongnu",
                "basename": "nongnu-devel-generated.nix",
                "nix_attrs": ["nongnuDevelPackages"],
            },
    })
        }
    )

    def master_sha(self) -> str:
        '''Return the SHA of current master tip.'''
        cmdline = ['git', 'ls-remote', '--branches', self.git_url, 'refs/heads/master']
        result = subprocess.run(cmdline, capture_output = True, text = True)
        """Return the SHA of current master tip."""
        cmdline = ["git", "ls-remote", "--branches", self.git_url, "refs/heads/master"]
        result = subprocess.run(cmdline, capture_output=True, text=True, check=True)
        return result.stdout.split()[0]


@dataclasses.dataclass
class HereDirectory:
    path : pathlib.Path = pathlib.Path('.').resolve()
    path: pathlib.Path = pathlib.Path(__file__).resolve().parent

    def git_root(self) -> pathlib.Path:
        '''Returns the root directory of Git repository.'''
        cmdline = ['git', 'rev-parse', '--show-toplevel']
        result = subprocess.run(cmdline, capture_output = True, text = True)
        """Returns the root directory of Git repository."""
        cmdline = ["git", "rev-parse", "--show-toplevel"]
        result = subprocess.run(cmdline, capture_output=True, text=True, check=True)

        return pathlib.Path(result.stdout.rstrip()).resolve()

def main (emacs_overlay : EmacsOverlay,
          here_directory : HereDirectory,
          argument_parser : argparse.ArgumentParser) -> None:
    '''The entry point.'''

def main(
    emacs_overlay: EmacsOverlay,
    git_root: pathlib.Path,
    argument_parser: argparse.ArgumentParser,
) -> None:
    """The entry point."""

    args = argument_parser.parse_args()

    if args.commit == None:
    if args.commit is None:
        sha = emacs_overlay.master_sha()
    else:
        sha = args.commit

    match args.loglevel.lower():
        case 'debug':
        case "debug":
            loglevel = logging.DEBUG
        case 'info':
        case "info":
            loglevel = logging.INFO
        case _:
            loglevel = logging.INFO

    logger = get_logger(loglevel = loglevel)
    logging.basicConfig(
        level=loglevel,
        format="%(asctime)s - %(levelname)s - %(funcName)s - %(message)s",
        datefmt="[%Y-%m-%d %H:%M:%S]",
    )

    datestring = datetime.datetime.today().strftime('%Y-%m-%d')
    datestring = datetime.datetime.today().strftime("%Y-%m-%d")

    # The loops are decoupled because each phase interferes with the next ones;
    # e.g. it is pretty possible that an Elisp package updated in fetch_fileset
    # breaks the check because of another Elisp package.
    for name in emacs_overlay.elisp_packages_set:
        fetch_fileset(name, sha,
        fetch_fileset(
            name,
            sha,
            emacs_overlay=emacs_overlay,
                      here_directory = here_directory,
                      logger = logger)
        )

    for name in emacs_overlay.elisp_packages_set:
        check_fileset(name,
        check_fileset(
            name,
            emacs_overlay=emacs_overlay,
                      here_directory = here_directory,
                      logger = logger)
            git_root=git_root,
        )

    for name in emacs_overlay.elisp_packages_set:
        commit_fileset(name, sha, datestring = datestring,
        commit_fileset(
            name,
            sha,
            datestring=datestring,
            emacs_overlay=emacs_overlay,
                       here_directory = here_directory,
                       logger = logger)
        )


def get_argument_parser() -> argparse.ArgumentParser:
    '''Return a getopt-style parser for command-line arguments.'''
    """Return a getopt-style parser for command-line arguments."""
    parser = argparse.ArgumentParser(
        description = 'Fetch and commit Elisp package sets from nix-community/emacs-overlay',
        description="Fetch and commit Elisp package sets from nix-community/emacs-overlay",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        '--commit',
        help = 'Commit to be fetched, in SHA format. If not specified, retrieve the master tip.',
        default = None
        "--commit",
        help="Commit to be fetched, in SHA format. If not specified, retrieve the master tip.",
    )
    parser.add_argument(
        '--loglevel',
        help = 'Level of noisiness of logging messages. Values currently supported: INFO (default), DEBUG.',
        default = 'InFo'
        "--loglevel",
        help="Level of noisiness of logging messages.",
        default="INFO",
        choices=["INFO", "DEBUG"],
    )

    return parser

def get_logger (loglevel: int) -> logging.Logger:
    '''Return a basic logging facility to emit messages over console (stdout).'''
    logger = logging.getLogger('update-from-overlay')
    # Set the lowest level here, so that it does not clobber the one provided by
    # the function argument
    logger.setLevel(logging.DEBUG)

    console_formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s',
                                          datefmt='[%Y-%m-%d %H:%M:%S]')

    console_handler = logging.StreamHandler(stream = sys.stdout)
    console_handler.setLevel(loglevel)
    console_handler.setFormatter(console_formatter)

    logger.addHandler(console_handler)

    return logger

def fetch_fileset (name: str, sha: str,
def fetch_fileset(
    name: str,
    sha: str,
    emacs_overlay: EmacsOverlay,
                   here_directory : HereDirectory,
                   logger : logging.Logger) -> None:
    '''Fetch a fileset from emacs overlay.
) -> None:
    """Fetch a fileset from emacs overlay.

    Arguments:
    name -- The name of fileset.
    sha -- The commit SHA of emacs-overlay from which the files are retrieved.
    '''
    logger.debug('BEGIN')

    location = emacs_overlay.elisp_packages_set[name]['location']
    basename = emacs_overlay.elisp_packages_set[name]['basename']
    url = f'{emacs_overlay.raw_url}/{sha}/{location}/{basename}'
    """
    logging.debug("BEGIN")

    destination = pathlib.Path(here_directory.path, basename).resolve()
    location = emacs_overlay.elisp_packages_set[name]["location"]
    basename = emacs_overlay.elisp_packages_set[name]["basename"]
    url = f"{emacs_overlay.raw_url}/{sha}/{location}/{basename}"

    logger.debug(f'Getting {url}')
    logging.debug(f"Getting {url}")

    with urllib.request.urlopen (url) as input_stream, open(destination, 'wb') as output_file:
        logger.info(f'Installing {destination}')
    with (
        urllib.request.urlopen(url) as input_stream,
        open(basename, "wb") as output_file,
    ):
        logging.info(f"Installing {basename}")
        shutil.copyfileobj(input_stream, output_file)

    logger.debug('END')
    logging.debug("END")

def check_fileset (name: str,

def check_fileset(
    name: str,
    emacs_overlay: EmacsOverlay,
                   here_directory : HereDirectory,
                   logger : logging.Logger) -> None:
    '''Smoke-test the fileset.
    git_root: pathlib.Path,
) -> None:
    """Smoke-test the fileset.

    Arguments:
    name -- The name of fileset.
    '''
    logger.debug('BEGIN')

    for nix_attr in emacs_overlay.elisp_packages_set[name]['nix_attrs']:

        cmdline = ['nix-instantiate', '--show-trace', here_directory.git_root(),
                   '-A', f'emacsPackages.{nix_attr}']
        environment = os.environ
        environment['NIXPKGS_ALLOW_BROKEN'] = '1'

        logger.info(f'Testing {nix_attr}')
        # TODO: capture the output (to put it in the logfile).
        result = subprocess.run(cmdline, capture_output = True, text = True)
        logger.debug(f'''
    """
    logging.debug("BEGIN")

    for nix_attr in emacs_overlay.elisp_packages_set[name]["nix_attrs"]:
        cmdline = [
            "nix-instantiate",
            "--show-trace",
            str(git_root),
            "-A",
            f"emacsPackages.{nix_attr}",
        ]
        env = os.environ.copy()
        env["NIXPKGS_ALLOW_BROKEN"] = "1"

        logging.info(f"Testing {nix_attr}")
        result = subprocess.run(
            cmdline, capture_output=True, text=True, check=True, env=env
        )
        logging.debug(f"""
Shell Command: {result.args}
Output:
{result.stdout}''')
{result.stdout}""")

    logger.debug('END')
    logging.debug("END")

def commit_fileset (name: str, sha: str, datestring : str | None,

def commit_fileset(
    name: str,
    sha: str,
    datestring: str,
    emacs_overlay: EmacsOverlay,
                   here_directory : HereDirectory,
                   logger : logging.Logger) -> None:
    '''Commit the fileset.
) -> None:
    """Commit the fileset.

    Arguments:
    name -- The name of fileset.
    sha -- The commit SHA of emacs-overlay from which the files are retrieved.
    datestring -- The date to be written on commit message. If None, use the current time.
    '''
    logger.debug('BEGIN')
    datestring -- The date to be written on commit message.
    """
    logging.debug("BEGIN")

    nix_attrs = emacs_overlay.elisp_packages_set[name]['nix_attrs']
    basename = emacs_overlay.elisp_packages_set[name]['basename']
    nix_attrs = emacs_overlay.elisp_packages_set[name]["nix_attrs"]
    basename = emacs_overlay.elisp_packages_set[name]["basename"]

    if datestring == None:
        datestring = datetime.datetime.today().strftime('%Y-%m-%d')
        logger.debug(f'Date string not provided, using {datestring}')
    else:
        logger.debug(f'Date string was provided: {datestring}')
    logging.debug(f"Date: {datestring}")

    cmdline_verify = ['git', 'diff', '--exit-code', '--quiet', '--', basename]
    cmdline_verify = ["git", "diff", "--quiet", "--", basename]
    result_verify = subprocess.run(cmdline_verify)

    if result_verify.returncode != 0:
        attrs = ', '.join(nix_attrs)
        commit_message = f'''{attrs}: Updated at {datestring} (from emacs-overlay)
        attrs = ", ".join(nix_attrs)
        commit_message = f"""{attrs}: update on {datestring} (from emacs-overlay)

emacs-overlay commit: {sha}
'''
        cmdline_commit = ['git', 'commit', '--message', commit_message, '--', basename]
        result_commit = subprocess.run(cmdline_commit, capture_output = True, text = True)
        logger.info(f'File {basename} committed')
        logger.debug(f'''
"""
        cmdline_commit = ["git", "commit", "--message", commit_message, "--", basename]
        result_commit = subprocess.run(
            cmdline_commit, capture_output=True, text=True, check=True
        )
        logging.info(f"File {basename} committed")
        logging.debug(f"""
Shell Command: {result_commit.args}
Output:
{result_commit.stdout}''')
{result_commit.stdout}""")
    else:
        logger.info(f'File {basename} not modified, skipping')
        logging.info(f"File {basename} not modified, skipping")

    logger.debug('END')
    logging.debug("END")

if __name__ == '__main__':

if __name__ == "__main__":
    emacs_overlay = EmacsOverlay()
    here_directory = HereDirectory()
    argument_parser = get_argument_parser()
    with contextlib.chdir(here_directory.path):
        main(emacs_overlay = emacs_overlay,
             here_directory = here_directory,
             argument_parser = argument_parser)
        main(
            emacs_overlay=emacs_overlay,
            git_root=here_directory.git_root(),
            argument_parser=argument_parser,
        )