Skip to content

Commit a9c37ee

Browse files
committed
Refactor build information retrieval
- Change Tai-e to read build information from "META-INF/tai-e-build.properties" instead of "META-INF/MANIFEST.MF", which can be overwritten during repackaging - Replace dual-strategy reading (gradle.properties/.git for developers, MANIFEST.MF for users) with unified approach that doesn't depend on source code structure
1 parent 29046be commit a9c37ee

File tree

5 files changed

+159
-133
lines changed

5 files changed

+159
-133
lines changed

build.gradle.kts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,31 @@ application {
4141
mainClass.set("pascal.taie.Main")
4242
}
4343

44+
tasks.register("generateBuildInfo") {
45+
group = "build"
46+
description = "Generates build information properties file"
47+
// write tai-e build information into META-INF/tai-e-build.properties
48+
val buildPropsFile = rootProject.layout.buildDirectory.file(
49+
"resources/main/META-INF/tai-e-build.properties").get().asFile
50+
val versionProvider = projectVersionProvider
51+
val commitProvider = projectCommitProvider
52+
doFirst {
53+
buildPropsFile.parentFile.mkdirs()
54+
val buildProps = """
55+
version=${versionProvider.get()}
56+
commit=${commitProvider.get()}
57+
""".trimIndent()
58+
buildPropsFile.writeText(buildProps)
59+
}
60+
}
61+
4462
tasks.register<Jar>("fatJar", Jar::class) {
4563
group = "build"
4664
description = "Creates a single jar file including Tai-e and all dependencies"
4765
manifest {
4866
attributes["Main-Class"] = "pascal.taie.Main"
49-
attributes["Tai-e-Version"] = projectVersion
50-
attributes["Tai-e-Commit"] = projectCommit
5167
}
52-
archiveBaseName.set("tai-e-all")
68+
archiveBaseName.set("${projectArtifactId}-all")
5369
from(
5470
configurations.runtimeClasspath.get().map {
5571
if (it.isDirectory) it else zipTree(it)
@@ -65,10 +81,13 @@ tasks.jar {
6581
from("COPYING", "COPYING.LESSER")
6682
from(zipTree("lib/sootclasses-modified.jar"))
6783
destinationDirectory.set(rootProject.layout.buildDirectory)
68-
manifest {
69-
attributes["Tai-e-Version"] = projectVersion
70-
attributes["Tai-e-Commit"] = projectCommit
71-
}
84+
archiveBaseName.set(projectArtifactId)
85+
}
86+
87+
tasks.processResources {
88+
// Generate a build information properties file in resources directory,
89+
// so that it can be included in the class path and JAR file.
90+
finalizedBy("generateBuildInfo")
7291
}
7392

7493
tasks.withType<Test> {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import org.gradle.api.file.DirectoryProperty
2+
import org.gradle.api.provider.ValueSource
3+
import org.gradle.api.provider.ValueSourceParameters
4+
5+
/**
6+
* A ValueSource implementation that retrieves the current Git commit hash from the repository.
7+
*
8+
* This implementation reads Git metadata files directly from the `.git` directory to determine
9+
* the current commit hash. It supports both regular Git repositories and repositories with
10+
* references stored in the info/refs file.
11+
*
12+
* This ValueSource is designed to be compatible with Gradle's Configuration Cache by properly
13+
* declaring its inputs and avoiding direct file system access during configuration time.
14+
*
15+
* @see ValueSource
16+
*/
17+
abstract class GitCommitValueSource
18+
: ValueSource<String, GitCommitValueSource.Parameters> {
19+
20+
/**
21+
* Parameters for the GitCommitValueSource.
22+
*
23+
* Contains the configuration needed to locate and read Git metadata files.
24+
*/
25+
interface Parameters : ValueSourceParameters {
26+
27+
/**
28+
* The directory containing `.git` directory.
29+
*/
30+
val projectDir: DirectoryProperty
31+
}
32+
33+
/**
34+
* Obtains the current Git commit hash by reading Git metadata files.
35+
*
36+
* The implementation follows this process:
37+
* 1. Reads the `.git/HEAD` file to determine the current reference
38+
* 2. If HEAD points to a branch reference (starts with "ref: "):
39+
* - First attempts to read the commit hash from the corresponding file under `.git/refs/`
40+
* - If the reference file doesn't exist, falls back to reading from `.git/info/refs`
41+
* 3. If HEAD contains a direct commit hash, returns it directly
42+
*
43+
* @return The current Git commit hash as a String, or "Unknown" if the commit hash
44+
* cannot be determined (e.g., not a Git repository, corrupted Git metadata,
45+
* or I/O errors)
46+
*/
47+
override fun obtain(): String {
48+
return try {
49+
val projectDir = parameters.projectDir.asFile.get()
50+
val gitHeadFile = projectDir.resolve(".git/HEAD")
51+
if (!gitHeadFile.exists()) return "Unknown"
52+
53+
val gitHead = gitHeadFile.readText().trim()
54+
if (gitHead.startsWith("ref: ")) {
55+
val ref = gitHead.substring(5).trim()
56+
// ref format may be like "refs/heads/master",
57+
// which indicates we should read the file at '.git/refs/heads/master'
58+
// to get the actual commit hash for the specified branch
59+
val refFile = projectDir.resolve(".git/${ref}")
60+
if (refFile.exists()) {
61+
refFile.readText().trim()
62+
} else {
63+
// read from '.git/info/refs' line by line
64+
val infoRefsFile = projectDir.resolve(".git/info/refs")
65+
if (infoRefsFile.exists()) {
66+
infoRefsFile.readLines()
67+
.firstOrNull { line -> line.endsWith(ref) }
68+
?.split("\t")
69+
?.get(0) ?: "Unknown"
70+
} else {
71+
"Unknown"
72+
}
73+
}
74+
} else {
75+
// HEAD contains a direct commit hash (detached HEAD state)
76+
gitHead
77+
}
78+
} catch (e: Exception) {
79+
// Return "Unknown" for any I/O errors or unexpected file formats
80+
"Unknown"
81+
}
82+
}
83+
84+
}

buildSrc/src/main/kotlin/ProjectExtensions.kt

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import org.gradle.api.Project
2+
import org.gradle.api.provider.Provider
23
import org.gradle.api.file.RegularFile
34
import org.gradle.api.internal.project.ProjectInternal
45
import org.gradle.jvm.toolchain.JavaLanguageVersion
56
import org.gradle.jvm.toolchain.JavaToolchainService
6-
import kotlin.io.path.Path
7-
import kotlin.io.path.exists
8-
import kotlin.io.path.forEachLine
9-
import kotlin.io.path.readText
10-
import kotlin.io.path.useLines
117

128
fun Project.getProperty(key: String) =
139
providers.gradleProperty(key).get()
@@ -27,31 +23,12 @@ val Project.projectUrl: String
2723
val Project.projectDescription: String
2824
get() = getProperty("projectDescription")
2925

30-
val Project.projectCommit: String
31-
get() {
32-
try {
33-
val gitHead = Path(rootDir.path, ".git", "HEAD").readText()
34-
if (gitHead.startsWith("ref: ")) {
35-
val ref = gitHead.substring(5).trim()
36-
// path '.git/refs/heads/branchName'
37-
val p = Path(rootDir.path, ".git", ref)
38-
if (p.exists()) {
39-
return p.readText().trim()
40-
} else {
41-
// read from '.git/info/refs' line by line
42-
Path(rootDir.path, ".git", "info", "refs").forEachLine {
43-
if (it.endsWith(ref)) {
44-
return it.split("\t")[0]
45-
}
46-
}
47-
}
48-
} else {
49-
return gitHead.trim()
50-
}
51-
} catch (e: Exception) {
52-
logger.warn("Failed to read Git commit hash: {}", e.toString())
53-
}
54-
return "Unknown"
26+
val Project.projectVersionProvider: Provider<String>
27+
get() = providers.gradleProperty("projectVersion")
28+
29+
val Project.projectCommitProvider: Provider<String>
30+
get() = providers.of(GitCommitValueSource::class.java) {
31+
parameters.projectDir.set(rootProject.layout.projectDirectory)
5532
}
5633

5734
val Project.isSnapshot: Boolean

integration-tests/src/test/java/pascal/taie/integration/FatJarTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ void testLogFile() throws Exception {
9797
// Check if the log file contains expected content
9898
assertTrue(logContent.contains("Writing log to"));
9999
assertTrue(result.stdout().contains("Writing log to"));
100+
String version = logContent.split("Tai-e Version: ")[1].split("\n")[0].strip();
101+
assertFalse(version.isEmpty(), "Tai-e version should not be empty");
102+
assertFalse(version.toLowerCase().contains("unknown"), "Tai-e version should not be unknown");
103+
String commit = logContent.split("Tai-e Commit: ")[1].split("\n")[0].strip();
104+
assertFalse(commit.isEmpty(), "Tai-e commit should not be empty");
105+
assertFalse(commit.toLowerCase().contains("unknown"), "Tai-e commit should not be unknown");
100106
// Check if the options.yml file is created
101107
File optionsFile = new File(tempDir, "output/options.yml");
102108
assertTrue(optionsFile.exists(), "Options file should be created");

src/main/java/pascal/taie/util/RuntimeInfoLogger.java

Lines changed: 36 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@
2626
import org.apache.logging.log4j.Logger;
2727

2828
import javax.annotation.Nullable;
29-
import java.io.FileReader;
3029
import java.net.JarURLConnection;
3130
import java.net.URL;
32-
import java.nio.file.Files;
33-
import java.nio.file.Path;
3431
import java.util.Properties;
35-
import java.util.jar.Manifest;
32+
import java.util.jar.JarFile;
3633

3734
/**
3835
* A utility class for logging runtime information about the environment and the Tai-e application.
@@ -53,11 +50,12 @@ public class RuntimeInfoLogger {
5350
"os.arch",
5451
};
5552

56-
private static final String VERSION_MANIFEST_KEY = "Tai-e-Version";
53+
private static final String BUILD_PROPERTY_FILE_PATH =
54+
"META-INF/tai-e-build.properties";
5755

58-
private static final String COMMIT_MANIFEST_KEY = "Tai-e-Commit";
56+
private static final String VERSION_KEY = "version";
5957

60-
private static final String VERSION_PROPERTY_KEY = "projectVersion";
58+
private static final String COMMIT_KEY = "commit";
6159

6260
private static final String UNKNOWN = "Unknown";
6361

@@ -80,112 +78,54 @@ private static void logEnvInfo() {
8078
}
8179

8280
/**
83-
* Logs Tai-e version and commit information by attempting to read from the manifest file
84-
* or fallback methods if the manifest is not available.
81+
* Logs Tai-e version and commit information by attempting to read from the build properties file
82+
* or fallback methods if the build properties are not available.
8583
*/
8684
private static void logTaieInfo() {
87-
Manifest manifest = getManifest();
88-
String version = manifest != null ? readVersionFromManifest(manifest)
89-
: readVersionFromGradleProperties();
85+
Properties properties = getBuildProperties();
86+
String version = properties != null
87+
? properties.getProperty(VERSION_KEY)
88+
: UNKNOWN;
9089
logger.info("Tai-e Version: {}", version);
91-
String commit = manifest != null ? readCommitFromManifest(manifest)
92-
: readCommitFromDotGit();
90+
String commit = properties != null
91+
? properties.getProperty(COMMIT_KEY)
92+
: UNKNOWN;
9393
logger.info("Tai-e Commit: {}", commit);
9494
}
9595

9696
/**
97-
* Reads the Tai-e version from the provided manifest.
97+
* Retrieves the build properties of the current JAR file, if available.
9898
*
99-
* @param manifest the manifest to read from
100-
* @return the Tai-e version, or {@code "Unknown"} if not found
101-
*/
102-
private static String readVersionFromManifest(Manifest manifest) {
103-
String version = manifest.getMainAttributes().getValue(VERSION_MANIFEST_KEY);
104-
if (version == null) {
105-
logger.warn("Manifest does not contain Tai-e version information");
106-
return UNKNOWN;
107-
}
108-
return version;
109-
}
110-
111-
/**
112-
* Reads the Tai-e version from the gradle.properties file.
113-
*
114-
* @return the Tai-e version, or {@code "Unknown"} if an error occurs
115-
*/
116-
private static String readVersionFromGradleProperties() {
117-
try {
118-
Properties properties = new Properties();
119-
properties.load(new FileReader(Path.of("gradle.properties").toFile()));
120-
return properties.getProperty(VERSION_PROPERTY_KEY);
121-
} catch (Exception e) {
122-
logger.warn("Failed to read version from 'gradle.properties': {}", e.toString());
123-
}
124-
return UNKNOWN;
125-
}
126-
127-
/**
128-
* Reads the Tai-e commit hash from the provided manifest.
129-
*
130-
* @param manifest the manifest to read from
131-
* @return the Tai-e commit hash, or {@code "Unknown"} if not found
132-
*/
133-
private static String readCommitFromManifest(Manifest manifest) {
134-
String commit = manifest.getMainAttributes().getValue(COMMIT_MANIFEST_KEY);
135-
if (commit == null) {
136-
logger.warn("Manifest does not contain Tai-e commit information");
137-
return UNKNOWN;
138-
}
139-
return commit;
140-
}
141-
142-
/**
143-
* Reads the current git commit hash from the .git directory.
144-
*
145-
* @return the current git commit hash, or {@code "Unknown"} if an error occurs
146-
*/
147-
private static String readCommitFromDotGit() {
148-
try {
149-
String gitHead = Files.readString(Path.of(".git", "HEAD"));
150-
if (gitHead.startsWith("ref: ")) {
151-
String ref = gitHead.substring(5).trim();
152-
// path '.git/refs/heads/branchName'
153-
Path p = Path.of(".git", ref);
154-
if (p.toFile().exists()) {
155-
return Files.readString(p).trim();
156-
} else {
157-
// read from '.git/info/refs' line by line
158-
return Files.lines(Path.of(".git", "info", "refs"))
159-
.filter(line -> line.endsWith(ref))
160-
.map(line -> line.split("\t")[0])
161-
.findFirst()
162-
.orElse(UNKNOWN);
163-
}
164-
} else {
165-
return gitHead.trim();
166-
}
167-
} catch (Exception e) {
168-
logger.warn("Failed to read Git commit hash: {}", e.toString());
169-
}
170-
return UNKNOWN;
171-
}
172-
173-
/**
174-
* Retrieves the manifest of the current JAR file, if available.
175-
*
176-
* @return the manifest, or {@code null} if an error occurs or the manifest is not found
99+
* @return the build properties, or {@code null} if an error occurs or the build properties is not found
177100
*/
178101
@Nullable
179-
private static Manifest getManifest() {
102+
private static Properties getBuildProperties() {
180103
try {
181104
URL url = RuntimeInfoLogger.class.getProtectionDomain().getCodeSource().getLocation();
182105
if (url.getPath().endsWith(".jar")) {
183106
var jarConnection = (JarURLConnection) new URL("jar:" + url + "!/")
184107
.openConnection();
185-
return jarConnection.getManifest();
108+
JarFile jarFile = jarConnection.getJarFile();
109+
var buildPropsEntry = jarFile.getJarEntry(BUILD_PROPERTY_FILE_PATH);
110+
if (buildPropsEntry != null) {
111+
try (var inputStream = jarFile.getInputStream(buildPropsEntry)) {
112+
Properties properties = new Properties();
113+
properties.load(inputStream);
114+
return properties;
115+
}
116+
}
117+
} else {
118+
try (var inputStream = RuntimeInfoLogger.class
119+
.getClassLoader().getResourceAsStream(BUILD_PROPERTY_FILE_PATH)) {
120+
if (inputStream != null) {
121+
Properties properties = new Properties();
122+
properties.load(inputStream);
123+
return properties;
124+
}
125+
}
186126
}
187127
} catch (Exception e) {
188-
logger.warn("Failed to read manifest: {}", e.toString());
128+
logger.warn("Failed to read build properties: {}", e.toString());
189129
}
190130
return null;
191131
}

0 commit comments

Comments
 (0)