pax_global_header00006660000000000000000000000064126174731370014525gustar00rootroot0000000000000052 comment=44b237224c374426a82820c5e62dac06fd1b4a08 appstream-dep11-0.4.0/000077500000000000000000000000001261747313700144325ustar00rootroot00000000000000appstream-dep11-0.4.0/.gitignore000066400000000000000000000000361261747313700164210ustar00rootroot00000000000000dist/ build/ *.pyc *.egg-info appstream-dep11-0.4.0/LICENSE000066400000000000000000000167431261747313700154520ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. appstream-dep11-0.4.0/MAINTAINERS000066400000000000000000000000471261747313700161300ustar00rootroot00000000000000Matthias Klumpp E-mail: mak@debian.org appstream-dep11-0.4.0/NEWS000066400000000000000000000060151261747313700151330ustar00rootroot00000000000000Version 0.4.0 ~~~~~~~~~~~~~~ Released: 2015-11-07 Features: * Switch to LMDB instead of KyotoCabinet as default database backend * Make private source-checksum field known to the validator * Update README * Update the HTML startpage * Fix various quirks concerning packages moving metadata around * Just enumerate screenshots * Create very large thumbnails, for HiDPI displays * Include component-id in issue-page ID Bugfixes: * Make multiprocessing and cruft-removal work again * Simplify autocleanup, and prevent it from dropping all packages from the db * Prevent invalid components from overriding existing ones * We can't handle symlinks, so throw a better error in that case Version 0.3.0 ~~~~~~~~~~~~~~ Released: 2015-09-12 Features: * validator: Adapt to the current DEP-11 spec * Implement parser for the tag * Implement DEP-11 0.8 spec * Make generator use the new MediaBaseUrl DEP-11 field by default * Update validator to recognize Releases tag * Further refine icon-finding logic Bugfixes: * Ensure translated fields have a template set * Fix another issue where the 64x64 icon was not available * Make package filename the first parameter for any icon-storing method * Properly handle multiple components in one package * The to_yaml() functions also set ignore reasons - account for that * Don't set icon if storing it failed (even if we don't know the reason) Version 0.2.0 ~~~~~~~~~~~~~~ Released: 2015-08-10 Features: * Add very cheap backlink to pages * DataUrl is actually an AssetsUrl * Add small how-to to README * generator: Configure logging * Find Contents file in Ubuntu archive layout * Handle contents files in iso-8859-1 encoding * Explicitly write hints HTML in UTF-8 * Make IconFinder find icons in Oxygen theme * Add function to allow reprocessing of packages * Refactor issue reporting * Completely get rid of hardcoded severities * Add a few more hacks to find more icons and bundle them correctly * Special-case the Adwaita theme as well, when searching for icons * Make gui-app-without-icon tag more useful * Add found metadata to the HTML as well * Show architecture in metainfo HTML * html: Display some pretty statistics * Include icon in metadata overview * Show global validation result in HTML * When finding ID collisions, display which packages are involved * Make screenshot-read-error hint more useful * Mention optional dependency on Pygments Bugfixes: * Make html pages find their static content easier * Do not accidentally extract invalid icon sizes * Expire asset-cache when removing processed components as well * Remove epoch from package-ids * Get rid of binid * Explicitly set HTTP request timeout * Fix a couple of icon detection and storage bugs * html: Nobody needs to be informed about the update date twice * Do not excess-cleanup the whole cache * Fix dbus provides type element * html: Correctly fold same issues into one view Version 0.1.0 ~~~~~~~~~~~~~~ Released: 2015-07-10 Notes: * Initial release after splitting the code out of DAK appstream-dep11-0.4.0/README.md000066400000000000000000000054341261747313700157170ustar00rootroot00000000000000# Debian AppStream DEP-11 tools This Python module allows you to generate and validate DEP-11 metadata, which is Debians implementation of AppStream. The tools can be used standalone, or be integrated with other services. You can find out more about DEP-11 YAML at the [Debian Wiki](https://wiki.debian.org/DEP-11), and more about AppStream distro metadata at [Freedesktop](http://www.freedesktop.org/software/appstream/docs/chap-DistroData.html#sect-AppStream-ASXML). ## Dependencies In order to use AppStream-DEP11, the following components are needed: * Python 3 (ideally >> 3.4.3) * GIR for RSvg-2.0, * python-apt, * python-cairo, * python-gi, * Jinja2, * python-lmdb, * python-lxml, * python-pil, * Voluptuous, * PyYAML * Pygments (optional) To install all dependencies on Debian systems, use ```ShellSession sudo apt install gir1.2-rsvg-2.0 python3-apt python3-cairo python3-gi python3-jinja2 python3-lmdb \ python3-gi-cairo python3-lxml python3-pil python3-voluptuous python3-yaml python3-pygments ``` ## How to use ### Generating distro metadata To generate AppStream distribution metadata for your repository, create a local mirror of the repository first. Then create a new folder, and write a `dep11-config.yml` configuration file for the metadata generator. A minimal configuration file may look like this: ```YAML ArchiveRoot: /srv/archive.tanglu.org/tanglu/ MediaBaseUrl: http://metadata.tanglu.org/dep11/media HtmlBaseUrl: http://metadata.tanglu.org/dep11/hints_html/ Suites: chromodoris: components: - main - contrib architectures: - amd64 - i386 ``` Key | Comment ------------ | ------------- ArchiveRoot | A local URL to the mirror of your archive, containing the dists/ and pool/ directories MediaBaseUrl | The http or https URL which should be used in the generated metadata to fetch media like screenshots or icons HtmlBaseUrl | The http or https URL to the web location where the HTML hints will be published. (This setting is optional, but recommended) Suites | A list of suites which should be recognized by the generator. Each suite has the components and architectures which should be seached for metadata as children. After the config file has been written, you can generate the metadata as follows: ```Bash cd /srv/dep11/workspace # path where the dep11-config.yml file is located dep11-generator process . chromodoris # replace "chromodoris" with the name of the suite you want to analyze ``` The generator is assuming you have enough memory on your machine to cache stuff. Resulting metadata will be placed in `export/data/`, machine-readable issue-hints can be found in `export/hints/` and the processed screenshots are located in `export/media/`. ### Validating metadata Just run `dep11-validate .yml.gz` to check a file for spec-compliance. appstream-dep11-0.4.0/TODO000066400000000000000000000031021261747313700151160ustar00rootroot00000000000000= DEP-11 Data Extractor TODO List = === Known issues === * If an icon is symlinked, we just ignore it at time, since the symlink itself doesn't contain data. We should either support that case or drop a meaningful error message (Reference package: git-cola == 2.2.1-1) * The extractor can't get a list of files for certain .deb packages, see 'qtwebkit-opensource-src/libqt5webkit5-dbg_5.4.2+dfsg-3_amd64.deb' as an example. Might be an issue with python-apt, some multiprocessing insanity leading to this happening sometimes, or could even be a problem with the package itself. This issue happens only rarely. === Planned Features === * Extract localizstion status for AppStream components and add them as `Languages` field. * Extract more metadata from things which do not have AppStream upstream metadata yet. * Expand the HTML pages to include more and more useful information. === Whishlist / Random Ideas === * Maybe pre-filter for interesting packages based on Contents.gz to speed up the generator. * Could we maybe scan just one .deb package per architecture, and simply reuse the previously extracted metadata for all other architectures, given that checksums on the files relevent for the metadata match between archs? Would need to be stored on the database somewhere, and might grow into a pretty big storage of checksums, so do we want that? Also, would something like this save a sufficient amount of disk-space for screenshots/icons and improve the generator speed to make implementing this functionality worth the effort? appstream-dep11-0.4.0/data/000077500000000000000000000000001261747313700153435ustar00rootroot00000000000000appstream-dep11-0.4.0/data/dep11-hints.yml000066400000000000000000000141141261747313700201240ustar00rootroot00000000000000internal-unknown-tag: text: The generator emitted a tag '%(tag_name)s' which is unknown. This is a bug in the metadata generator, please file a bugreport. severity: warning not-an-application: text: "The .desktop file found in /usr/share/applications does not have a 'Type=Application' field." severity: error invisible-application: text: "The .desktop file in /usr/share/applications has the invisible flag set, the application is therefore ignored." severity: error desktop-file-read-error: text: "Unable to read data from .desktop file: %(msg)s" severity: error metainfo-parse-error: text: "Unable to parse AppStream upstream XML, the file is likely malformed. Error:
%(msg)s" severity: error ancient-metadata: text: The AppStream metadata should be updated to follow a more recent version of the specification.
Please consult the XML quickstart guide for more information. severity: info svgz-decompress-error: text: "Unable to decompress SVGZ icon '%(icon_fname)s'. Error: %(error)s" severity: error icon-format-unsupported: text: "Icon file '%(icon_fname)s' uses an unsupported image file format." severity: error icon-not-found: text: > The icon '%(icon_fname)s' was not found in the archive. This issue can have multiple reasons:
  • The icon is not present in the archive.
  • The icon is in a wrong directory.
  • The icon is not available in a suitable size (at least 64x64px)
To make the icon easier to find, place it in /usr/share/icons/hicolor/<size>/apps and ensure the Icon= value of the .desktop file is set correctly. severity: error icon-open-failed: text: > Unable to open icon file '%(icon_fname)s'. Error: %(error)s
This means the generator could not render the icon to its appropriate size, and the icon has therefore been ignored. severity: error deb-filelist-error: text: Could not determine file list for '%(pkg_fname)s'. This could be an error in the archive, dpkg, apt_pkg or the DEP-11 generator.
If you think this error is in the generator, please file a bug. severity: error deb-extract-error: text: "Could not extract file '%(fname)s' from package '%(pkg_fname)s'. Error: %(error)s" severity: error deb-empty-file: text: "File '%(fname)s' from package '%(pkg_fname)s' appeared to be empty." severity: error metainfo-no-id: text: Could not determine an id for this component. The AppStream upstream metadata likely lacks an <id/> tag.
The identifier tag is absolutely essential for AppStream metadata, and must never be missing. severity: error missing-desktop-file: text: Found an AppStream upstream XML file, but the associated .desktop file is missing. This often happens when the .desktop file is renamed, but the <id/> tag of the AppStream metainfo file is not adepted as well, or if the metainfo file is located in a different package than the .desktop file.
Please fix the packaging or work with upstream to resolve this issue. severity: error gui-app-without-icon: text: The GUI application (application which has a .desktop file for the XDG menu and Type=Application) with AppStream-ID '%(cid)s' has been found, but we could not find a matching icon. severity: error screenshot-download-error: text: > Error while downloading screenshot from '%(url)s' for component '%(cpt_id)s': %(error)s
This might be a temporary server issue. severity: warning screenshot-read-error: text: > Error while reading screenshot data for '%(url)s' of component '%(cpt_id)s': %(error)s
Maybe the server returned an invalid image (e.g. an error page), or the image data is broken. severity: warning metainfo-no-type: text: Component has no type defined. A component type (desktop, input-method, generic, ...) is essential. This issue is likely a mistyped component typename in the metainfo file, or an internal bug in the data extractor (componenty without Type= count as generic components automatically, so unless there is a typo somewhere, no component should ever have no type). severity: error metainfo-no-name: text: Component has no name specified. Ensure that the AppStream upstream metadata or the .desktop file (if there is any) specify a component name. severity: error metainfo-no-package: text: Component has no package defined. A package must be associated with a component. This is likely a bug in the generator. severity: error metainfo-no-summary: text: > Component does not contain a short summary. Ensure that the components metainfo file has a summary tag, or that its .desktop file has a Comment= field set. Especially applications by KDE often use the GenericName field for a short summary, instead of the Comment field (which is not its intended use case).
More information can be found in the Desktop Entry specification. severity: error metainfo-localized-field-without-template: text: The field '%(field_id)s' has no untranslated template entry ("C"). This is usually an error in the accompanying .desktop file, or the translation template in the AppStream XML is missing. severity: error metainfo-duplicate-id: text: The component-id '%(cid)s' already appeared in package '%(pkgname)s'. AppStream-IDs must be unique, please resolve which package will be providing this component by default.
This issue may happen temporarily when metadata is moved from one package to another. In that case, ignore this issue, it will vanish soon. severity: error appstream-dep11-0.4.0/data/templates/000077500000000000000000000000001261747313700173415ustar00rootroot00000000000000appstream-dep11-0.4.0/data/templates/default/000077500000000000000000000000001261747313700207655ustar00rootroot00000000000000appstream-dep11-0.4.0/data/templates/default/base.html000066400000000000000000000020361261747313700225660ustar00rootroot00000000000000 {% block head %} Debian DEP-11 Report - {% block title %}{% endblock %} {% block head_extra %}{% endblock %} {% endblock %}
{% block header_content %}{% endblock %}
{% block float_right %}{% endblock %}
{% block content %}{% endblock %}

Generated by the Debian DEP-11 metadata generator.

{% block page_details %}{% endblock %}
appstream-dep11-0.4.0/data/templates/default/issues_index.html000066400000000000000000000024121261747313700243540ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Hints summary for {{suite}}/{{section}}{% endblock %} {% block header_content %} ⇦ | Hints summary for {{suite}}/{{section}} {% endblock %} {% block content %}

Metadata processing hints found for {{suite}}/{{section}}

{% for maintainer, summary in package_summaries.items() %}

{{maintainer|e}}

    {% for pkgname, hints in summary.items() %}
  • {{pkgname}}  {% if hints.info_count %} Infos: {{hints.info_count}} {% endif %} {% if hints.warning_count %} Warnings: {{hints.warning_count}} {% endif %} {% if hints.error_count %} Errors: {{hints.error_count}} {% endif %}
  • {% endfor %}
{% endfor %} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/issues_page.html000066400000000000000000000036601261747313700241670ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}{{package_name}} in {{suite}}{% endblock %} {% block header_content %} ⇦ | {{package_name}} [{{suite}}] {% endblock %} {% block content %}

Hints for {{package_name}} in {{suite}}

{% for entry in entries %}

{{entry.identifier}} {% for arch in entry.archs %} ⚙ {{arch}} {% endfor %}

{% if entry.errors|length %}

Errors

    {% for tag in entry.errors %}
  • {{tag.tag_name}}
    {{tag.description}}
  • {% endfor %}
{% endif %} {% if entry.warnings|length %}

Warnings

    {% for tag in entry.warnings %}
  • {{tag.tag_name}}
    {{tag.description}}
  • {% endfor %}
{% endif %} {% if entry.infos|length %}

