Make update: Show Git LFS download progress

Solves the issue of the script potentially sitting for a long time
without having any progress reported. Confusingly, such behavior
depends on Git version.

In older versions (< 2.33) there will be progress reported, but it
then got changed by the commit in Git:

  7a132c628e

The delayed checkout is exactly how Git LFS integrates into the Git
process. Another affecting factor for the behavior is that submodule
configured to use "checkout" update policy is forced to have quite
flag passed to the "git checkout":

  https://github.com/git/git/blob/v2.43.2/builtin/submodule--helper.c#L2258

This is done to avoid the long message at the end of checkout about
the detached state of HEAD, with instructions how to resolve that.

There are two possible solutions: either use "rebase" update policy
for submodules, or skip Git LFS download during the submodule update.

Changing the update policy is possible, but it needs to be done with
a bit of care, and possible revised process for updating/merging
tests data.

This change follows the second idea of delaying LFS download for a
later step, so the process is the following:
- Run `git submodule update`, but tell Git LFS to not resolve the links
  by using GIT_LFS_SKIP_SMUDGE=1 environment variable.
- Run `git lfs pull` for the submodule, to resolve the links.

Doing so bypasses hardcoded silencing in the Git. It also potentially
allows to recover from an aborted download process.

The `git lfs pull` seems to be a nominal step to resolve the LFS links
after the smudging has been skipped. It is also how in earlier Git
versions some Windows limitations were bypassed:

  https://www.mankier.com/7/git-lfs-faq

The submodule update now also receives the "--progress" flag, which
logs the initial Git repository checkout process, which further
improves the feedback.

The byproduct of this change is that an error during precompiled
libraries and tests data update is not considered to be fatal.
It seems to be more fitting with other update steps, and allows
more easily reuse some code.

There is also a cosmetic change: the messages at the end of the
update process now have their own header, allowing more easily
see them in the wall-of-text.

Pull Request: https://projects.blender.org/blender/blender/pulls/118673
This commit is contained in:
Sergey Sharybin 2024-02-23 17:40:59 +01:00 committed by Sergey Sharybin
parent c75d2d09e3
commit 9277377f6b
2 changed files with 67 additions and 21 deletions

@ -131,7 +131,7 @@ def ensure_git_lfs(args: argparse.Namespace) -> None:
call((args.git_command, "lfs", "install", "--skip-repo"), exit_on_error=True) call((args.git_command, "lfs", "install", "--skip-repo"), exit_on_error=True)
def update_precompiled_libraries(args: argparse.Namespace) -> None: def update_precompiled_libraries(args: argparse.Namespace) -> str:
""" """
Configure and update submodule for precompiled libraries Configure and update submodule for precompiled libraries
@ -153,21 +153,24 @@ def update_precompiled_libraries(args: argparse.Namespace) -> None:
if sys.platform == "linux" and not args.use_linux_libraries: if sys.platform == "linux" and not args.use_linux_libraries:
print("Skipping Linux libraries configuration") print("Skipping Linux libraries configuration")
return return ""
submodule_dir = f"lib/{platform}_{arch}" submodule_dir = f"lib/{platform}_{arch}"
submodule_directories = get_submodule_directories(args) submodule_directories = get_submodule_directories(args)
if Path(submodule_dir) not in submodule_directories: if Path(submodule_dir) not in submodule_directories:
print("Skipping libraries update: no configured submodule") return "Skipping libraries update: no configured submodule\n"
return
make_utils.git_enable_submodule(args.git_command, submodule_dir) make_utils.git_enable_submodule(args.git_command, submodule_dir)
make_utils.git_update_submodule(args.git_command, submodule_dir)
if not make_utils.git_update_submodule(args.git_command, submodule_dir):
return "Error updating precompiled libraries\n"
return ""
def update_tests_data_files(args: argparse.Namespace) -> None: def update_tests_data_files(args: argparse.Namespace) -> str:
""" """
Configure and update submodule with files used by regression tests Configure and update submodule with files used by regression tests
""" """
@ -177,7 +180,11 @@ def update_tests_data_files(args: argparse.Namespace) -> None:
submodule_dir = "tests/data" submodule_dir = "tests/data"
make_utils.git_enable_submodule(args.git_command, submodule_dir) make_utils.git_enable_submodule(args.git_command, submodule_dir)
make_utils.git_update_submodule(args.git_command, submodule_dir)
if not make_utils.git_update_submodule(args.git_command, submodule_dir):
return "Error updating test data\n"
return ""
def git_update_skip(args: argparse.Namespace, check_remote_exists: bool = True) -> str: def git_update_skip(args: argparse.Namespace, check_remote_exists: bool = True) -> str:
@ -566,9 +573,7 @@ def submodules_update(args: argparse.Namespace, branch: Optional[str]) -> str:
print(f"Skipping tests submodule {submodule_path}") print(f"Skipping tests submodule {submodule_path}")
continue continue
exitcode = call((args.git_command, "submodule", "update", "--init", submodule_path), if not make_utils.git_update_submodule(args.git_command, submodule_path):
exit_on_error=False)
if exitcode != 0:
msg += f"Error updating Git submodule {submodule_path}\n" msg += f"Error updating Git submodule {submodule_path}\n"
add_submodule_push_url(args) add_submodule_push_url(args)
@ -579,6 +584,7 @@ def submodules_update(args: argparse.Namespace, branch: Optional[str]) -> str:
if __name__ == "__main__": if __name__ == "__main__":
args = parse_arguments() args = parse_arguments()
blender_skip_msg = "" blender_skip_msg = ""
libraries_skip_msg = ""
submodules_skip_msg = "" submodules_skip_msg = ""
blender_version = make_utils. parse_blender_version() blender_version = make_utils. parse_blender_version()
@ -600,19 +606,18 @@ if __name__ == "__main__":
blender_skip_msg = "Blender repository skipped: " + blender_skip_msg + "\n" blender_skip_msg = "Blender repository skipped: " + blender_skip_msg + "\n"
if not args.no_libraries: if not args.no_libraries:
update_precompiled_libraries(args) libraries_skip_msg += update_precompiled_libraries(args)
if args.use_tests: if args.use_tests:
update_tests_data_files(args) libraries_skip_msg += update_tests_data_files(args)
if not args.no_submodules: if not args.no_submodules:
submodules_skip_msg = submodules_update(args, branch) submodules_skip_msg = submodules_update(args, branch)
# Report any skipped repositories at the end, so it's not as easy to miss. # Report any skipped repositories at the end, so it's not as easy to miss.
skip_msg = blender_skip_msg + submodules_skip_msg skip_msg = blender_skip_msg + libraries_skip_msg + submodules_skip_msg
if skip_msg: if skip_msg:
print() print_stage("Update finished with the following messages")
print(skip_msg.strip()) print(skip_msg.strip())
print()
# For failed submodule update we throw an error, since not having correct # For failed submodule update we throw an error, since not having correct
# submodules can make Blender throw errors. # submodules can make Blender throw errors.

