liuyibo commited on
Commit
5ddd667
·
verified ·
1 Parent(s): eb638a7

Add environment artifact: export_and_publish.py

Browse files
Files changed (1) hide show
  1. export_and_publish.py +232 -0
export_and_publish.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export an environment-only image and upload an explicit allowlist to a private HF repo."""
2
+ import argparse
3
+ import gzip
4
+ import hashlib
5
+ import io
6
+ import json
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import tarfile
12
+ from pathlib import Path
13
+
14
+ os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
15
+
16
+
17
+ class PartsWriter(io.RawIOBase):
18
+ def __init__(self, directory, prefix, limit=1024**3):
19
+ self.directory, self.prefix, self.limit = directory, prefix, limit
20
+ self.files = []
21
+ self.current = None
22
+ self.used = 0
23
+
24
+ def writable(self):
25
+ return True
26
+
27
+ def write(self, data):
28
+ length = len(data)
29
+ view = memoryview(data)
30
+ while view:
31
+ if self.current is None or self.used == self.limit:
32
+ if self.current:
33
+ self.current.close()
34
+ path = self.directory / f"{self.prefix}.part-{len(self.files):03d}"
35
+ self.current = path.open("xb")
36
+ self.files.append(path)
37
+ self.used = 0
38
+ take = min(len(view), self.limit - self.used)
39
+ self.current.write(view[:take])
40
+ self.used += take
41
+ view = view[take:]
42
+ return length
43
+
44
+ def close(self):
45
+ if self.current:
46
+ self.current.close()
47
+ super().close()
48
+
49
+
50
+ def sha256(path):
51
+ digest = hashlib.sha256()
52
+ with path.open("rb") as source:
53
+ for block in iter(lambda: source.read(1024 * 1024), b""):
54
+ digest.update(block)
55
+ return digest.hexdigest()
56
+
57
+
58
+ METADATA_FILES = {
59
+ "Dockerfile", "smoke_test.py", "environment-inventory.json", "requirements.snapshot.txt",
60
+ "prepare_environment.py", "export_and_publish.py", "model_setup.md", "validation.json", "README.md",
61
+ }
62
+ PART_PATTERN = re.compile(r"codiffact-env_20261006-cu128\.tar\.gz\.part-([0-9]{3})")
63
+
64
+
65
+ def validate_release(directory, release):
66
+ seen, numbers = set(), []
67
+ for item in release["files"]:
68
+ name = item["file"]
69
+ match = PART_PATTERN.fullmatch(name)
70
+ assert name in METADATA_FILES or match, "Unapproved upload filename"
71
+ assert name not in seen, "Duplicate artifact"
72
+ seen.add(name)
73
+ path = directory / name
74
+ assert path.is_file() and not path.is_symlink()
75
+ assert path.resolve().parent == directory.resolve()
76
+ assert path.stat().st_size == item["bytes"] and sha256(path) == item["sha256"]
77
+ if match:
78
+ numbers.append(int(match.group(1)))
79
+ assert METADATA_FILES <= seen and sorted(numbers) == list(range(len(numbers))) and numbers
80
+ actual_parts = {p.name for p in directory.glob("*.part-*")}
81
+ assert actual_parts == {name for name in seen if PART_PATTERN.fullmatch(name)}
82
+
83
+
84
+ def main():
85
+ parser = argparse.ArgumentParser()
86
+ parser.add_argument("--image", required=True)
87
+ parser.add_argument("--repo", required=True)
88
+ parser.add_argument("--token-file", type=Path, required=True)
89
+ parser.add_argument("--context", type=Path, required=True)
90
+ parser.add_argument("--out", type=Path, required=True)
91
+ parser.add_argument("--resume-upload", action="store_true")
92
+ args = parser.parse_args()
93
+ tokens = set(re.findall(r"(?<![A-Za-z0-9_])hf_[A-Za-z0-9]+(?![A-Za-z0-9_])", args.token_file.read_text()))
94
+ if len(tokens) != 1:
95
+ raise ValueError("Expected one token in the supplied token file")
96
+ token = next(iter(tokens))
97
+ token_bytes = token.encode()
98
+ from huggingface_hub import HfApi
99
+
100
+ api = HfApi(token=token)
101
+ identity = api.whoami()
102
+ assert args.repo.split("/")[0] == identity["name"], "Only publish to the authenticated user's namespace"
103
+ root = Path(__file__).resolve().parents[2]
104
+ manifest_path = args.out / "environment-release.json"
105
+ if args.resume_upload:
106
+ release = json.loads(manifest_path.read_text())
107
+ assert release["repo"] == args.repo and release["image_tag"] == args.image
108
+ for item in release["files"]:
109
+ assert sha256(args.out / item["file"]) == item["sha256"]
110
+ else:
111
+ assert not args.out.exists(), "Use a fresh output directory or --resume-upload"
112
+ info = json.loads(subprocess.check_output(["docker", "image", "inspect", args.image], text=True))[0]
113
+ assert not any("TOKEN=" in value or "PROXY=" in value.upper() for value in info["Config"].get("Env", []))
114
+ inventory = json.loads((args.context / "environment-inventory.json").read_text())
115
+ assert not any(any(part in item["file"].split("/") for part in (".git", ".cache", "hfkey.txt")) for item in inventory["files"])
116
+ args.out.mkdir(parents=True)
117
+ validations = {}
118
+ for mode in ("cpu", "gpu"):
119
+ cmd = ["docker", "run", "--rm", "--network", "none"]
120
+ if mode == "gpu":
121
+ cmd.extend(["--gpus", "device=0"])
122
+ cmd.extend([args.image, "python", "/opt/codiffact/smoke_test.py"])
123
+ if mode == "gpu":
124
+ cmd.append("--gpu")
125
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
126
+ validations[mode] = json.loads(result.stdout.strip().splitlines()[-1])
127
+ print("OFFLINE_VALIDATION", mode, "passed", flush=True)
128
+ (args.out / "validation.json").write_text(json.dumps(validations, indent=2))
129
+ prefix = "codiffact-env_20261006-cu128.tar.gz"
130
+ writer = PartsWriter(args.out, prefix)
131
+ process = subprocess.Popen(["docker", "save", args.image], stdout=subprocess.PIPE)
132
+ image_secret_scan_tail = b""
133
+ try:
134
+ with gzip.GzipFile(filename="", mode="wb", fileobj=writer, compresslevel=1, mtime=0) as compressed:
135
+ for block in iter(lambda: process.stdout.read(4 * 1024 * 1024), b""):
136
+ scan = image_secret_scan_tail + block
137
+ assert token_bytes not in scan, "Token detected; export aborted"
138
+ image_secret_scan_tail = scan[-len(token_bytes):]
139
+ compressed.write(block)
140
+ assert process.wait() == 0, "docker save failed"
141
+ finally:
142
+ if process.poll() is None:
143
+ process.terminate()
144
+ process.wait()
145
+ writer.close()
146
+ files = list(writer.files)
147
+ for name in ("Dockerfile", "smoke_test.py", "environment-inventory.json", "requirements.snapshot.txt"):
148
+ destination = args.out / name
149
+ shutil.copy2(args.context / name, destination)
150
+ files.append(destination)
151
+ for name in ("prepare_environment.py", "export_and_publish.py"):
152
+ destination = args.out / name
153
+ shutil.copy2(Path(__file__).parent / name, destination)
154
+ files.append(destination)
155
+ shutil.copy2(root / "docs/model_setup.md", args.out / "model_setup.md")
156
+ files.extend([args.out / "model_setup.md", args.out / "validation.json"])
157
+ body = (
158
+ "---\ntags:\n- docker\n- environment\n- codiffact\n---\n\n"
159
+ "# CoDiffAct environment-only image\n\n"
160
+ "Private Linux/amd64 Docker archive. Contains installed runtime dependencies; no project, datasets, pretrained weights or credentials. "
161
+ "Each dependency retains its own license; this card does not relicense them.\n\n"
162
+ "Download all parts, verify SHA256SUMS, then load:\n\n```bash\n"
163
+ "sha256sum -c SHA256SUMS\n"
164
+ "cat codiffact-env_20261006-cu128.tar.gz.part-* | gzip -dc | docker load\n"
165
+ f"docker run --rm --network none --gpus all {args.image} python /opt/codiffact/smoke_test.py --gpu\n"
166
+ "```\n\n"
167
+ "See model_setup.md for public model sources, paths and mounts. validation.json records offline dependency, OSMesa, audio/video and CUDA smoke checks, "
168
+ "not full model inference/training. A compatible host NVIDIA driver and NVIDIA Container Toolkit are required.\n\n"
169
+ "The exact installed-prefix snapshot is embedded in the archive. requirements.snapshot.txt is an inventory, not a universal pip installation lock. "
170
+ "Rebuilding with prepare_environment.py requires the recorded source environment; Dockerfile alone is not a complete from-PyPI recipe.\n"
171
+ )
172
+ (args.out / "README.md").write_text(body)
173
+ files.append(args.out / "README.md")
174
+ for path in files:
175
+ if ".part-" not in path.name:
176
+ assert token_bytes not in path.read_bytes(), "Token detected in upload material"
177
+ release = {
178
+ "repo": args.repo, "private": True, "image_tag": args.image,
179
+ "image_id": info["Id"], "uncompressed_image_bytes": info["Size"],
180
+ "base_image": "nvidia/cuda@sha256:001469ea0f3dec85a1ca929aeea3b58ae369d4c11228b10aec1f642bb6ca7a6f",
181
+ "files": [{"file": p.name, "bytes": p.stat().st_size, "sha256": sha256(p)} for p in files],
182
+ "no_user_token_in_export": True,
183
+ }
184
+ manifest_path.write_text(json.dumps(release, indent=2))
185
+ sums = [f"{item['sha256']} {item['file']}" for item in release["files"]]
186
+ sums.append(f"{sha256(manifest_path)} {manifest_path.name}")
187
+ (args.out / "SHA256SUMS").write_text("\n".join(sums) + "\n")
188
+ print("EXPORT_COMPLETE", sum(p.stat().st_size for p in writer.files), "bytes", flush=True)
189
+ validate_release(args.out, release)
190
+ api.create_repo(repo_id=args.repo, repo_type="model", private=True, exist_ok=True)
191
+ assert api.repo_info(args.repo).private, "Refusing to upload to a public repository"
192
+ allowed = [item["file"] for item in release["files"]] + ["SHA256SUMS", manifest_path.name]
193
+ for name in ("SHA256SUMS", manifest_path.name):
194
+ assert not (args.out / name).is_symlink()
195
+ assert token_bytes not in (args.out / name).read_bytes()
196
+ parts = sorted(name for name in allowed if ".part-" in name)
197
+ metadata = sorted(set(allowed) - set(parts))
198
+ remote = {s.rfilename: s for s in api.repo_info(args.repo, files_metadata=True).siblings}
199
+ assert not ({name for name in remote if ".part-" in name} - set(parts)), "Old remote parts present; use a new version repository"
200
+ for name in parts:
201
+ previous = getattr(remote.get(name), "lfs", None)
202
+ assert not previous or previous.sha256 == sha256(args.out / name), "Never overwrite another image release; use a new version repository"
203
+ for name in parts + metadata:
204
+ path = args.out / name
205
+ expected = sha256(path)
206
+ existing = remote.get(name)
207
+ lfs = getattr(existing, "lfs", None)
208
+ if lfs and lfs.sha256 == expected:
209
+ print("ALREADY_UPLOADED", name, flush=True)
210
+ continue
211
+ print("UPLOADING", name, path.stat().st_size, flush=True)
212
+ api.upload_file(path_or_fileobj=str(path), path_in_repo=name, repo_id=args.repo,
213
+ repo_type="model", commit_message=f"Add environment artifact: {name}")
214
+ final = api.repo_info(args.repo, files_metadata=True)
215
+ remote = {s.rfilename: s for s in final.siblings}
216
+ for name in allowed:
217
+ assert name in remote, name
218
+ assert remote[name].size == (args.out / name).stat().st_size, name
219
+ lfs = getattr(remote[name], "lfs", None)
220
+ if lfs:
221
+ assert lfs.sha256 == sha256(args.out / name), name
222
+ receipt = {"repo": args.repo, "private": final.private, "commit": final.sha, "files": allowed, "complete": True}
223
+ (args.out.parent / "upload_receipt.json").write_text(json.dumps(receipt, indent=2))
224
+ print("HF_UPLOAD_VERIFIED", args.repo, final.sha, flush=True)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ try:
229
+ main()
230
+ except Exception as error:
231
+ print("FAILED", type(error).__name__, "See local build/validation state; secret values suppressed.", flush=True)
232
+ raise SystemExit(1)