|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# mfconfig [manual|init|repath] [source] [dest] [repo-path] |
| 4 | +# |
| 5 | +# Operate on the MarlinFirmware/Configurations repository. |
| 6 | +# |
| 7 | +# The MarlinFirmware/Configurations layout could be broken up into branches, |
| 8 | +# but this makes management more complicated and requires more commits to |
| 9 | +# perform the same operation, so this uses a single branch with subfolders. |
| 10 | +# |
| 11 | +# init - Initialize the repo with a base commit and changes: |
| 12 | +# - Source will be an 'import' branch containing all current configs. |
| 13 | +# - Create an empty 'WORK' branch from 'init-repo'. |
| 14 | +# - Add Marlin config files, but reset all to defaults. |
| 15 | +# - Commit this so changes will be clear in following commits. |
| 16 | +# - Add changed Marlin config files and commit. |
| 17 | +# |
| 18 | +# manual - Import changes from a local Marlin folder, then init. |
| 19 | +# - Replace 'default' configs with your local Marlin configs. |
| 20 | +# - Wait for manual propagation to the rest of the configs. |
| 21 | +# - Run init with the given 'source' and 'dest' |
| 22 | +# |
| 23 | +# repath - Add path labels to all config files, if needed |
| 24 | +# - Add a #define CONFIG_EXAMPLES_DIR to each Configuration*.h file. |
| 25 | +# |
| 26 | +# CI - Run in CI mode, using the current folder as the repo. |
| 27 | +# - For GitHub Actions to update the Configurations repo. |
| 28 | +# |
| 29 | +import os, sys, subprocess, shutil, datetime, tempfile |
| 30 | +from pathlib import Path |
| 31 | + |
| 32 | +# Set to 1 for extra debug commits (no deployment) |
| 33 | +DEBUG = 0 |
| 34 | + |
| 35 | +# Get the shell arguments into ACTION, IMPORT, and EXPORT |
| 36 | +ACTION = sys.argv[1] if len(sys.argv) > 1 else 'manual' |
| 37 | +IMPORT = sys.argv[2] if len(sys.argv) > 2 else 'import-2.1.x' |
| 38 | +EXPORT = sys.argv[3] if len(sys.argv) > 3 else 'bugfix-2.1.x' |
| 39 | + |
| 40 | +# Get repo paths |
| 41 | +CI = False |
| 42 | +if ACTION == 'CI': |
| 43 | + _REPOS = "." |
| 44 | + REPOS = Path(_REPOS) |
| 45 | + CONFIGREPO = REPOS |
| 46 | + ACTION = 'init' |
| 47 | + CI = True |
| 48 | +else: |
| 49 | + _REPOS = sys.argv[4] if len(sys.argv) > 4 else '~/Projects/Maker/Firmware' |
| 50 | + REPOS = Path(_REPOS).expanduser() |
| 51 | + CONFIGREPO = REPOS / "Configurations" |
| 52 | + |
| 53 | +def usage(): |
| 54 | + print(f"Usage: {os.path.basename(sys.argv[0])} [manual|init|repath] [source] [dest] [repo-path]") |
| 55 | + |
| 56 | +if ACTION not in ('manual','init','repath'): |
| 57 | + print(f"Unknown action '{ACTION}'") |
| 58 | + usage() |
| 59 | + sys.exit(1) |
| 60 | + |
| 61 | +CONFIGCON = CONFIGREPO / "config" |
| 62 | +CONFIGDEF = CONFIGCON / "default" |
| 63 | +CONFIGEXA = CONFIGCON / "examples" |
| 64 | + |
| 65 | +# Configurations repo folder must exist |
| 66 | +if not CONFIGREPO.exists(): |
| 67 | + print(f"Can't find Configurations repo at {_REPOS}") |
| 68 | + sys.exit(1) |
| 69 | + |
| 70 | +# Run git within CONFIGREPO |
| 71 | +GITSTDERR = None if DEBUG else subprocess.DEVNULL |
| 72 | +def git(etc): |
| 73 | + if DEBUG: |
| 74 | + print(f"> git {' '.join(etc)}") |
| 75 | + if etc[0] == "push": |
| 76 | + info("*** DRY RUN ***") |
| 77 | + return subprocess.run(["echo"]) |
| 78 | + return subprocess.run(["git"] + etc, cwd=CONFIGREPO, stdout=subprocess.PIPE, stderr=GITSTDERR) |
| 79 | + |
| 80 | +# Get the current branch name |
| 81 | +def branch(): return git(["rev-parse", "--abbrev-ref", "HEAD"]) |
| 82 | + |
| 83 | +# git add . ; git commit -m ... |
| 84 | +def commit(msg, who="."): git(["add", who]) ; return git(["commit", "-m", msg]) |
| 85 | + |
| 86 | +# git checkout ... |
| 87 | +def checkout(etc): return git(["checkout"] + ([etc] if isinstance(etc, str) else etc)) |
| 88 | + |
| 89 | +# git branch -D ... |
| 90 | +def gitbd(name): return git(["branch", "-D", name]).stdout |
| 91 | + |
| 92 | +# git status --porcelain : to check for changes |
| 93 | +def changes(): return git(["status", "--porcelain"]).stdout.decode().strip() |
| 94 | + |
| 95 | +# Configure git user |
| 96 | +git([ "config", "user.email", "[email protected]"]) |
| 97 | +git(["config", "user.name", "Scott Lahteine"]) |
| 98 | + |
| 99 | +# Stash uncommitted changes at the destination? |
| 100 | +if changes(): |
| 101 | + print(f"There are uncommitted Configurations repo changes.") |
| 102 | + STASH_YES = input("Stash changes? [Y/n] ") ; print() |
| 103 | + if STASH_YES not in ('Y','y',''): print("Can't continue") ; sys.exit() |
| 104 | + git(["stash", "-m", f"!!GitHub_Desktop<{branch()}>"]) |
| 105 | + if changes(): print(f"Can't stash changes!") ; sys.exit(1) |
| 106 | + |
| 107 | +def info(msg): |
| 108 | + infotag = "[INFO] " if CI else "" |
| 109 | + print(f"- {infotag}{msg}") |
| 110 | + |
| 111 | +def add_path_labels(): |
| 112 | + info("Adding path labels to all configs...") |
| 113 | + for fn in CONFIGEXA.glob("**/Configuration*.h"): |
| 114 | + fldr = str(fn.parent.relative_to(CONFIGCON)).replace("examples/", "") |
| 115 | + with open(fn, 'r') as f: |
| 116 | + lines = f.readlines() |
| 117 | + emptyline = -1 |
| 118 | + for i, line in enumerate(lines): |
| 119 | + issp = line.isspace() |
| 120 | + if emptyline < 0: |
| 121 | + if issp: emptyline = i |
| 122 | + elif not issp: |
| 123 | + if not "CONFIG_EXAMPLES_DIR" in line: |
| 124 | + lines.insert(emptyline, f"\n#define CONFIG_EXAMPLES_DIR \"{fldr}\"\n") |
| 125 | + with open(fn, 'w') as f: f.writelines(lines) |
| 126 | + break |
| 127 | + |
| 128 | +if ACTION == "repath": |
| 129 | + add_path_labels() |
| 130 | + |
| 131 | +elif ACTION == "manual": |
| 132 | + |
| 133 | + MARLINREPO = Path(REPOS / "MarlinFirmware") |
| 134 | + if not MARLINREPO.exists(): |
| 135 | + print("Can't find MarlinFirmware at {_REPOS}!") |
| 136 | + sys.exit(1) |
| 137 | + |
| 138 | + info(f"Updating '{IMPORT}' from Marlin...") |
| 139 | + |
| 140 | + checkout(IMPORT) |
| 141 | + |
| 142 | + # Replace examples/default with our local copies |
| 143 | + shutil.copy(MARLINREPO / "Marlin" / "Configuration*.h", CONFIGDEF) |
| 144 | + |
| 145 | + #git add . && git commit -m "Changes from Marlin ($(date '+%Y-%m-%d %H:%M'))." |
| 146 | + #commit(f"Changes from Marlin ({datetime.datetime.now()}).") |
| 147 | + |
| 148 | + print(f"Prepare the import branch and continue when ready.") |
| 149 | + INIT_YES = input("Ready to init? [y/N] ") ; print() |
| 150 | + if INIT_YES not in ('Y','y'): print("Done.") ; sys.exit() |
| 151 | + |
| 152 | + ACTION = 'init' |
| 153 | + |
| 154 | +if ACTION == "init": |
| 155 | + print(f"Building branch '{EXPORT}'...") |
| 156 | + info("Init WORK branch...") |
| 157 | + |
| 158 | + info(f"Copy {IMPORT} to temporary location...") |
| 159 | + |
| 160 | + # Use the import branch as the source |
| 161 | + result = checkout(IMPORT) |
| 162 | + if result.returncode != 0: |
| 163 | + print(f"Can't find branch '{IMPORT}'!") ; sys.exit() |
| 164 | + |
| 165 | + # Copy to a temporary location |
| 166 | + TEMP = Path(tempfile.mkdtemp()) |
| 167 | + TEMPCON = TEMP / "config" |
| 168 | + shutil.copytree(CONFIGCON, TEMPCON) |
| 169 | + |
| 170 | + # Strip #error lines from Configuration.h |
| 171 | + for fn in TEMPCON.glob("**/Configuration.h"): |
| 172 | + with open(fn, 'r') as f: |
| 173 | + lines = f.readlines() |
| 174 | + outlines = [] |
| 175 | + for line in lines: |
| 176 | + if not line.startswith("#error"): |
| 177 | + outlines.append(line) |
| 178 | + with open(fn, 'w') as f: |
| 179 | + f.writelines(outlines) |
| 180 | + |
| 181 | + # Create a fresh 'WORK' as a copy of 'init-repo' (README, LICENSE, etc.) |
| 182 | + gitbd("WORK") |
| 183 | + if CI: git(["fetch", "origin", "init-repo"]) |
| 184 | + checkout(["init-repo", "-b", "WORK"]) |
| 185 | + |
| 186 | + # Copy default configurations into the repo |
| 187 | + info("Create configs in default state...") |
| 188 | + for fn in TEMPCON.glob("**/*"): |
| 189 | + if fn.is_dir(): continue |
| 190 | + relpath = fn.relative_to(TEMPCON) |
| 191 | + os.makedirs(CONFIGCON / os.path.dirname(relpath), exist_ok=True) |
| 192 | + if fn.name.startswith("Configuration"): |
| 193 | + shutil.copy(TEMPCON / "default" / fn.name, CONFIGCON / relpath) |
| 194 | + |
| 195 | + # DEBUG: Commit the reset for review |
| 196 | + if DEBUG: commit("[DEBUG] Create defaults") |
| 197 | + |
| 198 | + def replace_in_file(fn, search, replace): |
| 199 | + with open(fn, 'r') as f: lines = f.read() |
| 200 | + with open(fn, 'w') as f: f.write(lines.replace(search, replace)) |
| 201 | + |
| 202 | + # Update the %VERSION% in the README.md file |
| 203 | + replace_in_file(CONFIGREPO / "README.md", "%VERSION%", EXPORT.replace("release-", "")) |
| 204 | + |
| 205 | + # Commit all changes up to now; amend if not debugging |
| 206 | + if DEBUG: |
| 207 | + commit("[DEBUG] Update README.md version", "README.md") |
| 208 | + else: |
| 209 | + git(["add", "."]) |
| 210 | + git(["commit", "--amend", "--no-edit"]) |
| 211 | + |
| 212 | + # Copy configured Configuration*.h to the working copy |
| 213 | + info("Copy examples into place...") |
| 214 | + for fn in TEMPCON.glob("examples/**/Configuration*.h"): |
| 215 | + shutil.copy(fn, CONFIGCON / fn.relative_to(TEMPCON)) |
| 216 | + |
| 217 | + # Put #define CONFIG_EXAMPLES_DIR .. before the first blank line |
| 218 | + add_path_labels() |
| 219 | + |
| 220 | + info("Commit config changes...") |
| 221 | + commit("Examples Customizations") |
| 222 | + |
| 223 | + # Copy over all files not matching Configuration*.h to the working copy |
| 224 | + info("Copy extras into place...") |
| 225 | + for fn in TEMPCON.glob("examples/**/*"): |
| 226 | + if fn.is_dir(): continue |
| 227 | + if fn.name.startswith("Configuration"): continue |
| 228 | + shutil.copy(fn, CONFIGCON / fn.relative_to(TEMPCON)) |
| 229 | + |
| 230 | + info("Commit extras...") |
| 231 | + commit("Examples Extras") |
| 232 | + |
| 233 | + # Delete the temporary folder |
| 234 | + shutil.rmtree(TEMP) |
| 235 | + |
| 236 | + # Push to the remote (if desired) |
| 237 | + if CI: |
| 238 | + PUSH_YES = 'Y' |
| 239 | + else: |
| 240 | + print() |
| 241 | + PUSH_YES = input(f"Push to upstream/{EXPORT}? [y/N] ") |
| 242 | + print() |
| 243 | + |
| 244 | + REMOTE = "origin" if CI else "upstream" |
| 245 | + |
| 246 | + if PUSH_YES in ('Y','y'): |
| 247 | + info("Push to remote...") |
| 248 | + git(["push", "-f", REMOTE, f"WORK:{EXPORT}"]) |
| 249 | + |
| 250 | + info("Done.") |
0 commit comments