@ -8,6 +8,7 @@ Utility functions for make update and make tests.
""" """
import re import re
import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
@ -19,18 +20,30 @@ from typing import (
) )
def call(cmd: Sequence[str], exit_on_error: bool = True, silent: bool = False) -> int: def call(cmd: Sequence[str], exit_on_error: bool = True, silent: bool = False, env=None) -> int:
if not silent: if not silent:
print(" ".join([str(x) for x in cmd])) cmd_str = ""
if env:
cmd_str += " ".join([f"{item[0]}={item[1]}" for item in env.items()])
cmd_str += " "
cmd_str += " ".join([str(x) for x in cmd])
print(cmd_str)
env_full = None
if env:
env_full = os.environ.copy()
for key, value in env.items():
env_full[key] = value
# Flush to ensure correct order output on Windows. # Flush to ensure correct order output on Windows.
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
if silent: if silent:
retcode = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) retcode = subprocess.call(
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env_full)
else: else:
retcode = subprocess.call(cmd) retcode = subprocess.call(cmd, env=env_full)
if exit_on_error and retcode != 0: if exit_on_error and retcode != 0:
sys.exit(retcode) sys.exit(retcode)
@ -127,15 +140,43 @@ def git_enable_submodule(git_command: str, submodule_dir: str):
call(command, exit_on_error=True, silent=False) call(command, exit_on_error=True, silent=False)
def git_update_submodule(git_command: str, submodule_dir: str): def git_update_submodule(git_command: str, submodule_dir: str) -> bool:
""" """
Update the given submodule. Update the given submodule.
The submodule is denoted by its path within the repository. The submodule is denoted by its path within the repository.
This function will initialize the submodule if it has not been initialized. This function will initialize the submodule if it has not been initialized.
Returns true if the update succeeded
""" """
call((git_command, "submodule", "update", "--init", submodule_dir)) # Use the two stage update process:
# - Step 1: checkout the submodule to the desired (by the parent repository) hash, but
# skip the LFS smudging.
# - Step 2: Fetch LFS files, if needed.
#
# This allows to show download progress, potentially allowing resuming the download
# progress, and even recovering from partial/corrupted checkout of submodules.
#
# This bypasses the limitation of submodules which are configured as "update=checkout"
# with regular `git submodule update` which, depending on the Git version will not report
# any progress. This is because submodule--helper.c configures Git checkout process with
# the "quiet" flag, so that there is no detached head information printed after submodule
# update, and since Git 2.33 the LFS messages "Filtering contents..." is suppressed by
#
# https://github.com/git/git/commit/7a132c628e57b9bceeb88832ea051395c0637b16
#
# Doing "git lfs pull" after checkout with GIT_LFS_SKIP_SMUDGE=true seems to be the
# valid process. For example, https://www.mankier.com/7/git-lfs-faq
env = {"GIT_LFS_SKIP_SMUDGE": "1"}
if call((git_command, "submodule", "update", "--init", "--progress", submodule_dir),
exit_on_error=False, env=env) != 0:
return False
return call((git_command, "-C", submodule_dir, "lfs", "pull"),
exit_on_error=False) == 0
def command_missing(command: str) -> bool: def command_missing(command: str) -> bool: