From c2b2abd869fce1063a4f71468e968fa581bf0eda Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Tue, 10 Feb 2026 16:39:22 +0200 Subject: [PATCH 01/20] feat: Allow pinning dependency to a specific commit --- .gitignore | 1 + README.md | 25 ++++++- mama/build_target.py | 71 ++++++++++--------- mama/types/git.py | 55 +++++++++----- pyproject.toml | 3 + tests/test_git_pin_change/mamafile.py | 24 +++++++ .../test_git_pin_change.py | 35 +++++++++ tests/test_git_pinning/mamafile.py | 13 ++++ tests/test_git_pinning/test_git_pinning.py | 27 +++++++ 9 files changed, 199 insertions(+), 55 deletions(-) create mode 100644 tests/test_git_pin_change/mamafile.py create mode 100644 tests/test_git_pin_change/test_git_pin_change.py create mode 100644 tests/test_git_pinning/mamafile.py create mode 100644 tests/test_git_pinning/test_git_pinning.py diff --git a/.gitignore b/.gitignore index fb3127d..80e0c85 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ dist/ mama/mama.spec mama.cmake +packages/ diff --git a/README.md b/README.md index 3a62f0b..3eff85c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Each mama build target exports CMake `${ProjectName}_INCLUDES` and `${ProjectNam are gathered in correct linker order inside `MAMA_INCLUDES` and `MAMA_LIBS`. This ensures the least amount of friction for developers - everything just works. -There is no central package repository, all packages are pulled and updated from public or +There is no central package repository, all packages are pulled and updated from public or private git repositories. Package versioning is done through git tags or branches. Custom build systems are also supported. For additional documentation explore: [build_target.py](mama/build_target.py) @@ -219,7 +219,7 @@ And then rebuilding with an artifactory package available $ mama rebuild googletest ========= Mama Build Tool ========== - Target googletest CLEAN linux - - Target googletest BUILD [cleaned target] + - Target googletest BUILD [cleaned target] Artifactory fetch ftp.myartifactory.com/googletest-linux-x64-release-ebb36f3 770.6KB |<==================================================| 100 % Artifactory unzip googletest-linux-x64-release-ebb36f3 @@ -233,11 +233,30 @@ $ mama rebuild googletest ## For Mama Contributors We are open for any improvements and feedback via pull requests. +### Development Setup The package `setuptools>=65.0` is required, ensure the version is correct with `pip3 show setuptools`. You can set up local development with `$ pip3 install -e . --no-cache-dir` but make sure you have latest setuptools (>65.0) and latest pip3 (>22.3). This command will fail with older toolkits. -Uploading a source distributionP: +### Running Tests + +Install pytest and run all tests from the project root: + +```bash +uv venv +.\.venv\Scripts\activate +pip install pytest +pytest +``` + +Or to run a specific test: + +```bash +pytest tests/test_git_pinning/ +``` + +### Publishing +Uploading a source distribution: 1. Get dependencies: `pip3 install build twine` 2. Build sdist: `python -m build` 3. Upload with twine: `twine upload --skip-existing dist/*` diff --git a/mama/build_target.py b/mama/build_target.py index 18b3c4c..b1a7972 100644 --- a/mama/build_target.py +++ b/mama/build_target.py @@ -37,11 +37,11 @@ class BuildTarget: Customization points: ``` class MyProject(mama.BuildTarget): - + workspace = 'build' def configure(self): - self.add_git('ReCpp', + self.add_git('ReCpp', 'http://github.com/RedFox20/ReCpp.git') def configure(self): @@ -128,9 +128,9 @@ def source_dir(self, subpath=''): """ Returns the current source directory. ``` - self.source_dir() + self.source_dir() # --> C:/Projects/ReCpp - self.source_dir('lib/ReCpp.lib') + self.source_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/lib/ReCpp.lib ``` """ @@ -142,7 +142,7 @@ def build_dir(self, subpath=''): """ Returns the current build directory. ``` - self.build_dir() + self.build_dir() # --> C:/Projects/ReCpp/build/windows self.build_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/build/windows/lib/ReCpp.lib @@ -200,12 +200,12 @@ def add_local(self, name, source_dir, mamafile=None, always_build=False, args=[] return self.dep.add_child(LocalSource(name, source_dir, mamafile, always_build, args)) - def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shallow=True, args=[]) -> BuildDependency: + def add_git(self, name, git_url, git_branch='', git_tag='', git_commit='', mamafile=None, shallow=True, args=[]) -> BuildDependency: """ Add a remote GIT dependency. The dependency will be cloned and updated according to mamabuild. Use `mama update` to force update the git repositories. - + If the remote GIT repository does not contain a `mamafile.py`, you will have to provide your own relative or absolute mamafile path. @@ -215,13 +215,15 @@ def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shall ``` self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git') self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git', git_branch='master') - self.add_git('opencv', 'https://github.com/opencv/opencv.git', + self.add_git('opencv', 'https://github.com/opencv/opencv.git', git_branch='3.4', mamafile='mama/opencv_cfg.py') + self.add_git('mylib', 'https://github.com/user/mylib.git', + git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') ``` """ if self.dep.from_artifactory: # already loaded from artifactory? return self.get_dependency(name) - return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, args)) + return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, git_commit, args)) def add_artifactory_pkg(self, name, version='latest', fullname=None) -> BuildDependency: @@ -300,7 +302,7 @@ def inject_products(self, dst_dep, src_dep, include_path, libs, libfilters=None) Name of defines is given via `include_path` and `libs` params. `libfilters` does simple string matching; if nothing matches, the first export lib is chosen. ``` - self.inject_products('libpng', 'zlib', + self.inject_products('libpng', 'zlib', 'ZLIB_INCLUDE_DIR', 'ZLIB_LIBRARY', 'zlibstatic') ``` @@ -325,7 +327,7 @@ def get_product_defines(self): Returns a list of injected defines: ``` defines = self.get_product_defines() - # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', + # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', # 'ZLIB_LIBRARY=path/to/lib/zlib.a', ... ] ``` """ @@ -377,9 +379,9 @@ def add_build_dependency(self, all=None, windows=None, linux=None, macos=None, i Manually add a build dependency to prevent unnecessary rebuilds. @note Normally the build dependency is detected from the packaged libraries. - + if the dependency file does not exist, then the project will be rebuilt - + if your project has no build dependencies, it will always be rebuilt, so make sure to add_build_dependency or export_lib ``` @@ -424,7 +426,7 @@ def package(self): def export_include(self, include_path, build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient). - + Export include path relative to source directory OR if build_dir=True, then relative to build directory. ``` self.export_include('include') # MyRepo/include @@ -439,7 +441,7 @@ def export_include(self, include_path, build_dir=False): def export_includes(self, include_paths=[''], build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient) - + Export include paths relative to source directory OR if build_dir=True, then relative to build directory Example: @@ -454,7 +456,7 @@ def export_includes(self, include_paths=[''], build_dir=False): def export_lib(self, relative_path, src_dir=False, build_dir=True): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export a specific lib relative to build directory OR if src_dir=True, then relative to source directory Example: @@ -471,17 +473,17 @@ def export_lib(self, relative_path, src_dir=False, build_dir=True): def export_libs(self, path = '.', pattern_substrings = ['.lib', '.a'], src_dir=False, build_dir=True, order=None): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export several libs relative to build directory using EXTENSION MATCHING OR if src_dir=True, then relative to source directory - + Example: ``` self.export_libs() # gather any .lib or .a from build dir self.export_libs('.', ['.dll', '.so']) # gather any .dll or .so from build dir self.export_libs('lib', src_dir=True) # export everything from project/lib directory self.export_libs('external/lib') # gather specific static libs from build dir - + # export the libs in a particular order for Linux linker self.export_libs('lib', order=[ 'xphoto', 'calib3d', 'flann', 'core' @@ -500,7 +502,7 @@ def export_asset(self, asset, category=None, src_dir=True, build_dir=False): This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_asset('extras/csharp/NanoMesh.cs') @@ -521,12 +523,12 @@ def export_assets(self, assets_path: str, pattern_substrings = [], category=None This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_assets('extras/csharp', ['.cs']) --> {deploy}/extras/csharp/NanoMesh.cs - + self.export_assets('extras/csharp', ['.cs'], category='dotnet') --> {deploy}/dotnet/NanoMesh.cs ``` @@ -562,7 +564,7 @@ def inject_env(self): ``` def build(self): self.inject_env() # prepare platform - self.my_custom_build() # + self.my_custom_build() # ``` """ cmake.inject_env(self) @@ -613,7 +615,7 @@ def add_c_flags(self, *flags): for flag in flags: if isinstance(flag, list): self.add_c_flags(*flag) else: self._add_dict_flag(self.cmake_cflags, flag) - + def add_cl_flags(self, *flags): """ @@ -669,7 +671,7 @@ def add_platform_ld_flags(self, windows=None, linux=None, macos=None, ios=None, Adds linker flags depending on configuration platform. Supports many different usages: strings, list of strings, or space separate string. ``` - self.add_platform_ld_flags(windows='/LTCG', + self.add_platform_ld_flags(windows='/LTCG', ios=['-lobjc', '-rdynamic'], linux='-rdynamic -s') ``` @@ -802,7 +804,7 @@ def copy(self, src: str, dst: str, filter: list = None): Utility for copying files and folders ``` # copies built .so into an android archive - self.copy(self.build_dir('libAwesome.so'), + self.copy(self.build_dir('libAwesome.so'), self.source_dir('deploy/Awesome.aar/jni/armeabi-v7a')) ``` - filter: can be a string or list of strings to filter files by suffix @@ -858,7 +860,7 @@ def download_and_unzip(self, remote_zip: str, extract_dir: str, unless_file_exis unless_file_exists -- If the specified file exists, then download and unzip steps are skipped. ``` - self.download_and_unzip('http://example.com/archive.zip', + self.download_and_unzip('http://example.com/archive.zip', 'bin', 'bin/unzipped_file.txt') # --> 'bin/' on success # --> None on failure @@ -981,7 +983,7 @@ def run_program(self, working_dir: str, command: str, exit_on_fail=True, env=Non """ Run any program in any directory. Can be used for custom tools. ``` - self.run_program(self.source_dir('bin'), + self.run_program(self.source_dir('bin'), self.source_dir('bin/DbTool')) ``` """ @@ -1018,7 +1020,7 @@ def gtest(self, executable: str, args: str, src_dir=True, gdb=False): The gtest report is written to $src_dir/test/report.xml. Arguments - executable -- which executable to run - - args -- a string of options separated by spaces, + - args -- a string of options separated by spaces, 'gdb', 'nogdb' or gtest fixture/test partial name - src_dir -- [True] If true, then executable is relative to source directory. - gdb -- [False] If true, then run with gdb. @@ -1132,8 +1134,8 @@ def package(self): # custom export AGL as include from source folder self.export_includes(['AGL']) # custom export any .lib or .a from build folder - self.export_libs('.', ['.lib', '.a']) - + self.export_libs('.', ['.lib', '.a']) + if self.windows: self.export_syslib('opengl32.lib') @@ -1142,7 +1144,7 @@ def package(self): ``` """ pass - + def default_package(self): """ @@ -1234,7 +1236,7 @@ def papa_deploy(self, package_path, src_dir=False, MyPackageName/libawesome.so MyPackageName/include/... MyPackageName/someassets/extra.txt - + PAPA descriptor `papa.txt` format: P MyPackageName I include @@ -1280,7 +1282,7 @@ def start(self, args): """ pass - + ############################################ @@ -1501,4 +1503,3 @@ def build(self): ###################################################################################### - \ No newline at end of file diff --git a/mama/types/git.py b/mama/types/git.py index 5a6ca43..5cdf443 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -17,13 +17,14 @@ class Git(DepSource): """ For BuildDependency whose source is from a Git repository """ - def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow:bool, args:list): + def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow:bool, commit:str, args:list): super(Git, self).__init__(name) if not url: raise RuntimeError("Git url must not be empty!") self.is_git = True self.url = url self.branch = branch self.tag = tag + self.commit = commit self.mamafile = mamafile self.shallow = shallow self.args = args @@ -36,6 +37,7 @@ def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow self.tag_changed = False self.branch_changed = False self.commit_changed = False + self.commit_pin_changed = False def __repr__(self): return self.__str__() @@ -43,21 +45,22 @@ def __str__(self): s = f'DepSource Git {self.name} {self.url}' tag = self.branch_or_tag() if tag: s += ' ' + tag + if self.commit: s += ' commit=' + self.commit if self.mamafile: s += ' ' + self.mamafile return s @staticmethod def from_papa_string(s: str) -> "Git": p = s.split(',') - name, url, branch, tag, mamafile = p[0:5] - args = p[5:] + name, url, branch, tag, mamafile, commit = p[0:6] + args = p[6:] shallow = True # shallow is the default - return Git(name, url, branch, tag, mamafile, shallow, args) + return Git(name, url, branch, tag, mamafile, shallow, commit, args) def get_papa_string(self): fields = DepSource.papa_join( - self.name, self.url, self.branch, self.tag, self.mamafile, self.args) + self.name, self.url, self.branch, self.tag, self.mamafile, self.commit, self.args) return 'git ' + fields @@ -97,6 +100,12 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: console(f' {self.name} using stored commit hash: {result}') return result + # explicit commit pin? + if self.commit: + if dep.config.verbose: + console(f' {self.name} using pinned commit hash: {self.commit}') + return self.commit + # is the tag actually a commit hash? if self.tag and all(c in string.hexdigits for c in self.tag): if dep.config.verbose: @@ -137,7 +146,7 @@ def git_status_file(self, dep: BuildDependency): def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n" + status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n{self.commit}\n" if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') @@ -150,7 +159,8 @@ def read_stored_status(self, dep: BuildDependency): tag = lines[1].rstrip() branch = lines[2].rstrip() commit = lines[3].rstrip() - return (url, tag, branch, commit) + commit_pin = lines[4].rstrip() + return (url, tag, branch, commit, commit_pin) def reset_status(self, dep: BuildDependency): @@ -171,14 +181,17 @@ def check_status(self, dep: BuildDependency): self.tag_changed = True self.branch_changed = True self.commit_changed = True + self.commit_pin_changed = True return True - self.fetch_origin(dep) + if not self.commit: + self.fetch_origin(dep) self.url_changed = self.url != status[0] self.tag_changed = self.tag != status[1] self.branch_changed = self.branch != status[2] + self.commit_pin_changed = self.commit != status[4] self.commit_changed = self.get_commit_hash(dep, use_cache=False) != status[3] #console(f'check_status {self.url} {self.branch_or_tag()}: urlc={self.url_changed} tagc={self.tag_changed} brnc={self.branch_changed} cmtc={self.commit_changed}') - return self.url_changed or self.tag_changed or self.branch_changed or self.commit_changed + return self.url_changed or self.tag_changed or self.branch_changed or self.commit_changed or self.commit_pin_changed def branch_or_tag(self): @@ -188,6 +201,9 @@ def branch_or_tag(self): def checkout_current_branch(self, dep: BuildDependency): + if self.commit: + self.run_git(dep, f"checkout {self.commit}") + return branch = self.branch_or_tag() if branch: if self.tag and self.tag_changed: @@ -270,12 +286,17 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): if is_dir_empty(dep.src_dir): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) - branch = self.branch_or_tag() - if branch: branch = f" --branch {self.branch_or_tag()}" - depth = '' if unshallow else '--depth 1' - clone_args = f"--recurse-submodules {depth} {branch} {self.url}" - self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch(dep) + if self.commit: + clone_args = f"--recurse-submodules {self.url}" + self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) + self.checkout_current_branch(dep) + else: + branch = self.branch_or_tag() + if branch: branch = f" --branch {self.branch_or_tag()}" + depth = '' if unshallow else '--depth 1' + clone_args = f"--recurse-submodules {depth} {branch} {self.url}" + self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) + self.checkout_current_branch(dep) else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) @@ -283,7 +304,7 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): self.unshallow(dep) self.checkout_current_branch(dep) self.run_git(dep, 'submodule update --init --recursive') - if not self.tag: # pull if not a tag + if not self.tag and not self.commit: # pull if not a tag or commit pin self.run_git(dep, "reset --hard -q") self.run_git(dep, "pull") @@ -305,7 +326,7 @@ def unshallow(self, dep: BuildDependency): console(f' - Unshallowing {dep.name}', color=Color.YELLOW) self.run_git(dep, 'config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"') self.run_git(dep, 'remote update') - # this last step is allowed to fail, just in case it was + # this last step is allowed to fail, just in case it was # semi-shallow (history was complete, but remote refs were shallow) self.run_git(dep, 'fetch --unshallow', throw=False) diff --git a/pyproject.toml b/pyproject.toml index 594a21d..8f03564 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ classifiers = [ [project.scripts] mama = "mama.main:main" +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.setuptools.dynamic] version = {attr = "mama._version.__version__"} diff --git a/tests/test_git_pin_change/mamafile.py b/tests/test_git_pin_change/mamafile.py new file mode 100644 index 0000000..2705810 --- /dev/null +++ b/tests/test_git_pin_change/mamafile.py @@ -0,0 +1,24 @@ +import mama +import os + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + stage = os.environ.get('GIT_PIN_CHANGE_TEST') # Uses env variable to dynamically change the pinned commit + + remote_name = 'ExampleRemote' + remote_url = 'https://github.com/BatteredBunny/MamaExampleRemote.git' + + # Switches between having REMOTE_VERSION and not to demonstrate that the contents actually change + if stage == '0': + self.add_git(remote_name, remote_url, git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION + if stage == '1': + self.add_git(remote_name, remote_url, git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 + elif stage == '2': + self.add_git(remote_name, remote_url, git_tag='v1.0.0') # has no REMOTE_VERSION + elif stage == '3': + self.add_git(remote_name, remote_url, git_tag='v2.0.0') # has REMOTE_VERSION 2 \ No newline at end of file diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py new file mode 100644 index 0000000..2f1eda1 --- /dev/null +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -0,0 +1,35 @@ +import os +import sys + +def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: + if echo: print(f'exec: {cmd}') + result = os.system(cmd) + if result != 0 and exit_on_fail: + print(f'exec failed: code: {result} {cmd}') + if result >= 255: + result = 1 + sys.exit(result) + return result + +def file_contains(filepath, text): + with open(filepath, 'r') as f: + content = f.read() + return text in content + +def stage(num: str, expects: bool): + os.environ['GIT_PIN_CHANGE_TEST'] = num + shell_exec("mama update") + + result = file_contains(os.path.join('packages', 'ExampleRemote', 'ExampleRemote', 'remote.h'), 'REMOTE_VERSION') + + if expects: + assert result + else: + assert not result + +def test_git_pin_update(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + # Stages 0-3, switching between having REMOTE_VERSION and not to demonstrate that the contents actually change + for i in range(4): + stage(str(i), i % 2 == 1) \ No newline at end of file diff --git a/tests/test_git_pinning/mamafile.py b/tests/test_git_pinning/mamafile.py new file mode 100644 index 0000000..b4f462a --- /dev/null +++ b/tests/test_git_pinning/mamafile.py @@ -0,0 +1,13 @@ +import mama + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v1.0.0') + self.add_git('ExampleRemote2', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v2.0.0') + self.add_git('ExampleRemote3', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') + self.add_git('ExampleRemote4', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') \ No newline at end of file diff --git a/tests/test_git_pinning/test_git_pinning.py b/tests/test_git_pinning/test_git_pinning.py new file mode 100644 index 0000000..c069096 --- /dev/null +++ b/tests/test_git_pinning/test_git_pinning.py @@ -0,0 +1,27 @@ +import os +import sys + +def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: + if echo: print(f'exec: {cmd}') + result = os.system(cmd) + if result != 0 and exit_on_fail: + print(f'exec failed: code: {result} {cmd}') + if result >= 255: + result = 1 + sys.exit(result) + return result + +def file_contains(filepath, text): + with open(filepath, 'r') as f: + content = f.read() + return text in content + +def test_git_pinning(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + shell_exec("mama clean") + + assert not file_contains(os.path.join('packages', 'ExampleRemote', 'ExampleRemote', 'remote.h'), 'REMOTE_VERSION') + assert file_contains(os.path.join('packages', 'ExampleRemote2', 'ExampleRemote2', 'remote.h'), 'REMOTE_VERSION 2') + assert not file_contains(os.path.join('packages', 'ExampleRemote3', 'ExampleRemote3', 'remote.h'), 'REMOTE_VERSION') + assert file_contains(os.path.join('packages', 'ExampleRemote4', 'ExampleRemote4', 'remote.h'), 'REMOTE_VERSION 2') \ No newline at end of file From 962747a10a4dc453c297fb263599328487a6dc2f Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Tue, 10 Feb 2026 16:39:35 +0200 Subject: [PATCH 02/20] feat: Run tests on github action --- .github/workflows/tests.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..12fc7bf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Install dependencies + run: | + pip install -e . + pip install pytest + + - name: Run tests + run: pytest -v From 2c92e6bbccdf9211b18816dedbff55c49461b6f7 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 11:54:46 +0200 Subject: [PATCH 03/20] fix: Infinite loop in mama deploy dir copying --- mama/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mama/util.py b/mama/util.py index 1b2d437..d414359 100644 --- a/mama/util.py +++ b/mama/util.py @@ -434,7 +434,10 @@ def copy_dir(src_dir: str, out_dir: str, filter: list = None) -> bool: os.makedirs(out_dir, exist_ok=True) copied = False root = os.path.dirname(src_dir) - for fulldir, _, files in os.walk(src_dir): + norm_out = os.path.normcase(os.path.normpath(out_dir)) + for fulldir, dirs, files in os.walk(src_dir): + # skip the output directory to prevent infinite recursion + dirs[:] = [d for d in dirs if os.path.normcase(os.path.normpath(os.path.join(fulldir, d))) != norm_out] reldir = fulldir[len(root):].lstrip('\\/') if reldir: dst_folder = os.path.join(out_dir, reldir) From 487d69dbcc411416fec2323b90d418472645a3d1 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 11:55:04 +0200 Subject: [PATCH 04/20] feat: Support old papa file format --- mama/types/git.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mama/types/git.py b/mama/types/git.py index 5cdf443..da24f3f 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -52,8 +52,15 @@ def __str__(self): @staticmethod def from_papa_string(s: str) -> "Git": p = s.split(',') - name, url, branch, tag, mamafile, commit = p[0:6] - args = p[6:] + name, url, branch, tag, mamafile = p[0:5] + commit = '' + args = p[5:] + + # New format + if len(p) > 5 and p[5]: + commit = p[5] + args = p[6:] + shallow = True # shallow is the default return Git(name, url, branch, tag, mamafile, shallow, commit, args) From ff239cf6463375a827ad5aa0628bd5d06a181f9a Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 11:55:25 +0200 Subject: [PATCH 05/20] refactor: Clean up tests --- .../test_git_pin_change.py | 16 ++---------- tests/test_git_pinning/test_git_pinning.py | 25 ++++++------------ tests/testutils.py | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 tests/testutils.py diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py index 2f1eda1..3ba4828 100644 --- a/tests/test_git_pin_change/test_git_pin_change.py +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -1,20 +1,8 @@ import os import sys -def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: - if echo: print(f'exec: {cmd}') - result = os.system(cmd) - if result != 0 and exit_on_fail: - print(f'exec failed: code: {result} {cmd}') - if result >= 255: - result = 1 - sys.exit(result) - return result - -def file_contains(filepath, text): - with open(filepath, 'r') as f: - content = f.read() - return text in content +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from testutils import shell_exec, file_contains def stage(num: str, expects: bool): os.environ['GIT_PIN_CHANGE_TEST'] = num diff --git a/tests/test_git_pinning/test_git_pinning.py b/tests/test_git_pinning/test_git_pinning.py index c069096..c7d1756 100644 --- a/tests/test_git_pinning/test_git_pinning.py +++ b/tests/test_git_pinning/test_git_pinning.py @@ -1,27 +1,18 @@ import os import sys -def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: - if echo: print(f'exec: {cmd}') - result = os.system(cmd) - if result != 0 and exit_on_fail: - print(f'exec failed: code: {result} {cmd}') - if result >= 255: - result = 1 - sys.exit(result) - return result +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from testutils import shell_exec, file_contains -def file_contains(filepath, text): - with open(filepath, 'r') as f: - content = f.read() - return text in content +def get_dep_path(dep_name): + return os.path.join('packages', dep_name, dep_name) def test_git_pinning(): os.chdir(os.path.dirname(os.path.abspath(__file__))) shell_exec("mama clean") - assert not file_contains(os.path.join('packages', 'ExampleRemote', 'ExampleRemote', 'remote.h'), 'REMOTE_VERSION') - assert file_contains(os.path.join('packages', 'ExampleRemote2', 'ExampleRemote2', 'remote.h'), 'REMOTE_VERSION 2') - assert not file_contains(os.path.join('packages', 'ExampleRemote3', 'ExampleRemote3', 'remote.h'), 'REMOTE_VERSION') - assert file_contains(os.path.join('packages', 'ExampleRemote4', 'ExampleRemote4', 'remote.h'), 'REMOTE_VERSION 2') \ No newline at end of file + assert not file_contains(os.path.join(get_dep_path('ExampleRemote'), 'remote.h'), 'REMOTE_VERSION') + assert file_contains(os.path.join(get_dep_path('ExampleRemote2'), 'remote.h'), 'REMOTE_VERSION 2') + assert not file_contains(os.path.join(get_dep_path('ExampleRemote3'), 'remote.h'), 'REMOTE_VERSION') + assert file_contains(os.path.join(get_dep_path('ExampleRemote4'), 'remote.h'), 'REMOTE_VERSION 2') \ No newline at end of file diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 0000000..216594e --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,26 @@ +import os +import sys + +def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: + if echo: print(f'exec: {cmd}') + result = os.system(cmd) + if result != 0 and exit_on_fail: + print(f'exec failed: code: {result} {cmd}') + if result >= 255: + result = 1 + sys.exit(result) + return result + +def file_contains(filepath, text): + with open(filepath, 'r') as f: + content = f.read() + return text in content + +def file_exists(filepath): + return os.path.isfile(filepath) + +def is_windows(): + return os.name == 'nt' + +def is_linux(): + return os.name == 'posix' and sys.platform != 'darwin' \ No newline at end of file From f296c6da113b2506241242944df0c0090503cf7f Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 11:56:39 +0200 Subject: [PATCH 06/20] feat: Add tests for mama deploy and papa file parsing --- tests/test_papa_deploy/CMakeLists.txt | 12 +++++ tests/test_papa_deploy/consumer.cpp | 7 +++ tests/test_papa_deploy/mamafile.py | 7 +++ tests/test_papa_deploy/test_papa_deploy.py | 25 ++++++++++ tests/test_papa_parse/papa.txt | 5 ++ tests/test_papa_parse/papa_old.txt | 5 ++ tests/test_papa_parse/test_papa_parse.py | 58 ++++++++++++++++++++++ 7 files changed, 119 insertions(+) create mode 100644 tests/test_papa_deploy/CMakeLists.txt create mode 100644 tests/test_papa_deploy/consumer.cpp create mode 100644 tests/test_papa_deploy/mamafile.py create mode 100644 tests/test_papa_deploy/test_papa_deploy.py create mode 100644 tests/test_papa_parse/papa.txt create mode 100644 tests/test_papa_parse/papa_old.txt create mode 100644 tests/test_papa_parse/test_papa_parse.py diff --git a/tests/test_papa_deploy/CMakeLists.txt b/tests/test_papa_deploy/CMakeLists.txt new file mode 100644 index 0000000..dd54da3 --- /dev/null +++ b/tests/test_papa_deploy/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.6) +project(example_consumer) + +include(mama.cmake) +include_directories(${MAMA_INCLUDES}) + +file(GLOB EXAMPLE_CONSUMER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/*.h) +source_group(ExampleConsumer FILES ${EXAMPLE_CONSUMER_SOURCES}) +add_executable(ExampleConsumer ${EXAMPLE_CONSUMER_SOURCES}) +target_link_libraries(ExampleConsumer ${MAMA_LIBS}) + +install(TARGETS ExampleConsumer DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/bin) diff --git a/tests/test_papa_deploy/consumer.cpp b/tests/test_papa_deploy/consumer.cpp new file mode 100644 index 0000000..81d5c19 --- /dev/null +++ b/tests/test_papa_deploy/consumer.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char** argv) +{ + example::print_remote(argv[0]); + return 0; +} \ No newline at end of file diff --git a/tests/test_papa_deploy/mamafile.py b/tests/test_papa_deploy/mamafile.py new file mode 100644 index 0000000..1f5564e --- /dev/null +++ b/tests/test_papa_deploy/mamafile.py @@ -0,0 +1,7 @@ +import mama + +class ExampleConsumer(mama.BuildTarget): + workspace = 'packages' + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') diff --git a/tests/test_papa_deploy/test_papa_deploy.py b/tests/test_papa_deploy/test_papa_deploy.py new file mode 100644 index 0000000..a6e5fa2 --- /dev/null +++ b/tests/test_papa_deploy/test_papa_deploy.py @@ -0,0 +1,25 @@ +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from testutils import is_linux, shell_exec, file_exists, is_windows + +def test_git_pinning(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + shell_exec("mama build") + shell_exec("mama deploy") + + if is_windows(): + assert file_exists(os.path.join('bin', 'ExampleConsumer.exe')) + else: + assert file_exists(os.path.join('bin', 'ExampleConsumer')) + + if is_windows(): + platform_name = 'windows' + elif is_linux(): + platform_name = 'linux' + else: + raise Exception("Unsupported platform") + + assert file_exists(os.path.join('packages', 'ExampleConsumer', platform_name, 'deploy', 'ExampleConsumer', 'papa.txt')) \ No newline at end of file diff --git a/tests/test_papa_parse/papa.txt b/tests/test_papa_parse/papa.txt new file mode 100644 index 0000000..d473ccc --- /dev/null +++ b/tests/test_papa_parse/papa.txt @@ -0,0 +1,5 @@ +P ExampleConsumer +D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,,4acd9052f27a459314651dd485ae8fa79a04d49d, +I include +I include/test_papa_deploy +L RelWithDebInfo\ExampleConsumer.lib \ No newline at end of file diff --git a/tests/test_papa_parse/papa_old.txt b/tests/test_papa_parse/papa_old.txt new file mode 100644 index 0000000..3596fbb --- /dev/null +++ b/tests/test_papa_parse/papa_old.txt @@ -0,0 +1,5 @@ +P ExampleConsumer +D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,, +I include +I include/test_papa_deploy +L RelWithDebInfo\ExampleConsumer.lib \ No newline at end of file diff --git a/tests/test_papa_parse/test_papa_parse.py b/tests/test_papa_parse/test_papa_parse.py new file mode 100644 index 0000000..34f0612 --- /dev/null +++ b/tests/test_papa_parse/test_papa_parse.py @@ -0,0 +1,58 @@ +import os +from mama.papa_deploy import PapaFileInfo + +# Tests new papa file format parsing +def test_papa_parse(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + papa = PapaFileInfo('papa.txt') + + assert papa.project_name == 'ExampleConsumer' + + assert len(papa.dependencies) == 1 + dep = papa.dependencies[0] + assert dep.is_git + assert dep.name == 'ExampleRemote' + assert dep.url == 'https://github.com/BatteredBunny/MamaExampleRemote.git' + assert dep.branch == '' + assert dep.tag == '' + assert dep.mamafile == '' + assert dep.commit == '4acd9052f27a459314651dd485ae8fa79a04d49d' + + assert len(papa.includes) == 2 + assert papa.includes[0].endswith('include') + assert papa.includes[1].endswith('include/test_papa_deploy') + + assert len(papa.libs) == 1 + assert papa.libs[0].endswith('RelWithDebInfo/ExampleConsumer.lib') + + assert len(papa.syslibs) == 0 + assert len(papa.assets) == 0 + +# Test old papa file format parsing +def test_papa_parse_old(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + papa = PapaFileInfo('papa_old.txt') + + assert papa.project_name == 'ExampleConsumer' + + assert len(papa.dependencies) == 1 + dep = papa.dependencies[0] + assert dep.is_git + assert dep.name == 'ExampleRemote' + assert dep.url == 'https://github.com/BatteredBunny/MamaExampleRemote.git' + assert dep.branch == '' + assert dep.tag == '' + assert dep.mamafile == '' + assert dep.commit == '' + + assert len(papa.includes) == 2 + assert papa.includes[0].endswith('include') + assert papa.includes[1].endswith('include/test_papa_deploy') + + assert len(papa.libs) == 1 + assert papa.libs[0].endswith('RelWithDebInfo/ExampleConsumer.lib') + + assert len(papa.syslibs) == 0 + assert len(papa.assets) == 0 \ No newline at end of file From 6919e744325424181dbc454aa449d50080d0f968 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 12:16:21 +0200 Subject: [PATCH 07/20] feat: Add test for updating stale dep --- tests/test_stale_dep/mamafile.py | 10 +++++++++ tests/test_stale_dep/test_stale_dep.py | 29 ++++++++++++++++++++++++++ tests/testutils.py | 12 ++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/test_stale_dep/mamafile.py create mode 100644 tests/test_stale_dep/test_stale_dep.py diff --git a/tests/test_stale_dep/mamafile.py b/tests/test_stale_dep/mamafile.py new file mode 100644 index 0000000..286c74d --- /dev/null +++ b/tests/test_stale_dep/mamafile.py @@ -0,0 +1,10 @@ +import mama + +class test(mama.BuildTarget): + workspace = 'packages' + + def build(self): + self.nothing_to_build() + + def dependencies(self): + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git') diff --git a/tests/test_stale_dep/test_stale_dep.py b/tests/test_stale_dep/test_stale_dep.py new file mode 100644 index 0000000..0b76fd2 --- /dev/null +++ b/tests/test_stale_dep/test_stale_dep.py @@ -0,0 +1,29 @@ +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from testutils import shell_exec, file_contains, rmtree + +def get_dep_path(dep_name): + return os.path.join('packages', dep_name, dep_name) + +# Simulates stale dependency updating +def test_stale_dep(): + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + # Clean state for testing + if os.path.exists('packages'): + rmtree('packages') + + dep_dir = get_dep_path('ExampleRemote') + header = os.path.join(dep_dir, 'remote.h') + shell_exec('mama build unshallow') + assert file_contains(header, 'REMOTE_VERSION') + + # Switch to older commit + old_commit = '4acd9052f27a459314651dd485ae8fa79a04d49d' + shell_exec(f'cd {dep_dir} && git reset --hard {old_commit}') + assert not file_contains(header, 'REMOTE_VERSION') + + shell_exec('mama update') + assert file_contains(header, 'REMOTE_VERSION') diff --git a/tests/testutils.py b/tests/testutils.py index 216594e..87e7403 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,4 +1,5 @@ import os +import shutil import sys def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: @@ -23,4 +24,13 @@ def is_windows(): return os.name == 'nt' def is_linux(): - return os.name == 'posix' and sys.platform != 'darwin' \ No newline at end of file + return os.name == 'posix' and sys.platform != 'darwin' + +def onerror(func, path, _): + import stat + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + +def rmtree(path: str): + shutil.rmtree(path, onerror=onerror) \ No newline at end of file From c55d63fe24451af81e86c642aacff8b045fbae9f Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 12:18:39 +0200 Subject: [PATCH 08/20] feat: Add support for non-shallow commit pinning --- mama/types/git.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/mama/types/git.py b/mama/types/git.py index da24f3f..1c0a41f 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -144,7 +144,7 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: def fetch_origin(self, dep: BuildDependency): - self.run_git(dep, f"pull origin {self.branch_or_tag()} -q") + self.run_git(dep, f"fetch origin {self.branch_or_tag()} -q") def git_status_file(self, dep: BuildDependency): @@ -287,6 +287,21 @@ def print_output(p:SubProcess, line:str): raise RuntimeError(f'Target {self.name} clone failed: {cmd}') + def clone_commit_pinned(self, dep: BuildDependency, shallow: bool): + # Pinned commit follows a different clone strategy + if dep.config.print: + console(f' - Target {dep.name: <16} CLONE commit-pinned {self.commit}', color=Color.BLUE) + os.makedirs(dep.src_dir, exist_ok=True) + execute(f'git init {dep.src_dir}') + execute(f'cd {dep.src_dir} && git remote add origin {self.url}') + depth = '--depth 1' if shallow else '' + self.run_git(dep, f'fetch {depth} origin {self.commit}') + self.run_git(dep, 'reset --hard FETCH_HEAD') + self.run_git(dep, 'submodule update --init --recursive') + if dep.config.print: + console(f' - Target {dep.name: <16} CLONE SUCCESS', color=Color.BLUE) + + def clone_or_pull(self, dep: BuildDependency, wiped=False): # by default we create a shallow clone, unless unshallow is specified in config or this dep unshallow = dep.config.unshallow or (not self.shallow) @@ -294,9 +309,7 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) if self.commit: - clone_args = f"--recurse-submodules {self.url}" - self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch(dep) + self.clone_commit_pinned(dep, shallow=not unshallow) else: branch = self.branch_or_tag() if branch: branch = f" --branch {self.branch_or_tag()}" @@ -307,11 +320,17 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) + if self.commit: + depth = '--depth 1' if not unshallow else '' + self.run_git(dep, f'fetch {depth} origin {self.commit}') + self.run_git(dep, 'reset --hard FETCH_HEAD') + self.run_git(dep, 'submodule update --init --recursive') + return if unshallow: self.unshallow(dep) self.checkout_current_branch(dep) self.run_git(dep, 'submodule update --init --recursive') - if not self.tag and not self.commit: # pull if not a tag or commit pin + if not self.tag: # pull if not a tag self.run_git(dep, "reset --hard -q") self.run_git(dep, "pull") From 9ce501d43e306372c83203c3c9c674a8d8ee95bc Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 12:22:29 +0200 Subject: [PATCH 09/20] refactor: Clean up test_git_pinning --- tests/test_git_pinning/test_git_pinning.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_git_pinning/test_git_pinning.py b/tests/test_git_pinning/test_git_pinning.py index c7d1756..4685176 100644 --- a/tests/test_git_pinning/test_git_pinning.py +++ b/tests/test_git_pinning/test_git_pinning.py @@ -4,15 +4,15 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from testutils import shell_exec, file_contains -def get_dep_path(dep_name): - return os.path.join('packages', dep_name, dep_name) +def remote_file_contains(dep_name, text): + return file_contains(os.path.join('packages', dep_name, dep_name, 'remote.h'), text) def test_git_pinning(): os.chdir(os.path.dirname(os.path.abspath(__file__))) shell_exec("mama clean") - assert not file_contains(os.path.join(get_dep_path('ExampleRemote'), 'remote.h'), 'REMOTE_VERSION') - assert file_contains(os.path.join(get_dep_path('ExampleRemote2'), 'remote.h'), 'REMOTE_VERSION 2') - assert not file_contains(os.path.join(get_dep_path('ExampleRemote3'), 'remote.h'), 'REMOTE_VERSION') - assert file_contains(os.path.join(get_dep_path('ExampleRemote4'), 'remote.h'), 'REMOTE_VERSION 2') \ No newline at end of file + assert not remote_file_contains('ExampleRemote', 'REMOTE_VERSION') + assert remote_file_contains('ExampleRemote2', 'REMOTE_VERSION 2') + assert not remote_file_contains('ExampleRemote3', 'REMOTE_VERSION') + assert remote_file_contains('ExampleRemote4', 'REMOTE_VERSION 2') \ No newline at end of file From ee800f574161c045060ebb5b561e8e827f6b7c6f Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 14:04:10 +0200 Subject: [PATCH 10/20] review changes --- mama/types/git.py | 83 ++++++++----------- tests/conftest.py | 5 ++ .../test_git_pin_change.py | 27 +++--- tests/test_git_pinning/test_git_pinning.py | 21 ++--- tests/test_papa_deploy/test_papa_deploy.py | 19 ++--- tests/test_papa_parse/test_papa_parse.py | 12 +-- tests/test_stale_dep/test_stale_dep.py | 22 ++--- tests/testutils.py | 24 ++++-- 8 files changed, 101 insertions(+), 112 deletions(-) create mode 100644 tests/conftest.py diff --git a/mama/types/git.py b/mama/types/git.py index 1c0a41f..803c1bf 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -24,7 +24,7 @@ def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow self.url = url self.branch = branch self.tag = tag - self.commit = commit + self.commit_pin = commit self.mamafile = mamafile self.shallow = shallow self.args = args @@ -45,7 +45,7 @@ def __str__(self): s = f'DepSource Git {self.name} {self.url}' tag = self.branch_or_tag() if tag: s += ' ' + tag - if self.commit: s += ' commit=' + self.commit + if self.commit_pin: s += ' commit_pin=' + self.commit_pin if self.mamafile: s += ' ' + self.mamafile return s @@ -53,21 +53,21 @@ def __str__(self): def from_papa_string(s: str) -> "Git": p = s.split(',') name, url, branch, tag, mamafile = p[0:5] - commit = '' + commit_pin = '' args = p[5:] # New format if len(p) > 5 and p[5]: - commit = p[5] + commit_pin = p[5] args = p[6:] shallow = True # shallow is the default - return Git(name, url, branch, tag, mamafile, shallow, commit, args) + return Git(name, url, branch, tag, mamafile, shallow, commit_pin, args) def get_papa_string(self): fields = DepSource.papa_join( - self.name, self.url, self.branch, self.tag, self.mamafile, self.commit, self.args) + self.name, self.url, self.branch, self.tag, self.mamafile, self.commit_pin, self.args) return 'git ' + fields @@ -108,10 +108,10 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: return result # explicit commit pin? - if self.commit: + if self.commit_pin: if dep.config.verbose: - console(f' {self.name} using pinned commit hash: {self.commit}') - return self.commit + console(f' {self.name} using pinned commit hash: {self.commit_pin}') + return self.commit_pin # is the tag actually a commit hash? if self.tag and all(c in string.hexdigits for c in self.tag): @@ -153,7 +153,7 @@ def git_status_file(self, dep: BuildDependency): def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n{self.commit}\n" + status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n{self.commit_pin}\n" if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') @@ -190,12 +190,12 @@ def check_status(self, dep: BuildDependency): self.commit_changed = True self.commit_pin_changed = True return True - if not self.commit: + if not self.commit_pin: self.fetch_origin(dep) self.url_changed = self.url != status[0] self.tag_changed = self.tag != status[1] self.branch_changed = self.branch != status[2] - self.commit_pin_changed = self.commit != status[4] + self.commit_pin_changed = self.commit_pin != status[4] self.commit_changed = self.get_commit_hash(dep, use_cache=False) != status[3] #console(f'check_status {self.url} {self.branch_or_tag()}: urlc={self.url_changed} tagc={self.tag_changed} brnc={self.branch_changed} cmtc={self.commit_changed}') return self.url_changed or self.tag_changed or self.branch_changed or self.commit_changed or self.commit_pin_changed @@ -207,15 +207,18 @@ def branch_or_tag(self): return '' - def checkout_current_branch(self, dep: BuildDependency): - if self.commit: - self.run_git(dep, f"checkout {self.commit}") - return - branch = self.branch_or_tag() - if branch: - if self.tag and self.tag_changed: - self.run_git(dep, "reset --hard") - self.run_git(dep, f"checkout {branch}") + def checkout_current_branch_or_commit(self, dep: BuildDependency): + if self.commit_pin: + self.run_git(dep, f'fetch --depth 1 origin {self.commit_pin}') + self.run_git(dep, f'checkout {self.commit_pin}') + else: + branch = self.branch_or_tag() + if branch: + if self.tag and self.tag_changed: + self.run_git(dep, "reset --hard") + if self.tag: + self.run_git(dep, f"fetch origin tag {self.tag}") + self.run_git(dep, f"checkout {branch}") def reclone_wipe(self, dep: BuildDependency): @@ -287,50 +290,34 @@ def print_output(p:SubProcess, line:str): raise RuntimeError(f'Target {self.name} clone failed: {cmd}') - def clone_commit_pinned(self, dep: BuildDependency, shallow: bool): - # Pinned commit follows a different clone strategy - if dep.config.print: - console(f' - Target {dep.name: <16} CLONE commit-pinned {self.commit}', color=Color.BLUE) - os.makedirs(dep.src_dir, exist_ok=True) - execute(f'git init {dep.src_dir}') - execute(f'cd {dep.src_dir} && git remote add origin {self.url}') - depth = '--depth 1' if shallow else '' - self.run_git(dep, f'fetch {depth} origin {self.commit}') - self.run_git(dep, 'reset --hard FETCH_HEAD') - self.run_git(dep, 'submodule update --init --recursive') - if dep.config.print: - console(f' - Target {dep.name: <16} CLONE SUCCESS', color=Color.BLUE) - - def clone_or_pull(self, dep: BuildDependency, wiped=False): # by default we create a shallow clone, unless unshallow is specified in config or this dep unshallow = dep.config.unshallow or (not self.shallow) if is_dir_empty(dep.src_dir): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) - if self.commit: - self.clone_commit_pinned(dep, shallow=not unshallow) + if self.commit_pin: + # pinned commits always use shallow clone + clone_args = f"--no-checkout --depth 1 {self.url}" else: branch = self.branch_or_tag() if branch: branch = f" --branch {self.branch_or_tag()}" depth = '' if unshallow else '--depth 1' clone_args = f"--recurse-submodules {depth} {branch} {self.url}" - self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch(dep) + + self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) + self.checkout_current_branch_or_commit(dep) + + if self.commit_pin: + self.run_git(dep, 'submodule update --init --recursive') else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) - if self.commit: - depth = '--depth 1' if not unshallow else '' - self.run_git(dep, f'fetch {depth} origin {self.commit}') - self.run_git(dep, 'reset --hard FETCH_HEAD') - self.run_git(dep, 'submodule update --init --recursive') - return if unshallow: self.unshallow(dep) - self.checkout_current_branch(dep) + self.checkout_current_branch_or_commit(dep) self.run_git(dep, 'submodule update --init --recursive') - if not self.tag: # pull if not a tag + if not self.tag and not self.commit_pin: # pull if not a tag self.run_git(dep, "reset --hard -q") self.run_git(dep, "pull") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5793767 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import os +import sys + +# Add the tests directory to sys.path so test files can import testutils directly +sys.path.insert(0, os.path.dirname(__file__)) diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py index 3ba4828..247c107 100644 --- a/tests/test_git_pin_change/test_git_pin_change.py +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -1,23 +1,22 @@ import os -import sys +from testutils import init, shell_exec, file_contains -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from testutils import shell_exec, file_contains - -def stage(num: str, expects: bool): - os.environ['GIT_PIN_CHANGE_TEST'] = num +def stage(num: int, expects: bool, assert_message: str = ""): + os.environ['GIT_PIN_CHANGE_TEST'] = str(num) shell_exec("mama update") - result = file_contains(os.path.join('packages', 'ExampleRemote', 'ExampleRemote', 'remote.h'), 'REMOTE_VERSION') + result = file_contains('packages/ExampleRemote/ExampleRemote/remote.h', 'REMOTE_VERSION') if expects: - assert result + assert result, assert_message else: - assert not result + assert not result, assert_message -def test_git_pin_update(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) +# Test that switches between having REMOTE_VERSION and not to demonstrate that the contents actually change when changing git_commit or git_tag pins +def test_git_pin_change(): + init(__file__, clean_dirs=['packages']) - # Stages 0-3, switching between having REMOTE_VERSION and not to demonstrate that the contents actually change - for i in range(4): - stage(str(i), i % 2 == 1) \ No newline at end of file + stage(0, False, "Failed to pin to a specific commit") + stage(1, True, "Failed to update commit pin to a new commit") + stage(2, False, "Failed to switch from commit pin to tag pin") + stage(3, True, "Failed to update between tag pins") diff --git a/tests/test_git_pinning/test_git_pinning.py b/tests/test_git_pinning/test_git_pinning.py index 4685176..4ad97c5 100644 --- a/tests/test_git_pinning/test_git_pinning.py +++ b/tests/test_git_pinning/test_git_pinning.py @@ -1,18 +1,15 @@ -import os -import sys - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from testutils import shell_exec, file_contains +from testutils import init, shell_exec, file_contains def remote_file_contains(dep_name, text): - return file_contains(os.path.join('packages', dep_name, dep_name, 'remote.h'), text) + return file_contains(f'packages/{dep_name}/{dep_name}/remote.h', text) +# Make sure different git pinning methods work def test_git_pinning(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) - + init(__file__, clean_dirs=['packages']) shell_exec("mama clean") - assert not remote_file_contains('ExampleRemote', 'REMOTE_VERSION') - assert remote_file_contains('ExampleRemote2', 'REMOTE_VERSION 2') - assert not remote_file_contains('ExampleRemote3', 'REMOTE_VERSION') - assert remote_file_contains('ExampleRemote4', 'REMOTE_VERSION 2') \ No newline at end of file + # https://github.com/BatteredBunny/MamaExampleRemote repo has different commits that either do or dont have the REMOTE_VERSION line + assert not remote_file_contains('ExampleRemote', 'REMOTE_VERSION'), "Tag pinning went wrong" + assert remote_file_contains('ExampleRemote2', 'REMOTE_VERSION 2'), "Tag pinning went wrong" + assert not remote_file_contains('ExampleRemote3', 'REMOTE_VERSION'), "Commit pinning went wrong" + assert remote_file_contains('ExampleRemote4', 'REMOTE_VERSION 2'), "Commit pinning went wrong" diff --git a/tests/test_papa_deploy/test_papa_deploy.py b/tests/test_papa_deploy/test_papa_deploy.py index a6e5fa2..1aa50fd 100644 --- a/tests/test_papa_deploy/test_papa_deploy.py +++ b/tests/test_papa_deploy/test_papa_deploy.py @@ -1,19 +1,18 @@ -import os -import sys +from testutils import init, is_linux, shell_exec, file_exists, is_windows -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from testutils import is_linux, shell_exec, file_exists, is_windows - -def test_git_pinning(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) +# Generic test to verify basic build and deploy functions work +def test_papa_deploy(): + init(__file__, clean_dirs=['bin', 'packages']) shell_exec("mama build") shell_exec("mama deploy") if is_windows(): - assert file_exists(os.path.join('bin', 'ExampleConsumer.exe')) + extension = '.exe' else: - assert file_exists(os.path.join('bin', 'ExampleConsumer')) + extension = '' + + assert file_exists(f'bin/ExampleConsumer{extension}'), "Deployed executable not found" if is_windows(): platform_name = 'windows' @@ -22,4 +21,4 @@ def test_git_pinning(): else: raise Exception("Unsupported platform") - assert file_exists(os.path.join('packages', 'ExampleConsumer', platform_name, 'deploy', 'ExampleConsumer', 'papa.txt')) \ No newline at end of file + assert file_exists(f'packages/ExampleConsumer/{platform_name}/deploy/ExampleConsumer/papa.txt'), "Deployed papa.txt not found for dependency" diff --git a/tests/test_papa_parse/test_papa_parse.py b/tests/test_papa_parse/test_papa_parse.py index 34f0612..82a2195 100644 --- a/tests/test_papa_parse/test_papa_parse.py +++ b/tests/test_papa_parse/test_papa_parse.py @@ -1,9 +1,9 @@ -import os +from testutils import init from mama.papa_deploy import PapaFileInfo # Tests new papa file format parsing def test_papa_parse(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) + init(__file__) papa = PapaFileInfo('papa.txt') @@ -17,7 +17,7 @@ def test_papa_parse(): assert dep.branch == '' assert dep.tag == '' assert dep.mamafile == '' - assert dep.commit == '4acd9052f27a459314651dd485ae8fa79a04d49d' + assert dep.commit_pin == '4acd9052f27a459314651dd485ae8fa79a04d49d' assert len(papa.includes) == 2 assert papa.includes[0].endswith('include') @@ -31,7 +31,7 @@ def test_papa_parse(): # Test old papa file format parsing def test_papa_parse_old(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) + init(__file__) papa = PapaFileInfo('papa_old.txt') @@ -45,7 +45,7 @@ def test_papa_parse_old(): assert dep.branch == '' assert dep.tag == '' assert dep.mamafile == '' - assert dep.commit == '' + assert dep.commit_pin == '' assert len(papa.includes) == 2 assert papa.includes[0].endswith('include') @@ -55,4 +55,4 @@ def test_papa_parse_old(): assert papa.libs[0].endswith('RelWithDebInfo/ExampleConsumer.lib') assert len(papa.syslibs) == 0 - assert len(papa.assets) == 0 \ No newline at end of file + assert len(papa.assets) == 0 diff --git a/tests/test_stale_dep/test_stale_dep.py b/tests/test_stale_dep/test_stale_dep.py index 0b76fd2..d9c5fc6 100644 --- a/tests/test_stale_dep/test_stale_dep.py +++ b/tests/test_stale_dep/test_stale_dep.py @@ -1,29 +1,21 @@ -import os -import sys - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from testutils import shell_exec, file_contains, rmtree +from testutils import init, shell_exec, file_contains def get_dep_path(dep_name): - return os.path.join('packages', dep_name, dep_name) + return f'packages/{dep_name}/{dep_name}' # Simulates stale dependency updating def test_stale_dep(): - os.chdir(os.path.dirname(os.path.abspath(__file__))) - - # Clean state for testing - if os.path.exists('packages'): - rmtree('packages') + init(__file__, clean_dirs=['packages']) dep_dir = get_dep_path('ExampleRemote') - header = os.path.join(dep_dir, 'remote.h') + header = f'{dep_dir}/remote.h' shell_exec('mama build unshallow') - assert file_contains(header, 'REMOTE_VERSION') + assert file_contains(header, 'REMOTE_VERSION'), 'Failed to clone dependency repo' # Switch to older commit old_commit = '4acd9052f27a459314651dd485ae8fa79a04d49d' shell_exec(f'cd {dep_dir} && git reset --hard {old_commit}') - assert not file_contains(header, 'REMOTE_VERSION') + assert not file_contains(header, 'REMOTE_VERSION'), "Failed to switch to old commit" # Should only fail if something happens to the testing repo shell_exec('mama update') - assert file_contains(header, 'REMOTE_VERSION') + assert file_contains(header, 'REMOTE_VERSION'), "Failed updating to latest commit" diff --git a/tests/testutils.py b/tests/testutils.py index 87e7403..aa2d901 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,8 +1,17 @@ import os import shutil import sys +from typing import Iterable -def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: +def init(caller_file: str = '', clean_dirs: Iterable[str] = []): + # Needed for mama commands to perform work in the correct directory + if caller_file: + os.chdir(os.path.dirname(os.path.abspath(caller_file))) + + for d in clean_dirs: + rmdir(d) + +def shell_exec(cmd: str, exit_on_fail: bool = True, echo: bool = True) -> int: if echo: print(f'exec: {cmd}') result = os.system(cmd) if result != 0 and exit_on_fail: @@ -12,18 +21,18 @@ def shell_exec(cmd, exit_on_fail=True, echo=True) -> int: sys.exit(result) return result -def file_contains(filepath, text): +def file_contains(filepath: str, text: str) -> bool: with open(filepath, 'r') as f: content = f.read() return text in content -def file_exists(filepath): +def file_exists(filepath: str) -> bool: return os.path.isfile(filepath) -def is_windows(): +def is_windows() -> bool: return os.name == 'nt' -def is_linux(): +def is_linux() -> bool: return os.name == 'posix' and sys.platform != 'darwin' def onerror(func, path, _): @@ -32,5 +41,6 @@ def onerror(func, path, _): os.chmod(path, stat.S_IWUSR) func(path) -def rmtree(path: str): - shutil.rmtree(path, onerror=onerror) \ No newline at end of file +def rmdir(path: str): + if os.path.exists(path): + shutil.rmtree(path, onerror=onerror) \ No newline at end of file From 1dbaa70823cf1bb19c472f1d9922ce128a989ef1 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Wed, 11 Feb 2026 14:41:50 +0200 Subject: [PATCH 11/20] fix: Support old git_status file --- mama/types/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mama/types/git.py b/mama/types/git.py index 803c1bf..e64ee4a 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -166,7 +166,7 @@ def read_stored_status(self, dep: BuildDependency): tag = lines[1].rstrip() branch = lines[2].rstrip() commit = lines[3].rstrip() - commit_pin = lines[4].rstrip() + commit_pin = '' if len(lines) <= 4 else lines[4].rstrip() return (url, tag, branch, commit, commit_pin) From fc95f6041d91289f3a66223d351bb3ec6747b151 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 12:25:38 +0200 Subject: [PATCH 12/20] review changes --- tests/test_papa_deploy/test_papa_deploy.py | 19 ++---------- tests/testutils.py | 34 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/tests/test_papa_deploy/test_papa_deploy.py b/tests/test_papa_deploy/test_papa_deploy.py index 1aa50fd..ebe6634 100644 --- a/tests/test_papa_deploy/test_papa_deploy.py +++ b/tests/test_papa_deploy/test_papa_deploy.py @@ -1,4 +1,4 @@ -from testutils import init, is_linux, shell_exec, file_exists, is_windows +from testutils import init, shell_exec, file_exists, executable_extension, native_platform_name # Generic test to verify basic build and deploy functions work def test_papa_deploy(): @@ -7,18 +7,5 @@ def test_papa_deploy(): shell_exec("mama build") shell_exec("mama deploy") - if is_windows(): - extension = '.exe' - else: - extension = '' - - assert file_exists(f'bin/ExampleConsumer{extension}'), "Deployed executable not found" - - if is_windows(): - platform_name = 'windows' - elif is_linux(): - platform_name = 'linux' - else: - raise Exception("Unsupported platform") - - assert file_exists(f'packages/ExampleConsumer/{platform_name}/deploy/ExampleConsumer/papa.txt'), "Deployed papa.txt not found for dependency" + assert file_exists(f'bin/ExampleConsumer{executable_extension()}'), "Deployed executable not found" + assert file_exists(f'packages/ExampleConsumer/{native_platform_name()}/deploy/ExampleConsumer/papa.txt'), "Deployed papa.txt not found for dependency" diff --git a/tests/testutils.py b/tests/testutils.py index aa2d901..15d615b 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -35,6 +35,40 @@ def is_windows() -> bool: def is_linux() -> bool: return os.name == 'posix' and sys.platform != 'darwin' +def is_macos() -> bool: + return sys.platform == 'darwin' + +def executable_extension() -> str: + if is_windows(): + return '.exe' + + return '' + +def static_library_extension() -> str: + if is_windows(): + return '.lib' + else: + return '.a' + +def dynamic_library_extension() -> str: + if is_windows(): + return '.dll' + elif is_macos(): + return '.dylib' + else: + return '.so' + +# Excludes for example android +def native_platform_name() -> str: + if is_windows(): + return 'windows' + elif is_linux(): + return 'linux' + elif is_macos(): + return 'macos' + else: + raise Exception("Unsupported platform") + def onerror(func, path, _): import stat if not os.access(path, os.W_OK): From a31028f91113d133b756b2596f54451a7ab8e9a9 Mon Sep 17 00:00:00 2001 From: Jorma Rebane Date: Thu, 12 Feb 2026 13:40:36 +0200 Subject: [PATCH 13/20] feature: support commit pinning via tags --- .gitignore | 3 +++ mama/types/git.py | 21 ++++++++++++++------- tests/test/example_consumer/CMakeLists.txt | 6 ++---- tests/test/example_consumer/mamafile.py | 8 ++++++-- tests/test/example_library/CMakeLists.txt | 5 ++++- tests/test/example_library/library.cpp | 2 +- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index fb3127d..543a33a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ dist/ mama/mama.spec mama.cmake + +packages/ +bin/ \ No newline at end of file diff --git a/mama/types/git.py b/mama/types/git.py index 5a6ca43..8b098c5 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -81,6 +81,9 @@ def get_current_repository_commit(dep: BuildDependency): console(f' {dep.name: <16} git show --format=%h -s: {result}') return result + @staticmethod + def is_hex_string(s: str) -> bool: + return len(s) > 0 and all(c in string.hexdigits for c in s) def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: bool): """ @@ -98,7 +101,7 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: return result # is the tag actually a commit hash? - if self.tag and all(c in string.hexdigits for c in self.tag): + if Git.is_hex_string(self.tag): if dep.config.verbose: console(f' {self.name} using tag as the commit hash: {self.tag}') return self.tag @@ -187,11 +190,13 @@ def branch_or_tag(self): return '' - def checkout_current_branch(self, dep: BuildDependency): + def checkout_current_branch_or_tag(self, dep: BuildDependency, is_commit_pin=False): branch = self.branch_or_tag() if branch: if self.tag and self.tag_changed: self.run_git(dep, "reset --hard") + if is_commit_pin: + self.run_git(dep, f"fetch --depth 1 origin {branch}") self.run_git(dep, f"checkout {branch}") @@ -270,18 +275,20 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): if is_dir_empty(dep.src_dir): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) - branch = self.branch_or_tag() - if branch: branch = f" --branch {self.branch_or_tag()}" + br_or_tag = self.branch_or_tag() + is_commit_pin = Git.is_hex_string(br_or_tag) + checkout_branch = '' if is_commit_pin or len(br_or_tag) == 0 else f' --branch {br_or_tag}' depth = '' if unshallow else '--depth 1' - clone_args = f"--recurse-submodules {depth} {branch} {self.url}" + clone_args = f"--recurse-submodules {depth} {checkout_branch} {self.url}" self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch(dep) + self.checkout_current_branch_or_tag(dep, is_commit_pin=is_commit_pin) else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) if unshallow: self.unshallow(dep) - self.checkout_current_branch(dep) + is_commit_pin = Git.is_hex_string(self.branch_or_tag()) + self.checkout_current_branch_or_tag(dep, is_commit_pin=is_commit_pin) self.run_git(dep, 'submodule update --init --recursive') if not self.tag: # pull if not a tag self.run_git(dep, "reset --hard -q") diff --git a/tests/test/example_consumer/CMakeLists.txt b/tests/test/example_consumer/CMakeLists.txt index 5691f30..ae2a168 100644 --- a/tests/test/example_consumer/CMakeLists.txt +++ b/tests/test/example_consumer/CMakeLists.txt @@ -1,12 +1,10 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) project(example_consumer) include(mama.cmake) include_directories(${MAMA_INCLUDES}) -file(GLOB_RECURSE EXAMPLE_CONSUMER_SOURCES *.cpp *.h) -source_group(ExampleConsumer FILES ${EXAMPLE_CONSUMER_SOURCES}) -add_executable(ExampleConsumer ${EXAMPLE_CONSUMER_SOURCES}) +add_executable(ExampleConsumer consumer.cpp) target_link_libraries(ExampleConsumer ${MAMA_LIBS}) install(TARGETS ExampleConsumer DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/bin) diff --git a/tests/test/example_consumer/mamafile.py b/tests/test/example_consumer/mamafile.py index b36eecf..b8443ee 100644 --- a/tests/test/example_consumer/mamafile.py +++ b/tests/test/example_consumer/mamafile.py @@ -1,11 +1,15 @@ import mama class ExampleConsumer(mama.BuildTarget): - workspace = 'wolf3d' + workspace = 'packages' + + def init(self): + self.prefer_gcc() def dependencies(self): self.add_local('ExampleLibrary', '../example_library') - self.add_git('ExampleRemote', 'https://github.com/RedFox20/MamaExampleRemote.git') + self.add_git('ExampleRemote', 'https://github.com/RedFox20/MamaExampleRemote.git', + git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') # optional: pre-build configuration step def configure(self): diff --git a/tests/test/example_library/CMakeLists.txt b/tests/test/example_library/CMakeLists.txt index 92e89f5..c43ccf2 100644 --- a/tests/test/example_library/CMakeLists.txt +++ b/tests/test/example_library/CMakeLists.txt @@ -1,6 +1,9 @@ -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) project(example_library) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS ON) file(GLOB_RECURSE EXAMPLE_LIBRARY_SOURCES *.cpp *.h) source_group(ExampleLibrary FILES ${EXAMPLE_LIBRARY_SOURCES}) diff --git a/tests/test/example_library/library.cpp b/tests/test/example_library/library.cpp index 7bd61ab..169f14b 100644 --- a/tests/test/example_library/library.cpp +++ b/tests/test/example_library/library.cpp @@ -5,7 +5,7 @@ namespace example { // ensure C++17 features are used - namespace fs = std::experimental::filesystem; + namespace fs = std::filesystem; bool print_file_exists(const std::string& str) { From b445a86094753f569515e104094402ceebf86710 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 13:47:16 +0200 Subject: [PATCH 14/20] fix: Bug in test_stale_dep test --- mama/types/git.py | 6 +++++- tests/test_stale_dep/test_stale_dep.py | 24 +++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/mama/types/git.py b/mama/types/git.py index e64ee4a..6ba6692 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -151,9 +151,13 @@ def git_status_file(self, dep: BuildDependency): return path_join(dep.build_dir, 'git_status') + @staticmethod + def format_git_status(url, tag, branch, commit, commit_pin=''): + return f"{url}\n{tag}\n{branch}\n{commit}\n{commit_pin}\n" + def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n{self.commit_pin}\n" + status = Git.format_git_status(self.url, self.tag, self.branch, commit, self.commit_pin) if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') diff --git a/tests/test_stale_dep/test_stale_dep.py b/tests/test_stale_dep/test_stale_dep.py index d9c5fc6..7af384c 100644 --- a/tests/test_stale_dep/test_stale_dep.py +++ b/tests/test_stale_dep/test_stale_dep.py @@ -1,8 +1,24 @@ -from testutils import init, shell_exec, file_contains +from testutils import init, shell_exec, file_contains, native_platform_name +from mama.types.git import Git + +REPO_URL = 'https://github.com/BatteredBunny/MamaExampleRemote.git' +OLD_COMMIT = '4acd9052f27a459314651dd485ae8fa79a04d49d' +OLD_SHORT = OLD_COMMIT[:7] def get_dep_path(dep_name): return f'packages/{dep_name}/{dep_name}' +def get_git_status_path(dep_name): + return f'packages/{dep_name}/{native_platform_name()}/git_status' + +def switch_to_stale_commit(dep_name): + dep_dir = get_dep_path(dep_name) + shell_exec(f'cd {dep_dir} && git reset --hard {OLD_COMMIT}') + + status_file = get_git_status_path(dep_name) + with open(status_file, 'w') as f: + f.write(Git.format_git_status(REPO_URL, '', '', OLD_SHORT)) + # Simulates stale dependency updating def test_stale_dep(): init(__file__, clean_dirs=['packages']) @@ -12,10 +28,8 @@ def test_stale_dep(): shell_exec('mama build unshallow') assert file_contains(header, 'REMOTE_VERSION'), 'Failed to clone dependency repo' - # Switch to older commit - old_commit = '4acd9052f27a459314651dd485ae8fa79a04d49d' - shell_exec(f'cd {dep_dir} && git reset --hard {old_commit}') - assert not file_contains(header, 'REMOTE_VERSION'), "Failed to switch to old commit" # Should only fail if something happens to the testing repo + switch_to_stale_commit('ExampleRemote') + assert not file_contains(header, 'REMOTE_VERSION'), 'Failed to switch to stale commit' shell_exec('mama update') assert file_contains(header, 'REMOTE_VERSION'), "Failed updating to latest commit" From 6c27b9e76f3d5a5cdc74b7d0ab86b926446845fc Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 13:52:16 +0200 Subject: [PATCH 15/20] revert git_commit_pin changes --- mama/build_target.py | 71 +++++++++++++++++++-------------------- mama/types/git.py | 80 ++++++++++++-------------------------------- 2 files changed, 56 insertions(+), 95 deletions(-) diff --git a/mama/build_target.py b/mama/build_target.py index b1a7972..18b3c4c 100644 --- a/mama/build_target.py +++ b/mama/build_target.py @@ -37,11 +37,11 @@ class BuildTarget: Customization points: ``` class MyProject(mama.BuildTarget): - + workspace = 'build' def configure(self): - self.add_git('ReCpp', + self.add_git('ReCpp', 'http://github.com/RedFox20/ReCpp.git') def configure(self): @@ -128,9 +128,9 @@ def source_dir(self, subpath=''): """ Returns the current source directory. ``` - self.source_dir() + self.source_dir() # --> C:/Projects/ReCpp - self.source_dir('lib/ReCpp.lib') + self.source_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/lib/ReCpp.lib ``` """ @@ -142,7 +142,7 @@ def build_dir(self, subpath=''): """ Returns the current build directory. ``` - self.build_dir() + self.build_dir() # --> C:/Projects/ReCpp/build/windows self.build_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/build/windows/lib/ReCpp.lib @@ -200,12 +200,12 @@ def add_local(self, name, source_dir, mamafile=None, always_build=False, args=[] return self.dep.add_child(LocalSource(name, source_dir, mamafile, always_build, args)) - def add_git(self, name, git_url, git_branch='', git_tag='', git_commit='', mamafile=None, shallow=True, args=[]) -> BuildDependency: + def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shallow=True, args=[]) -> BuildDependency: """ Add a remote GIT dependency. The dependency will be cloned and updated according to mamabuild. Use `mama update` to force update the git repositories. - + If the remote GIT repository does not contain a `mamafile.py`, you will have to provide your own relative or absolute mamafile path. @@ -215,15 +215,13 @@ def add_git(self, name, git_url, git_branch='', git_tag='', git_commit='', mamaf ``` self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git') self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git', git_branch='master') - self.add_git('opencv', 'https://github.com/opencv/opencv.git', + self.add_git('opencv', 'https://github.com/opencv/opencv.git', git_branch='3.4', mamafile='mama/opencv_cfg.py') - self.add_git('mylib', 'https://github.com/user/mylib.git', - git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') ``` """ if self.dep.from_artifactory: # already loaded from artifactory? return self.get_dependency(name) - return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, git_commit, args)) + return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, args)) def add_artifactory_pkg(self, name, version='latest', fullname=None) -> BuildDependency: @@ -302,7 +300,7 @@ def inject_products(self, dst_dep, src_dep, include_path, libs, libfilters=None) Name of defines is given via `include_path` and `libs` params. `libfilters` does simple string matching; if nothing matches, the first export lib is chosen. ``` - self.inject_products('libpng', 'zlib', + self.inject_products('libpng', 'zlib', 'ZLIB_INCLUDE_DIR', 'ZLIB_LIBRARY', 'zlibstatic') ``` @@ -327,7 +325,7 @@ def get_product_defines(self): Returns a list of injected defines: ``` defines = self.get_product_defines() - # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', + # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', # 'ZLIB_LIBRARY=path/to/lib/zlib.a', ... ] ``` """ @@ -379,9 +377,9 @@ def add_build_dependency(self, all=None, windows=None, linux=None, macos=None, i Manually add a build dependency to prevent unnecessary rebuilds. @note Normally the build dependency is detected from the packaged libraries. - + if the dependency file does not exist, then the project will be rebuilt - + if your project has no build dependencies, it will always be rebuilt, so make sure to add_build_dependency or export_lib ``` @@ -426,7 +424,7 @@ def package(self): def export_include(self, include_path, build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient). - + Export include path relative to source directory OR if build_dir=True, then relative to build directory. ``` self.export_include('include') # MyRepo/include @@ -441,7 +439,7 @@ def export_include(self, include_path, build_dir=False): def export_includes(self, include_paths=[''], build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient) - + Export include paths relative to source directory OR if build_dir=True, then relative to build directory Example: @@ -456,7 +454,7 @@ def export_includes(self, include_paths=[''], build_dir=False): def export_lib(self, relative_path, src_dir=False, build_dir=True): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export a specific lib relative to build directory OR if src_dir=True, then relative to source directory Example: @@ -473,17 +471,17 @@ def export_lib(self, relative_path, src_dir=False, build_dir=True): def export_libs(self, path = '.', pattern_substrings = ['.lib', '.a'], src_dir=False, build_dir=True, order=None): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export several libs relative to build directory using EXTENSION MATCHING OR if src_dir=True, then relative to source directory - + Example: ``` self.export_libs() # gather any .lib or .a from build dir self.export_libs('.', ['.dll', '.so']) # gather any .dll or .so from build dir self.export_libs('lib', src_dir=True) # export everything from project/lib directory self.export_libs('external/lib') # gather specific static libs from build dir - + # export the libs in a particular order for Linux linker self.export_libs('lib', order=[ 'xphoto', 'calib3d', 'flann', 'core' @@ -502,7 +500,7 @@ def export_asset(self, asset, category=None, src_dir=True, build_dir=False): This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_asset('extras/csharp/NanoMesh.cs') @@ -523,12 +521,12 @@ def export_assets(self, assets_path: str, pattern_substrings = [], category=None This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_assets('extras/csharp', ['.cs']) --> {deploy}/extras/csharp/NanoMesh.cs - + self.export_assets('extras/csharp', ['.cs'], category='dotnet') --> {deploy}/dotnet/NanoMesh.cs ``` @@ -564,7 +562,7 @@ def inject_env(self): ``` def build(self): self.inject_env() # prepare platform - self.my_custom_build() # + self.my_custom_build() # ``` """ cmake.inject_env(self) @@ -615,7 +613,7 @@ def add_c_flags(self, *flags): for flag in flags: if isinstance(flag, list): self.add_c_flags(*flag) else: self._add_dict_flag(self.cmake_cflags, flag) - + def add_cl_flags(self, *flags): """ @@ -671,7 +669,7 @@ def add_platform_ld_flags(self, windows=None, linux=None, macos=None, ios=None, Adds linker flags depending on configuration platform. Supports many different usages: strings, list of strings, or space separate string. ``` - self.add_platform_ld_flags(windows='/LTCG', + self.add_platform_ld_flags(windows='/LTCG', ios=['-lobjc', '-rdynamic'], linux='-rdynamic -s') ``` @@ -804,7 +802,7 @@ def copy(self, src: str, dst: str, filter: list = None): Utility for copying files and folders ``` # copies built .so into an android archive - self.copy(self.build_dir('libAwesome.so'), + self.copy(self.build_dir('libAwesome.so'), self.source_dir('deploy/Awesome.aar/jni/armeabi-v7a')) ``` - filter: can be a string or list of strings to filter files by suffix @@ -860,7 +858,7 @@ def download_and_unzip(self, remote_zip: str, extract_dir: str, unless_file_exis unless_file_exists -- If the specified file exists, then download and unzip steps are skipped. ``` - self.download_and_unzip('http://example.com/archive.zip', + self.download_and_unzip('http://example.com/archive.zip', 'bin', 'bin/unzipped_file.txt') # --> 'bin/' on success # --> None on failure @@ -983,7 +981,7 @@ def run_program(self, working_dir: str, command: str, exit_on_fail=True, env=Non """ Run any program in any directory. Can be used for custom tools. ``` - self.run_program(self.source_dir('bin'), + self.run_program(self.source_dir('bin'), self.source_dir('bin/DbTool')) ``` """ @@ -1020,7 +1018,7 @@ def gtest(self, executable: str, args: str, src_dir=True, gdb=False): The gtest report is written to $src_dir/test/report.xml. Arguments - executable -- which executable to run - - args -- a string of options separated by spaces, + - args -- a string of options separated by spaces, 'gdb', 'nogdb' or gtest fixture/test partial name - src_dir -- [True] If true, then executable is relative to source directory. - gdb -- [False] If true, then run with gdb. @@ -1134,8 +1132,8 @@ def package(self): # custom export AGL as include from source folder self.export_includes(['AGL']) # custom export any .lib or .a from build folder - self.export_libs('.', ['.lib', '.a']) - + self.export_libs('.', ['.lib', '.a']) + if self.windows: self.export_syslib('opengl32.lib') @@ -1144,7 +1142,7 @@ def package(self): ``` """ pass - + def default_package(self): """ @@ -1236,7 +1234,7 @@ def papa_deploy(self, package_path, src_dir=False, MyPackageName/libawesome.so MyPackageName/include/... MyPackageName/someassets/extra.txt - + PAPA descriptor `papa.txt` format: P MyPackageName I include @@ -1282,7 +1280,7 @@ def start(self, args): """ pass - + ############################################ @@ -1503,3 +1501,4 @@ def build(self): ###################################################################################### + \ No newline at end of file diff --git a/mama/types/git.py b/mama/types/git.py index 6ba6692..e796726 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -17,14 +17,13 @@ class Git(DepSource): """ For BuildDependency whose source is from a Git repository """ - def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow:bool, commit:str, args:list): + def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow:bool, args:list): super(Git, self).__init__(name) if not url: raise RuntimeError("Git url must not be empty!") self.is_git = True self.url = url self.branch = branch self.tag = tag - self.commit_pin = commit self.mamafile = mamafile self.shallow = shallow self.args = args @@ -37,7 +36,6 @@ def __init__(self, name:str, url:str, branch:str, tag:str, mamafile:str, shallow self.tag_changed = False self.branch_changed = False self.commit_changed = False - self.commit_pin_changed = False def __repr__(self): return self.__str__() @@ -45,7 +43,6 @@ def __str__(self): s = f'DepSource Git {self.name} {self.url}' tag = self.branch_or_tag() if tag: s += ' ' + tag - if self.commit_pin: s += ' commit_pin=' + self.commit_pin if self.mamafile: s += ' ' + self.mamafile return s @@ -53,21 +50,14 @@ def __str__(self): def from_papa_string(s: str) -> "Git": p = s.split(',') name, url, branch, tag, mamafile = p[0:5] - commit_pin = '' args = p[5:] - - # New format - if len(p) > 5 and p[5]: - commit_pin = p[5] - args = p[6:] - shallow = True # shallow is the default - return Git(name, url, branch, tag, mamafile, shallow, commit_pin, args) + return Git(name, url, branch, tag, mamafile, shallow, args) def get_papa_string(self): fields = DepSource.papa_join( - self.name, self.url, self.branch, self.tag, self.mamafile, self.commit_pin, self.args) + self.name, self.url, self.branch, self.tag, self.mamafile, self.args) return 'git ' + fields @@ -107,12 +97,6 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: console(f' {self.name} using stored commit hash: {result}') return result - # explicit commit pin? - if self.commit_pin: - if dep.config.verbose: - console(f' {self.name} using pinned commit hash: {self.commit_pin}') - return self.commit_pin - # is the tag actually a commit hash? if self.tag and all(c in string.hexdigits for c in self.tag): if dep.config.verbose: @@ -144,20 +128,16 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: def fetch_origin(self, dep: BuildDependency): - self.run_git(dep, f"fetch origin {self.branch_or_tag()} -q") + self.run_git(dep, f"pull origin {self.branch_or_tag()} -q") def git_status_file(self, dep: BuildDependency): return path_join(dep.build_dir, 'git_status') - @staticmethod - def format_git_status(url, tag, branch, commit, commit_pin=''): - return f"{url}\n{tag}\n{branch}\n{commit}\n{commit_pin}\n" - def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = Git.format_git_status(self.url, self.tag, self.branch, commit, self.commit_pin) + status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n" if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') @@ -170,8 +150,7 @@ def read_stored_status(self, dep: BuildDependency): tag = lines[1].rstrip() branch = lines[2].rstrip() commit = lines[3].rstrip() - commit_pin = '' if len(lines) <= 4 else lines[4].rstrip() - return (url, tag, branch, commit, commit_pin) + return (url, tag, branch, commit) def reset_status(self, dep: BuildDependency): @@ -192,17 +171,14 @@ def check_status(self, dep: BuildDependency): self.tag_changed = True self.branch_changed = True self.commit_changed = True - self.commit_pin_changed = True return True - if not self.commit_pin: - self.fetch_origin(dep) + self.fetch_origin(dep) self.url_changed = self.url != status[0] self.tag_changed = self.tag != status[1] self.branch_changed = self.branch != status[2] - self.commit_pin_changed = self.commit_pin != status[4] self.commit_changed = self.get_commit_hash(dep, use_cache=False) != status[3] #console(f'check_status {self.url} {self.branch_or_tag()}: urlc={self.url_changed} tagc={self.tag_changed} brnc={self.branch_changed} cmtc={self.commit_changed}') - return self.url_changed or self.tag_changed or self.branch_changed or self.commit_changed or self.commit_pin_changed + return self.url_changed or self.tag_changed or self.branch_changed or self.commit_changed def branch_or_tag(self): @@ -211,18 +187,12 @@ def branch_or_tag(self): return '' - def checkout_current_branch_or_commit(self, dep: BuildDependency): - if self.commit_pin: - self.run_git(dep, f'fetch --depth 1 origin {self.commit_pin}') - self.run_git(dep, f'checkout {self.commit_pin}') - else: - branch = self.branch_or_tag() - if branch: - if self.tag and self.tag_changed: - self.run_git(dep, "reset --hard") - if self.tag: - self.run_git(dep, f"fetch origin tag {self.tag}") - self.run_git(dep, f"checkout {branch}") + def checkout_current_branch(self, dep: BuildDependency): + branch = self.branch_or_tag() + if branch: + if self.tag and self.tag_changed: + self.run_git(dep, "reset --hard") + self.run_git(dep, f"checkout {branch}") def reclone_wipe(self, dep: BuildDependency): @@ -300,28 +270,20 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): if is_dir_empty(dep.src_dir): if not wiped and dep.config.print: console(f" - Target {dep.name: <16} CLONE because src is missing", color=Color.BLUE) - if self.commit_pin: - # pinned commits always use shallow clone - clone_args = f"--no-checkout --depth 1 {self.url}" - else: - branch = self.branch_or_tag() - if branch: branch = f" --branch {self.branch_or_tag()}" - depth = '' if unshallow else '--depth 1' - clone_args = f"--recurse-submodules {depth} {branch} {self.url}" - + branch = self.branch_or_tag() + if branch: branch = f" --branch {self.branch_or_tag()}" + depth = '' if unshallow else '--depth 1' + clone_args = f"--recurse-submodules {depth} {branch} {self.url}" self.clone_with_filtered_progress(dep, clone_args, dep.src_dir) - self.checkout_current_branch_or_commit(dep) - - if self.commit_pin: - self.run_git(dep, 'submodule update --init --recursive') + self.checkout_current_branch(dep) else: if dep.config.print: console(f" - Pulling {dep.name: <16} SCM change detected", color=Color.BLUE) if unshallow: self.unshallow(dep) - self.checkout_current_branch_or_commit(dep) + self.checkout_current_branch(dep) self.run_git(dep, 'submodule update --init --recursive') - if not self.tag and not self.commit_pin: # pull if not a tag + if not self.tag: # pull if not a tag self.run_git(dep, "reset --hard -q") self.run_git(dep, "pull") From bb9541ed8cd8bb88caba7823044f9d543b1439f5 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 13:57:16 +0200 Subject: [PATCH 16/20] fix tests after merge --- mama/types/git.py | 5 ++- tests/test_git_pin_change/mamafile.py | 4 +-- .../test_git_pin_change.py | 2 +- tests/test_git_pinning/mamafile.py | 6 ++-- tests/test_papa_deploy/mamafile.py | 2 +- tests/test_papa_parse/papa.txt | 2 +- tests/test_papa_parse/papa_old.txt | 5 --- tests/test_papa_parse/test_papa_parse.py | 31 +------------------ 8 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 tests/test_papa_parse/papa_old.txt diff --git a/mama/types/git.py b/mama/types/git.py index 14e1a4d..01bbf5c 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -137,10 +137,13 @@ def fetch_origin(self, dep: BuildDependency): def git_status_file(self, dep: BuildDependency): return path_join(dep.build_dir, 'git_status') + @staticmethod + def format_git_status(url: str, tag: str, branch: str, commit: str): + return f"{url}\n{tag}\n{branch}\n{commit}\n" def save_status(self, dep: BuildDependency): commit = self.get_commit_hash(dep) - status = f"{self.url}\n{self.tag}\n{self.branch}\n{commit}\n" + status = self.format_git_status(self.url, self.tag, self.branch, commit) if save_file_if_contents_changed(self.git_status_file(dep), status): if dep.config.verbose: console(f' {self.name} write git status commit={commit}') diff --git a/tests/test_git_pin_change/mamafile.py b/tests/test_git_pin_change/mamafile.py index 2705810..3a01cbf 100644 --- a/tests/test_git_pin_change/mamafile.py +++ b/tests/test_git_pin_change/mamafile.py @@ -15,9 +15,9 @@ def dependencies(self): # Switches between having REMOTE_VERSION and not to demonstrate that the contents actually change if stage == '0': - self.add_git(remote_name, remote_url, git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION + self.add_git(remote_name, remote_url, git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION if stage == '1': - self.add_git(remote_name, remote_url, git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 + self.add_git(remote_name, remote_url, git_tag='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 elif stage == '2': self.add_git(remote_name, remote_url, git_tag='v1.0.0') # has no REMOTE_VERSION elif stage == '3': diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py index 247c107..83b01eb 100644 --- a/tests/test_git_pin_change/test_git_pin_change.py +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -12,7 +12,7 @@ def stage(num: int, expects: bool, assert_message: str = ""): else: assert not result, assert_message -# Test that switches between having REMOTE_VERSION and not to demonstrate that the contents actually change when changing git_commit or git_tag pins +# Test that switches between having REMOTE_VERSION and not to demonstrate that the contents actually change when changing git_tag pins def test_git_pin_change(): init(__file__, clean_dirs=['packages']) diff --git a/tests/test_git_pinning/mamafile.py b/tests/test_git_pinning/mamafile.py index b4f462a..43ae688 100644 --- a/tests/test_git_pinning/mamafile.py +++ b/tests/test_git_pinning/mamafile.py @@ -5,9 +5,9 @@ class test(mama.BuildTarget): def build(self): self.nothing_to_build() - + def dependencies(self): self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v1.0.0') self.add_git('ExampleRemote2', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='v2.0.0') - self.add_git('ExampleRemote3', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') - self.add_git('ExampleRemote4', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') \ No newline at end of file + self.add_git('ExampleRemote3', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') + self.add_git('ExampleRemote4', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='993e326cf840bc2df9d67b14d6e2fe0d38736713') \ No newline at end of file diff --git a/tests/test_papa_deploy/mamafile.py b/tests/test_papa_deploy/mamafile.py index 1f5564e..913956b 100644 --- a/tests/test_papa_deploy/mamafile.py +++ b/tests/test_papa_deploy/mamafile.py @@ -4,4 +4,4 @@ class ExampleConsumer(mama.BuildTarget): workspace = 'packages' def dependencies(self): - self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') + self.add_git('ExampleRemote', 'https://github.com/BatteredBunny/MamaExampleRemote.git', git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') diff --git a/tests/test_papa_parse/papa.txt b/tests/test_papa_parse/papa.txt index d473ccc..3596fbb 100644 --- a/tests/test_papa_parse/papa.txt +++ b/tests/test_papa_parse/papa.txt @@ -1,5 +1,5 @@ P ExampleConsumer -D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,,4acd9052f27a459314651dd485ae8fa79a04d49d, +D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,, I include I include/test_papa_deploy L RelWithDebInfo\ExampleConsumer.lib \ No newline at end of file diff --git a/tests/test_papa_parse/papa_old.txt b/tests/test_papa_parse/papa_old.txt deleted file mode 100644 index 3596fbb..0000000 --- a/tests/test_papa_parse/papa_old.txt +++ /dev/null @@ -1,5 +0,0 @@ -P ExampleConsumer -D git ExampleRemote,https://github.com/BatteredBunny/MamaExampleRemote.git,,,, -I include -I include/test_papa_deploy -L RelWithDebInfo\ExampleConsumer.lib \ No newline at end of file diff --git a/tests/test_papa_parse/test_papa_parse.py b/tests/test_papa_parse/test_papa_parse.py index 82a2195..a72319d 100644 --- a/tests/test_papa_parse/test_papa_parse.py +++ b/tests/test_papa_parse/test_papa_parse.py @@ -1,7 +1,7 @@ from testutils import init from mama.papa_deploy import PapaFileInfo -# Tests new papa file format parsing +# Test papa file format parsing def test_papa_parse(): init(__file__) @@ -17,35 +17,6 @@ def test_papa_parse(): assert dep.branch == '' assert dep.tag == '' assert dep.mamafile == '' - assert dep.commit_pin == '4acd9052f27a459314651dd485ae8fa79a04d49d' - - assert len(papa.includes) == 2 - assert papa.includes[0].endswith('include') - assert papa.includes[1].endswith('include/test_papa_deploy') - - assert len(papa.libs) == 1 - assert papa.libs[0].endswith('RelWithDebInfo/ExampleConsumer.lib') - - assert len(papa.syslibs) == 0 - assert len(papa.assets) == 0 - -# Test old papa file format parsing -def test_papa_parse_old(): - init(__file__) - - papa = PapaFileInfo('papa_old.txt') - - assert papa.project_name == 'ExampleConsumer' - - assert len(papa.dependencies) == 1 - dep = papa.dependencies[0] - assert dep.is_git - assert dep.name == 'ExampleRemote' - assert dep.url == 'https://github.com/BatteredBunny/MamaExampleRemote.git' - assert dep.branch == '' - assert dep.tag == '' - assert dep.mamafile == '' - assert dep.commit_pin == '' assert len(papa.includes) == 2 assert papa.includes[0].endswith('include') From 6f003793289e59ec24e347adc8667be5414fad75 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 14:11:29 +0200 Subject: [PATCH 17/20] improve git pin change test --- tests/test_git_pin_change/mamafile.py | 6 +++++- tests/test_git_pin_change/test_git_pin_change.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_git_pin_change/mamafile.py b/tests/test_git_pin_change/mamafile.py index 3a01cbf..46f0a4e 100644 --- a/tests/test_git_pin_change/mamafile.py +++ b/tests/test_git_pin_change/mamafile.py @@ -21,4 +21,8 @@ def dependencies(self): elif stage == '2': self.add_git(remote_name, remote_url, git_tag='v1.0.0') # has no REMOTE_VERSION elif stage == '3': - self.add_git(remote_name, remote_url, git_tag='v2.0.0') # has REMOTE_VERSION 2 \ No newline at end of file + self.add_git(remote_name, remote_url, git_tag='v2.0.0') # has REMOTE_VERSION 2 + elif stage == '4': + self.add_git(remote_name, remote_url, git_branch='old') # Branched from old commit which has no REMOTE_VERSION + elif stage == '5': + self.add_git(remote_name, remote_url, git_branch='master') # Should go to latest commit which has REMOTE_VERSION 2 \ No newline at end of file diff --git a/tests/test_git_pin_change/test_git_pin_change.py b/tests/test_git_pin_change/test_git_pin_change.py index 83b01eb..08b3644 100644 --- a/tests/test_git_pin_change/test_git_pin_change.py +++ b/tests/test_git_pin_change/test_git_pin_change.py @@ -20,3 +20,5 @@ def test_git_pin_change(): stage(1, True, "Failed to update commit pin to a new commit") stage(2, False, "Failed to switch from commit pin to tag pin") stage(3, True, "Failed to update between tag pins") + stage(4, False, "Failed to switch from tag pin to branch pin") + stage(5, True, "Failed to update between branch pins") From 534fb6b20d00ea949371845aacd45013dd9b63bc Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 14:25:50 +0200 Subject: [PATCH 18/20] hacky fix for switching between tag pin and branch --- mama/types/git.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/mama/types/git.py b/mama/types/git.py index 01bbf5c..81fcb7c 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -131,7 +131,15 @@ def init_commit_hash(self, dep: BuildDependency, use_cache: bool, fetch_remote: def fetch_origin(self, dep: BuildDependency): - self.run_git(dep, f"pull origin {self.branch_or_tag()} -q") + branch = self.branch_or_tag() + if not Git.is_hex_string(branch): + if self.tag: + self.run_git(dep, f"fetch origin tag {branch} -q") + else: + # detached head cant perform this, e.g when switching from tag pin previously + result = self.run_git(dep, f"pull origin {branch} -q", throw=False) + if result != 0: + self.run_git(dep, f"fetch origin {branch} -q") def git_status_file(self, dep: BuildDependency): @@ -200,7 +208,15 @@ def checkout_current_branch_or_tag(self, dep: BuildDependency, is_commit_pin=Fal self.run_git(dep, "reset --hard") if is_commit_pin: self.run_git(dep, f"fetch --depth 1 origin {branch}") - self.run_git(dep, f"checkout {branch}") + self.run_git(dep, f"checkout {branch}") + elif self.branch: + # hacky fix + self.run_git(dep, f"fetch origin +refs/heads/{branch}:refs/remotes/origin/{branch}", throw=False) + self.run_git(dep, f"checkout -B {branch} origin/{branch}") + else: # tag + if self.tag_changed: + self.run_git(dep, f"fetch origin tag {branch}") + self.run_git(dep, f"checkout {branch}") def reclone_wipe(self, dep: BuildDependency): @@ -295,7 +311,11 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): self.run_git(dep, 'submodule update --init --recursive') if not self.tag: # pull if not a tag self.run_git(dep, "reset --hard -q") - self.run_git(dep, "pull") + if self.branch: + self.run_git(dep, f"fetch origin {self.branch} -q", throw=False) + self.run_git(dep, f"reset --hard origin/{self.branch} -q") + else: + self.run_git(dep, "pull") def unshallow(self, dep: BuildDependency): From 4d1292e76abd7692df432b91a7716b33bd60790d Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Thu, 12 Feb 2026 14:29:57 +0200 Subject: [PATCH 19/20] clean up testutils --- tests/testutils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testutils.py b/tests/testutils.py index 15d615b..6e8327e 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,8 +1,11 @@ import os import shutil +import subprocess import sys from typing import Iterable +import pytest + def init(caller_file: str = '', clean_dirs: Iterable[str] = []): # Needed for mama commands to perform work in the correct directory if caller_file: @@ -13,13 +16,10 @@ def init(caller_file: str = '', clean_dirs: Iterable[str] = []): def shell_exec(cmd: str, exit_on_fail: bool = True, echo: bool = True) -> int: if echo: print(f'exec: {cmd}') - result = os.system(cmd) - if result != 0 and exit_on_fail: - print(f'exec failed: code: {result} {cmd}') - if result >= 255: - result = 1 - sys.exit(result) - return result + result = subprocess.run(cmd, shell=True) + if result.returncode != 0 and exit_on_fail: + pytest.fail(f'exec failed: code: {result.returncode} {cmd}') + return result.returncode def file_contains(filepath: str, text: str) -> bool: with open(filepath, 'r') as f: From c63d8bb351f291a025ddb1a0a77edc62c3942598 Mon Sep 17 00:00:00 2001 From: BatteredBunny Date: Mon, 16 Feb 2026 13:01:18 +0200 Subject: [PATCH 20/20] review changes --- README.md | 5 +- mama/build_target.py | 71 ++++++++++++++------------- mama/types/git.py | 4 +- mama/util.py | 3 +- tests/test_git_pin_change/mamafile.py | 6 +-- tests/testutils.py | 7 ++- 6 files changed, 51 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 3eff85c..6473e76 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,8 @@ Install pytest and run all tests from the project root: ```bash uv venv -.\.venv\Scripts\activate -pip install pytest -pytest +uv pip install pytest +uv run pytest ``` Or to run a specific test: diff --git a/mama/build_target.py b/mama/build_target.py index 070657d..400e65d 100644 --- a/mama/build_target.py +++ b/mama/build_target.py @@ -37,11 +37,11 @@ class BuildTarget: Customization points: ``` class MyProject(mama.BuildTarget): - + workspace = 'build' def configure(self): - self.add_git('ReCpp', + self.add_git('ReCpp', 'http://github.com/RedFox20/ReCpp.git') def configure(self): @@ -129,9 +129,9 @@ def source_dir(self, subpath=''): """ Returns the current source directory. ``` - self.source_dir() + self.source_dir() # --> C:/Projects/ReCpp - self.source_dir('lib/ReCpp.lib') + self.source_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/lib/ReCpp.lib ``` """ @@ -143,7 +143,7 @@ def build_dir(self, subpath=''): """ Returns the current build directory. ``` - self.build_dir() + self.build_dir() # --> C:/Projects/ReCpp/build/windows self.build_dir('lib/ReCpp.lib') # --> C:/Projects/ReCpp/build/windows/lib/ReCpp.lib @@ -201,12 +201,12 @@ def add_local(self, name, source_dir, mamafile=None, always_build=False, args=[] return self.dep.add_child(LocalSource(name, source_dir, mamafile, always_build, args)) - def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shallow=True, args=[]) -> BuildDependency: + def add_git(self, name, git_url, git_branch='', git_tag='', git_commit='', mamafile=None, shallow=True, args=[]) -> BuildDependency: """ Add a remote GIT dependency. The dependency will be cloned and updated according to mamabuild. Use `mama update` to force update the git repositories. - + If the remote GIT repository does not contain a `mamafile.py`, you will have to provide your own relative or absolute mamafile path. @@ -216,12 +216,16 @@ def add_git(self, name, git_url, git_branch='', git_tag='', mamafile=None, shall ``` self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git') self.add_git('ReCpp', 'git@github.com:RedFox20/ReCpp.git', git_branch='master') - self.add_git('opencv', 'https://github.com/opencv/opencv.git', + self.add_git('opencv', 'https://github.com/opencv/opencv.git', git_branch='3.4', mamafile='mama/opencv_cfg.py') ``` """ if self.dep.from_artifactory: # already loaded from artifactory? return self.get_dependency(name) + + if git_tag == '' and git_commit != '': + git_tag = git_commit + return self.dep.add_child(Git(name, git_url, git_branch, git_tag, mamafile, shallow, args)) @@ -301,7 +305,7 @@ def inject_products(self, dst_dep, src_dep, include_path, libs, libfilters=None) Name of defines is given via `include_path` and `libs` params. `libfilters` does simple string matching; if nothing matches, the first export lib is chosen. ``` - self.inject_products('libpng', 'zlib', + self.inject_products('libpng', 'zlib', 'ZLIB_INCLUDE_DIR', 'ZLIB_LIBRARY', 'zlibstatic') ``` @@ -326,7 +330,7 @@ def get_product_defines(self): Returns a list of injected defines: ``` defines = self.get_product_defines() - # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', + # --> [ 'ZLIB_INCLUDE_DIR=path/to/zlib/include', # 'ZLIB_LIBRARY=path/to/lib/zlib.a', ... ] ``` """ @@ -378,9 +382,9 @@ def add_build_dependency(self, all=None, windows=None, linux=None, macos=None, i Manually add a build dependency to prevent unnecessary rebuilds. @note Normally the build dependency is detected from the packaged libraries. - + if the dependency file does not exist, then the project will be rebuilt - + if your project has no build dependencies, it will always be rebuilt, so make sure to add_build_dependency or export_lib ``` @@ -425,7 +429,7 @@ def package(self): def export_include(self, include_path, build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient). - + Export include path relative to source directory OR if build_dir=True, then relative to build directory. ``` self.export_include('include') # MyRepo/include @@ -440,7 +444,7 @@ def export_include(self, include_path, build_dir=False): def export_includes(self, include_paths=[''], build_dir=False): """ CUSTOM PACKAGE INCLUDES (if self.default_package() is insufficient) - + Export include paths relative to source directory OR if build_dir=True, then relative to build directory Example: @@ -455,7 +459,7 @@ def export_includes(self, include_paths=[''], build_dir=False): def export_lib(self, relative_path, src_dir=False, build_dir=True): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export a specific lib relative to build directory OR if src_dir=True, then relative to source directory Example: @@ -472,17 +476,17 @@ def export_lib(self, relative_path, src_dir=False, build_dir=True): def export_libs(self, path = '.', pattern_substrings = ['.lib', '.a'], src_dir=False, build_dir=True, order=None): """ CUSTOM PACKAGE LIBS (if self.default_package() is insufficient) - + Export several libs relative to build directory using EXTENSION MATCHING OR if src_dir=True, then relative to source directory - + Example: ``` self.export_libs() # gather any .lib or .a from build dir self.export_libs('.', ['.dll', '.so']) # gather any .dll or .so from build dir self.export_libs('lib', src_dir=True) # export everything from project/lib directory self.export_libs('external/lib') # gather specific static libs from build dir - + # export the libs in a particular order for Linux linker self.export_libs('lib', order=[ 'xphoto', 'calib3d', 'flann', 'core' @@ -501,7 +505,7 @@ def export_asset(self, asset, category=None, src_dir=True, build_dir=False): This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_asset('extras/csharp/NanoMesh.cs') @@ -522,12 +526,12 @@ def export_assets(self, assets_path: str, pattern_substrings = [], category=None This can be later used when creating a deployment category -- (optional) Can be used for grouping the assets and flattening folder structure - + Example: ``` self.export_assets('extras/csharp', ['.cs']) --> {deploy}/extras/csharp/NanoMesh.cs - + self.export_assets('extras/csharp', ['.cs'], category='dotnet') --> {deploy}/dotnet/NanoMesh.cs ``` @@ -563,7 +567,7 @@ def inject_env(self): ``` def build(self): self.inject_env() # prepare platform - self.my_custom_build() # + self.my_custom_build() # ``` """ cmake.inject_env(self) @@ -614,7 +618,7 @@ def add_c_flags(self, *flags): for flag in flags: if isinstance(flag, list): self.add_c_flags(*flag) else: self._add_dict_flag(self.cmake_cflags, flag) - + def add_cl_flags(self, *flags): """ @@ -670,7 +674,7 @@ def add_platform_ld_flags(self, windows=None, linux=None, macos=None, ios=None, Adds linker flags depending on configuration platform. Supports many different usages: strings, list of strings, or space separate string. ``` - self.add_platform_ld_flags(windows='/LTCG', + self.add_platform_ld_flags(windows='/LTCG', ios=['-lobjc', '-rdynamic'], linux='-rdynamic -s') ``` @@ -803,7 +807,7 @@ def copy(self, src: str, dst: str, filter: list = None): Utility for copying files and folders ``` # copies built .so into an android archive - self.copy(self.build_dir('libAwesome.so'), + self.copy(self.build_dir('libAwesome.so'), self.source_dir('deploy/Awesome.aar/jni/armeabi-v7a')) ``` - filter: can be a string or list of strings to filter files by suffix @@ -859,7 +863,7 @@ def download_and_unzip(self, remote_zip: str, extract_dir: str, unless_file_exis unless_file_exists -- If the specified file exists, then download and unzip steps are skipped. ``` - self.download_and_unzip('http://example.com/archive.zip', + self.download_and_unzip('http://example.com/archive.zip', 'bin', 'bin/unzipped_file.txt') # --> 'bin/' on success # --> None on failure @@ -982,7 +986,7 @@ def run_program(self, working_dir: str, command: str, exit_on_fail=True, env=Non """ Run any program in any directory. Can be used for custom tools. ``` - self.run_program(self.source_dir('bin'), + self.run_program(self.source_dir('bin'), self.source_dir('bin/DbTool')) ``` """ @@ -1019,7 +1023,7 @@ def gtest(self, executable: str, args: str, src_dir=True, gdb=False): The gtest report is written to $src_dir/test/report.xml. Arguments - executable -- which executable to run - - args -- a string of options separated by spaces, + - args -- a string of options separated by spaces, 'gdb', 'nogdb' or gtest fixture/test partial name - src_dir -- [True] If true, then executable is relative to source directory. - gdb -- [False] If true, then run with gdb. @@ -1133,8 +1137,8 @@ def package(self): # custom export AGL as include from source folder self.export_includes(['AGL']) # custom export any .lib or .a from build folder - self.export_libs('.', ['.lib', '.a']) - + self.export_libs('.', ['.lib', '.a']) + if self.windows: self.export_syslib('opengl32.lib') @@ -1143,7 +1147,7 @@ def package(self): ``` """ pass - + def default_package(self): """ @@ -1235,7 +1239,7 @@ def papa_deploy(self, package_path, src_dir=False, MyPackageName/libawesome.so MyPackageName/include/... MyPackageName/someassets/extra.txt - + PAPA descriptor `papa.txt` format: P MyPackageName I include @@ -1281,7 +1285,7 @@ def start(self, args): """ pass - + ############################################ @@ -1502,4 +1506,3 @@ def build(self): ###################################################################################### - \ No newline at end of file diff --git a/mama/types/git.py b/mama/types/git.py index 81fcb7c..11d1f50 100644 --- a/mama/types/git.py +++ b/mama/types/git.py @@ -310,12 +310,12 @@ def clone_or_pull(self, dep: BuildDependency, wiped=False): self.checkout_current_branch_or_tag(dep, is_commit_pin=is_commit_pin) self.run_git(dep, 'submodule update --init --recursive') if not self.tag: # pull if not a tag - self.run_git(dep, "reset --hard -q") if self.branch: self.run_git(dep, f"fetch origin {self.branch} -q", throw=False) self.run_git(dep, f"reset --hard origin/{self.branch} -q") else: - self.run_git(dep, "pull") + self.run_git(dep, "fetch -q", throw=False) + self.run_git(dep, "reset --hard @{upstream} -q") # https://git-scm.com/docs/gitrevisions#Documentation/gitrevisions.txt-branchnameupstreamegmasterupstreamu def unshallow(self, dep: BuildDependency): diff --git a/mama/util.py b/mama/util.py index d414359..24d4d64 100644 --- a/mama/util.py +++ b/mama/util.py @@ -42,7 +42,7 @@ def copy_files(fromFolder: str, toFolder: str, fileNames: List[str]): def deploy_framework(framework: str, deployFolder: str): if not os.path.exists(framework): - raise IOError(f'no framework found at: {framework}') + raise IOError(f'no framework found at: {framework}') if os.path.exists(deployFolder): name = os.path.basename(framework) deployPath = os.path.join(deployFolder, name) @@ -438,6 +438,7 @@ def copy_dir(src_dir: str, out_dir: str, filter: list = None) -> bool: for fulldir, dirs, files in os.walk(src_dir): # skip the output directory to prevent infinite recursion dirs[:] = [d for d in dirs if os.path.normcase(os.path.normpath(os.path.join(fulldir, d))) != norm_out] + reldir = fulldir[len(root):].lstrip('\\/') if reldir: dst_folder = os.path.join(out_dir, reldir) diff --git a/tests/test_git_pin_change/mamafile.py b/tests/test_git_pin_change/mamafile.py index 46f0a4e..4e43c0a 100644 --- a/tests/test_git_pin_change/mamafile.py +++ b/tests/test_git_pin_change/mamafile.py @@ -15,9 +15,9 @@ def dependencies(self): # Switches between having REMOTE_VERSION and not to demonstrate that the contents actually change if stage == '0': - self.add_git(remote_name, remote_url, git_tag='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION - if stage == '1': - self.add_git(remote_name, remote_url, git_tag='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 + self.add_git(remote_name, remote_url, git_commit='4acd9052f27a459314651dd485ae8fa79a04d49d') # has no REMOTE_VERSION + elif stage == '1': + self.add_git(remote_name, remote_url, git_commit='993e326cf840bc2df9d67b14d6e2fe0d38736713') # has REMOTE_VERSION 2 elif stage == '2': self.add_git(remote_name, remote_url, git_tag='v1.0.0') # has no REMOTE_VERSION elif stage == '3': diff --git a/tests/testutils.py b/tests/testutils.py index 6e8327e..d52e88e 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -2,15 +2,18 @@ import shutil import subprocess import sys -from typing import Iterable +from typing import Iterable, Optional import pytest -def init(caller_file: str = '', clean_dirs: Iterable[str] = []): +def init(caller_file: str = '', clean_dirs: Optional[Iterable[str]] = None): # Needed for mama commands to perform work in the correct directory if caller_file: os.chdir(os.path.dirname(os.path.abspath(caller_file))) + if clean_dirs is None: + clean_dirs = () + for d in clean_dirs: rmdir(d)