I wrote this tool to do automatic updating of layers managed by the
setup-layers script. This
addresses some of the functionality I discussed in my OSSNA 2023 talk
<https://www.youtube.com/watch?v=MiTwL9xZdW4>.

A detailed explanation of what the following script does is found in the
comments, but in
a nutshell:

* If the --sync-upstream argument is provided, any layers that have remotes
called
  "origin" and "upstream" have upstream changes on their configured branch
applied
  to the origin remote (and local clone).
* Updates rev and description for all layers managed by setup-layers after
pulling
  the latest changes on the configured branch.
* Leaves the setup-layers.json file in an updated state so git diff will
show changes.
  Changes can then be reviewed and managed based on local project policy.

I am looking for comments and feedback before I send this as a proper patch.

In particular:

* Are there any problems with this general approach?
* Should this be managed in OE-Core in the same way setup-layers is?
* I made some stylistic changes (e.g. main function, --dry-run, etc) that I
plan to also patch into
  setup-layers if this is accepted and there are no objections.


---

#!/usr/bin/env python3
#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#

# This script is idempotent and assumes that the layers it is updating have
# been managed by setup-layers according to the JSON formatted layers data.
#
# For each layer repository in the JSON formatted layers data file:
#
# * If remotes named upstream and origin exist and the --sync-upstream
argument
# has been provided, merge upstream changes on the configured branch to the
# origin remote.
# * Pull the latest changes on the configured branch from origin.
# * Update the JSON layers data to reflect the latest layer repo HEAD.
# * Leave layer repository in an updated state with the configured branch
# checked out.
#
# After reviewing the changes in the JSON layers data file, they can either
be
# committed or reverted. If they are reverted, it will be necessary to
re-run
# setup-layers to ensure your layer repositories reflect the intent
captured in
# the configuration. For the sake of completeness, you may also wish to
re-run
# setup-layers even if you commit the configuration changes, as this script
# leaves the local layer repo clones checked out to the configured branch
rather
# than the configured rev. The HEAD of the configured branch and the
configured
# rev are identical at the completion of this script, but the latter will be
# more consistent with expectations established by the setup-layers script.
#
# The term "setup-layers" refers to the script found at
# openembedded-core/scripts/oe-setup-layers. Pass the --help argument to
this
# script for the most up to date usage guidance.

import argparse
import json
import os
import subprocess

def _do_git_command(repodir, command):
    print("Running '{}' in {}".format(command, repodir))
    if not args.dry_run:
        subprocess.check_output(command, shell=True, cwd=repodir)

