모바일 앱
a410944 기준입니다.
DroidDesk는 Android의 Linux 커널을 이용해 데스크톱용 프로그램과 창 관리 환경을 실행하고, 그 화면과 입력을 휴대폰 앱에 연결하는 도구입니다. 기본 구성의 중심은 Termux 사용자 공간, XFCE 데스크톱, Termux:X11에서 가져온 화면·입력 구성요소입니다.
이 저장소에서 배울 핵심은 운영체제를 새로 만드는 법보다, 서로 다른 실행 환경의 경계를 연결하는 법입니다. 같은 설치 버튼에도 Dart와 Kotlin의 경계, 앱 권한의 경계, 프로세스의 경계, 파일 경로의 경계가 숨어 있습니다.
Android와 데스크톱 프로그램은 같은 건물의 전기·수도에 해당하는 커널을 이용합니다. 하지만 각 층의 방 번호와 출입 규칙은 다릅니다. DroidDesk는 프로그램이 찾는 주소를 고치고, 화면을 전달하는 통로를 열고, 사용자가 누른 키를 알맞은 곳에 전달합니다.
Flutter는 안내데스크, Kotlin은 시설 관리자, X11은 화면·입력 통로에 가깝습니다. 이 비유에서 중요한 것은 통로가 생겨도 건물의 출입 권한까지 무제한이 되지는 않는다는 점입니다.
| 경로 | 시작점과 실행 환경 | 화면 연결 |
|---|---|---|
| 셸 설치판 | termux-linux-setup.sh. 별도 Termux에 패키지와 실행 스크립트를 설치하고 PRoot 배포판도 구성합니다. | 별도 Termux:X11 또는 선택형 VNC |
| 독립형 앱 · 비루팅 | app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/LinuxRuntime.kt. 앱 내부에 Termux bootstrap을 풀고 기본 XFCE를 직접 실행합니다. | 앱에 내장된 X11 서버 |
| 독립형 앱 · 루팅 | app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/ChrootRuntime.kt. Ubuntu ARM64 rootfs를 준비하고 su를 거쳐 chroot로 실행합니다. | 같은 내장 X11 서버에 소켓을 연결 |
이 글은 독립형 앱의 실행 흐름을 중심으로 살펴봅니다. 루트의 셸 스크립트는 비교 대상으로 읽습니다. 한쪽의 설치 방법이나 지원 목록을 다른 쪽의 보장으로 옮기지 않는 것이 첫 번째 소스 읽기 연습입니다.
2026-10-23 GitHub 기준 스타 4,383개, 포크 407개이며 GitHub Trending Kotlin Weekly #4로 선정됐습니다. 2026-04-20 생성 이후 약 6개월 만의 수치입니다. 다만 스타 수와 순위만으로 실제 사용자 수나 실행 안정성을 알 수는 없습니다.
README가 제시하는 장면은 이해하기 쉽습니다. 휴대폰에서 코드와 문서를 편집하고, 외부 화면에 연결해 데스크톱처럼 쓰다가 그대로 들고 나옵니다. 이 장면의 전달력, 여러 설치 단계를 묶는 자동화, 앱 안의 직접 X11 표시는 관심을 끌 만한 요소입니다. 이는 기능과 소스에서 읽어낸 해석이며, 트렌딩 유입 원인을 측정한 결과는 아닙니다.
| 선택지 | 주요 접근 | DroidDesk에서 볼 차이 |
|---|---|---|
| Termux + Termux:X11 수동 구성 | 사용자가 패키지·환경 변수·화면 서버를 직접 조합합니다. | 앱이 bootstrap, XFCE 설치, 실행, 상태 표시를 연결합니다. 대신 자체 경로 호환 계층을 관리해야 합니다. |
| UserLAnd | 공식 소개상 루트 없이 Android 위에서 Linux 배포판이나 앱을 실행합니다. | DroidDesk의 기본 비루팅 경로는 Termux 패키지를 직접 쓰고, 별도 프로세스의 X11 서버를 앱 화면에 연결합니다. |
| 원격 데스크톱 | 다른 컴퓨터의 프로그램 화면을 네트워크로 전달받습니다. | DroidDesk는 휴대폰에서 프로그램을 실행하므로 별도 호스트가 필요 없는 대신 휴대폰 자원을 소비합니다. |
| DroidDesk 셸 설치판 | Termux에서 다중 데스크톱과 PRoot·선택형 VNC를 구성합니다. | 독립형 앱은 별도 Termux 앱 없이 시작하며, 비루팅 기본 데스크톱은 XFCE로 정규화됩니다. |
비교 근거: Termux:X11 공식 저장소,
UserLAnd 공식 소개,
DroidDesk의 README.md와 LinuxRuntime.normalizedDesktop()입니다.
제품 간 속도 우열을 측정한 벤치마크는 이 비교에 포함되지 않습니다.
DroidDesk의 눈에 띄는 장점은 새로운 리눅스 기술보다, 설치부터 화면·입력·앱 실행까지 이어지는 통합 경험에 있습니다.
| 문서의 표현 | 커밋 a410944에서 확인한 구현 |
|---|---|
| 여러 데스크톱 지원 | 셸 설치판은 XFCE/LXQt/MATE/KDE 분기가 있습니다. 앱의 비루팅 normalizedDesktop()은 인자를 무시하고 xfce4를 반환합니다. |
| 비루팅 앱은 PRoot 미사용 | 기본 데스크톱에는 맞습니다. 그러나 installOptionalApp("proot_debian")은 installMinimalDebian()으로 PRoot와 Debian을 설치하는 선택 기능입니다. |
| 두 앱 모드의 GPU 가속 | 비루팅 경로는 Adreno 장치와 Freedreno ICD 파일을 확인합니다. 루팅 ChrootRuntime의 기본 프로필은 LIBGL_ALWAYS_SOFTWARE=true입니다. |
| Ubuntu에서 되면 모두 가능 | README의 포괄적 주장입니다. Android 커널·권한·ARM64 패키지·그래픽 환경의 제약을 넘어서는 호환성 보장으로 읽을 수 없습니다. |
기능 이름을 발견했다면 호출되는가 → 어떤 조건으로 분기하는가 → 성공을 무엇으로 판정하는가를 확인하세요. 클래스나 바이너리가 존재한다는 사실과 사용자가 그 기능을 실제로 쓰는지는 서로 다른 질문입니다.
전통적인 웹 서비스처럼 프론트엔드와 원격 백엔드가 있는 구조는 아닙니다. 여기서 백엔드에 해당하는 역할은 기기 안의 Kotlin 실행 계층이 담당합니다. 패키지와 rootfs를 받는 네트워크 통신은 있지만, 핵심 데스크톱 실행에 별도 애플리케이션 서버를 두지 않습니다.
| 항목 | 선언 / 잠금 버전 | 역할과 출처 |
|---|---|---|
| Dart SDK | ^3.10.3 | app/pubspec.yaml. lock에는 >=3.10.3 <4.0.0. |
| Flutter SDK | lock 제약 >=3.38.4 | 정확한 설치 버전 고정값은 아닙니다. app/pubspec.lock의 SDK 조건입니다. |
| provider | ^6.1.2 / 6.1.5+1 | ChangeNotifierProvider와 AppState로 상태 변경을 화면에 전달합니다. |
| shared_preferences | ^2.3.4 / 2.5.5 | 테마·자동 시작·업데이트 확인 시각 같은 설정을 저장합니다. |
| google_fonts | ^6.2.1 / 6.3.3 | 글꼴 구성. 화면 테마는 app/lib/theme/droid_theme.dart에 있습니다. |
| 화면 보조 라이브러리 | percent_indicator ^4.2.3 / 4.2.5flutter_animate ^4.5.2 / 4.5.2 | 진행 표시와 애니메이션을 위한 의존성입니다. |
| 링크·벡터 표시 | url_launcher ^6.3.2 / 6.3.2flutter_svg ^2.3.0 / 2.3.0 | URL 열기와 SVG 표시를 위한 의존성입니다. |
app/pubspec.yaml은 허용하는 범위를, app/pubspec.lock은 의존성 해석 결과를 기록합니다.
예를 들어 ^6.1.2와 6.1.5+1은 모순이 아닙니다.
SDK 최소 조건만 보고 정확한 Flutter 빌드 버전을 확정하면 안 됩니다.| 계층 | 주요 구성 | 코드에서의 역할 |
|---|---|---|
| Kotlin | LinuxRuntime, ChrootRuntime, MainActivity | 설치·프로세스 제어·환경 변수·Flutter 호출 처리 |
| Android IPC | Binder, AIDL, ParcelFileDescriptor | 앱 UI 프로세스와 X11 프로세스 사이의 연결 전달 |
| Java / Termux:X11 | app/android/app/src/main/java/com/termux/x11/LorieView.java, 입력 처리 클래스 | 화면 Surface 연결, 키보드·터치·마우스 이벤트 처리 |
| C 호환 계층 | app/assets/socket_hook.c | libc 경로 호출과 X11 소켓 연결 주소 재작성 |
| 데스크톱 구성 | XFCE, D-Bus, PulseAudio, Mesa, X11/TUR 저장소 | 창·메뉴·앱 간 통신·소리·그래픽·패키지 공급 |
| 선택형 배포판 | Ubuntu 24.04.4 ARM64 / Debian | 루팅 chroot용 다운로드 URL / 비루팅 선택형 PRoot 설치 대상 |
XFCE·Mesa·일반 Linux 패키지는 설치 시 저장소에서 받습니다.
전체 버전을 고정하는 패키지 잠금 목록은 확인되지 않습니다.
libXlorie.so 같은 동봉 바이너리도 정확한 상류 소스 리비전이 미기록이라고 COMPLIANCE.md에 적혀 있습니다.
| 항목 | 실제 설정값 | 파일 |
|---|---|---|
| 앱 버전 | 1.3.1+6: 표시 버전 1.3.1, 빌드 번호 6 | app/pubspec.yaml |
| Android Gradle Plugin / Kotlin | 8.11.1 / 2.2.20 | app/android/settings.gradle.kts |
| Gradle / Java 대상 | 8.14 / Java·JVM 17 | app/android/gradle/wrapper/gradle-wrapper.properties, app/android/app/build.gradle.kts |
| Android·CPU | minSdk=28, targetSdk=28, arm64-v8a | app/android/app/build.gradle.kts |
| 네이티브 빌드 | CMake 3.22.1 지정. NDK·compileSdk는 Flutter 설정 참조 | app/android/app/build.gradle.kts, app/android/app/src/main/cpp/CMakeLists.txt |
| 정적 검사·테스트 의존성 | flutter_lints ^6.0.0, flutter_test | app/pubspec.yaml. 의존성 선언이 테스트 커버리지를 뜻하지는 않습니다. |
| 릴리스 경로 | Flutter APK 빌드 후 GitHub Releases에 첨부하는 수동 절차 | RELEASING.md. 추적 파일에 .github/workflows/는 없습니다. |
빌드 도구 버전, 의존성 해석 결과, 실행 중 내려받는 패키지 버전은 각각 다른 수준의 재현성을 제공합니다.
Gradle에 남은 wlroots 관련 주석만으로 Wayland가 실제로 동작한다고 판단하지 마세요.
현재 app/android/app/src/main/cpp/CMakeLists.txt는 JNI 파일 하나와 Android·log 라이브러리만 연결합니다.
위 그림의 두 통로를 혼동하지 마세요. Linux 프로그램은 X11 소켓으로 서버에 접속하고, Android 화면 쪽은 Binder로 받은 파일 디스크립터를 통해 네이티브 연결을 붙입니다. MethodChannel로 화면 픽셀을 계속 보내는 구조가 아닙니다.
설치 명령의 전달, 데스크톱 프로세스의 실행, 화면의 표시는 서로 다른 경로이므로 각각의 성공 조건을 확인해야 합니다.
app/lib/services/platform_bridge.dart의 DroidDeskPlatform은
Flutter 화면이 Android 세부 API를 직접 알지 않도록 중간 창구를 제공합니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/MainActivity.kt의
configureFlutterEngine()이 같은 채널 이름으로 요청을 받습니다.
// platform_bridge.dart: 설치 요청 부분 발췌
static const _channel = MethodChannel('com.droiddesk/core');
static Future<bool> installDesktopNative({String de = 'xfce4'}) async {
final result = await _channel.invokeMethod('installDesktopNative', {
'de': de,
});
return result as bool? ?? false;
}
Kotlin은 이 요청을 installThread("install-desktop-native")에서 처리하고,
LinuxRuntime.installDesktopEnvironmentNative()를 호출합니다.
진행 상황은 onInstallProgress라는 반대 방향 호출로 Dart에 돌아옵니다.
최종 bool 응답과 여러 번 발생하는 진행 알림의 역할이 다릅니다.
app/lib/state/app_state.dart의 initialize()는 콜백을 등록하고
notifyListeners()로 화면을 갱신합니다.
onInstallProgress 처리에서 음수는 실패, 0 이상 1 미만은 진행 중, 1 이상은 완료로 해석합니다.
문자열 기반 메서드명과 동적 Map이 계약이므로 이름·키·자료형이 어긋나면 런타임 오류가 날 수 있습니다.
app/android/app/src/main/AndroidManifest.xml은 X11ServerService에
android:process=":x11", android:exported="false"를 선언합니다.
X11 서버가 UI와 별도의 프로세스·파일 디스크립터 테이블을 갖게 하려는 구조입니다.
이는 Linux 배포판 전체를 보안 컨테이너로 격리한다는 뜻은 아닙니다.
// app/android/app/src/main/aidl/com/orailnoor/droiddesk/x11/IX11Service.aidl
interface IX11Service {
boolean startServer();
int getServerPid();
ParcelFileDescriptor getXConnection();
ParcelFileDescriptor getLogcatOutput();
}
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/x11/X11ServerService.kt의 ensureServerStarted()는
HandlerThread에서 초기화하고 CountDownLatch로 중복 시작 요청을 합칩니다.
CmdEntryPoint.start(arrayOf(":0", "-nolock"))가 실제 서버 시작 지점입니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/x11/X11ServiceClient.kt는 Binder 호출을 단일 작업 스레드에서 수행합니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/view/DesktopActivity.kt의 connectToX11Service()는 받은 연결을
LorieView.connect(connectionFd.detachFd())로 넘깁니다.
같은 서버 PID에 이미 연결돼 있으면 새 FD를 닫고 연결을 재사용합니다.
화면은 가게 진열창이고 X11 서버는 안쪽 작업장입니다.
진열창을 다시 열 때 작업장을 새로 짓지 않도록, 창의 수명과 작업장의 수명을 분리합니다.
X11ServiceClient.connect()가 서비스를 시작한 뒤 바인딩하는 이유도 화면 이탈 때 서버가 바로 사라지지 않게 하기 위해서입니다.
반대로 서버를 완전히 중지할 때는 X11ServerService.onDestroy()가 자기 프로세스를 종료합니다.
Java 서비스 객체를 없애도 네이티브 스레드가 자동으로 모두 끝나는 것은 아니라는 설명이 소스에 있습니다.
객체 해제와 프로세스 종료는 서로 다른 생명주기라는 점을 배울 수 있습니다.
Termux 패키지가 원래 기대하는 접두 경로는 /data/data/com.termux/files/usr입니다.
DroidDesk는 자신의 context.filesDir/usr에 패키지를 둡니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/LinuxRuntime.kt는 이 주소 차이를 세 가지 수준에서 보정합니다.
| 수준 | 실제 함수 | 해결하려는 문제 |
|---|---|---|
| 텍스트·스크립트 | patchShebangs(), wrapDpkgForPath() | 스크립트·설정 파일 본문의 옛 경로, 패키지 도구의 PATH·설정 경로 |
| ELF 바이너리 | patchElfRunpaths(), patchElfFile() | 공유 라이브러리 검색 경로인 RPATH/RUNPATH에 박힌 옛 경로 |
| 실행 중 호출 | getTermuxEnv()와 C의 rewrite_path(), connect() | libc 파일 접근과 X11 Unix 소켓 연결을 실제 앱 경로로 전환 |
patchElfFile()는 ELF64 프로그램 헤더의 PT_DYNAMIC에서
DT_STRTAB·DT_RPATH·DT_RUNPATH를 찾습니다.
주소를 파일 오프셋으로 바꾼 뒤 경로를 덮어쓰며, 공간이 좁으면 ${ORIGIN}이나
${ORIGIN}/../lib를 사용합니다. 단순한 전체 문자열 치환과 다릅니다.
실행 환경의 핵심 값 — getTermuxEnv()와 socket_hook.c를 요약
PREFIX = 앱 내부 files/usr
TMPDIR = 앱 내부 files/tmp
DISPLAY = :0
LD_PRELOAD = PREFIX/lib/libsocket_hook.so (파일이 있을 때)
프로그램이 찾는 /tmp/.X11-unix/X0
→ connect() 후크
→ TMPDIR/.X11-unix/X0
원래의 libc 함수 주소
→ dlsym(RTLD_NEXT, "connect")
→ 보정한 주소로 실제 연결
app/assets/socket_hook.c를 빌드한 공유 라이브러리는 동적 링커의 LD_PRELOAD로 먼저 로드됩니다.
후크가 모든 시스템 호출을 완벽히 가로채는 것은 아닙니다.
이 코드는 정의된 libc 함수와 경로 규칙을 보정하는 계층이며, 임의의 Ubuntu 바이너리를 보편적으로 변환하는 도구는 아닙니다.
LinuxRuntime.isBootstrapped()는 .bootstrap_extracted 마커와
bin/bash를 확인합니다. getInstalledDE()는 마지막 설치 단계에서 기록하는
.de_installed를 읽습니다. 패키지 파일 몇 개만 생긴 부분 설치를 완료로 오인하지 않으려는 장치입니다.
installPackageGroup()은 설치 명령 뒤에 shebang을 다시 보정하고
dpkg --configure -a로 미완료 설정을 복구합니다.
최초 설치가 실패했어도 복구가 성공하고 dpkg-query -W로 요청 패키지가 조회되면 성공으로 판단합니다.
데이터베이스 트랜잭션처럼 모든 변경을 원상복구하는 방식은 아닙니다.
| 질문 | 판정 지점 | 판정의 한계 |
|---|---|---|
| 설치가 끝났나? | 완료 마커와 필요한 파일 | 파일이 있다고 기능 전체가 정상이라는 보장은 없습니다. |
| 세션이 살아 있나? | sessionProcess?.isAlive | 프로세스 생존과 실제 창 표시 성공은 다릅니다. |
| 데스크톱이 준비됐나? | waitForDesktopReady()가 XFCE 프로세스 확인 | 기본 45초 제한, 준비 확인 후 650ms 대기. 픽셀을 검사하는 방식은 아닙니다. |
MainActivity.runPackageOperation()의 AtomicBoolean.compareAndSet()은
앱 스토어 패키지 작업이 겹치지 않게 합니다. 로그는 appendTerminalOutput()에서
약 150ms 단위로 모아 보냅니다. 패키지 관리자의 일관성과 UI 갱신 비용을 함께 생각한 부분입니다.
루팅 경로에서도 파일을 푸는 것만으로는 끝나지 않습니다.
ChrootRuntime.ensureMounts()가 /dev·/proc·/sys 등을 연결하고,
bindX11Socket()이 앱의 X11 소켓 디렉토리를 chroot 안에 bind mount합니다.
그 뒤 su -c를 거쳐 chroot 안의 셸과 데스크톱 세션을 시작합니다.
비루팅 경로의 경로 후크와 루팅 경로의 마운트는 같은 연결 문제를 다른 권한 수준에서 푸는 방법입니다.
새 기능을 설계한다면 재시도 조건, 중간 실패 후 상태, 중복 실행 방지, 완료 판정부터 써 보세요. 설치 명령 하나를 호출하는 것보다, 실패한 설치를 다시 이어갈 수 있게 만드는 일이 제품 코드에서 더 큰 비중을 차지합니다.
Kotlin의 실행·X11·화면 코드는 각각 app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/·app/android/app/src/main/kotlin/com/orailnoor/droiddesk/x11/·app/android/app/src/main/kotlin/com/orailnoor/droiddesk/view/에 모여 있습니다.
파일이 많아 보이지만 먼저 app/lib/main.dart → AppState → DroidDeskPlatform → MainActivity 순서로 읽으면
사용자 행동이 어느 실행 함수로 이어지는지 찾을 수 있습니다.
| 분류 | 파일 수 | 물리적 줄 수 | 읽을 때의 의미 |
|---|---|---|---|
| Kotlin · .kt | 22 | 8,208 | Android와 Linux를 연결하는 중심 계층 |
| Dart · .dart | 18 | 7,485 | 사용자 화면과 상태·업데이트 서비스 |
| Java · .java | 12 | 3,366 | Termux:X11 화면·입력 코드 포함 |
| 셸 · .sh | 3 | 1,199 | 설치판과 보조 스크립트 |
| C / C++ | 2 / 1 | 554 / 50 | 경로 후크·chroot 호환 코드 / 미완성 JNI |
| Gradle Kotlin DSL · .kts | 3 | 148 | 빌드 설정. 위 .kt 수치와 별도 집계 |
커밋 a410944의 Git 추적 파일 118개를 기준으로, 확장자별 텍스트의 실제 줄을 셌습니다.
마지막 개행이 없어도 한 줄로 세며 주석·빈 줄을 포함합니다.
압축 bootstrap 내부, 이미지, 글꼴, 동봉 .so, 라이선스·문서 등은 위 소스 합계에서 제외합니다.
위 표의 합계는 21,010줄이며 순수 실행문 수나 개발자가 직접 작성한 코드만의 분량은 아닙니다.
app/android/app/src/main/cpp/compositor_jni.cpp에는 Wayland/wlroots 초기화·종료·입력 주입이 TODO로 남아 있습니다.
JNI 함수 이름도 현재 Kotlin 패키지 경로와 다릅니다.
이 파일을 근거로 완성된 Wayland 렌더러가 있다고 설명하면 안 됩니다.
fetch_deps.sh는 wlroots 0.17.4-1, Wayland 1.22.0-1 등의 다운로드 목록과
작성자 컴퓨터의 절대 경로를 갖고 있습니다. 현재 앱의 실제 의존성 잠금 파일로 볼 수 없습니다.
app/test_ffi.dart 역시 메모리 할당·해제 실험이며 자동화된 앱 테스트 모음이 아닙니다.
한 기능을 화면에서 시스템 호출까지 따라가 보면, 여러 언어를 배우는 일이 하나의 문제를 푸는 과정으로 연결됩니다. 처음부터 모든 파일을 이해하려 하기보다 설치 진행률이나 클립보드처럼 작은 기능 하나를 선택하세요.
app/lib/main.dart는 ChangeNotifierProvider로 AppState를 공급하고,
context.watch<AppState>()로 상태를 구독합니다.
isSetupComplete에 따라 시작 화면도 달라집니다.
위젯이 설치 프로세스를 직접 다루지 않고 상태를 받아 그린다는 점에 집중하세요.
실습 아이디어: 다운로드·추출·설치의 진행 이벤트를 표로 만든 뒤,
각 이벤트가 _isDownloading·_isExtracting·_isInstallingDE 중 무엇을 바꾸는지 표시하세요.
화면을 다시 그리는 것과 실제 설치가 진행되는 것을 분리해서 설명하는 연습입니다.
MainActivity는 설치를 작업 스레드에 보내고 결과·콜백을 runOnUiThread로 돌려보냅니다.
하지만 코드 전체가 코루틴 기반으로 통일돼 있지는 않습니다.
thread, Executor, Handler, CountDownLatch를 구분해서 읽어야 합니다.
HandlerThread를 만든다고 Android 프로세스가 하나 더 생기는 것은 아닙니다.
별도 프로세스는 Manifest의 :x11 선언에서 생깁니다.실습 아이디어: X11ServiceClient.onServiceConnected()에서
Binder 요청과 화면 콜백이 각각 어느 스레드에서 실행되는지 화살표 두 색으로 표시해 보세요.
기다리는 호출이 UI에 놓이면 어떤 현상이 생길지도 함께 적습니다.
LinuxRuntime.getTermuxEnv()에서 PATH·HOME·SHELL·
DISPLAY·LD_LIBRARY_PATH·APT_CONFIG를 찾아보세요.
같은 바이너리도 어떤 환경을 물려받느냐에 따라 인터프리터·라이브러리·화면 서버를 찾지 못할 수 있습니다.
실습 아이디어: 각 변수를 빼면 실패할 것 같은 기능과 예상 오류를 한 줄씩 적으세요. 예측이라는 점을 표시하고, 코드에서 해당 값을 읽는 소비자를 찾으며 근거를 보강합니다. Android Termux의 Bionic 환경과 Ubuntu의 glibc 환경을 구별하는 것도 다음 학습 주제입니다.
patchElfFile()는 파일 안의 동적 라이브러리 검색 정보를 고칩니다.
반면 app/assets/socket_hook.c는 프로그램 실행 중 libc 함수를 감싸 호출 인자를 고칩니다.
설치된 파일을 바꾸는 작업과 실행 중 호출을 중개하는 작업의 차이를 배울 수 있습니다.
실습 아이디어: rewrite_path()를 순수 문자열 함수로 옮겨 모의 입력을 넣으세요.
정확한 접두 경로, 접두 경로와 비슷하지만 다른 이름, 빈 값, 상대 경로를 구별해야 합니다.
실제 후크를 주입하지 않아도 경계 조건과 버퍼 크기 문제를 공부할 수 있습니다.
X11은 프로그램이 창·입력과 통신하는 체계이고, GPU 드라이버는 그래픽 연산을 처리하는 경로입니다.
LinuxRuntime.getTermuxEnv()는 Adreno와 ICD 파일이 있을 때 zink를 선택하고,
그렇지 않으면 swrast·llvmpipe로 전환합니다.
Mesa의 Zink 문서는 OpenGL 작업을 Vulkan API로 보내는 역할을 설명합니다. LLVMpipe 문서는 CPU 기반 소프트웨어 래스터화 경로를 설명합니다. 실습 아이디어: 장치 노드 존재 여부와 ICD 파일 존재 여부의 네 조합을 표로 만들어 선택되는 환경 변수를 써 보세요.
app/lib/services/update_service.dart의 UpdateService.check()는
자동 확인에 6시간 간격 제한과 건너뛴 태그를 적용하며, 수동 확인은 이 둘을 무시합니다. 초안·사전 릴리스는 제외하고 첫 APK 자산을 선택합니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/AppUpdater.kt의 download()는 예상 크기가 0보다 클 때 파일 크기를 검사하고, verify()는 패키지명과
설치된 앱·내려받은 APK의 서명 인증서 지문을 확인합니다.
실습 아이디어: 새 버전 없음, APK 없음, 크기 불일치, 패키지명 불일치, 서명 불일치를
서로 다른 실패 사례로 나누세요. 버전 비교는 업데이트 후보를 고르고, 서명 확인은 설치할 파일의 신뢰 조건을 검사합니다.
isNewer()는 앞의 숫자 세 부분을 비교하므로 모든 사전 릴리스 규칙을 구현한 완전한 SemVer 파서는 아닙니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/AndroidAppBridge.kt의 syncLaunchers()는 Android 앱 목록을
Linux의 .desktop 파일로 바꾸고 Python 런처를 만듭니다.
런처는 Unix 소켓으로 패키지명을 보내고, serve()와 launchPackage()가
Android의 정상 실행 Intent로 연결합니다. Android 앱이 Linux 프로그램으로 바뀌는 것은 아닙니다.
루팅 DesktopActivity.startDesktopSessionIfRequested()에는
AndroidAppSync.syncApps()와 FIFO 기반 android_intent_pipe 경로도 남아 있습니다.
유사한 기능의 구현이 둘이면 현재 호출 경로를 함께 확인해야 합니다.
클래스 이름 하나만 보고 연동이 모두 한 방식으로 통일됐다고 판단하지 마세요.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/ClipboardSync.kt는 800ms 간격으로 평문을 동기화하고
busy.compareAndSet()으로 작업 중복을 막습니다.
실습 아이디어: 양쪽 텍스트가 동시에 바뀌었을 때 Android 변경을 먼저 처리하는 분기를 찾고,
자신이 원하는 충돌 해결 규칙과 비교해 보세요. 이 기능의 시작·중지는 데스크톱 창의 포커스와도 연결됩니다.
| 항목 | 확인한 조건 | 해석 |
|---|---|---|
| CPU | abiFilters = arm64-v8a, ARM64 bootstrap·동봉 .so | 이 구성은 32비트 ARM·x86 Android를 지원 대상으로 빌드하지 않습니다. |
| Android | minSdk=28, targetSdk=28 | 최소 API 28은 Android 9입니다. 최신 OS에서 모든 기기가 검증됐다는 뜻은 아닙니다. |
| 루트 권한 | 기본 앱 경로는 불필요. chroot 경로는 su 필요 | PRoot 안의 가상 root 사용자와 Android의 실제 루트 권한은 다릅니다. |
| GPU | 비루팅: Adreno 장치 노드 + Freedreno ICD 존재 시 Turnip/Zink | 기타 조건은 llvmpipe. 루팅 Ubuntu 기본 프로필도 소프트웨어 렌더링입니다. |
| 네트워크 | 최초 데스크톱 패키지 설치, rootfs·선택 앱 다운로드에 필요 | 이미 설치한 로컬 앱의 실행과 온라인 설치·업데이트를 구분해야 합니다. |
| 화면·입력 | 휴대폰 화면만으로 시작 가능 | 코딩·문서 작업에는 키보드와 마우스가 편하지만 앱 코드의 필수 조건은 아닙니다. |
ARM64와 최소 Android 버전을 만족하는 것만으로 GPU 가속, 외부 화면 출력, 모든 Ubuntu 앱의 호환성까지 보장되지는 않습니다. 낮게 설정된 targetSdk는 앱 내부 실행 파일의 호환성을 위한 선택이라고 Gradle 주석에 설명돼 있습니다. Android 정책 전체를 해제하거나 모든 제조사 제약을 없애는 설정으로 해석해서는 안 됩니다.
이 커밋에는 기기 RAM의 공식 최소·권장 사양표가 없습니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/RootfsManager.kt 주석의 압축 300~500MB·해제 후 2~4GB도 일반적인 예상치이며,
고정된 Ubuntu Base 파일의 실측 크기나 XFCE까지 설치한 최종 사용량은 아닙니다.
| 구분 | 학습용 준비 기준 | 근거 수준 |
|---|---|---|
| 소스 읽기·모의 실습 | 일반 개발용 컴퓨터와 텍스트 편집기 | Android 기기나 루팅 없이 이 글의 과제를 진행할 수 있습니다. |
| 가벼운 기기 체험 | RAM 4GB 이상, 여유 저장 공간 8GB 이상부터 검토 | 작성자의 준비용 권장치입니다. 검증된 최소 사양이 아닙니다. |
| 브라우저·에디터 병행 | RAM 6~8GB 이상과 추가 저장 공간을 고려 | 작업량에 따른 여유 제안입니다. Blender·LLM 성능 기준으로 사용할 수 없습니다. |
실제 공간에는 다운로드 캐시, 압축 해제본, 패키지, 사용자 파일이 함께 들어갑니다.
LinuxRuntime.getStorageInfo()와 cleanPackageCache()가 별도 기능인 이유입니다.
CPU 렌더링은 앱 실행 가능 여부와 별개로 응답성·발열·전력 소비에 영향을 줍니다.
USB-C 단자가 있다고 영상 출력이 되는 것은 아닙니다. README의 직접 HDMI 연결 경로는 휴대폰이 USB-C 영상 출력을 지원할 때의 안내입니다. Raspberry Pi 경로는 셸 설치판에서 VNC를 켜고, Pi가 USB 테더링 네트워크를 통해 화면을 받는 방식입니다.
pi-launch_phone.sh는 기본 경로의 게이트웨이를 휴대폰 IP로 간주해
vncviewer를 5901 포트로 연결합니다.
USB 인터페이스 자체를 판별하지 않으므로 다른 기본 경로가 잡혀 있으면 잘못된 주소를 선택할 수 있습니다.
셸 설치판의 선택형 VNC 실행 명령은 -localhost no를 사용해 외부 연결을 허용합니다.
USB 테더링에서만 연결된다고 코드가 강제하지는 않습니다.
이 설정은 앱 내부 X11 경로와 구별해서 이해해야 하며, 접속 가능한 네트워크와 인증 설정을 함께 검토해야 합니다.
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/service/DroidDeskService.kt는 포그라운드 알림과 PARTIAL_WAKE_LOCK을 사용합니다.
이는 CPU 수면과 세션 유지에 도움을 주는 수단이지 Android의 모든 백그라운드 종료를 막는 보장은 아닙니다.
실제 기기별 동작은 제조사 정책과 배터리 설정의 영향을 받습니다.
아래 소스 탐색 명령은 저장소 루트를 기준으로 적었습니다. 코드를 고치는 과제는 원본과 분리한 연습용 코드에서 진행하세요. 실습의 완료 기준을 실행 화면 하나로 끝내지 말고, 정상·실패·중복 요청을 설명할 수 있는 결과물로 정하세요.
할 일: app/pubspec.yaml과 app/pubspec.lock에서 직접 의존성 일곱 개의
이름·허용 범위·해석 버전을 표로 만드세요. SDK 조건과 패키지 버전도 분리합니다.
완료 기준: provider가 ^6.1.2인데 lock에는 6.1.5+1인 이유,
Flutter >=3.38.4가 정확한 설치 버전이 아닌 이유를 각각 설명합니다.
# 읽기 전용 탐색: 의존성 선언과 잠금 결과
rg -n 'provider|shared_preferences|google_fonts|sdk:' app/pubspec.yaml
rg -n -A8 '^ provider:|^sdks:' app/pubspec.lock
할 일: Dart의 installDesktopNative에서 Kotlin 런타임까지 호출을 찾고,
onInstallProgress가 돌아오는 방향을 그립니다. 호출명·인자·반환값을 화살표에 써 넣으세요.
완료 기준: de 인자는 문자열, 최종 응답은 bool,
진행 이벤트는 progress와 status라는 사실을 소스 위치와 함께 설명합니다.
여기에 normalizedDesktop()를 붙여 다른 DE를 요청했을 때의 결과도 예측합니다.
# 읽기 전용 탐색: 요청과 콜백이 만나는 네 위치
rg -n 'installDesktopNative|onInstallProgress' \
app/lib/services/platform_bridge.dart app/lib/state/app_state.dart \
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/MainActivity.kt
rg -n 'normalizedDesktop|installDesktopEnvironmentNative' \
app/android/app/src/main/kotlin/com/orailnoor/droiddesk/runtime/LinuxRuntime.kt
할 일: 연습용 코드에 진행률을 받아 대기/진행/성공/실패 중 하나를 반환하는 함수를 만듭니다.
AppState.initialize()의 규칙을 먼저 옮기고, NaN과 범위 밖 값은 별도로 처리하도록 설계합니다.
완료 기준: -1, 0, 0.5, 1, 1.2, NaN 입력의 기대 결과를 표로 적고,
실패 뒤 재시도와 완료 뒤 늦게 도착한 진행 이벤트를 어떻게 다룰지도 정합니다.
연습용 의사 코드 — 실제 AppState 구현의 직접 인용은 아닙니다.
분류(progress):
숫자가 유효하지 않으면 → 실패
progress < 0 이면 → 실패
progress < 1 이면 → 진행
그 밖에는 → 성공
추가 상태: 작업 ID, 마지막 상태, 취소 여부
질문: 이전 작업의 완료 이벤트가 새 작업에 도착하면?
할 일: UpdateService.check()의 판단을 연습용 순수 함수로 분리하고,
버전·draft·prerelease·assets만 가진 모의 JSON을 넣습니다. 실제 GitHub 조회나 APK 설치는 필요 없습니다.
완료 기준: 같은 버전, 낮은 버전, 높은 버전이지만 APK 없음, 사전 릴리스, 건너뛴 태그의 결과를 설명합니다. 자동 확인과 수동 확인의 차이도 적습니다.
확장: AppUpdater.download(), AppUpdater.verify()를 읽고 크기·패키지명·서명 지문 불일치를
서로 다른 오류로 표현하세요. SHA-256이 APK 전체의 공개 체크섬인지 인증서 지문인지 구별해야 합니다.
할 일: X11ServerService.ensureServerStarted()의 제한 없는
latch.await()를 읽고, 연습용 모델에 시간 제한과 취소를 추가하는 설계안을 만듭니다.
완료 기준: 동시 시작 요청 두 개, 서버 초기화 실패, 대기 중 화면 종료, 제한 시간 뒤 뒤늦은 성공의 네 경우를 시퀀스 도식으로 설명합니다. 실패를 UI에 보낸 뒤 FD가 나중에 도착하면 누가 닫는지도 명시합니다.
확장: 연결 해제 콜백과 active 플래그,
DesktopActivity.connectToX11Service()의 FD 재사용·해제 규칙을 설계 근거로 연결하세요.
매주 소스에서 질문 하나를 고르고 설명 자료 하나를 남기는 방식입니다. 처음에는 프로그램을 실행하지 않고도 따라갈 수 있습니다. 학습 진도는 읽은 파일 수보다, 입력에서 결과까지의 흐름을 스스로 설명할 수 있는지로 확인하세요.
| 주차 | 학습 주제와 읽을 코드 | 직접 남길 결과물 |
|---|---|---|
| 1주차 | Dart 기본 문법, Future, Flutter 위젯. app/lib/main.dart·AppState | 설정 전/설정 후 화면이 선택되는 조건표와 의존성 버전표 |
| 2주차 | Provider와 플랫폼 채널. app/lib/services/platform_bridge.dart·MainActivity.configureFlutterEngine() | 요청명·인자·응답·콜백을 연결한 인터페이스 명세 한 장 |
| 3주차 | 프로세스, 환경 변수, 패키지 설치 상태. LinuxRuntime | 부분 설치 → 복구 → 완료 마커의 상태 전이도 |
| 4주차 | Binder·AIDL·FD 소유권. X11ServerService·X11ServiceClient | 화면 이탈·재진입·서버 종료 시퀀스와 FD 해제 책임표 |
| 5주차 | C 함수 포인터, 동적 링커, ELF64. app/assets/socket_hook.c·patchElfFile() | 경로 치환 모의 테스트와 RPATH/RUNPATH 수정 단계 설명 |
| 6주차 | 업데이트·서명·재현 가능한 빌드. UpdateService·AppUpdater·COMPLIANCE.md | 모의 릴리스 사례표와 바이너리-소스 대응 목록 초안 |
건물 전체 배선을 외우려 하지 말고 조명 스위치 하나에서 전등까지 따라가세요. 설치 버튼 하나를 끝까지 설명할 수 있으면, 앱 스토어·업데이트·클립보드에도 같은 추적 방법을 적용할 수 있습니다.
| 용어 | 쉬운 설명 |
|---|---|
| ARM64 / aarch64 | 이 앱이 대상으로 삼는 64비트 ARM CPU 구조입니다. Android 빌드의 ABI 이름은 arm64-v8a입니다. |
| ABI | 바이너리가 서로 호출되고 로드될 때 지켜야 하는 규약입니다. CPU 구조와 함수 호출 방식 등이 맞아야 합니다. |
| Termux | Android에서 셸과 Linux 도구를 쓸 수 있게 하는 환경입니다. DroidDesk 앱은 그 사용자 공간을 자신의 저장 영역에 구성합니다. |
| bootstrap | 패키지 관리와 셸 실행을 시작할 수 있는 최소 기본 파일 묶음입니다. 여기서는 ARM64 ZIP이 앱 자산으로 들어갑니다. |
| prefix | 프로그램·라이브러리·설정 파일이 모이는 공통 경로입니다. DroidDesk의 경로 재배치가 필요한 출발점입니다. |
| TUR | Termux User Repository입니다. 추가 앱을 공급하는 패키지 저장소이며 DroidDesk와 같은 프로젝트는 아닙니다. |
| XFCE | 패널·창 관리자·파일 관리자 등을 묶은 데스크톱 환경입니다. 비루팅 앱의 기본 지원 데스크톱입니다. |
| X11 | 그래픽 프로그램과 화면·입력을 담당하는 서버가 통신하는 체계입니다. Linux 프로그램이 클라이언트입니다. |
| DISPLAY=:0 | 프로그램이 사용할 X 서버의 디스플레이 번호입니다. 화면 해상도나 Android 모니터 번호 자체를 뜻하지 않습니다. |
| VNC | 화면을 원격으로 전달하고 입력을 돌려보내는 방식입니다. 이 저장소에서는 주로 셸 설치판의 선택형 외부 접속 경로입니다. |
| PRoot | 시스템 호출을 추적해 파일 경로 등을 바꾸며 root 없이 chroot와 비슷한 환경을 만드는 도구입니다. 실제 root 권한을 주지는 않습니다. |
| chroot / rootfs | chroot는 프로세스가 보는 루트 경로를 바꾸는 기능이고, rootfs는 그 안의 기본 파일 트리입니다. chroot만으로 완전한 보안 격리가 되지는 않습니다. |
| MethodChannel | Flutter와 플랫폼 코드가 메서드명·인자·응답을 주고받는 통로입니다. 이 앱의 채널 이름은 com.droiddesk/core입니다. |
| Binder / AIDL | Android 프로세스 사이 호출 장치와 그 인터페이스 정의 언어입니다. UI와 X11 서비스의 계약에 쓰입니다. |
| FD | 열린 파일·소켓 같은 자원을 가리키는 프로세스 내부 번호입니다. 번호만 복사하는 것과 IPC로 실제 자원 핸들을 전달하는 것은 다릅니다. |
| JNI / .so | JNI는 Java·Kotlin 쪽과 C/C++를 연결하는 규약입니다. .so는 실행 중 불러오는 공유 라이브러리 파일입니다. |
| LD_PRELOAD | 동적 링커가 지정한 라이브러리를 먼저 로드하도록 하는 설정입니다. 여기서는 libc 함수의 경로 인자를 보정하는 데 사용합니다. |
| shebang | 셸 스크립트 첫 줄의 #!와 실행기 경로입니다. 잘못된 절대 경로가 남으면 파일이 있어도 실행에 실패할 수 있습니다. |
| ELF / RPATH / RUNPATH | Linux 바이너리 형식과 공유 라이브러리를 찾는 검색 경로 정보입니다. Kotlin 코드가 해당 바이너리 메타데이터를 직접 읽고 고칩니다. |
| D-Bus / PulseAudio | 각각 데스크톱 구성요소 사이의 메시지 통신과 소리 처리를 담당합니다. X11 화면 연결과는 별도 역할입니다. |
| Turnip / Zink | Turnip은 Adreno용 Mesa Vulkan 드라이버이고, Zink는 OpenGL 작업을 Vulkan 경로로 전달하는 Mesa 드라이버입니다. |
| llvmpipe | CPU로 그래픽을 계산하는 Mesa 소프트웨어 렌더러입니다. GPU 경로가 준비되지 않았을 때의 대안입니다. |
| WakeLock | 필요한 동안 CPU 수면 등을 제어하는 Android 장치입니다. 앱 강제 종료를 모두 막는 보호막은 아닙니다. |
| 서명 인증서 지문 | 앱 서명 인증서를 해시로 식별한 값입니다. 이 앱의 업데이트 코드는 설치된 앱과 다운로드 APK의 지문 교집합을 확인합니다. |
PRoot의 시스템 호출 추적과 실제 루트 권한의 차이는 PRoot-Distro 공식 문서에서도 설명합니다. 이 문서의 최신 명령 체계와 DroidDesk가 설치 시 받는 패키지 버전은 별개이므로 개념 설명과 버전 정보를 구별해 읽으세요.
| 항목 | 확인 내용 |
|---|---|
| 선정 | GitHub Trending Kotlin Weekly #4 · 2026-10-23 분석 |
| 스타 / 포크 | 4,383 / 407 · 2026-10-23 GitHub 기준 |
| 생성일 / 주 언어 | 2026-04-20 / Kotlin · 2026-10-23 GitHub 기준 |
| 최신 릴리스 | v1.3.1 · 2026-10-14 · 2026-10-23 GitHub 기준. 앱 선언 버전은 1.3.1+6입니다. |
| 최근 push | 2026-10-14 · 2026-10-23 GitHub 기준 |
| 분석 기준 커밋 | a410944 · 2026-10-14 · 커밋 메시지 Update README.md |
| 코드 규모 | Git 추적 118파일. Kotlin 8,208줄, Dart 7,485줄, Java 3,366줄, 셸 1,199줄, C 554줄, C++ 50줄, Gradle KTS 148줄 |
| 대상 플랫폼 | Android ARM64 · 앱 minSdk 28 · 비루팅 Termux 기본 경로 / 루팅 Ubuntu chroot |
| 테스트 / CI | flutter_test 의존성, FFI 실험 파일은 있으나 일반적인 app/test/ 테스트 모음과 GitHub Actions 워크플로는 추적 파일에서 확인되지 않습니다. |
| 프로젝트 라이선스 | GPL-3.0-only. 상류 파일의 고지는 유지되며 동봉 구성요소는 각자의 라이선스·출처 기록을 확인해야 합니다. |
커밋 a410944의 실행 흐름만으로 기기별 설치 성공, 발열·배터리, GPU 성능, 외부 모니터, 앱별 동작을 보장할 수는 없습니다.
README의 Blender 실행·로컬 AI 초당 5토큰 이상 등의 주장은 기기·모델·측정 조건이 충분히 제시된 공통 벤치마크가 아닙니다.
기본 비루팅 데스크톱, 선택형 PRoot Debian, 루팅 chroot, 셸 설치판은 제약이 다릅니다.
실제 루트 권한이 없는 환경에서 커널 권한이 필요한 네트워크 기능까지 자동으로 가능해지는 것은 아닙니다.
APP_TROUBLESHOOTING.md의 PRoot 중심 설명도 현재 기본 앱 경로 전체에 그대로 적용할 수 없습니다.
COMPLIANCE.md와 THIRD_PARTY_NOTICES.md에는 네이티브 바이너리의 정확한 소스 리비전·재현 빌드, custom-prefix bootstrap, 일부 동봉물의 출처·고지 등 미완료 항목이 남아 있습니다. 프로젝트가 GPL 소스라는 사실만으로 배포 APK의 모든 출처·재현성 문제가 해결됐다고 볼 수 없습니다.