From 99090856712abf381207039a23af216b63651f2f Mon Sep 17 00:00:00 2001 From: Lukas-Heiligenbrunner <30468956+Lukas-Heiligenbrunner@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:19:59 +0200 Subject: [PATCH] Multi platform builds (#74) * test arm64 build * add seperate dockerfile form arm64 add to build jobs * builds for arm64 target * overwrite qemu binfmt config on start * seperate action jobs for two archs add sidebar on package overview to show lots of aur stuff link to aur homepage of package setup code for multiplatform builds * add real description from aur * add database migration to store platforms and build flags * use tags selected in addpackage view properly show right archs and build flags on sidepanel * supported platforms just arm64/32 bc. of docker archlinux availability limitations * new endpoint for retrying builds fix updateing pkgs by updating all platforms * new patch endpoint to update package finish pkg settings page * split to seperate repos per architecture fix names of platforms to same names as pacman $arch try to move old files to new dir while migrating --- .github/workflows/docker-build.yml | 22 +- .github/workflows/docker-builder-publish.yml | 44 ++ .github/workflows/docker-publish.yml | 43 +- backend/Cargo.lock | 63 ++- backend/Cargo.toml | 5 +- backend/src/api/backend.rs | 2 + backend/src/api/build.rs | 55 ++- backend/src/api/package.rs | 110 ++++- backend/src/api/types/input.rs | 36 +- backend/src/api/types/output.rs | 2 + backend/src/aur/api.rs | 3 + backend/src/builder/build.rs | 263 ++++++------ backend/src/builder/cancel.rs | 58 +++ backend/src/builder/init.rs | 16 +- backend/src/builder/mod.rs | 1 + backend/src/builder/queue.rs | 80 +++- backend/src/db/builds.rs | 1 + backend/src/db/files.rs | 1 + .../m20240907_131839_platform_buildflags.rs | 141 +++++++ backend/src/db/migration/mod.rs | 6 +- backend/src/db/packages.rs | 4 +- backend/src/package/add.rs | 86 ++-- backend/src/package/update.rs | 53 ++- backend/src/pacman-repo-utils/Cargo.toml | 7 +- backend/src/pacman-repo-utils/src/repo_add.rs | 48 ++- .../src/pacman-repo-utils/src/repo_init.rs | 2 +- backend/src/repo/mod.rs | 1 + backend/src/repo/platforms.rs | 1 + backend/src/repo/utils.rs | 19 +- backend/src/scheduler/aur_version_update.rs | 2 +- backend/src/utils/logger.rs | 5 +- backend/src/utils/startup.rs | 57 ++- docker-compose.yaml | 15 + Dockerfile => docker/Dockerfile | 16 +- docker/add-aur.sh | 66 +++ docker/build-rust.sh | 19 + docker/builder.Dockerfile | 4 + entrypoint.sh => docker/entrypoint.sh | 0 docs/docs/setup/pacman-repo.md | 2 +- frontend/lib/api/builds.dart | 5 + frontend/lib/api/packages.dart | 48 ++- .../lib/components/add_package_popup.dart | 105 +++++ frontend/lib/components/api/APIBuilder.dart | 3 + frontend/lib/components/aur_search_table.dart | 11 +- .../lib/components/build_flag_settings.dart | 87 ++++ frontend/lib/components/builds_table.dart | 4 + .../components/dashboard/your_packages.dart | 4 +- frontend/lib/components/packages_table.dart | 6 +- .../lib/components/platform_settings.dart | 69 ++++ frontend/lib/components/routing/router.dart | 8 + frontend/lib/constants/platforms.dart | 1 + frontend/lib/models/build.dart | 4 +- frontend/lib/models/extended_package.dart | 55 +++ frontend/lib/models/package.dart | 26 -- frontend/lib/models/simple_packge.dart | 25 ++ .../lib/providers/api/package_provider.dart | 5 +- .../lib/providers/api/packages_provider.dart | 4 +- .../lib/screens/Package_settings_screen.dart | 86 ++++ frontend/lib/screens/build_screen.dart | 2 +- frontend/lib/screens/package_screen.dart | 377 +++++++++++++----- frontend/lib/screens/packages_screen.dart | 5 +- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + frontend/pubspec.lock | 74 +++- frontend/pubspec.yaml | 2 + 65 files changed, 1938 insertions(+), 442 deletions(-) create mode 100644 .github/workflows/docker-builder-publish.yml create mode 100644 backend/src/builder/cancel.rs create mode 100644 backend/src/db/migration/m20240907_131839_platform_buildflags.rs create mode 100644 backend/src/repo/platforms.rs create mode 100644 docker-compose.yaml rename Dockerfile => docker/Dockerfile (61%) create mode 100644 docker/add-aur.sh create mode 100644 docker/build-rust.sh create mode 100644 docker/builder.Dockerfile rename entrypoint.sh => docker/entrypoint.sh (100%) create mode 100644 frontend/lib/components/add_package_popup.dart create mode 100644 frontend/lib/components/build_flag_settings.dart create mode 100644 frontend/lib/components/platform_settings.dart create mode 100644 frontend/lib/constants/platforms.dart create mode 100644 frontend/lib/models/extended_package.dart delete mode 100644 frontend/lib/models/package.dart create mode 100644 frontend/lib/models/simple_packge.dart create mode 100644 frontend/lib/screens/Package_settings_screen.dart diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 11c4981..b8eda71 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -7,7 +7,7 @@ on: - '!master' jobs: - build-and-push: + build-amd64: runs-on: ubuntu-latest steps: @@ -20,6 +20,24 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build Docker image run: | - docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} . + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/amd64 -f ./docker/Dockerfile . + + build-aarch64: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build ARM64 Docker image + run: | + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/arm64/v8 -f ./docker/Dockerfile . \ No newline at end of file diff --git a/.github/workflows/docker-builder-publish.yml b/.github/workflows/docker-builder-publish.yml new file mode 100644 index 0000000..45553a8 --- /dev/null +++ b/.github/workflows/docker-builder-publish.yml @@ -0,0 +1,44 @@ +name: Make helper containers +on: + workflow_dispatch: + push: + branches: + - "**" + +jobs: + build: + name: Build AUR enabled Arch containers + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Checkout + uses: actions/checkout@v2 + - id: repo_name_lc + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Build paru container + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + context: . + file: ./docker/builder.Dockerfile + tags: | + ghcr.io/lukas-heiligenbrunner/aurcache-builder:latest \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0799453..8ed9686 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -8,7 +8,7 @@ on: - '*' jobs: - build-and-push: + build-and-push-arm64: runs-on: ubuntu-latest steps: @@ -34,13 +34,50 @@ jobs: - name: Build and push Docker image for main branch if: github.ref == 'refs/heads/master' run: | - docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:git . + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/amd64 -f ./docker/Dockerfile -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:git . docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:git - name: Build and push Docker image for tag if: startsWith(github.ref, 'refs/tags/') run: | - docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} . + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/amd64 -f ./docker/Dockerfile -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} . + docker tag ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:latest + docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} + docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:latest + + build-and-push-aarch64: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version tag + id: extract_tag + run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}" + + - id: repo_name_lc + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + + - name: Build and push ARM64 Docker image for main branch + if: github.ref == 'refs/heads/master' + run: | + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/arm64/v8 -f ./docker/Dockerfile.arm64 -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:git . + docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:git + + - name: Build and push ARM64 Docker image for tag + if: startsWith(github.ref, 'refs/tags/') + run: | + docker build --build-arg LATEST_COMMIT_SHA=${{ github.event.head_commit.id }} --build-arg TARGET_ARCH=linux/arm64/v8 -f ./docker/Dockerfile.arm64 -t ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} . docker tag ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:latest docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:${{ steps.extract_tag.outputs.tag }} docker push ghcr.io/${{ steps.repo_name_lc.outputs.lowercase }}:latest \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8b1e089..2dabfbc 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -162,9 +168,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "arrayvec" @@ -273,6 +279,7 @@ dependencies = [ "dotenvy", "env_logger", "log", + "openssl", "pacman-repo-utils", "reqwest 0.12.7", "rocket", @@ -313,7 +320,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -978,12 +985,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1769,6 +1776,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1821,6 +1839,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -2026,6 +2053,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.3.1+3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.103" @@ -2034,6 +2070,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2088,6 +2125,7 @@ dependencies = [ "md5", "sha2", "tar", + "xz2", "zstd", ] @@ -3817,9 +3855,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" dependencies = [ "filetime", "libc", @@ -4682,6 +4720,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c807ba2..2fa3c86 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] aur-rs = "0.1.1" tokio = "1.39.2" -anyhow = "1.0.86" +anyhow = "1.0.89" reqwest = { version = "0.12.5", features = ["blocking", "gzip"] } @@ -28,6 +28,9 @@ log = "0.4.22" backon = "1.2.0" pacman-repo-utils = {path = "./src/pacman-repo-utils"} +[target.aarch64-unknown-linux-gnu.dependencies] +openssl = { version = "*", features = ["vendored"] } + [[bin]] name = "aurcache" path = "src/main.rs" diff --git a/backend/src/api/backend.rs b/backend/src/api/backend.rs index 058a553..eff169c 100644 --- a/backend/src/api/backend.rs +++ b/backend/src/api/backend.rs @@ -12,12 +12,14 @@ pub fn build_api() -> Vec { package_list, package_add_endpoint, package_del, + package_update_entity_endpoint, build_output, delete_build, list_builds, stats, get_build, get_package, + rery_build, package_update_endpoint, cancel_build, health diff --git a/backend/src/api/build.rs b/backend/src/api/build.rs index 1009a51..051131c 100644 --- a/backend/src/api/build.rs +++ b/backend/src/api/build.rs @@ -7,11 +7,12 @@ use rocket::{delete, get, post, State}; use crate::api::types::authenticated::Authenticated; use crate::api::types::input::ListBuildsModel; -use crate::builder::types::Action; +use crate::builder::types::{Action, BuildStates}; +use crate::package::update::update_platform; use rocket_okapi::openapi; use sea_orm::{ - ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect, - RelationTrait, + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, + QueryOrder, QuerySelect, RelationTrait, Set, }; use tokio::sync::broadcast::Sender; @@ -33,7 +34,7 @@ pub async fn build_output( .map_err(|e| NotFound(e.to_string()))? .ok_or(NotFound("couldn't find id".to_string()))?; - return match build.output { + match build.output { None => Err(NotFound("No Output".to_string())), Some(v) => match startline { None => Ok(v), @@ -58,7 +59,7 @@ pub async fn build_output( Ok(output) } }, - }; + } } /// get list of all builds @@ -83,6 +84,7 @@ pub async fn list_builds( .column(packages::Column::Version) .column(builds::Column::EndTime) .column(builds::Column::StartTime) + .column(builds::Column::Platform) .order_by(builds::Column::StartTime, Order::Desc) .limit(limit) .offset(page.zip(limit).map(|(page, limit)| page * limit)); @@ -120,6 +122,7 @@ pub async fn get_build( .column(packages::Column::Version) .column(builds::Column::EndTime) .column(builds::Column::StartTime) + .column(builds::Column::Platform) .into_model::() .one(db) .await @@ -165,3 +168,45 @@ pub async fn cancel_build( Ok(()) } + +#[openapi(tag = "build")] +#[post("/build//retry")] +pub async fn rery_build( + db: &State, + tx: &State>, + buildid: i32, + _a: Authenticated, +) -> Result, NotFound> { + let db = db as &DatabaseConnection; + + // Fetch the build details + let old_build = Builds::find_by_id(buildid) + .one(db) + .await + .map_err(|e| NotFound(e.to_string()))? + .ok_or(NotFound("Build not found".to_string()))?; + + // Extract the platform and package ID + let platform = old_build.platform; + let pkg_id = old_build.pkg_id; + + // Fetch the package details + let package = packages::Entity::find_by_id(pkg_id) + .one(db) + .await + .map_err(|e| NotFound(e.to_string()))? + .ok_or(NotFound("Package not found".to_string()))?; + + let mut pacage_am: packages::ActiveModel = package.clone().into(); + pacage_am.status = Set(BuildStates::ENQUEUED_BUILD); + pacage_am + .save(db) + .await + .map_err(|e| NotFound(e.to_string()))?; + + let new_buildid = update_platform(&platform, package, db, tx) + .await + .map_err(|e| NotFound(e.to_string()))?; + + Ok(Json(new_buildid)) +} diff --git a/backend/src/api/package.rs b/backend/src/api/package.rs index d0aef6b..187279d 100644 --- a/backend/src/api/package.rs +++ b/backend/src/api/package.rs @@ -8,13 +8,15 @@ use crate::package::update::package_update; use rocket::response::status::{BadRequest, NotFound}; use rocket::serde::json::Json; -use rocket::{delete, get, post, State}; +use rocket::{delete, get, patch, post, State}; use crate::api::types::authenticated::Authenticated; -use crate::api::types::input::ListPackageModel; +use crate::api::types::input::{ExtendedPackageModel, PackagePatchModel, SimplePackageModel}; use crate::api::types::output::{AddBody, UpdateBody}; +use crate::aur::api::get_info_by_name; use rocket_okapi::openapi; -use sea_orm::DatabaseConnection; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, DatabaseConnection, NotSet}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; use tokio::sync::broadcast::Sender; @@ -27,9 +29,56 @@ pub async fn package_add_endpoint( tx: &State>, _a: Authenticated, ) -> Result<(), BadRequest> { - package_add(db, input.name.clone(), tx) + package_add( + db, + input.name.clone(), + tx, + input.platforms.clone(), + input.build_flags.clone(), + ) + .await + .map_err(|e| BadRequest(e.to_string())) +} + +/// Add new Package to build queue +#[openapi(tag = "Packages")] +#[patch("/package/", data = "")] +pub async fn package_update_entity_endpoint( + db: &State, + input: Json, + id: i32, + _a: Authenticated, +) -> Result<(), BadRequest> { + let db = db as &DatabaseConnection; + + // Start building the update operation + let update_pkg = packages::ActiveModel { + id: Set(id), + name: input.name.clone().map(Set).unwrap_or(NotSet), + status: input.status.map(Set).unwrap_or(NotSet), + out_of_date: input.out_of_date.map(Set).unwrap_or(NotSet), + version: input.version.clone().map(Set).unwrap_or(NotSet), + latest_aur_version: input.latest_aur_version.clone().map(Set).unwrap_or(NotSet), + latest_build: input.latest_build.map(Set).unwrap_or(NotSet), + build_flags: input + .build_flags + .clone() + .map(|v| Set(v.join(";"))) + .unwrap_or(NotSet), + platforms: input + .platforms + .clone() + .map(|v| Set(v.join(";"))) + .unwrap_or(NotSet), + }; + + // Execute the update query + update_pkg + .update(db) .await - .map_err(|e| BadRequest(e.to_string())) + .map_err(|e| BadRequest(e.to_string()))?; + + Ok(()) } /// Update a package with id @@ -41,7 +90,7 @@ pub async fn package_update_endpoint( input: Json, tx: &State>, _a: Authenticated, -) -> Result, BadRequest> { +) -> Result>, BadRequest> { package_update(db, id, input.force, tx) .await .map(Json) @@ -68,10 +117,10 @@ pub async fn package_list( limit: Option, page: Option, _a: Authenticated, -) -> Result>, NotFound> { +) -> Result>, NotFound> { let db = db as &DatabaseConnection; - let all: Vec = Packages::find() + let all: Vec = Packages::find() .select_only() .column(packages::Column::Name) .column(packages::Column::Id) @@ -82,7 +131,7 @@ pub async fn package_list( .order_by(packages::Column::Id, Order::Desc) .limit(limit) .offset(page.zip(limit).map(|(page, limit)| page * limit)) - .into_model::() + .into_model::() .all(db) .await .map_err(|e| NotFound(e.to_string()))?; @@ -91,29 +140,50 @@ pub async fn package_list( } /// get specific package by id +/// This requires 1 API call to the AUR (rate limited 4000 per day) +/// https://wiki.archlinux.org/title/Aurweb_RPC_interface #[openapi(tag = "Packages")] #[get("/package/")] pub async fn get_package( db: &State, id: u64, _a: Authenticated, -) -> Result, NotFound> { +) -> Result, NotFound> { let db = db as &DatabaseConnection; - let all: ListPackageModel = Packages::find() + let pkg = Packages::find() .filter(packages::Column::Id.eq(id)) - .select_only() - .column(packages::Column::Name) - .column(packages::Column::Id) - .column(packages::Column::Status) - .column_as(packages::Column::OutOfDate, "outofdate") - .column_as(packages::Column::LatestAurVersion, "latest_aur_version") - .column_as(packages::Column::Version, "latest_version") - .into_model::() .one(db) .await .map_err(|e| NotFound(e.to_string()))? .ok_or(NotFound("id not found".to_string()))?; - Ok(Json(all)) + let aur_info = get_info_by_name(&pkg.name) + .await + .map_err(|e| NotFound(e.to_string()))?; + + let aur_url = format!( + "https://aur.archlinux.org/packages/{}", + aur_info.package_base + ); + + let ext_pkg = ExtendedPackageModel { + id: pkg.id, + name: pkg.name, + status: pkg.status, + outofdate: pkg.out_of_date, + latest_version: pkg.version, + latest_aur_version: aur_info.version, + last_updated: aur_info.last_modified, + first_submitted: aur_info.first_submitted, + licenses: aur_info.license.map(|l| l.join(", ")), + maintainer: aur_info.maintainer, + aur_flagged_outdated: aur_info.out_of_date.unwrap_or(0) != 0, + selected_platforms: pkg.platforms.split(";").map(|v| v.to_string()).collect(), + selected_build_flags: Some(pkg.build_flags.split(";").map(|v| v.to_string()).collect()), + aur_url, + project_url: aur_info.url, + description: aur_info.description, + }; + Ok(Json(ext_pkg)) } diff --git a/backend/src/api/types/input.rs b/backend/src/api/types/input.rs index 97b16af..dd50e59 100644 --- a/backend/src/api/types/input.rs +++ b/backend/src/api/types/input.rs @@ -12,7 +12,7 @@ pub struct ApiPackage { #[derive(FromQueryResult, Deserialize, JsonSchema, Serialize)] #[serde(crate = "rocket::serde")] -pub struct ListPackageModel { +pub struct SimplePackageModel { pub id: i32, pub name: String, pub status: i32, @@ -21,6 +21,39 @@ pub struct ListPackageModel { pub latest_aur_version: String, } +#[derive(FromQueryResult, Deserialize, JsonSchema, Serialize, Default)] +#[serde(crate = "rocket::serde")] +pub struct ExtendedPackageModel { + pub id: i32, + pub name: String, + pub status: i32, + pub outofdate: i32, + pub latest_version: Option, + pub latest_aur_version: String, + pub last_updated: u32, + pub first_submitted: u32, + pub licenses: Option, + pub maintainer: Option, + pub aur_flagged_outdated: bool, + pub selected_platforms: Vec, + pub selected_build_flags: Option>, + pub aur_url: String, + pub project_url: Option, + pub description: Option, +} + +#[derive(FromQueryResult, Deserialize, JsonSchema, Serialize, Default)] +pub struct PackagePatchModel { + pub name: Option, + pub status: Option, + pub out_of_date: Option, + pub version: Option>, + pub latest_aur_version: Option>, + pub latest_build: Option>, + pub build_flags: Option>, + pub platforms: Option>, +} + #[derive(FromQueryResult, Deserialize, JsonSchema, Serialize)] #[serde(crate = "rocket::serde")] pub struct ListBuildsModel { @@ -31,6 +64,7 @@ pub struct ListBuildsModel { status: i32, start_time: Option, end_time: Option, + platform: String, } #[derive(FromQueryResult, Deserialize, JsonSchema, Serialize)] diff --git a/backend/src/api/types/output.rs b/backend/src/api/types/output.rs index b2f52ca..caef54e 100644 --- a/backend/src/api/types/output.rs +++ b/backend/src/api/types/output.rs @@ -6,6 +6,8 @@ use rocket_okapi::JsonSchema; #[serde(crate = "rocket::serde")] pub struct AddBody { pub(crate) name: String, + pub(crate) platforms: Option>, + pub(crate) build_flags: Option>, } #[derive(Deserialize, JsonSchema)] diff --git a/backend/src/aur/api.rs b/backend/src/aur/api.rs index 59907ab..f5ef772 100644 --- a/backend/src/aur/api.rs +++ b/backend/src/aur/api.rs @@ -3,6 +3,9 @@ use aur_rs::{Package, Request}; use backon::{ConstantBuilder, FibonacciBuilder, Retryable}; use std::time::Duration; +// https://wiki.archlinux.org/title/Aurweb_RPC_interface +// API rate limit 4000 requests per day + pub async fn query_aur(query: &str) -> anyhow::Result> { let request = Request::default(); let response = (|| async { request.search_package_by_name(query).await }) diff --git a/backend/src/builder/build.rs b/backend/src/builder/build.rs index 7eeb7fa..5eed8ec 100644 --- a/backend/src/builder/build.rs +++ b/backend/src/builder/build.rs @@ -2,13 +2,13 @@ use crate::builder::logger::BuildLogger; use crate::builder::types::BuildStates; use crate::db::files::ActiveModel; use crate::db::migration::JoinType; -use crate::db::prelude::{Builds, Files, PackagesFiles}; +use crate::db::prelude::{Files, PackagesFiles}; use crate::db::{builds, files, packages, packages_files}; use crate::repo::utils::try_remove_archive_file; use anyhow::anyhow; use bollard::container::{ AttachContainerOptions, Config, CreateContainerOptions, KillContainerOptions, LogOutput, - RemoveContainerOptions, + WaitContainerOptions, }; use bollard::image::CreateImageOptions; use bollard::models::{ContainerCreateResponse, CreateImageInfo, HostConfig}; @@ -29,151 +29,130 @@ use std::{env, fs}; use tokio::sync::Mutex; use tokio::time::timeout; -static BUILDER_IMAGE: &str = "docker.io/greyltc/archlinux-aur:paru"; - -pub(crate) async fn cancel_build( - build_id: i32, - job_containers: Arc>>, - db: DatabaseConnection, -) -> anyhow::Result<()> { - let build = Builds::find_by_id(build_id) - .one(&db) - .await? - .ok_or(anyhow!("No build found"))?; - - let mut build: builds::ActiveModel = build.into(); - build.status = Set(Some(4)); - build.end_time = Set(Some( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - )); - let _ = build.clone().update(&db).await; - - let container_id = job_containers - .lock() - .await - .get(&build_id) - .ok_or(anyhow!("Build container not found"))? - .clone(); - - let docker = Docker::connect_with_unix_defaults()?; - docker - .remove_container( - &container_id, - Some(RemoveContainerOptions { - force: true, - ..Default::default() - }), - ) - .await?; - - job_containers - .lock() - .await - .remove(&build_id) - .ok_or(anyhow!( - "Failed to remove build container from active build map" - ))?; - Ok(()) -} +static BUILDER_IMAGE: &str = "ghcr.io/lukas-heiligenbrunner/aurcache-builder:latest"; pub(crate) async fn prepare_build( - mut new_build: builds::ActiveModel, + mut build_model: builds::ActiveModel, db: DatabaseConnection, mut package_model: packages::ActiveModel, - job_containers: Arc>>, -) -> anyhow::Result<()> { - let build_id = new_build.id.clone().unwrap(); - let build_logger = BuildLogger::new(build_id, db.clone()); +) -> anyhow::Result<(packages::ActiveModel, builds::ActiveModel, String)> { + // set build status to building + build_model.status = Set(Some(BuildStates::ACTIVE_BUILD)); + build_model.start_time = Set(Some( + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, + )); + let build_model = build_model.save(&db).await?; // update status to building package_model.status = Set(BuildStates::ACTIVE_BUILD); package_model = package_model.update(&db).await?.into(); - let package_name = package_model.name.clone().unwrap(); - let package_id = package_model.id.clone().unwrap(); + let target_platform = format!("linux/{}", build_model.platform.clone().unwrap()); - match build( - package_name, - build_id, - package_id, - &db, - build_logger.clone(), - job_containers, - ) - .await - { - Ok(_) => { - // update package success status - package_model.status = Set(BuildStates::SUCCESSFUL_BUILD); - package_model.out_of_date = Set(false as i32); - package_model.update(&db).await?; - - new_build.status = Set(Some(BuildStates::SUCCESSFUL_BUILD)); - new_build.end_time = Set(Some( - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, - )); - _ = new_build.update(&db).await; - build_logger - .append("finished package build".to_string()) - .await?; - } - Err(e) => { - package_model.status = Set(BuildStates::FAILED_BUILD); - package_model.update(&db).await?; - - new_build.status = Set(Some(BuildStates::FAILED_BUILD)); - new_build.end_time = Set(Some( - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64, - )); - let _ = new_build.update(&db).await; + #[cfg(target_arch = "aarch64")] + if target_platform != "linux/arm64" { + return Err(anyhow!( + "Unsupported host architecture aarch64 for cross-compile" + )); + } - build_logger.append(e.to_string()).await?; - } - }; - Ok(()) + Ok((package_model, build_model, target_platform)) } -pub async fn build( - name: String, - build_id: i32, - pkg_id: i32, - db: &DatabaseConnection, - build_logger: BuildLogger, - job_containers: Arc>>, -) -> anyhow::Result<()> { +async fn establish_docker_connection() -> anyhow::Result { let docker = Docker::connect_with_unix_defaults()?; - docker .ping() .await .map_err(|e| anyhow!("Connection to Docker Socket failed: {}", e))?; + Ok(docker) +} - repull_image(&docker, &build_logger).await?; - - let (create_info, host_active_build_path) = - create_build_container(&docker, build_id, name.clone()).await?; +pub async fn build( + build_model: builds::ActiveModel, + db: &DatabaseConnection, + package_model: packages::ActiveModel, + job_containers: Arc>>, + build_logger: BuildLogger, +) -> anyhow::Result<()> { + let (package_model_am, build_model_am, target_platform) = + prepare_build(build_model, db.clone(), package_model).await?; + let package_model: packages::Model = package_model_am.try_into()?; + let build_model: builds::Model = build_model_am.try_into()?; + let docker = establish_docker_connection().await?; + + repull_image( + &docker, + &build_logger, + BUILDER_IMAGE, + target_platform.clone(), + ) + .await?; + + let (create_info, host_active_build_path) = create_build_container( + &docker, + build_model.id, + package_model.name.clone(), + target_platform, + package_model.build_flags.split(";").collect(), + ) + .await?; let id = create_info.id; + let docker2 = docker.clone(); + let id2 = id.clone(); + let build_logger2 = build_logger.clone(); + // start listening to container before starting it + tokio::spawn(async move { + _ = monitor_build_output(&build_logger2, &docker2, id2.clone()).await; + }); + // start build container docker.start_container::(&id, None).await?; // insert container id to container map - job_containers.lock().await.insert(build_id, id.clone()); + job_containers + .lock() + .await + .insert(build_model.id, id.clone()); // monitor build output - match timeout( + let build_result = timeout( job_timeout_from_env(), - monitor_build_output(&build_logger, &docker, id.clone()), + docker + .wait_container( + &id, + Some(WaitContainerOptions { + condition: "not-running", + }), + ) + .next(), ) - .await - { - Ok(v) => v?, + .await; + + debug!("Build container was removed"); + + match build_result { + Ok(v) => { + let t = v.ok_or(anyhow!("Failed to get build result"))??; + let exit_code = t.status_code; + if exit_code != 0 { + build_logger + .append(format!( + "Build #{} failed for package '{}', exit code: {}", + build_model.id, package_model.name, exit_code + )) + .await?; + return Err(anyhow!("Build failed with exit code: {}", exit_code)); + } + } + // timeout branch Err(_) => { build_logger - .append(format!("Build #{build_id} timed out for package '{name}'")) + .append(format!( + "Build #{} timed out for package '{}'", + build_model.id, package_model.name + )) .await?; // kill build container docker @@ -182,17 +161,15 @@ pub async fn build( } } - // remove build from container map - job_containers - .lock() - .await - .remove(&build_id) - .ok_or(anyhow!( - "Failed to remove build container from active builds map" - ))?; - // move built tar.gz archives to host and repo-add - move_and_add_pkgs(&build_logger, host_active_build_path.clone(), pkg_id, db).await?; + move_and_add_pkgs( + &build_logger, + host_active_build_path.clone(), + package_model.id, + db, + build_model.platform, + ) + .await?; // remove active build dir fs::remove_dir(host_active_build_path)?; Ok(()) @@ -242,9 +219,12 @@ async fn create_build_container( docker: &Docker, build_id: i32, name: String, + arch: String, + build_flags: Vec<&str>, ) -> anyhow::Result<(ContainerCreateResponse, PathBuf)> { let (host_build_path_docker, host_active_build_path) = create_build_paths(name.clone())?; + let build_flags = build_flags.join(" "); // create new docker container for current build let build_dir_base = "/var/cache/makepkg/pkg"; let mountpoints = vec![format!("{}:{}", host_build_path_docker, build_dir_base)]; @@ -252,8 +232,8 @@ async fn create_build_container( let (makepkg_config, makepkg_config_path) = create_makepkg_config(name.clone(), build_dir_base)?; let cmd = format!( - "cat < {}\n{}\nEOF\nparu -Syu --noconfirm --noprogressbar --color never {}", - makepkg_config_path, makepkg_config, name + "cat < {}\n{}\nEOF\nparu {} {}", + makepkg_config_path, makepkg_config, build_flags, name ); let (cpu_limit, memory_limit) = limits_from_env(); @@ -279,7 +259,7 @@ async fn create_build_container( .create_container::<&str, &str>( Some(CreateContainerOptions { name: container_name.as_str(), - platform: None, + platform: Some(arch.as_str()), }), conf, ) @@ -348,11 +328,20 @@ fn limits_from_env() -> (u64, i64) { (cpu_limit, memory_limit) } -async fn repull_image(docker: &Docker, build_logger: &BuildLogger) -> anyhow::Result<()> { +async fn repull_image( + docker: &Docker, + build_logger: &BuildLogger, + image: &str, + arch: String, +) -> anyhow::Result<()> { + build_logger + .append(format!("Pulling image: {}", image)) + .await?; // repull image to make sure it's up to date let mut stream = docker.create_image( Some(CreateImageOptions { - from_image: BUILDER_IMAGE, + from_image: image, + platform: arch.as_str(), ..Default::default() }), None, @@ -388,6 +377,7 @@ async fn move_and_add_pkgs( host_build_path: PathBuf, pkg_id: i32, db: &DatabaseConnection, + platform: String, ) -> anyhow::Result<()> { let archive_paths = fs::read_dir(host_build_path.clone())?.collect::>(); if archive_paths.is_empty() { @@ -398,6 +388,7 @@ async fn move_and_add_pkgs( // remove files assosicated with package let old_files: Vec<(packages_files::Model, Option)> = PackagesFiles::find() .filter(packages_files::Column::PackageId.eq(pkg_id)) + .filter(files::Column::Platform.eq(platform.clone())) .join(JoinType::LeftJoin, packages_files::Relation::Files.def()) .select_also(files::Entity) .all(db) @@ -423,7 +414,8 @@ async fn move_and_add_pkgs( for archive in archive_paths { let archive = archive?; let archive_name = archive.file_name().to_str().unwrap().to_string(); - fs::copy(archive.path(), format!("./repo/{archive_name}"))?; + let pkg_path = format!("./repo/{platform}/{archive_name}"); + fs::copy(archive.path(), pkg_path.clone())?; // remove old file from shared path fs::remove_file(archive.path())?; @@ -435,6 +427,7 @@ async fn move_and_add_pkgs( None => { let file = files::ActiveModel { filename: Set(archive_name.clone()), + platform: Set(platform.clone()), ..Default::default() }; file.save(&txn).await? @@ -450,9 +443,9 @@ async fn move_and_add_pkgs( package_file.save(&txn).await?; pacman_repo_utils::repo_add( - format!("./repo/{}", archive_name).as_str(), - "./repo/repo.db.tar.gz".to_string(), - "./repo/repo.files.tar.gz".to_string(), + pkg_path.as_str(), + format!("./repo/{platform}/repo.db.tar.gz"), + format!("./repo/{platform}/repo.files.tar.gz"), )?; info!("Successfully added '{}' to the repo archive", archive_name); } diff --git a/backend/src/builder/cancel.rs b/backend/src/builder/cancel.rs new file mode 100644 index 0000000..49142a0 --- /dev/null +++ b/backend/src/builder/cancel.rs @@ -0,0 +1,58 @@ +use crate::db::builds; +use crate::db::prelude::Builds; +use anyhow::anyhow; +use bollard::container::RemoveContainerOptions; +use bollard::Docker; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, Set}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::Mutex; + +pub(crate) async fn cancel_build( + build_id: i32, + job_containers: Arc>>, + db: DatabaseConnection, +) -> anyhow::Result<()> { + let build = Builds::find_by_id(build_id) + .one(&db) + .await? + .ok_or(anyhow!("No build found"))?; + + let mut build: builds::ActiveModel = build.into(); + build.status = Set(Some(4)); + build.end_time = Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64, + )); + let _ = build.clone().update(&db).await; + + let container_id = job_containers + .lock() + .await + .get(&build_id) + .ok_or(anyhow!("Build container not found"))? + .clone(); + + let docker = Docker::connect_with_unix_defaults()?; + docker + .remove_container( + &container_id, + Some(RemoveContainerOptions { + force: true, + ..Default::default() + }), + ) + .await?; + + job_containers + .lock() + .await + .remove(&build_id) + .ok_or(anyhow!( + "Failed to remove build container from active build map" + ))?; + Ok(()) +} diff --git a/backend/src/builder/init.rs b/backend/src/builder/init.rs index b704103..547bf86 100644 --- a/backend/src/builder/init.rs +++ b/backend/src/builder/init.rs @@ -1,4 +1,4 @@ -use crate::builder::build::cancel_build; +use crate::builder::cancel::cancel_build; use crate::builder::queue::queue_package; use crate::builder::types::Action; use sea_orm::DatabaseConnection; @@ -11,11 +11,7 @@ use tokio::task::JoinHandle; pub fn init_build_queue(db: DatabaseConnection, tx: Sender) -> JoinHandle<()> { tokio::spawn(async move { - let max_concurrent_builds = env::var("MAX_CONCURRENT_BUILDS") - .ok() - .and_then(|x| x.parse::().ok()) - .unwrap_or(1); - let semaphore = Arc::new(Semaphore::new(max_concurrent_builds)); + let semaphore = new_semaphore(); let job_containers: Arc>> = Arc::new(Mutex::new(HashMap::new())); loop { @@ -40,3 +36,11 @@ pub fn init_build_queue(db: DatabaseConnection, tx: Sender) -> JoinHandl } }) } + +fn new_semaphore() -> Arc { + let max_concurrent_builds = env::var("MAX_CONCURRENT_BUILDS") + .ok() + .and_then(|x| x.parse::().ok()) + .unwrap_or(1); + Arc::new(Semaphore::new(max_concurrent_builds)) +} diff --git a/backend/src/builder/mod.rs b/backend/src/builder/mod.rs index fca9f44..73c7a09 100644 --- a/backend/src/builder/mod.rs +++ b/backend/src/builder/mod.rs @@ -1,4 +1,5 @@ mod build; +mod cancel; pub mod init; mod logger; mod queue; diff --git a/backend/src/builder/queue.rs b/backend/src/builder/queue.rs index f1b91e5..05b4be0 100644 --- a/backend/src/builder/queue.rs +++ b/backend/src/builder/queue.rs @@ -1,16 +1,18 @@ -use crate::builder::build::prepare_build; +use crate::builder::build::build; +use crate::builder::logger::BuildLogger; use crate::builder::types::BuildStates; use crate::db::builds::ActiveModel; -use crate::db::packages; -use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; +use crate::db::{builds, packages}; +use sea_orm::{ActiveModelTrait, DatabaseConnection, Set, TransactionTrait}; use std::collections::HashMap; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::{Mutex, Semaphore}; +/// Queue a package for building pub(crate) async fn queue_package( package_model: Box, - mut build_model: Box, + build_model: Box, db: DatabaseConnection, semaphore: Arc, job_containers: Arc>>, @@ -18,21 +20,65 @@ pub(crate) async fn queue_package( let permits = Arc::clone(&semaphore); // spawn new thread for each pkg build - // todo add queue and build two packages in parallel tokio::spawn(async move { let _permit = permits.acquire().await.unwrap(); - - // set build status to building - build_model.status = Set(Some(BuildStates::ACTIVE_BUILD)); - build_model.start_time = Set(Some( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - )); - let build_model = build_model.save(&db).await.unwrap(); - - let _ = prepare_build(build_model, db, *package_model, job_containers).await; + start_build(*build_model, &db, *package_model, job_containers).await; }); Ok(()) } + +async fn start_build( + mut build_model: builds::ActiveModel, + db: &DatabaseConnection, + mut package_model: packages::ActiveModel, + job_containers: Arc>>, +) { + let build_id = build_model.id.clone().unwrap(); + let build_logger = BuildLogger::new(build_id, db.clone()); + + let build_result = build( + build_model.clone(), + db, + package_model.clone(), + job_containers.clone(), + build_logger.clone(), + ) + .await; + + let txn = db.begin().await.unwrap(); + build_model.end_time = Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64, + )); + + match build_result { + Ok(_) => { + // update package success status + package_model.status = Set(BuildStates::SUCCESSFUL_BUILD); + package_model.out_of_date = Set(false as i32); + _ = package_model.update(&txn).await; + + build_model.status = Set(Some(BuildStates::SUCCESSFUL_BUILD)); + + let _ = build_model.update(&txn).await; + _ = build_logger + .append("finished package build".to_string()) + .await; + } + Err(e) => { + package_model.status = Set(BuildStates::FAILED_BUILD); + _ = package_model.update(&txn).await; + + build_model.status = Set(Some(BuildStates::FAILED_BUILD)); + let _ = build_model.update(&txn).await; + + _ = build_logger.append(e.to_string()).await; + } + }; + txn.commit().await.unwrap(); + + // remove build from container map + _ = job_containers.lock().await.remove(&build_id); +} diff --git a/backend/src/db/builds.rs b/backend/src/db/builds.rs index d68ab9e..7d132c6 100644 --- a/backend/src/db/builds.rs +++ b/backend/src/db/builds.rs @@ -15,6 +15,7 @@ pub struct Model { pub status: Option, pub start_time: Option, pub end_time: Option, + pub platform: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/backend/src/db/files.rs b/backend/src/db/files.rs index a3564de..cc9ac3f 100644 --- a/backend/src/db/files.rs +++ b/backend/src/db/files.rs @@ -8,6 +8,7 @@ pub struct Model { pub filename: String, #[sea_orm(primary_key)] pub id: i32, + pub platform: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/backend/src/db/migration/m20240907_131839_platform_buildflags.rs b/backend/src/db/migration/m20240907_131839_platform_buildflags.rs new file mode 100644 index 0000000..9aa3d8d --- /dev/null +++ b/backend/src/db/migration/m20240907_131839_platform_buildflags.rs @@ -0,0 +1,141 @@ +use crate::db::helpers::dbtype::{database_type, DbType}; +use sea_orm_migration::prelude::*; +use std::fs; +use std::path::Path; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + match database_type() { + DbType::Sqlite => { + db.execute_unprepared( + r#" +ALTER TABLE packages +ADD COLUMN build_flags TEXT; + +ALTER TABLE packages +ADD COLUMN platforms TEXT; + +ALTER TABLE builds +ADD COLUMN platform TEXT; + +ALTER TABLE files +ADD COLUMN platform TEXT; + +UPDATE packages +SET build_flags = '-Syu;--noconfirm;--noprogressbar;--color never', + platforms = 'x86_64'; + +UPDATE builds + SET platform = 'x86_64'; + +UPDATE files + SET platform = 'x86_64'; +"#, + ) + .await?; + } + DbType::Postgres => { + db.execute_unprepared( + r#" +ALTER TABLE public.packages +ADD COLUMN build_flags TEXT; + +ALTER TABLE public.packages +ADD COLUMN platforms TEXT; + +ALTER TABLE public.builds +ADD COLUMN platform TEXT; + +ALTER TABLE public.files +ADD COLUMN platform TEXT; + +UPDATE public.packages +SET build_flags = '-Syu;--noconfirm;--noprogressbar;--color never', + platforms = 'x86_64'; + +UPDATE public.builds + SET platform = 'x86_64'; + +UPDATE public.files + SET platform = 'x86_64'; +"#, + ) + .await?; + } + } + + // try to copy pkg files to new location + let src_path = Path::new("./repo"); + let dest_path = Path::new("./repo/x86_64"); + + // Iterate over the files in the source directory + if let Ok(entries) = fs::read_dir(src_path) { + for entry in entries.flatten() { + let path = entry.path(); + + // Only copy files (not directories) + if path.is_file() { + let file_name = entry.file_name(); + let dest_file = dest_path.join(file_name); + + // Copy the file to the destination directory + _ = fs::copy(path.clone(), dest_file); + _ = fs::remove_file(path); + } + } + } + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + match database_type() { + DbType::Sqlite => { + db.execute_unprepared( + r#" +ALTER TABLE packages +DROP COLUMN build_flags; + +ALTER TABLE packages +DROP COLUMN platforms; + +ALTER TABLE builds +DROP COLUMN platform; + +ALTER TABLE files +DROP COLUMN platform; +"#, + ) + .await?; + } + DbType::Postgres => { + db.execute_unprepared( + r#" +ALTER TABLE public.packages +DROP COLUMN build_flags; + +ALTER TABLE public.packages +DROP COLUMN platforms; + +ALTER TABLE builds +DROP COLUMN platform; + +ALTER TABLE files +DROP COLUMN platform; +"#, + ) + .await?; + } + } + + Ok(()) + } +} diff --git a/backend/src/db/migration/mod.rs b/backend/src/db/migration/mod.rs index ac63e89..d700893 100644 --- a/backend/src/db/migration/mod.rs +++ b/backend/src/db/migration/mod.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod create; +mod m20240907_131839_platform_buildflags; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(create::Migration)] + vec![ + Box::new(create::Migration), + Box::new(m20240907_131839_platform_buildflags::Migration), + ] } } diff --git a/backend/src/db/packages.rs b/backend/src/db/packages.rs index 84e294b..2dbdeca 100644 --- a/backend/src/db/packages.rs +++ b/backend/src/db/packages.rs @@ -13,9 +13,11 @@ pub struct Model { pub name: String, pub status: i32, pub out_of_date: i32, - pub version: String, + pub version: Option, pub latest_aur_version: Option, pub latest_build: Option, + pub build_flags: String, + pub platforms: String, } impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/src/package/add.rs b/backend/src/package/add.rs index e698154..2e54fe2 100644 --- a/backend/src/package/add.rs +++ b/backend/src/package/add.rs @@ -2,6 +2,7 @@ use crate::aur::api::get_info_by_name; use crate::builder::types::{Action, BuildStates}; use crate::db::prelude::Packages; use crate::db::{builds, packages}; +use crate::repo::platforms::PLATFORMS; use anyhow::anyhow; use sea_orm::ColumnTrait; use sea_orm::QueryFilter; @@ -13,15 +14,31 @@ pub async fn package_add( db: &DatabaseConnection, pkg_name: String, tx: &Sender, + platforms: Option>, + build_flags: Option>, ) -> anyhow::Result<()> { - let txn = db.begin().await?; + let platforms = match platforms { + None => vec!["x86_64".to_string()], + Some(platforms) => { + check_platforms(&platforms)?; + platforms + } + }; + let build_flags = build_flags.unwrap_or_else(|| { + vec![ + "-Syu".to_string(), + "--noconfirm".to_string(), + "--noprogressbar".to_string(), + "--color never".to_string(), + ] + }); // remove leading and trailing whitespaces let pkg_name = pkg_name.trim(); if Packages::find() .filter(packages::Column::Name.eq(pkg_name)) - .one(&txn) + .one(db) .await? .is_some() { @@ -33,33 +50,56 @@ pub async fn package_add( let new_package = packages::ActiveModel { name: Set(pkg_name.to_string()), status: Set(BuildStates::ENQUEUED_BUILD), - version: Set(pkg.version.clone()), + version: Set(Some(pkg.version.clone())), latest_aur_version: Set(Option::from(pkg.version.clone())), + platforms: Set(platforms.join(";")), + build_flags: Set(build_flags.join(";")), ..Default::default() }; - let mut new_package = new_package.save(&txn).await?; + let mut new_package = new_package.save(db).await?; - // set build status to pending - let build = builds::ActiveModel { - pkg_id: new_package.id.clone(), - output: Set(None), - status: Set(Some(BuildStates::ENQUEUED_BUILD)), - // todo add new column for enqueued_time - start_time: Set(Some( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - )), - ..Default::default() - }; - let new_build = build.save(&txn).await?; - new_package.latest_build = Set(Some(new_build.id.clone().unwrap())); - let new_package = new_package.save(&txn).await?; + // trigger new build for each platform + for platform in platforms { + let txn = db.begin().await?; - txn.commit().await?; + // set build status to pending + let build = builds::ActiveModel { + pkg_id: new_package.id.clone(), + output: Set(None), + status: Set(Some(BuildStates::ENQUEUED_BUILD)), + // todo add new column for enqueued_time + platform: Set(platform.clone()), + start_time: Set(Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64, + )), + ..Default::default() + }; + let new_build = build.save(&txn).await?; - let _ = tx.send(Action::Build(Box::from(new_package), Box::from(new_build))); + // todo -- setting latest build to latest x86_64 build for now + if platform == "x86_64" { + new_package.latest_build = Set(Some(new_build.id.clone().unwrap())); + new_package = new_package.save(&txn).await?; + } + + txn.commit().await?; + let _ = tx.send(Action::Build( + Box::from(new_package.clone()), + Box::from(new_build), + )); + } Ok(()) } + +fn check_platforms(platforms: &Vec) -> anyhow::Result<()> { + for platform in platforms { + if !PLATFORMS.contains(&platform.as_str()) { + return Err(anyhow!("Invalid platform: {}", platform)); + } + } + Ok(()) +} diff --git a/backend/src/package/update.rs b/backend/src/package/update.rs index ee38f48..587bd0b 100644 --- a/backend/src/package/update.rs +++ b/backend/src/package/update.rs @@ -1,5 +1,5 @@ use crate::aur::api::get_info_by_name; -use crate::builder::types::Action; +use crate::builder::types::{Action, BuildStates}; use crate::db::prelude::Packages; use crate::db::{builds, packages}; use anyhow::anyhow; @@ -12,45 +12,62 @@ pub async fn package_update( pkg_id: i32, force: bool, tx: &Sender, -) -> anyhow::Result { +) -> anyhow::Result> { let txn = db.begin().await?; - let mut pkg_model: packages::ActiveModel = Packages::find_by_id(pkg_id) + let mut pkg_model_active: packages::ActiveModel = Packages::find_by_id(pkg_id) .one(&txn) .await? .ok_or(anyhow!("id not found"))? .into(); + let pkg_model: packages::Model = pkg_model_active.clone().try_into()?; - let pkg = get_info_by_name(pkg_model.name.clone().unwrap().as_str()) + let pkg = get_info_by_name(pkg_model.name.as_str()) .await .map_err(|_| anyhow!("couldn't download package metadata".to_string()))?; - if !force && pkg_model.version.clone().unwrap() == pkg.version { + if !force && pkg_model.version == Some(pkg.version.clone()) { return Err(anyhow!("Package is already up to date")); } - pkg_model.status = Set(3); - pkg_model.version = Set(pkg.version.clone()); - let pkg_model = pkg_model.save(&txn).await?; + pkg_model_active.status = Set(BuildStates::ENQUEUED_BUILD); + pkg_model_active.version = Set(Some(pkg.version.clone())); + let pkg_aktive_model = pkg_model_active.save(&txn).await?; + txn.commit().await?; + + let mut build_ids = vec![]; + + let pkg_model: packages::Model = pkg_aktive_model.clone().try_into()?; + for platform in pkg_model.platforms.clone().split(";") { + let build_id = update_platform(platform, pkg_model.clone(), db, tx).await?; + build_ids.push(build_id); + } + + Ok(build_ids) +} +pub async fn update_platform( + platform: &str, + pkg: packages::Model, + db: &DatabaseConnection, + tx: &Sender, +) -> anyhow::Result { + let txn = db.begin().await?; // set build status to pending + let start_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let build = builds::ActiveModel { - pkg_id: pkg_model.id.clone(), + pkg_id: Set(pkg.id), output: Set(None), - status: Set(Some(3)), - start_time: Set(Some( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64, - )), + status: Set(Some(BuildStates::ENQUEUED_BUILD)), + start_time: Set(Some(start_time)), + platform: Set(platform.to_string()), ..Default::default() }; let new_build = build.save(&txn).await?; let build_id = new_build.id.clone().unwrap(); txn.commit().await?; - let _ = tx.send(Action::Build(Box::from(pkg_model), Box::from(new_build))); - + let pkg_am: packages::ActiveModel = pkg.into(); + let _ = tx.send(Action::Build(Box::from(pkg_am), Box::from(new_build))); Ok(build_id) } diff --git a/backend/src/pacman-repo-utils/Cargo.toml b/backend/src/pacman-repo-utils/Cargo.toml index fa9e843..4f39950 100644 --- a/backend/src/pacman-repo-utils/Cargo.toml +++ b/backend/src/pacman-repo-utils/Cargo.toml @@ -10,10 +10,11 @@ edition = "2021" base64 = "0.22.1" md5 = "0.7.0" sha2 = "0.10.8" -flate2 = "1.0.31" +flate2 = "1.0.34" +xz2 = "0.1.7" zstd = "0.13.2" -tar = "0.4.41" -anyhow = "1.0.86" +tar = "0.4.42" +anyhow = "1.0.89" log = "0.4.22" \ No newline at end of file diff --git a/backend/src/pacman-repo-utils/src/repo_add.rs b/backend/src/pacman-repo-utils/src/repo_add.rs index 7620245..f427b39 100644 --- a/backend/src/pacman-repo-utils/src/repo_add.rs +++ b/backend/src/pacman-repo-utils/src/repo_add.rs @@ -3,13 +3,14 @@ use anyhow::anyhow; use crate::pkginfo::parser::Pkginfo; use crate::repo_database::db::add_to_db_file; use crate::repo_database::desc::Desc; -use log::{debug, error}; +use log::{debug, error, warn}; use sha2::{Digest, Sha256}; use std::fs::{self, File}; -use std::io::Read; +use std::io::{BufReader, Read}; use std::path::Path; use tar::Archive; -use zstd::Decoder; +use xz2::read::XzDecoder; +use zstd::stream::read::Decoder as ZstdDecoder; pub fn repo_add_impl( pkgfile: &str, @@ -21,19 +22,40 @@ pub fn repo_add_impl( // Path to the .tar.zst file let file = File::open(Path::new(pkgfile))?; - let mut archive = Archive::new(Decoder::new(file)?); + let ext = Path::new(pkgfile).extension().and_then(|e| e.to_str()); - // Iterate over the entries in the tar archive - for entry in archive.entries()? { - let entry = entry?; - - if !entry.path()?.display().to_string().starts_with('.') { - files.push(format!("{}", entry.path()?.display())); + // Select the appropriate decompression method + let decompressor: Box = match ext { + Some("zst") => { + let decoder = ZstdDecoder::new(BufReader::new(file))?; + Box::new(decoder) + } + Some("xz") => { + let decoder = XzDecoder::new(BufReader::new(file)); + Box::new(decoder) } + _ => { + return Err(anyhow!("Unsupported file type")); + } + }; + let mut archive = Archive::new(decompressor); - if entry.path()? == Path::new(".PKGINFO") { - debug!("Found .PKGINFO file in '{}'.", pkgfile); - pkginfo.parse(entry)?; + // Iterate over the entries in the tar archive + for entry in archive.entries()? { + match entry { + Ok(entry) => { + if let Ok(path) = entry.path() { + if !path.display().to_string().starts_with('.') { + files.push(format!("{}", path.display())); + } + + if path == Path::new(".PKGINFO") { + debug!("Found .PKGINFO file in '{}'.", pkgfile); + pkginfo.parse(entry)?; + } + } + } + Err(e) => warn!("Error reading entry: {:?}", e), } } diff --git a/backend/src/pacman-repo-utils/src/repo_init.rs b/backend/src/pacman-repo-utils/src/repo_init.rs index 8cc872d..83d5c19 100644 --- a/backend/src/pacman-repo-utils/src/repo_init.rs +++ b/backend/src/pacman-repo-utils/src/repo_init.rs @@ -15,7 +15,7 @@ pub fn init_repo_impl(path: &PathBuf, name: &str) -> anyhow::Result<()> { // create repo folder info!("Initializing empty pacman Repo archive"); - _ = fs::create_dir(path); + _ = fs::create_dir_all(path); create_empty_archive(&path, &name, "db")?; create_empty_archive(&path, &name, "files")?; diff --git a/backend/src/repo/mod.rs b/backend/src/repo/mod.rs index b5614dd..f39b66a 100644 --- a/backend/src/repo/mod.rs +++ b/backend/src/repo/mod.rs @@ -1 +1,2 @@ +pub mod platforms; pub mod utils; diff --git a/backend/src/repo/platforms.rs b/backend/src/repo/platforms.rs new file mode 100644 index 0000000..eb0bdda --- /dev/null +++ b/backend/src/repo/platforms.rs @@ -0,0 +1 @@ +pub static PLATFORMS: &[&str] = &["x86_64", "aarch64", "armv7h"]; diff --git a/backend/src/repo/utils.rs b/backend/src/repo/utils.rs index 031631a..9886431 100644 --- a/backend/src/repo/utils.rs +++ b/backend/src/repo/utils.rs @@ -15,18 +15,21 @@ pub async fn try_remove_archive_file( .all(db) .await?; if package_files.is_empty() { - let filename = file.filename.clone(); - file.delete(db).await?; + let platform = file.platform.clone(); pacman_repo_utils::repo_remove( - filename.clone(), - "./repo/repo.db.tar.gz".to_string(), - "./repo/repo.files.tar.gz".to_string(), + file.filename.clone(), + format!("./repo/{platform}/repo.db.tar.gz"), + format!("./repo/{platform}/repo.files.tar.gz"), )?; - match fs::remove_file(format!("./repo/{}", filename)) { - Ok(_) => info!("Removed old file: {}", filename), - Err(_) => warn!("Failed to remove package file: {}", filename), + + let file_path = format!("./repo/{}/{}", platform, file.filename); + match fs::remove_file(file_path.clone()) { + Ok(_) => info!("Removed old file: {}", file_path), + Err(_) => warn!("Failed to remove package file: {}", file_path), } + + file.delete(db).await?; } Ok(()) diff --git a/backend/src/scheduler/aur_version_update.rs b/backend/src/scheduler/aur_version_update.rs index 08e4099..b4c915c 100644 --- a/backend/src/scheduler/aur_version_update.rs +++ b/backend/src/scheduler/aur_version_update.rs @@ -55,7 +55,7 @@ async fn aur_check_versions(db: DatabaseConnection) -> anyhow::Result<()> { let latest_version = package.version.clone().unwrap(); package.latest_aur_version = Set(Option::from(result.version.clone())); - package.out_of_date = Set(if latest_version == result.version { + package.out_of_date = Set(if latest_version == Some(result.version.clone()) { 0 } else { 1 diff --git a/backend/src/utils/logger.rs b/backend/src/utils/logger.rs index 250216d..2e3ec4b 100644 --- a/backend/src/utils/logger.rs +++ b/backend/src/utils/logger.rs @@ -1,6 +1,5 @@ use env_logger::Env; use log::LevelFilter; -use std::env; use std::str::FromStr; pub fn init_logger() { @@ -14,8 +13,8 @@ pub fn init_logger() { // increase default rocket logging to warn .filter_module( "rocket", - LevelFilter::from_str(env::var(env_name).unwrap_or("warn".to_string()).as_str()) - .unwrap_or(LevelFilter::Warn), + LevelFilter::from_str("warn".to_string().as_str()).unwrap_or(LevelFilter::Warn), ) + .filter_module("hyper::proto", LevelFilter::Warn) .init(); } diff --git a/backend/src/utils/startup.rs b/backend/src/utils/startup.rs index ff72afa..246bc65 100644 --- a/backend/src/utils/startup.rs +++ b/backend/src/utils/startup.rs @@ -5,6 +5,16 @@ use tokio::fs; #[cfg(debug_assertions)] use log::warn; +use crate::repo::platforms::PLATFORMS; +#[cfg(not(debug_assertions))] +#[cfg(target_arch = "x86_64")] +use { + log::debug, + std::fs::File, + std::io::{BufRead, BufReader, Write}, + std::path::Path, +}; + const CONTAINER_STORAGE_DIRS: [&str; 2] = ["/run/containers/storage", "/run/libpod"]; const START_BANNER: &str = r" _ _ _____ _____ _ @@ -33,7 +43,50 @@ pub async fn startup_tasks() { } } - if let Err(e) = pacman_repo_utils::init_repo(&PathBuf::from("./repo"), "repo") { - error!("Failed to initialize pacman repo: {:?}", e); + for platform in PLATFORMS { + if let Err(e) = + pacman_repo_utils::init_repo(&PathBuf::from(format!("./repo/{platform}")), "repo") + { + error!("Failed to initialize pacman repo: {:?}", e); + } } + + // disable on debug builds since annoying bc. of root permissions + #[cfg(not(debug_assertions))] + #[cfg(target_arch = "x86_64")] + init_qemu_binfmt().await.unwrap(); +} + +/// This is required to initialize the binfmt configuration for QEMU on x86_64 correctly +/// aarch64 is not supported by qemu binfmt +/// see https://stackoverflow.com/questions/75954301/using-sudo-in-podman-with-qemu-architecture-emulation-leads-to-sudo-effective-u +#[cfg(not(debug_assertions))] +#[cfg(target_arch = "x86_64")] +async fn init_qemu_binfmt() -> anyhow::Result<()> { + let source_dir = Path::new("/usr/lib/binfmt.d"); + let target_dir = Path::new("/etc/binfmt.d"); + + // Iterate over all .conf files in the source directory + for entry in std::fs::read_dir(source_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("conf") { + let file = File::open(&path)?; + let reader = BufReader::new(file); + + // Create the target file path + let target_path = target_dir.join(path.file_name().unwrap()); + let mut target_file = File::create(&target_path)?; + + for mut line in reader.lines().map_while(Result::ok) { + line.push('C'); + target_file.write_all(line.as_bytes())?; + } + + debug!("Created qemu binfmt config: {}", path.display()); + } + } + + Ok(()) } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..81a504c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +version: '3' +services: + aurcache: + build: + context: . + dockerfile: docker/Dockerfile + ports: + - "8080:8080" + - "8081:8081" + environment: + - LOG_LEVEL=debug + volumes: + - ./aurcache/db:/app/db + - ./aurcache/repo:/app/repo + privileged: true \ No newline at end of file diff --git a/Dockerfile b/docker/Dockerfile similarity index 61% rename from Dockerfile rename to docker/Dockerfile index 15bc452..a2771be 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,4 @@ +ARG TARGET_ARCH=linux/amd64 ARG LATEST_COMMIT_SHA=dev FROM ghcr.io/cirruslabs/flutter:3.24.3 AS frontend_builder @@ -6,7 +7,7 @@ WORKDIR /app COPY frontend /app RUN flutter build web --release -FROM rust AS builder +FROM rust:1.80.1 AS builder ARG LATEST_COMMIT_SHA ENV LATEST_COMMIT_SHA ${LATEST_COMMIT_SHA} # Install necessary tools and dependencies @@ -20,15 +21,14 @@ COPY backend/Cargo.toml /app COPY --from=frontend_builder /app/build/web /app/web -# Build the Rust binary -RUN cargo build --release --features static - -# Stage 2: Create the final image -FROM quay.io/podman/stable +ARG TARGET_ARCH +ADD docker/build-rust.sh /root +RUN bash /root/build-rust.sh $TARGET_ARCH +FROM --platform=$TARGET_ARCH quay.io/podman/stable # Copy the built binary from the previous stage -COPY --from=builder --chmod=0755 /app/target/release/aurcache /usr/local/bin/aurcache -COPY --chmod=0755 entrypoint.sh /entrypoint.sh +COPY --from=builder --chmod=0755 /app/target/aurcache /usr/local/bin/aurcache +COPY --chmod=0755 docker/entrypoint.sh /entrypoint.sh WORKDIR /app CMD /entrypoint.sh diff --git a/docker/add-aur.sh b/docker/add-aur.sh new file mode 100644 index 0000000..32cac1a --- /dev/null +++ b/docker/add-aur.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# this script takes two arguments and sets up unattended AUR access for user ${1} via a helper, ${2} +set -o pipefail +set -o errexit +set -o nounset +set -o verbose +set -o xtrace + +AUR_USER="${1:-ab}" + +# we're gonna need sudo to use the helper properly +pacman -Sy --noconfirm +pacman --sync --needed --noconfirm --noprogressbar sudo base-devel git || echo "Nothing to do" + +# create the user +AUR_USER_HOME="/var/${AUR_USER}" +useradd "${AUR_USER}" --system --shell /usr/bin/nologin --create-home --home-dir "${AUR_USER_HOME}" + +# lock out the AUR_USER's password +passwd --lock "${AUR_USER}" + +# give the aur user passwordless sudo powers for pacman +echo "${AUR_USER} ALL=(ALL) NOPASSWD: /usr/bin/pacman" > "/etc/sudoers.d/allow_${AUR_USER}_to_pacman" + +# let root cd with sudo +echo "root ALL=(ALL) CWD=* ALL" > /etc/sudoers.d/permissive_root_Chdir_Spec + +# build config setup +sudo -u ${AUR_USER} -D~ bash -c 'mkdir -p .config/pacman' + +# use all possible cores for builds +sudo -u ${AUR_USER} -D~ bash -c 'echo MAKEFLAGS="-j\$(nproc)" > .config/pacman/makepkg.conf' + +# don't compress the packages built here +#sudo -u ${AUR_USER} -D~ bash -c 'echo PKGEXT=".pkg.tar" >> .config/pacman/makepkg.conf' + +# setup storage for AUR packages built +NEW_PKGDEST="/var/cache/makepkg/pkg" +NPDP=$(dirname "${NEW_PKGDEST}") +mkdir -p "${NPDP}" +install -o "${AUR_USER}" -d "${NEW_PKGDEST}" +sudo -u ${AUR_USER} -D~ bash -c "echo \"PKGDEST=${NEW_PKGDEST}\" >> .config/pacman/makepkg.conf" + +# setup place for foreign packages +FOREIGN_PKG="/var/cache/foreign-pkg" +FPP=$(dirname "${FOREIGN_PKG}") +mkdir -p "${FPP}" +install -o "${AUR_USER}" -d "${FOREIGN_PKG}" + +# get helper pkgbuild +sudo -u "${AUR_USER}" -D~ bash -c "curl --silent --location https://aur.archlinux.org/cgit/aur.git/snapshot/paru-bin.tar.gz | bsdtar -xvf -" + +# make helper +sudo -u "${AUR_USER}" -D~//paru-bin bash -c "makepkg -s --noprogressbar --noconfirm --needed" + +# install helper +pacman --upgrade --needed --noconfirm --noprogressbar "${NEW_PKGDEST}"/*.pkg.* + +# cleanup +sudo rm -rf "${NEW_PKGDEST}"/* +rm -rf "${AUR_USER_HOME}/paru-bin" +rm -rf "${AUR_USER_HOME}/.cache/go-build" +rm -rf "${AUR_USER_HOME}/.cargo" + +# chuck deps +pacman -Rns --noconfirm $(pacman -Qtdq) || echo "Nothing to remove" \ No newline at end of file diff --git a/docker/build-rust.sh b/docker/build-rust.sh new file mode 100644 index 0000000..edf29e6 --- /dev/null +++ b/docker/build-rust.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# this script takes two arguments and sets up unattended AUR access for user ${1} via a helper, ${2} +set -o pipefail +set -o errexit +set -o nounset +set -o verbose +set -o xtrace + +TARGET_ARCH="${1:-ab}" + +if [ "$TARGET_ARCH" == "linux/arm64/v8" ]; then + rustup target add aarch64-unknown-linux-gnu + apt update -y && apt install -y gcc-aarch64-linux-gnu + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc cargo build --release --target=aarch64-unknown-linux-gnu --features static + mv target/aarch64-unknown-linux-gnu/release/aurcache target/aurcache +else + cargo build --release --features static + mv target/release/aurcache target/aurcache +fi \ No newline at end of file diff --git a/docker/builder.Dockerfile b/docker/builder.Dockerfile new file mode 100644 index 0000000..6bc427e --- /dev/null +++ b/docker/builder.Dockerfile @@ -0,0 +1,4 @@ +FROM lopsided/archlinux:latest + +ADD docker/add-aur.sh /root +RUN bash /root/add-aur.sh ab paru \ No newline at end of file diff --git a/entrypoint.sh b/docker/entrypoint.sh similarity index 100% rename from entrypoint.sh rename to docker/entrypoint.sh diff --git a/docs/docs/setup/pacman-repo.md b/docs/docs/setup/pacman-repo.md index 5a5ec68..9adaca5 100644 --- a/docs/docs/setup/pacman-repo.md +++ b/docs/docs/setup/pacman-repo.md @@ -6,5 +6,5 @@ Add the following to your `/etc/pacman.conf` on your target machine to use the r # nano /etc/pacman.conf [repo] SigLevel = Optional TrustAll -Server = http://:8080/ +Server = http://:8080/$arch ``` diff --git a/frontend/lib/api/builds.dart b/frontend/lib/api/builds.dart index d9dc737..7940d1e 100644 --- a/frontend/lib/api/builds.dart +++ b/frontend/lib/api/builds.dart @@ -43,4 +43,9 @@ extension BuildsAPI on ApiClient { final resp = await getRawClient().get(uri); return resp.data.toString(); } + + Future retryBuild({required int id}) async { + final resp = await getRawClient().post("/build/$id/retry"); + return resp.data as int; + } } diff --git a/frontend/lib/api/packages.dart b/frontend/lib/api/packages.dart index 27f98a6..a2a0573 100644 --- a/frontend/lib/api/packages.dart +++ b/frontend/lib/api/packages.dart @@ -1,35 +1,63 @@ -import '../models/package.dart'; +import 'package:aurcache/models/extended_package.dart'; + +import '../models/simple_packge.dart'; import 'api_client.dart'; extension PackagesAPI on ApiClient { - Future> listPackages({int? limit}) async { + Future> listPackages({int? limit}) async { final resp = await getRawClient() .get("/packages/list", queryParameters: {'limit': limit}); final responseObject = resp.data as List; - final List packages = - responseObject.map((e) => Package.fromJson(e)).toList(growable: false); + final List packages = responseObject + .map((e) => SimplePackage.fromJson(e)) + .toList(growable: false); return packages; } - Future getPackage(int id) async { + Future getPackage(int id) async { final resp = await getRawClient().get("/package/$id"); - final package = Package.fromJson(resp.data); + final package = ExtendedPackage.fromJson(resp.data); return package; } - Future addPackage({required String name}) async { - final resp = await getRawClient().post("/package", data: {'name': name}); + Future patchPackage( + {required int id, + String? name, + bool? outofdate, + int? status, + String? version, + latest_aur_version, + latest_build, + List? platforms, + List? build_flags}) async { + final resp = await getRawClient().patch("/package/$id", data: { + "name": name, + "status": status, + "out_of_date": outofdate, + "version": version, + "latest_aur_version": latest_aur_version, + "latest_build": latest_build, + "build_flags": build_flags, + "platforms": platforms + }); + return resp.statusCode == 200; + } + + Future addPackage( + {required String name, required List selectedArchs}) async { + final resp = await getRawClient() + .post("/package", data: {'name': name, 'platforms': selectedArchs}); print(resp.data); } - Future updatePackage({bool force = false, required int id}) async { + Future> updatePackage({bool force = false, required int id}) async { final resp = await getRawClient() .post("/package/$id/update", data: {'force': force}); print(resp.data); - return resp.data as int; + return resp.data as List; } Future deletePackage(int id) async { diff --git a/frontend/lib/components/add_package_popup.dart b/frontend/lib/components/add_package_popup.dart new file mode 100644 index 0000000..4af91ce --- /dev/null +++ b/frontend/lib/components/add_package_popup.dart @@ -0,0 +1,105 @@ +import 'package:aurcache/constants/platforms.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tags_x/flutter_tags_x.dart'; + +class AddPackagePopup extends StatelessWidget { + const AddPackagePopup( + {super.key, required this.packageName, required this.successCallback}); + final String packageName; + final void Function(List successCallback) successCallback; + + @override + Widget build(BuildContext context) { + List selectedArchs = ["x86_64"]; + + return Stack( + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(false); // Dismiss dialog on outside tap + }, + child: Container( + color: Colors.black.withOpacity(0.5), // Adjust opacity for blur + ), + ), + // Delete confirmation dialog + AlertDialog( + title: Text("Install package $packageName?"), + content: SizedBox( + height: 150, + width: 450, + child: Column( + children: [ + const Text( + "Select the target architectures you want this package build for:"), + const SizedBox( + height: 10, + ), + ArchTags(selectedArchs: selectedArchs), + const SizedBox( + height: 15, + ), + const Text( + "Remember: Supported platforms depend strongly on the AUR package and its PKGBUILD."), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + successCallback(selectedArchs); + }, + child: const Text('Install'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); // Dismiss dialog + }, + child: const Text('Cancel'), + ), + ], + ), + ], + ); + } +} + +Future showPackageAddPopup( + BuildContext context, + String packageName, + void Function(List) successCallback, +) async { + return (await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => AddPackagePopup( + packageName: packageName, successCallback: successCallback), + ))!; +} + +class ArchTags extends StatelessWidget { + const ArchTags({super.key, required this.selectedArchs}); + + final List selectedArchs; + + @override + Widget build(BuildContext context) { + return Tags( + itemBuilder: (idx) => ItemTags( + index: idx, + title: Platforms[idx], + active: selectedArchs.contains(Platforms[idx]), + activeColor: Colors.green, + onPressed: (i) { + if (i.active!) { + selectedArchs.add(i.title!); + } else { + selectedArchs.remove(i.title!); + } + }, + ), + itemCount: Platforms.length, + ); + } +} diff --git a/frontend/lib/components/api/APIBuilder.dart b/frontend/lib/components/api/APIBuilder.dart index 22d0426..702d629 100644 --- a/frontend/lib/components/api/APIBuilder.dart +++ b/frontend/lib/components/api/APIBuilder.dart @@ -60,6 +60,9 @@ class _APIBuilderState return FutureBuilder( future: fut, builder: (context, snapshot) { + if (snapshot.hasError) { + print(snapshot.error); + } if (snapshot.hasData) { return widget.onData(snapshot.data!); } else { diff --git a/frontend/lib/components/aur_search_table.dart b/frontend/lib/components/aur_search_table.dart index f0b606d..5bf2d9b 100644 --- a/frontend/lib/components/aur_search_table.dart +++ b/frontend/lib/components/aur_search_table.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../api/API.dart'; import '../constants/color_constants.dart'; +import 'add_package_popup.dart'; import 'confirm_popup.dart'; class AurSearchTable extends StatelessWidget { @@ -39,13 +40,11 @@ class AurSearchTable extends StatelessWidget { TextButton( child: const Text("Install", style: TextStyle(color: greenColor)), onPressed: () async { - final confirmResult = await showConfirmationDialog( - context, - "Install Package?", - "Are you sure to install Package: ${package.name}", () async { - await API.addPackage(name: package.name); + final confirmResult = await showPackageAddPopup( + context, package.name, (archs) async { + await API.addPackage(name: package.name, selectedArchs: archs); context.go("/"); - }, null); + }); if (!confirmResult) return; }, ), diff --git a/frontend/lib/components/build_flag_settings.dart b/frontend/lib/components/build_flag_settings.dart new file mode 100644 index 0000000..2b77a3c --- /dev/null +++ b/frontend/lib/components/build_flag_settings.dart @@ -0,0 +1,87 @@ +import 'package:aurcache/models/extended_package.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_tags_x/flutter_tags_x.dart'; + +class BuildFlagSettings extends StatefulWidget { + const BuildFlagSettings( + {super.key, required this.pkg, required this.changed}); + final ExtendedPackage pkg; + final void Function(List) changed; + + @override + State createState() => _BuildFlagSettingsState(); +} + +class _BuildFlagSettingsState extends State { + List buildFlags = []; + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + buildFlags = widget.pkg.selected_build_flags.toList(growable: true); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 5, + ), + const Text( + "Build flags:", + style: TextStyle(fontSize: 18), + textAlign: TextAlign.start, + ), + const SizedBox( + height: 20, + ), + Tags( + itemBuilder: (idx) => ItemTags( + index: idx, + title: buildFlags[idx], + active: true, + activeColor: Colors.white38, + pressEnabled: false, + removeButton: ItemTagsRemoveButton( + onRemoved: () { + setState(() { + buildFlags.remove(buildFlags[idx]); + }); + widget.changed(buildFlags); + return true; + }, + ), + ), + itemCount: buildFlags.length, + ), + const SizedBox( + height: 15, + ), + const Text("Add new build flags:"), + SizedBox( + width: 200, + child: TextField( + controller: _controller, + decoration: InputDecoration( + label: const Text("--noconfirm"), + suffixIcon: IconButton( + onPressed: () { + setState(() { + if (_controller.text.isNotEmpty && + !buildFlags.contains(_controller.text)) { + buildFlags.add(_controller.text); + widget.changed(buildFlags); + } + }); + }, + icon: const Icon(Icons.add))), + ), + ) + ], + ); + } +} diff --git a/frontend/lib/components/builds_table.dart b/frontend/lib/components/builds_table.dart index 9f9ce43..ddba408 100644 --- a/frontend/lib/components/builds_table.dart +++ b/frontend/lib/components/builds_table.dart @@ -24,6 +24,9 @@ class BuildsTable extends StatelessWidget { DataColumn( label: Text("Version"), ), + DataColumn( + label: Text("Platform"), + ), DataColumn( label: Text("Status"), ), @@ -38,6 +41,7 @@ class BuildsTable extends StatelessWidget { DataCell(Text(build.id.toString())), DataCell(Text(build.pkg_name)), DataCell(Text(build.version)), + DataCell(Text(build.platform)), DataCell(IconButton( icon: Icon( switchSuccessIcon(build.status), diff --git a/frontend/lib/components/dashboard/your_packages.dart b/frontend/lib/components/dashboard/your_packages.dart index a74cf3e..7a1fe30 100644 --- a/frontend/lib/components/dashboard/your_packages.dart +++ b/frontend/lib/components/dashboard/your_packages.dart @@ -4,7 +4,7 @@ import 'package:aurcache/providers/api/packages_provider.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../constants/color_constants.dart'; -import '../../models/package.dart'; +import '../../models/simple_packge.dart'; import '../table_info.dart'; class YourPackages extends StatelessWidget { @@ -25,7 +25,7 @@ class YourPackages extends StatelessWidget { "Your Packages", style: Theme.of(context).textTheme.titleMedium, ), - APIBuilder, PackagesDTO>( + APIBuilder, PackagesDTO>( key: const Key("Packages on dashboard"), interval: const Duration(seconds: 10), dto: PackagesDTO(limit: 10), diff --git a/frontend/lib/components/packages_table.dart b/frontend/lib/components/packages_table.dart index 510b9fc..a9faf98 100644 --- a/frontend/lib/components/packages_table.dart +++ b/frontend/lib/components/packages_table.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../api/API.dart'; import '../constants/color_constants.dart'; -import '../models/package.dart'; +import '../models/simple_packge.dart'; import '../providers/api/builds_provider.dart'; import '../providers/api/packages_provider.dart'; import '../providers/api/stats_provider.dart'; @@ -14,7 +14,7 @@ import 'confirm_popup.dart'; class PackagesTable extends StatelessWidget { const PackagesTable({super.key, required this.data}); - final List data; + final List data; @override Widget build(BuildContext context) { @@ -45,7 +45,7 @@ class PackagesTable extends StatelessWidget { data.map((e) => buildDataRow(e, context)).toList(growable: false)); } - DataRow buildDataRow(Package package, BuildContext context) { + DataRow buildDataRow(SimplePackage package, BuildContext context) { return DataRow( cells: [ DataCell(Text(package.id.toString())), diff --git a/frontend/lib/components/platform_settings.dart b/frontend/lib/components/platform_settings.dart new file mode 100644 index 0000000..edfbf4f --- /dev/null +++ b/frontend/lib/components/platform_settings.dart @@ -0,0 +1,69 @@ +import 'package:aurcache/models/extended_package.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_tags_x/flutter_tags_x.dart'; + +import '../constants/platforms.dart'; + +class PlatformSettings extends StatefulWidget { + const PlatformSettings({super.key, required this.pkg, required this.changed}); + final ExtendedPackage pkg; + final void Function(List) changed; + + @override + State createState() => _PlatformSettingsState(); +} + +class _PlatformSettingsState extends State { + List platforms = []; + + @override + void initState() { + super.initState(); + platforms = widget.pkg.selected_platforms.toList(growable: true); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 5, + ), + const Text( + "Selected build platforms:", + style: TextStyle(fontSize: 18), + textAlign: TextAlign.start, + ), + const SizedBox( + height: 20, + ), + Tags( + itemBuilder: (idx) => ItemTags( + index: idx, + title: Platforms[idx], + active: platforms.contains(Platforms[idx]), + activeColor: Colors.green, + onPressed: (i) { + if (i.active!) { + setState(() { + platforms.add(i.title!); + }); + } else { + setState(() { + platforms.remove(i.title!); + }); + } + widget.changed(platforms); + }, + ), + itemCount: Platforms.length, + ), + const SizedBox( + height: 15, + ), + ], + ); + } +} diff --git a/frontend/lib/components/routing/router.dart b/frontend/lib/components/routing/router.dart index f33aa4d..cc2c1c3 100644 --- a/frontend/lib/components/routing/router.dart +++ b/frontend/lib/components/routing/router.dart @@ -1,3 +1,4 @@ +import 'package:aurcache/screens/Package_settings_screen.dart'; import 'package:aurcache/screens/aur_screen.dart'; import 'package:aurcache/screens/build_screen.dart'; import 'package:aurcache/screens/builds_screen.dart'; @@ -54,6 +55,13 @@ final appRouter = GoRouter( return PackageScreen(pkgID: id); }, ), + GoRoute( + path: '/package/:id/settings', + builder: (context, state) { + final id = int.parse(state.pathParameters['id']!); + return Packagesettingsscreen(pkgID: id); + }, + ), ], ), ], diff --git a/frontend/lib/constants/platforms.dart b/frontend/lib/constants/platforms.dart new file mode 100644 index 0000000..151f685 --- /dev/null +++ b/frontend/lib/constants/platforms.dart @@ -0,0 +1 @@ +final Platforms = ["x86_64", "aarch64", "armv7h"]; diff --git a/frontend/lib/models/build.dart b/frontend/lib/models/build.dart index 02495d4..b383fc0 100644 --- a/frontend/lib/models/build.dart +++ b/frontend/lib/models/build.dart @@ -1,6 +1,6 @@ class Build { final int id; - final String pkg_name; + final String pkg_name, platform; final int pkg_id; final String version; final int status; @@ -11,6 +11,7 @@ class Build { {required this.id, required this.pkg_id, required this.pkg_name, + required this.platform, required this.version, required this.start_time, required this.end_time, @@ -31,6 +32,7 @@ class Build { end_time: endTime, pkg_name: json["pkg_name"] as String, version: json["version"] as String, + platform: json["platform"] as String, ); } } diff --git a/frontend/lib/models/extended_package.dart b/frontend/lib/models/extended_package.dart new file mode 100644 index 0000000..5634b6e --- /dev/null +++ b/frontend/lib/models/extended_package.dart @@ -0,0 +1,55 @@ +class ExtendedPackage { + final int id; + final String name; + final bool outofdate; + final int status, last_updated, first_submitted; + final String latest_version, latest_aur_version, aur_url; + final String? licenses, maintainer, project_url, description; + final bool aur_flagged_outdated; + final List selected_platforms; + final List selected_build_flags; + + ExtendedPackage({ + required this.id, + required this.name, + required this.status, + required this.latest_version, + required this.latest_aur_version, + required this.outofdate, + required this.last_updated, + required this.first_submitted, + required this.licenses, + required this.maintainer, + required this.aur_flagged_outdated, + required this.selected_platforms, + required this.selected_build_flags, + required this.aur_url, + required this.project_url, + required this.description, + }); + + factory ExtendedPackage.fromJson(Map json) { + return ExtendedPackage( + id: json["id"] as int, + outofdate: json["outofdate"] as num != 0, + status: json["status"] as int, + name: json["name"] as String, + latest_version: json["latest_version"] as String, + latest_aur_version: json["latest_aur_version"] as String, + last_updated: json["last_updated"] as int, + first_submitted: json["first_submitted"] as int, + licenses: json["licenses"] as String?, + maintainer: json["maintainer"] as String?, + aur_flagged_outdated: json["aur_flagged_outdated"] as bool, + selected_platforms: (json["selected_platforms"] as List) + .map((e) => e as String) + .toList(growable: false), + selected_build_flags: (json["selected_build_flags"] as List) + .map((e) => e as String) + .toList(growable: false), + aur_url: json['aur_url'] as String, + project_url: json['project_url'] as String?, + description: json['description'] as String?, + ); + } +} diff --git a/frontend/lib/models/package.dart b/frontend/lib/models/package.dart deleted file mode 100644 index 34d9a77..0000000 --- a/frontend/lib/models/package.dart +++ /dev/null @@ -1,26 +0,0 @@ -class Package { - final int id; - final String name; - final bool outofdate; - final int status; - final String latest_version, latest_aur_version; - - Package( - {required this.id, - required this.name, - required this.status, - required this.latest_version, - required this.latest_aur_version, - required this.outofdate}); - - factory Package.fromJson(Map json) { - return Package( - id: json["id"] as int, - outofdate: json["outofdate"] as num != 0, - status: json["status"] as int, - name: json["name"] as String, - latest_version: json["latest_version"] as String, - latest_aur_version: json["latest_aur_version"] as String, - ); - } -} diff --git a/frontend/lib/models/simple_packge.dart b/frontend/lib/models/simple_packge.dart new file mode 100644 index 0000000..42f1905 --- /dev/null +++ b/frontend/lib/models/simple_packge.dart @@ -0,0 +1,25 @@ +class SimplePackage { + final int id; + final String name; + final bool outofdate; + final int status; + final String latest_version, latest_aur_version; + + SimplePackage( + {required this.id, + required this.name, + required this.status, + required this.latest_version, + required this.latest_aur_version, + required this.outofdate}); + + factory SimplePackage.fromJson(Map json) { + return SimplePackage( + id: json["id"] as int, + outofdate: json["outofdate"] as num != 0, + status: json["status"] as int, + name: json["name"] as String, + latest_version: json["latest_version"] as String, + latest_aur_version: json["latest_aur_version"] as String); + } +} diff --git a/frontend/lib/providers/api/package_provider.dart b/frontend/lib/providers/api/package_provider.dart index 6aaf29a..426ce9b 100644 --- a/frontend/lib/providers/api/package_provider.dart +++ b/frontend/lib/providers/api/package_provider.dart @@ -1,7 +1,8 @@ import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/models/extended_package.dart'; import '../../api/API.dart'; -import '../../models/package.dart'; +import '../../models/simple_packge.dart'; import 'BaseProvider.dart'; class PackageDTO { @@ -10,7 +11,7 @@ class PackageDTO { PackageDTO({required this.pkgID}); } -class PackageProvider extends BaseProvider { +class PackageProvider extends BaseProvider { @override loadFuture(context, {dto}) { // todo search solution to force an exising dto diff --git a/frontend/lib/providers/api/packages_provider.dart b/frontend/lib/providers/api/packages_provider.dart index 4dee760..4827581 100644 --- a/frontend/lib/providers/api/packages_provider.dart +++ b/frontend/lib/providers/api/packages_provider.dart @@ -2,7 +2,7 @@ import 'package:aurcache/api/packages.dart'; import 'package:aurcache/providers/api/BaseProvider.dart'; import '../../api/API.dart'; -import '../../models/package.dart'; +import '../../models/simple_packge.dart'; class PackagesDTO { final int limit; @@ -10,7 +10,7 @@ class PackagesDTO { PackagesDTO({required this.limit}); } -class PackagesProvider extends BaseProvider, PackagesDTO> { +class PackagesProvider extends BaseProvider, PackagesDTO> { @override loadFuture(context, {dto}) { data = API.listPackages(limit: dto?.limit); diff --git a/frontend/lib/screens/Package_settings_screen.dart b/frontend/lib/screens/Package_settings_screen.dart new file mode 100644 index 0000000..504e42b --- /dev/null +++ b/frontend/lib/screens/Package_settings_screen.dart @@ -0,0 +1,86 @@ +import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/components/build_flag_settings.dart'; +import 'package:aurcache/components/platform_settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tags_x/flutter_tags_x.dart'; +import 'package:provider/provider.dart'; + +import '../api/API.dart'; +import '../components/api/APIBuilder.dart'; +import '../constants/platforms.dart'; +import '../models/extended_package.dart'; +import '../providers/api/package_provider.dart'; + +class Packagesettingsscreen extends StatefulWidget { + const Packagesettingsscreen({super.key, required this.pkgID}); + + final int pkgID; + + @override + State createState() => _PackagesettingsscreenState(); +} + +class _PackagesettingsscreenState extends State { + List buildFlags = []; + List buildPlatforms = []; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Package Settings"), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: TextButton( + onPressed: () async { + await API.patchPackage( + id: widget.pkgID, + build_flags: buildFlags, + platforms: buildPlatforms); + Navigator.pop(context); + }, + child: const Text("Save")), + ) + ], + ), + body: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => PackageProvider()), + ], + child: APIBuilder( + dto: PackageDTO(pkgID: widget.pkgID), + onLoad: () => const Text("loading"), + onData: (pkg) { + buildFlags = pkg.selected_build_flags; + buildPlatforms = pkg.selected_platforms; + + return Padding( + padding: const EdgeInsets.all(15.0), + child: Row( + children: [ + Expanded( + flex: 1, + child: PlatformSettings( + pkg: pkg, + changed: (List v) { + buildPlatforms = v; + }, + )), + Expanded( + flex: 1, + child: BuildFlagSettings( + pkg: pkg, + changed: (List v) { + buildFlags = v; + }, + )) + ], + ), + ); + }), + ), + ); + } +} diff --git a/frontend/lib/screens/build_screen.dart b/frontend/lib/screens/build_screen.dart index d35c0dc..70a9e0f 100644 --- a/frontend/lib/screens/build_screen.dart +++ b/frontend/lib/screens/build_screen.dart @@ -267,7 +267,7 @@ class _BuildScreenState extends State { ), ElevatedButton( onPressed: () async { - final buildid = await API.updatePackage(id: build.pkg_id); + final buildid = await API.retryBuild(id: build.id); context.pushReplacement("/build/$buildid"); }, child: const Text( diff --git a/frontend/lib/screens/package_screen.dart b/frontend/lib/screens/package_screen.dart index 08469f2..f5e0fb2 100644 --- a/frontend/lib/screens/package_screen.dart +++ b/frontend/lib/screens/package_screen.dart @@ -1,16 +1,18 @@ import 'package:aurcache/api/packages.dart'; import 'package:aurcache/components/api/APIBuilder.dart'; +import 'package:aurcache/models/extended_package.dart'; import 'package:aurcache/providers/api/builds_provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_tags_x/flutter_tags_x.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../api/API.dart'; import '../components/builds_table.dart'; import '../components/confirm_popup.dart'; import '../constants/color_constants.dart'; import '../models/build.dart'; -import '../models/package.dart'; import '../providers/api/package_provider.dart'; import '../providers/api/packages_provider.dart'; import '../providers/api/stats_provider.dart'; @@ -36,7 +38,7 @@ class _PackageScreenState extends State { ChangeNotifierProvider( create: (_) => PackageProvider()), ], - child: APIBuilder( + child: APIBuilder( dto: PackageDTO(pkgID: widget.pkgID), onLoad: () => const Text("loading"), onData: (pkg) { @@ -49,117 +51,42 @@ class _PackageScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - margin: const EdgeInsets.only(left: 15), - child: Text( - pkg.name, - style: const TextStyle(fontSize: 32), - ), - ), Row( children: [ - ElevatedButton( - onPressed: () async { - await showConfirmationDialog( - context, - "Force update Package", - "Are you sure to force an Package rebuild?", - () async { - await API.updatePackage( - force: true, id: pkg.id); - - context.pop(); - - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - }, - () {}, - ); - }, - child: const Text( - "Force Update", - style: TextStyle(color: Colors.yellowAccent), + Container( + margin: const EdgeInsets.only(left: 15), + child: Text( + pkg.name, + style: const TextStyle(fontSize: 32), ), ), - ElevatedButton( - onPressed: () async { - await showConfirmationDialog( - context, - "Delete Package", - "Are you sure to delete this Package?", - () async { - final succ = - await API.deletePackage(pkg.id); - if (succ) { - context.pop(); - - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - } - }, - () {}, - ); - }, - child: const Text( - "Delete", - style: TextStyle(color: Colors.redAccent), - ), - ), - const SizedBox( - width: 15, - ) + IconButton( + onPressed: () async { + await launchUrl( + Uri.parse(pkg.aur_url!), + webOnlyWindowName: '_blank', + ); + }, + icon: const Icon(Icons.link)) ], - ) + ), + _buildTopActionButtons(pkg) ], ), - const SizedBox( - height: 25, - ), - Container( - padding: const EdgeInsets.all(defaultPadding), - decoration: const BoxDecoration( - color: secondaryColor, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Builds of ${pkg.name}", - style: Theme.of(context).textTheme.titleMedium, - ), - SizedBox( - width: double.infinity, - child: APIBuilder, - BuildsDTO>( - key: const Key("Builds on Package info"), - dto: BuildsDTO(pkgID: pkg.id), - interval: const Duration(seconds: 5), - onData: (data) { - return BuildsTable(data: data); - }, - onLoad: () => const Text("no data"), - ), - ), - ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [_buildMainBody(pkg)], + ), ), - ), - ) + _buildSideBar(pkg), + ], + ), ], ), ); @@ -167,4 +94,244 @@ class _PackageScreenState extends State { ), ); } + + Widget _buildTopActionButtons(ExtendedPackage pkg) { + return Row( + children: [ + ElevatedButton( + onPressed: () async { + await showConfirmationDialog( + context, + "Force update Package", + "Are you sure to force an Package rebuild?", + () async { + await API.updatePackage(force: true, id: pkg.id); + + context.pop(); + + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + }, + () {}, + ); + }, + child: const Text( + "Force Update", + style: TextStyle(color: Colors.yellowAccent), + ), + ), + const SizedBox( + width: 10, + ), + ElevatedButton( + onPressed: () async { + await showConfirmationDialog( + context, + "Delete Package", + "Are you sure to delete this Package?", + () async { + final succ = await API.deletePackage(pkg.id); + if (succ) { + context.pop(); + + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + } + }, + () {}, + ); + }, + child: const Text( + "Delete", + style: TextStyle(color: Colors.redAccent), + ), + ), + const SizedBox( + width: 10, + ), + ElevatedButton( + onPressed: () { + context.push("/package/${pkg.id}/settings"); + }, + child: const Text( + "Settings", + style: TextStyle(color: Colors.blueAccent), + ), + ), + ], + ); + } + + Widget _buildSideBar(ExtendedPackage pkg) { + final last_updated = + DateTime.fromMillisecondsSinceEpoch(pkg.last_updated * 1000); + final first_submitted = + DateTime.fromMillisecondsSinceEpoch(pkg.first_submitted * 1000); + + return SizedBox( + width: 300, + child: Container( + color: secondaryColor, + padding: const EdgeInsets.all(defaultPadding), + margin: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 5, + ), + Text( + "Details for ${pkg.name}:", + style: const TextStyle(fontSize: 18), + textAlign: TextAlign.start, + ), + _sideCard( + title: "Latest AUR version", + subtitle: pkg.latest_aur_version, + ), + _sideCard( + title: "Last Updated", + subtitle: + "${last_updated.year}-${last_updated.month.toString().padLeft(2, '0')}-${last_updated.day.toString().padLeft(2, '0')}", + ), + _sideCard( + title: "First submitted", + subtitle: + "${first_submitted.year}-${first_submitted.month.toString().padLeft(2, '0')}-${first_submitted.day.toString().padLeft(2, '0')}", + ), + _sideCard( + title: "Licenses", + subtitle: pkg.licenses ?? "-", + ), + _sideCard( + title: "Maintainer", + subtitle: pkg.maintainer ?? "-", + ), + _sideCard( + title: "Flagged outdated", + subtitle: pkg.aur_flagged_outdated ? "yes" : "no", + ), + const Divider(), + const SizedBox( + height: 5, + ), + const Text( + "Selected build platforms:", + style: TextStyle(fontSize: 18), + textAlign: TextAlign.start, + ), + const SizedBox( + height: 20, + ), + Tags( + itemBuilder: (idx) => ItemTags( + index: idx, + title: pkg.selected_platforms[idx], + active: true, + activeColor: Colors.green, + pressEnabled: false, + ), + itemCount: pkg.selected_platforms.length, + ), + const SizedBox( + height: 15, + ), + const Divider(), + const SizedBox( + height: 5, + ), + const Text( + "Build flags:", + style: TextStyle(fontSize: 18), + textAlign: TextAlign.start, + ), + const SizedBox( + height: 20, + ), + Tags( + itemBuilder: (idx) => ItemTags( + index: idx, + title: pkg.selected_build_flags[idx], + active: true, + activeColor: Colors.white38, + pressEnabled: false, + ), + itemCount: pkg.selected_build_flags.length, + ), + ], + ), + ), + ); + } + + Widget _sideCard({required String title, required String subtitle}) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox( + height: 5, + ), + Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + const SizedBox( + height: 3, + ), + Text(subtitle), + const SizedBox( + height: 10, + ), + ]); + } + + Widget _buildMainBody(ExtendedPackage pkg) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (pkg.description != null) ...[ + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Text(pkg.description!), + ), + const SizedBox( + height: 25, + ) + ], + Container( + padding: const EdgeInsets.all(defaultPadding), + decoration: const BoxDecoration( + color: secondaryColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Builds of ${pkg.name}", + style: Theme.of(context).textTheme.titleMedium, + ), + SizedBox( + width: double.infinity, + child: APIBuilder, BuildsDTO>( + key: const Key("Builds on Package info"), + dto: BuildsDTO(pkgID: pkg.id), + interval: const Duration(seconds: 5), + onData: (data) { + return BuildsTable(data: data); + }, + onLoad: () => const Text("no data"), + ), + ), + ], + ), + ), + ) + ]); + } } diff --git a/frontend/lib/screens/packages_screen.dart b/frontend/lib/screens/packages_screen.dart index 2e3b58c..54e3a93 100644 --- a/frontend/lib/screens/packages_screen.dart +++ b/frontend/lib/screens/packages_screen.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import '../components/api/APIBuilder.dart'; import '../constants/color_constants.dart'; -import '../models/package.dart'; +import '../models/simple_packge.dart'; class PackagesScreen extends StatelessWidget { const PackagesScreen({super.key}); @@ -37,7 +37,8 @@ class PackagesScreen extends StatelessWidget { ), SizedBox( width: double.infinity, - child: APIBuilder, Object>( + child: APIBuilder, + Object>( key: const Key("Builds on seperate screen"), interval: const Duration(seconds: 10), onLoad: () => const Text("no data"), diff --git a/frontend/linux/flutter/generated_plugin_registrant.cc b/frontend/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/frontend/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/frontend/linux/flutter/generated_plugins.cmake b/frontend/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/frontend/linux/flutter/generated_plugins.cmake +++ b/frontend/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 62675c4..eb223d9 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -134,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.10+1" + flutter_tags_x: + dependency: "direct main" + description: + name: flutter_tags_x + sha256: b9e5e55ff79ff6b8250008eb3ea498ddd0a03213c441b25275484f601654a412 + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -405,6 +413,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + url: "https://pub.dev" + source: hosted + version: "6.3.10" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.dev" + source: hosted + version: "3.1.2" vector_graphics: dependency: transitive description: @@ -479,4 +551,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.5.0 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index dca5fa8..42f2217 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: go_router: ^14.2.0 provider: ^6.1.1 visibility_detector: ^0.4.0+2 + flutter_tags_x: any + url_launcher: any dev_dependencies: flutter_test: