diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e2aac28 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "dev-flutter-01", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml", + "docker-compose.yml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "dev-flutter", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/projects", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" + + "remoteUser": "developer" + // "postStartCommand": ". /opt/dev-init/dev.env" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..f7b2e23 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + dev-flutter: + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + # build: + # context: . + # dockerfile: .devcontainer/Dockerfile + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - ..:/workspaces:cached + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + diff --git a/.gitignore b/.gitignore index 16b4cc1..d40528f 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ doc/api/ # Built Visual Studio Code Extensions *.vsix +volumes/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6382208 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +FROM debian:bookworm-20241111-slim + +ARG USER_UID=1000 +ARG USER_GID=1000 + +ENV DEV_INIT_DIR=/opt/dev-init +ENV JAVA_VERSION="17" + +# Install essential packages +RUN dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + wget \ + curl \ + file \ + git \ + unzip \ + xz-utils \ + zip \ + libglu1-mesa \ + cmake cmake-curses-gui \ + gcc g++ \ + clang \ + cmake cmake-curses-gui \ + ninja-build pkg-config \ + libgtk-3-dev liblzma-dev \ + libstdc++-12-dev \ + sudo \ + micro \ + nano \ + llvm \ + libclang-dev \ + libc6:i386 \ + libncurses5:i386 \ + libstdc++6:i386 \ + lib32z1 \ + lib32z1-dev \ + openjdk-${JAVA_VERSION}-jdk \ + libpulse-java \ + gosu \ + && rm -rf /var/lib/apt/lists/* + +ENV JAVA_HOME /usr/lib/jvm/java-${JAVA_VERSION}-openjdk-amd64 + +# Create non-root user with specified UID and GID +RUN groupadd -g ${USER_GID} developer && \ + useradd -u ${USER_UID} -g ${USER_GID} -ms /bin/bash developer && \ + usermod -aG sudo developer && \ + echo "developer:developer" | chpasswd && \ + echo 'developer ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ + chmod 0440 /etc/sudoers + +RUN curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +RUN apt-get update -y && apt-get install -y ./google-chrome-stable_current_amd64.deb && \ + rm google-chrome-stable_current_amd64.deb + +COPY --chown=developer:developer sample_projects /opt/sample_projects +COPY dev-init dev-init $DEV_INIT_DIR +RUN chmod +x $DEV_INIT_DIR/* + +# add group 108 (kvm group) +RUN groupadd -g 108 kvm && \ + usermod -aG kvm developer + +COPY entrypoint.sh / +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 7838c84..3fa78b0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ -# dev-flutter +### Build and configure + +Run `docker compose build` to build the dev container. Then inside the container run selected init scripts. E.g.: + +``` +bash /opt/dev-init/dev-init-flutter.sh +bash /opt/dev-init/dev-init-rust.sh +bash /opt/dev-init/dev-init-android.sh +``` + +You can skip building the image by pulling `chodak166/dev-flutter`. + +Android SDK, gradle, flutter, rust and vscode data should be preserved as configured in compose file. The `/opt/dev-init/dev.env` file is also used by docker compose to setup environment variables. **See docker-compose.yml and .devcontainer/docker-compose.yml for details.** + +### Compile and debug the app + +Use `flutter create` or sample projects in `/opt/sample_projects` to perform first build and make gradle download dependencies. Then use `flutter run` to run the app. + + +### Run and debug in VSCode + +With `.devcontainer` present in root project just run 'Reopen in container' command (`ctrl+shift+p`) or run `dev-flutter-01` manually with: + +``` +docker compose run --name dev-flutter-01 -d dev-flutter +``` + +After connecting to the container install `flutter` and `dart` tools (dart-code.flutter and dart-code.dart-code). Installing toroxx.vscode-avdmanager is also recommended for AVD management.` + +AVD UI, flutter commands, dev tools and debug tools should be available from your IDE. + +### Run and debug USB device + +The `fluter devices` command should identify all connected USB devices along with emulators and system targets with no problem. E.g.: + +``` +$ flutter devices +Found 5 connected devices: + Armor 3 (mobile) • 3060MF1001017685 • android-arm64 • Android 8.1.0 (API 27) + BV7100 (mobile) • BV7100EEA0015872 • android-arm64 • Android 12 (API 31) + sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 14 (API 34) (emulator) + Linux (desktop) • linux • linux-x64 • Debian GNU/Linux 12 (bookworm) 5.15.0-125-generic + Chrome (web) • chrome • web-javascript • Google Chrome 131.0.6778.85 +``` +export ADB_TRACE=info + +In case of any issues try `adb kill-server` and then `adb start-server`. Run `export ADB_TRACE=all` to see verbose output. +You can also check open 5037 port with `fuser 5037/tcp`. Processes using this port can be killed with `fuser -k 5037/tcp`. -Docker image and scripts for flutter/rust/android development in vscode. \ No newline at end of file diff --git a/dev-init/dev-init-android.sh b/dev-init/dev-init-android.sh new file mode 100644 index 0000000..8345fcc --- /dev/null +++ b/dev-init/dev-init-android.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$SCRIPT_DIR/dev.env" + + +: ${ANDROID_SDK_ROOT:=/opt/android-sdk} +: ${ANDROID_VERSION:=34} +: ${ANDROID_BUILD_TOOLS_VERSION:=34.0.0} +: ${ANDROID_ARCHITECTURE:=x86_64} +: ${ANDROID_TOOLS_URL:="http://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"} + + +if [ ! -d $ANDROID_SDK_ROOT/cmdline-tools ]; then + cd /tmp + # Download and install Android SDK + wget -c $ANDROID_TOOLS_URL -O android-sdk.zip && \ + unzip android-sdk.zip -d /tmp && rm android-sdk.zip && \ + mkdir -p $ANDROID_SDK_ROOT/cmdline-tools/latest && \ + mv /tmp/cmdline-tools/* $ANDROID_SDK_ROOT/cmdline-tools/latest/ +fi + +SDKMANAGER="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" + +yes | $SDKMANAGER --licenses + +# Install SDK Platform Tools +$SDKMANAGER --install "platform-tools" +$SDKMANAGER --install "platforms;android-$ANDROID_VERSION" +$SDKMANAGER --install "build-tools;$ANDROID_BUILD_TOOLS_VERSION" +$SDKMANAGER --install "system-images;android-${ANDROID_VERSION};google_apis_playstore;${ANDROID_ARCHITECTURE}" +$SDKMANAGER --install "emulator" + +# for vscode android plugin: +mkdir -p ~/Library/Android/ ||: +ln -sf /opt/android-sdk/ ~/Library/Android/sdk ||: + +# Build demo project +"$ANDROID_SAMPLE_PROJECT/build.sh" + +flutter config --android-sdk $ANDROID_SDK_ROOT +flutter emulators --create + +echo "Android development environment setup completed successfully!" + diff --git a/dev-init/dev-init-flutter.sh b/dev-init/dev-init-flutter.sh new file mode 100644 index 0000000..f73b76d --- /dev/null +++ b/dev-init/dev-init-flutter.sh @@ -0,0 +1,38 @@ + +#!/bin/bash + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$SCRIPT_DIR/dev.env" + +# Download and setup Flutter +cd /tmp/ +curl -v -o flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz +tar xf flutter.tar.xz -C /tmp/ +mkdir -p 2>/dev/null ${FLUTTER_ROOT}/ ||: +mv /tmp/flutter/{.*,*} ${FLUTTER_ROOT}/ +rm flutter.tar.xz + +# Configure git +git config --global --add safe.directory ${FLUTTER_ROOT} + +# Configure Flutter +flutter config --no-analytics +flutter config --no-enable-android +flutter config --no-enable-ios +flutter config --no-enable-windows-desktop +flutter config --no-enable-macos-desktop +flutter config --enable-linux-desktop +flutter config --enable-web + +# Pre-download binaries and verify installation +flutter precache --linux --web +flutter doctor +flutter --version + +# Build demo project +"$FLUTTER_SAMPLE_PROJECT/build.sh" + +echo "Flutter development environment setup completed successfully!" diff --git a/dev-init/dev-init-rust.sh b/dev-init/dev-init-rust.sh new file mode 100644 index 0000000..ffa55d4 --- /dev/null +++ b/dev-init/dev-init-rust.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$SCRIPT_DIR/dev.env" + +mkdir -p ${RUSTUP_HOME} ${CARGO_HOME} 2>/dev/null ||: + +cd /tmp +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ +rustup default stable +rustup update +rustup component add rustfmt clippy +rustup target add wasm32-unknown-unknown +rustup toolchain install nightly +rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + +cargo install --version 2.6.0 flutter_rust_bridge_codegen +cargo install cargo-expand +cargo install wasm-pack + +dart pub global activate fvm +dart --disable-analytics + +# Build demo project +"$RUST_SAMPLE_PROJECT/build.sh" + +echo "Rust development environment setup completed successfully!" diff --git a/dev-init/dev.env b/dev-init/dev.env new file mode 100644 index 0000000..32fee7d --- /dev/null +++ b/dev-init/dev.env @@ -0,0 +1,10 @@ + +FLUTTER_ROOT=/opt/flutter +FLUTTER_VERSION=3.22.3 +FLUTTER_SAMPLE_PROJECT=/opt/sample_projects/flutter_linux_web_demo +RUST_SAMPLE_PROJECT=/opt/sample_projects/flutter_rust_demo +ANDROID_SAMPLE_PROJECT=/opt/sample_projects/flutter_android_demo +RUSTUP_HOME=/opt/rustup +CARGO_HOME=/opt/cargo +ANDROID_SDK_ROOT=/opt/android-sdk +PATH="${FLUTTER_ROOT}/bin:/opt/cargo/bin:$HOME/.pub-cache/bin:/opt/android-sdk/platform-tools:/opt/android-sdk/emulator:${PATH}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..194d83d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.8' +services: + dev-flutter: + image: chodak166/dev-flutter + container_name: dev-flutter-01 + build: + context: . + args: + USER_UID: ${UID:-1000} + USER_GID: ${GID:-1000} + network_mode: host + environment: + - DISPLAY=${DISPLAY} + - PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native + - TARGET_USER=developer + env_file: dev-init/dev.env + volumes: + - ./volumes/projects:/projects + - ./volumes/flutter:/opt/flutter + - ./volumes/rustup:/opt/rustup + - ./volumes/cargo:/opt/cargo + - ./volumes/android-sdk:/opt/android-sdk + - ./volumes/pub_cache/:/home/developer/.pub-cache + - ./volumes/home_android:/home/developer/.android + - ./volumes/home_gradle:/home/developer/.gradle + - ./volumes/home_vscode-server:/home/developer/.vscode-server + - /tmp/.X11-unix:/tmp/.X11-unix + - $HOME/.Xauthority:/developer/.Xauthority + - ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse + - /run/user/1000/at-spi:/run/user/1000/at-spi #TODO: pass UID? + - /dev/shm:/dev/shm + - ./dev-init:/opt/dev-init # debug + - .:/host # debug + + devices: + - /dev/dri:/dev/dri + - /dev/usb:/dev/usb + - /dev/udev:/dev/udev + - /dev/bus:/dev/bus + - /dev/kvm:/dev/kvm + - /dev/snd:/dev/snd + security_opt: + - seccomp:unconfined + privileged: true + cap_add: + - SYS_ADMIN + - NET_ADMIN + - SYS_PTRACE + stdin_open: true + tty: true + init: true diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d3032b0 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Exit on error +set -e + +set -a +. $DEV_INIT_DIR/dev.env +set +a + +# Check if TARGET_USER is set +if [ -z "$TARGET_USER" ]; then + echo "ERROR: TARGET_USER environment variable is not set" + exit 1 +fi + +targetHome=$(getent passwd $TARGET_USER | cut -f6 -d:) +targetUid=$(id -u $TARGET_USER) + +if [ -z "$TARGET_USER" ]; then + echo "ERROR: Cannot parse home directory for user $TARGET_USER" + exit 1 +fi + +# Change ownership of the specified directories +chown -R $TARGET_USER: $ANDROID_SDK_ROOT +chown -R $TARGET_USER: $targetHome/.gradle +chown -R $TARGET_USER: $targetHome/.android +chown -R $TARGET_USER: $targetHome/.pub-cache +chown -R $TARGET_USER: $targetHome/.vscode-server +chown -R $TARGET_USER: /opt/flutter +chown -R $TARGET_USER: /opt/rustup +chown -R $TARGET_USER: /opt/cargo +chown -R $TARGET_USER: /projects + +# fix +chown -R $TARGET_USER: /run/user/$targetUid +# chmod 777 /dev/kvm + +# If command line arguments are provided, run them as TARGET_USER +# Otherwise, run bash as TARGET_USER +if [ $# -eq 0 ]; then + exec gosu $TARGET_USER bash +else + exec gosu $TARGET_USER "$@" +fi diff --git a/sample_projects/flutter_android_demo/build.sh b/sample_projects/flutter_android_demo/build.sh new file mode 100755 index 0000000..e2dcf98 --- /dev/null +++ b/sample_projects/flutter_android_demo/build.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e # Exit on error +set -x # Print commands being executed + +cd "$(dirname "$0")" + +# Enable builds +# flutter config --enable-linux-desktop +# flutter config --enable-web +flutter config --enable-android + +if [ ! -f lib/main.dart ]; then + flutter create -t app . + flutter_rust_bridge_codegen integrate +fi + +# Create project structure (this will add platform-specific files) +# flutter create . + +# Get dependencies +# flutter pub get + +# Build Rust project first +# cd rust +# cargo build --release +# cd .. + +# Generate Flutter Rust Bridge bindings using config file +flutter_rust_bridge_codegen generate + +# Build Linux version +# flutter build linux --release + +# cp target/release/libnative.so build/linux/x64/release/bundle/lib/ + +# For web, we need to build the Rust code for wasm target +# cd rust +# rustup target add wasm32-unknown-unknown +# cargo build --release --target wasm32-unknown-unknown +# cd .. + +# Build Web version +# flutter build web --release +# flutter_rust_bridge_codegen build-web + +flutter build apk --verbose + diff --git a/sample_projects/flutter_linux_web_demo/build.sh b/sample_projects/flutter_linux_web_demo/build.sh new file mode 100755 index 0000000..3dcd202 --- /dev/null +++ b/sample_projects/flutter_linux_web_demo/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +cd "$(dirname "$0")" + +# Enable Linux and Web builds +flutter config --enable-linux-desktop +flutter config --enable-web + +# Create project structure (this will add platform-specific files) +flutter create . + +# Get dependencies +flutter pub get + +# Build Linux version +flutter build linux --release + +# Build Web version +flutter build web --release diff --git a/sample_projects/flutter_linux_web_demo/lib/main.dart b/sample_projects/flutter_linux_web_demo/lib/main.dart new file mode 100644 index 0000000..ae9b7b7 --- /dev/null +++ b/sample_projects/flutter_linux_web_demo/lib/main.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Linux & Web Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Flutter Linux & Web Demo'), + ), + body: const Center( + child: Text( + 'Hello, World!', + style: TextStyle(fontSize: 24), + ), + ), + ); + } +} diff --git a/sample_projects/flutter_linux_web_demo/pubspec.yaml b/sample_projects/flutter_linux_web_demo/pubspec.yaml new file mode 100644 index 0000000..7f1684a --- /dev/null +++ b/sample_projects/flutter_linux_web_demo/pubspec.yaml @@ -0,0 +1,20 @@ +name: flutter_linux_web_demo +description: A Flutter demo project for Linux and Web platforms +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/sample_projects/flutter_rust_demo/build.sh b/sample_projects/flutter_rust_demo/build.sh new file mode 100755 index 0000000..0b227cb --- /dev/null +++ b/sample_projects/flutter_rust_demo/build.sh @@ -0,0 +1,43 @@ +#!/bin/bash +cd "$(dirname "$0")" + +if [ ! -f lib/main.dart ]; then + flutter create . + flutter_rust_bridge_codegen integrate +fi + +set -e # Exit on error +set -x # Print commands being executed + +# Enable Linux and Web builds +flutter config --enable-linux-desktop +flutter config --enable-web + +# Create project structure (this will add platform-specific files) +# flutter create . + +# Get dependencies +flutter pub get + +# Build Rust project first +# cd rust +# cargo build --release +# cd .. + +# Generate Flutter Rust Bridge bindings using config file +flutter_rust_bridge_codegen generate + +# Build Linux version +flutter build linux --release +# cp target/release/libnative.so build/linux/x64/release/bundle/lib/ + +# For web, we need to build the Rust code for wasm target +# cd rust +# rustup target add wasm32-unknown-unknown +# cargo build --release --target wasm32-unknown-unknown +# cd .. + +# Build Web version +# flutter build web --release +flutter_rust_bridge_codegen build-web +cp -r web/pkg build/web/