Skip to content

Commit 7978fb2

Browse files
author
BuildEngine Bot
committed
buildengine: Phase 2 — LocalWorkspaceModelBuilder (Gradle-free project model)
Scans src/main/{java,kotlin,res,assets} + AndroidManifest.xml + libs/*.jar + libs/*.aar directly and produces the same AndroidModule/GradleProject/ WorkspaceImpl shape WorkspaceModelBuilder builds from a Gradle sync today — so Java/Kotlin completion, indexing and XML resource resolution keep working once Gradle sync is gone, not just APK building. Reuses tooling:builder-model-impl's Default* classes (plain data holders around AGP builder-model interfaces, no Gradle runtime dependency) instead of inventing new model types, so existing consumers of the project model are untouched. Updated buildengine/PLAN.md: keep tooling:builder-model-impl in Phase 4's removal list, added Phase 2 status + known limitations (single-module only, no transitive Maven deps, ProjectSyncIssues shape unverified against the real jar, not yet wired into ProjectManagerImpl).
1 parent 6e61bbb commit 7978fb2

2 files changed

Lines changed: 285 additions & 23 deletions

File tree

‎buildengine/PLAN.md‎

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,52 @@ structured errors with file/line).
4141
| 12 | No forced huge SDK download | Addressed by design (core tools ~few MB, Kotlin optional) but depends on how tools are bundled |
4242
| 13 | Fully local, no source upload | Naturally true — no network code anywhere in this module |
4343

44-
## Phase 2 — Local project model (the part most likely to surprise you)
45-
46-
`core/projects/.../internal/WorkspaceModelBuilder.kt` currently builds the
47-
IDE's understanding of "what are this project's modules/source-sets/
48-
dependencies" **from the Gradle Tooling API model** (`IGradleProject`).
49-
That same model feeds Java/Kotlin code completion and indexing — not just
50-
building. Removing Gradle means this also needs a local replacement:
51-
a parser that walks the project's folder layout (`src/main/java`,
52-
`src/main/kotlin`, `src/main/res`, `libs/*.jar`) and produces the same
53-
`ModuleProject`/`AndroidModule` shape the rest of the IDE already consumes.
54-
This is the part I'd flag as the real critical path — get it wrong and
55-
autocomplete/error-highlighting silently degrades even if the APK still
56-
builds.
57-
58-
## Phase 3 — Wire BuildManager into the app
44+
## Phase 2 — Local project model: IN PROGRESS
45+
46+
Added `core/projects/.../internal/LocalWorkspaceModelBuilder.kt`. It builds
47+
the exact same `WorkspaceImpl`/`AndroidModule` shape as `WorkspaceModelBuilder`
48+
does today, but by scanning the module's `src/main/{java,kotlin,res,assets}`
49+
+ `libs/*.jar`/`*.aar` + `AndroidManifest.xml` directly — no Gradle sync.
50+
51+
Deliberately reuses `AndroidModule`/`GradleProject`/`tooling:builder-model-
52+
impl`'s `Default*` classes rather than inventing new ones — those are plain
53+
mutable data holders around real AGP builder-model *interfaces*, not
54+
Gradle-runtime-coupled, so **`tooling:builder-model-impl` should be kept**,
55+
not removed in Phase 4 (updating the removal list below).
56+
57+
**Not done yet / honest limitations of this first pass:**
58+
- Single Android app module only (root dir or `app/` subfolder). Multi-
59+
module local projects (`:app`, `:core`, ...) need a second pass that
60+
walks subfolders for additional manifests.
61+
- No transitive Maven dependency resolution — only `libs/*.jar`/`*.aar`,
62+
matching what the build engine itself supports (see Phase 1 table, req #6).
63+
- `ProjectSyncIssues`'s exact interface shape was written from memory —
64+
I could not fetch the real `com.android.tools.build:builder-model` jar in
65+
this sandbox (no Maven network access here) to confirm the method
66+
signature. **This needs a real compile pass against the actual AGP
67+
builder-model jar before you trust it** — flagged with a comment in the
68+
file itself.
69+
- **Not wired in yet.** `ProjectManagerImpl.kt` (line ~100) still calls the
70+
old `WorkspaceModelBuilder.build(projectDir, CachingProject(project))`.
71+
Swapping the call site is deliberately left undone until you can compile
72+
and test-run this locally — I don't have an Android SDK/Gradle-connected
73+
environment here to verify these ~250 lines of AGP-model-shaped code
74+
actually compile. Recommended next step: add a per-project toggle (e.g.
75+
a `.acs-local-build` marker file in the project root) so
76+
`ProjectManagerImpl` can call `LocalWorkspaceModelBuilder.build(...)`
77+
for opted-in projects while everything else keeps using Gradle — safest
78+
way to test this without risking every existing project.
79+
80+
## Phase 4 — Removal list (updated)
81+
82+
- `tooling:api`, `tooling:impl`, `tooling:model`, `tooling:events`,
83+
`tooling:plugin`, `tooling:plugin-config` — remove once Phase 2/3 proven
84+
- **`tooling:builder-model-impl` — KEEP.** It's reused by
85+
`LocalWorkspaceModelBuilder` (Phase 2); it only depends on AGP's
86+
`builder-model` *interfaces*, not the Gradle runtime.
87+
- `composite-builds/*` (build-logic, build-deps and their Gradle plugin glue)
88+
- Gradle wrapper usage for *target projects* (the IDE's own build can keep
89+
using Gradle — that's a separate concern from what it builds for users)
5990

6091
Files that call into Gradle today and need a `BuildManager`-based
6192
replacement (found via `grep`, not yet touched):
@@ -70,14 +101,6 @@ replacement (found via `grep`, not yet touched):
70101
- `core/app/src/main/java/com/tom/rv2ide/viewmodel/RunTasksViewModel.kt` + `RunTasksDialogFragment.kt` + `RunTasksListAdapter.kt` — currently list *Gradle tasks*; needs to become a fixed Build/Clean/Rebuild/Cancel action list instead (there are no arbitrary Gradle tasks anymore)
71102
- `core/app/src/main/java/com/tom/rv2ide/viewmodel/EditorViewModel.kt` — build-trigger entry point
72103

73-
## Phase 4 — Removal list (only after Phase 3 is verified working)
74-
75-
- `tooling:api`, `tooling:impl`, `tooling:model`, `tooling:events`,
76-
`tooling:plugin`, `tooling:plugin-config`, `tooling:builder-model-impl`
77-
- `composite-builds/*` (build-logic, build-deps and their Gradle plugin glue)
78-
- Gradle wrapper usage for *target projects* (the IDE's own build can keep
79-
using Gradle — that's a separate concern from what it builds for users)
80-
81104
## Bundling the actual tool binaries — DECIDED: first-run download
82105

83106
Added `tools/ToolsProvisioner.kt`. On first build (not on app open — so
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
* This file is part of AndroidCodeStudio.
3+
*
4+
* AndroidCodeStudio is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* AndroidCodeStudio is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with AndroidCodeStudio. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.tom.rv2ide.projects.internal
19+
20+
import com.android.builder.model.v2.ide.LibraryType
21+
import com.android.builder.model.v2.ide.ProjectType as AgpProjectType
22+
import com.android.builder.model.v2.models.ProjectSyncIssues
23+
import com.tom.rv2ide.builder.model.DefaultAndroidGradlePluginProjectFlags
24+
import com.tom.rv2ide.builder.model.DefaultJavaCompileOptions
25+
import com.tom.rv2ide.builder.model.DefaultLibrary
26+
import com.tom.rv2ide.builder.model.DefaultLibraryInfo
27+
import com.tom.rv2ide.builder.model.DefaultSourceProvider
28+
import com.tom.rv2ide.builder.model.DefaultSourceSetContainer
29+
import com.tom.rv2ide.builder.model.DefaultViewBindingOptions
30+
import com.tom.rv2ide.projects.GradleProject
31+
import com.tom.rv2ide.projects.android.AndroidModule
32+
import com.tom.rv2ide.tooling.api.IProject
33+
import com.tom.rv2ide.tooling.api.models.GradleTask
34+
import org.slf4j.LoggerFactory
35+
import org.xmlpull.v1.XmlPullParserFactory
36+
import java.io.File
37+
import java.io.FileReader
38+
import java.util.zip.ZipFile
39+
40+
/**
41+
* Requirement #1/#9/#10 (Phase 2): builds the same [WorkspaceImpl] shape that
42+
* [WorkspaceModelBuilder] produces from a Gradle sync, but by scanning the
43+
* project's folder layout directly — no Gradle daemon, no Tooling API
44+
* server process, no network.
45+
*
46+
* Deliberately reuses [GradleProject]/[AndroidModule]/the `tooling:builder-
47+
* model-impl` data classes rather than inventing new ones: those classes
48+
* are plain, mutable data holders around real AGP builder-model
49+
* *interfaces* (no Gradle runtime dependency), so every existing consumer
50+
* of the project model — Java/Kotlin completion, indexing, XML resource
51+
* resolution — keeps working unmodified. Only the *source* of the data
52+
* changes (filesystem scan vs. Gradle sync).
53+
*
54+
* Current scope: a single Android application module at the project root
55+
* (`AndroidManifest.xml` + `src/main/{java,kotlin,res,assets}` + `libs/`),
56+
* which is what Android Code Studio / Sketchware-Pro-style projects use.
57+
* Multi-module (`:app`, `:core`, ...) local projects and full transitive
58+
* Maven dependency resolution are follow-ups — see buildengine/PLAN.md.
59+
*/
60+
internal object LocalWorkspaceModelBuilder {
61+
62+
private val log = LoggerFactory.getLogger(LocalWorkspaceModelBuilder::class.java)
63+
64+
private const val ANDROID_JAR_PLACEHOLDER = "build-tools/android.jar"
65+
66+
fun build(projectDir: File, buildToolsRoot: File): WorkspaceImpl? {
67+
return try {
68+
val moduleDir = resolveAppModuleDir(projectDir)
69+
val manifest = File(moduleDir, "src/main/AndroidManifest.xml").takeIf { it.exists() }
70+
?: File(moduleDir, "AndroidManifest.xml").takeIf { it.exists() }
71+
?: run {
72+
log.warn("No AndroidManifest.xml found under {}", moduleDir)
73+
return null
74+
}
75+
76+
val namespace = readManifestPackage(manifest)
77+
val androidJar = File(buildToolsRoot, "android.jar")
78+
79+
val root = GradleProject(
80+
name = projectDir.name,
81+
description = "",
82+
path = ":",
83+
projectDir = projectDir,
84+
buildDir = File(projectDir, "build"),
85+
buildScript = File(projectDir, "build.gradle.kts").takeIf { it.exists() }
86+
?: File(projectDir, "build.gradle"),
87+
tasks = emptyList<GradleTask>() // No arbitrary Gradle tasks anymore; RunTasksViewModel
88+
// is repointed to fixed Build/Clean/Rebuild actions in Phase 3.
89+
)
90+
91+
val appModule = buildAndroidModule(moduleDir, manifest, namespace, androidJar)
92+
93+
WorkspaceImpl(
94+
projectDir,
95+
root,
96+
java.util.concurrent.CopyOnWriteArrayList(listOf<GradleProject>(appModule)),
97+
EmptyProjectSyncIssues
98+
)
99+
} catch (error: Throwable) {
100+
log.error("Unable to build local (Gradle-free) project model", error)
101+
null
102+
}
103+
}
104+
105+
/** For now: the project root itself, or its `app/` subfolder if that's
106+
* where the manifest actually lives (both layouts are common). */
107+
private fun resolveAppModuleDir(projectDir: File): File {
108+
val appSub = File(projectDir, "app")
109+
val appHasManifest = File(appSub, "src/main/AndroidManifest.xml").exists()
110+
return if (appHasManifest) appSub else projectDir
111+
}
112+
113+
private fun buildAndroidModule(
114+
moduleDir: File,
115+
manifest: File,
116+
namespace: String?,
117+
androidJar: File
118+
): AndroidModule {
119+
val srcMain = File(moduleDir, "src/main")
120+
val javaDir = File(srcMain, "java")
121+
val kotlinDir = File(srcMain, "kotlin")
122+
val resDir = File(srcMain, "res")
123+
val assetsDir = File(srcMain, "assets")
124+
125+
val sourceProvider = DefaultSourceProvider().apply {
126+
javaDirectories = listOfNotNull(javaDir.takeIf { it.exists() })
127+
kotlinDirectories = listOfNotNull(kotlinDir.takeIf { it.exists() })
128+
resDirectories = listOfNotNull(resDir.takeIf { it.exists() })
129+
resourcesDirectories = emptyList()
130+
assetsDirectories = listOfNotNull(assetsDir.takeIf { it.exists() })
131+
manifestFile = manifest
132+
name = "main"
133+
}
134+
val sourceSetContainer = DefaultSourceSetContainer().apply {
135+
sourceProvider = sourceProvider
136+
}
137+
138+
val (libraryMap, libraryKeys) = collectLibraries(moduleDir)
139+
140+
return AndroidModule(
141+
name = moduleDir.name,
142+
description = "",
143+
path = ":${moduleDir.name}",
144+
projectDir = moduleDir,
145+
buildDir = File(moduleDir, "build"),
146+
buildScript = File(moduleDir, "build.gradle.kts").takeIf { it.exists() }
147+
?: File(moduleDir, "build.gradle"),
148+
tasks = emptyList(),
149+
resourcePrefix = null,
150+
namespace = namespace,
151+
androidTestNamespace = null,
152+
testFixtureNamespace = null,
153+
projectType = AgpProjectType.APPLICATION,
154+
mainSourceSet = sourceSetContainer,
155+
flags = DefaultAndroidGradlePluginProjectFlags(emptyMap()),
156+
compilerSettings = DefaultJavaCompileOptions().apply {
157+
sourceCompatibility = "1.8"
158+
targetCompatibility = "1.8"
159+
encoding = "UTF-8"
160+
},
161+
viewBindingOptions = DefaultViewBindingOptions(),
162+
bootClassPaths = listOf(androidJar),
163+
libraries = libraryKeys,
164+
libraryMap = libraryMap,
165+
lintCheckJars = emptyList(),
166+
variants = emptyList(),
167+
configuredVariant = null,
168+
classesJar = null
169+
)
170+
}
171+
172+
/** Mirrors ClasspathCollector's libs/*.jar + libs/*.aar convention
173+
* (buildengine module), duplicated here in a Gradle-model shape so
174+
* IntelliSense sees the same jars the build engine compiles against. */
175+
private fun collectLibraries(moduleDir: File): Pair<Map<String, DefaultLibrary>, Set<String>> {
176+
val libsDir = File(moduleDir, "libs")
177+
val jars = libsDir.listFiles { f -> f.extension == "jar" }?.toList() ?: emptyList()
178+
val aars = libsDir.listFiles { f -> f.extension == "aar" }?.toList() ?: emptyList()
179+
180+
val map = mutableMapOf<String, DefaultLibrary>()
181+
for (jar in jars) {
182+
val key = "local:jar:${jar.name}"
183+
map[key] = DefaultLibrary().apply {
184+
this.key = key
185+
artifact = jar
186+
type = LibraryType.JAVA_LIBRARY
187+
libraryInfo = DefaultLibraryInfo(
188+
attributes = emptyMap(), buildType = null, capabilities = emptyList(),
189+
isTestFixtures = false, productFlavors = emptyMap(),
190+
group = "local", name = jar.nameWithoutExtension, version = "unspecified"
191+
)
192+
}
193+
}
194+
for (aar in aars) {
195+
val key = "local:aar:${aar.name}"
196+
map[key] = DefaultLibrary().apply {
197+
this.key = key
198+
artifact = aar
199+
type = LibraryType.ANDROID_LIBRARY
200+
libraryInfo = DefaultLibraryInfo(
201+
attributes = emptyMap(), buildType = null, capabilities = emptyList(),
202+
isTestFixtures = false, productFlavors = emptyMap(),
203+
group = "local", name = aar.nameWithoutExtension, version = "unspecified"
204+
)
205+
}
206+
}
207+
return map to map.keys
208+
}
209+
210+
/** Reads the legacy `package` attribute from AndroidManifest.xml.
211+
* (Local, manifest-based projects — like Sketchware Pro's — don't
212+
* require a `namespace {}` build.gradle entry the way modern AGP does.) */
213+
private fun readManifestPackage(manifest: File): String? {
214+
return try {
215+
val parser = XmlPullParserFactory.newInstance().newPullParser()
216+
parser.setInput(FileReader(manifest))
217+
var event = parser.eventType
218+
while (event != org.xmlpull.v1.XmlPullParser.END_DOCUMENT) {
219+
if (event == org.xmlpull.v1.XmlPullParser.START_TAG && parser.name == "manifest") {
220+
return parser.getAttributeValue(null, "package")
221+
}
222+
event = parser.next()
223+
}
224+
null
225+
} catch (e: Exception) {
226+
log.warn("Failed to read package name from {}", manifest, e)
227+
null
228+
}
229+
}
230+
231+
/** No sync issues are possible without a Gradle sync step.
232+
* NOTE: verify this matches the real AGP `ProjectSyncIssues` interface
233+
* shape at compile time (offline sandbox couldn't fetch builder-model.jar
234+
* to confirm the exact method signature — should be a single
235+
* `getSyncIssues(): Collection<SyncIssue>`). */
236+
private object EmptyProjectSyncIssues : ProjectSyncIssues {
237+
override fun getSyncIssues(): Collection<com.android.builder.model.v2.ide.SyncIssue> = emptyList()
238+
}
239+
}

0 commit comments

Comments
 (0)