runongpu 0.1.0__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.
runongpu-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MashrafeeAryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: runongpu
3
+ Version: 0.1.0
4
+ Summary: A CLI tool that runs GitHub projects on free cloud GPUs
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: typer
9
+ Requires-Dist: rich
10
+ Requires-Dist: playwright
11
+ Dynamic: license-file
12
+
13
+ # RunOnGPU
14
+
15
+ RunOnGPU is a CLI tool that helps you run GitHub projects on a GPU with minimal setup.
16
+
17
+ It is useful if you want to test CUDA, PyTorch, or other GPU code but do not have a local NVIDIA GPU.
18
+
19
+ ## Requirements
20
+
21
+ * Windows
22
+ * Python 3.10+
23
+ * Git
24
+ * Google Chrome
25
+ * Google account for Colab
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install runongpu
31
+ python -m playwright install
32
+
33
+ Check setup:
34
+
35
+ ```bash
36
+ runongpu doctor
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Go to the project you want to run and initialize RunOnGPU:
42
+
43
+ ```bash
44
+ runongpu init
45
+ ```
46
+
47
+ Enter your GitHub repo URL when asked.
48
+
49
+ This creates a `runongpu.txt` file. Edit this file to tell RunOnGPU how to set up, build, test, and run your project.
50
+
51
+ Then run:
52
+
53
+ ```bash
54
+ runongpu run
55
+ ```
56
+
57
+ RunOnGPU will open Colab, copy the starter notebook, clone your repo, set the runtime to a T4 GPU, and run your commands.
58
+
59
+ ## runongpu.txt
60
+
61
+ `runongpu.txt` controls what happens inside Colab.
62
+
63
+ It has four sections:
64
+
65
+ ```text
66
+ [setup]
67
+ # install dependencies here
68
+
69
+ [build]
70
+ # compile or build the project here
71
+
72
+ [test]
73
+ # run tests here
74
+
75
+ [run]
76
+ # run the final program here
77
+ ```
78
+
79
+ Add one command per line.
80
+
81
+ ## Example: CUDA
82
+
83
+ If your repo has this structure:
84
+
85
+ ```text
86
+ my-cuda-project/
87
+ ├── main.cu
88
+ └── runongpu.txt
89
+ ```
90
+
91
+ Use:
92
+
93
+ ```text
94
+ [setup]
95
+
96
+ [build]
97
+ nvcc main.cu -o vector_add
98
+
99
+ [test]
100
+
101
+ [run]
102
+ ./vector_add
103
+ ```
104
+
105
+ ## Example: CUDA in a Subfolder
106
+
107
+ If your repo has this structure:
108
+
109
+ ```text
110
+ runongpu-examples/
111
+ ├── cuda/
112
+ │ └── vector-add/
113
+ │ └── main.cu
114
+ └── runongpu.txt
115
+ ```
116
+
117
+ Use:
118
+
119
+ ```text
120
+ [setup]
121
+
122
+ [build]
123
+ cd cuda/vector-add && nvcc main.cu -o vector_add
124
+
125
+ [test]
126
+
127
+ [run]
128
+ cd cuda/vector-add && ./vector_add
129
+ ```
130
+
131
+ ## Example: Python
132
+
133
+ ```text
134
+ [setup]
135
+ pip install -r requirements.txt
136
+
137
+ [build]
138
+
139
+ [test]
140
+ pytest
141
+
142
+ [run]
143
+ python main.py
144
+ ```
145
+
146
+ ## Example: CMake
147
+
148
+ ```text
149
+ [setup]
150
+
151
+ [build]
152
+ cmake -S . -B build
153
+ cmake --build build
154
+
155
+ [test]
156
+ ctest --test-dir build --output-on-failure
157
+
158
+ [run]
159
+ ./build/my_program
160
+ ```
161
+
162
+ ## Notes
163
+
164
+ On the first run, you may need to sign into Google Colab. RunOnGPU saves the copied notebook URL and reuses it on future runs.
165
+
166
+ Do not interact with the Colab window while RunOnGPU is setting it up.
167
+
168
+ ## Run Tests
169
+
170
+ ```bash
171
+ python -m pytest
172
+ ```
@@ -0,0 +1,160 @@
1
+ # RunOnGPU
2
+
3
+ RunOnGPU is a CLI tool that helps you run GitHub projects on a GPU with minimal setup.
4
+
5
+ It is useful if you want to test CUDA, PyTorch, or other GPU code but do not have a local NVIDIA GPU.
6
+
7
+ ## Requirements
8
+
9
+ * Windows
10
+ * Python 3.10+
11
+ * Git
12
+ * Google Chrome
13
+ * Google account for Colab
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install runongpu
19
+ python -m playwright install
20
+
21
+ Check setup:
22
+
23
+ ```bash
24
+ runongpu doctor
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ Go to the project you want to run and initialize RunOnGPU:
30
+
31
+ ```bash
32
+ runongpu init
33
+ ```
34
+
35
+ Enter your GitHub repo URL when asked.
36
+
37
+ This creates a `runongpu.txt` file. Edit this file to tell RunOnGPU how to set up, build, test, and run your project.
38
+
39
+ Then run:
40
+
41
+ ```bash
42
+ runongpu run
43
+ ```
44
+
45
+ RunOnGPU will open Colab, copy the starter notebook, clone your repo, set the runtime to a T4 GPU, and run your commands.
46
+
47
+ ## runongpu.txt
48
+
49
+ `runongpu.txt` controls what happens inside Colab.
50
+
51
+ It has four sections:
52
+
53
+ ```text
54
+ [setup]
55
+ # install dependencies here
56
+
57
+ [build]
58
+ # compile or build the project here
59
+
60
+ [test]
61
+ # run tests here
62
+
63
+ [run]
64
+ # run the final program here
65
+ ```
66
+
67
+ Add one command per line.
68
+
69
+ ## Example: CUDA
70
+
71
+ If your repo has this structure:
72
+
73
+ ```text
74
+ my-cuda-project/
75
+ ├── main.cu
76
+ └── runongpu.txt
77
+ ```
78
+
79
+ Use:
80
+
81
+ ```text
82
+ [setup]
83
+
84
+ [build]
85
+ nvcc main.cu -o vector_add
86
+
87
+ [test]
88
+
89
+ [run]
90
+ ./vector_add
91
+ ```
92
+
93
+ ## Example: CUDA in a Subfolder
94
+
95
+ If your repo has this structure:
96
+
97
+ ```text
98
+ runongpu-examples/
99
+ ├── cuda/
100
+ │ └── vector-add/
101
+ │ └── main.cu
102
+ └── runongpu.txt
103
+ ```
104
+
105
+ Use:
106
+
107
+ ```text
108
+ [setup]
109
+
110
+ [build]
111
+ cd cuda/vector-add && nvcc main.cu -o vector_add
112
+
113
+ [test]
114
+
115
+ [run]
116
+ cd cuda/vector-add && ./vector_add
117
+ ```
118
+
119
+ ## Example: Python
120
+
121
+ ```text
122
+ [setup]
123
+ pip install -r requirements.txt
124
+
125
+ [build]
126
+
127
+ [test]
128
+ pytest
129
+
130
+ [run]
131
+ python main.py
132
+ ```
133
+
134
+ ## Example: CMake
135
+
136
+ ```text
137
+ [setup]
138
+
139
+ [build]
140
+ cmake -S . -B build
141
+ cmake --build build
142
+
143
+ [test]
144
+ ctest --test-dir build --output-on-failure
145
+
146
+ [run]
147
+ ./build/my_program
148
+ ```
149
+
150
+ ## Notes
151
+
152
+ On the first run, you may need to sign into Google Colab. RunOnGPU saves the copied notebook URL and reuses it on future runs.
153
+
154
+ Do not interact with the Colab window while RunOnGPU is setting it up.
155
+
156
+ ## Run Tests
157
+
158
+ ```bash
159
+ python -m pytest
160
+ ```
@@ -0,0 +1,32 @@
1
+ # Package metadata used by pip and Python build tools.
2
+ # This section defines what RunOnGPU is, what Python version it supports,
3
+ # and which third-party libraries must be installed with it.
4
+ [project]
5
+ name = "runongpu"
6
+ version = "0.1.0"
7
+ description = "A CLI tool that runs GitHub projects on free cloud GPUs"
8
+ requires-python = ">=3.10"
9
+ readme = "README.md"
10
+
11
+ # Runtime dependencies for the CLI.
12
+ # typer: builds clean terminal commands like `runongpu doctor`
13
+ # rich: prints readable, colored terminal output
14
+ # playwright: controls Chrome so RunOnGPU can automate Google Colab
15
+ dependencies = [
16
+ "typer",
17
+ "rich",
18
+ "playwright"
19
+ ]
20
+
21
+ # Exposes RunOnGPU as a terminal command.
22
+ # After installing the package, users can type `runongpu` in their terminal,
23
+ # and Python will run the Typer app defined in runongpu/cli.py.
24
+ [project.scripts]
25
+ runongpu = "runongpu.cli:app"
26
+
27
+ # Build configuration for modern Python packaging.
28
+ # setuptools packages the project so it can be installed locally during
29
+ # development and later published as a real Python package.
30
+ [build-system]
31
+ requires = ["setuptools"]
32
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,107 @@
1
+ # Builds the command-line interface for RunOnGPU.
2
+ import typer
3
+
4
+ # Prints styled terminal output for a better CLI experience.
5
+ from rich.console import Console
6
+
7
+ # Project helpers for saving local config, creating runongpu.txt, and loading user settings.
8
+ from runongpu.config import create_runongpu_template_file, get_folder_name_from_repo_url, save_notebook_url, save_repo_url, load_config
9
+ from runongpu.colab import open_colab
10
+ from runongpu.parser import parse_config
11
+
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+
17
+ @app.command()
18
+ def doctor():
19
+ # Verifies that the user's local RunOnGPU setup has the required pieces installed.
20
+ console.print("[bold cyan]Checking RunOnGPU setup...[/bold cyan]")
21
+
22
+ console.print("[green]✓ CLI is running[/green]")
23
+ console.print("[green]✓ Typer is installed[/green]")
24
+ console.print("[green]✓ Rich is installed[/green]")
25
+
26
+ saved_config = load_config()
27
+
28
+ if saved_config is None:
29
+ console.print("[yellow]⚠ No repo URL saved yet[/yellow]")
30
+ console.print("[yellow]Run: runongpu init[/yellow]")
31
+ else:
32
+ console.print("[green]✓ Repo URL is saved[/green]")
33
+ console.print(f"[green] Repo URL: {saved_config["repo_url"]}")
34
+
35
+ try:
36
+ import playwright
37
+ console.print("[green]✓ Playwright is installed[/green]")
38
+ except ImportError:
39
+ console.print("[red]✗ Playwright is not installed[/red]")
40
+ console.print("[yellow]Run: pip install playwright[/yellow]")
41
+
42
+
43
+ @app.command()
44
+ def init():
45
+ # Store the GitHub repo that RunOnGPU should clone inside Colab.
46
+ repo_url = typer.prompt("Enter your Github repo URL")
47
+
48
+ console.print("[yellow]Deriving folder name[/yellow]")
49
+ folder_name = get_folder_name_from_repo_url(repo_url)
50
+ console.print("[green]Successfully derived folder name[/green]")
51
+
52
+ save_repo_url(repo_url, folder_name)
53
+
54
+ # Create a starter runongpu.txt so users know where to put setup/build/test/run commands.
55
+ create_runongpu_template_file()
56
+
57
+ console.print("[green]✓ Github repo URL saved. [/green]")
58
+
59
+
60
+ @app.command()
61
+ def config():
62
+ # Show the saved local settings without opening Colab.
63
+ saved_config = load_config()
64
+
65
+ if saved_config is None:
66
+ console.print("[yellow]No repo url found. Run `runongpu init` first.[/yellow]")
67
+ return
68
+
69
+ console.print("[bold cyan]Saved RunOnGPU config:[/bold cyan]")
70
+ console.print(f"GitHub repo URL: [green]{saved_config['repo_url']}[/green]")
71
+
72
+ notebook_url = saved_config.get("notebook_url", "")
73
+
74
+ if notebook_url:
75
+ console.print(f"Colab notebook URL: [green]{notebook_url}[/green]")
76
+ else:
77
+ console.print("Colab notebook URL: [yellow]Not saved yet[/yellow]")
78
+
79
+
80
+ @app.command()
81
+ def run():
82
+ # Main workflow: parse project commands, open Colab, write the notebook cell, and save the notebook URL.
83
+ saved_config = load_config()
84
+
85
+ if saved_config is None:
86
+ console.print("[red]No repo URL saved. Run `runongpu init` first.[/red]")
87
+ return
88
+
89
+ try:
90
+ console.print("[yellow]Parsing runongpu.txt. [/yellow]")
91
+ project_config = parse_config()
92
+ console.print("[green] Successfully parsed through runongpu.txt")
93
+ except ValueError as error:
94
+ console.print(f"[red] runongpu.txt error: [/red]\n {error}")
95
+ return
96
+
97
+ notebook_url = saved_config.get("notebook_url", "")
98
+
99
+ console.print("[bold cyan]Starting RunOnGPU...[/bold cyan]")
100
+ current_notebook_url = open_colab(notebook_url, project_config)
101
+
102
+ # Save the copied Colab notebook so future runs reuse it instead of creating another copy.
103
+ save_notebook_url(current_notebook_url)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ app()
@@ -0,0 +1,199 @@
1
+ import os
2
+ import subprocess
3
+ import time
4
+ from pathlib import Path
5
+ from urllib.error import URLError
6
+ from urllib.request import urlopen
7
+
8
+ from playwright.sync_api import sync_playwright
9
+
10
+ from runongpu.config import load_config
11
+
12
+ from rich.console import Console
13
+
14
+ console = Console()
15
+
16
+ # Path to the real Chrome executable on Windows.
17
+ # RunOnGPU uses real Chrome because Colab/Google login is more reliable there
18
+ # than in Playwright's bundled browser.
19
+ CHROME_EXE = (
20
+ Path(os.environ["PROGRAMFILES"])
21
+ / "Google"
22
+ / "Chrome"
23
+ / "Application"
24
+ / "chrome.exe"
25
+ )
26
+
27
+ # Dedicated Chrome profile for RunOnGPU.
28
+ # This keeps Colab login/session data persistent without touching the user's
29
+ # everyday Chrome profile.
30
+ RUNONGPU_PROFILE_DIR = Path.home() / ".runongpu" / "chrome-profile"
31
+
32
+ # Local Chrome DevTools Protocol port.
33
+ # Playwright connects to this port to control the real Chrome window.
34
+ DEBUG_PORT = 9222
35
+
36
+ # Shared starter notebook used only when the user does not already have a saved
37
+ # RunOnGPU notebook URL.
38
+ TEMPLATE_URL = "https://colab.research.google.com/drive/1pB8iVjR4-tPVSEBFjY8ow6N_F34bcMwi?usp=sharing"
39
+
40
+
41
+ def wait_for_debug_port(timeout_seconds: int = 15) -> None:
42
+ """Wait until Chrome is ready for Playwright to connect."""
43
+ start_time = time.time()
44
+
45
+ while time.time() - start_time < timeout_seconds:
46
+ try:
47
+ # Chrome exposes this local endpoint after remote debugging starts.
48
+ with urlopen(f"http://127.0.0.1:{DEBUG_PORT}/json/version", timeout=1):
49
+ return
50
+ except URLError:
51
+ # Chrome can take a moment to launch, so retry briefly instead of failing immediately.
52
+ time.sleep(0.5)
53
+
54
+
55
+ raise RuntimeError(
56
+ f"Chrome did not open remote debugging port {DEBUG_PORT}. "
57
+ "Close Chrome and try again, or use a different debug port."
58
+ )
59
+
60
+
61
+ def open_colab(notebook_url: str = "", project_config: dict | None = None) -> str:
62
+ """Open a saved Colab notebook, or copy the template notebook on first run."""
63
+
64
+ target_url = notebook_url or TEMPLATE_URL
65
+
66
+ # Launch real Chrome with remote debugging enabled so Playwright can attach.
67
+ # The custom profile lets users sign into Colab once and reuse that session.
68
+ subprocess.Popen([
69
+ str(CHROME_EXE),
70
+ f"--remote-debugging-port={DEBUG_PORT}",
71
+ f"--user-data-dir={RUNONGPU_PROFILE_DIR}",
72
+ "--no-first-run",
73
+ "--no-default-browser-check",
74
+ target_url,
75
+ ])
76
+
77
+ # Avoid connecting before Chrome has opened its debugging endpoint.
78
+ wait_for_debug_port()
79
+
80
+ with sync_playwright() as playwright:
81
+ # Attach to the already-open real Chrome window instead of launching a new browser.
82
+ browser = playwright.chromium.connect_over_cdp(
83
+ f"http://127.0.0.1:{DEBUG_PORT}"
84
+ )
85
+
86
+ # Use the active browser context and newest tab opened by RunOnGPU.
87
+ context = browser.contexts[0]
88
+ page = context.pages[-1]
89
+
90
+ if not notebook_url:
91
+ while True:
92
+ # Saving a copy usually opens a new Colab tab. expect_page captures
93
+ # that tab directly instead of guessing with context.pages[-1].
94
+ with context.expect_page() as new_page_info:
95
+ page.get_by_role("button", name="File", exact=True).click()
96
+ page.get_by_text("Save a copy in Drive").click()
97
+
98
+ # Switch automation to the copied notebook tab.
99
+ page = new_page_info.value
100
+ page.wait_for_load_state("domcontentloaded")
101
+
102
+ copied_successfully = (
103
+ page.url != target_url
104
+ and "accounts.google.com" not in page.url
105
+ )
106
+
107
+ if copied_successfully:
108
+ break
109
+
110
+ input("Please sign into Colab, then press Enter to try again. If already signed in, press enter...")
111
+
112
+ current_url = page.url
113
+
114
+ if project_config is None:
115
+ raise RuntimeError("project_config was not passed into open_colab().")
116
+
117
+ # project_config contains the parsed setup/build/test/run commands from runongpu.txt.
118
+ console.print(f"[cyan]project_config:[/cyan] {project_config}")
119
+ console.print("[cyan]Writing RunOnGPU cell...[/cyan]")
120
+ write_runongpu_cell(page, project_config)
121
+
122
+ console.print("[green]✓ RunOnGPU cell written[/green]")
123
+ set_t4_gpu_and_run_all(page)
124
+
125
+ input("Colab is open. Press Enter when done...")
126
+
127
+ browser.close()
128
+
129
+ return current_url
130
+
131
+
132
+ def write_runongpu_cell(page, project_config: dict):
133
+ saved_config = load_config()
134
+
135
+ # Close popups such as Gemini/Colab assistant before selecting the target cell.
136
+ page.keyboard.press("Escape")
137
+ page.wait_for_timeout(1000)
138
+
139
+ # Target a known marker in the template instead of relying on the first visible editor.
140
+ # This avoids accidentally pasting into Gemini or another popup editor.
141
+ target_cell = page.get_by_text("# Paste your code here:", exact=False).first
142
+ target_cell.click()
143
+
144
+ # Preserve the intended execution order from runongpu.txt:
145
+ # install dependencies, build the project, run tests, then run the program.
146
+ all_commands = (
147
+ project_config["setup"]
148
+ + project_config["build"]
149
+ + project_config["test"]
150
+ + project_config["run"]
151
+ )
152
+
153
+ # Colab shell commands use !, so each parsed command becomes a notebook shell line.
154
+ command_lines = "\n".join(f"!{command}" for command in all_commands)
155
+
156
+ page.keyboard.press("Control+A")
157
+
158
+ folder_name = saved_config["folder_name"]
159
+ repo_url = saved_config["repo_url"]
160
+
161
+ code = f"""
162
+ # Paste your code here:
163
+ folder_name = "{folder_name}"
164
+ github_repo_url = "{repo_url}"
165
+ !rm -rf {folder_name}
166
+ !git clone {{github_repo_url}}
167
+ %cd {{folder_name}}
168
+ {command_lines}
169
+ """
170
+
171
+ # Clipboard paste is faster and more reliable than typing long generated cells.
172
+ page.evaluate("text => navigator.clipboard.writeText(text)", code)
173
+ page.keyboard.press("Control+V")
174
+
175
+
176
+ def set_t4_gpu_and_run_all(page) -> None:
177
+ # Open Colab runtime settings so the notebook uses a GPU runtime.
178
+ console.print("[cyan]Opening Runtime menu...[/cyan]")
179
+ page.get_by_role("button", name="Runtime", exact=True).click()
180
+
181
+ console.print("[cyan]Opening Change runtime type...[/cyan]")
182
+ page.get_by_role("menuitem", name="Change runtime type", exact=True).click()
183
+
184
+ # Select the free T4 GPU option available in Colab.
185
+ console.print("[cyan]Selecting T4 GPU...[/cyan]")
186
+ page.get_by_role("radio", name="T4 GPU").click()
187
+
188
+ console.print("[cyan]Saving runtime settings...[/cyan]")
189
+ page.get_by_role("button", name="Save").click()
190
+
191
+ # Give Colab time to close the runtime dialog before sending keyboard shortcuts.
192
+ console.print("[cyan]Waiting for runtime dialog to close...[/cyan]")
193
+ page.wait_for_timeout(3000)
194
+ page.keyboard.press("Escape")
195
+
196
+ # Start the notebook after the runtime is configured.
197
+ console.print("[cyan]Running all cells...[/cyan]")
198
+ page.keyboard.press("Control+F9")
199
+ console.print("[green]✓ Runtime set and cells started[/green]")
@@ -0,0 +1,111 @@
1
+ # Configuration helpers for RunOnGPU.
2
+ # This file manages small local settings that should persist between CLI runs,
3
+ # such as the GitHub repository URL and the copied Colab notebook URL.
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+ # Store RunOnGPU settings in the user's home directory instead of the project repo.
12
+ # This keeps personal/local state out of Git.
13
+ CONFIG_DIR = Path.home() / ".runongpu"
14
+
15
+ CONFIG_FILE = CONFIG_DIR / "config.json"
16
+
17
+
18
+ def save_repo_url(repo_url: str, folder_name: str) -> None:
19
+ CONFIG_DIR.mkdir(exist_ok=True)
20
+ config = {
21
+ "repo_url": repo_url,
22
+ "notebook_url": "",
23
+ "folder_name": folder_name
24
+ }
25
+
26
+ # Persist the selected project so future `runongpu run` calls know what to clone.
27
+ with open(CONFIG_FILE, "w", encoding="utf-8") as file:
28
+ json.dump(config, file, indent=4)
29
+
30
+
31
+ def load_config() -> dict | None:
32
+ # Return None when the user has not run `runongpu init` yet.
33
+ if not CONFIG_FILE.exists():
34
+ return None
35
+
36
+ # Convert the saved JSON config back into a Python dictionary.
37
+ with open(CONFIG_FILE, "r", encoding="utf-8") as file:
38
+ return json.load(file)
39
+
40
+
41
+ def save_notebook_url(notebook_url: str) -> None:
42
+ # Keep the copied Colab notebook URL so RunOnGPU reuses it instead of
43
+ # creating a new notebook copy every time.
44
+ config = load_config()
45
+
46
+ if config is None:
47
+ config = {}
48
+
49
+ config["notebook_url"] = notebook_url
50
+
51
+ with open(CONFIG_FILE, "w", encoding="utf-8") as file:
52
+ json.dump(config, file, indent=4)
53
+
54
+
55
+ def create_runongpu_template_file() -> None:
56
+ # Create a starter runongpu.txt in the current project folder.
57
+ # The file documents the command sections that RunOnGPU later parses.
58
+ txt_path = Path("runongpu.txt")
59
+
60
+ if not txt_path.exists():
61
+
62
+ txt_path.write_text(
63
+ """# RunOnGPU Configuration
64
+ #
65
+ # Add one command per line under each section.
66
+ # Empty sections are allowed.
67
+ #
68
+ # Example Python project:
69
+ #
70
+ # [setup]
71
+ # pip install -r requirements.txt
72
+ #
73
+ # [run]
74
+ # python main.py
75
+ #
76
+ # Example CUDA project:
77
+ #
78
+ # [build]
79
+ # nvcc main.cu -o main
80
+ #
81
+ # [run]
82
+ # ./main
83
+ #
84
+ # Example CMake project:
85
+ #
86
+ # [build]
87
+ # cmake -S . -B build
88
+ # cmake --build build
89
+ #
90
+ # [run]
91
+ # ./build/my_program
92
+
93
+ [setup]
94
+
95
+ [build]
96
+
97
+ [test]
98
+
99
+ [run]
100
+ """,
101
+ encoding="utf-8",
102
+ )
103
+ console.print("[green]✓ Added runongpu.txt to the repo. [/green]")
104
+ else:
105
+ console.print("[yellow]runongpu.txt already exists. Good job! Proud of you! [/yellow]")
106
+
107
+
108
+ def get_folder_name_from_repo_url(repo_url: str) -> str:
109
+ # Git clones repositories into a folder named after the repo, so derive that
110
+ # folder name from the URL instead of asking the user to type it manually.
111
+ return repo_url.rstrip("/").split("/")[-1].replace(".git", "")
@@ -0,0 +1,46 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def parse_config() -> dict:
5
+ # Each section maps to the list of commands RunOnGPU should run in Colab.
6
+ config = {
7
+ "setup": [],
8
+ "build": [],
9
+ "test": [],
10
+ "run": [],
11
+ }
12
+
13
+ current_section = None
14
+
15
+ # Path("runongpu.txt") looks for the file in the current terminal directory.
16
+ # Users should run RunOnGPU from the project folder that contains runongpu.txt.
17
+ txt_path = Path("runongpu.txt")
18
+
19
+ for line in txt_path.read_text(encoding="utf-8").splitlines():
20
+ line = line.strip()
21
+
22
+ # Ignore spacing and documentation inside runongpu.txt.
23
+ if not line:
24
+ continue
25
+
26
+ if line.startswith("#"):
27
+ continue
28
+
29
+ # Section headers decide where the following commands should be stored.
30
+ # Example: [build] makes current_section equal to "build".
31
+ if line.startswith("[") and line.endswith("]"):
32
+ section = line[1:-1].lower()
33
+
34
+ if section not in config:
35
+ raise ValueError(f"Unknown section: {section}")
36
+
37
+ current_section = section
38
+ continue
39
+
40
+ # Commands must appear under a valid section so RunOnGPU knows when to run them.
41
+ if current_section is None:
42
+ raise ValueError("Command found before any section header.")
43
+
44
+ config[current_section].append(line)
45
+
46
+ return config
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: runongpu
3
+ Version: 0.1.0
4
+ Summary: A CLI tool that runs GitHub projects on free cloud GPUs
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: typer
9
+ Requires-Dist: rich
10
+ Requires-Dist: playwright
11
+ Dynamic: license-file
12
+
13
+ # RunOnGPU
14
+
15
+ RunOnGPU is a CLI tool that helps you run GitHub projects on a GPU with minimal setup.
16
+
17
+ It is useful if you want to test CUDA, PyTorch, or other GPU code but do not have a local NVIDIA GPU.
18
+
19
+ ## Requirements
20
+
21
+ * Windows
22
+ * Python 3.10+
23
+ * Git
24
+ * Google Chrome
25
+ * Google account for Colab
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install runongpu
31
+ python -m playwright install
32
+
33
+ Check setup:
34
+
35
+ ```bash
36
+ runongpu doctor
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ Go to the project you want to run and initialize RunOnGPU:
42
+
43
+ ```bash
44
+ runongpu init
45
+ ```
46
+
47
+ Enter your GitHub repo URL when asked.
48
+
49
+ This creates a `runongpu.txt` file. Edit this file to tell RunOnGPU how to set up, build, test, and run your project.
50
+
51
+ Then run:
52
+
53
+ ```bash
54
+ runongpu run
55
+ ```
56
+
57
+ RunOnGPU will open Colab, copy the starter notebook, clone your repo, set the runtime to a T4 GPU, and run your commands.
58
+
59
+ ## runongpu.txt
60
+
61
+ `runongpu.txt` controls what happens inside Colab.
62
+
63
+ It has four sections:
64
+
65
+ ```text
66
+ [setup]
67
+ # install dependencies here
68
+
69
+ [build]
70
+ # compile or build the project here
71
+
72
+ [test]
73
+ # run tests here
74
+
75
+ [run]
76
+ # run the final program here
77
+ ```
78
+
79
+ Add one command per line.
80
+
81
+ ## Example: CUDA
82
+
83
+ If your repo has this structure:
84
+
85
+ ```text
86
+ my-cuda-project/
87
+ ├── main.cu
88
+ └── runongpu.txt
89
+ ```
90
+
91
+ Use:
92
+
93
+ ```text
94
+ [setup]
95
+
96
+ [build]
97
+ nvcc main.cu -o vector_add
98
+
99
+ [test]
100
+
101
+ [run]
102
+ ./vector_add
103
+ ```
104
+
105
+ ## Example: CUDA in a Subfolder
106
+
107
+ If your repo has this structure:
108
+
109
+ ```text
110
+ runongpu-examples/
111
+ ├── cuda/
112
+ │ └── vector-add/
113
+ │ └── main.cu
114
+ └── runongpu.txt
115
+ ```
116
+
117
+ Use:
118
+
119
+ ```text
120
+ [setup]
121
+
122
+ [build]
123
+ cd cuda/vector-add && nvcc main.cu -o vector_add
124
+
125
+ [test]
126
+
127
+ [run]
128
+ cd cuda/vector-add && ./vector_add
129
+ ```
130
+
131
+ ## Example: Python
132
+
133
+ ```text
134
+ [setup]
135
+ pip install -r requirements.txt
136
+
137
+ [build]
138
+
139
+ [test]
140
+ pytest
141
+
142
+ [run]
143
+ python main.py
144
+ ```
145
+
146
+ ## Example: CMake
147
+
148
+ ```text
149
+ [setup]
150
+
151
+ [build]
152
+ cmake -S . -B build
153
+ cmake --build build
154
+
155
+ [test]
156
+ ctest --test-dir build --output-on-failure
157
+
158
+ [run]
159
+ ./build/my_program
160
+ ```
161
+
162
+ ## Notes
163
+
164
+ On the first run, you may need to sign into Google Colab. RunOnGPU saves the copied notebook URL and reuses it on future runs.
165
+
166
+ Do not interact with the Colab window while RunOnGPU is setting it up.
167
+
168
+ ## Run Tests
169
+
170
+ ```bash
171
+ python -m pytest
172
+ ```
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ runongpu/cli.py
5
+ runongpu/colab.py
6
+ runongpu/config.py
7
+ runongpu/parser.py
8
+ runongpu.egg-info/PKG-INFO
9
+ runongpu.egg-info/SOURCES.txt
10
+ runongpu.egg-info/dependency_links.txt
11
+ runongpu.egg-info/entry_points.txt
12
+ runongpu.egg-info/requires.txt
13
+ runongpu.egg-info/top_level.txt
14
+ tests/test_config.py
15
+ tests/test_config_persistance.py
16
+ tests/test_parser.py
17
+ tests/test_template.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ runongpu = runongpu.cli:app
@@ -0,0 +1,3 @@
1
+ typer
2
+ rich
3
+ playwright
@@ -0,0 +1 @@
1
+ runongpu
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ from runongpu.config import get_folder_name_from_repo_url
2
+
3
+
4
+ def test_get_folder_name_from_https_repo_url():
5
+ # Git clones this URL into a local folder named "RunOnGPU".
6
+ result = get_folder_name_from_repo_url(
7
+ "https://github.com/MashrafeeAryan/RunOnGPU.git"
8
+ )
9
+
10
+ assert result == "RunOnGPU"
11
+
12
+
13
+ def test_get_folder_name_from_url_without_git_suffix():
14
+ # GitHub URLs may be copied without ".git", so both formats should work.
15
+ result = get_folder_name_from_repo_url(
16
+ "https://github.com/MashrafeeAryan/runongpu-examples"
17
+ )
18
+
19
+ assert result == "runongpu-examples"
20
+
21
+
22
+ def test_get_folder_name_from_url_with_trailing_slash():
23
+ # Trailing slashes should not create an empty folder name.
24
+ result = get_folder_name_from_repo_url(
25
+ "https://github.com/MashrafeeAryan/runongpu-examples/"
26
+ )
27
+
28
+ assert result == "runongpu-examples"
@@ -0,0 +1,43 @@
1
+ import runongpu.config as config_module
2
+
3
+
4
+ def test_save_and_load_repo_config(tmp_path, monkeypatch):
5
+ # Store config in a temp folder so this test never touches the user's real ~/.runongpu.
6
+ fake_config_dir = tmp_path / ".runongpu"
7
+ fake_config_file = fake_config_dir / "config.json"
8
+
9
+ monkeypatch.setattr(config_module, "CONFIG_DIR", fake_config_dir)
10
+ monkeypatch.setattr(config_module, "CONFIG_FILE", fake_config_file)
11
+
12
+ config_module.save_repo_url(
13
+ "https://github.com/MashrafeeAryan/RunOnGPU.git",
14
+ "RunOnGPU",
15
+ )
16
+
17
+ result = config_module.load_config()
18
+
19
+ assert result["repo_url"] == "https://github.com/MashrafeeAryan/RunOnGPU.git"
20
+ assert result["folder_name"] == "RunOnGPU"
21
+ assert result["notebook_url"] == ""
22
+
23
+
24
+ def test_save_notebook_url_updates_existing_config(tmp_path, monkeypatch):
25
+ # Notebook URL should be saved so future runs reuse the same Colab notebook.
26
+ fake_config_dir = tmp_path / ".runongpu"
27
+ fake_config_file = fake_config_dir / "config.json"
28
+
29
+ monkeypatch.setattr(config_module, "CONFIG_DIR", fake_config_dir)
30
+ monkeypatch.setattr(config_module, "CONFIG_FILE", fake_config_file)
31
+
32
+ config_module.save_repo_url(
33
+ "https://github.com/MashrafeeAryan/RunOnGPU.git",
34
+ "RunOnGPU",
35
+ )
36
+
37
+ config_module.save_notebook_url("https://colab.research.google.com/drive/test")
38
+
39
+ result = config_module.load_config()
40
+
41
+ assert result["repo_url"] == "https://github.com/MashrafeeAryan/RunOnGPU.git"
42
+ assert result["folder_name"] == "RunOnGPU"
43
+ assert result["notebook_url"] == "https://colab.research.google.com/drive/test"
@@ -0,0 +1,95 @@
1
+ import pytest
2
+
3
+ from runongpu.parser import parse_config
4
+
5
+
6
+ def test_parse_valid_config(tmp_path, monkeypatch):
7
+ config_file = tmp_path / "runongpu.txt"
8
+ config_file.write_text(
9
+ """
10
+ # RunOnGPU config
11
+
12
+ [setup]
13
+ pip install -r requirements.txt
14
+
15
+ [build]
16
+ nvcc main.cu -o vector_add
17
+
18
+ [test]
19
+ pytest
20
+
21
+ [run]
22
+ ./vector_add
23
+ """,
24
+ encoding="utf-8",
25
+ )
26
+
27
+ monkeypatch.chdir(tmp_path)
28
+
29
+ result = parse_config()
30
+
31
+ assert result == {
32
+ "setup": ["pip install -r requirements.txt"],
33
+ "build": ["nvcc main.cu -o vector_add"],
34
+ "test": ["pytest"],
35
+ "run": ["./vector_add"],
36
+ }
37
+
38
+
39
+ def test_parse_empty_sections(tmp_path, monkeypatch):
40
+ config_file = tmp_path / "runongpu.txt"
41
+ config_file.write_text(
42
+ """
43
+ [setup]
44
+
45
+ [build]
46
+
47
+ [test]
48
+
49
+ [run]
50
+ """,
51
+ encoding="utf-8",
52
+ )
53
+
54
+ monkeypatch.chdir(tmp_path)
55
+
56
+ assert parse_config() == {
57
+ "setup": [],
58
+ "build": [],
59
+ "test": [],
60
+ "run": [],
61
+ }
62
+
63
+
64
+ def test_unknown_section_raises_error(tmp_path, monkeypatch):
65
+ config_file = tmp_path / "runongpu.txt"
66
+ config_file.write_text(
67
+ """
68
+ [install]
69
+ pip install numpy
70
+ """,
71
+ encoding="utf-8",
72
+ )
73
+
74
+ monkeypatch.chdir(tmp_path)
75
+
76
+ with pytest.raises(ValueError, match="Unknown section"):
77
+ parse_config()
78
+
79
+
80
+ def test_command_before_section_raises_error(tmp_path, monkeypatch):
81
+ config_file = tmp_path / "runongpu.txt"
82
+ config_file.write_text(
83
+ """
84
+ pip install numpy
85
+
86
+ [run]
87
+ python main.py
88
+ """,
89
+ encoding="utf-8",
90
+ )
91
+
92
+ monkeypatch.chdir(tmp_path)
93
+
94
+ with pytest.raises(ValueError, match="Command found before any section header"):
95
+ parse_config()
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ from runongpu.config import create_runongpu_template_file
4
+
5
+
6
+ def test_create_runongpu_template_file(tmp_path, monkeypatch):
7
+ # Run the test inside a temporary folder so we do not touch the real repo.
8
+ monkeypatch.chdir(tmp_path)
9
+
10
+ create_runongpu_template_file()
11
+
12
+ template_file = Path("runongpu.txt")
13
+
14
+ assert template_file.exists()
15
+
16
+ contents = template_file.read_text(encoding="utf-8")
17
+
18
+ # The generated template should include all sections the parser understands.
19
+ assert "[setup]" in contents
20
+ assert "[build]" in contents
21
+ assert "[test]" in contents
22
+ assert "[run]" in contents
23
+
24
+
25
+ def test_create_runongpu_template_file_does_not_overwrite_existing_file(tmp_path, monkeypatch):
26
+ # Existing user configs should be preserved instead of overwritten.
27
+ monkeypatch.chdir(tmp_path)
28
+
29
+ template_file = Path("runongpu.txt")
30
+ template_file.write_text("[run]\npython main.py\n", encoding="utf-8")
31
+
32
+ create_runongpu_template_file()
33
+
34
+ assert template_file.read_text(encoding="utf-8") == "[run]\npython main.py\n"