Skip to content

Commit 84a0e00

Browse files
committed
feat: Add Git LFS integration for efficient multi-platform binary management
- Add 'generate_lfs_scripts' config option to enable LFS helper script generation - Generate configure-lfs-skip-smudge.sh script to configure selective LFS downloads - Generate .gitattributes file for proper LFS tracking of binary files - Add comprehensive documentation for LFS integration workflow This feature allows users to: - Skip downloading binaries for non-current platforms automatically - Save significant bandwidth and storage (e.g., 170MB vs 560MB for 3 platforms) - Use simple 'git lfs pull' to get only current platform files - Override with --include flag when other platforms are needed The scripts are generated during 'dotbins sync' or 'dotbins init' when generate_lfs_scripts: true is set in the configuration.
1 parent d57d575 commit 84a0e00

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

dotbins/cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ def _initialize(config: Config) -> None:
9090
config.generate_shell_scripts()
9191
config.generate_readme()
9292

93+
# Generate LFS scripts if configured
94+
if config.generate_lfs_scripts:
95+
from .utils import write_gitattributes_for_lfs, write_lfs_skip_smudge_script
96+
97+
write_lfs_skip_smudge_script(config.tools_dir, config.platforms)
98+
write_gitattributes_for_lfs(config.tools_dir)
99+
93100

