2 # SPDX-License-Identifier: GPL-2.0+
9 version_compare = apt_pkg.version_compare
12 import flask_sqlalchemy
17 app = flask.Flask("crossqa")
18 app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db'
19 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
20 db = flask_sqlalchemy.SQLAlchemy(app)
22 index_template = """<!DOCTYPE html>
25 <title>Debian cross build quality assurance</title>
29 <h1>Debian cross build quality assurance</h1>
32 <h3>Recently failed builds</h3>
44 {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
47 <a href="{{ url_for("show_source", source=build.source) }}">
48 {{- build.source|e -}}
51 <td>{{ build.version|e }}</td>
52 <td>{{ build.architecture|e }}</td>
54 {{- build.starttime|sqltimestamp|formatts -}}
57 <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">log</a>
58 <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
66 <h3>Details about this service</h3>
68 <li>Maintainer: Helmut Grohne <helmut@subdivi.de></li>
69 <li>Source: git://git.subdivi.de/~helmut/crossqa.git</li>
76 src_template = """<!DOCTYPE html>
79 <title>{{ sourcepackage|e }} - Debian cross build</title>
81 tr.dep.bad td:nth-child(1) {
82 background-color: #faa;
84 tr.dep.tempbad td:nth-child(1) {
85 background-color: #ffa;
87 tr.dep.good td:nth-child(1) {
88 background-color: #afa;
90 tr.build.bad td:nth-child(4) {
91 background-color: #faa;
93 tr.build.tempbad td:nth-child(4) {
94 background-color: #ffa;
96 tr.build.good td:nth-child(4) {
97 background-color: #afa;
112 border-top: 1px solid;
119 <a href="https://tracker.debian.org/pkg/{{ sourcepackage|e }}">
120 {{- sourcepackage|e -}}
125 <h3>Cross build dependency satisfiability</h3>
130 <th>architectures</th>
134 {%- set okarchs = depresult.pop(None, None) -%}
135 {%- for reason, archs in depresult.items()|sort -%}
136 <tr class="dep {{ "tempbad" if reason.startswith("skew") else "bad" }}">
137 <td>{{ reason|e }}</td>
138 <td>{{ archs|arch_format }}</td>
142 <tr class="dep good">
144 <td>{{ okarchs|arch_format }}</td>
149 {%- if show_debcheck -%}
152 {%- if show_bootstrapdn -%}
154 <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
158 <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/latest/packages/{{ sourcepackage|e }}.html">debcheck</a>
164 <h3>Cross builds</h3>
171 <th>architecture</th>
176 {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
177 <tr class="build {{ "good" if build.success else "bad" }}">
179 {{- build.starttime|sqltimestamp|formatts -}}
181 <td>{{ build.version|e }}</td>
182 <td>{{ build.architecture|e }}</td>
184 <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
185 {{- "ok" if build.success else "failed" -}}
187 <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
194 <p>No build performed yet.</p>
196 <form method="POST" action="{{ url_for("request_schedule")|e }}">
197 <input type="submit" name="schedule" value="cross build" />
198 {{ sourcepackage|e }} for
199 <input type="hidden" name="source" value="{{ sourcepackage|e }}" />
200 <select name="architecture">
201 <option value="any">any</option>
202 {%- for architecture in architectures|sort -%}
203 <option value="{{ architecture|e }}">{{ architecture|e }}</option>
209 <h3>Details about this service</h3>
211 <li>Maintainer: Helmut Grohne <helmut@subdivi.de></li>
212 <li>Source: git://git.subdivi.de/~helmut/crossqa.git</li>
219 schedule_template = """<!DOCTYPE html>
222 <p>Scheduled a build of {{ request.form["source"]|e }}
223 {%- if request.form["architecture"] != "any" %}
224 for {{ request.form["architecture"]|e -}}
231 @app.template_filter("sqltimestamp")
232 def sqltimestamp_filter(s):
233 strptime = datetime.datetime.strptime
235 return strptime(s, "%Y-%m-%d %H:%M:%S.%f").replace(microsecond=0)
237 return strptime(s, "%Y-%m-%d %H:%M:%S")
241 assert isinstance(ts, datetime.datetime)
242 dt = datetime.datetime.utcnow() - ts
243 if dt < datetime.timedelta(seconds=1):
245 if dt < datetime.timedelta(seconds=100):
246 return "%d s" % dt.seconds
247 if dt < datetime.timedelta(minutes=100):
248 return "%d m" % (dt.seconds // 60)
249 if dt < datetime.timedelta(days=1):
250 return "%d h" % (dt.seconds // (60 * 60))
251 return "%d d" % dt.days
254 @app.template_filter("formatts")
255 def formatts_filter(ts):
256 return jinja2.Markup('<time title="%s" datetime="%s">%s</time>' %
257 (ts, ts, formatts(ts)))
259 @app.template_filter('arch_format')
260 @jinja2.contextfilter
261 def arch_format_filter(context, some_archs):
262 if context["architectures"] == some_archs:
264 return ", ".join(sorted(some_archs))
266 def collect_depstate(conn, source):
269 query = sqlalchemy.text("""
270 SELECT version, architecture, satisfiable, reason
271 FROM depstate WHERE source = :source;""")
272 for row in conn.execute(query, source=source):
273 if version is None or version_compare(version, row.version) > 0:
274 version = row.version
276 depstate[row.architecture] = None if row.satisfiable else row.reason
278 raise werkzeug.exceptions.NotFound()
280 for arch, reason in depstate.items():
281 depresult.setdefault(reason, set()).add(arch)
282 return version, depresult
286 with db.engine.connect() as conn:
287 builds = list(conn.execute("""
288 SELECT source, version, architecture, starttime, filename
293 return flask.render_template_string(index_template, builds=builds)
295 @app.route("/src/<source>")
296 def show_source(source):
297 context = dict(sourcepackage=source)
298 with db.engine.connect() as conn:
299 query = sqlalchemy.text("SELECT architecture FROM depcheck;")
300 context["architectures"] = set(row[0] for row in conn.execute(query))
301 context["version"], context["depresult"] = collect_depstate(conn,
303 query = sqlalchemy.text("""
304 SELECT version, architecture, success, starttime, filename
305 FROM builds WHERE source = :source;""")
306 context["builds"] = list(conn.execute(query, source=source))
307 context["show_bootstrapdn"] = \
308 any(reason and not reason.startswith("skew ")
309 for reason in context["depresult"].keys())
310 context["show_debcheck"] = \
311 any(context["depresult"].keys())
312 return flask.render_template_string(src_template, **context)
314 @app.route("/build/<path:filename>")
315 def show_log(filename):
316 if filename.endswith(".xz"):
317 return flask.send_from_directory("logs", filename,
318 mimetype="application/octet-stream")
319 filename = flask.safe_join("logs", filename + ".xz")
321 return flask.send_file(lzma.open(filename, "rb"),
322 mimetype="text/plain")
323 except FileNotFoundError:
324 raise werkzeug.exceptions.NotFound()
327 @app.route("/schedule", methods=["POST"])
328 def request_schedule():
329 source = flask.request.form["source"]
330 architecture = flask.request.form["architecture"]
331 with db.engine.connect() as conn:
332 query = sqlalchemy.text("""
333 SELECT 1 FROM depstate WHERE source = :source;""")
334 if not conn.execute(query, source=source).first():
335 raise werkzeug.exceptions.BadRequest()
336 if architecture == "any":
339 query = sqlalchemy.text("""
340 SELECT 1 FROM depcheck WHERE architecture = :architecture;""")
341 if not conn.execute(query, architecture=architecture).first():
342 raise werkzeug.exceptions.BadRequest()
343 query = sqlalchemy.text("""
344 INSERT INTO buildrequests (source, architecture, requesttime)
345 VALUES (:source, :architecture, datetime('now'));""")
346 conn.execute(query, source=source, architecture=architecture)
347 return flask.render_template_string(schedule_template)