Skip to content

Commit afdd0a9

Browse files
committed
feat(pack): add --archive-format zip|tar.gz option (default zip)
Adds --archive-format [zip|tar.gz] to apm pack --archive so callers can opt into .tar.gz output. Default remains zip. Threaded through BuildOptions → pack_bundle → export_plugin_bundle (both apm and plugin bundle formats).
1 parent f87c8f5 commit afdd0a9

4 files changed

Lines changed: 56 additions & 16 deletions

File tree

src/apm_cli/bundle/packer.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Bundle packer -- creates self-contained APM bundles from the resolved dependency tree."""
22

33
import shutil
4+
import tarfile
45
import zipfile
56
from dataclasses import dataclass, field
67
from pathlib import Path
@@ -28,6 +29,7 @@ def pack_bundle(
2829
fmt: str = "apm",
2930
target: str | list[str] | None = None,
3031
archive: bool = False,
32+
archive_format: str = "zip",
3133
dry_run: bool = False,
3234
force: bool = False,
3335
logger=None,
@@ -41,7 +43,8 @@ def pack_bundle(
4143
target: Target filter -- ``"copilot"``, ``"claude"``, ``"all"``, a list of
4244
target strings (e.g. ``["claude", "vscode"]``), or *None*
4345
(auto-detect from apm.yml / project structure).
44-
archive: If *True*, produce a ``.zip`` and remove the directory.
46+
archive: If *True*, produce a ``.zip`` (or ``.tar.gz`` when *archive_format* is ``"tar.gz"``) and remove the directory.
47+
archive_format: Archive format when *archive* is True -- ``"zip"`` (default) or ``"tar.gz"``.
4548
dry_run: If *True*, resolve the file list but write nothing to disk.
4649
force: On collision (plugin format), last writer wins.
4750
@@ -64,6 +67,7 @@ def pack_bundle(
6467
output_dir=output_dir,
6568
target=target,
6669
archive=archive,
70+
archive_format=archive_format,
6771
dry_run=dry_run,
6872
force=force,
6973
logger=logger,
@@ -270,12 +274,22 @@ def pack_bundle(
270274

271275
# 10. Archive if requested
272276
if archive:
273-
archive_path = output_dir / f"{pkg_name}-{pkg_version}.zip"
274-
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
275-
for fp in sorted(bundle_dir.rglob("*")):
276-
if fp.is_symlink() or not fp.is_file():
277-
continue
278-
zf.write(fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}")
277+
if archive_format == "tar.gz":
278+
archive_path = output_dir / f"{pkg_name}-{pkg_version}.tar.gz"
279+
with tarfile.open(archive_path, "w:gz") as tf:
280+
for fp in sorted(bundle_dir.rglob("*")):
281+
if fp.is_symlink() or not fp.is_file():
282+
continue
283+
tf.add(fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}")
284+
else:
285+
archive_path = output_dir / f"{pkg_name}-{pkg_version}.zip"
286+
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
287+
for fp in sorted(bundle_dir.rglob("*")):
288+
if fp.is_symlink() or not fp.is_file():
289+
continue
290+
zf.write(
291+
fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}"
292+
)
279293
shutil.rmtree(bundle_dir)
280294
result.bundle_path = archive_path
281295

src/apm_cli/bundle/plugin_exporter.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import json
1212
import re
1313
import shutil
14+
import tarfile
1415
import zipfile
1516
from pathlib import Path, PurePosixPath
1617

@@ -407,6 +408,7 @@ def export_plugin_bundle(
407408
output_dir: Path,
408409
target: str | None = None,
409410
archive: bool = False,
411+
archive_format: str = "zip",
410412
dry_run: bool = False,
411413
force: bool = False,
412414
logger=None,
@@ -420,7 +422,8 @@ def export_plugin_bundle(
420422
project_root: Root of the project containing ``apm.yml``.
421423
output_dir: Parent directory for the generated bundle.
422424
target: Unused for plugin format (reserved for future use).
423-
archive: If True, produce a ``.zip`` and remove the directory.
425+
archive: If True, produce a ``.zip`` (or ``.tar.gz`` when *archive_format* is ``"tar.gz"``) and remove the directory.
426+
archive_format: Archive format when *archive* is True -- ``"zip"`` (default) or ``"tar.gz"``.
424427
dry_run: If True, resolve the file list without writing to disk.
425428
force: On collision, last writer wins instead of first.
426429
@@ -645,13 +648,24 @@ def export_plugin_bundle(
645648

646649
# 15. Archive if requested
647650
if archive:
648-
archive_path = output_dir / f"{bundle_dir.name}.zip"
649-
ensure_path_within(archive_path, output_dir)
650-
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
651-
for fp in sorted(bundle_dir.rglob("*")):
652-
if fp.is_symlink() or not fp.is_file():
653-
continue # reject symlinks injected after write
654-
zf.write(fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}")
651+
if archive_format == "tar.gz":
652+
archive_path = output_dir / f"{bundle_dir.name}.tar.gz"
653+
ensure_path_within(archive_path, output_dir)
654+
with tarfile.open(archive_path, "w:gz") as tf:
655+
for fp in sorted(bundle_dir.rglob("*")):
656+
if fp.is_symlink() or not fp.is_file():
657+
continue
658+
tf.add(fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}")
659+
else:
660+
archive_path = output_dir / f"{bundle_dir.name}.zip"
661+
ensure_path_within(archive_path, output_dir)
662+
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
663+
for fp in sorted(bundle_dir.rglob("*")):
664+
if fp.is_symlink() or not fp.is_file():
665+
continue # reject symlinks injected after write
666+
zf.write(
667+
fp, arcname=f"{bundle_dir.name}/{fp.relative_to(bundle_dir).as_posix()}"
668+
)
655669
shutil.rmtree(bundle_dir)
656670
result.bundle_path = archive_path
657671

src/apm_cli/commands/pack.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,15 @@ def _parse_marketplace_filter(
168168
"--archive",
169169
is_flag=True,
170170
default=False,
171-
help="Produce a .zip archive instead of a directory.",
171+
help="Produce an archive instead of a directory (see --archive-format).",
172+
)
173+
@click.option(
174+
"--archive-format",
175+
"archive_format",
176+
type=click.Choice(["zip", "tar.gz"]),
177+
default="zip",
178+
show_default=True,
179+
help="Archive format when --archive is set.",
172180
)
173181
@click.option(
174182
"-o",
@@ -267,6 +275,7 @@ def pack_cmd( # noqa: PLR0913 -- Click handler, one param per CLI option
267275
fmt,
268276
target,
269277
archive,
278+
archive_format,
270279
output,
271280
dry_run,
272281
force,
@@ -324,6 +333,7 @@ def pack_cmd( # noqa: PLR0913 -- Click handler, one param per CLI option
324333
bundle_format=fmt,
325334
bundle_target=effective_target,
326335
bundle_archive=archive,
336+
bundle_archive_format=archive_format,
327337
bundle_output=Path(output),
328338
bundle_force=force,
329339
marketplace_offline=offline,

src/apm_cli/core/build_orchestrator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class BuildOptions:
4141
bundle_format: str = "plugin"
4242
bundle_target: Any = None
4343
bundle_archive: bool = False
44+
bundle_archive_format: str = "zip"
4445
bundle_output: Path | None = None
4546
bundle_force: bool = False
4647
# Marketplace-only options
@@ -105,6 +106,7 @@ def produce(self, options: BuildOptions, logger: Any) -> ProducerResult:
105106
fmt=options.bundle_format,
106107
target=options.bundle_target,
107108
archive=options.bundle_archive,
109+
archive_format=options.bundle_archive_format,
108110
dry_run=options.dry_run,
109111
force=options.bundle_force,
110112
logger=logger,

0 commit comments

Comments
 (0)