Hints

    {% for tag in entry.infos %}
  • {{tag.tag_name}}
    {{tag.description}}
  • {% endfor %}
{% endif %} {% endfor %}
{% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/metainfo_index.html000066400000000000000000000017431261747313700246510ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}DEP-11 components for {{suite}}/{{section}}{% endblock %} {% block header_content %} ⇦ | DEP-11 components summary for {{suite}}/{{section}} {% endblock %} {% block content %}

Metadata found for {{suite}}/{{section}}

{% for maintainer, summary in package_summaries.items() %}

{{maintainer|e}}

    {% for pkgname, info in summary.items() %}
  • {{pkgname}} 
      {% for cid in info.cids %}
    • {{cid}}
    • {% endfor %}
  • {% endfor %}
{% endfor %} {% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/metainfo_page.html000066400000000000000000000020401261747313700244450ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}{{package_name}} in {{suite}}{% endblock %} {% block head_extra %} {% endblock %} {% block header_content %} ⇦ | {{package_name}} [{{suite}}] {% endblock %} {% block content %}

DEP-11 metadata for {{package_name}} in {{suite}}

{% for cpt in cpts %}

{{cpt.cid}} {% for arch in cpt.archs %} ⚙ {{arch}} {% endfor %}

Icon
{{cpt.mdata}}
{% endfor %}
{% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/section_overview.html000066400000000000000000000033771261747313700252570ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}DEP-11 data for {{distro}}: {{suite}} suite{% endblock %} {% block header_content %} ⇦ | DEP-11 data for {{distro}}/{{suite}} {% endblock %} {% block content %}

Overview for {{section}}

Data

Issues - Issues found while extracting the data

Metainfo - Per-package view of the generated data

Health

Issue overview

{{valid_percentage}}% Valid
{{info_percentage}}% Infos
{{warning_percentage}}% Warnings
{{error_percentage}}% Errors
  • {{metainfo_count}} valid components
  • {{error_count}} errors
  • {{warning_count}} warnings
  • {{info_count}} infos/hints

Global data validation result

{{validate_result}}
{% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/sections_index.html000066400000000000000000000030751261747313700246760ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}DEP-11 data for {{distro}}: {{suite}} suite{% endblock %} {% block header_content %} ⇦ | DEP-11 data for {{distro}}/{{suite}} {% endblock %} {% block content %}

Select an archive section

Sections

{% for section in sections %}

{{section}}

{% endfor %}

Health of suite "{{suite}}"