def _do_git_checkout_branch(repodir, remote, branch):
    _do_git_command(repodir, 'git checkout -B {0} --track {1}/{0}
--quiet'.format(branch, remote))

def _do_git_pull(repodir, remote, branch):
    _do_git_fetch(repodir, remote)
    _do_git_merge(repodir, remote, branch)

def _do_git_fetch(repodir, remote):
    _do_git_command(repodir, 'git fetch {} --prune --prune-tags
--quiet'.format(remote))

def _do_git_merge(repodir, remote, branch):
    _do_git_command(repodir, 'git merge {}/{} --ff-only
--quiet'.format(remote, branch))

def _do_git_force_push(repodir, remote, branch):
    _do_git_command(repodir, 'git push {} heads/{} --force
--quiet'.format(remote, branch))

def _get_git_rev(repodir):
    rev = subprocess.check_output("git rev-parse HEAD", shell=True,
cwd=repodir, stderr=subprocess.DEVNULL)
    return rev.strip().decode("utf-8")

def _get_git_desc(repodir):
    desc = subprocess.check_output("git describe HEAD --always",
shell=True, cwd=repodir, stderr=subprocess.DEVNULL)
    return desc.strip().decode("utf-8")

def _do_sync_upstream(repodir, repo_name, repo_data):
    if not args.sync_upstream:
        return

    repo_remote = repo_data['git-remote']
    branch = repo_remote['branch']
    remotes = repo_remote['remotes']

    print("Attempting to sync layer repo {} with
upstream.".format(repo_name))

    if 'origin' not in remotes or 'upstream' not in remotes:
        print("Origin and/or upstream remotes not found on
{}".format(repo_name))
        return

    _do_git_checkout_branch(repodir, 'origin', branch)
    _do_git_pull(repodir, 'origin', branch)
    _do_git_fetch(repodir, 'upstream')
    _do_git_merge(repodir, 'upstream', branch)
    _do_git_force_push(repodir, 'origin', branch)

def _do_update_layer_json(repodir, repo_name, layer_data):
    print("Updating layer JSON for {} repo.".format(repo_name))
    branch = layer_data['sources'][repo_name]['git-remote']['branch']

    _do_git_checkout_branch(repodir, 'origin', branch)
    _do_git_pull(repodir, 'origin', branch)

    layer_data['sources'][repo_name]['git-remote']['rev'] =
_get_git_rev(repodir)
    layer_data['sources'][repo_name]['git-remote']['describe'] =
_get_git_desc(repodir)

def _do_update_layers(layer_data):
    print("Updating layers...")
    repos = layer_data['sources']
    for repo_name in repos:
        repo_data = repos[repo_name]
        repodir = os.path.abspath(os.path.join(args.destdir,
repo_data['path']))

        if 'contains_this_file' in repo_data.keys():
            continue

        _do_sync_upstream(repodir, repo_name, repo_data)
        _do_update_layer_json(repodir, repo_name, layer_data)

def _do_save_layer_config(layer_data):
    print("Saving updated layer config.")
    if args.dry_run:
        return

    with open(args.jsondata, 'w') as f:
        json.dump(layer_data, f, ensure_ascii=False, indent=4)

    print("\nReview {} for layer changes.".format(args.jsondata))

def _parse_args():
    global args

    try:
        defaultdest = os.path.dirname(subprocess.check_output('git
rev-parse --show-toplevel', universal_newlines=True, shell=True,
cwd=os.path.dirname(__file__)))
    except subprocess.CalledProcessError as e:
        defaultdest = os.path.abspath(".")

    parser = argparse.ArgumentParser(description="Updates layer setup JSON
in addition to layer repos with upstream remotes.")

    parser.add_argument('--destdir', default=defaultdest, help='Where to
find the layers (default is
{defaultdest}).'.format(defaultdest=defaultdest))
    parser.add_argument('--jsondata', default=__file__+".json", help='File
containing the layer data in json format (default is
{defaultjson}).'.format(defaultjson=__file__+".json"))
    parser.add_argument('--sync-upstream', action='store_true', help="For
repositories with origin and upstream remotes, merge any upstream changes
to the configured branch.")
    parser.add_argument('--dry-run', action='store_true', help="Performs a
dry run, but does not make any actual changes.")

    args = parser.parse_args()

def _parse_layer_config():
    with open(args.jsondata) as f:
        layer_data = json.load(f)

    supported_versions = ["1.0"]
    if layer_data["version"] not in supported_versions:
        raise Exception("File {} has version {}, which is not in supported
versions: {}".format(args.jsondata, layer_data["version"],
supported_versions))

    return layer_data

def main():
    _parse_args()

    layer_data = _parse_layer_config()
    _do_update_layers(layer_data)
    _do_save_layer_config(layer_data)

if __name__=="__main__":
    main()



--
"Perfection must be reached by degrees; she requires the slow hand of
time." - Voltaire
-=-=-=-=-=-=-=-=-=-=-=-
Links: You receive all messages sent to this group.
View/Reply Online (#184211): 
https://lists.openembedded.org/g/openembedded-core/message/184211
Mute This Topic: https://lists.openembedded.org/mt/100104869/21656
Group Owner: openembedded-core+ow...@lists.openembedded.org
Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub 
[arch...@mail-archive.com]
-=-=-=-=-=-=-=-=-=-=-=-

Reply via email to