Skip to content

Commit 5af617d

Browse files
committed
[GR-58613] Improve mx build output.
PullRequest: mx/1841
2 parents c3fdc7f + 3c3feff commit 5af617d

File tree

11 files changed

+457
-63
lines changed

11 files changed

+457
-63
lines changed

java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/CompilerDaemon.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.io.IOException;
2929
import java.io.InputStreamReader;
3030
import java.io.OutputStreamWriter;
31+
import java.io.PrintWriter;
3132
import java.net.InetAddress;
3233
import java.net.ServerSocket;
3334
import java.net.Socket;
@@ -45,6 +46,7 @@ public abstract class CompilerDaemon {
4546
// These values are used in mx.py so keep in sync.
4647
public static final String REQUEST_HEADER_COMPILE = "MX DAEMON/COMPILE: ";
4748
public static final String REQUEST_HEADER_SHUTDOWN = "MX DAEMON/SHUTDOWN";
49+
public static final String RESPONSE_DONE = "MX DAEMON/DONE:";
4850

4951
/**
5052
* The deamon will shut down after receiving this many requests with an unrecognized header.
@@ -122,7 +124,7 @@ private static void usage() {
122124
abstract Compiler createCompiler();
123125

124126
interface Compiler {
125-
int compile(String[] args) throws Exception;
127+
int compile(String[] args, PrintWriter out) throws Exception;
126128
}
127129

128130
public class Connection implements Runnable {
@@ -164,21 +166,27 @@ public void run() {
164166
String[] args = commandLine.split("\u0000");
165167
logf("%sCompiling %s%n", prefix, String.join(" ", args));
166168

167-
int result = compiler.compile(args);
168-
if (result != 0 && args.length != 0 && args[0].startsWith("GET / HTTP")) {
169-
// GR-52712
170-
System.err.printf("%sFailing compilation received on %s%n", prefix, connectionSocket);
169+
int result;
170+
PrintWriter log = new PrintWriter(output);
171+
try {
172+
result = compiler.compile(args, log);
173+
if (result != 0 && args.length != 0 && args[0].startsWith("GET / HTTP")) {
174+
// GR-52712
175+
System.err.printf("%sFailing compilation received on %s%n", prefix, connectionSocket);
176+
}
177+
} finally {
178+
log.flush();
171179
}
172180
logf("%sResult = %d%n", prefix, result);
173181

174-
output.write(result + "\n");
182+
output.write(RESPONSE_DONE + result + "\n");
175183
} else {
176184
int unrecognizedRequestCount = unrecognizedRequests.incrementAndGet();
177185
System.err.printf("%sUnrecognized request %d (len=%d): \"%s\"%n", prefix, unrecognizedRequestCount, request.length(), printable(request));
178186
if (unrecognizedRequestCount > MAX_UNRECOGNIZED_REQUESTS) {
179187
shutdown(String.format("%sReceived %d unrecognized requests: ", prefix, unrecognizedRequestCount));
180188
}
181-
output.write("-1\n");
189+
output.write(RESPONSE_DONE + "-1\n");
182190
}
183191
} finally {
184192
// close IO streams, then socket

java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/ECJDaemon.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
public class ECJDaemon extends CompilerDaemon {
3131

3232
private final class ECJCompiler implements Compiler {
33-
public int compile(String[] args) throws Exception {
34-
boolean result = (Boolean) compileMethod.invoke(null, args, new PrintWriter(System.out), new PrintWriter(System.err), null);
33+
public int compile(String[] args, PrintWriter out) throws Exception {
34+
boolean result = (Boolean) compileMethod.invoke(null, args, out, out, null);
3535
return result ? 0 : -1;
3636
}
3737
}

java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/JavacDaemon.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
*/
2525
package com.oracle.mxtool.compilerserver;
2626

27+
import java.io.PrintWriter;
2728
import java.lang.reflect.Method;
2829

2930
public class JavacDaemon extends CompilerDaemon {
3031

3132
private final class JavacCompiler implements Compiler {
32-
public int compile(String[] args) throws Exception {
33+
public int compile(String[] args, PrintWriter out) throws Exception {
3334
final Object receiver = javacMainClass.getDeclaredConstructor().newInstance();
34-
int result = (Integer) compileMethod.invoke(receiver, (Object) args);
35+
int result = (Integer) compileMethod.invoke(receiver, args, out);
3536
if (result != 0 && result != 1) {
3637
// @formatter:off
3738
/*
@@ -56,7 +57,7 @@ public int compile(String[] args) throws Exception {
5657

5758
JavacDaemon() throws Exception {
5859
this.javacMainClass = Class.forName("com.sun.tools.javac.Main");
59-
this.compileMethod = javacMainClass.getMethod("compile", String[].class);
60+
this.compileMethod = javacMainClass.getMethod("compile", String[].class, PrintWriter.class);
6061
}
6162

6263
@Override

src/mx/_impl/build/report.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#
2+
# ----------------------------------------------------------------------------------------------------
3+
#
4+
# Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
5+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
6+
#
7+
# This code is free software; you can redistribute it and/or modify it
8+
# under the terms of the GNU General Public License version 2 only, as
9+
# published by the Free Software Foundation.
10+
#
11+
# This code is distributed in the hope that it will be useful, but WITHOUT
12+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
# version 2 for more details (a copy is included in the LICENSE file that
15+
# accompanied this code).
16+
#
17+
# You should have received a copy of the GNU General Public License version
18+
# 2 along with this work; if not, write to the Free Software Foundation,
19+
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
#
21+
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
# or visit www.oracle.com if you need additional information or have any
23+
# questions.
24+
#
25+
# ----------------------------------------------------------------------------------------------------
26+
#
27+
28+
import html
29+
import os
30+
import time
31+
32+
from .. import mx
33+
34+
def write_task_report(f, task):
35+
f.write('\n<div class="task">\n')
36+
f.write(f' <h2>{task.name}</h2>\n')
37+
f.write(f' <p class="result {task.status}">{task.statusInfo}</p>\n')
38+
l = str(task._log).strip()
39+
if l:
40+
f.write(' <span class="log"><pre>\n')
41+
f.write(html.escape(l))
42+
f.write('\n </pre></span>\n')
43+
f.write('</div>\n')
44+
45+
def write_style(f):
46+
f.write('''
47+
<style>
48+
.header dt { font-weight: bold; }
49+
.success:before { content: "success"; color: green; }
50+
.failed:before { content: "failed"; color: red; }
51+
.skipped:before { content: "skipped"; margin-right: 2em; color: blue; }
52+
.log:before { content: "build log:"; }
53+
.log pre { border: 1px inset; padding: 5px; max-height: 350px; overflow: auto; }
54+
</style>
55+
''')
56+
57+
class BuildReport:
58+
def __init__(self, cmd_args):
59+
self.tasks = []
60+
self.properties = {}
61+
if cmd_args:
62+
self.properties['arguments'] = " ".join(cmd_args)
63+
64+
def set_tasks(self, tasks):
65+
self.tasks = tasks
66+
67+
def add_info(self, key, value):
68+
self.properties[key] = value
69+
70+
def _write_header(self, f):
71+
f.write('<h1>mx build report</h1>\n<dl class="header">\n')
72+
for k in self.properties:
73+
f.write(f" <dt>{k}</dt>\n")
74+
v = self.properties[k]
75+
if isinstance(v, list):
76+
f.write(" <dd>\n")
77+
for i in v:
78+
f.write(f" {html.escape(str(i))}<br/>\n")
79+
f.write(" </dd>\n")
80+
else:
81+
f.write(f" <dd>{html.escape(str(v))}</dd>\n")
82+
f.write('</dl>\n')
83+
84+
def _write_report(self, filename):
85+
allSkipped = True
86+
for t in self.tasks:
87+
if t.status != "skipped":
88+
allSkipped = False
89+
break
90+
if allSkipped:
91+
# don't bother writing a build log if there was nothing to do
92+
return
93+
with open(filename, 'w', encoding='utf-8') as f:
94+
f.write('<!DOCTYPE html>\n')
95+
f.write('<html>\n')
96+
f.write('<body>\n')
97+
self._write_header(f)
98+
for t in self.tasks:
99+
if t.status is not None:
100+
# only report on tasks that were started
101+
write_task_report(f, t)
102+
write_style(f)
103+
f.write('</body>\n')
104+
f.write('</html>\n')
105+
mx.log(f"mx build log written to {filename}")
106+
107+
def __enter__(self):
108+
self.properties['started'] = time.strftime("%Y-%m-%d %H:%M:%S")
109+
return self
110+
111+
def __exit__(self, exc_type, exc_value, traceback):
112+
self.properties['finished'] = time.strftime("%Y-%m-%d %H:%M:%S")
113+
114+
reportDir = mx.primary_suite().get_output_root(jdkDependent=False)
115+
mx.ensure_dir_exists(reportDir)
116+
base_name = time.strftime("buildlog-%Y%m%d-%H%M%S")
117+
reportFile = os.path.join(reportDir, base_name + ".html")
118+
reportIdx = 0
119+
while os.path.exists(reportFile):
120+
reportIdx += 1
121+
reportFile = os.path.join(reportDir, f'{base_name}_{reportIdx}.html')
122+
self._write_report(reportFile)

src/mx/_impl/build/tasks/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
"BuildTask",
3131
"NoOpTask",
3232
"Task",
33+
"TaskAbortException",
3334
"TaskSequence"
3435
]
3536

3637
from .build import Buildable, BuildTask
37-
from .task import Task
38+
from .task import Task, TaskAbortException
3839
from .noop import NoOpTask
3940
from .sequence import TaskSequence

src/mx/_impl/build/tasks/build.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,13 @@ def _timestamp(self) -> str:
181181
return ''
182182

183183
def logBuild(self, reason: Optional[str] = None) -> None:
184+
timestamp = self._timestamp()
185+
if self.args.build_logs == 'oneline':
186+
self.log(f'{timestamp}{self}...', echo=True, log=False)
184187
if reason:
185-
log(self._timestamp() + f'{self}... [{reason}]')
188+
self.log(f'{timestamp}{self}... [{reason}]')
186189
else:
187-
log(self._timestamp() + f'{self}...')
190+
self.log(f'{timestamp}{self}...')
188191

189192
def logBuildDone(self, duration: float) -> None:
190193
timestamp = self._timestamp()
@@ -193,13 +196,15 @@ def logBuildDone(self, duration: float) -> None:
193196
# Strip hours if 0
194197
if durationStr.startswith('0:'):
195198
durationStr = durationStr[2:]
196-
log(timestamp + f'{self} [duration: {duration}]')
199+
self.log(f'{timestamp}{self} [duration: {duration}]', echo=True)
197200

198201
def logClean(self) -> None:
199-
log(f'Cleaning {self.name}...')
202+
self.log(f'Cleaning {self.name}...')
200203

201204
def logSkip(self, reason: Optional[str] = None) -> None:
205+
self.status = "skipped"
202206
if reason:
207+
self.statusInfo = reason
203208
logv(f'[{reason} - skipping {self.name}]')
204209
else:
205210
logv(f'[skipping {self.name}]')

src/mx/_impl/build/tasks/task.py

+80-2
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,22 @@
2828
from __future__ import annotations
2929
from abc import ABCMeta, abstractmethod
3030
from argparse import Namespace
31+
from threading import Lock
3132
from typing import Dict, Optional, MutableSequence
3233

3334
from ..daemon import Daemon
3435
from ..suite import Dependency
35-
from ...support.logging import nyi
36+
from ... import mx
37+
from ...support.logging import nyi, setLogTask
3638
from ...support.processes import Process
3739

38-
__all__ = ["Task"]
40+
__all__ = ["Task", "TaskAbortException"]
3941

4042
Args = Namespace
4143

44+
class TaskAbortException(Exception):
45+
pass
46+
4247
class Task(object, metaclass=ABCMeta):
4348
"""A task executed during a build."""
4449

@@ -48,6 +53,8 @@ class Task(object, metaclass=ABCMeta):
4853
parallelism: int
4954
proc: Optional[Process]
5055

56+
consoleLock = Lock()
57+
5158
def __init__(self, subject: Dependency, args: Args, parallelism: int):
5259
"""
5360
:param subject: the dependency for which this task is executed
@@ -59,6 +66,13 @@ def __init__(self, subject: Dependency, args: Args, parallelism: int):
5966
self.parallelism = parallelism
6067
self.deps = []
6168
self.proc = None
69+
self.subprocs = []
70+
self._log = mx.LinesOutputCapture()
71+
self._echoImportant = not hasattr(args, 'build_logs') or args.build_logs in ["important", "full"]
72+
self._echoAll = hasattr(args, 'build_logs') and args.build_logs == "full"
73+
self._exitcode = 0
74+
self.status = None
75+
self.statusInfo = ""
6276

6377
def __str__(self) -> str:
6478
return nyi('__str__', self)
@@ -70,6 +84,70 @@ def __repr__(self) -> str:
7084
def name(self) -> str:
7185
return self.subject.name
7286

87+
def enter(self):
88+
self.status = "running"
89+
setLogTask(self)
90+
91+
def leave(self):
92+
if self.status == "running":
93+
self.status = "success"
94+
setLogTask(None)
95+
96+
def abort(self, code):
97+
self._exitcode = code
98+
self.status = "failed"
99+
raise TaskAbortException(code)
100+
101+
def log(self, msg, echo=False, log=True, important=True, replace=False):
102+
"""
103+
Log output for this build task.
104+
105+
Whether the output also goes to the console depends on the `--build-logs` option:
106+
* In `silent`, `oneline` and `interactive` mode, only messages with `echo=True` are printed.
107+
* In `full` mode, all messages are printed.
108+
* In `important` mode, only messages with `important=True` are printed.
109+
110+
`log=False` can be used to only do output, without including it in the log. This is useful
111+
in combination with `echo=True` to print a shorter summary of information that's already in
112+
the log in a more detailed form.
113+
114+
`replace=True` replaces the last logged line. This is useful for status output, e.g. download progress.
115+
"""
116+
if log:
117+
if replace:
118+
del self._log.lines[-1]
119+
self._log(msg)
120+
if echo or self._echoAll or (important and self._echoImportant):
121+
with Task.consoleLock:
122+
print(msg.rstrip())
123+
124+
def getLastLogLine(self):
125+
for line in reversed(self._log.lines):
126+
if line.strip():
127+
return line
128+
return None
129+
130+
def addSubproc(self, p):
131+
self.subprocs += [p]
132+
133+
def cancelSubprocs(self):
134+
from ...support.processes import _is_process_alive, _kill_process
135+
from signal import SIGTERM
136+
for p in self.subprocs:
137+
if not _is_process_alive(p):
138+
continue
139+
if mx.is_windows():
140+
p.terminate()
141+
else:
142+
_kill_process(p.pid, SIGTERM)
143+
144+
@property
145+
def exitcode(self):
146+
if self._exitcode != 0:
147+
return self._exitcode
148+
else:
149+
return self.proc.exitcode
150+
73151
@property
74152
def build_time(self):
75153
return getattr(self.subject, "build_time", 1)

0 commit comments

Comments
 (0)