{{valid_percentage}}% Valid
{{info_percentage}}% Infos
{{warning_percentage}}% Warnings
{{error_percentage}}% Errors
  • {{metainfo_count}} valid components
  • {{error_count}} errors
  • {{warning_count}} warnings
  • {{info_count}} infos/hints
{% endblock %} {% block float_right %} Last updated on: {{time}} {% endblock %} appstream-dep11-0.4.0/data/templates/default/static/000077500000000000000000000000001261747313700222545ustar00rootroot00000000000000appstream-dep11-0.4.0/data/templates/default/static/css/000077500000000000000000000000001261747313700230445ustar00rootroot00000000000000appstream-dep11-0.4.0/data/templates/default/static/css/highlight.css000066400000000000000000000064071261747313700255340ustar00rootroot00000000000000.hll { background-color: #ffffcc } .c { color: #999988; font-style: italic } /* Comment */ .err { color: #a61717; background-color: #e3d2d2 } /* Error */ .k { color: #000000; font-weight: bold } /* Keyword */ .o { color: #000000; font-weight: bold } /* Operator */ .cm { color: #999988; font-style: italic } /* Comment.Multiline */ .cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */ .c1 { color: #999988; font-style: italic } /* Comment.Single */ .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .ge { color: #000000; font-style: italic } /* Generic.Emph */ .gr { color: #aa0000 } /* Generic.Error */ .gh { color: #999999 } /* Generic.Heading */ .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .go { color: #888888 } /* Generic.Output */ .gp { color: #555555 } /* Generic.Prompt */ .gs { font-weight: bold } /* Generic.Strong */ .gu { color: #aaaaaa } /* Generic.Subheading */ .gt { color: #aa0000 } /* Generic.Traceback */ .kc { color: #000000; font-weight: bold } /* Keyword.Constant */ .kd { color: #000000; font-weight: bold } /* Keyword.Declaration */ .kn { color: #000000; font-weight: bold } /* Keyword.Namespace */ .kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */ .kr { color: #000000; font-weight: bold } /* Keyword.Reserved */ .kt { color: #445588; font-weight: bold } /* Keyword.Type */ .m { color: #009999 } /* Literal.Number */ .s { color: #d01040 } /* Literal.String */ .na { color: #008080 } /* Name.Attribute */ .nb { color: #0086B3 } /* Name.Builtin */ .nc { color: #445588; font-weight: bold } /* Name.Class */ .no { color: #008080 } /* Name.Constant */ .nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */ .ni { color: #800080 } /* Name.Entity */ .ne { color: #990000; font-weight: bold } /* Name.Exception */ .nf { color: #990000; font-weight: bold } /* Name.Function */ .nl { color: #990000; font-weight: bold } /* Name.Label */ .nn { color: #555555 } /* Name.Namespace */ .nt { color: #000080 } /* Name.Tag */ .nv { color: #008080 } /* Name.Variable */ .ow { color: #000000; font-weight: bold } /* Operator.Word */ .w { color: #bbbbbb } /* Text.Whitespace */ .mf { color: #009999 } /* Literal.Number.Float */ .mh { color: #009999 } /* Literal.Number.Hex */ .mi { color: #009999 } /* Literal.Number.Integer */ .mo { color: #009999 } /* Literal.Number.Oct */ .sb { color: #d01040 } /* Literal.String.Backtick */ .sc { color: #d01040 } /* Literal.String.Char */ .sd { color: #d01040 } /* Literal.String.Doc */ .s2 { color: #d01040 } /* Literal.String.Double */ .se { color: #d01040 } /* Literal.String.Escape */ .sh { color: #d01040 } /* Literal.String.Heredoc */ .si { color: #d01040 } /* Literal.String.Interpol */ .sx { color: #d01040 } /* Literal.String.Other */ .sr { color: #009926 } /* Literal.String.Regex */ .s1 { color: #d01040 } /* Literal.String.Single */ .ss { color: #990073 } /* Literal.String.Symbol */ .bp { color: #999999 } /* Name.Builtin.Pseudo */ .vc { color: #008080 } /* Name.Variable.Class */ .vg { color: #008080 } /* Name.Variable.Global */ .vi { color: #008080 } /* Name.Variable.Instance */ .il { color: #009999 } /* Literal.Number.Integer.Long */ .p-Indicator { color: #3c5d5d; font-weight: bold } appstream-dep11-0.4.0/data/templates/default/static/css/style.css000066400000000000000000000072441261747313700247250ustar00rootroot00000000000000html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust:100%; height:100%; } body { border-sizing: border-box; font-family: Cantarell,"Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 14px; line-height: 20px; color: #333333; margin: 0; height: 93%; } a { color: #337ab7; text-decoration: none; } a { background-color: transparent; } .headbar { border-radius: 4px; display: block; border: 1px solid transparent; margin-bottom: 20px; min-height: 50px; position: relative; background-color: #f8f8f8; border-color: #e7e7e7; border-width: 0 0 1px; z-index: 1000; border-radius: 0; margin-bottom: 14px; } .headbar-content { font-size: 18px; line-height: 20px; padding: 15px; float: left; } .headbar-content-right { font-size: 14px; line-height: 20px; padding: 15px; float: right; } .content { padding: 0em 1em 0em 1em; } .wrapper { width: 60%; } .wrapper hr { border-top: none; border-bottom: 1px solid #819eb7; margin-bottom: 1em; } hr { border-top: none; border-bottom: 1px solid #d70a53; margin-bottom: 1em; } footer { text-align: center; margin-top: 1em; } span.avoidwrap { display: inline-block; } .infobox { border-color: #eee; border-image: none; border-radius: 3px; border-style: solid; border-width: 1px 1px 1px 5px; margin: 20px 0; padding: 20px; } .infobox h2 { margin-bottom: 5px; margin-top: 0; } .infobox p:last-child{margin-bottom:0} .infobox-hint { border-left-color: #1b809e; } .infobox-hint h2 { color: #1b809e; } .infobox-error { border-left-color: #ce4844; } .infobox-error h2 { color: #ce4844; } .infobox-warning { border-left-color: #aa6708; } .infobox-warning h2 { color: #aa6708; } /* label styles copied from Bootstrap */ .label { border-radius: 0.25em; color: #fff; display: inline; font-size: 75%; font-weight: 700; line-height: 1; padding: 0.2em 0.6em 0.3em; text-align: center; vertical-align: baseline; white-space: nowrap; } .label-info { background-color: #5bc0de; } .label-warning { background-color: #f0ad4e; } .label-error { background-color: #d9534f; } .label-neutral { background-color: #777; } .overviewlisting a { color: #000000; text-decoration: none; } .overviewlisting li { padding: 2px 4px 2px; } code { background-color: #f9f2f4; border-radius: 4px; color: #c7254e; font-size: 90%; padding: 2px 4px; } .well{ min-height:20px; padding:19px; margin-bottom:20px; background-color:#f5f5f5; border:1px solid #e3e3e3; border-radius:4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05); box-shadow:inset 0 1px 1px rgba(0,0,0,.05) } .progress { background-color: #f5f5f5; border-radius: 4px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset; height: 20px; margin-bottom: 20px; overflow: hidden; } .progress-bar { background-color: #337ab7; box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15) inset; color: #fff; float: left; font-size: 12px; height: 100%; line-height: 20px; text-align: center; transition: width 0.6s ease 0s; width: 0; } .progress-bar-blue { background-color: #5bc0de; } .progress-bar-green { background-color: #5cb85c; } .progress-bar-yellow { background-color: #f0ad4e; } .progress-bar-red { background-color: #d9534f; } .sr-only { border: 0 none; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } appstream-dep11-0.4.0/data/templates/default/static/img/000077500000000000000000000000001261747313700230305ustar00rootroot00000000000000appstream-dep11-0.4.0/data/templates/default/static/img/cpt-nogui.png000066400000000000000000000033561261747313700254520ustar00rootroot00000000000000PNG  IHDR@C,pbKGD pHYs P%tIMEu1 {IDATxݜklTE[P*"P J"TDQ H0H@D hHP#SVbQ,((QPTKAdiw{ {wۓtw9gΝ9-DK]hS8Ni+R*X^ws@+۲JN}F&L6?DHC*^ =ƀ6]c)K5yj`Xo:΄b>/V53>+6k(u< 4pVvX 5(| FK&^ux %zL7OtNh n-9Ly5q ٿ6m0= JSr߶e B a[~`0-OK_HqBN$dR 5bJgn~S:OK0(Ad[A!@[_*-cJcJ-~)$_-ə%eTaR[өs~uJP(%գLš5}9nd>PDp9LGm"29lƂq'G6˱q-b#S,U6m`1!ᎹPWw_`>pL{+]@4r_WIp)$Id\'lk||t\6+P(~r/%842/[ $:/?eU`R!k $ӽ"/PH*}噲J'[)͍)tfuXYL-w;(O+IxJVý$10y676ؠ|uO\c$*Ŏ8#]l ^6KGC2"w?p(W',M)vBi 0W謍=x#K6m/}&)YCI[il[-\*ʹheYwku7-]rL ~5? tC66-s8*%hDL&GpGtTS$zR: ,ّnm lHe7I(ZLyEhg-B5Xo1x.SM/! hOGe&ZDf9|&?%Lr <&uOE@SW$N%*{|Й,,:b2y=W2iAI7X&T |0-S#]>~ (VTAeb闠wD(P@bÀᑒrۈl 8]`I(HN]B˹4gRR64a`RhXIZY43yG[k OyI=o^9%P pBZ20O|?H zA lńtdo%::DL7 & ~!ODZ"-sU)_VG29}5Be,2ixP1"a)Q*N=a*X^ ȼA %m?rIENDB`appstream-dep11-0.4.0/data/templates/default/static/img/no-image.png000066400000000000000000000017521261747313700252370ustar00rootroot00000000000000PNG  IHDR@@iqbKGD pHYs P%tIME6jǠOOϠOMJ(rx)E,-ڔ+=IxI! ^2)!Ps `9s"@"wY:JO40|(DKBFǦ^3I=UmBl|F*E^3s/o?FW: ߤ׈pE.f ]_p~U!k7U/c(#'&f5 ɱo"ޖ0 P3A20wE 0pBlS+"P G`9pM/٬60.P |EInY pV3!7ׇͰm³;tMC|@X7%9]`Җ׏'l$Qn`2M+ǣU 4sLTV(cnCi%+" Jma? |S>$WI2KOXk8(}" "$E EȩPǿ OYB-3W '@Jc1NğYCjUi,hjO]I$^A~C<Aq4ha_#G|< QOU)#r {X:a-|m+!qU8i;[-x-xl-U)b)hfY͢_)"!dƒ}IENDB`appstream-dep11-0.4.0/data/templates/default/suites_index.html000066400000000000000000000046071261747313700243650ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}DEP-11 data for {{distro}}: Suites overview{% endblock %} {% block header_content %} DEP-11 data hints for {{distro}} {% endblock %} {% block content %}

Welcome!

Welcome to the DEP-11 generator HTML pages!

These pages exist to provide a user-friendly view on the issues discovered by the DEP-11 generator while extracting metadata from packages in the {{distro}} archive. They can also be used to take a look at the raw metadata, to spot possible problems with the data itself or the generation process.

Select a suite

{% for suite in suites %}

{{suite}}

{% endfor %}

What is DEP-11 and AppStream?

AppStream is a cross-distro XML format to provide metadata for software components and to assign unique identifiers to software.
In Debian, we parse all XML provided by upstream projects as well as other metadata (.desktop-files, ...), and compile a single YAML metadata file from it, which is then shipped to users via APT.

While the official AppStream specification is based on XML, Debian uses a YAML version of the format for easier use in existing scripts and for better archive integration. This format is called DEP-11, and initially had a much wider scope in enhancing archive metadata than AppStream had. Today AppStream covers that as well.

The generated metadata can for example be used by software centers like GNOME-Software or KDE Discover to display a user-friendly application-centric way on the package archive.

More information

See AppStream @ wiki.d.o for information about AppStream integration and usage in Debian.
The offical AppStream specification can be found at freedesktop.org, a description of the DEP-11 YAML format is hosted there as well.

You can find the source-code of the DEP-11 generator here.

{% endblock %} appstream-dep11-0.4.0/dep11/000077500000000000000000000000001261747313700153445ustar00rootroot00000000000000appstream-dep11-0.4.0/dep11/__init__.py000066400000000000000000000003071261747313700174550ustar00rootroot00000000000000 from dep11.utils import build_cpt_global_id from dep11.extractor import MetadataExtractor from dep11.component import DEP11Component, IconSize, DEP11YamlDumper from dep11.datacache import DataCache appstream-dep11-0.4.0/dep11/component.py000066400000000000000000000346061261747313700177310ustar00rootroot00000000000000#!/usr/bin/env python """ Contains the definition of a DEP-11 component. """ # Copyright (c) 2014 Abhishek Bhattacharjee # Copyright (c) 2014 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import yaml import datetime from dep11.utils import str_enc_dec, build_cpt_global_id from dep11.hints import HintSeverity, hint_tag_is_error import logging as log import hashlib ########################################################################### DEP11_VERSION = "0.8" time_str = str(datetime.date.today()) dep11_header_template = { "File": "DEP-11", "Version": DEP11_VERSION } ########################################################################### class DEP11YamlDumper(yaml.Dumper): ''' Custom YAML dumper, to ensure resulting YAML file can be read by all parsers (even the Java one) ''' def increase_indent(self, flow=False, indentless=False): return super(DEP11YamlDumper, self).increase_indent(flow, False) def get_dep11_header(suite_name, component_name, base_url): head_dict = dep11_header_template head_dict['Origin'] = "%s-%s" % (suite_name, component_name) head_dict['MediaBaseUrl'] = base_url return yaml.dump(head_dict, Dumper=DEP11YamlDumper, default_flow_style=False, explicit_start=True, explicit_end=False, width=200, indent=2) def dict_to_dep11_yaml(d): return yaml.dump(d, Dumper=DEP11YamlDumper, default_flow_style=False, explicit_start=True, explicit_end=False, width=100, indent=2, allow_unicode=True) class IconSize: ''' A simple type representing an icon size ''' size = int() def __init__(self, size): if isinstance(size, str): self.set_from_string(size) else: self.size = size def __str__(self): return "%ix%i" % (self.size, self.size) def __int__(self): return self.size def set_from_string(self, s): wd, ht = s.split('x') if int(wd) != int(ht): log.warning("Processing asymetric icon.") self.size = int(wd) def __eq__(self, other): if type(other) is str: return str(self) == other if type(other) is IconSize: return self.size == other.size return self.size == other def __lt__(self, other): if type(other) is IconSize: return self.size < other.size return self.size < other def __gt__(self, other): if type(other) is IconSize: return self.size > other.size return self.size > other def __hash__(self): return self.size class ProvidedItemType: ''' Types supported as publicly provided interfaces. Used as keys in the 'Provides' field ''' BINARY = 'binaries' LIBRARY = 'libraries' MIMETYPE = 'mimetypes' DBUS = 'dbus' PYTHON_2 = 'python2' PYTHON_3 = 'python3' FIRMWARE = 'firmware' CODEC = 'codecs' class DEP11Component: ''' Used to store the properties of component data. Used by MetadataExtractor ''' def __init__(self, suitename, component, pkg, pkid=None): ''' Used to set the properties to None. ''' self._suitename = suitename self._component = component self._pkg = pkg self._pkid = pkid # properties self._hints = list() self._ignore = False self._srcdata_checksum = None self._global_id = None self._id = None self._type = None self._name = dict() self._categories = None self._icon = None self._summary = dict() self._description = None self._screenshots = None self._keywords = None self._archs = None self._provides = dict() self._url = None self._project_license = None self._project_group = None self._developer_name = dict() self._extends = list() self._compulsory_for_desktops = list() self._releases = list() self._languages = list() def add_hint(self, tag, params=dict()): if hint_tag_is_error(tag): self._ignore = True self._hints.append({'tag': tag, 'params': params}) def has_ignore_reason(self): return self._ignore def get_hints_dict(self): if not self._hints: return None hdict = dict() # add some helpful data if self.cid: hdict['ID'] = self.cid if self.kind: hdict['Type'] = self.kind if self._pkg: hdict['Package'] = self._pkg if self._pkid: hdict['PackageID'] = self._pkid if self.has_ignore_reason(): hdict['Ignored'] = True hdict['Hints'] = self._hints return hdict def get_hints_yaml(self): if not self._hints: return None return dict_to_dep11_yaml(self.get_hints_dict()) def set_srcdata_checksum_from_data(self, data): b = bytes(data, 'utf-8') md5sum = hashlib.md5(b).hexdigest() self._srcdata_checksum = md5sum @property def srcdata_checksum(self): return self._srcdata_checksum @srcdata_checksum.setter def srcdata_checksum(self, val): self._srcdata_checksum = val self._global_id = None @property def global_id(self): """ The global-id is used as a global, unique identifier for this component. Its primary usecase is to identify a media directory on the filesystem which is associated with this component. """ if self._global_id: return self._global_id self._global_id = build_cpt_global_id(self._id, self.srcdata_checksum) return self._global_id @property def cid(self): return self._id @cid.setter def cid(self, val): self._id = val self._global_id = None @property def kind(self): return self._type @kind.setter def kind(self, val): self._type = val @property def pkgname(self): return self._pkg @property def pkid(self): return self._pkid @property def name(self): return self._name @name.setter def name(self, val): self._name = val @property def categories(self): return self._categories @categories.setter def categories(self, val): self._categories = val @property def icon(self): return self._icon @icon.setter def icon(self, val): self._icon = val @property def summary(self): return self._summary @summary.setter def summary(self, val): self._summary = val @property def description(self): return self._description @description.setter def description(self, val): self._description = val @property def screenshots(self): return self._screenshots @screenshots.setter def screenshots(self, val): self._screenshots = val @property def keywords(self): return self._keywords @keywords.setter def keywords(self, val): self._keywords = val @property def archs(self): return self._archs @archs.setter def archs(self, val): self._archs = val @property def provides(self): return self._provides @provides.setter def provides(self, val): self._provides = val @property def url(self): return self._url @url.setter def url(self, val): self._url = val @property def compulsory_for_desktops(self): return self._compulsory_for_desktops @compulsory_for_desktops.setter def compulsory_for_desktops(self, val): self._compulsory_for_desktops = val @property def project_license(self): return self._project_license @project_license.setter def project_license(self, val): self._project_license = val @property def project_group(self): return self._project_group @project_group.setter def project_group(self, val): self._project_group = val @property def developer_name(self): return self._developer_name @developer_name.setter def developer_name(self, val): self._developer_name = val @property def extends(self): return self._extends @extends.setter def extends(self, val): self._extends = val @property def releases(self): return self._releases @releases.setter def releases(self, val): self._releases = val def add_provided_item(self, kind, value): if kind not in self.provides.keys(): self.provides[kind] = list() self.provides[kind].append(value) def _is_quoted(self, s): return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'")) def _cleanup(self, d): ''' Remove cruft locale, duplicates and extra encoding information ''' if not d: return d if d.get('x-test'): d.pop('x-test') if d.get('xx'): d.pop('xx') unlocalized = d.get('C') if unlocalized: to_remove = [] for k in list(d.keys()): val = d[k] # don't duplicate strings if val == unlocalized and k != 'C': d.pop(k) continue if self._is_quoted(val): d[k] = val.strip("\"'") # should not specify encoding if k.endswith('.UTF-8'): locale = k.strip('.UTF-8') d.pop(k) d[locale] = val continue return d def _check_translated(self): ''' Ensure each localized field has a translation template ('C') set. Some broken .desktop files do not properly set a template, and we don't want to return broken DEP-11 YAML because of broken upstream metadata. ''' def check_for_template(field, id_str): if not field: return if not field.get('C'): self.add_hint("metainfo-localized-field-without-template", {'field_id': id_str}) check_for_template(self.name, 'Name') check_for_template(self.summary, 'Summary') check_for_template(self.description, 'Description') check_for_template(self.developer_name, 'DeveloperName') if self.screenshots: for i, shot in enumerate(self.screenshots): caption = shot.get('caption') if caption: check_for_template(self.developer_name, "Screenshots/%i/caption" % (i)) def finalize_to_dict(self): ''' Do sanity checks and finalization work, then serialize the component to a Python dict. ''' # perform some cleanup work self.name = self._cleanup(self.name) self.summary = self._cleanup(self.summary) self.description = self._cleanup(self.description) self.developer_name = self._cleanup(self.developer_name) if self.screenshots: for shot in self.screenshots: caption = shot.get('caption') if caption: shot['caption'] = self._cleanup(caption) # validate the basics (if we don't ignore this already) if not self.has_ignore_reason(): if not self.cid: self.add_hint("metainfo-no-id") if not self.kind: self.add_hint("metainfo-no-type") if not self.name: self.add_hint("metainfo-no-name") if not self._pkg: self.add_hint("metainfo-no-package") if not self.summary: self.add_hint("metainfo-no-summary") # ensure translated elements have templates self._check_translated() d = dict() d['Package'] = str(self._pkg) if self.cid: d['ID'] = self.cid if self.kind: d['Type'] = self.kind # having the source-data checksum in the final output is useful for # later debugging. It also doesn't use much space. if self.srcdata_checksum: d['X-Source-Checksum'] = self.srcdata_checksum # check if we need to print ignore information, instead # of exporting the software component if self.has_ignore_reason(): d['Ignored'] = True return d if self.name: d['Name'] = self.name if self.summary: d['Summary'] = self.summary if self.categories: d['Categories'] = self.categories if self.description: d['Description'] = self.description if self.keywords: d['Keywords'] = self.keywords if self.screenshots: d['Screenshots'] = self.screenshots if self.archs: d['Architectures'] = self.archs if self.icon: d['Icon'] = {'cached': self.icon} if self.url: d['Url'] = self.url if self.provides: d['Provides'] = self.provides if self.project_license: d['ProjectLicense'] = self.project_license if self.project_group: d['ProjectGroup'] = self.project_group if self.developer_name: d['DeveloperName'] = self.developer_name if self.extends: d['Extends'] = self.extends if self.compulsory_for_desktops: d['CompulsoryForDesktops'] = self.compulsory_for_desktops if self.releases: d['Releases'] = self.releases return d def to_yaml_doc(self): return dict_to_dep11_yaml(self.finalize_to_dict()) appstream-dep11-0.4.0/dep11/datacache.py000066400000000000000000000203621261747313700176160ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import glob import shutil import logging as log import lmdb from math import pow def tobytes(s): if isinstance(s, bytes): return s return bytes(s, 'utf-8') class DataCache: """ A LMDB based cache for the DEP-11 generator """ def __init__(self, media_dir): self._pkgdb = None self._hintsdb = None self._datadb = None self._dbenv = None self.cache_dir = None self._opened = False self.media_dir = media_dir # set a huge map size to be futureproof. # This means we're cruel to non-64bit users, but this # software is supposed to be run on 64bit machines anyway. self._map_size = pow(1024, 4) def open(self, cachedir): self._dbenv = lmdb.open(cachedir, max_dbs=3, map_size=self._map_size) self._pkgdb = self._dbenv.open_db(b'packages') self._hintsdb = self._dbenv.open_db(b'hints') self._datadb = self._dbenv.open_db(b'metadata') self._opened = True self.cache_dir = cachedir return True def close(self): if not self._opened: return self._dbenv.close() self._pkgdb = None self._hintsdb = None self._datadb = None self._dbenv = None self._opened = False def reopen(self): self.close() self.open(self.cache_dir) def metadata_exists(self, global_id): gid = tobytes(global_id) with self._dbenv.begin(db=self._datadb) as txn: return txn.get(gid) != None def get_metadata(self, global_id): gid = tobytes(global_id) with self._dbenv.begin(db=self._datadb) as dtxn: d = dtxn.get(tobytes(gid)) if not d: return None return str(d, 'utf-8') def set_metadata(self, global_id, yaml_data): gid = tobytes(global_id) with self._dbenv.begin(db=self._datadb, write=True) as txn: txn.put(gid, tobytes(yaml_data)) def set_package_ignore(self, pkgid): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._pkgdb, write=True) as txn: txn.put(pkgid, b'ignore') def get_cpt_gids_for_pkg(self, pkgid): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._pkgdb) as txn: cs_str = txn.get(pkgid) if not cs_str: return None cs_str = str(cs_str, 'utf-8') if cs_str == 'ignore' or cs_str == 'seen': return None gids = cs_str.split("\n") return gids def get_metadata_for_pkg(self, pkgid): gids = self.get_cpt_gids_for_pkg(pkgid) if not gids: return None data = "" for gid in gids: d = self.get_metadata(gid) if d: data += d return data def set_components(self, pkgid, cpts): # if the package has no components, # mark it as always-ignore if len(cpts) == 0: self.set_package_ignore(pkgid) return pkgid = tobytes(pkgid) gids = list() hints_str = "" for cpt in cpts: # check for ignore-reasons first, to avoid a database query if not cpt.has_ignore_reason(): if self.metadata_exists(cpt.global_id): gids.append(cpt.global_id) else: # get the metadata in YAML format md_yaml = cpt.to_yaml_doc() # we need to check for ignore reasons again, since generating # the YAML doc may have raised more errors if not cpt.has_ignore_reason(): self.set_metadata(cpt.global_id, md_yaml) gids.append(cpt.global_id) hints_yml = cpt.get_hints_yaml() if hints_yml: hints_str += hints_yml self.set_hints(pkgid, hints_str) if gids: with self._dbenv.begin(db=self._pkgdb, write=True) as txn: txn.put(pkgid, bytes("\n".join(gids), 'utf-8')) elif hints_str: # we need to set some value for this package, to show that we've seen it with self._dbenv.begin(db=self._pkgdb, write=True) as txn: txn.put(pkgid, b'seen') def get_hints(self, pkgid): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._hintsdb) as txn: hints = txn.get(pkgid) if hints: hints = str(hints, 'utf-8') return hints def set_hints(self, pkgid, hints_yml): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._hintsdb, write=True) as txn: txn.put(pkgid, tobytes(hints_yml)) def _cleanup_empty_dirs(self, d): parent = os.path.abspath(os.path.join(d, os.pardir)) if not os.path.isdir(parent): return if not os.listdir(parent): os.rmdir(parent) parent = os.path.abspath(os.path.join(parent, os.pardir)) if not os.path.isdir(parent): return if not os.listdir(parent): os.rmdir(parent) def remove_package(self, pkgid): log.debug("Dropping package: %s" % (pkgid)) pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._pkgdb, write=True) as pktxn: pktxn.delete(pkgid) with self._dbenv.begin(db=self._hintsdb, write=True) as htxn: htxn.delete(pkgid) def is_ignored(self, pkgid): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._pkgdb) as txn: return txn.get(pkgid) == b'ignore' def package_exists(self, pkgid): pkgid = tobytes(pkgid) with self._dbenv.begin(db=self._pkgdb) as txn: return txn.get(pkgid) != None def get_packages_not_in_set(self, pkgset): res = set() if not pkgset: pkgset = set() with self._dbenv.begin(db=self._pkgdb) as txn: cursor = txn.cursor() for key, value in cursor: if not str(key, 'utf-8') in pkgset: res.add(key) return res def remove_orphaned_components(self): gid_pkg = dict() with self._dbenv.begin(db=self._pkgdb) as txn: cursor = txn.cursor() for key, value in cursor: if not value or value == b'ignore' or value == b'seen': continue value = str(value, 'utf-8') gids = value.split("\n") for gid in gids: if not gid_pkg.get(gid): gid_pkg[gid] = list() gid_pkg[gid].append(key) # remove the media and component data, if component is orphaned with self._dbenv.begin(db=self._datadb) as dtxn: cursor = dtxn.cursor() for gid, yaml in cursor: gid = str(gid, 'utf-8') # Check if we have a package which is still referencing this component pkgs = gid_pkg.get(gid) if pkgs: continue # drop cached media dirs = glob.glob(os.path.join(self.media_dir, "*", gid)) if dirs: shutil.rmtree(dirs[0]) log.info("Expired media: %s" % (gid)) # remove possibly empty directories self._cleanup_empty_dirs(dirs[0]) # drop component from db with self._dbenv.begin(db=self._datadb, write=True) as dtxn: dtxn.delete(tobytes(gid)) appstream-dep11-0.4.0/dep11/extractor.py000066400000000000000000000633711261747313700177430ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2014 Abhishek Bhattacharjee # Copyright (c) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import fnmatch import urllib.request import ssl import yaml from apt_inst import DebFile from io import BytesIO import zlib import cairo from gi.repository import Rsvg from PIL import Image import logging as log from dep11.component import DEP11Component, IconSize from dep11.parsers import read_desktop_data, read_appstream_upstream_xml from dep11.iconfinder import AbstractIconFinder from dep11.datacache import DataCache xdg_icon_sizes = [IconSize(64), IconSize(72), IconSize(96), IconSize(128), IconSize(256), IconSize(512)] class MetadataExtractor: ''' Takes a deb file and extracts component metadata from it. ''' def __init__(self, suite_name, component, icon_sizes, dcache, icon_finder=None): ''' Initialize the object with List of files. ''' self._suite_name = suite_name self._archive_component = component self._export_dir = dcache.media_dir self._dcache = dcache self.write_to_cache = True self._icon_ext_allowed = ('.png', '.svg', '.xcf', '.gif', '.svgz', '.jpg') if icon_finder: self._icon_finder = icon_finder self._icon_finder.set_allowed_icon_extensions(self._icon_ext_allowed) else: self._icon_finder = AbstractIconFinder(self._suite_name, self._archive_component) # list of large sizes to scale down, in order to find more icons self._large_icon_sizes = xdg_icon_sizes[:] # list of icon sizes we want self._icon_sizes = list() for strsize in icon_sizes: self._icon_sizes.append(IconSize(strsize)) # remove smaller icons - we don't want to scale up icons later while (len(self._large_icon_sizes) > 0) and (int(self._icon_sizes[0]) >= int(self._large_icon_sizes[0])): del self._large_icon_sizes[0] @property def icon_finder(self): return self._icon_finder @icon_finder.setter def icon_finder(self, val): self._icon_finder = val def reopen_cache(self): self._dcache.reopen() def get_path_for_cpt(self, cpt, basepath, subdir): gid = cpt.global_id if not gid: return None if len(cpt.cid) < 1: return None path = os.path.join(basepath, gid, subdir) return path def _get_deb_filelist(self, deb): ''' Returns a list of all files in a deb package ''' files = list() if not deb: return files try: deb.data.go(lambda item, data: files.append(item.name)) except SystemError as e: raise e return files def _scale_screenshot(self, imgsrc, cpt_export_path, cpt_scr_url): ''' scale images in three sets of two-dimensions (752x423 624x351 and 112x63) ''' thumbnails = list() name = os.path.basename(imgsrc) sizes = ['1248x702', '752x423', '624x351', '112x63'] for size in sizes: wd, ht = size.split('x') img = Image.open(imgsrc) newimg = img.resize((int(wd), int(ht)), Image.ANTIALIAS) newpath = os.path.join(cpt_export_path, size) if not os.path.exists(newpath): os.makedirs(newpath) newimg.save(os.path.join(newpath, name)) url = "%s/%s/%s" % (cpt_scr_url, size, name) thumbnails.append({'url': url, 'height': int(ht), 'width': int(wd)}) return thumbnails def _fetch_screenshots(self, cpt, cpt_export_path, cpt_public_url=""): ''' Fetches screenshots from the given url and stores it in png format. ''' if not cpt.screenshots: # don't ignore metadata if no screenshots are present return True success = True shots = list() cnt = 1 for shot in cpt.screenshots: # cache some locations which we need later origin_url = shot['source-image']['url'] if not origin_url: # url empty? skip this screenshot continue path = self.get_path_for_cpt(cpt, cpt_export_path, "screenshots") base_url = self.get_path_for_cpt(cpt, cpt_public_url, "screenshots") imgsrc = os.path.join(path, "source", "scr-%s.png" % (str(cnt))) # The Debian services use a custom setup for SSL verification, not trusting global CAs and # only Debian itself. If we are running on such a setup, ensure we load the global CA certs # in order to establish HTTPS connections to foreign services. # For more information, see https://wiki.debian.org/ServicesSSL context = None ca_path = '/etc/ssl/ca-global' if os.path.isdir(ca_path): ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, capath=ca_path) else: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) try: # FIXME: The context parameter is only supported since Python 3.4.3, which is not # yet widely available, so we can't use it here... #! image = urllib.request.urlopen(origin_url, context=ssl_context).read() image_req = urllib.request.urlopen(origin_url, timeout=30) if image_req.getcode() != 200: msg = "HTTP status code was %i." % (image_req.getcode()) cpt.add_hint("screenshot-download-error", {'url': origin_url, 'cpt_id': cpt.cid, 'error': msg}) success = False continue if not os.path.exists(os.path.dirname(imgsrc)): os.makedirs(os.path.dirname(imgsrc)) f = open(imgsrc, 'wb') f.write(image_req.read()) f.close() except Exception as e: cpt.add_hint("screenshot-download-error", {'url': origin_url, 'cpt_id': cpt.cid, 'error': str(e)}) success = False continue try: img = Image.open(imgsrc) wd, ht = img.size shot['source-image']['width'] = wd shot['source-image']['height'] = ht shot['source-image']['url'] = os.path.join(base_url, "source", "scr-%s.png" % (str(cnt))) img.close() except Exception as e: error_msg = str(e) # filter out the absolute path: we shouldn't add it if error_msg: error_msg = error_msg.replace(os.path.dirname(imgsrc), "") cpt.add_hint("screenshot-read-error", {'url': origin_url, 'cpt_id': cpt.cid, 'error': error_msg}) success = False continue # scale_screenshots will return a list of # dicts with {height,width,url} shot['thumbnails'] = self._scale_screenshot(imgsrc, path, base_url) shots.append(shot) cnt = cnt + 1 cpt.screenshots = shots return success def _icon_allowed(self, icon): if icon.endswith(self._icon_ext_allowed): return True return False def _render_svg_to_png(self, data, store_path, width, height): ''' Uses cairosvg to render svg data to png data. ''' img = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) ctx = cairo.Context(img) handle = Rsvg.Handle() svg = handle.new_from_data(data) wscale = float(width)/float(svg.props.width) hscale = float(height)/float(svg.props.height) ctx.scale(wscale, hscale); svg.render_cairo(ctx) img.write_to_png(store_path) def _store_icon(self, deb_fname, cpt, cpt_export_path, icon_path, size): ''' Extracts the icon from the deb package and stores it in the cache. Ensures the stored icon always has the size given in "size", and renders vectorgraphics if necessary. ''' svgicon = False if not self._icon_allowed(icon_path): cpt.add_hint("icon-format-unsupported", {'icon_fname': os.path.basename(icon_path)}) return False if not os.path.exists(deb_fname): return False path = self.get_path_for_cpt(cpt, cpt_export_path, "icons/%s" % (str(size))) icon_name = "%s_%s" % (cpt.pkgname, os.path.basename(icon_path)) icon_name_orig = icon_name icon_name = icon_name.replace(".svgz", ".png") icon_name = icon_name.replace(".svg", ".png") icon_store_location = "{0}/{1}".format(path, icon_name) if os.path.exists(icon_store_location): # we already extracted that icon, skip the extraction step # change scalable vector graphics to their .png extension cpt.icon = icon_name return True # filepath is checked because icon can reside in another binary # eg amarok's icon is in amarok-data icon_data = None try: icon_data = DebFile(deb_fname).data.extractdata(icon_path) except Exception as e: cpt.add_hint("deb-extract-error", {'fname': icon_name, 'pkg_fname': deb_fname, 'error': str(e)}) return False if not icon_data: cpt.add_hint("deb-extract-error", {'fname': icon_name, 'pkg_fname': deb_fname, 'error': "Icon data was empty. The icon might be a symbolic link, please do not symlink icons " "(instead place the icons in their appropriate directories in /usr/share/icons/hicolor/)."}) return False cpt.icon = icon_name if icon_name_orig.endswith(".svg"): svgicon = True elif icon_name_orig.endswith(".svgz"): svgicon = True try: icon_data = zlib.decompress(bytes(icon_data), 15+32) except Exception as e: cpt.add_hint("svgz-decompress-error", {'icon_fname': icon_name, 'error': str(e)}) return False if not os.path.exists(path): os.makedirs(path) if svgicon: # render the SVG to a bitmap self._render_svg_to_png(icon_data, icon_store_location, int(size), int(size)) return True else: # we don't trust upstream to have the right icon size present, and therefore # always adjust the icon to the right size stream = BytesIO(icon_data) stream.seek(0) img = None try: img = Image.open(stream) except Exception as e: cpt.add_hint("icon-open-failed", {'icon_fname': icon_name, 'error': str(e)}) return False newimg = img.resize((int(size), int(size)), Image.ANTIALIAS) newimg.save(icon_store_location) return True return False def _match_icon_on_filelist(self, cpt, filelist, icon_name, size): if size == "scalable": size_str = "scalable" else: size_str = str(size) icon_path = "usr/share/icons/hicolor/%s/apps/%s" % (size_str, icon_name) filtered = fnmatch.filter(filelist, icon_path) if not filtered: return None return filtered[0] def _match_and_store_icon(self, pkg_fname, cpt, cpt_export_path, filelist, icon_name, size): success = False matched_icon = self._match_icon_on_filelist(cpt, filelist, icon_name, size) if not matched_icon: return False if not size in self._icon_sizes: # scale icons to allowed sizes for asize in self._icon_sizes: success = self._store_icon(pkg_fname, cpt, cpt_export_path, matched_icon, asize) or success else: success = self._store_icon(pkg_fname, cpt, cpt_export_path, matched_icon, size) return success def _fetch_icon(self, cpt, cpt_export_path, pkg_fname, filelist): ''' Searches for icon if absolute path to an icon is not given. Component with invalid icons are ignored ''' if not cpt.icon: # if we don't know an icon-name or path, just return without error return True icon_str = cpt.icon cpt.icon = None all_icon_sizes = self._icon_sizes[:] all_icon_sizes.extend(self._large_icon_sizes) success = False if icon_str.startswith("/"): if icon_str[1:] in filelist: return self._store_icon(pkg_fname, cpt, cpt_export_path, icon_str[1:], IconSize(64)) else: ret = False icon_str = os.path.basename (icon_str) # check if there is some kind of file-extension. # if there is none, the referenced icon is likely a stock icon, and we assume .png if "." in icon_str: icon_name_ext = icon_str else: icon_name_ext = icon_str + ".png" found_sizes = list() for size in self._icon_sizes: ret = self._match_and_store_icon(pkg_fname, cpt, cpt_export_path, filelist, icon_name_ext, size) if ret: found_sizes.append(size) success = ret or success # try if we can add missing icon sizes by scaling down things # this also ensures that we also have an 64x64 sized icon if set(found_sizes) != set(self._icon_sizes): for size in self._icon_sizes: if size in found_sizes: continue for asize in all_icon_sizes: if asize < size: continue icon_fname = self._match_icon_on_filelist(cpt, filelist, icon_name_ext, asize) if not icon_fname: continue ret = self._store_icon(pkg_fname, cpt, cpt_export_path, icon_fname, size) if ret: found_sizes.append(size) success = ret or success break # a 64x64 icon is required, so double-check if we have one if success and not IconSize(64) in found_sizes: success = False if not success: # we cheat and test for larger icons as well, which can be scaled down # first check for a scalable graphic success = self._match_and_store_icon(pkg_fname, cpt, cpt_export_path, filelist, icon_str + ".svg", "scalable") if not success: success = self._match_and_store_icon(pkg_fname, cpt, cpt_export_path, filelist, icon_str + ".svgz", "scalable") # then try to scale down larger graphics if not success: for size in self._large_icon_sizes: success = self._match_and_store_icon(pkg_fname, cpt, cpt_export_path, filelist, icon_name_ext, size) or success if not success: last_pixmap = None # handle stuff in the pixmaps directory for path in filelist: if path.startswith("usr/share/pixmaps"): file_basename = os.path.basename(path) if ((file_basename == icon_str) or (os.path.splitext(file_basename)[0] == icon_str)): # the pixmap dir can contain icons in multiple formats, and store_icon() fails in case # the icon format is not allowed. We therefore only exit here, if the icon has a valid format if self._icon_allowed(path): return self._store_icon(pkg_fname, cpt, cpt_export_path, path, IconSize(64)) last_pixmap = path if last_pixmap: # we don't do a global icon search anymore, since we've found an (unsuitable) icon # already cpt.add_hint("icon-format-unsupported", {'icon_fname': os.path.basename(last_pixmap)}) return False icon_dict = self._icon_finder.find_icons(cpt.pkgname, icon_str, all_icon_sizes) success = False if icon_dict: for size in self._icon_sizes: if not size in icon_dict: continue success = self._store_icon(icon_dict[size]['deb_fname'], cpt, cpt_export_path, icon_dict[size]['icon_fname'], size) or success if not success: for size in self._large_icon_sizes: if not size in icon_dict: continue for asize in self._icon_sizes: success = self._store_icon(icon_dict[size]['deb_fname'], cpt, cpt_export_path, icon_dict[size]['icon_fname'], asize) or success return success if ("." in icon_str) and (not self._icon_allowed(icon_str)): cpt.add_hint("icon-format-unsupported", {'icon_fname': icon_str}) else: cpt.add_hint("icon-not-found", {'icon_fname': icon_str}) return False return success def process(self, pkgname, pkg_fname, pkgid=None, metainfo_files=None): ''' Reads the metadata from the xml file and the desktop files. And returns a list of DEP11Component objects. ''' deb = None try: deb = DebFile(pkg_fname) except Exception as e: log.error("Error reading deb file '%s': %s" % (pkg_fname, e)) if not deb: return list() try: filelist = self._get_deb_filelist(deb) except: log.error("List of files for '%s' could not be read" % (pkg_fname)) filelist = None if not filelist: cpt = DEP11Component(self._suite_name, self._archive_component, pkgname, pkgid) cpt.add_hint("deb-filelist-error", {'pkg_fname': os.path.basename(pkg_fname)}) return [cpt] if not pkgid: # we didn't get an identifier, so start guessing one. idname, ext = os.path.splitext(os.path.basename(pkg_fname)) if not idname: idname = os.path.basename(pkg_fname) pkgid = idname export_path = "%s/%s" % (self._export_dir, self._archive_component) component_dict = dict() # if we don't have an explicit list of interesting files, we simply scan all if not metainfo_files: metainfo_files = filelist # first cache all additional metadata (.desktop/.pc/etc.) files mdata_raw = dict() for meta_file in metainfo_files: if meta_file.endswith(".desktop") and meta_file.startswith("usr/share/applications"): # We have a .desktop file dcontent = None cpt_id = os.path.basename(meta_file) error = None try: dcontent = str(deb.data.extractdata(meta_file), 'utf-8') except Exception as e: error = {'tag': "deb-extract-error", 'params': {'fname': cpt_id, 'pkg_fname': os.path.basename(pkg_fname), 'error': str(e)}} if not dcontent and not error: error = {'tag': "deb-empty-file", 'params': {'fname': cpt_id, 'pkg_fname': os.path.basename(pkg_fname)}} mdata_raw[cpt_id] = {'error': error, 'data': dcontent} # process all AppStream XML files for meta_file in metainfo_files: if meta_file.endswith(".xml") and meta_file.startswith("usr/share/appdata"): xml_content = None cpt = DEP11Component(self._suite_name, self._archive_component, pkgname, pkgid) try: xml_content = str(deb.data.extractdata(meta_file), 'utf-8') except Exception as e: # inability to read an AppStream XML file is a valid reason to skip the whole package cpt.add_hint("deb-extract-error", {'fname': meta_file, 'pkg_fname': os.path.basename(pkg_fname), 'error': str(e)}) return [cpt] if not xml_content: continue read_appstream_upstream_xml(cpt, xml_content) component_dict[cpt.cid] = cpt # Reads the desktop files associated with the xml file if not cpt.cid: # if there is no ID at all, we dump this component, since we cannot do anything with it at all cpt.add_hint("metainfo-no-id") continue cpt.set_srcdata_checksum_from_data(xml_content) if cpt.kind == "desktop-app": data = mdata_raw.get(cpt.cid) if not data: cpt.add_hint("missing-desktop-file") continue if data['error']: # add a non-fatal hint that we couldn't process the .desktop file cpt.add_hint(data['error']['tag'], data['error']['params']) else: # we have a .desktop component, extend it with the associated .desktop data read_desktop_data(cpt, data['data']) cpt.set_srcdata_checksum_from_data(xml_content+data['data']) del mdata_raw[cpt.cid] # now process the remaining metadata files, which have not been processed together with the XML for mid, mdata in mdata_raw.items(): if mid.endswith(".desktop"): # We have a .desktop file cpt = DEP11Component(self._suite_name, self._archive_component, pkgname, pkgid) cpt.cid = mid if mdata['error']: # add a fatal hint that we couldn't process this file cpt.add_hint(mdata['error']['tag'], mdata['error']['params']) component_dict[cpt.cid] = cpt else: ret = read_desktop_data(cpt, mdata['data']) if ret or not cpt.has_ignore_reason(): component_dict[cpt.cid] = cpt cpt.set_srcdata_checksum_from_data(mdata['data']) else: # this means that reading the .desktop file failed and we should # silently ignore this issue (since the file was marked to be invisible on purpose) pass # fetch media (icons/screenshots), if we don't ignore the component already cpts = component_dict.values() for cpt in cpts: if cpt.has_ignore_reason(): continue if not cpt.global_id: log.error("Component '%s' from package '%s' has no source-data checksum / global-id." % (cpt.cid, pkg_fname)) continue # check if we have a component generated from # this source data in the cache already. # To account for packages which change their package name, we # also need to check if the package this component is associated # with matches ours. existing_mdata = self._dcache.get_metadata(cpt.global_id) if existing_mdata: s = "Package: %s\n" % (pkgname) if s in existing_mdata: continue else: # the exact same metadata exists in a different package already, raise ab error. # ATTENTION: This does not cover the case where *different* metadata (as in, different summary etc.) # but with the *same ID* exists. This kind of issue can only be catched when listing all IDs per # suite/acomponent combination and checking for dupes (we do that in the DEP-11 validator and display # the result prominently on the HTML pages) ecpt = yaml.safe_load(existing_mdata) cpt.add_hint("metainfo-duplicate-id", {'cid': cpt.cid, 'pkgname': ecpt.get('Package', '')}) continue self._fetch_icon(cpt, export_path, pkg_fname, filelist) if cpt.kind == 'desktop-app' and not cpt.icon: cpt.add_hint("gui-app-without-icon", {'cid': cpt.cid}) else: self._fetch_screenshots(cpt, export_path) # write data to cache if self.write_to_cache: # write the components we found to the cache self._dcache.set_components(pkgid, cpts) return cpts appstream-dep11-0.4.0/dep11/hints.py000066400000000000000000000061671261747313700170550ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (c) 2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import sys import yaml import logging as log __all__ = [] _DEP11_HINT_DESCRIPTIONS = None class HintSeverity: ''' Importance of a component parsing hint. ''' ERROR = 3 WARNING = 2 INFO = 1 __all__.append('HintSeverity') class Hint: ''' An issue found with the metadata. ''' severity = HintSeverity.INFO tag_name = str() text_params = dict() def __init__(self, severity, tag, params): self.severity = severity self.tag_name = tag self.text_params = params def __str__(self): return "%s: %s (%s)" % (str(self.severity), tag_name, str(text_params)) __all__.append('Hint') def get_hints_index_fname(): ''' Find the YAML tag description, even if the DEP-11 metadata generator is not properly installed. ''' fname = os.path.join(sys.prefix, "share", "dep11", "dep11-hints.yml") if os.path.isfile(fname): return fname fname = os.path.dirname(os.path.realpath(__file__)) fname = os.path.realpath(os.path.join(fname, "..", "data", "dep11-hints.yml")) if os.path.isfile(fname): return fname raise Exception("Could not find tag description file (dep11-hints.yml).") def get_hint_description_index(): global _DEP11_HINT_DESCRIPTIONS if not _DEP11_HINT_DESCRIPTIONS: fname = get_hints_index_fname() f = open(fname, 'r') _DEP11_HINT_DESCRIPTIONS = yaml.safe_load(f.read()) f.close() return _DEP11_HINT_DESCRIPTIONS __all__.append('get_hint_description_index') def get_hint_tag_info(tag_name): idx = get_hint_description_index() tag = idx.get(tag_name) if not tag: log.error("Could not find tag name: %s", tag_name) tag = idx.get("internal-unknown-tag") return tag __all__.append('get_hint_tag_info') def get_hint_severity(tag_name): tag = get_hint_tag_info(tag_name) severity = tag.get('severity') if not severity: log.error("Tag %s has no severity!", tag_name) if severity == "warning": return HintSeverity.WARNING if severity == "info": return HintSeverity.INFO return HintSeverity.ERROR __all__.append('get_hint_severity') def hint_tag_is_error(tag_name): tag = get_hint_tag_info(tag_name) severity = tag.get('severity') if not severity: log.error("Tag %s has no severity!", tag_name) if severity == "error": return True return False __all__.append('hint_tag_is_error') appstream-dep11-0.4.0/dep11/iconfinder.py000066400000000000000000000136711261747313700200460ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (c) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import gzip import re from dep11.component import IconSize from dep11.utils import read_packages_dict_from_file class AbstractIconFinder: ''' An icon-finder finds an icon in the archive, if it has not yet been found in the analyzed package already. AbstractIconFinder is a dummy class, not implementing the methods needed to find an icon. ''' def __init__(self, suite_name, archive_component): pass def find_icons(self, pkgname, icon_str, icon_sizes): return None def set_allowed_icon_extensions(self, exts): pass def _decode_contents_line(line): try: return str(line, 'utf-8') except: return str(line, 'iso-8859-1') class ContentsListIconFinder(AbstractIconFinder): ''' An implementation of an IconFinder, using a Contents-.gz file present in Debian archive mirrors to find icons. ''' def __init__(self, suite_name, archive_component, arch_name, archive_mirror_dir, pkgdict=None): self._suite_name = suite_name self._component = archive_component self._mirror_dir = archive_mirror_dir contents_basename = "Contents-%s.gz" % (arch_name) contents_fname = os.path.join(archive_mirror_dir, "dists", suite_name, archive_component, contents_basename) # Ubuntu does not place the Contents file in a component-specific directory, # so fall back to the global one. if not os.path.isfile(contents_fname): path = os.path.join(archive_mirror_dir, "dists", suite_name, contents_basename) if os.path.isfile(path): contents_fname = path # load and preprocess insanely large file. # we don't show mercy to memory here, we just want this to be fast. self._contents_data = list() f = gzip.open(contents_fname, 'r') for line in f: line = _decode_contents_line(line) if line.startswith("usr/share/icons/hicolor/") or line.startswith("usr/share/pixmaps/"): self._contents_data.append(line) continue # allow Oxygen icon theme, needed to support KDE apps if line.startswith("usr/share/icons/oxygen"): self._contents_data.append(line) continue # in rare events, GNOME needs the same treatment, so special-case Adwaita as well if line.startswith("usr/share/icons/Adwaita"): self._contents_data.append(line) continue f.close() self._packages_dict = pkgdict if not self._packages_dict: self._packages_dict = read_packages_dict_from_file(archive_mirror_dir, suite_name, archive_component, arch_name) def _query_icon(self, size, icon): ''' Find icon files in the archive which match a size. ''' if not self._contents_data: return None valid = None if size: valid = re.compile('^usr/share/icons/.*/' + size + '/apps/' + icon + '[\.png|\.svg|\.svgz]') else: valid = re.compile('^usr/share/pixmaps/' + icon + '.png') res = list() for line in self._contents_data: if valid.match(line): res.append(line) for line in res: line = line.strip(' \t\n\r') if not " " in line: continue parts = line.split(" ", 1) path = parts[0].strip() group_pkg = parts[1].strip() if not "/" in group_pkg: continue pkgname = group_pkg.split("/", 1)[1].strip() pkg = self._packages_dict.get(pkgname) if not pkg: continue deb_fname = os.path.join(self._mirror_dir, pkg['filename']) return {'icon_fname': path, 'deb_fname': deb_fname} return None def find_icons(self, package, icon, sizes): ''' Tries to find the best possible icon available ''' size_map_flist = dict() for size in sizes: flist = self._query_icon(str(size), icon) if flist: size_map_flist[size] = flist if not IconSize(64) in size_map_flist: # see if we can find a scalable vector graphic as icon # we assume "64x64" as size here, and resize the vector # graphic later. flist = self._query_icon("scalable", icon) if flist: size_map_flist[IconSize(64)] = flist else: if IconSize(128) in size_map_flist: # Lots of software doesn't have a 64x64 icon, but a 128x128 icon. # We just implement this small hack to resize the icon to the # appropriate size. size_map_flist[IconSize(64)] = size_map_flist[IconSize(128)] else: # some software doesn't store icons in sized XDG directories. # catch these here, and assume that the size is 64x64 flist = self._query_icon(None, icon) if flist: size_map_flist[IconSize(64)] = flist return size_map_flist def set_allowed_icon_extensions(self, exts): self._allowed_exts = exts appstream-dep11-0.4.0/dep11/parsers.py000066400000000000000000000322171261747313700174020ustar00rootroot00000000000000#!/usr/bin/env python3 """ Reads AppStream XML metadata and metadata from XDG .desktop files. """ # Copyright (c) 2014 Abhishek Bhattacharjee # Copyright (c) 2014 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import re from configparser import RawConfigParser import lxml.etree as et from xml.sax.saxutils import escape from io import StringIO from dep11.component import DEP11Component, ProvidedItemType from dep11.utils import str_enc_dec def read_desktop_data(cpt, dcontent): ''' Parses a .desktop file and sets ComponentData properties ''' df = RawConfigParser(allow_no_value=True) items = None try: df.readfp(StringIO(dcontent)) items = df.items("Desktop Entry") if df.get("Desktop Entry", "Type") != "Application": # ignore this file, isn't an application cpt.add_hint("not-an-application") return False try: if df.get("Desktop Entry", "NoDisplay") == "True": # we ignore this .desktop file, shouldn't be displayed cpt.add_hint("invisible-application") return False except: # we don't care if the NoDisplay variable doesn't exist # if it isn't there, the file should be processed pass except Exception as e: # this .desktop file is not interesting cpt.add_hint("desktop-file-read-error", str(e)) return True # if we reached this step, we are dealing with a GUI desktop app cpt.kind = 'desktop-app' for item in items: if len(item) != 2: continue key = item[0] value = str_enc_dec(item[1]) if not value: continue if key.startswith("name"): if key == 'name': cpt.name['C'] = value else: cpt.name[key[5:-1]] = value elif key == 'categories': value = value.split(';') value.pop() cpt.categories = value elif key.startswith('comment'): if key == 'comment': cpt.summary['C'] = value else: cpt.summary[key[8:-1]] = value elif key.startswith('keywords'): value = re.split(';|,', value) if not value[-1]: value.pop() if key[8:] == '': if cpt.keywords: if set(value) not in \ [set(val) for val in cpt.keywords.values()]: cpt.keywords.update( {'C': list(map(str_enc_dec, value))} ) else: cpt.keywords = { 'C': list(map(str_enc_dec, value)) } else: if cpt.keywords: if set(value) not in \ [set(val) for val in cpt.keywords.values()]: cpt.keywords.update( {key[9:-1]: list(map(str_enc_dec, value))} ) else: cpt.keywords = { key[9:-1]: list(map(str_enc_dec, value)) } elif key == 'mimetype': value = value.split(';') if len(value) > 1: value.pop() for val in value: cpt.add_provided_item( ProvidedItemType.MIMETYPE, val ) elif key == 'icon': cpt.icon = value return True def _get_tag_locale(subs): attr_dic = subs.attrib if attr_dic: locale = attr_dic.get('{http://www.w3.org/XML/1998/namespace}lang') if locale: return locale return "C" def _parse_description_tag(subs): ''' Handles the description tag ''' def prepare_desc_string(s): ''' Clears linebreaks and XML-escapes the resulting string ''' if not s: return "" s = s.strip() s = " ".join(s.split()) return escape(s) ddict = dict() # The description tag translation is combined per language, # for faster parsing on the client side. # In case no translation is found, the untranslated version is used instead. # the DEP-11 YAML stores the description as HTML for usubs in subs: locale = _get_tag_locale(usubs) if usubs.tag == 'p': if not locale in ddict: ddict[locale] = "" ddict[locale] += "

%s

" % str_enc_dec(prepare_desc_string(usubs.text)) elif usubs.tag == 'ul' or usubs.tag == 'ol': tmp_dict = dict() # find the right locale, or fallback to untranslated for u_usubs in usubs: locale = _get_tag_locale(u_usubs) if not locale in tmp_dict: tmp_dict[locale] = "" if u_usubs.tag == 'li': tmp_dict[locale] += "
  • %s
  • " % str_enc_dec(prepare_desc_string(u_usubs.text)) for locale, value in tmp_dict.items(): if not locale in ddict: # This should not happen (but better be prepared) ddict[locale] = "" ddict[locale] += "<%s>%s" % (usubs.tag, value, usubs.tag) return ddict def _parse_screenshots_tag(subs): ''' Handles screenshots, caption, source-image etc. ''' shots = [] for usubs in subs: # for one screeshot tag if usubs.tag == 'screenshot': screenshot = dict() attr_dic = usubs.attrib if attr_dic.get('type'): if attr_dic['type'] == 'default': screenshot['default'] = True # in case of old styled xmls url = usubs.text if url: url = url.strip() screenshot['source-image'] = {'url': url} shots.append(screenshot) continue # else look for captions and image tag for tags in usubs: if tags.tag == 'caption': # for localisation attr_dic = tags.attrib if attr_dic: for v in attr_dic.values(): key = v else: key = 'C' if screenshot.get('caption'): screenshot['caption'][key] = str_enc_dec(tags.text) else: screenshot['caption'] = {key: str_enc_dec(tags.text)} if tags.tag == 'image': screenshot['source-image'] = {'url': tags.text} # only add the screenshot if we have a source image if screenshot.get ('source-image'): shots.append(screenshot) return shots def _parse_releases_tag(relstag): ''' Parses a releases tag and returns the last three releases ''' rels = list() for subs in relstag: # for one screeshot tag if subs.tag != 'release': continue release = dict() attr_dic = subs.attrib if attr_dic.get('version'): release['version'] = attr_dic['version'] if attr_dic.get('timestamp'): try: release['unix-timestamp'] = int(attr_dic['timestamp']) except: # the timestamp was wrong - we silently ignore the error # TODO: Emit warning hint continue else: # we can't use releases which don't have a timestamp # TODO: Emit a warning hint here continue # else look for captions and image tag for usubs in subs: if usubs.tag == 'description': release['description'] = _parse_description_tag(usubs) rels.append(release) # sort releases, newest first rels = sorted(rels, key=lambda k: k['unix-timestamp'], reverse=True) if len(rels) > 3: return rels[:3] return rels def read_appstream_upstream_xml(cpt, xml_content): ''' Reads the appdata from the xml file in usr/share/appdata. Sets ComponentData properties ''' root = None try: root = et.fromstring(bytes(xml_content, 'utf-8')) except Exception as e: cpt.add_hint("metainfo-parse-error", str(e)) return if root is None: cpt.add_hint("metainfo-parse-error", "Error is unknown, the root node was null.") if root.tag == 'application': # we parse ancient AppStream XML, but it is a good idea to update it to make use of newer features, remove some ancient # oddities and to simplify the parser in future. So we add a hint for that. cpt.add_hint("ancient-metadata") key = root.attrib.get('type') if key: if key == 'desktop': cpt.kind = 'desktop-app' else: # for other components like addon,codec, inputmethod etc cpt.kind = root.attrib['type'] for subs in root: locale = _get_tag_locale(subs) if subs.tag == 'id': cpt.cid = subs.text # legacy support key = subs.attrib.get('type') if key and not cpt.kind: if key == 'desktop': cpt.kind = 'desktop-app' else: cpt.kind = key elif subs.tag == "name": cpt.name[locale] = subs.text elif subs.tag == "summary": cpt.summary[locale] = subs.text elif subs.tag == "description": desc = _parse_description_tag(subs) cpt.description = desc elif subs.tag == "screenshots": screen = _parse_screenshots_tag(subs) cpt.screenshots = screen elif subs.tag == "provides": for bins in subs: if bins.tag == "binary": cpt.add_provided_item( ProvidedItemType.BINARY, bins.text ) if bins.tag == 'library': cpt.add_provided_item( ProvidedItemType.LIBRARY, bins.text ) if bins.tag == 'dbus': if not cpt.provides.get(ProvidedItemType.DBUS): cpt.provides[ProvidedItemType.DBUS] = list() bus_kind = bins.attrib.get('type') if bus_kind == "session": bus_kind = "user" if bus_kind: cpt.provides[ProvidedItemType.DBUS].append({'type': bus_kind, 'service': bins.text}) if bins.tag == 'firmware': if not cpt.provides.get(ProvidedItemType.FIRMWARE): cpt.provides[ProvidedItemType.FIRMWARE] = list() fw_type = bins.attrib.get('type') fw_data = {'type': fw_type} _valid = True if fw_type == "flashed": fw_data['guid'] = bins.text elif fw_type == "runtime": fw_data['fname'] = bins.text else: _valid = False if _valid: cpt.provides[ProvidedItemType.FIRMWARE].append(fw_data) if bins.tag == 'python2': cpt.add_provided_item( ProvidedItemType.PYTHON_2, bins.text ) if bins.tag == 'python3': cpt.add_provided_item( ProvidedItemType.PYTHON_3, bins.text ) if bins.tag == 'codec': cpt.add_provided_item( ProvidedItemType.CODEC, bins.text ) elif subs.tag == "url": if cpt.url: cpt.url.update({subs.attrib['type']: subs.text}) else: cpt.url = {subs.attrib['type']: subs.text} elif subs.tag == "project_license": cpt.project_license = subs.text elif subs.tag == "project_group": cpt.project_group = subs.text elif subs.tag == "developer_name": cpt.developer_name[locale] = subs.text elif subs.tag == "extends": cpt.extends.append(subs.text) elif subs.tag == "compulsory_for_desktop": cpt.compulsory_for_desktops.append(subs.text) elif subs.tag == "releases": releases = _parse_releases_tag(subs) cpt.releases = releases appstream-dep11-0.4.0/dep11/utils.py000066400000000000000000000044351261747313700170640ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (c) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import gzip from apt_pkg import TagFile, version_compare def str_enc_dec(val): ''' Handles encoding decoding for localized values. ''' if isinstance(val, str): val = bytes(val, 'utf-8') val = val.decode("utf-8", "replace") return val def read_packages_dict_from_file(archive_root, suite, component, arch): source_path = archive_root + "/dists/%s/%s/binary-%s/Packages.gz" % (suite, component, arch) f = gzip.open(source_path, 'rb') tagf = TagFile(f) package_dict = dict() for section in tagf: pkg = dict() pkg['arch'] = section['Architecture'] pkg['version'] = section['Version'] pkg['name'] = section['Package'] if not section.get('Filename'): print("Package %s-%s has no filename specified." % (pkg['name'], pkg['version'])) continue pkg['filename'] = section['Filename'] pkg['maintainer'] = section['Maintainer'] pkg2 = package_dict.get(pkg['name']) if pkg2: compare = version_compare(pkg2['version'], pkg['version']) if compare >= 0: continue package_dict[pkg['name']] = pkg return package_dict def build_cpt_global_id(cptid, checksum): if (not checksum) or (not cptid): return None gid = None parts = None if cptid.startswith(("org.", "net.", "com.", "io.")): parts = cptid.split(".", 2) if parts and len(parts) > 2: gid = "%s/%s/%s/%s" % (parts[0].lower(), parts[1], parts[2], checksum) else: gid = "%s/%s/%s" % (cptid[0].lower(), cptid, checksum) return gid appstream-dep11-0.4.0/dep11/validate.py000066400000000000000000000265231261747313700175170ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import yaml import gzip import xml.etree.ElementTree as ET from voluptuous import Schema, Required, All, Any, Length, Range, Match, Url __all__ = [] schema_header = Schema({ Required('File'): All(str, 'DEP-11', msg="Must be \"DEP-11\""), Required('Origin'): All(str, Length(min=1)), Required('Version'): All(str, Match(r'(\d+\.?)+$'), msg="Must be a valid version number"), Required('MediaBaseUrl'): All(str, Url()), 'Time': All(str, str), 'Priority': All(str, int), }) schema_provides_dbus = Schema({ Required('type'): All(str, Length(min=1)), Required('service'): All(str, Length(min=1)), }) schema_provides_firmware = Schema({ Required('type'): All(str, Length(min=1)), Any('guid', 'fname'): All(str, Length(min=1)) }) schema_provides = Schema({ Any('mimetypes', 'binaries', 'libraries', 'python3', 'python2', 'modaliases', 'fonts'): All(list, [str], Length(min=1)), 'dbus': All(list, Length(min=1), [schema_provides_dbus]), 'firmware': All(list, Length(min=1), [schema_provides_firmware]), }) schema_keywords = Schema({ Required('C'): All(list, [str], Length(min=1), msg="Must have an unlocalized 'C' key"), dict: All(list, [str], Length(min=1)), }, extra = True) schema_translated = Schema({ Required('C'): All(str, Length(min=1), msg="Must have an unlocalized 'C' key"), dict: All(str, Length(min=1)), }, extra = True) schema_image = Schema({ Required('width'): All(int, Range(min=10)), Required('height'): All(int, Range(min=10)), Required('url'): All(str, str, Length(min=1)), }) schema_screenshots = Schema({ Required('default', default=False): All(bool), Required('source-image'): All(dict, Length(min=1), schema_image), 'thumbnails': All(list, Length(min=1), [schema_image]), 'caption': All(dict, Length(min=1), schema_translated), }) schema_icon = Schema({ 'stock': All(str, Length(min=1)), 'cached': All(str, Match(r'.*[.].*$'), msg='Icon entry is missing filename or extension'), 'local': All(str, Match(r'^[\'"]?(?:/[^/]+)*[\'"]?$'), msg='Icon entry should be an absolute path'), 'remote': All(str, str, Length(min=1)), }) schema_url = Schema({ Any('homepage', 'bugtracker', 'faq', 'help', 'donation'): All(str, Url()), }) schema_releases = Schema({ Required('unix-timestamp'): All(int), Required('version'): All(str, Length(min=1)), 'description': All(dict, Length(min=1), schema_translated), }) schema_component = Schema({ Required('Type'): All(str, Any('generic', 'desktop-app', 'web-app', 'addon', 'codec', 'inputmethod', 'font')), Required('ID'): All(str, Length(min=1)), Required('Name'): All(dict, Length(min=1), schema_translated), Required('Package'): All(str, Length(min=1)), 'Summary': All(dict, {str: str}, Length(min=1), schema_translated), 'Description': All(dict, {str: str}, Length(min=1), schema_translated), 'Categories': All(list, [str], Length(min=1)), 'CompulsoryForDesktops': All(list, [str], Length(min=1)), 'Url': All(dict, Length(min=1), schema_url), 'Icon': All(dict, Length(min=1), schema_icon), 'Keywords': All(dict, Length(min=1), schema_keywords), 'Provides': All(dict, Length(min=1), schema_provides), 'ProjectGroup': All(str, Length(min=1)), 'ProjectLicense': All(str, Length(min=1)), 'DeveloperName': All(dict, Length(min=1), schema_translated), 'Screenshots': All(list, Length(min=1), [schema_screenshots]), 'Extends': All(list, [str], Length(min=1)), 'Releases': All(list, Length(min=1), [schema_releases]), # Internal, non-specified fields 'X-Source-Checksum': All(str, Length(min=10)), }) class DEP11Validator: issue_list = list() def __init__(self): pass def add_issue(self, msg): self.issue_list.append(msg) def _is_quoted(self, s): return (s.startswith("\"") and s.endswith("\"")) or (s.startswith("\'") and s.endswith("\'")) def _test_localized_dict(self, doc, ldict, id_string): ret = True for lang, value in ldict.items(): if lang == 'x-test': self.add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: x-test")) if lang == 'xx': self.add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Found cruft locale: xx")) if lang.endswith('.UTF-8'): self.add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "AppStream locale names should not specify encoding (ends with .UTF-8)")) if self._is_quoted(value): self.add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "String is quoted: '%s' @ %s" % (value, lang))) if " " in lang: self.add_issue("[%s][%s]: %s" % (doc['ID'], id_string, "Locale name contains space: '%s'" % (lang))) # this - as opposed to the other issues - is an error ret = False return ret def _test_localized(self, doc, key): ldict = doc.get(key, None) if not ldict: return True return self._test_localized_dict(doc, ldict, key) def _test_custom_objects(self, lines): ret = True for i in range(0, len(lines)): if "!!python/" in lines[i]: self.add_issue("Python object encoded in line %i." % (i)) ret = False return ret def _validate_description_tag(self, docid, child, allowed_tags): ret = True if not child.tag in allowed_tags: self.add_issue("[%s]: %s" % (docid, "Invalid description markup found: '%s' @ data['Description']" % (child.tag))) ret = False if child.attrib.get('{http://www.w3.org/XML/1998/namespace}lang'): self.add_issue("[%s]: Invalid, localized tag in long description: '%s' => %s @ data['Description']" % (docid, child.tag, child.text)) ret = False elif len(child.attrib) > 0: self.add_issue("[%s]: Markup tag has attributes: '%s' => %s @ data['Description']" % (docid, child.tag, child.attrib)) ret = False return ret def _validate_description(self, docid, desc, poshint="Description"): ret = True ET.register_namespace("xml", "http://www.w3.org/XML/1998/namespace") try: root = ET.fromstring("%s" % (desc)) except Exception as e: self.add_issue("[%s]: %s" % (docid, "Broken description markup found: %s @ data['%s']" % (str(e), poshint))) return False for child in root: if not self._validate_description_tag(docid, child, ['p', 'ul', 'ol']): ret = False if (child.tag == 'ul') or (child.tag == 'ol'): for child2 in child: if not self._validate_description_tag(docid, child2, ['li']): ret = False return ret def validate_data(self, data): ret = True ids_found = dict() lines = data.split("\n") # see if there are any Python-specific objects encoded ret = self._test_custom_objects(lines) try: docs = yaml.load_all(data) header = next(docs) except Exception as e: self.add_issue("Could not parse file: %s" % (str(e))) return False try: schema_header(header) except Exception as e: self.add_issue("Invalid DEP-11 header: %s" % (str(e))) ret = False for doc in docs: docid = doc.get('ID') pkgname = doc.get('Package') if not pkgname: pkgname = "?unknown?" if not doc: self.add_issue("FATAL: Empty document found.") ret = False continue if not docid: self.add_issue("FATAL: Component without ID found.") ret = False continue if ids_found.get(docid): self.add_issue("FATAL: Found two components with the same ID: %s (in packages %s and %s)." % (docid, ids_found[docid], pkgname)) ret = False continue else: ids_found[docid] = pkgname try: schema_component(doc) except Exception as e: self.add_issue("[%s]: %s" % (docid, str(e))) ret = False continue # more tests for the icon key icon = doc.get('Icon') if (doc['Type'] == "desktop-app") or (doc['Type'] == "web-app"): if not doc.get('Icon'): self.add_issue("[%s]: %s" % (docid, "Components containing an application must have an 'Icon' key.")) ret = False if icon: if (not icon.get('stock')) and (not icon.get('cached')) and (not icon.get('local')): self.add_issue("[%s]: %s" % (docid, "A 'stock', 'cached' or 'local' icon must at least be provided. @ data['Icon']")) ret = False if not self._test_localized(doc, 'Name'): ret = False if not self._test_localized(doc, 'Summary'): ret = False if not self._test_localized(doc, 'Description'): ret = False if not self._test_localized(doc, 'DeveloperName'): ret = False for shot in doc.get('Screenshots', list()): caption = shot.get('caption') if caption: if not self._test_localized_dict(doc, caption, "Screenshots.x.caption"): ret = False for rel in doc.get('Releases', list()): desc = rel.get('description') if not desc: continue if not self._test_localized_dict(doc, desc, "Releases.x.description"): ret = False for d in desc.values(): if not self._validate_description(docid, d, "Releases.x.description"): ret = False desc = doc.get('Description', dict()) for d in desc.values(): if not self._validate_description(docid, d): ret = False return ret def validate_file(self, fname): f = None if fname.endswith(".gz"): f = gzip.open(fname, 'r') else: f = open(fname, 'r') data = str(f.read(), 'utf-8') f.close() return self.validate_data(data) def print_issues(self): for issue in self.issue_list: print(issue) def clear_issues(): self.issue_list = list() __all__.append('DEP11Validator') appstream-dep11-0.4.0/scripts/000077500000000000000000000000001261747313700161215ustar00rootroot00000000000000appstream-dep11-0.4.0/scripts/dep11-generator000077500000000000000000001003241261747313700207450ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import sys import yaml import apt_pkg import gzip import tarfile import glob import shutil import time import traceback from jinja2 import Environment, FileSystemLoader from optparse import OptionParser import multiprocessing as mp import logging as log from dep11 import MetadataExtractor, DataCache, build_cpt_global_id from dep11.component import DEP11Component, get_dep11_header, dict_to_dep11_yaml from dep11.iconfinder import ContentsListIconFinder from dep11.utils import read_packages_dict_from_file from dep11.hints import get_hint_tag_info from dep11.validate import DEP11Validator try: import pygments from pygments.lexers import YamlLexer from pygments.formatters import HtmlFormatter except: pygments = None def safe_move_file(old_fname, new_fname): if not os.path.isfile(old_fname): return if os.path.isfile(new_fname): os.remove(new_fname) os.rename(old_fname, new_fname) def get_pkg_id(name, version, arch): return "%s/%s/%s" % (name, version, arch) def equal_dicts(d1, d2, ignore_keys): ignored = set(ignore_keys) for k1, v1 in d1.items(): if k1 not in ignored and (k1 not in d2 or d2[k1] != v1): return False for k2, v2 in d2.items(): if k2 not in ignored and k2 not in d1: return False return True def extract_metadata(mde, sn, pkgname, package_fname, version, arch, pkid): # we're now in a new process and can (re)open a LMDB connection mde.reopen_cache() cpts = mde.process(pkgname, package_fname, pkid) msgtxt = "Processed: %s (%s/%s), found %i" % (pkgname, sn, arch, len(cpts)) return msgtxt def load_generator_config(wdir): conf_fname = os.path.join(wdir, "dep11-config.yml") if not os.path.isfile(conf_fname): print("Could not find configuration! Make sure 'dep11-config.yml' exists!") return None f = open(conf_fname, 'r') conf = yaml.safe_load(f.read()) f.close() if not conf: print("Configuration is empty!") return None if not conf.get("ArchiveRoot"): print("You need to specify an archive root path.") return None if not conf.get("Suites"): print("Config is missing information about suites!") return None if not conf.get("MediaBaseUrl"): print("You need to specify an URL where additional data (like screenshots) can be downloaded.") return None return conf class DEP11Generator: def __init__(self): pass def initialize(self, dep11_dir): dep11_dir = os.path.abspath(dep11_dir) conf = load_generator_config(dep11_dir) if not conf: return False self._dep11_url = conf.get("MediaBaseUrl") self._icon_sizes = conf.get("IconSizes") if not self._icon_sizes: self._icon_sizes = ["128x128", "64x64"] self._archive_root = conf.get("ArchiveRoot") cache_dir = os.path.join(dep11_dir, "cache") if conf.get("CacheDir"): cache_dir = conf.get("CacheDir") self._export_dir = os.path.join(dep11_dir, "export") if conf.get("ExportDir"): self._export_dir = conf.get("ExportDir") if not os.path.exists(cache_dir): os.makedirs(cache_dir) if not os.path.exists(self._export_dir): os.makedirs(self._export_dir) self._suites_data = conf['Suites'] self._distro_name = conf.get("DistroName") if not self._distro_name: self._distro_name = "Debian" # initialize our on-dik metadata pool self._cache = DataCache(self._get_media_dir()) ret = self._cache.open(cache_dir) os.chdir(dep11_dir) return ret def _get_media_dir(self): mdir = os.path.join(self._export_dir, "media") if not os.path.exists(mdir): os.makedirs(mdir) return mdir def _get_packages_for(self, suite, component, arch): return read_packages_dict_from_file(self._archive_root, suite, component, arch).values() def make_icon_tar(self, suitename, component, pkglist): ''' Generate icons-%(size).tar.gz ''' dep11_mediadir = self._get_media_dir() names_seen = set() tar_location = os.path.join(self._export_dir, "data", suitename, component) size_tars = dict() for pkg in pkglist: pkid = get_pkg_id(pkg['name'], pkg['version'], pkg['arch']) gids = self._cache.get_cpt_gids_for_pkg(pkid) if not gids: # no component global-ids == no icons to add to the tarball continue for gid in gids: for size in self._icon_sizes: icon_location_glob = os.path.join (dep11_mediadir, component, gid, "icons", size, "*.png") tar = None if size not in size_tars: icon_tar_fname = os.path.join(tar_location, "icons-%s.tar.gz" % (size)) size_tars[size] = tarfile.open(icon_tar_fname+".new", "w:gz") tar = size_tars[size] for filename in glob.glob(icon_location_glob): icon_name = os.path.basename(filename) if size+"/"+icon_name in names_seen: continue tar.add(filename, arcname=icon_name) names_seen.add(size+"/"+icon_name) for tar in size_tars.values(): tar.close() # FIXME Ugly.... safe_move_file(tar.name, tar.name.replace(".new", "")) def process_suite(self, suite_name): ''' Extract new metadata for a given suite. ''' suite = self._suites_data.get(suite_name) if not suite: log.error("Suite '%s' not found!" % (suite_name)) return False dep11_mediadir = self._get_media_dir() # We need 'forkserver' as startup method to prevent deadlocks on join() # Something in the extractor is doing weird things, makes joining impossible # when using simple fork as startup method. mp.set_start_method('forkserver') for component in suite['components']: all_cpt_pkgs = list() for arch in suite['architectures']: pkglist = self._get_packages_for(suite_name, component, arch) # compile a list of packages that we need to look into pkgs_todo = dict() for pkg in pkglist: pkid = get_pkg_id(pkg['name'], pkg['version'], pkg['arch']) # check if we scanned the package already if self._cache.package_exists(pkid): continue pkgs_todo[pkid] = pkg # set up metadata extractor iconf = ContentsListIconFinder(suite_name, component, arch, self._archive_root) mde = MetadataExtractor(suite_name, component, self._icon_sizes, self._cache, iconf) # Multiprocessing can't cope with LMDB open in the cache, # but instead of throwing an error or doing something else # that makes debugging easier, it just silently skips each # multprocessing task. Stupid thing. # (remember to re-open the cache later) self._cache.close() # set up multiprocessing with mp.Pool(maxtasksperchild=16) as pool: def handle_results(message): log.info(message) def handle_error(e): traceback.print_exception(type(e), e, e.__traceback__) log.error(str(e)) pool.terminate() sys.exit(5) log.info("Processing %i packages in %s/%s/%s" % (len(pkgs_todo), suite_name, component, arch)) for pkid, pkg in pkgs_todo.items(): package_fname = os.path.join (self._archive_root, pkg['filename']) if not os.path.exists(package_fname): log.warning('Package not found: %s' % (package_fname)) continue pool.apply_async(extract_metadata, (mde, suite_name, pkg['name'], package_fname, pkg['version'], pkg['arch'], pkid), callback=handle_results, error_callback=handle_error) pool.close() pool.join() # reopen the cache, we need it self._cache.reopen() hints_dir = os.path.join(self._export_dir, "hints", suite_name, component) if not os.path.exists(hints_dir): os.makedirs(hints_dir) dep11_dir = os.path.join(self._export_dir, "data", suite_name, component) if not os.path.exists(dep11_dir): os.makedirs(dep11_dir) # now write data to disk hints_fname = os.path.join(hints_dir, "DEP11Hints_%s.yml.gz" % (arch)) data_fname = os.path.join(dep11_dir, "Components-%s.yml.gz" % (arch)) hints_f = gzip.open(hints_fname+".new", 'wb') data_f = gzip.open(data_fname+".new", 'wb') dep11_header = get_dep11_header(suite_name, component, os.path.join(self._dep11_url, component)) data_f.write(bytes(dep11_header, 'utf-8')) for pkg in pkglist: pkid = get_pkg_id(pkg['name'], pkg['version'], pkg['arch']) data = self._cache.get_metadata_for_pkg(pkid) if data: data_f.write(bytes(data, 'utf-8')) hint = self._cache.get_hints(pkid) if hint: hints_f.write(bytes(hint, 'utf-8')) data_f.close() safe_move_file(data_fname+".new", data_fname) hints_f.close() safe_move_file(hints_fname+".new", hints_fname) all_cpt_pkgs.extend(pkglist) # create icon tarball self.make_icon_tar(suite_name, component, all_cpt_pkgs) log.info("Completed metadata extraction for suite %s/%s" % (suite_name, component)) def expire_cache(self): pkgids = set() for suite_name in self._suites_data: suite = self._suites_data[suite_name] for component in suite['components']: for arch in suite['architectures']: pkglist = self._get_packages_for(suite_name, component, arch) for pkg in pkglist: pkid = get_pkg_id(pkg['name'], pkg['version'], pkg['arch']) pkgids.add(pkid) # clean cache oldpkgs = self._cache.get_packages_not_in_set(pkgids) for pkid in oldpkgs: pkid = str(pkid, 'utf-8') self._cache.remove_package(pkid) # ensure we don't leave cruft self._cache.remove_orphaned_components() def remove_processed(self, suite_name): ''' Delete information about processed packages, to reprocess them later. ''' suite = self._suites_data.get(suite_name) if not suite: log.error("Suite '%s' not found!" % (suite_name)) return False for component in suite['components']: all_cpt_pkgs = list() for arch in suite['architectures']: pkglist = self._get_packages_for(suite_name, component, arch) for pkg in pkglist: package_fname = os.path.join (self._archive_root, pkg['filename']) pkid = get_pkg_id(pkg['name'], pkg['version'], pkg['arch']) # we ignore packages without any interesting metadata here if self._cache.is_ignored(pkid): continue self._cache.remove_package(pkid) # drop all components which don't have packages self._cache.remove_orphaned_components() class HTMLGenerator: def __init__(self): pass def initialize(self, dep11_dir): dep11_dir = os.path.abspath(dep11_dir) conf = load_generator_config(dep11_dir) if not conf: return False self._archive_root = conf.get("ArchiveRoot") self._html_url = conf.get("HtmlBaseUrl") if not self._html_url: self._html_url = "." template_dir = os.path.dirname(os.path.realpath(__file__)) template_dir = os.path.realpath(os.path.join(template_dir, "..", "data", "templates", "default")) if not os.path.isdir(template_dir): template_dir = os.path.join(sys.prefix, "share", "dep11", "templates") self._template_dir = template_dir self._distro_name = conf.get("DistroName") if not self._distro_name: self._distro_name = "Debian" self._export_dir = os.path.join(dep11_dir, "export") if conf.get("ExportDir"): self._export_dir = conf.get("ExportDir") if not os.path.exists(self._export_dir): os.makedirs(self._export_dir) self._suites_data = conf['Suites'] self._html_export_dir = os.path.join(self._export_dir, "html") self._dep11_url = conf.get("MediaBaseUrl") os.chdir(dep11_dir) return True def render_template(self, name, out_dir, out_name = None, *args, **kwargs): if not out_name: out_path = os.path.join(out_dir, name) else: out_path = os.path.join(out_dir, out_name) # create subdirectories if necessary out_dir = os.path.dirname(os.path.realpath(out_path)) if not os.path.exists(out_dir): os.makedirs(out_dir) j2_env = Environment(loader=FileSystemLoader(self._template_dir)) template = j2_env.get_template(name) content = template.render(root_url=self._html_url, distro=self._distro_name, time=time.strftime("%Y-%m-%d %H:%M:%S %Z"), *args, **kwargs) log.debug("Render: %s" % (out_path.replace(self._html_export_dir, ""))) with open(out_path, 'wb') as f: f.write(bytes(content, 'utf-8')) def _highlight_yaml(self, yml_data): if not yml_data: return "" if not pygments: return yml_data.replace("\n", "
    \n") return pygments.highlight(yml_data, YamlLexer(), HtmlFormatter()) def _expand_hint(self, hint_data): tag_name = hint_data['tag'] tag = get_hint_tag_info(tag_name) desc = "" try: desc = tag['text'] % hint_data['params'] except Exception as e: desc = "Error while expanding hint description: %s" % (str(e)) severity = tag.get('severity') if not severity: log.error("Tag %s has no severity!", tag_name) severity = "info" return {'tag_name': tag_name, 'description': desc, 'severity': severity} def update_html(self): dep11_hintsdir = os.path.join(self._export_dir, "hints") if not os.path.exists(dep11_hintsdir): return dep11_minfodir = os.path.join(self._export_dir, "data") if not os.path.exists(dep11_minfodir): return export_dir = self._html_export_dir media_dir = os.path.join(self._export_dir, "media") noimage_url = os.path.join(self._html_url, "static", "img", "no-image.png") # Render archive suites index page self.render_template("suites_index.html", export_dir, "index.html", suites=self._suites_data.keys()) # TODO: Remove old HTML files for suite_name in self._suites_data: suite = self._suites_data[suite_name] export_dir = os.path.join(self._export_dir, "html", suite_name) suite_error_count = 0 suite_warning_count = 0 suite_info_count = 0 suite_metainfo_count = 0 for component in suite['components']: issue_summaries = dict() mdata_summaries = dict() export_dir_section = os.path.join(self._export_dir, "html", suite_name, component) export_dir_issues = os.path.join(export_dir_section, "issues") export_dir_metainfo = os.path.join(export_dir_section, "metainfo") error_count = 0 warning_count = 0 info_count = 0 metainfo_count = 0 hint_pages = dict() cpt_pages = dict() for arch in suite['architectures']: h_fname = os.path.join(dep11_hintsdir, suite_name, component, "DEP11Hints_%s.yml.gz" % (arch)) hints_data = None if os.path.isfile(h_fname): f = gzip.open(h_fname, 'r') hints_data = yaml.safe_load_all(f.read()) f.close() d_fname = os.path.join(dep11_minfodir, suite_name, component, "Components-%s.yml.gz" % (arch)) dep11_data = None if os.path.isfile(d_fname): f = gzip.open(d_fname, 'r') dep11_data = yaml.safe_load_all(f.read()) f.close() pkg_index = read_packages_dict_from_file(self._archive_root, suite_name, component, arch) if hints_data: for hdata in hints_data: pkg_name = hdata['Package'] pkg_id = hdata.get('PackageID') if not pkg_id: pkg_id = pkg_name pkg = pkg_index.get(pkg_name) maintainer = None if pkg: maintainer = pkg['maintainer'] if not maintainer: maintainer = "Unknown" if not issue_summaries.get(maintainer): issue_summaries[maintainer] = dict() hints_raw = hdata.get('Hints', list()) # expand all hints to show long descriptions errors = list() warnings = list() infos = list() for hint in hints_raw: ehint = self._expand_hint(hint) severity = ehint['severity'] if severity == "info": infos.append(ehint) elif severity == "warning": warnings.append(ehint) else: errors.append(ehint) if not hint_pages.get(pkg_name): hint_pages[pkg_name] = list() # we fold multiple architectures with the same issues into one view pkid_noarch = pkg_id if "/" in pkg_id: pkid_noarch = pkg_id[:pkg_id.rfind("/")] pcid = "" if hdata.get('ID'): pcid = "%s: %s" % (pkid_noarch, hdata.get('ID')) else: pcid = pkid_noarch page_data = {'identifier': pcid, 'errors': errors, 'warnings': warnings, 'infos': infos, 'archs': [arch]} try: l = hint_pages[pkg_name] index = next(i for i, v in enumerate(l) if equal_dicts(v, page_data, ['archs'])) hint_pages[pkg_name][index]['archs'].append(arch) except StopIteration: hint_pages[pkg_name].append(page_data) # add info to global issue count error_count += len(errors) warning_count += len(warnings) info_count += len(infos) # add info for global index if not issue_summaries[maintainer].get(pkg_name): issue_summaries[maintainer][pkg_name] = {'error_count': len(errors), 'warning_count': len(warnings), 'info_count': len(infos)} if dep11_data: for mdata in dep11_data: pkg_name = mdata.get('Package') if not pkg_name: # we probably hit the header continue pkg = pkg_index.get(pkg_name) maintainer = None if pkg: maintainer = pkg['maintainer'] if not maintainer: maintainer = "Unknown" if not mdata_summaries.get(maintainer): mdata_summaries[maintainer] = dict() # ugly hack to have the screenshot entries linked #if mdata.get('Screenshots'): # sshot_baseurl = os.path.join(self._dep11_url, component) # for i in range(len(mdata['Screenshots'])): # url = mdata['Screenshots'][i]['source-image']['url'] # url = "%s" % (os.path.join(sshot_baseurl, url), url) # mdata['Screenshots'][i]['source-image']['url'] = Markup(url) # thumbnails = mdata['Screenshots'][i]['thumbnails'] # for j in range(len(thumbnails)): # url = thumbnails[j]['url'] # url = "%s" % (os.path.join(sshot_baseurl, url), url) # thumbnails[j]['url'] = Markup(url) # mdata['Screenshots'][i]['thumbnails'] = thumbnails mdata_yml = dict_to_dep11_yaml(mdata) mdata_yml = self._highlight_yaml(mdata_yml) cid = mdata.get('ID') # try to find an icon for this component (if it's a GUI app) icon_url = None if mdata['Type'] == 'desktop-app' or mdata['Type'] == "web-app": icon_name = mdata['Icon'].get("cached") cptgid = build_cpt_global_id(cid, mdata.get('X-Source-Checksum')) if icon_name and cptgid: icon_fname = os.path.join(component, cptgid, "icons", "64x64", icon_name) if os.path.isfile(os.path.join(media_dir, icon_fname)): icon_url = os.path.join(self._dep11_url, icon_fname) else: icon_url = noimage_url else: icon_url = noimage_url else: icon_url = os.path.join(self._html_url, "static", "img", "cpt-nogui.png") if not cpt_pages.get(pkg_name): cpt_pages[pkg_name] = list() page_data = {'cid': cid, 'mdata': mdata_yml, 'icon_url': icon_url, 'archs': [arch]} try: l = cpt_pages[pkg_name] index = next(i for i, v in enumerate(l) if equal_dicts(v, page_data, ['archs'])) cpt_pages[pkg_name][index]['archs'].append(arch) except StopIteration: cpt_pages[pkg_name].append(page_data) # increase valid metainfo count metainfo_count += 1 # check if we had this package, and add to summary pksum = mdata_summaries[maintainer].get(pkg_name) if not pksum: pksum = dict() if pksum.get('cids'): if not cid in pksum['cids']: pksum['cids'].append(cid) else: pksum['cids'] = [cid] mdata_summaries[maintainer][pkg_name] = pksum if not dep11_data and not hints_data: log.warning("Suite %s/%s/%s does not contain DEP-11 data or issue hints.", suite_name, component, arch) # now write the HTML pages with the previously collected & transformed issue data for pkg_name, entry_list in hint_pages.items(): # render issues page self.render_template("issues_page.html", export_dir_issues, "%s.html" % (pkg_name), package_name=pkg_name, entries=entry_list, suite=suite_name, section=component) # render page with all components found in a package for pkg_name, cptlist in cpt_pages.items(): # render metainfo page self.render_template("metainfo_page.html", export_dir_metainfo, "%s.html" % (pkg_name), package_name=pkg_name, cpts=cptlist, suite=suite_name, section=component) # Now render our issue index page self.render_template("issues_index.html", export_dir_issues, "index.html", package_summaries=issue_summaries, suite=suite_name, section=component) # ... and the metainfo index page self.render_template("metainfo_index.html", export_dir_metainfo, "index.html", package_summaries=mdata_summaries, suite=suite_name, section=component) validate_result = "Validation was not performed." if dep11_data: # do format validation validator = DEP11Validator() ret = validator.validate_file(d_fname) if ret: validate_result = "No errors found." else: validate_result = "" for issue in validator.issue_list: validate_result += issue.replace("FATAL", "FATAL")+"
    \n" # sum up counts for suite statistics suite_metainfo_count += metainfo_count suite_error_count += error_count suite_warning_count += warning_count suite_info_count += info_count # calculate statistics for this component count = metainfo_count + error_count + warning_count + info_count valid_perc = 100/count*metainfo_count if count > 0 else 0 error_perc = 100/count*error_count if count > 0 else 0 warning_perc = 100/count*warning_count if count > 0 else 0 info_perc = 100/count*info_count if count > 0 else 0 # Render our overview page self.render_template("section_overview.html", export_dir_section, "index.html", suite=suite_name, section=component, valid_percentage=valid_perc, error_percentage=error_perc, warning_percentage=warning_perc, info_percentage=info_perc, metainfo_count=metainfo_count, error_count=error_count, warning_count=warning_count, info_count=info_count, validate_result=validate_result) # calculate statistics for this suite count = suite_metainfo_count + suite_error_count + suite_warning_count + suite_info_count valid_perc = 100/count*suite_metainfo_count if count > 0 else 0 error_perc = 100/count*suite_error_count if count > 0 else 0 warning_perc = 100/count*suite_warning_count if count > 0 else 0 info_perc = 100/count*suite_info_count if count > 0 else 0 # Render archive components index/overview page self.render_template("sections_index.html", export_dir, "index.html", sections=suite['components'], suite=suite_name, valid_percentage=valid_perc, error_percentage=error_perc, warning_percentage=warning_perc, info_percentage=info_perc, metainfo_count=suite_metainfo_count, error_count=suite_error_count, warning_count=suite_warning_count, info_count=suite_info_count) # Copy the static files target_static_dir = os.path.join(self._export_dir, "html", "static") shutil.rmtree(target_static_dir, ignore_errors=True) shutil.copytree(os.path.join(self._template_dir, "static"), target_static_dir) def main(): parser = OptionParser() (options, args) = parser.parse_args() if len(args) == 0: print("You need to specify a command!") sys.exit(1) command = args[0] # configure logging log_level = log.INFO if os.environ.get("DEBUG"): log_level = log.DEBUG log.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', level=log_level) if command == "process": if len(args) != 3: print("Invalid number of arguments: You need to specify a DEP-11 data dir and a suite.") sys.exit(1) gen = DEP11Generator() ret = gen.initialize(args[1]) if not ret: print("Initialization failed, can not continue.") sys.exit(2) gen.process_suite(args[2]) elif command == "cleanup": if len(args) != 2: print("Invalid number of arguments: You need to specify a DEP-11 data dir.") sys.exit(1) gen = DEP11Generator() ret = gen.initialize(args[1]) if not ret: print("Initialization failed, can not continue.") sys.exit(2) gen.expire_cache() elif command == "update-html": if len(args) != 2: print("Invalid number of arguments: You need to specify a DEP-11 data dir.") sys.exit(1) hgen = HTMLGenerator() ret = hgen.initialize(args[1]) if not ret: print("Initialization failed, can not continue.") sys.exit(2) hgen.update_html() elif command == "remove-processed": if len(args) != 3: print("Invalid number of arguments: You need to specify a DEP-11 data dir and suite.") sys.exit(1) gen = DEP11Generator() ret = gen.initialize(args[1]) if not ret: print("Initialization failed, can not continue.") sys.exit(2) gen.remove_processed(args[2]) else: print("Run with --help for a list of available command-line options!") if __name__ == "__main__": apt_pkg.init() main() appstream-dep11-0.4.0/scripts/dep11-validate000077500000000000000000000030561261747313700205540ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import sys from dep11.validate import DEP11Validator from optparse import OptionParser def main(): parser = OptionParser() parser.add_option("--no-color", action="store_true", dest="no_color", default=False, help="don'r print colored output") (options, args) = parser.parse_args() if len(args) < 1: print("You need to specify a file to validate!") sys.exit(4) fname = args[0] validator = DEP11Validator() ret = validator.validate_file(fname) validator.print_issues() if ret: msg = "Validation successful." else: msg = "Validation failed!" if options.no_color: print(msg) elif ret: print('\033[92m' + msg + '\033[0m') else: print('\033[91m' + msg + '\033[0m') if not ret: sys.exit(1) if __name__ == "__main__": main() appstream-dep11-0.4.0/setup.py000077500000000000000000000027351261747313700161560ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (C) 2014-2015 Matthias Klumpp # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3.0 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. import os import sys from setuptools import setup, find_packages data_target = os.path.join(sys.prefix, "share", "dep11") data_files = list() for root, dirs, files in os.walk("data/"): for fname in files: tdir = root.replace("data/", "") data_files.append( (os.path.join(data_target, tdir), [os.path.join(root, fname)]) ) setup(name = 'dep11', version = '0.2', description = 'DEP-11 metadata tools for Debian', url = 'https://github.com/ximion/dep11', # TODO: Move that to Debian infrastructure soon author = 'Matthias Klumpp', author_email = 'mak@debian.org', license = 'LGPL-3+', packages = ['dep11'], scripts = ['scripts/dep11-generator', 'scripts/dep11-validate'], data_files=data_files, zip_safe = False, )