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 src_template = """<!DOCTYPE html>
25 <title>{{ sourcepackage|e }} - Debian cross build</title>
27 tr.dep.bad td:nth-child(1) {
28 background-color: #faa;
30 tr.dep.tempbad td:nth-child(1) {
31 background-color: #ffa;
33 tr.dep.good td:nth-child(1) {
34 background-color: #afa;
36 tr.build.bad td:nth-child(4) {
37 background-color: #faa;
39 tr.build.tempbad td:nth-child(4) {
40 background-color: #ffa;
42 tr.build.good td:nth-child(4) {
43 background-color: #afa;
58 border-top: 1px solid;
65 <a href="https://tracker.debian.org/pkg/{{ sourcepackage|e }}">
66 {{- sourcepackage|e -}}
71 <h3>Cross build dependency satisfiability</h3>
76 <th>architectures</th>
80 {%- set okarchs = depresult.pop(None, None) -%}
81 {%- for reason, archs in depresult.items()|sort -%}
82 <tr class="dep {{ "tempbad" if reason.startswith("skew") else "bad" }}">
83 <td>{{ reason|e }}</td>
84 <td>{{ archs|arch_format }}</td>
90 <td>{{ okarchs|arch_format }}</td>
97 {%- if show_bootstrapdn -%}
99 <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
103 <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
108 <h3>Cross builds</h3>
115 <th>architecture</th>
120 {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
121 <tr class="build {{ "good" if build.success else "bad" }}">
123 {{- build.starttime|sqltimestamp|formatts -}}
125 <td>{{ build.version|e }}</td>
126 <td>{{ build.architecture|e }}</td>
128 <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
129 {{- "ok" if build.success else "failed" -}}
131 <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
138 <p>No build performed yet.</p>
140 <form method="POST" action="{{ url_for("request_schedule")|e }}">
141 <input type="submit" name="schedule" value="cross build" />
142 {{ sourcepackage|e }} for
143 <input type="hidden" name="source" value="{{ sourcepackage|e }}" />
144 <select name="architecture">
145 <option value="any">any</option>
146 {%- for architecture in architectures -%}
147 <option value="{{ architecture|e }}">{{ architecture|e }}</option>
153 <h3>Details about this service</h3>
155 <li>Maintainer: Helmut Grohne <helmut@subdivi.de></li>
156 <li>Source: git://git.subdivi.de/~helmut/crossqa.git</li>
163 schedule_template = """<!DOCTYPE html>
166 <p>Scheduled a build of {{ request.form["source"]|e }}
167 {%- if request.form["architecture"] != "any" %}
168 for {{ request.form["architecture"]|e -}}
175 @app.template_filter("sqltimestamp")
176 def sqltimestamp_filter(s):
177 strptime = datetime.datetime.strptime
179 return strptime(s, "%Y-%m-%d %H:%M:%S.%f").replace(microsecond=0)
181 return strptime(s, "%Y-%m-%d %H:%M:%S")
185 assert isinstance(ts, datetime.datetime)
186 dt = datetime.datetime.utcnow() - ts
187 if dt < datetime.timedelta(seconds=1):
189 if dt < datetime.timedelta(seconds=100):
190 return "%d s" % dt.seconds
191 if dt < datetime.timedelta(minutes=100):
192 return "%d m" % (dt.seconds // 60)
193 if dt < datetime.timedelta(days=1):
194 return "%d h" % (dt.seconds // (60 * 60))
195 return "%d d" % dt.days
198 @app.template_filter("formatts")
199 def formatts_filter(ts):
200 return jinja2.Markup('<time title="%s" datetime="%s">%s</time>' %
201 (ts, ts, formatts(ts)))
203 @app.template_filter('arch_format')
204 @jinja2.contextfilter
205 def arch_format_filter(context, some_archs):
206 if context["architectures"] == some_archs:
208 return ", ".join(sorted(some_archs))
210 def collect_depstate(conn, source):
213 query = sqlalchemy.text("""
214 SELECT version, architecture, satisfiable, reason
215 FROM depstate WHERE source = :source;""")
216 for row in conn.execute(query, source=source):
217 if version is None or version_compare(version, row.version) > 0:
218 version = row.version
220 depstate[row.architecture] = None if row.satisfiable else row.reason
222 raise werkzeug.exceptions.NotFound()
224 for arch, reason in depstate.items():
225 depresult.setdefault(reason, set()).add(arch)
226 return version, depresult
228 @app.route("/src/<source>")
229 def show_source(source):
230 context = dict(sourcepackage=source)
231 with db.engine.connect() as conn:
232 query = sqlalchemy.text("SELECT architecture FROM depcheck;")
233 context["architectures"] = set(row[0] for row in conn.execute(query))
234 context["version"], context["depresult"] = collect_depstate(conn,
236 query = sqlalchemy.text("""
237 SELECT version, architecture, success, starttime, filename
238 FROM builds WHERE source = :source;""")
239 context["builds"] = list(conn.execute(query, source=source))
240 context["show_bootstrapdn"] = \
241 any(reason and not reason.startswith("skew ")
242 for reason in context["depresult"].keys())
243 return flask.render_template_string(src_template, **context)
245 @app.route("/build/<path:filename>")
246 def show_log(filename):
247 if filename.endswith(".xz"):
248 return flask.send_from_directory("logs", filename,
249 mimetype="application/octet-stream")
250 filename = flask.safe_join("logs", filename + ".xz")
252 return flask.send_file(lzma.open(filename, "rb"),
253 mimetype="text/plain")
254 except FileNotFoundError:
255 raise werkzeug.exceptions.NotFound()
258 @app.route("/schedule", methods=["POST"])
259 def request_schedule():
260 source = flask.request.form["source"]
261 architecture = flask.request.form["architecture"]
262 with db.engine.connect() as conn:
263 query = sqlalchemy.text("""
264 SELECT 1 FROM depstate WHERE source = :source;""")
265 if not conn.execute(query, source=source).first():
266 raise werkzeug.exceptions.BadRequest()
267 if architecture == "any":
270 query = sqlalchemy.text("""
271 SELECT 1 FROM depcheck WHERE architecture = :architecture;""")
272 if not conn.execute(query, architecture=architecture).first():
273 raise werkzeug.exceptions.BadRequest()
274 query = sqlalchemy.text("""
275 INSERT INTO buildrequests (source, architecture, requesttime)
276 VALUES (:source, :architecture, datetime('now'));""")
277 conn.execute(query, source=source, architecture=architecture)
278 return flask.render_template_string(schedule_template)