source: appstream-generator/src/asgen/config.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 14.5 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.config;
21
22import std.stdio;
23import std.array;
24import std.string : format, toLower;
25import std.path : dirName, getcwd, buildPath, buildNormalizedPath;
26import std.conv : to;
27import std.json;
28import std.typecons;
29static import std.file;
30
31public import gi.appstreamtypes : FormatVersion;
32
33import asgen.utils : existsAndIsDir, randomString;
34import asgen.logging;
35
36
37public immutable generatorVersion = "0.6.4";
38
39/**
40 * Describes a suite in a software repository.
41 **/
42struct Suite
43{
44    string name;
45    int dataPriority = 0;
46    string baseSuite;
47    string iconTheme;
48    string[] sections;
49    string[] architectures;
50    bool isImmutable;
51}
52
53/**
54 * The AppStream metadata type we want to generate.
55 **/
56enum DataType
57{
58    XML,
59    YAML
60}
61
62/**
63 * Distribution-specific backends.
64 **/
65enum Backend
66{
67    Unknown,
68    Dummy,
69    Debian,
70    Ubuntu,
71    Archlinux,
72    RpmMd
73}
74
75enum GeneratorFeature
76{
77    NONE = 0,
78    PROCESS_DESKTOP     = 1 << 0,
79    VALIDATE            = 1 << 1,
80    NO_DOWNLOADS        = 1 << 2,
81    STORE_SCREENSHOTS   = 1 << 3,
82    OPTIPNG             = 1 << 4,
83    METADATA_TIMESTAMPS = 1 << 5,
84    IMMUTABLE_SUITES    = 1 << 6,
85    PROCESS_FONTS       = 1 << 6
86}
87
88final class Config
89{
90private:
91    private string workspaceDir;
92    private string exportDir;
93
94    private string tmpDir;
95
96    // Thread local
97    private static bool instantiated_;
98
99    // Thread global
100    private __gshared Config instance_;
101
102    private this () {
103        formatVersion = FormatVersion.V0_10;
104    }
105
106public:
107    FormatVersion formatVersion;
108    string projectName;
109    string archiveRoot;
110    string mediaBaseUrl;
111    string htmlBaseUrl;
112
113    Backend backend;
114    Suite[] suites;
115    string[] oldsuites;
116    DataType metadataType;
117    uint enabledFeatures; // bitfield
118
119    bool[string] allowedCustomKeys; // set of allowed keys in <custom/> tags
120
121    string dataExportDir;
122    string hintsExportDir;
123    string mediaExportDir;
124    string htmlExportDir;
125
126    string caInfo;
127
128    static Config get ()
129    {
130        if (!instantiated_) {
131            synchronized (Config.classinfo) {
132                if (!instance_)
133                    instance_ = new Config ();
134
135                instantiated_ = true;
136            }
137        }
138
139        return instance_;
140    }
141
142    @property
143    string formatVersionStr ()
144    {
145        import asgen.bindings.appstream_utils;
146        import std.string : fromStringz;
147        return fromStringz (as_format_version_to_string (formatVersion));
148    }
149
150    @property
151    const string databaseDir ()
152    {
153        return buildPath (workspaceDir, "db");
154    }
155
156    @property
157    const string cacheRootDir ()
158    {
159        return buildPath (workspaceDir, "cache");
160    }
161
162    @property
163    string templateDir () {
164        // find a suitable template directory
165        // first check the workspace
166        auto tdir = buildPath (workspaceDir, "templates");
167        tdir = getVendorTemplateDir (tdir, true);
168
169        if (tdir is null) {
170            immutable exeDir = dirName (std.file.thisExePath ());
171            tdir = buildNormalizedPath (exeDir, "..", "data", "templates");
172
173            tdir = getVendorTemplateDir (tdir);
174            if (tdir is null) {
175                tdir = getVendorTemplateDir ("/usr/share/appstream/templates");
176            }
177        }
178
179        return tdir;
180    }
181
182    /**
183     * Helper function to determine a vendor template directory.
184     */
185    private string getVendorTemplateDir (const string dir, bool allowRoot = false) @safe
186    {
187        string tdir;
188        if (projectName !is null) {
189            tdir = buildPath (dir, projectName.toLower ());
190            if (existsAndIsDir (tdir))
191                return tdir;
192        }
193        tdir = buildPath (dir, "default");
194        if (existsAndIsDir (tdir))
195            return tdir;
196        if (allowRoot) {
197            if (existsAndIsDir (dir))
198                return dir;
199        }
200
201        return null;
202    }
203
204    private void setFeature (GeneratorFeature feature, bool enabled)
205    {
206        if (enabled)
207            enabledFeatures |= feature;
208        else
209            disableFeature (feature);
210    }
211
212    private void disableFeature (GeneratorFeature feature)
213    {
214        enabledFeatures &= ~feature;
215    }
216
217    bool featureEnabled (GeneratorFeature feature)
218    {
219        return (enabledFeatures & feature) > 0;
220    }
221
222    void loadFromFile (string fname)
223    {
224        // read the configuration JSON file
225        auto f = File (fname, "r");
226        string jsonData;
227        string line;
228        while ((line = f.readln ()) !is null)
229            jsonData ~= line;
230
231        JSONValue root = parseJSON (jsonData);
232
233        workspaceDir = dirName (fname);
234        if (workspaceDir.empty)
235            workspaceDir = getcwd ();
236
237        this.projectName = "Unknown";
238        if ("ProjectName" in root)
239            this.projectName = root["ProjectName"].str;
240
241        this.archiveRoot = root["ArchiveRoot"].str;
242
243        this.mediaBaseUrl = "";
244        if ("MediaBaseUrl" in root)
245            this.mediaBaseUrl = root["MediaBaseUrl"].str;
246
247        this.htmlBaseUrl = "";
248        if ("HtmlBaseUrl" in root)
249            this.htmlBaseUrl = root["HtmlBaseUrl"].str;
250
251        // set the default export directory locations, allow people to override them in the config
252        exportDir      = buildPath (workspaceDir, "export");
253        mediaExportDir = buildPath (exportDir, "media");
254        dataExportDir  = buildPath (exportDir, "data");
255        hintsExportDir = buildPath (exportDir, "hints");
256        htmlExportDir  = buildPath (exportDir, "html");
257
258        if ("ExportDirs" in root) {
259            auto edirs = root["ExportDirs"].object;
260            foreach (dirId; edirs.byKeyValue) {
261                switch (dirId.key) {
262                    case "Media":
263                        mediaExportDir = dirId.value.str;
264                        break;
265                    case "Data":
266                        dataExportDir = dirId.value.str;
267                        break;
268                    case "Hints":
269                        hintsExportDir = dirId.value.str;
270                        break;
271                    case "Html":
272                        htmlExportDir = dirId.value.str;
273                        break;
274                    default:
275                        logWarning ("Unknown export directory specifier in config: %s", dirId.key);
276                }
277            }
278        }
279
280        this.metadataType = DataType.XML;
281        if ("MetadataType" in root)
282            if (root["MetadataType"].str.toLower () == "yaml")
283                this.metadataType = DataType.YAML;
284
285        if ("CAInfo" in root)
286            this.caInfo = root["CAInfo"].str;
287
288        // allow specifying the AppStream format version we build data for.
289        if ("FormatVersion" in root) {
290            immutable versionStr = root["FormatVersion"].str;
291            if (versionStr == "0.8")
292                formatVersion = FormatVersion.V0_8;
293            else if (versionStr == "0.9")
294                formatVersion = FormatVersion.V0_9;
295            else if (versionStr == "0.10")
296                formatVersion = FormatVersion.V0_10;
297        }
298
299        // we default to the Debian backend for now
300        auto backendName = "debian";
301        if ("Backend" in root)
302            backendName = root["Backend"].str.toLower ();
303        switch (backendName) {
304            case "dummy":
305                this.backend = Backend.Dummy;
306                this.metadataType = DataType.YAML;
307                break;
308            case "debian":
309                this.backend = Backend.Debian;
310                this.metadataType = DataType.YAML;
311                break;
312            case "ubuntu":
313                this.backend = Backend.Ubuntu;
314                this.metadataType = DataType.YAML;
315                break;
316            case "arch":
317            case "archlinux":
318                this.backend = Backend.Archlinux;
319                this.metadataType = DataType.XML;
320                break;
321            case "mageia":
322            case "rpmmd":
323                this.backend = Backend.RpmMd;
324                this.metadataType = DataType.XML;
325                break;
326            default:
327                break;
328        }
329
330        auto hasImmutableSuites = false;
331        foreach (suiteName; root["Suites"].object.byKey ()) {
332            Suite suite;
333            suite.name = suiteName;
334
335            // having a suite named "pool" will result in the media pool being copied on
336            // itself if immutableSuites is used. Since 'pool' is a bad suite name anyway,
337            // we error out early on this.
338            if (suiteName == "pool")
339                throw new Exception ("The name 'pool' is forbidden for a suite.");
340
341            auto sn = root["Suites"][suiteName];
342            if ("dataPriority" in sn)
343                suite.dataPriority = to!int (sn["dataPriority"].integer);
344            if ("baseSuite" in sn)
345                suite.baseSuite = sn["baseSuite"].str;
346            if ("useIconTheme" in sn)
347                suite.iconTheme = sn["useIconTheme"].str;
348            if ("sections" in sn)
349                foreach (sec; sn["sections"].array)
350                    suite.sections ~= sec.str;
351            if ("architectures" in sn)
352                foreach (arch; sn["architectures"].array)
353                    suite.architectures ~= arch.str;
354            if ("immutable" in sn) {
355                suite.isImmutable = sn["immutable"].type == JSON_TYPE.TRUE;
356                if (suite.isImmutable)
357                    hasImmutableSuites = true;
358            }
359
360            suites ~= suite;
361        }
362
363        if ("Oldsuites" in root.object) {
364            import std.algorithm.iteration : map;
365
366            oldsuites = map!"a.str"(root["Oldsuites"].array).array;
367        }
368
369        if ("AllowedCustomKeys" in root.object)
370            foreach (ref key; root["AllowedCustomKeys"].array)
371                allowedCustomKeys[key.str] = true;
372
373        // Enable features which are default-enabled
374        setFeature (GeneratorFeature.PROCESS_DESKTOP, true);
375        setFeature (GeneratorFeature.VALIDATE, true);
376        setFeature (GeneratorFeature.STORE_SCREENSHOTS, true);
377        setFeature (GeneratorFeature.OPTIPNG, true);
378        setFeature (GeneratorFeature.METADATA_TIMESTAMPS, true);
379        setFeature (GeneratorFeature.IMMUTABLE_SUITES, true);
380        setFeature (GeneratorFeature.PROCESS_FONTS, true);
381
382        // apply vendor feature settings
383        if ("Features" in root.object) {
384            auto featuresObj = root["Features"].object;
385            foreach (featureId; featuresObj.byKey ()) {
386                switch (featureId) {
387                    case "validateMetainfo":
388                        setFeature (GeneratorFeature.VALIDATE, featuresObj[featureId].type == JSON_TYPE.TRUE);
389                        break;
390                    case "processDesktop":
391                        setFeature (GeneratorFeature.PROCESS_DESKTOP, featuresObj[featureId].type == JSON_TYPE.TRUE);
392                        break;
393                    case "noDownloads":
394                            setFeature (GeneratorFeature.NO_DOWNLOADS, featuresObj[featureId].type == JSON_TYPE.TRUE);
395                            break;
396                    case "createScreenshotsStore":
397                            setFeature (GeneratorFeature.STORE_SCREENSHOTS, featuresObj[featureId].type == JSON_TYPE.TRUE);
398                            break;
399                    case "optimizePNGSize":
400                            setFeature (GeneratorFeature.OPTIPNG, featuresObj[featureId].type == JSON_TYPE.TRUE);
401                            break;
402                    case "metadataTimestamps":
403                            setFeature (GeneratorFeature.METADATA_TIMESTAMPS, featuresObj[featureId].type == JSON_TYPE.TRUE);
404                            break;
405                    case "immutableSuites":
406                            setFeature (GeneratorFeature.METADATA_TIMESTAMPS, featuresObj[featureId].type == JSON_TYPE.TRUE);
407                            break;
408                    case "processFonts":
409                            setFeature (GeneratorFeature.PROCESS_FONTS, featuresObj[featureId].type == JSON_TYPE.TRUE);
410                            break;
411                    default:
412                        break;
413                }
414            }
415        }
416
417        // check if we need to disable features because some prerequisites are not met
418        if (featureEnabled (GeneratorFeature.OPTIPNG)) {
419            if (!std.file.exists ("/usr/bin/optipng")) {
420                setFeature (GeneratorFeature.OPTIPNG, false);
421                logError ("Disabled feature `optimizePNGSize`: The `optipng` binary was not found.");
422            }
423        }
424
425        if (featureEnabled (GeneratorFeature.NO_DOWNLOADS)) {
426            // since disallowing network access might have quite a lot of sideeffects, we print
427            // a message to the logs to make debugging easier.
428            // in general, running with noDownloads is discouraged.
429            logWarning ("Configuration does not permit downloading files. Several features will not be available.");
430        }
431
432        if (!featureEnabled (GeneratorFeature.IMMUTABLE_SUITES)) {
433            // Immutable suites won't work if the feature is disabled - log this error
434            if (hasImmutableSuites)
435                logError ("Suites are defined as immutable, but the `immutableSuites` feature is disabled. Immutability will not work!");
436        }
437    }
438
439    bool isValid ()
440    {
441        return this.projectName != null;
442    }
443
444    /**
445     * Get unique temporary directory to use during one generator run.
446     */
447    string getTmpDir ()
448    {
449        if (tmpDir.empty) {
450            synchronized (this) {
451                string root;
452                if (cacheRootDir.empty)
453                    root = "/tmp/";
454                else
455                    root = cacheRootDir;
456
457                tmpDir = buildPath (root, "tmp", format ("asgen-%s", randomString (8)));
458            }
459        }
460
461        return tmpDir;
462    }
463}
Note: See TracBrowser for help on using the repository browser.