Skip to content

Commit 2df243d

Browse files
authored
Merge pull request #5 from gpu-mode/rust
Rust port
2 parents b86af3e + 3205bea commit 2df243d

File tree

14 files changed

+1348
-26
lines changed

14 files changed

+1348
-26
lines changed

.github/workflows/build.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,22 @@ jobs:
2222
with:
2323
fetch-depth: 0
2424
-
25-
name: Set up Go
26-
uses: actions/setup-go@v5
25+
name: Set up Zig
26+
uses: mlugg/setup-zig@v1
27+
-
28+
name: Set up Rust
29+
uses: actions-rs/toolchain@v1
30+
with:
31+
toolchain: stable
32+
profile: minimal
33+
override: true
34+
-
35+
name: Install zigbuild
36+
run: cargo install cargo-zigbuild
37+
-
38+
name: Install openssl
39+
run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config
40+
2741
-
2842
name: Run GoReleaser
2943
uses: goreleaser/goreleaser-action@v6

.goreleaser.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
version: 2
22
builds:
3-
- env:
4-
- CGO_ENABLED=0
5-
goos:
6-
- linux
7-
- windows
8-
- darwin
9-
goarch:
10-
- amd64
11-
- arm64
3+
- id: rust
4+
builder: rust
5+
dir: rust
126
binary: popcorn-cli