94101
def _get_tool(source: str, dest_dir: str | Path, name: str | None = None) -> None:
95102
"""Get a specific tool and install it directly to a location.

dotbins/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
humanize_time_ago,
2828
log,
2929
replace_home_in_path,
30+
write_gitattributes_for_lfs,
31+
write_lfs_skip_smudge_script,
3032
write_shell_scripts,
3133
)
3234
from .versions import VersionStore
@@ -61,6 +63,7 @@ class Config:
6163
- The tools directory where binaries will be stored
6264
- Supported platforms and architectures
6365
- Tool definitions and their settings
66+
- Git LFS configuration options
6467
6568
The configuration is typically loaded from a YAML file, with tools
6669
organized by platform and architecture.
@@ -70,6 +73,7 @@ class Config:
7073
platforms: dict[str, list[str]] = field(default_factory=_default_platforms)
7174
tools: dict[str, ToolConfig] = field(default_factory=dict)
7275
defaults: DefaultsDict = field(default_factory=lambda: DEFAULTS.copy())
76+
generate_lfs_scripts: bool = field(default=False)
7377
config_path: Path | None = field(default=None, init=False)
7478
_bin_dir: Path | None = field(default=None, init=False)
7579
_update_summary: UpdateSummary = field(default_factory=UpdateSummary, init=False)
@@ -244,6 +248,12 @@ def sync_tools(
244248
self.generate_readme(verbose=verbose)
245249
if generate_shell_scripts:
246250
self.generate_shell_scripts(print_shell_setup=False)
251+
252+
# Generate LFS scripts if configured
253+
if self.generate_lfs_scripts:
254+
write_lfs_skip_smudge_script(self.tools_dir, self.platforms)
255+
write_gitattributes_for_lfs(self.tools_dir)
256+
247257
_maybe_copy_config_file(copy_config_file, self.config_path, self.tools_dir)
248258

249259
def generate_shell_scripts(self: Config, print_shell_setup: bool = True) -> None:
@@ -514,6 +524,7 @@ def _config_from_dict(data: RawConfigDict) -> Config:
514524
platforms = data.get("platforms", _default_platforms())
515525
raw_tools = data.get("tools", {})
516526
raw_defaults = data.get("defaults", {})
527+
generate_lfs_scripts = bool(data.get("generate_lfs_scripts", False))
517528

518529
defaults: DefaultsDict = DEFAULTS.copy()
519530
defaults.update(raw_defaults) # type: ignore[typeddict-item]
@@ -536,6 +547,7 @@ def _config_from_dict(data: RawConfigDict) -> Config:
536547
platforms=platforms,
537548
tools=tool_configs,
538549
defaults=defaults,
550+
generate_lfs_scripts=generate_lfs_scripts,
539551
)
540552
config.validate()
541553
return config

dotbins/utils.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,149 @@ def write_shell_scripts(
296296
log(f" [b]PowerShell:[/] [yellow]. {tools_dir2}/shell/powershell.ps1[/]", "info", "👉")
297297

298298

299+
def write_lfs_skip_smudge_script(
300+
tools_dir: Path,
301+
platforms: dict[str, list[str]],
302+
) -> None:
303+
"""Generate a Git LFS skip-smudge script for non-current architectures.
304+
305+
This script configures Git LFS to skip downloading (smudging) binary files
306+
for architectures that are not the current system's architecture, saving
307+
bandwidth and storage space.
308+
309+
Args:
310+
tools_dir: The base directory where tools are installed
311+
platforms: Dictionary mapping platform names to list of architectures
312+
313+
"""
314+
script_path = tools_dir / "configure-lfs-skip-smudge.sh"
315+
316+
script_content = textwrap.dedent("""\
317+
#!/usr/bin/env bash
318+
# Configure Git LFS to skip downloading files for non-current architectures
319+
# Generated by dotbins
320+
321+
set -e
322+
323+
# Detect current OS and architecture
324+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
325+
[[ "$OS" == "darwin" ]] && OS="macos"
326+
327+
ARCH=$(uname -m)
328+
[[ "$ARCH" == "x86_64" ]] && ARCH="amd64"
329+
[[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]] && ARCH="arm64"
330+
331+
CURRENT_PLATFORM="$OS/$ARCH"
332+
333+
echo "Detected platform: $CURRENT_PLATFORM"
334+
echo ""
335+
echo "This script will configure Git LFS to skip downloading files for other platforms."
336+
echo "You only need to run this once after cloning the repository."
337+
echo ""
338+
339+
# Create include patterns for current platform
340+
include_patterns="$CURRENT_PLATFORM/bin/**"
341+
342+
# Create exclude patterns for all other platforms
343+
exclude_patterns=""
344+
""")
345+
346+
# Add exclude patterns for each platform/arch combination
347+
for platform, architectures in platforms.items():
348+
for arch in architectures:
349+
script_content += f"""
350+
if [[ "$OS" != "{platform}" ]] || [[ "$ARCH" != "{arch}" ]]; then
351+
exclude_patterns="${{exclude_patterns}} {platform}/{arch}/bin/**"
352+
fi"""
353+
354+
script_content += """
355+
356+
echo "Setting up Git LFS skip-smudge for non-current platforms..."
357+
358+
# Configure Git to skip smudge for specific paths
359+
# This prevents automatic downloading of LFS files during checkout
360+
for pattern in $exclude_patterns; do
361+
echo " Configuring skip for: $pattern"
362+
# Set skip-smudge for specific paths
363+
git config --local --add lfs.fetchexclude "$pattern"
364+
done
365+
366+
echo ""
367+
echo "✅ LFS skip-smudge configuration complete!"
368+
echo ""
369+
echo "Files for your current platform ($CURRENT_PLATFORM) will be downloaded automatically."
370+
echo "Other platforms will be skipped to save bandwidth and disk space."
371+
echo ""
372+
echo "Useful commands:"
373+
echo " Download current platform files: git lfs pull"
374+
echo " Download a specific platform: git lfs pull --include=\\"linux/amd64/bin/**\\""
375+
echo " Download ALL platforms: git lfs pull --include=\\"*\\""
376+
echo " Check LFS file status: git lfs ls-files"
377+
echo ""
378+
echo "To reset this configuration and download all platforms by default:"
379+
echo " git config --local --unset-all lfs.fetchexclude"
380+
"""
381+
382+
with open(script_path, "w", encoding="utf-8") as f:
383+
f.write(script_content)
384+
385+
# Make the script executable on Unix-like systems
386+
if os.name != "nt":
387+
script_path.chmod(script_path.stat().st_mode | 0o755)
388+
389+
tools_dir_str = replace_home_in_path(tools_dir, "~")
390+
log(
391+
f"Generated LFS skip-smudge script: {tools_dir_str}/configure-lfs-skip-smudge.sh",
392+
"success",
393+
"📝",
394+
)
395+
396+
397+
def write_gitattributes_for_lfs(tools_dir: Path) -> None:
398+
"""Generate a .gitattributes file for Git LFS tracking of binary files.
399+
400+
Args:
401+
tools_dir: The base directory where tools are installed
402+
403+
"""
404+
gitattributes_path = tools_dir / ".gitattributes"
405+
406+
# Common binary file extensions that should be tracked by LFS
407+
gitattributes_content = textwrap.dedent("""\
408+
# Git LFS tracking for binary files
409+
# Generated by dotbins
410+
411+
# Track all files in platform directories as LFS
412+
linux/*/bin/* filter=lfs diff=lfs merge=lfs -text
413+
macos/*/bin/* filter=lfs diff=lfs merge=lfs -text
414+
windows/*/bin/* filter=lfs diff=lfs merge=lfs -text
415+
416+
# Track common binary extensions
417+
*.exe filter=lfs diff=lfs merge=lfs -text
418+
*.dll filter=lfs diff=lfs merge=lfs -text
419+
*.so filter=lfs diff=lfs merge=lfs -text
420+
*.dylib filter=lfs diff=lfs merge=lfs -text
421+
*.appimage filter=lfs diff=lfs merge=lfs -text
422+
*.AppImage filter=lfs diff=lfs merge=lfs -text
423+
424+
# Track archives (in case any are stored)
425+
*.zip filter=lfs diff=lfs merge=lfs -text
426+
*.tar filter=lfs diff=lfs merge=lfs -text
427+
*.tar.gz filter=lfs diff=lfs merge=lfs -text
428+
*.tgz filter=lfs diff=lfs merge=lfs -text
429+
*.tar.bz2 filter=lfs diff=lfs merge=lfs -text
430+
*.tar.xz filter=lfs diff=lfs merge=lfs -text
431+
*.7z filter=lfs diff=lfs merge=lfs -text
432+
*.rar filter=lfs diff=lfs merge=lfs -text
433+
""")
434+
435+
with open(gitattributes_path, "w", encoding="utf-8") as f:
436+
f.write(gitattributes_content)
437+
438+
tools_dir_str = replace_home_in_path(tools_dir, "~")
439+
log(f"Generated .gitattributes for LFS: {tools_dir_str}/.gitattributes", "success", "📝")
440+
441+
299442
STYLE_EMOJI_MAP = {
300443
"success": "✅",
301444
"error": "❌",

0 commit comments

Comments
 (0)