Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,22 @@ jobs:
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
name: Set up Zig
uses: mlugg/setup-zig@v1
-
name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
-
name: Install zigbuild
run: cargo install cargo-zigbuild
-
name: Install openssl
run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config

-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
12 changes: 3 additions & 9 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
version: 2
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
- id: rust
builder: rust
dir: rust
binary: popcorn-cli
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,25 @@ A command-line interface tool for submitting solutions to the [Popcorn Discord B

### Option 2: Building from source

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

## Usage

Set the `POPCORN_API_URL` environment variable to the URL of the Popcorn API
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).

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.
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.

If you want to re-register (you can do this any number of times), you can run `popcorn-cli reregister [discord|github]`.

After this, you can submit a solution by running:

Then, simply run the binary:
```bash
popcorn-cli <submission-file>
popcorn-cli submit <submission-file>
```

The interactive CLI will guide you through the process of:
1. Selecting a leaderboard
2. Choosing a runner
3. Selecting GPU options
4. Setting submission mode
5. Submitting your work

2. Selecting GPU options
3. Setting submission mode
4. Submitting your work
18 changes: 18 additions & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Cargo
/target/

# Backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# IDEs and editors
/.idea/
/.vscode/
*.swp
*.swo
22 changes: 22 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "popcorn-cli"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.5.3", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "multipart"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ratatui = "0.26.1"
crossterm = "0.27.0"
anyhow = "1.0"
ctrlc = "3.4.6"
dirs = "5.0"
serde_yaml = "0.9"
webbrowser = "0.8"
base64-url = "3.0.0"
urlencoding = "2.1.3"
3 changes: 3 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Popcorn CLI (Rust Version)

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`
8 changes: 8 additions & 0 deletions rust/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
set -e

echo "Building Popcorn CLI (Rust version)..."
cargo build --release

echo "Build complete! Binary is available at: target/release/popcorn-cli"
echo "Run with: ./target/release/popcorn-cli <filepath>"
145 changes: 145 additions & 0 deletions rust/src/cmd/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use anyhow::{anyhow, Result};
use base64_url;
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::fs::{File, OpenOptions};
use std::path::PathBuf;
use urlencoding;
use webbrowser;

use crate::service; // Assuming service::create_client is needed

// Configuration structure
#[derive(Serialize, Deserialize, Debug, Default)]
struct Config {
cli_id: Option<String>,
}

// Helper function to get the config file path
fn get_config_path() -> Result<PathBuf> {
dirs::home_dir()
.map(|mut path| {
path.push(".popcorn.yaml");
path
})
.ok_or_else(|| anyhow!("Could not find home directory"))
}

// Helper function to load config
fn load_config() -> Result<Config> {
let path = get_config_path()?;
if !path.exists() {
return Ok(Config::default());
}
let file = File::open(path)?;
serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e))
}

// Helper function to save config
fn save_config(config: &Config) -> Result<()> {
let path = get_config_path()?;
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true) // Overwrite existing file
.open(path)?;
serde_yaml::to_writer(file, config).map_err(|e| anyhow!("Failed to write config file: {}", e))
}

// Structure for the API response
#[derive(Deserialize)]
struct AuthInitResponse {
state: String, // This is the cli_id
}

// Function to handle the login logic
pub async fn run_auth(reset: bool, auth_provider: &str) -> Result<()> {
println!("Attempting authentication via {}...", auth_provider);

let popcorn_api_url = std::env::var("POPCORN_API_URL")
.map_err(|_| anyhow!("POPCORN_API_URL environment variable not set"))?;

let client = service::create_client(None)?;

let init_url = format!("{}/auth/init?provider={}", popcorn_api_url, auth_provider);
println!("Requesting CLI ID from {}", init_url);

let init_resp = client.get(&init_url).send().await?;

let status = init_resp.status();

if !status.is_success() {
let error_text = init_resp.text().await?;
return Err(anyhow!(
"Failed to initialize auth ({}): {}",
status,
error_text
));
}

let auth_init_data: AuthInitResponse = init_resp.json().await?;
let cli_id = auth_init_data.state;
println!("Received CLI ID: {}", cli_id);

let state_json = serde_json::json!({
"cli_id": cli_id,
"is_reset": reset
})
.to_string();
let state_b64 = base64_url::encode(&state_json);

let auth_url = match auth_provider {
"discord" => {
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";
format!("{}&state={}", base_auth_url, state_b64)
}
"github" => {
let client_id = "Ov23lieFd2onYk4OnKIR";
let redirect_uri = "http://localhost:8000/auth/cli/github";
// URL encode the redirect URI
let encoded_redirect_uri = urlencoding::encode(redirect_uri);
format!(
"https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&state={}",
client_id, encoded_redirect_uri, state_b64
)
}
_ => {
return Err(anyhow!(
"Unsupported authentication provider: {}",
auth_provider
))
}
};

println!(
"\n>>> Please open the following URL in your browser to log in via {}:",
auth_provider
);
println!("{}", auth_url);
println!("\nWaiting for you to complete the authentication in your browser...");
println!(
"After successful authentication with {}, the CLI ID will be saved.",
auth_provider
);

if webbrowser::open(&auth_url).is_err() {
println!(
"Could not automatically open the browser. Please copy the URL above and paste it manually."
);
}

// Save the cli_id to config file optimistically
let mut config = load_config().unwrap_or_default();
config.cli_id = Some(cli_id.clone());
save_config(&config)?;

println!(
"\nSuccessfully initiated authentication. Your CLI ID ({}) has been saved to {}. To use the CLI on different machines, you can copy the config file.",
cli_id,
get_config_path()?.display()
);
println!("You can now use other commands that require authentication.");

Ok(())
}
109 changes: 109 additions & 0 deletions rust/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use dirs;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::fs::File;
use std::path::PathBuf;

mod auth;
mod submit;

#[derive(Serialize, Deserialize, Debug, Default)]
struct Config {
cli_id: Option<String>,
}

fn get_config_path() -> Result<PathBuf> {
dirs::home_dir()
.map(|mut path| {
path.push(".popcorn.yaml");
path
})
.ok_or_else(|| anyhow!("Could not find home directory"))
}

fn load_config() -> Result<Config> {
let path = get_config_path()?;
if !path.exists() {
return Err(anyhow!(
"Config file not found at {}. Please run `popcorn register` first.",
path.display()
));
}
let file = File::open(path)?;
serde_yaml::from_reader(file).map_err(|e| anyhow!("Failed to parse config file: {}", e))
}

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Option<Commands>,

/// Optional: Path to the solution file
filepath: Option<String>,
}

#[derive(Subcommand, Debug)]
enum AuthProvider {
Discord,
Github,
}

#[derive(Subcommand, Debug)]
enum Commands {
Reregister {
#[command(subcommand)]
provider: AuthProvider,
},
Register {
#[command(subcommand)]
provider: AuthProvider,
},
Submit {
filepath: Option<String>,
},
}

pub async fn execute(cli: Cli) -> Result<()> {
match cli.command {
Some(Commands::Reregister { provider }) => {
let provider_str = match provider {
AuthProvider::Discord => "discord",
AuthProvider::Github => "github",
};
auth::run_auth(true, provider_str).await
}
Some(Commands::Register { provider }) => {
let provider_str = match provider {
AuthProvider::Discord => "discord",
AuthProvider::Github => "github",
};
auth::run_auth(false, provider_str).await
}
Some(Commands::Submit { filepath }) => {
let config = load_config()?;
let cli_id = config.cli_id.ok_or_else(|| {
anyhow!(
"cli_id not found in config file ({}). Please run `popcorn register` first.",
get_config_path()
.map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string())
)
})?;
let file_to_submit = filepath.or(cli.filepath);
submit::run_submit_tui(file_to_submit, cli_id).await
}
None => {
let config = load_config()?;
let cli_id = config.cli_id.ok_or_else(|| {
anyhow!(
"cli_id not found in config file ({}). Please run `popcorn register` first.",
get_config_path()
.map_or_else(|_| "unknown path".to_string(), |p| p.display().to_string())
)
})?;
submit::run_submit_tui(cli.filepath, cli_id).await
}
}
}
Loading
Loading