nativ 0.3.0

Nativ CLI — compile .nativ DSL to real SwiftUI and Jetpack Compose
use clap::{Args, Subcommand};
use std::path::{Path, PathBuf};

#[derive(Args)]
pub struct CiArgs {
    #[command(subcommand)]
    pub command: CiCommand,
}

#[derive(Subcommand)]
pub enum CiCommand {
    /// Create a GitHub Actions workflow for a Nativ project
    Init(CiInitArgs),
}

#[derive(Args)]
pub struct CiInitArgs {
    /// Project directory (default: current directory)
    #[arg(short, long, default_value = ".")]
    pub dir: String,

    /// Overwrite an existing workflow file
    #[arg(long)]
    pub force: bool,
}

pub fn run(args: CiArgs) -> Result<(), Box<dyn std::error::Error>> {
    match args.command {
        CiCommand::Init(args) => init(args),
    }
}

fn init(args: CiInitArgs) -> Result<(), Box<dyn std::error::Error>> {
    let project_dir = Path::new(&args.dir);
    if !project_dir.join("nativ.toml").is_file() {
        return Err(format!("nativ.toml not found in {}", project_dir.display()).into());
    }

    let workflow = workflow_path(project_dir);
    if workflow.exists() && !args.force {
        return Err(format!(
            "{} already exists (pass --force to overwrite)",
            workflow.display()
        )
        .into());
    }

    if let Some(parent) = workflow.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&workflow, github_actions_workflow())?;
    println!("CI workflow written to {}", workflow.display());
    Ok(())
}

fn workflow_path(project_dir: &Path) -> PathBuf {
    project_dir
        .join(".github")
        .join("workflows")
        .join("nativ.yml")
}

fn github_actions_workflow() -> &'static str {
    r#"name: Nativ CI

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      submit:
        description: Optional store submission through user-owned Fastlane lanes
        type: choice
        default: none
        options:
          - none
          - ios
          - android

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Check Nativ sources
        run: nativ check --dir .

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Build web preview
        run: nativ build --web --dir .

  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "17"
      - uses: gradle/actions/setup-gradle@v4
        with:
          gradle-version: "8.7"
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Generate Android project
        run: nativ build --android --dir .
      - name: Compile Android debug APK
        working-directory: build/android
        run: gradle :app:assembleDebug

  build-ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Generate iOS project
        run: nativ build --ios --dir .
      - name: Compile iOS simulator app
        run: |
          PROJECT=$(ls build/ios/*.xcodeproj | head -n 1)
          SCHEME=$(basename "$PROJECT" .xcodeproj)
          xcodebuild build \
            -project "$PROJECT" \
            -scheme "$SCHEME" \
            -destination 'generic/platform=iOS Simulator' \
            CODE_SIGNING_ALLOWED=NO

  submit-ios:
    if: ${{ github.event_name == 'workflow_dispatch' && inputs.submit == 'ios' }}
    needs: build-ios
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Install Fastlane
        run: gem install fastlane
      - name: Submit iOS
        run: nativ submit ios --lane beta --dir .

  submit-android:
    if: ${{ github.event_name == 'workflow_dispatch' && inputs.submit == 'android' }}
    needs: build-android
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "17"
      - uses: gradle/actions/setup-gradle@v4
        with:
          gradle-version: "8.7"
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
      - name: Install Nativ
        run: cargo install nativ --locked
      - name: Install Fastlane
        run: gem install fastlane
      - name: Submit Android
        run: nativ submit android --lane internal --dir .
"#
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn workflow_includes_native_build_jobs() {
        let workflow = github_actions_workflow();

        assert!(workflow.contains("nativ check --dir ."));
        assert!(workflow.contains("nativ build --web --dir ."));
        assert!(workflow.contains("nativ build --android --dir ."));
        assert!(workflow.contains("gradle-version: \"8.7\""));
        assert!(workflow.contains("gradle :app:assembleDebug"));
        assert!(workflow.contains("nativ build --ios --dir ."));
        assert!(workflow.contains("xcodebuild build"));
        assert!(workflow.contains("workflow_dispatch"));
        assert!(workflow.contains("nativ submit ios --lane beta --dir ."));
        assert!(workflow.contains("nativ submit android --lane internal --dir ."));
    }

    #[test]
    fn workflow_path_uses_github_actions_default() {
        assert_eq!(
            workflow_path(Path::new("Demo")),
            Path::new("Demo")
                .join(".github")
                .join("workflows")
                .join("nativ.yml")
        );
    }
}