Skip to content

Commit 5e4e966

Browse files
committed
Add toolchain files and beman-submodule
This pull request adds a new script, beman-submodule, which provides some of the features of git submodule but without git submodule's disadvantage that users need to be aware of it to clone the repository. More information can be found in tools/beman-submodule/README.md. It also pulls in the toolchain files from exemplar, preserving history, using a git filter-branch.
1 parent da8cfe5 commit 5e4e966

File tree

4 files changed

+664
-0
lines changed

4 files changed

+664
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2+
3+
name: beman-submodule tests
4+
5+
on:
6+
push:
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
beman-submodule-script-ci:
12+
name: beman_module.py ci
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: 3.13
22+
23+
- name: Install pytest
24+
run: |
25+
python3 -m pip install pytest
26+
27+
- name: Run pytest
28+
run: |
29+
cd tools/beman-submodule/
30+
pytest

tools/beman-submodule/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# beman-submodule
2+
3+
<!-- SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -->
4+
5+
## What is this script?
6+
7+
`beman-submodule` provides some of the features of `git submodule`, adding child git
8+
repositories to a parent git repository, but unlike with `git submodule`, the entire child
9+
repo is directly checked in, so only maintainers, not users, need to run this script. The
10+
command line interface mimics `git submodule`'s.
11+
12+
## How do I add a beman submodule to my repository?
13+
14+
The first beman submodule you should add is this repository, `infra/`, which you can
15+
bootstrap by running:
16+
17+
<!-- markdownlint-disable MD013 -->
18+
```sh
19+
curl -s https://raw.githubusercontent.com/bemanproject/infra/refs/heads/main/tools/beman-submodule/beman-submodule | python3 - add https://github.com/bemanproject/infra.git
20+
```
21+
22+
Once that's added, you can run the script from `infra/tools/beman-submodule/beman-submodule`.
23+
24+
## How do I update a beman submodule to the latest trunk?
25+
26+
You can run `beman-submodule update --remote` to update all beman submodule to latest
27+
trunk, or e.g. `beman-submodule update --remote infra` to update only a specific one.
28+
29+
## How does it work under the hood?
30+
31+
Along with the files from the child repository, it creates a dotfile called
32+
`.beman_submodule`, which looks like this:
33+
34+
```ini
35+
[beman_submodule]
36+
remote=https://github.com/bemanproject/infra.git
37+
commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77
38+
```
39+
40+
## How do I update a beman submodule to a specific commit or change the remote URL?
41+
42+
You can edit the corresponding lines in the `.beman_submodule` file and run
43+
`beman-submodule update` to update the state of the beman submodule to the new
44+
`.beman_submodule` settings.
45+
46+
## How can I make CI ensure that my beman submodules are in a valid state?
47+
48+
Add this job to your CI workflow:
49+
50+
```yaml
51+
beman-submodule-test:
52+
runs-on: ubuntu-latest
53+
name: "Check beman submodules for consistency"
54+
steps:
55+
- name: Checkout
56+
uses: actions/checkout@v4
57+
- name: beman submodule consistency check
58+
run: |
59+
(set -o pipefail; ./infra/tools/beman-submodule/beman-submodule status | grep -qvF '+')
60+
```
61+
62+
This will fail if the contents of any beman submodule don't match what's specified in the
63+
`.beman_submodule` file.
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
import argparse
6+
import configparser
7+
import filecmp
8+
import os
9+
import pathlib
10+
import shutil
11+
import subprocess
12+
import sys
13+
import tempfile
14+
15+
def directory_compare(dir1, dir2, ignore):
16+
compared = filecmp.dircmp(dir1, dir2, ignore=ignore)
17+
if compared.left_only or compared.right_only or compared.diff_files:
18+
return False
19+
for common_dir in compared.common_dirs:
20+
path1 = os.path.join(dir1, common_dir)
21+
path2 = os.path.join(dir2, common_dir)
22+
if not directory_compare(path1, path2, ignore):
23+
return False
24+
return True
25+
26+
class BemanSubmodule:
27+
def __init__(self, dirpath, remote, commit_hash):
28+
self.dirpath = dirpath
29+
self.remote = remote
30+
self.commit_hash = commit_hash
31+
32+
def parse_beman_submodule_file(path):
33+
config = configparser.ConfigParser()
34+
read_result = config.read(path)
35+
def fail():
36+
raise Exception(f'Failed to parse {path} as a .beman_submodule file')
37+
if not read_result:
38+
fail()
39+
if not 'beman_submodule' in config:
40+
fail()
41+
if not 'remote' in config['beman_submodule']:
42+
fail()
43+
if not 'commit_hash' in config['beman_submodule']:
44+
fail()
45+
return BemanSubmodule(
46+
str(pathlib.Path(path).resolve().parent),
47+
config['beman_submodule']['remote'], config['beman_submodule']['commit_hash'])
48+
49+
def get_beman_submodule(dir):
50+
beman_submodule_filepath = os.path.join(dir, '.beman_submodule')
51+
if os.path.isfile(beman_submodule_filepath):
52+
return parse_beman_submodule_file(beman_submodule_filepath)
53+
else:
54+
return None
55+
56+
def find_beman_submodules_in(dir):
57+
assert os.path.isdir(dir)
58+
result = []
59+
for dirpath, _, filenames in os.walk(dir):
60+
if '.beman_submodule' in filenames:
61+
result.append(parse_beman_submodule_file(os.path.join(dirpath, '.beman_submodule')))
62+
return sorted(result, key=lambda module: module.dirpath)
63+
64+
def cwd_git_repository_path():
65+
process = subprocess.run(
66+
['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True,
67+
check=False)
68+
if process.returncode == 0:
69+
return process.stdout.strip()
70+
elif "fatal: not a git repository" in process.stderr:
71+
return None
72+
else:
73+
raise Exception("git rev-parse --show-toplevel failed")
74+
75+
def clone_beman_submodule_into_tmpdir(beman_submodule, remote):
76+
tmpdir = tempfile.TemporaryDirectory()
77+
subprocess.run(
78+
['git', 'clone', beman_submodule.remote, tmpdir.name], capture_output=True,
79+
check=True)
80+
if not remote:
81+
subprocess.run(
82+
['git', '-C', tmpdir.name, 'reset', '--hard', beman_submodule.commit_hash],
83+
capture_output=True, check=True)
84+
return tmpdir
85+
86+
def beman_submodule_status(beman_submodule):
87+
tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False)
88+
if directory_compare(tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git']):
89+
status_character=' '
90+
else:
91+
status_character='+'
92+
parent_repo_path = cwd_git_repository_path()
93+
if not parent_repo_path:
94+
raise Exception('this is not a git repository')
95+
relpath = pathlib.Path(
96+
beman_submodule.dirpath).relative_to(pathlib.Path(parent_repo_path))
97+
return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath)
98+
99+
def beman_submodule_update(beman_submodule, remote):
100+
tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote)
101+
shutil.rmtree(beman_submodule.dirpath)
102+
with open(os.path.join(tmpdir.name, '.beman_submodule'), 'w') as f:
103+
f.write('[beman_submodule]\n')
104+
f.write(f'remote={beman_submodule.remote}\n')
105+
f.write(f'commit_hash={beman_submodule.commit_hash}\n')
106+
shutil.rmtree(os.path.join(tmpdir.name, '.git'))
107+
shutil.copytree(tmpdir.name, beman_submodule.dirpath)
108+
109+
def update_command(remote, path):
110+
if not path:
111+
parent_repo_path = cwd_git_repository_path()
112+
if not parent_repo_path:
113+
raise Exception('this is not a git repository')
114+
beman_submodules = find_beman_submodules_in(parent_repo_path)
115+
else:
116+
beman_submodule = get_beman_submodule(path)
117+
if not beman_submodule:
118+
raise Exception(f'{path} is not a beman_submodule')
119+
beman_submodules = [beman_submodule]
120+
for beman_submodule in beman_submodules:
121+
beman_submodule_update(beman_submodule, remote)
122+
123+
def add_command(repository, path):
124+
tmpdir = tempfile.TemporaryDirectory()
125+
subprocess.run(
126+
['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name)
127+
repository_name = os.listdir(tmpdir.name)[0]
128+
if not path:
129+
path = repository_name
130+
if os.path.exists(path):
131+
raise Exception(f'{path} exists')
132+
os.makedirs(path)
133+
tmpdir_repo = os.path.join(tmpdir.name, repository_name)
134+
sha_process = subprocess.run(
135+
['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True,
136+
cwd=tmpdir_repo)
137+
with open(os.path.join(tmpdir_repo, '.beman_submodule'), 'w') as f:
138+
f.write('[beman_submodule]\n')
139+
f.write(f'remote={repository}\n')
140+
f.write(f'commit_hash={sha_process.stdout.strip()}\n')
141+
shutil.rmtree(os.path.join(tmpdir_repo, '.git'))
142+
shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True)
143+
144+
def status_command(paths):
145+
if not paths:
146+
parent_repo_path = cwd_git_repository_path()
147+
if not parent_repo_path:
148+
raise Exception('this is not a git repository')
149+
beman_submodules = find_beman_submodules_in(parent_repo_path)
150+
else:
151+
beman_submodules = []
152+
for path in paths:
153+
beman_submodule = get_beman_submodule(path)
154+
if not beman_submodule:
155+
raise Exception(f'{path} is not a beman_submodule')
156+
beman_submodules.append(beman_submodule)
157+
for beman_submodule in beman_submodules:
158+
print(beman_submodule_status(beman_submodule))
159+
160+
def get_parser():
161+
parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool')
162+
subparsers = parser.add_subparsers(dest='command', help='available commands')
163+
parser_update = subparsers.add_parser('update', help='update beman_submodules')
164+
parser_update.add_argument(
165+
'--remote', action='store_true',
166+
help='update a beman_submodule to its latest from upstream')
167+
parser_update.add_argument(
168+
'beman_submodule_path', nargs='?',
169+
help='relative path to the beman_submodule to update')
170+
parser_add = subparsers.add_parser('add', help='add a new beman_submodule')
171+
parser_add.add_argument('repository', help='git repository to add')
172+
parser_add.add_argument(
173+
'path', nargs='?', help='path where the repository will be added')
174+
parser_status = subparsers.add_parser(
175+
'status', help='show the status of beman_submodules')
176+
parser_status.add_argument('paths', nargs='*')
177+
return parser
178+
179+
def parse_args(args):
180+
return get_parser().parse_args(args);
181+
182+
def usage():
183+
return get_parser().format_help()
184+
185+
def run_command(args):
186+
if args.command == 'update':
187+
update_command(args.remote, args.beman_submodule_path)
188+
elif args.command == 'add':
189+
add_command(args.repository, args.path)
190+
elif args.command == 'status':
191+
status_command(args.paths)
192+
else:
193+
raise Exception(usage())
194+
195+
def check_for_git(path):
196+
env = os.environ.copy()
197+
if path is not None:
198+
env["PATH"] = path
199+
return shutil.which("git", path=env.get("PATH")) is not None
200+
201+
def main():
202+
try:
203+
if not check_for_git(None):
204+
raise Exception('git not found in PATH')
205+
args = parse_args(sys.argv[1:])
206+
run_command(args)
207+
except Exception as e:
208+
print("Error:", e, file=sys.stderr)
209+
sys.exit(1)
210+
211+
if __name__ == '__main__':
212+
main()

0 commit comments

Comments
 (0)