leanup 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {leanup-0.0.2 → leanup-0.0.3}/PKG-INFO +8 -3
  2. {leanup-0.0.2 → leanup-0.0.3}/README.md +2 -2
  3. {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/leanup/__init__.py +3 -1
  4. leanup-0.0.3/leanup/cli.py +12 -0
  5. leanup-0.0.3/leanup/const.py +15 -0
  6. leanup-0.0.3/leanup/utils/__init__.py +1 -0
  7. leanup-0.0.3/leanup/utils/executor.py +183 -0
  8. {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/PKG-INFO +8 -3
  9. leanup-0.0.3/leanup.egg-info/SOURCES.txt +29 -0
  10. leanup-0.0.3/leanup.egg-info/entry_points.txt +2 -0
  11. leanup-0.0.3/leanup.egg-info/requires.txt +11 -0
  12. {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/top_level.txt +0 -1
  13. {leanup-0.0.2 → leanup-0.0.3}/pyproject.toml +9 -2
  14. leanup-0.0.3/tests/conftest.py +22 -0
  15. leanup-0.0.3/tests/test_executor.py +88 -0
  16. leanup-0.0.3/tests/test_leanup.py +17 -0
  17. leanup-0.0.2/build/lib/build/lib/cli/__init__.py +0 -15
  18. leanup-0.0.2/build/lib/build/lib/cli/install.py +0 -143
  19. leanup-0.0.2/build/lib/build/lib/cli/main.py +0 -184
  20. leanup-0.0.2/build/lib/build/lib/cli/utils.py +0 -245
  21. leanup-0.0.2/build/lib/build/lib/dist/leanup-0.0.1-py3-none-any.whl +0 -0
  22. leanup-0.0.2/build/lib/build/lib/dist/leanup-0.0.1.tar.gz +0 -0
  23. leanup-0.0.2/build/lib/build/lib/tests/test_leanup.py +0 -10
  24. leanup-0.0.2/build/lib/cli/__init__.py +0 -15
  25. leanup-0.0.2/build/lib/cli/install.py +0 -143
  26. leanup-0.0.2/build/lib/cli/main.py +0 -184
  27. leanup-0.0.2/build/lib/cli/utils.py +0 -245
  28. leanup-0.0.2/build/lib/dist/leanup-0.0.1-py3-none-any.whl +0 -0
  29. leanup-0.0.2/build/lib/dist/leanup-0.0.1.tar.gz +0 -0
  30. leanup-0.0.2/build/lib/dist/leanup-0.0.2-py3-none-any.whl +0 -0
  31. leanup-0.0.2/build/lib/dist/leanup-0.0.2.tar.gz +0 -0
  32. leanup-0.0.2/build/lib/leanup/__init__.py +0 -5
  33. leanup-0.0.2/build/lib/leanup/leanup.py +0 -1
  34. leanup-0.0.2/build/lib/tests/__init__.py +0 -1
  35. leanup-0.0.2/build/lib/tests/test_leanup.py +0 -10
  36. leanup-0.0.2/dist/leanup-0.0.1-py3-none-any.whl +0 -0
  37. leanup-0.0.2/dist/leanup-0.0.1.tar.gz +0 -0
  38. leanup-0.0.2/dist/leanup-0.0.2-py3-none-any.whl +0 -0
  39. leanup-0.0.2/dist/leanup-0.0.2.tar.gz +0 -0
  40. leanup-0.0.2/leanup/__init__.py +0 -5
  41. leanup-0.0.2/leanup/leanup.py +0 -1
  42. leanup-0.0.2/leanup.egg-info/SOURCES.txt +0 -46
  43. leanup-0.0.2/leanup.egg-info/requires.txt +0 -6
  44. leanup-0.0.2/tests/__init__.py +0 -1
  45. leanup-0.0.2/tests/test_leanup.py +0 -10
  46. {leanup-0.0.2 → leanup-0.0.3}/LICENSE +0 -0
  47. {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/leanup/leanup.py +0 -0
  48. {leanup-0.0.2 → leanup-0.0.3}/leanup.egg-info/dependency_links.txt +0 -0
  49. {leanup-0.0.2 → leanup-0.0.3}/setup.cfg +0 -0
  50. {leanup-0.0.2/build/lib/build/lib → leanup-0.0.3}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leanup
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Python package for Lean Environment Management
5
5
  Author-email: Lean-zh Community <leanprover@outlook.com>
6
6
  Maintainer-email: Lean-zh Community <leanprover@outlook.com>
@@ -10,6 +10,11 @@ Project-URL: changelog, https://github.com/{{ cookiecutter.github_username }}/{{
10
10
  Project-URL: homepage, https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
+ Requires-Dist: gitpython
14
+ Requires-Dist: psutil
15
+ Requires-Dist: click
16
+ Requires-Dist: platformdirs
17
+ Requires-Dist: loguru
13
18
  Provides-Extra: dev
14
19
  Requires-Dist: coverage; extra == "dev"
15
20
  Requires-Dist: mypy; extra == "dev"
@@ -23,8 +28,8 @@ Dynamic: license-file
23
28
  <a href="https://pypi.python.org/pypi/leanup">
24
29
  <img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
25
30
  </a>
26
- <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml">
27
- <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml/badge.svg" alt="Tests" />
31
+ <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
32
+ <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
28
33
  </a>
29
34
  <a href="https://codecov.io/gh/Lean-zh/leanup">
30
35
  <img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
@@ -4,8 +4,8 @@
4
4
  <a href="https://pypi.python.org/pypi/leanup">
5
5
  <img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
6
6
  </a>
7
- <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml">
8
- <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml/badge.svg" alt="Tests" />
7
+ <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
8
+ <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
9
9
  </a>
10
10
  <a href="https://codecov.io/gh/Lean-zh/leanup">
11
11
  <img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
@@ -2,4 +2,6 @@
2
2
 
3
3
  __author__ = """Lean-zh Community"""
4
4
  __email__ = 'leanprover@outlook.com'
5
- __version__ = '0.0.2'
5
+ __version__ = '0.0.3'
6
+
7
+ from leanup.utils import CommandExecutor
@@ -0,0 +1,12 @@
1
+ import click
2
+
3
+ @click.group()
4
+ def main():
5
+ """A command-line interface for LeanUp."""
6
+ pass
7
+
8
+ @main.group()
9
+ def repo():
10
+ """Manage Lean repo installations"""
11
+ pass
12
+
@@ -0,0 +1,15 @@
1
+ import os
2
+ import platform
3
+ import platformdirs
4
+ from pathlib import Path
5
+
6
+ OS_TYPE = None
7
+ if platform.system() == 'Windows':
8
+ OS_TYPE = 'Windows'
9
+ elif platform.system() == 'Darwin':
10
+ OS_TYPE = 'MacOS'
11
+ elif platform.system() == 'Linux':
12
+ OS_TYPE = 'Linux'
13
+
14
+ LEANUP_CACHE_DIR = Path(
15
+ os.getenv('LEANUP_CACHE_DIR', platformdirs.user_cache_dir("leanup")))
@@ -0,0 +1 @@
1
+ from leanup.utils.executor import CommandExecutor
@@ -0,0 +1,183 @@
1
+ import subprocess
2
+ import os
3
+ from pathlib import Path
4
+ import tempfile
5
+ from contextlib import contextmanager
6
+ from typing import Optional, Union, Tuple, List, Dict, Any, Generator
7
+ import git
8
+ from psutil import Process, NoSuchProcess
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class CommandExecutor:
14
+ def __init__(self, cwd: Optional[str] = None, timeout: Optional[int] = None):
15
+ """
16
+ Initialize command executor
17
+
18
+ Args:
19
+ cwd: Default working directory
20
+ timeout: Default timeout for commands
21
+ """
22
+ self.cwd = cwd
23
+ self.timeout = timeout
24
+ self.active_processes = [] # Track active processes
25
+
26
+ @contextmanager
27
+ def working_directory(
28
+ self,
29
+ path: Optional[Union[str, Path]] = None,
30
+ chdir: bool = False,
31
+ ) -> Generator[Path, None, None]:
32
+ """
33
+ Context manager for working directory operations
34
+
35
+ Args:
36
+ path: Target working directory path. If None, creates a temporary directory
37
+ chdir: Whether to actually change the current working directory
38
+
39
+ Yields:
40
+ Path object representing working directory
41
+ """
42
+ origin = Path.cwd()
43
+ if path is None: # Create temporary directory
44
+ tmp_dir = tempfile.TemporaryDirectory()
45
+ path = tmp_dir.__enter__()
46
+ is_temporary = True
47
+ else:
48
+ is_temporary = False
49
+
50
+ path = Path(path)
51
+ path.mkdir(parents=True, exist_ok=True)
52
+
53
+ if chdir: os.chdir(path)
54
+ try:
55
+ yield path
56
+ finally:
57
+ if chdir: os.chdir(origin)
58
+ if is_temporary:
59
+ tmp_dir.__exit__(None, None, None)
60
+
61
+ def execute_in_directory(self, command: list,
62
+ directory: Optional[Union[str, Path]] = None,
63
+ chdir: bool = False,
64
+ **kwargs) -> Tuple[str, str, int]:
65
+ """
66
+ Execute command in specified directory using context manager
67
+
68
+ Args:
69
+ command: Command list to execute
70
+ directory: Target directory for execution
71
+ chdir: Whether to change working directory
72
+ **kwargs: Additional arguments passed to execute()
73
+
74
+ Returns:
75
+ Tuple containing (stdout, stderr, return_code)
76
+ """
77
+ with self.working_directory(directory, chdir=chdir) as work_dir:
78
+ return self.execute(command, cwd=str(work_dir), **kwargs)
79
+
80
+ def execute(self, command: list,
81
+ cwd: Optional[str] = None,
82
+ text: bool = True,
83
+ input: Union[str, None] = None,
84
+ capture_output: bool = True,
85
+ timeout: Optional[int] = None) -> Tuple[str, str, int]:
86
+ """
87
+ Execute command and capture output
88
+
89
+ Args:
90
+ command: Command list to execute
91
+ cwd: Working directory for command execution
92
+ text: Whether to use text mode
93
+ input: Input to pass to command
94
+ capture_output: Whether to capture command output
95
+ timeout: Command execution timeout in seconds
96
+
97
+ Returns:
98
+ Tuple containing (stdout, stderr, return_code)
99
+ """
100
+ working_dir = cwd or self.cwd
101
+ self.active_processes = []
102
+ timeout = timeout if timeout is not None else self.timeout
103
+
104
+ try:
105
+ # Configure output pipes
106
+ stdout_pipe = subprocess.PIPE if capture_output else None
107
+ stderr_pipe = subprocess.PIPE if capture_output else None
108
+
109
+ # Create and start process
110
+ proc = subprocess.Popen(
111
+ command,
112
+ cwd=working_dir,
113
+ stdout=stdout_pipe,
114
+ stderr=stderr_pipe,
115
+ text=text
116
+ )
117
+
118
+ # Track process and its children
119
+ main_pid = proc.pid
120
+ self.active_processes = self._get_process_children(main_pid) + [Process(main_pid)]
121
+
122
+ # Wait for completion and get output
123
+ proc_output, proc_error = proc.communicate(input=input, timeout=timeout)
124
+ exit_code = proc.returncode
125
+
126
+ # Ensure output strings are not None
127
+ command_output = proc_output or ""
128
+ error_output = proc_error or ""
129
+
130
+ except Exception as e:
131
+ # Handle execution errors
132
+ command_output, error_output, exit_code = "", str(e), -1
133
+ finally:
134
+ # Always cleanup processes
135
+ self._cleanup_processes()
136
+ return command_output, error_output, exit_code
137
+
138
+ def _get_process_children(self, pid: int) -> List[Process]:
139
+ """
140
+ Get child processes for given PID
141
+
142
+ Args:
143
+ pid: Parent process ID
144
+
145
+ Returns:
146
+ List of child processes
147
+ """
148
+ try:
149
+ parent_process = Process(pid)
150
+ return parent_process.children(recursive=True)
151
+ except NoSuchProcess:
152
+ return []
153
+
154
+ def _cleanup_processes(self):
155
+ """
156
+ Clean up all tracked processes
157
+ Attempts to terminate processes gracefully
158
+ """
159
+ for process in self.active_processes:
160
+ try:
161
+ process.terminate()
162
+ process.wait(timeout=5)
163
+ except Exception as e:
164
+ if not isinstance(e, NoSuchProcess):
165
+ logger.debug(f"Failed to terminate process {process}: {e}")
166
+
167
+ # Git operations
168
+ def git_clone(self, repo_url: str, target_dir: Optional[str] = None) -> Tuple[bool, str]:
169
+ """
170
+ Clone a git repository
171
+
172
+ Args:
173
+ repo_url: Repository URL to clone
174
+ target_dir: Target directory for clone
175
+
176
+ Returns:
177
+ Tuple of (success_status, error_message)
178
+ """
179
+ try:
180
+ git.Repo.clone_from(repo_url, target_dir or os.path.basename(repo_url.split('/')[-1].split('.')[0]))
181
+ return True, ""
182
+ except Exception as e:
183
+ return False, str(e)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leanup
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Python package for Lean Environment Management
5
5
  Author-email: Lean-zh Community <leanprover@outlook.com>
6
6
  Maintainer-email: Lean-zh Community <leanprover@outlook.com>
@@ -10,6 +10,11 @@ Project-URL: changelog, https://github.com/{{ cookiecutter.github_username }}/{{
10
10
  Project-URL: homepage, https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
+ Requires-Dist: gitpython
14
+ Requires-Dist: psutil
15
+ Requires-Dist: click
16
+ Requires-Dist: platformdirs
17
+ Requires-Dist: loguru
13
18
  Provides-Extra: dev
14
19
  Requires-Dist: coverage; extra == "dev"
15
20
  Requires-Dist: mypy; extra == "dev"
@@ -23,8 +28,8 @@ Dynamic: license-file
23
28
  <a href="https://pypi.python.org/pypi/leanup">
24
29
  <img src="https://img.shields.io/pypi/v/leanup.svg" alt="PyPI version" />
25
30
  </a>
26
- <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml">
27
- <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yml/badge.svg" alt="Tests" />
31
+ <a href="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml">
32
+ <img src="https://github.com/Lean-zh/leanup/actions/workflows/ci.yaml/badge.svg" alt="Tests" />
28
33
  </a>
29
34
  <a href="https://codecov.io/gh/Lean-zh/leanup">
30
35
  <img src="https://codecov.io/gh/Lean-zh/leanup/branch/main/graph/badge.svg" alt="Coverage" />
@@ -0,0 +1,29 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ./leanup/__init__.py
5
+ ./leanup/cli.py
6
+ ./leanup/const.py
7
+ ./leanup/leanup.py
8
+ ./leanup/utils/__init__.py
9
+ ./leanup/utils/executor.py
10
+ ./tests/__init__.py
11
+ ./tests/conftest.py
12
+ ./tests/test_executor.py
13
+ ./tests/test_leanup.py
14
+ leanup/__init__.py
15
+ leanup/cli.py
16
+ leanup/const.py
17
+ leanup/leanup.py
18
+ leanup.egg-info/PKG-INFO
19
+ leanup.egg-info/SOURCES.txt
20
+ leanup.egg-info/dependency_links.txt
21
+ leanup.egg-info/entry_points.txt
22
+ leanup.egg-info/requires.txt
23
+ leanup.egg-info/top_level.txt
24
+ leanup/utils/__init__.py
25
+ leanup/utils/executor.py
26
+ tests/__init__.py
27
+ tests/conftest.py
28
+ tests/test_executor.py
29
+ tests/test_leanup.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ leanup = leanup.cli:main
@@ -0,0 +1,11 @@
1
+ gitpython
2
+ psutil
3
+ click
4
+ platformdirs
5
+ loguru
6
+
7
+ [dev]
8
+ coverage
9
+ mypy
10
+ pytest
11
+ ruff
@@ -1,4 +1,3 @@
1
- build
2
1
  dist
3
2
  leanup
4
3
  tests
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "leanup"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "Python package for Lean Environment Management"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -18,7 +18,11 @@ classifiers = [
18
18
  ]
19
19
  license = {text = "MIT license"}
20
20
  dependencies = [
21
-
21
+ "gitpython",
22
+ "psutil", # Process management
23
+ "click", # Command line interface
24
+ "platformdirs", # Platform-specific directories
25
+ "loguru", # Logging
22
26
  ]
23
27
 
24
28
  [project.optional-dependencies]
@@ -29,6 +33,9 @@ dev = [
29
33
  "ruff" # linting
30
34
  ]
31
35
 
36
+ [project.scripts]
37
+ leanup = "leanup.cli:main"
38
+
32
39
  [project.urls]
33
40
 
34
41
  bugs = "https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/issues"
@@ -0,0 +1,22 @@
1
+ import tempfile
2
+ import pytest
3
+ from pathlib import Path
4
+ from leanup.const import LEANUP_CACHE_DIR
5
+ from leanup.utils import CommandExecutor
6
+
7
+
8
+ @pytest.fixture
9
+ def cache_dir():
10
+ """Fixture to provide the LeanUp cache directory."""
11
+ return LEANUP_CACHE_DIR
12
+
13
+ @pytest.fixture
14
+ def executor():
15
+ """Create a CommandExecutor instance for testing"""
16
+ return CommandExecutor()
17
+
18
+ @pytest.fixture
19
+ def temp_dir():
20
+ """Create a temporary directory for testing"""
21
+ with tempfile.TemporaryDirectory() as tmp_dir:
22
+ yield Path(tmp_dir)
@@ -0,0 +1,88 @@
1
+ import os
2
+ import pytest
3
+ from pathlib import Path
4
+ import tempfile
5
+ from leanup.const import OS_TYPE
6
+ from leanup.utils.executor import CommandExecutor
7
+
8
+ def test_basic_execute(executor:CommandExecutor):
9
+ """Test basic command execution"""
10
+ cmd = ['echo', 'hello']
11
+
12
+ output, error, code = executor.execute(cmd)
13
+ assert code == 0
14
+ assert 'hello' in output
15
+ assert error == ''
16
+
17
+ def test_execute_with_error(executor:CommandExecutor):
18
+ """Test command execution with error"""
19
+ if OS_TYPE == 'Windows':
20
+ cmd = ['dir', '/invalid_path']
21
+ else:
22
+ cmd = ['ls', '/nonexistent_directory']
23
+
24
+ output, error, code = executor.execute(cmd)
25
+ assert code != 0
26
+ assert error != ''
27
+
28
+ def test_working_directory(executor:CommandExecutor, temp_dir:Path):
29
+ """Test working directory context manager"""
30
+ test_file = temp_dir / 'test.txt'
31
+ test_file.write_text('test content')
32
+
33
+ with executor.working_directory(temp_dir, chdir=True):
34
+ if OS_TYPE == 'Windows':
35
+ cmd = ['dir']
36
+ else:
37
+ cmd = ['ls']
38
+ output, error, code = executor.execute(cmd)
39
+ assert code == 0
40
+ assert 'test.txt' in output
41
+
42
+ def test_execute_in_directory(executor:CommandExecutor, temp_dir:Path):
43
+ """Test execute_in_directory method"""
44
+ test_file = temp_dir / 'test.txt'
45
+ test_file.write_text('test content')
46
+
47
+ if OS_TYPE == 'Windows':
48
+ cmd = ['dir']
49
+ else:
50
+ cmd = ['ls']
51
+
52
+ output, error, code = executor.execute_in_directory(cmd, directory=temp_dir)
53
+ assert code == 0
54
+ assert 'test.txt' in output
55
+
56
+ def test_timeout(executor:CommandExecutor):
57
+ """Test command execution with timeout"""
58
+ if OS_TYPE == 'Windows':
59
+ cmd = ['timeout', '10']
60
+ else:
61
+ cmd = ['sleep', '10']
62
+
63
+ output, error, code = executor.execute(cmd, timeout=1)
64
+ assert code != 0
65
+
66
+
67
+ def test_multiple_commands(executor:CommandExecutor):
68
+ """Test executing multiple commands sequentially"""
69
+ commands = []
70
+ if OS_TYPE == 'Windows':
71
+ commands = [
72
+ ['echo', 'first'],
73
+ ['echo', 'second'],
74
+ ]
75
+ else:
76
+ commands = [
77
+ ['echo', 'first'],
78
+ ['echo', 'second'],
79
+ ]
80
+
81
+ results = []
82
+ for cmd in commands:
83
+ output, error, code = executor.execute(cmd)
84
+ results.append((output, code))
85
+
86
+ assert all(code == 0 for _, code in results)
87
+ assert 'first' in results[0][0]
88
+ assert 'second' in results[1][0]
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Tests for `leanup` package."""
4
+
5
+ import pytest
6
+ from leanup.const import LEANUP_CACHE_DIR, OS_TYPE
7
+
8
+ def test_leanup_basic():
9
+ """Test if the package can be imported."""
10
+ from leanup import __version__
11
+ print(f"LeanUp version: {__version__}")
12
+ print(f"LeanUp cache directory: {LEANUP_CACHE_DIR}")
13
+
14
+ def test_system():
15
+ """Test if the computer system is recognized."""
16
+ assert OS_TYPE in ['Windows', 'MacOS', 'Linux'], f"Unexpected OS_TYPE: {OS_TYPE}"
17
+
@@ -1,15 +0,0 @@
1
- from .install import (
2
- install_repl, install_lean_repo, install_lean, install_mathlib, get_valid_versions
3
- )
4
- from .utils import (
5
- list_lookeng_cache, list_mathlib_cache, list_repo_cache,
6
- read_lookeng_cache, read_mathlib_cache, read_repo_cache,
7
- get_lookeng_versions, get_mathlib_versions
8
- )
9
-
10
-
11
- help_message = """
12
- Lean Repo management CLI
13
-
14
- A tool for managing Lean repositories.
15
- """
@@ -1,143 +0,0 @@
1
- from lookeng.utils import working_directory, execute_command, execute_popen_command
2
- from lookeng.constants import MATHLIB_URL
3
- from lookeng import constants
4
- import os
5
- import toml, subprocess, shutil, re
6
- from pathlib import Path
7
- from typing import Tuple, Optional, List
8
- from loguru import logger
9
- from git import Repo, BadName
10
- from .utils import (
11
- url_to_repo_name, install_lean, get_valid_versions, get_tag_sha,
12
- detect_lakefile, update_lakefile_toml, update_lakefile_lean, url_to_prefix
13
- )
14
-
15
- class InstallationError(Exception):
16
- pass
17
-
18
- def install_lean_repo(url:str, version: str,
19
- prefix:Optional[str]=None,
20
- force: bool = False,
21
- dest_dir: Optional[str] = None,
22
- with_mathlib:bool=False,
23
- build_cmds:Optional[list[str]]=None) -> Path:
24
- """
25
- Download and build Lean 4 Repo.
26
-
27
- Args:
28
- url: Lean repository URL
29
- version: Lean tag (e.g. 'v4.10.0')
30
- prefix: Prefix for the installation directory, extract from url by default
31
- force: If True, overwrite existing installation
32
- dest_dir: Custom installation directory
33
- build_cmds: List of build commands to run
34
-
35
- Returns:
36
- Path: Installation directory path
37
-
38
- Raises:
39
- InstallationError: If installation fails
40
- """
41
- # make sure `elan`` is installed
42
- if shutil.which('lake') is None:
43
- install_lean()
44
- os.environ['PATH'] += os.pathsep + os.path.expanduser('~/.elan/bin')
45
- # default build commands
46
- if build_cmds is None:
47
- build_cmds = ['lake update -R', 'lake build']
48
- # Setup paths
49
- dest_dir = dest_dir and Path(dest_dir).resolve()
50
- if dest_dir is not None and not force and dest_dir.exists():
51
- logger.info(f"Destination directory {dest_dir} already exists")
52
- return dest_dir
53
-
54
- # Check existing cache
55
- username, repo_name = url_to_repo_name(url)
56
- prefix = prefix or url_to_prefix(url)
57
- cache_dir = Path(constants.LOOKENG_CACHE_DIR)
58
- cache_dir.mkdir(parents=True, exist_ok=True)
59
- cache_path = cache_dir / f"{prefix}-{version}"
60
-
61
- if cache_path.exists() and not force:
62
- logger.info(f"Version {version} already installed at {cache_path}")
63
- if dest_dir is not None:
64
- _, err, code = execute_command(["cp", "-r", cache_path, dest_dir])
65
- if code != 0:
66
- raise InstallationError(f"Failed to copy cache to destination directory: {err}")
67
- return dest_dir
68
- return cache_path
69
-
70
- work_dir = dest_dir and dest_dir.parent # set None if not specified
71
- with working_directory(work_dir, chdir=True) as work_dir:
72
- if dest_dir is None: # use temp dir
73
- repo_path = Path(work_dir) / f"{prefix}-{version}"
74
- logger.debug(f"Working in temporary directory: {work_dir}")
75
- else:
76
- repo_path = dest_dir
77
- logger.debug(f"Working in directory: {work_dir}")
78
- if repo_path.exists(): # remove existing directory
79
- shutil.rmtree(repo_path)
80
- cache_repo_base = cache_dir / url_to_prefix(url) # track the latest repo
81
- # Create new repo
82
- try:
83
- if not (version in get_valid_versions(url, update=False) or version in get_valid_versions(url, update=False)):
84
- raise InstallationError(f"Unknown version: {version}")
85
- Repo.clone_from(str(cache_repo_base), repo_path, branch=version)
86
- except BadName:
87
- raise InstallationError(f"Version {version} not found in repository")
88
- if with_mathlib and repo_name not in ['mathlib', 'mathlib4']:
89
- # update lakefile
90
- lakefile, file_type = detect_lakefile(repo_path)
91
- mathlib_rev = get_tag_sha(MATHLIB_URL, version)
92
- logger.debug(f"Detected {file_type} lakefile at {lakefile}")
93
- if file_type == 'toml':
94
- update_lakefile_toml(lakefile, mathlib_rev)
95
- else:
96
- update_lakefile_lean(lakefile, version)
97
- for cmd in build_cmds:
98
- logger.info(f"Running command: {cmd}")
99
- _, err, code = execute_command(cmd.split(), cwd=repo_path, capture_output=False)
100
- if code != 0:
101
- raise InstallationError(f"Failed to run {cmd}: {err}")
102
-
103
- # Save to cache
104
- if cache_path.exists():
105
- shutil.rmtree(cache_path)
106
- logger.debug(f"Saving installation to cache: {cache_path}")
107
- _, err, code = execute_command(["mv", repo_path, cache_path])
108
- if code != 0:
109
- raise InstallationError(f"Failed to save installation to cache: {err}")
110
- if dest_dir is None:
111
- repo_path = cache_path # return the cache path
112
- logger.info(f"Setup completed successfully in {repo_path}")
113
- return repo_path
114
-
115
- def install_mathlib(version: str,
116
- force: bool = False,
117
- dest_dir: Optional[str] = None):
118
- """Install mathlib4."""
119
- build_cmds = ['lake exe cache get', 'lake build']
120
- return install_lean_repo(MATHLIB_URL, version, prefix='mathlib', force=force, dest_dir=dest_dir, build_cmds=build_cmds)
121
-
122
- # set up REPL
123
- def install_repl( version: str
124
- , force: bool = False
125
- , dest_dir:Optional[Path] = None
126
- , url:Optional[str]=None) -> Path:
127
- """
128
- Install specified version of Lean REPL
129
-
130
- Args:
131
- version: REPL version to install, None for latest
132
- force: Force reinstall if already exists
133
- dest_dir: Destination directory for installation
134
-
135
- Returns:
136
- Path to installed REPL
137
-
138
- Raises:
139
- InstallationError: If installation fails
140
- """
141
- if url is None:
142
- url = 'https://github.com/leanprover-community/repl'
143
- return install_lean_repo(url, version, prefix='repl', force=force, dest_dir=dest_dir, with_mathlib=True)