-
-
Notifications
You must be signed in to change notification settings - Fork 84
Expand file tree
/
Copy pathgradle.ts
More file actions
248 lines (232 loc) · 6.99 KB
/
gradle.ts
File metadata and controls
248 lines (232 loc) · 6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as fs from 'fs';
import { abortIfCancelled, askForItemSelection } from '../utils/clack';
import {
plugin,
pluginKts,
pluginsBlock,
pluginsBlockKts,
sourceContext,
sourceContextKts,
} from './templates';
import * as bash from '../utils/bash';
import * as Sentry from '@sentry/node';
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import pc from 'picocolors';
import { fetchSdkVersion } from '../utils/release-registry';
/**
* A Gradle project may contain multiple modules, some of them may be applications, some of them may be libraries.
* We are only interested in applications. For example:
*
* myproject/
* app/
* lib1/
* lib2/
* wearApp/
*
* In this case^ we are interested in app/ and wearApp/
*
* @param buildGradleFiles a list of build.gradle(.kts) paths that contain the com.android.application plugin
* @returns the selected project for setting up
*/
export async function selectAppFile(
buildGradleFiles: string[],
): Promise<string> {
const appFiles = [];
for (let index = 0; index < buildGradleFiles.length; index++) {
const file = buildGradleFiles[index];
const text = fs.readFileSync(file, 'utf8');
if (/\(?["']com\.android\.application["']\)?(?!.*\S)/.test(text)) {
appFiles.push(file);
}
}
if (appFiles.length === 0) {
Sentry.setTag('custom-build-logic', true);
const appFile = await abortIfCancelled(
clack.text({
message: `Unable to find your app's directory.
Please enter the relative path to your app's build.gradle file from the root project`,
placeholder: 'app/build.gradle.kts',
validate(value) {
if (!value.includes('.gradle') || !fs.existsSync(value))
return `Not a valid gradle file.`;
},
}),
);
return appFile;
}
let appFile;
if (appFiles.length === 1) {
Sentry.setTag('multiple-projects', false);
appFile = appFiles[0];
} else {
Sentry.setTag('multiple-projects', true);
appFile = (
await askForItemSelection(
appFiles,
'Which project do you want to add Sentry to?',
)
).value;
}
Sentry.setTag('custom-build-logic', false);
return appFile;
}
/**
* Patches a build.gradle(.kts) file that contains `com.android.application` plugin.
* There are multiple cases we have to handle here:
* - An existing `plugins {}` block:
* - We just have to add our plugin inside the block
* - No existing `plugins {}` block
* - We have to add the entire block in the beginning of the file, BUT *after imports*
*
* For example (2nd case):
*
* ```
* import net.ltgt.gradle.errorprone.errorprone
*
* // our plugins block goes here <--
* plugins {
* id("io.sentry.android.gradle") version "3.12.0"
* }
*
* apply(plugin = "com.android.application")
*
* android {
* ...
* }
* ```
*
* In the end we run `./gradlew` to verify the config is build-able and not broken.
*
* @param appFile the selected Gradle application project
* @returns true if successfully added Sentry Gradle config, false otherwise
*/
export async function addGradlePlugin(
appFile: string,
orgSlug: string,
projectSlug: string,
): Promise<boolean> {
const gradleScript = fs.readFileSync(appFile, 'utf8');
if (/\(?["']io\.sentry\.android\.gradle["']\)?/.test(gradleScript)) {
// sentry gradle plugin is already installed
clack.log.success(
pc.greenBright(
`${pc.bold('Sentry Gradle plugin')} is already added to the project.`,
),
);
maybeAddSourceContextConfig(appFile, gradleScript, orgSlug, projectSlug);
return true;
}
const pluginVersion = await fetchSdkVersion(
'sentry.java.android.gradle-plugin',
);
const pluginsBlockMatch = /plugins\s*{[^{}]*}/.exec(gradleScript);
let newGradleScript;
if (!pluginsBlockMatch) {
// no "plugins {}" block, we can just add our own after imports
const regex = /import\s+[\w.]+/gm;
let importsMatch = regex.exec(gradleScript);
let insertIndex = 0;
while (importsMatch) {
insertIndex = importsMatch.index + importsMatch[0].length + 1;
importsMatch = regex.exec(gradleScript);
}
if (appFile.endsWith('.kts')) {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginsBlockKts(pluginVersion) +
gradleScript.slice(insertIndex);
} else {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginsBlock(pluginVersion) +
gradleScript.slice(insertIndex);
}
} else {
const insertIndex =
pluginsBlockMatch.index + pluginsBlockMatch[0].length - 1;
if (appFile.endsWith('.kts')) {
newGradleScript =
gradleScript.slice(0, insertIndex) +
pluginKts(pluginVersion) +
gradleScript.slice(insertIndex);
} else {
newGradleScript =
gradleScript.slice(0, insertIndex) +
plugin(pluginVersion) +
gradleScript.slice(insertIndex);
}
}
fs.writeFileSync(appFile, newGradleScript, 'utf8');
maybeAddSourceContextConfig(appFile, newGradleScript, orgSlug, projectSlug);
const buildSpinner = clack.spinner();
buildSpinner.start(
'Running ./gradlew to verify changes (this may take a few minutes)...',
);
try {
await bash.execute('./gradlew');
buildSpinner.stop(
pc.greenBright(
`${pc.bold('Sentry Gradle plugin')} added to the project.`,
),
);
} catch (e) {
buildSpinner.stop();
Sentry.captureException('Gradle Sync failed');
return false;
}
return true;
}
/**
* Looks for the applications packageName (namespace) in the specified build.gradle(.kts) file.
*
* ```
* android {
* namespace 'my.package.name' <-- this is what we extract
*
* compileSdkVersion = 31
* ...
* }
* ```
* @param appFile
* @returns the packageName(namespace) of the app if available
*/
export function getNamespace(appFile: string): string | undefined {
const gradleScript = fs.readFileSync(appFile, 'utf8');
const namespaceMatch = /namespace\s*=?\s*['"]([^'"]+)['"]/i.exec(
gradleScript,
);
if (!namespaceMatch || namespaceMatch.length <= 1) {
clack.log.warn('Unable to determine application package name.');
Sentry.captureException('No package name');
return undefined;
}
const namespace = namespaceMatch[1];
return namespace;
}
/**
* Adds source context configuration to the gradleScript if `sentry {}` block is not yet configured,
*
* @param appFile
* @param gradleScript
*/
function maybeAddSourceContextConfig(
appFile: string,
gradleScript: string,
orgSlug: string,
projectSlug: string,
) {
if (!/sentry\s*\{[^}]*\}/i.test(gradleScript)) {
// if no sentry {} block is configured, we add our own with source context enabled
if (appFile.endsWith('.kts')) {
fs.appendFileSync(
appFile,
sourceContextKts(orgSlug, projectSlug),
'utf8',
);
} else {
fs.appendFileSync(appFile, sourceContext(orgSlug, projectSlug), 'utf8');
}
}
}