README.md

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,25 @@ A command-line interface tool for submitting solutions to the [Popcorn Discord B
1212

1313
### Option 2: Building from source
1414

15-
If you want to build from source, you'll need:
16-
1. Install [Go](https://golang.org/doc/install)
17-
2. Run:
18-
```bash
19-
GOPROXY=direct go install github.com/s1ro1/popcorn-cli@latest
20-
```
21-
3. Make sure the `popcorn-cli` binary is in your PATH
15+
This app is written in Rust, so you can just install it via `cargo install`
2216

2317
## Usage
2418

25-
Set the `POPCORN_API_URL` environment variable to the URL of the Popcorn API
19+
Set the `POPCORN_API_URL` environment variable to the URL of the Popcorn API. You can get this from the [GPU Mode Discord server](https://discord.gg/gpumode).
20+
21+
Then, you need to be registered to use this app. You can register by running: `popcorn-cli register [discord|github]`. We strongly reccomend using your Discord account to register, as this will match your submissions to your Discord account.
22+
Once you're registered, there is a file created in your `$HOME` called `.popcorn-cli.yaml` that contains your registration token. This token is sent with each request.
23+
24+
If you want to re-register (you can do this any number of times), you can run `popcorn-cli reregister [discord|github]`.
25+
26+
After this, you can submit a solution by running:
2627

27-
Then, simply run the binary:
2828
```bash
29-
popcorn-cli <submission-file>
29+
popcorn-cli submit <submission-file>
3030
```
3131

3232
The interactive CLI will guide you through the process of:
3333
1. Selecting a leaderboard
34-
2. Choosing a runner
35-
3. Selecting GPU options
36-
4. Setting submission mode
37-
5. Submitting your work
38-
34+
2. Selecting GPU options
35+
3. Setting submission mode
36+
4. Submitting your work

rust/.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Cargo
2+
/target/
3+
4+
# Backup files generated by rustfmt
5+
**/*.rs.bk
6+
7+
# MSVC Windows builds of rustc generate these, which store debugging information
8+
*.pdb
9+
10+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
11+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
12+
Cargo.lock
13+
14+
# IDEs and editors
15+
/.idea/
16+
/.vscode/
17+
*.swp
18+
*.swo

rust/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "popcorn-cli"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
clap = { version = "4.5.3", features = ["derive"] }
10+
reqwest = { version = "0.11", features = ["json", "multipart"] }
11+
tokio = { version = "1", features = ["full"] }
12+
serde = { version = "1.0", features = ["derive"] }
13+
serde_json = "1.0"
14+
ratatui = "0.26.1"
15+
crossterm = "0.27.0"
16+
anyhow = "1.0"
17+
ctrlc = "3.4.6"
18+
dirs = "5.0"
19+
serde_yaml = "0.9"
20+
webbrowser = "0.8"
21+
base64-url = "3.0.0"
22+
urlencoding = "2.1.3"

rust/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Popcorn CLI (Rust Version)
2+
3+
Run `./build.sh` and then ` POPCORN_API_URL="http://127.0.0.1:8000" target/release/popcorn-cli ../../discord/discord-cluster-manager/reference-kernels/problems/pmpp/grayscale_py/submission.py`

rust/build.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo "Building Popcorn CLI (Rust version)..."
5+
cargo build --release
6+
7+
echo "Build complete! Binary is available at: target/release/popcorn-cli"
8+
echo "Run with: ./target/release/popcorn-cli <filepath>"

rust/src/cmd/auth.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use anyhow::{anyhow, Result};
2+
use base64_url;
3+
use dirs;
4+
use serde::{Deserialize, Serialize};
5+
use serde_yaml;
6+
use std::fs::{File, OpenOptions};
7+
use std::path::PathBuf;
8+
use urlencoding;
9+
use webbrowser;
10+
11+
use crate::service; // Assuming service::create_client is needed
12+
13+
// Configuration structure
14+
#[derive(Serialize, Deserialize, Debug, Default)]
15+
struct Config {
16+
cli_id: Option<String>,
17+
}
18+
19+
// Helper function to get the config file path
20+
fn get_config_path() -> Result<PathBuf> {
21+
dirs::home_dir()
22+
.map(|mut path| {
23+
path.push(".popcorn.yaml");
24+
path
25+
})
26+
.ok_or_else(|| anyhow!("Could not find home directory"))
27+
}
28+
29+
// Helper function to load config
30+
fn load_config() -> Result<Config> {
31+
let path = get_config_path()?;
32+
if !path.exists() {
33+
return Ok(Config::default());
34+
}
35+
let file = File::open(path)?;
36+
serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e))
37+
}
38+
39+
// Helper function to save config
40+
fn save_config(config: &Config) -> Result<()> {
41+
let path = get_config_path()?;
42+
let file = OpenOptions::new()
43+
.write(true)
44+
.create(true)
45+
.truncate(true) // Overwrite existing file
46+
.open(path)?;
47+
serde_yaml::to_writer(file, config).map_err(|e| anyhow!("Failed to write config file: {}", e))
48+
}
49+
50+
// Structure for the API response
51+
#[derive(Deserialize)]
52+
struct AuthInitResponse {
53+
state: String, // This is the cli_id
54+
}
55+
56+
// Function to handle the login logic
57+
pub async fn run_auth(reset: bool, auth_provider: &str) -> Result<()> {
58+
println!("Attempting authentication via {}...", auth_provider);
59+
60+
let popcorn_api_url = std::env::var("POPCORN_API_URL")
61+
.map_err(|_| anyhow!("POPCORN_API_URL environment variable not set"))?;
62+
63+
let client = service::create_client(None)?;
64+
65+
let init_url = format!("{}/auth/init?provider={}", popcorn_api_url, auth_provider);
66+
println!("Requesting CLI ID from {}", init_url);
67+
68+
let init_resp = client.get(&init_url).send().await?;
69+
70+
let status = init_resp.status();
71+
72+
if !status.is_success() {
73+
let error_text = init_resp.text().await?;
74+
return Err(anyhow!(
75+
"Failed to initialize auth ({}): {}",
76+
status,
77+
error_text
78+
));
79+
}
80+
81+
let auth_init_data: AuthInitResponse = init_resp.json().await?;
82+
let cli_id = auth_init_data.state;
83+
println!("Received CLI ID: {}", cli_id);
84+
85+
let state_json = serde_json::json!({
86+
"cli_id": cli_id,
87+
"is_reset": reset
88+
})
89+
.to_string();
90+
let state_b64 = base64_url::encode(&state_json);
91+
92+
let auth_url = match auth_provider {
93+
"discord" => {
94+
let base_auth_url = "https://discord.com/oauth2/authorize?client_id=1357446383497511096&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauth%2Fcli%2Fdiscord&scope=identify";
95+
format!("{}&state={}", base_auth_url, state_b64)
96+
}
97+
"github" => {
98+
let client_id = "Ov23lieFd2onYk4OnKIR";
99+
let redirect_uri = "http://localhost:8000/auth/cli/github";
100+
// URL encode the redirect URI
101+
let encoded_redirect_uri = urlencoding::encode(redirect_uri);
102+
format!(
103+
"https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&state={}",
104+
client_id, encoded_redirect_uri, state_b64
105+
)
106+
}
107+
_ => {
108+
return Err(anyhow!(
109+
"Unsupported authentication provider: {}",
110+
auth_provider
111+
))
112+
}
113+
};
114+
115+
println!(
116+
"\n>>> Please open the following URL in your browser to log in via {}:",
117+
auth_provider
118+
);
119+
println!("{}", auth_url);
120+
println!("\nWaiting for you to complete the authentication in your browser...");
121+
println!(
122+
"After successful authentication with {}, the CLI ID will be saved.",
123+
auth_provider
124+
);
125+
126+
if webbrowser::open(&auth_url).is_err() {
127+
println!(
128+
"Could not automatically open the browser. Please copy the URL above and paste it manually."
129+
);
130+
}
131+
132+
// Save the cli_id to config file optimistically
133+
let mut config = load_config().unwrap_or_default();
134+
config.cli_id = Some(cli_id.clone());
135+
save_config(&config)?;
136+
137+
println!(
138+
"\nSuccessfully initiated authentication. Your CLI ID ({}) has been saved to {}. To use the CLI on different machines, you can copy the config file.",
139+
cli_id,
140+
get_config_path()?.display()
141+
);
142+
println!("You can now use other commands that require authentication.");
143+
144+
Ok(())
145+
}

rust/src/cmd/mod.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::{Parser, Subcommand};
3+
use dirs;
4+
use serde::{Deserialize, Serialize};
5+
use serde_yaml;
6+
use std::fs::File;
7+
use std::path::PathBuf;
8+
9+
mod auth;
10+
mod submit;
11+
12+
#[derive(Serialize, Deserialize, Debug, Default)]
13+
struct Config {
14+
cli_id: Option<String>,
15+
}
16+
17+
fn get_config_path() -> Result<PathBuf> {
18+
dirs::home_dir()
19+
.map(|mut path| {
20+
path.push(".popcorn.yaml");
21+
path
22+
})
23+
.ok_or_else(|| anyhow!("Could not find home directory"))
24+
}
25+
26+
fn load_config() -> Result<Config> {
27+
let path = get_config_path()?;
28+
if !path.exists() {
29+
return Err(anyhow!(
30+
"Config file not found at {}. Please run `popcorn register` first.",
31+
path.display()
32+
));
33+
}
34+
let file = File::open(path)?;
35+
serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e))
36+
}
37+
38+
#[derive(Parser, Debug)]
39+
#[command(author, version, about, long_about = None)]
40+
pub struct Cli {
41+
#[command(subcommand)]
42+
command: Option<Commands>,
43+
44+
/// Optional: Path to the solution file
45+
filepath: Option<String>,
46+
}
47+
48+
#[derive(Subcommand, Debug)]
49+
enum AuthProvider {
50+
Discord,
51+
Github,
52+
}
53+
54+
#[derive(Subcommand, Debug)]
55+
enum Commands {
56+
Reregister {
57+
#[command(subcommand)]
58+
provider: AuthProvider,
59+
},
60+
Register {
61+
#[command(subcommand)]
62+
provider: AuthProvider,
63+
},
64+
Submit {
65+
filepath: Option<String>,
66+
},
67+
}
68+
69+
pub async fn execute(cli: Cli) -> Result<()> {
70+
match cli.command {
71+
Some(Commands::Reregister { provider }) => {
72+
let provider_str = match provider {
73+
AuthProvider::Discord => "discord",
74+
AuthProvider::Github => "github",
75+
};
76+
auth::run_auth(true, provider_str).await
77+
}
78+
Some(Commands::Register { provider }) => {
79+
let provider_str = match provider {
80+
AuthProvider::Discord => "discord",
81+
AuthProvider::Github => "github",
82+
};
83+
auth::run_auth(false, provider_str).await
84+
}
85+
Some(Commands::Submit { filepath }) => {
86+
let config = load_config()?;
87+
let cli_id = config.cli_id.ok_or_else(|| {
88+
anyhow!(
89+
"cli_id not found in config file ({}). Please run `popcorn register` first.",
90+
get_config_path()
91+
.map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string())
92+
)
93+
})?;
94+
let file_to_submit = filepath.or(cli.filepath);
95+
submit::run_submit_tui(file_to_submit, cli_id).await
96+
}
97+
None => {
98+
let config = load_config()?;
99+
let cli_id = config.cli_id.ok_or_else(|| {
100+
anyhow!(
101+
"cli_id not found in config file ({}). Please run `popcorn register` first.",
102+
get_config_path()
103+
.map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string())
104+
)
105+
})?;
106+
submit::run_submit_tui(cli.filepath, cli_id).await
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)