first commit

This commit is contained in:
Ward 2023-02-23 22:05:14 +02:00
commit 42a42d1921
72 changed files with 10272 additions and 0 deletions

7
archive/.cz.toml Normal file
View File

@ -0,0 +1,7 @@
[tool.commitizen]
name = "cz_conventional_commits"
version = "1.5.2"
tag_format = "$version"
version_files = [
'repolib/__version__.py:__version__'
]

20
archive/.readthedocs.yml Normal file
View File

@ -0,0 +1,20 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF
formats:
- pdf
- epub
- htmlzip

13
archive/.travis.yml Normal file
View File

@ -0,0 +1,13 @@
dist: bionic
addons:
apt:
packages:
- libdbus-1-dev
language: python
python: "3.8"
install:
- pip install -r requirements.txt
scripts:
- pytest --pylint

675
archive/LICENSE Normal file
View File

@ -0,0 +1,675 @@
### GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
### Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom
to share and change all versions of a program--to make sure it remains
free software for all its users. We, the Free Software Foundation, use
the GNU General Public License for most of our software; it applies
also to any other work released this way by its authors. You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you
have certain responsibilities if you distribute copies of the
software, or if you modify it: responsibilities to respect the freedom
of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the
manufacturer can do so. This is fundamentally incompatible with the
aim of protecting users' freedom to change the software. The
systematic pattern of such abuse occurs in the area of products for
individuals to use, which is precisely where it is most unacceptable.
Therefore, we have designed this version of the GPL to prohibit the
practice for those products. If such problems arise substantially in
other domains, we stand ready to extend this provision to those
domains in future versions of the GPL, as needed to protect the
freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish
to avoid the special danger that patents applied to a free program
could make it effectively proprietary. To prevent this, the GPL
assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
### TERMS AND CONDITIONS
#### 0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of
an exact copy. The resulting work is called a "modified version" of
the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user
through a computer network, with no transfer of a copy, is not
conveying.
An interactive user interface displays "Appropriate Legal Notices" to
the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
#### 1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
#### 2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such
circumvention is effected by exercising rights under this License with
respect to the covered work, and you disclaim any intention to limit
operation or modification of the work as a means of enforcing, against
the work's users, your or third parties' legal rights to forbid
circumvention of technological measures.
#### 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
#### 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these
conditions:
- a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
- b) The work must carry prominent notices stating that it is
released under this License and any conditions added under
section 7. This requirement modifies the requirement in section 4
to "keep intact all notices".
- c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
- d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
#### 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
- a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
- b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the Corresponding
Source from a network server at no charge.
- c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
- d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
- e) Convey the object code using peer-to-peer transmission,
provided you inform other peers where the object code and
Corresponding Source of the work are being offered to the general
public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal,
family, or household purposes, or (2) anything designed or sold for
incorporation into a dwelling. In determining whether a product is a
consumer product, doubtful cases shall be resolved in favor of
coverage. For a particular product received by a particular user,
"normally used" refers to a typical or common use of that class of
product, regardless of the status of the particular user or of the way
in which the particular user actually uses, or expects or is expected
to use, the product. A product is a consumer product regardless of
whether the product has substantial commercial, industrial or
non-consumer uses, unless such uses represent the only significant
mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to
install and execute modified versions of a covered work in that User
Product from a modified version of its Corresponding Source. The
information must suffice to ensure that the continued functioning of
the modified object code is in no case prevented or interfered with
solely because modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or
updates for a work that has been modified or installed by the
recipient, or for the User Product in which it has been modified or
installed. Access to a network may be denied when the modification
itself materially and adversely affects the operation of the network
or violates the rules and protocols for communication across the
network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
#### 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
- a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
- b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
- c) Prohibiting misrepresentation of the origin of that material,
or requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
- d) Limiting the use for publicity purposes of names of licensors
or authors of the material; or
- e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
- f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions
of it) with contractual assumptions of liability to the recipient,
for any liability that these contractual assumptions directly
impose on those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the
above requirements apply either way.
#### 8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
#### 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run
a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
#### 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
#### 11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned
or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within the
scope of its coverage, prohibits the exercise of, or is conditioned on
the non-exercise of one or more of the rights that are specifically
granted under this License. You may not convey a covered work if you
are a party to an arrangement with a third party that is in the
business of distributing software, under which you make payment to the
third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties
who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
#### 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under
this License and any other pertinent obligations, then as a
consequence you may not convey it at all. For example, if you agree to
terms that obligate you to collect a royalty for further conveying
from those to whom you convey the Program, the only way you could
satisfy both those terms and this License would be to refrain entirely
from conveying the Program.
#### 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
#### 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions
of the GNU 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 Program
specifies that a certain numbered version of the GNU General Public
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that numbered version or
of any later version published by the Free Software Foundation. If the
Program does not specify a version number of the GNU General Public
License, you may choose any version ever published by the Free
Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to
choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
#### 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
#### 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
#### 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
### How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively state
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper
mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands \`show w' and \`show c' should show the
appropriate parts of the General Public License. Of course, your
program's commands might be different; for a GUI interface, you would
use an "about box".
You should also get your employer (if you work as a programmer) or
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. For more information on this, and how to apply and follow
the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your
program into proprietary programs. If your program is a subroutine
library, you may consider it more useful to permit linking proprietary
applications with the library. If this is what you want to do, use the
GNU Lesser General Public License instead of this License. But first,
please read <https://www.gnu.org/licenses/why-not-lgpl.html>.

157
archive/LICENSE.LESSER Normal file
View File

@ -0,0 +1,157 @@
### GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
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.

167
archive/README.rst Normal file
View File

@ -0,0 +1,167 @@
=======
RepoLib
=======
RepoLib is a Python library and CLI tool-set for managing your software
system software repositories. It's currently set up to handle APT repositories
on Debian-based linux distributions.
RepoLib is intended to operate on DEB822-format sources. It aims to provide
feature parity with software-properties for most commonly used functions.
Documentation
=============
Documentation is available online at `Read The Docs <https://repolib.rtfd.io/>`_.
Basic CLI Usage
---------------
RepoLib includes a CLI program for managing software repositories,
:code:`apt-manage`
.
Usage is divided into subcommands for most tasks. Currently implemented commands
are:
apt-manage add # Adds repositories to the system
apt-manage list # Lists configuration details of repositories
Additional information is available with the built-in help:
apt-manage --help
Add
^^^
Apt-manage allows entering a URL for a repository, a complete debian line, or a
Launchpad PPA shortcut (e.g. "ppa:user/repo"). It also adds signing keys for PPA
style repositories automatically.
List
^^^^
With no options, it outputs a list of the currently configured repositories on
the system (all those found in
:code:`/etc/apt/sources.list.d/`
. With a configured repository as an argument, it outputs the configuration
details of the specified repository.
Remove
^^^^^^
Accepts one repository as an argument. Removes the specified repository.
NOTE: The system repository (/etc/at/sources.list.d/system.sources) cannot be
removed.
Source
^^^^^^
Allows enabling or disabling source code for the given repository.
Modify
^^^^^^
Allows changing configuration details of a given repository
Installation
============
From System Package Manager
---------------------------
If your operating system packages repolib, you can install it by running::
sudo apt install python3-repolib
Uninstall
^^^^^^^^^
To uninstall, simply do::
sudo apt remove python3-repolib
From PyPI
---------
Repolib is available on PyPI. You can install it for your current user with::
pip3 install repolib
Alternatively, you can install it system-wide using::
sudo pip3 install repolib
Uninstall
^^^^^^^^^
To uninstall, simply do::
sudo pip3 uninstall repolib
From Git
--------
First, clone the git repository onto your local system::
git clone https://github.com/isantop/repolib
cd repolib
Debian
------
On debian based distributions, you can build a .deb package locally and install
it onto your system. You will need the following build-dependencies:
* debhelper (>=11)
* dh-python
* python3-all
* python3-setuptools
You can use this command to install these all in one go::
sudo apt install debhelper dh-python python3-all python3-setuptools
Then build and install the package::
debuild -us -uc
cd ..
sudo dpkg -i python3-repolib_*.deb
Uninstall
^^^^^^^^^
To uninstall, simply do::
sudo apt remove python3-repolib
setuptools setup.py
-------------------
You can build and install the package using python3-setuptools. First, install
the dependencies::
sudo apt install python3-all python3-setuptools
Then build and install the package::
sudo python3 ./setup.py install
Uninstall
^^^^^^^^^
You can uninstall RepoLib by removing the following files/directories:
* /usr/local/lib/python3.7/dist-packages/repolib/
* /usr/local/lib/python3.7/dist-packages/repolib-\*.egg-info
* /usr/local/bin/apt-manage
This command will remove all of these for you::
sudo rm -r /usr/local/lib/python3.7/dist-packages/repolib* /usr/local/bin/apt-manage

18
archive/SECURITY.md Normal file
View File

@ -0,0 +1,18 @@
# Security Policy
## Supported Versions
The following versions of RepoLib currently receive updates for security:
| Version | Supported |
| ------- | ------------------------ |
| 1.1.x | :heavy_check_mark:
| 1.0.x | :heavy_exclamation_mark: |
Please note that only the current supported patch version of each release
is supported.
## Reporting a Vulnerability
When Filing an issue for a potential security vulnerability, please be sure
to include a `[SECURITY]` tag in the issue title.

276
archive/TESTING.md Normal file
View File

@ -0,0 +1,276 @@
## Testing
The following components of RepoLib should be tested with each revision:
### CLI - apt-manage command
The `apt-manage` command should be tested every single revision to ensure that
it is working correctly. This will also generally test that changes to the
underlying library have not affected anything.
#### Adding repositories
Adding repositories should be tested.
1. Test Adding PPAs
```
sudo apt-manage -b add ppa:system76/proposed
```
Verify that the information output and the resulting deb lines (at the end of
the output) appear correct for the given PPA.
```
sudo apt-manage add -s ppa:system76/proposed
```
Verify that the `deb-src` is included in the `Types` field, then remove the
repository: `sudo apt-manage remove ppa-system76-proposed`
```
sudo apt-manage add -d ppa:system76/proposed
```
Verify that the `Enabled` field is set to `no`, then remove the
repository: `sudo apt-manage remove ppa-system76-proposed`
```
sudo apt-manage add ppa:system76/proposed
```
Verify that the command asks for verification before completing and displays
information about the PPA (similar to `add-apt-repository`). Verify that the
command fetches the signing key and adds it to the system. Verify that the
the correct `.list` file is added to `/etc/apt/sources.list.d`, then remove the
repository: `sudo apt-manage remove ppa-system76-proposed`
2. Test adding deb repositories
```
sudo apt-manage add --format list deb http://example.com/ubuntu focal main
```
Verify that the added repository matches the given input, and that there is a
commented-out `deb-src` repository with it. Ensure that the added repository
file ends in `.list` and that the contents match legacy Deb format.
Remove the repository with `sudo apt-manage remove
3. Test adding URLs
```
sudo apt-manage add --format list http://example.com/ubuntu
```
Verify that the repository is correctly expanded to include the `deb` at the
beginning, and the correct `{RELEASE} main` suites and components, where
{RELEASE} matches the current version codename. Verify that a matching `deb-src`
entry is added as well in the command output.
4. Test adding Pop Development repositories
```
sudo apt-manage add popdev:master
```
Verify that the repository details are correct, that the `Signed-by` field
points to `/etc/apt/keyrings/popdev-archive-keyring.gpg`, that the key file
exists at that path. Then, delete the repostiory:
```
sudo apt-manage remove popdev-master
```
#### Test Listing Details
1. Test that all repos are listed
```
apt-manage list
```
Verify that all configured repositories added to `/etc/apt/sources.list.d` are
printed in the command output.
2. Test that details for all repositories are listed
```
apt-manage list -a
```
Verify that the specific configuration for each repository listed in step 1 is
presented in the output.
3. Test that details for a specific repository are listed
```
apt-manage list system
```
Verify that the detailed configuration for only the specified repository is
listed in the output.
#### Removing repositories
1. Test cancelling removal
```
sudo apt-manage remove example-com-ubuntu
sudo apt-manage remove example-com-ubuntu
sudo apt-manage remove example-com-ubuntu
```
On the first run, enter 'n' and press enter. Verify that the source is not
removed using `apt-manage list`.
On the second run, simply press enter. Verify that the source is not removed by
using `apt-manage list`.
On the third run, enter 'v' and press enter. Verify that the source is not removed by
using `apt-manage list`.
2. Test removing sources
```
sudo apt-manage remove example-com-ubuntu
```
Enter 'y'. Verify that the source is removed using `apt-manage list`.
3. Verify protection of system sources
```
sudo apt-manage remove system
```
Verify that the command returns an error and that no action is taken (even if a
system.sources file does not exist) using `apt-manage list`
#### Modifying a repository
##### Setup
Add a testing repository:
```
sudo apt-manage add popdev:master
apt-manage list popdev-master
```
Verify that the details are correct in the output.
1. Change Repository Name
```
sudo apt-manage modify popdev-master --name 'Testing Repo'
apt-manage list popdev-master
```
Verify that the name was updated in the final output to match the given input
`Testing Repo`
2. Disable Repository
```
sudo apt-manage modify popdev-master --disable
apt-manage list popdev-master
```
Ensure that the repository is now listed as `Enabled: no`.
3. Add/Remove URI
```
sudo apt-manage modify popdev-master --add-uri http://example.com/
apt-manage list popdev-master
```
Ensure that the `http://example.com` URI was added to the source.
```
sudo apt-manage modify popdev-master --remove-uri http://example.com
apt-manage list popdev-master
```
Ensure that the `http://example.com` URI was removed.
4. Add/Remove Suite
```
sudo apt-manage modify popdev-master --add-suite xenial
apt-manage list popdev-master
```
Ensure that the suite `xenial` was added to the source.
```
sudo apt-manage modify popdev-master --remove-suite xenial
apt-manage list popdev-master
```
Ensure that the suite `xenial` was removed from the source.
5. Add/Remove Components
```
sudo apt-manage modify popdev-master --add-component 'universe multiverse'
apt-manage list popdev-master
```
Ensure that the components `universe` and `multiverse` were added to the source.
```
sudo apt-manage modify popdev-master --remove-component 'main multiverse'
apt-manage list popdev-master
```
Ensure that the components `main` and `multiverse` were removed from the source.
#### Enabling/Disabling Source Code
##### Setup
Add a testing repository:
```
apt-manage list popdev-master
```
Verify that the details are correct in the output and that `Types:` is just
`deb`.
1. Enable source code for one repo
```
sudo apt-manage modify --source-enable popdev-master
apt-manage list popdev-master
```
Verify that the `Types:` is now `deb deb-src`.
2. Disable source code for one repo
```
sudo apt-manage modify --source-disable popdev-master
apt-manage list popdev-master
```
Verify that the `Types:` is now just `deb`.
Finally, remove the testing repository:
```
sudo apt-manage remove popdev-master
```
### Installation/upgrading (packaging tests)
_This section is to test the installation behavior of the Debian package,
and doesn't need to be run for changes to repolib itself._
**Confirm add-apt-repository is installed when software-properties-common is not installed:**
- [ ] Make sure `software-properties-common` is not installed.
- [ ] Remove `python3-repolib` with `sudo dpkg -r --force-all python3-repolib`.
- [ ] Ensure `/usr/bin/add-apt-repository` doesn't exist (if it does, remove it.)
- [ ] Install `python3-repolib` (can be done with `sudo apt install -f`).
- [ ] Ensure `/usr/bin/add-apt-repository` exists and is a link to `/usr/lib/repolib/add-apt-repository`.
**Confirm add-apt-repository is not installed when software-properties-common is installed:**
- [ ] Remove `python3-repolib` with `sudo dpkg -r --force-all python3-repolib`.
- [ ] Ensure `/usr/bin/add-apt-repository` doesn't exist (if it does, remove it.)
- [ ] Install `software-properties-common`, confirm `/usr/bin/add-apt-repository` now exists and is not a link.
- Can be done with `apt download software-properties-common python3-software-properties`
followed by `dpkg -i` on the downloaded files.
- [ ] Install `python3-repolib` again, confirm `/usr/bin/add-apt-repository` still isn't a link.
**Confirm add-apt-repository is installed when software-properties-common is removed:**
- [ ] Remove `software-properties-common` and confirm that `/usr/bin/add-apt-repository` still exists and is now a link.

172
archive/apt-manage-spec.rst Normal file
View File

@ -0,0 +1,172 @@
===================
Spec for apt-manage
===================
apt-manage is the CLI interface to manage software sources using RepoLib. It is
written in Python and shipped with RepoLib
Commands
========
There are several commands provided by apt-manage::
add
remove
modify
list
source
add
---
The add command is used for adding new software sources to the system. It
requires root. It has the following options::
--disable, -d
--source-code, -s
--expand, -e
--disable
^^^^^^^^^
The --disable option (short form -d) will add the source and disable it
immediately
--source-code
^^^^^^^^^^^^^
The --source-code option (short form -s) will enable source code for the
new source as well as binary packages.
--expand
^^^^^^^^
The --expand options (short form -e) will show expanded details about the source
prior to adding it.
remove
------
The remove command will remove the selected source. It has no options. Note that
the system sources cannot be removed. This requires root.
modify
------
The modify command will allow making modifications to the specified repository.
It requires providing a repository to modify. It requires root. It accepts the
following options::
--enable, -e
--disable, -d
--add-suite
--remove-suite
--add-component
--remove-component
--add-uri
--remove-uri
--add-option
--remove-option
--enable
^^^^^^^^
The enable option (short form -e) sets the source to be enabled.
--disable
^^^^^^^^^
The disable option (short form -d) sets the source to be disabled.
--add-suite
^^^^^^^^^^^
The --add-suite option attempts to add the specified suite or suites to the
source. If a given suite is already added to the source, it is ignored. Note
that legacy deb sources cannot have multiple suites.
--remove-suite
^^^^^^^^^^^^^^
The --remove-suite option attempts to remove the specified suite or suites from
the source. If a given suite is not added to the source, it is ignored. The last
configured suite on a source cannot be removed.
--add-component
^^^^^^^^^^^^^^^
The --add-component option attempts to add the specified component or components
to the source. If a given component is already added to the source, it is
ignored.
--remove-component
^^^^^^^^^^^^^^^^^^
The --remove-component option attempts to remove the specified component or
components from the source. If a given component is not added to the source, it
is ignored. The last configured component on a source cannot be removed.
--add-uri
^^^^^^^^^
The --add-uri option attempts to add the specified URI or URIs to the source. If
a given URI is already added to the source, it is ignored. Note that legacy deb
sources cannot have multiple URIs.
--remove-uri
^^^^^^^^^^^^
The --remove-uri option attempts to remove the specified URI or URIs from the
source. If a given URI is not added to the source, it is ignored. The last
configured URI on a source cannot be removed.
--add-option
^^^^^^^^^^^^
The --add-option option attempts to add the given option or options as well as
values to the source. If a given option is already added to the source, it is
ignored.
--remove-option
^^^^^^^^^^^^^^^
The --remove-option option attempts to remove the given option or options from
the source. If a given option isn't added to the source, it is ignored.
list
----
The list command lists available software sources as well as details about
sources. With no further options, it lists all configured sources. With a
configured source, it lists details about the specified source. It has the
following option::
--verbose, -v
--legacy, -l
--verbose
^^^^^^^^^
The --verbose option (short form -v) lists all details for all configured
software sources. It has no effect if a specific source is provided.
source
------
The source command allows enabling or disabling source code in configured
sources. If a configured source is provided, this command will affect that
source. If no sources are provided, this command will affect all sources on the
system. Without options, it will list the status for source code packages. It
also accepts the following options, which require root::
--enable, -e
--disable, -d
--enable
^^^^^^^^
The --enable option (short form -e) will enable source code packages.
--disable
^^^^^^^^^
The --disable option (short form -d) will disable source code packages

194
archive/bin/add-apt-repository Executable file
View File

@ -0,0 +1,194 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2020, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
#pylint: disable=invalid-name
# Pylint will complain about our module name not being snake_case, however this
# is a command rather than a python module, and thus this is correct anyway.
import argparse
import os
import subprocess
import repolib
system_codename = repolib.util.DISTRO_CODENAME
system_components = [
'main',
'universe',
'multiverse',
'restricted'
]
system_suites = [
system_codename,
f'{system_codename}-updates',
f'{system_codename}-security',
f'{system_codename}-backports',
f'{system_codename}-proposed',
'updates',
'security',
'backports',
'proposed',
]
def get_args():
parser = argparse.ArgumentParser(
prog='add-apt-repository',
description=(
'add-apt-repository is a script for adding apt sources.list entries.'
'\nThis command has been deprecated in favor of `apt-manage`. See '
'`apt-manage --help` for more information.'
)
)
parser.add_argument(
'sourceline',
metavar='<sourceline>'
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
'-m',
'--massive-debug',
dest='debug',
action='store_true',
help='Print a lot of debug information to the command line'
)
group.add_argument(
'-r',
'--remove',
action='store_true',
help='remove repository from sources.list.d directory'
)
group.add_argument(
'-s',
'--enable-source',
dest='source',
action='store_true',
help='Allow downloading of source packages from the repository'
)
parser.add_argument(
'-y',
'--yes',
action='store_true',
help='Assum yes to all queries'
)
parser.add_argument(
'-n',
'--no-update',
dest='noupdate',
action='store_true',
help='Do not update package cache after adding'
)
parser.add_argument(
'-u',
'--update',
action='store_true',
help='Update package cache after adding (legacy option)'
)
parser.add_argument(
'-k',
'--keyserver',
metavar='KEYSERVER',
help='Legacy option, unused.'
)
return parser
parser = get_args()
args = parser.parse_args()
command = ['apt-manage']
if args.debug:
command.append('-bb')
sourceline = args.sourceline
run = True
remove = False
if sourceline in system_components:
command.append('modify')
command.append('system')
if not args.remove:
command.append('--add-component')
else:
command.append('--remove-component')
elif sourceline in system_suites:
command.append('modify')
command.append('system')
if not args.remove:
command.append('--add-suite')
else:
command.append('--remove-suite')
else:
if args.source:
command.append('source')
elif args.remove:
remove = True
command.append('remove')
else:
command.append('add')
if not args.yes:
command.append('--expand')
if not remove:
command.append(sourceline)
else:
sources = repolib.get_all_sources()
comp_source = repolib.DebLine(sourceline)
for source in sources:
if comp_source.uris[0] in source.uris:
name = str(source.filename.name)
name = name.replace(".list", "")
name = name.replace(".sources", "")
command.append(name)
run = True
if os.geteuid() != 0:
print('Error: must run as root')
run = False
if run:
subprocess.run(command)
if not args.noupdate:
subprocess.run(['apt', 'update'])
print('NOTE: add-apt-repository is deprecated in Pop!_OS. Use this instead:')
print_command = command.copy()
if '--expand' in print_command:
print_command.remove('--expand')
print(' '.join(print_command))

85
archive/bin/apt-manage Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2020, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
#pylint: disable=invalid-name
# Pylint will complain about our module name not being snake_case, however this
# is a command rather than a python module, and thus this is correct anyway.
import argparse
import logging
import os
import sys
import repolib
from repolib import command
SOURCES_DIR = '/etc/apt/sources.list.d'
def main(options=None):
""" Main function for apt-manage."""
# Set up Argument Parsing.
parser = repolib.command.parser
# Parse options
args = parser.parse_args()
if options:
args = parser.parse_args(options)
if not args.debug:
args.debug = 0
if args.debug > 2:
args.debug = 2
verbosity = {
0 : logging.WARN,
1 : logging.INFO,
2 : logging.DEBUG
}
log = logging.getLogger('apt-manage')
handler = logging.StreamHandler()
formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(verbosity[args.debug])
log.debug('Logging set up!')
repolib.set_logging_level(args.debug)
if not args.action:
args = parser.parse_args(sys.argv[1:] + ['list'])
log.debug('Arguments passed: %s', str(args))
log.debug('Got command: %s', args.action)
subcommand = args.action.capitalize()
command = getattr(repolib.command, subcommand)(log, args, parser)
result = command.run()
if not result:
sys.exit(1)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('')
sys.exit(130)

View File

@ -0,0 +1,42 @@
# Debian apt-manage completion
_apt_manage()
{
local cur prev words cword package
_init_completion -n ':=' || return
local special i
i=0
for (( i=0; i < ${#words[@]}-1; i++ )); do
if [[ ${words[i]} == @(add|list|modify|remove|key) ]]; then
special=${words[i]}
fi
done
if [[ -n $special ]]; then
case $special in
list|modify|remove|key)
COMPREPLY=( $( compgen -W '$(apt-manage list -n)' -- "$cur" ) )
return
;;
*)
;;
esac
fi
if [[ "$cur" == -* ]]; then
return
# COMPREPLY=( $(compgen -W '
# --help --disable --source-code --expand
# --verbose --legacy --no-names
# --enable --disable --name --add-suite --remove-suite
# --add-component --remove-component --add-uri --remove-uri
# ' -- "$cur") )
else
COMPREPLY=( $(compgen -W 'add list modify remove key' \
-- "$cur") )
fi
} &&
complete -F _apt_manage apt-manage

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<type>system</type>
<!-- Only root can own the service -->
<policy user="root">
<allow own="org.pop_os.repolib"/>
<allow send_destination="org.pop_os.repolib"/>
<allow receive_sender="org.pop_os.repolib"/>
</policy>
<policy group="adm">
<allow send_destination="org.pop_os.repolib"/>
<allow receive_sender="org.pop_os.repolib"/>
</policy>
<policy group="sudo">
<allow send_destination="org.pop_os.repolib"/>
<allow receive_sender="org.pop_os.repolib"/>
</policy>
</busconfig>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>Repoman</vendor>
<vendor_url>https://github.com/pop-os/repolib</vendor_url>
<icon_name>x-system-software-sources</icon_name>
<action id="org.pop_os.repolib.modifysources">
<description>Modifies a Debian Repository in the system software sources.</description>
<message>Authentication is required to change software sources.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>

View File

@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.pop_os.repolib
Exec=/usr/bin/python3 /usr/lib/repolib/service.py
User=root

316
archive/data/service.py Executable file
View File

@ -0,0 +1,316 @@
#!/usr/bin/python3
'''
Copyright 2020 Ian Santopietro (ian@system76.com)
This file is part of Repolib.
Repolib is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Repolib 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Repolib. If not, see <http://www.gnu.org/licenses/>.
'''
#pylint: skip-file
import shutil
import subprocess
import gi
from gi.repository import GObject, GLib
from pathlib import Path
import dbus
import dbus.service
import dbus.mainloop.glib
import time
import repolib
class RepolibException(dbus.DBusException):
_dbus_error_name = 'org.pop_os.repolib.RepolibException'
class PermissionDeniedByPolicy(dbus.DBusException):
_dbus_error_name = 'org.pop_os.repolib.PermissionDeniedByPolicy'
class AptException(Exception):
pass
class Repo(dbus.service.Object):
def __init__(self, conn=None, object_path=None, bus_name=None):
dbus.service.Object.__init__(self, conn, object_path, bus_name)
# These are used by PolKit to check privileges
self.dbus_info = None
self.polkit = None
self.enforce_polkit = True
try:
self.system_repo = repolib.SystemSource()
except:
self.system_repo = None
self.source = None
self.sources_dir = Path('/etc/apt/sources.list.d')
self.keys_dir = Path('/etc/apt/trusted.gpg.d')
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='as', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def add_apt_signing_key(self, cmd, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
print(cmd)
key_path = str(cmd.pop(-1))
with open(key_path, mode='wb') as keyfile:
try:
subprocess.run(cmd, check=True, stdout=keyfile)
except subprocess.CalledProcessError as e:
raise e
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='ss', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def install_signing_key(self, src, dest, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
shutil.copy2(src, dest)
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='s', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def delete_signing_key(self, src, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
key_path = Path(src)
key_path.unlink()
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='s', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def delete_prefs_file(self, src, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
prefs_path = Path(src)
prefs_path.unlink()
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='ss', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def output_prefs_to_disk(self, path, contents, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
full_path = Path(path)
with open(full_path, mode='w') as output_file:
output_file.write(contents)
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='ss', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def output_file_to_disk(self, filename, source, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
full_path = self.sources_dir / filename
with open(full_path, mode='w') as output_file:
output_file.write(source)
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='ss', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def backup_alt_file(self, alt_file, save_file, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
alt_path = self.sources_dir / alt_file
save_path = self.sources_dir / save_file
if alt_path.exists():
alt_path.rename(save_path)
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='s', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def delete_source_file(self, filename, sender=None, conn=None):
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
source_file = self.sources_dir / filename
source_file.unlink()
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='', out_signature='',
sender_keyword='sender', connection_keyword='conn'
)
def exit(self, sender=None, conn=None):
mainloop.quit()
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='b', out_signature='b',
sender_keyword='sender', connection_keyword='conn'
)
def set_system_source_code_enabled(self, enabled, sender=None, conn=None):
""" Enable or disable source code in the system source.
Arguments:
enabled (bool): The new state to set, True = Enabled.
"""
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
if self.system_repo:
self.system_repo.load_from_file()
new_types = [repolib.util.AptSourceType.BINARY]
if enabled:
new_types.append(repolib.util.AptSourceType.SOURCE)
self.system_repo.types = new_types
self.system_repo.save_to_disk()
return enabled
return False
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='sb', out_signature='b',
sender_keyword='sender', connection_keyword='conn'
)
def set_system_comp_enabled(self, comp, enable, sender=None, conn=None):
""" Enable or disable a component in the system source.
Arguments:
comp (str): the component to set
enable (bool): The new state to set, True = Enabled.
"""
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
if self.system_repo:
self.system_repo.load_from_file()
self.system_repo.set_component_enabled(component=comp, enabled=enable)
self.system_repo.save_to_disk()
return True
return False
@dbus.service.method(
"org.pop_os.repolib.Interface",
in_signature='sb', out_signature='b',
sender_keyword='sender', connection_keyword='conn'
)
def set_system_suite_enabled(self, suite, enable, sender=None, conn=None):
""" Enable or disable a suite in the system source.
Arguments:
suite (str): the suite to set
enable (bool): The new state to set, True = Enabled.
"""
self._check_polkit_privilege(
sender, conn, 'org.pop_os.repolib.modifysources'
)
if self.system_repo:
self.system_repo.load_from_file()
self.system_repo.set_suite_enabled(suite=suite, enabled=enable)
self.system_repo.save_to_disk()
return True
return False
@classmethod
def _log_in_file(klass, filename, string):
date = time.asctime(time.localtime())
ff = open(filename, "a")
ff.write("%s : %s\n" %(date,str(string)))
ff.close()
@classmethod
def _strip_source_line(self, source):
source = source.replace("#", "# ")
source = source.replace("[", "")
source = source.replace("]", "")
source = source.replace("'", "")
source = source.replace(" ", " ")
return source
def _check_polkit_privilege(self, sender, conn, privilege):
# from jockey
'''Verify that sender has a given PolicyKit privilege.
sender is the sender's (private) D-BUS name, such as ":1:42"
(sender_keyword in @dbus.service.methods). conn is
the dbus.Connection object (connection_keyword in
@dbus.service.methods). privilege is the PolicyKit privilege string.
This method returns if the caller is privileged, and otherwise throws a
PermissionDeniedByPolicy exception.
'''
if sender is None and conn is None:
# called locally, not through D-BUS
return
if not self.enforce_polkit:
# that happens for testing purposes when running on the session
# bus, and it does not make sense to restrict operations here
return
# get peer PID
if self.dbus_info is None:
self.dbus_info = dbus.Interface(conn.get_object('org.freedesktop.DBus',
'/org/freedesktop/DBus/Bus', False), 'org.freedesktop.DBus')
pid = self.dbus_info.GetConnectionUnixProcessID(sender)
# query PolicyKit
if self.polkit is None:
self.polkit = dbus.Interface(dbus.SystemBus().get_object(
'org.freedesktop.PolicyKit1',
'/org/freedesktop/PolicyKit1/Authority', False),
'org.freedesktop.PolicyKit1.Authority')
try:
# we don't need is_challenge return here, since we call with AllowUserInteraction
(is_auth, _, details) = self.polkit.CheckAuthorization(
('unix-process', {'pid': dbus.UInt32(pid, variant_level=1),
'start-time': dbus.UInt64(0, variant_level=1)}),
privilege, {'': ''}, dbus.UInt32(1), '', timeout=600)
except dbus.DBusException as e:
if e._dbus_error_name == 'org.freedesktop.DBus.Error.ServiceUnknown':
# polkitd timed out, connect again
self.polkit = None
return self._check_polkit_privilege(sender, conn, privilege)
else:
raise
if not is_auth:
Repo._log_in_file('/tmp/repolib.log','_check_polkit_privilege: sender %s on connection %s pid %i is not authorized for %s: %s' %
(sender, conn, pid, privilege, str(details)))
raise PermissionDeniedByPolicy(privilege)
if __name__ == '__main__':
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
name = dbus.service.BusName("org.pop_os.repolib", bus)
object = Repo(bus, '/Repo')
mainloop = GLib.MainLoop()
mainloop.run()

View File

@ -0,0 +1,70 @@
#compdef apt-manage
typeset -A opt_args
typeset -A opt_args
_arguments -C \
'1:cmd:->cmds' \
'2:sources:->source_lists' \
'*:: :->args' \
&& ret=0
case "$state" in
(cmds)
local commands; commands=(
'add'
'list'
'modify'
'remove'
'key'
)
_describe -t commands 'command' commands && ret=0
;;
(source_lists)
local sources
sources=( $(apt-manage list -n))
_describe -t sources 'source' sources && ret=0
;;
(args)
local arguments
arguments=(
# Main command
'--help'
# Add subcommand
'--disable'
'--source-code'
'--terse'
'--name'
'--identifier'
'--format'
'--skip-keys'
# Key Ssbcommand
'--path'
'--url'
'--ascii'
'--fingerprint'
'--keyserver'
'--remove'
# List subcommand
'--legacy'
'--verbose'
'--all'
'--file-names'
# Modify subcommand
'--enable'
'--source-enable'
'--source-disable'
'--add-uri'
'--add-suite'
'--add-component'
'--remove-uri'
'--remove-suite'
'--remove-component'
# Remove subcommand
'--assume-yes'
)
_describe -t arguments 'argument' arguments && ret=0
;;
esac
return 1

View File

@ -0,0 +1,16 @@
repolib for Debian
-----------------
# RepoLib
RepoLib is a Python library and CLI tool-set for managing your software
system software repositories. It's currently set up to handle APT repositories
on Debian-based linux distributions.
RepoLib is intended to operate on DEB822-format sources. It aims to provide
feature parity with software-properties for most commonly used functions.
-- Ian Santopietro <isantop@gmail.com> Mon, 29 Apr 2019 14:53:49 -0600

11
archive/debian/changelog Normal file
View File

@ -0,0 +1,11 @@
repolib (2.2.0~99pika2) kinetic; urgency=medium
* Remove stupid post scripts
-- Ward Nakchbandi <hotrod.master@hotmail.com> Fri, 09 Oct 2022 21:38:00 +0300
repolib (2.2.0~99pika1) kinetic; urgency=medium
* Initial Creation
-- Ward Nakchbandi <hotrod.master@hotmail.com> Fri, 09 Oct 2022 21:38:00 +0300

1
archive/debian/compat Normal file
View File

@ -0,0 +1 @@
11

43
archive/debian/control Normal file
View File

@ -0,0 +1,43 @@
Source: repolib
Section: python
Priority: optional
Maintainer: Ian Santopietro <isantop@gmail.com>
Build-Depends: debhelper (>= 11),
dh-python,
python3-all,
python3-dbus,
python3-debian,
python3-distro,
python3-gnupg,
python3-lazr.restfulclient,
python3-launchpadlib,
python3-setuptools,
python3-distutils,
python3-pytest
Standards-Version: 4.1.3
Homepage: https://github.com/isantop/repolib
XS-Python3-Version: >= 3.7
Vcs-Browser: https://github.com/isantop/repolib
Vcs-Git: https://github.com/isantop/repolib.git
Package: python3-repolib
Architecture: all
Depends: ${python3:Depends},
${misc:Depends},
apt,
lsb-release,
python3-dbus,
python3-debian,
python3-gnupg
Recommends: python3-lazr.restfulclient,
python3-launchpadlib
Description: Repository Management for APT in Python (Python 3)
RepoLib is a Python library and CLI tool-set for managing your software
system software repositories. It's currently set up to handle APT repositories
on Debian-based linux distributions.
.
RepoLib is intended to operate on DEB822-format sources. It aims to provide
feature parity with software-properties for most commonly used functions.
.
This package installs the library for Python 3.

68
archive/debian/copyright Normal file
View File

@ -0,0 +1,68 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: repolib
Source: <https://github.com/isantop/repolib>
Files: *
Copyright: 2019 Ian Santopietro <isantop@gmail.com>
License: BSD-2-Clause License
Copyright (c) 2019, Ian Santopietro
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Files: debian/*
Copyright: 2019 Ian Santopietro <isantop@gmail.com>
License: GPL-2+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This package 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 General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
Files: repolib/ppa.py
Copyright: (c) 2004-2009 Canonical Ltd., (c) 2019 Ian Santopietro <isantop@gmail.com>
License: GPL-2+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This package 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 General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".

24
archive/debian/rules Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/make -f
# See debhelper(7) (uncomment to enable)
# output every command that modifies files on the build system.
#export DH_VERBOSE = 1
export PYBUILD_NAME=repolib
export PYBUILD_OPTION=--test-pytest
%:
dh $@ --with python3 --buildsystem=pybuild
## Uncomment to disable testing during package builds
## NOTE for QA or Engineering Review: This should not be uncommented in a
## PR. If it is, DO NOT APPROVE THE PR!!!
# override_dh_auto_test:
# true
# If you need to rebuild the Sphinx documentation
# Add spinxdoc to the dh --with line
#override_dh_auto_build:
# dh_auto_build
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator

View File

@ -0,0 +1 @@
3.0 (native)

View File

@ -0,0 +1 @@
extend-diff-ignore = "^[^/]*[.]egg-info/"

1
archive/debian/triggers Normal file
View File

@ -0,0 +1 @@
interest /usr/bin/add-apt-repository

19
archive/docs/Makefile Normal file
View File

@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -0,0 +1,71 @@
==============
Adding Sources
==============
The ``add`` subcommand is used to add new repositories to the software sources.
You can specify a deb-line to configure into the system or a ``ppa:`` shortcut
to add the new source directly::
$ sudo apt-manage add deb http://apt.pop-os.org/ubuntu disco main
$ sudo apt-manage add ppa:system76/pop
If an internet connection is available, ``apt-manage`` will additionally attempt
to install the signing key for any ``ppa:`` shortcuts added.
Options for adding sources
==========================
Various options control adding sources to the system.
Source Code, Details, Disabling Sources
---------------------------------------
To enable source code for the added repository, use the ``--source-code`` flag::
$ apt-manage add --source-code ppa:system76/pop
The new source can be disabled upon adding it using the ``--disable`` flag::
$ apt-manage add --disable ppa:system76/pop
Details for the PPA are printed for review prior to adding the source by default.
This will print the generated configuration for the source as well as any
available details fetched for the source (typically only available for PPA
sources). To suppress this output, include ``--terse``.
Source File Format
------------------
The format which RepoLib saves the repository on disk in depends on the type of
repository being added, but regardless the ``--format`` flag can be used to
force either legacy ``.list`` format or modern ``.sources`` format::
apt-manage add popdev:master --format=list
apt-manage add ppa:system76/pop --format=sources
Names and Identifiers
---------------------
Names for PPA sources are automatically detected from Launchpad if an internet
connection is available. Otherwise they are automatically generated based on the
source type and details. Optionally, a name can be specified when the source is
added::
$ apt-manage add ppa:system76/pop --name "PPA for Pop_OS Software"
System-identifiers determine how the source is subsequently located within RepoLib and
on the system. It matches the filename for the source's configuration file. It
is automatically generated based on the source type, or can be specified
manually upon creation using the ``--identifier`` flag::
$ apt-manage add ppa:system76/pop --identifier pop-ppa
.. note::
Even though ``apt-manage`` allows modifcation or management of DEB822-format
sources, it does not currently support adding them to the system directly.
DEB822 sources can be manually added or added using third-party tools, and
``apt-manage`` will correctly operate on them subsequently.

View File

@ -0,0 +1,28 @@
==========
Apt Manage
==========
``apt-manage`` is a command line tool for managing your local software sources
using RepoLib. Run ``apt-manage`` standalone to get a listing of all of the
software repositories currently configured::
$ apt-manage
Configured sources:
system
pop-os-apps
ppa-system76-pop
``apt-manage`` operates on both DEB822-formated sources (located in the
``/etc/apt/sources.list.d/*.sources`` files) as well as using traditional
one-line format sources (in ``/etc/apt/sources.list.d/*.list`` files).
.. toctree::
:maxdepth 1
:caption: apt-manage Documentation
add
list
modify
remove
key

View File

@ -0,0 +1,78 @@
=====================
Managing Signing Keys
=====================
Signing keys are an important part of repository security and are generally
required to be used in repositories for all recent versions of APT. As previous
methods of handling Apt keys have been deprecated, Apt Manage provides easy
tools to use for managing signing keys for repositories in the ``key``
subcommand.
Most of the tools in the ``key`` subcommand are centered around adding a signing
key to a repository::
apt-manage key repo-id --fingerprint 63C46DF0140D738961429F4E204DD8AEC33A7AFF
Apt Manage supports adding keys from a variety of sources:
Existing Keyring Files, --name, --path
======================================
``--name`` sets the :ref:`signed_by` value of the existing repository to the
name of a file within the system key configuration directory::
apt-manage key popdev-master --name popdev
``--path`` sets the :ref:`signed_by` value of the existing repository to the
path of a file on disk::
apt-manage key popdev-master --path /etc/apt/keyrings/popdev-archive-keyring.gpg
Keyring Files Stored on the Internet, --url
===========================================
``--url`` will download a key file from the internet and install it into the
system, then set the repository to use that key::
apt-manage key popdev-master --url https://example.com/sigining-key.asc
Keys Stored on a Public Keyserver
=================================
``--fingerprint`` will fetch the specified fingerprint from a public keyserver.
By default, keys will be fetched from ``keyserver.ubuntu.com``, but any SKS
keyserver can be specified using the ``--keyserver=`` argument::
apt-manage key ppa-system76-pop \
--fingerprint=E6AC16572ED1AD6F96C7EBE01E5F8BBC5BEB10AE
apt-manage key popdev-master \
--fingerprint=63C46DF0140D738961429F4E204DD8AEC33A7AFF \
--keyserver=https://keyserver.example.com/
Adding ASCII-Armored Keys Directly, --ascii
===========================================
``--ascii`` Will take plain ascii data from the command line and add it to a new
keyring file, then set the repository to use that key::
apt-manage key popdev-master --ascii "$(/tmp/popdev-key.asc)"
Removing Keys
=============
Generally, manually removing keys is not necessary because removing a source
automatically removes the key (if it is the only source using that key). However,
If there is a need to remove a key manually (e.g. the signing key has changed
and must be re-added), then removal is supported::
apt-manage key popdev-master --remove
This will remove the key from the repository configuration and if no other
sources are using a particular key, it will also remove the keyring file from
disk.

View File

@ -0,0 +1,79 @@
=============================
Listing Configuration Details
=============================
To get a list of all of the sources configured on the system, use the ``list``
subcommand::
$ apt-manage list
Configured sources:
system - Pop_OS System Sources
pop-os-apps - Pop_OS Applications
ppa-system76-pop - Pop!_OS PPA
The sources are listed with the system source (if detected/configured) first,
followed by all DEB822-format sources detected first, then by all one-line
format sources. The system-identifier (used to identify sources in the system)
is listed at the beginning of the line, and the name of the source is listed
after.
Details of an individual source can be printed by specifying a source's
system-identifier::
$ apt-manage list ppa-system76-pop
Details for source ppa-system76-pop:
Name: Pop!_OS PPA
Enabled: yes
Types: deb deb-src
URIs: http://apt.pop-os.org/release
Suites: focal
Components: main
Listing all details of all sources at once
==========================================
Details about all sources can be listed at once using the ``--verbose`` flag::
$ apt-manage list --verbose
Configured sources:
system - Pop_OS System Sources
Name: Pop_OS System Sources
Enabled: yes
Types: deb deb-src
URIs: http://us.archive.ubuntu.com/ubuntu/
Suites: focal focal-security focal-updates focal-backports
Components: main restricted universe multiverse
pop-os-apps - Pop_OS Applications
Name: Pop_OS Applications
Enabled: yes
Types: deb
URIs: http://apt.pop-os.org/proprietary
Suites: focal
Components: main
ppa-system76-pop - Pop!_OS PPA
Name: Pop!_OS PPA
Enabled: yes
Types: deb deb-src
URIs: http://apt.pop-os.org/release
Suites: focal
Components: main
Note
^^^^
Passing the ``--verbose`` flag only applies to listing all sources. It has no
effect if a source is specified using the system-identifier; in that case, only
the specified source is printed. Additionally, if there are sources files which
contain errors, the ``--verbose`` flag will print details about them, including
the contents of the files and the stack trace of the exception which caused the
error.
Legacy sources.list entries
===========================
The contents of the system ``sources.list`` file can be appended to the end of
the output using the ``--legacy`` flag.

View File

@ -0,0 +1,82 @@
=================
Modifying Sources
=================
Modifications can be made to various configured sources using the ``modify``
subcommand.
Enabling/Disabling Sources: --enable | --disable
================================================
Sources can be disabled, which prevents software/updates from being installed
from the source but keeps it present in the system configuration for later use
or records for later. To disable a source, use ``--disable``::
$ apt-manage modify ppa-system76-pop --disable
To re-enable a source after it's been disabled, use ``--enable``::
$ apt-manage modify ppa-system76-pop --enable
Changing names of sources: --name
=================================
RepoLib allows setting up human-readable names for use in GUIs or other
user-facing contexts. To set or change a name of a source, use ``--name``::
$ apt-manage modify ppa-system76-pop --name "Pop_OS PPA"
Suites: --add-suite | --remove-suite
====================================
Suites for sources can be added or removed from the configuration. In one-line
sources, these are added with multiple lines, since each one-line source can
have only one suite each. DEB822 sources can have multiple suites.
To add a suite, use ``--add-suite``::
$ apt-manage modify ppa-system76-pop --add-suite groovy
Use ``--remove-suite`` to remove a suite::
$ apt-manage modify ppa-system76-pop --remove-suite focal
Components: --add-component | --remove-component
================================================
Both types of source format can have multiple components for each source. Note
that all components for one-line format sources will share all of a source's
components.
Components are managed similarly to suites::
$ apt-manage modify system --add-component universe
$ apt-manage modify system --remove-component restricted
URIs: --add-uri | --remove-uri
==============================
DEB822 sources may contain an arbitrary number of URIs. One-line sources require
an additional line for each individual URI added. All suites on a source are all
applied to all of the URIs equally.
URIs are managed similarly to both suites and components::
$ apt-manage modify system --add-uri http://apt.pop-os.org/ubuntu
$ apt-manage modify system --remove-uri http://us.archive.ubuntu.com/ubuntu
Notes
^^^^^
Multiple modifications may be applied on a single ``apt-manage modify`` calls::
$ apt-manage modify system --name "Pop_OS 20.10 System Sources" \
--add-suite groovy \
--remove-suite focal focal-proposed \
--add-uri http://apt.pop-os.org/ubuntu \
--remove-uri http://us.archive.ubuntu.com/ubuntu

View File

@ -0,0 +1,9 @@
================
Removing Sources
================
To remove a source from the system configuration, use the ``remove``
subcommand::
$ apt-manage remove ppa-system76-pop

177
archive/docs/conf.py Normal file
View File

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
#pylint: skip-file
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import sphinx_rtd_theme
# -- Project information -----------------------------------------------------
project = 'RepoLib'
copyright = '2019-2020, Ian Santopietro'
author = 'Ian Santopietro'
# The short X.Y version
version = '0.0.8'
# The full version, including alpha/beta/rc tags
release = ''
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'RepoLibdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'RepoLib.tex', 'RepoLib Documentation',
'Ian Santopietro', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'repolib', 'RepoLib Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'RepoLib', 'RepoLib Documentation',
author, 'RepoLib', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']

View File

@ -0,0 +1,276 @@
.. _deb822-explanation:
=======================================
Explanation of the DEB822 Source Format
=======================================
The sources described in ``/etc/apt/sources.list.d/`` on a Debian-based OS are
designed to support any number of different active and inactive sources, as well
as a large variety of source media and transport protocols. The files describe
one or more sources each and contain multiline stanzas defining one or more
sources per stanza, with the most preferred source listed first. Information
about available packages and versions from each source is fetched using the
``apt update`` command, or with an equivalent command from a different frontend.
.. _sources-list-d:
sources.list.d
==============
APT source information is stored locally within the ``/etc/apt/sources.list.d``
directory. In this directory are one or more files describing one or more
sources each. For :ref:`deb822-format` sources, each file needs to have the .sources
extension. The filenames may only contain letters, digits, underscore, hyphen,
and period characters. Files with other characters in their filenames will cause
APT to print a notice that it has ignore that file (unless the file matches a
pattern in the ``Dir::Ignore-Files-Silently`` configuration list, which will
force APT to silently ignore the file.
.. _one-line-format:
One-Line-Style Format
=====================
In order to understand some of the decisions behind using the :ref:`deb822-format`
sources, it is helpful to understand the older :ref:`one-line-format`.
:ref:`one-line-format` sources occupy one line in a file ending in ``.list``.
The line begins with a type (i.e. ``deb`` or ``deb-src`` followed by options and
arguments for this type. Individual entries cannot be continued onto multiple
lines (hence the "one-line" portion of this format's name). Empty lines in
``.list`` files are ignored, and a ``#`` character anywhere on the line signifies
that the remainder of that line is a comment. Consequently an entry can be
disabled by commenting out that entire line (prefixing it with a ``#``). If
options are provided, they are space-separated and all together are enclosed
within square brackets ( ``[]`` ). Options allowing multiple values should
separate each value with a comma ( ``,`` ) and each option name is separated
from its values by an equals sign ( ``=`` ).
This is the traditional format and is supported by all current APT versions.
It has the advantage of being relatively compact for single-sources and
relatively easy for humans to parse.
.. _one-line-disadvantages:
Disadvantages
-------------
Problems with the :ref:`one-line-format` begin when parsing entries via machine.
Traditional, optionless entries are relatively simple to parse, as each
different portion of the entry is separated with a space. With options, however,
this is no longer the case. The presence of options causes there to be no, 1, or
multiple segments of configuration between the type declaration and the URI.
Additionally, APT sources support a variety of URI schemas, with the capability
for extensions to add additional schemas on certain configurations. Thus,
supporting modern, optioned :ref:`one-line-format` source entries requires use
of either regular expressions or multi-level parsing in order to adequately
parse the entry. Further compounding this support is the fact that
:ref:`one-line-format` entries can have one or more components, preventing
parsing of sources backwards from the end towards the front.
Consider the following examples::
deb [] http://example.com.ubuntu disco main restricted multiverse universe
deb [ arch=amd64 ] http://example.com/ubuntu disco main nonfree
deb [lang=en_US] http://example.com/ubuntu disco main restricted universe multiverse
deb [ arch=amd64,armel lang=en_US,en_CA ] http://example.com/ubuntu disco main
Each of these entries are syntactically valid source entries, each cleanly
splits into eight segments when splitting on spaces. Depending on which entry
being parsed, the URI portion of the entry may be in index 2, 4, or 5 while
options (where present) may be in index 1, 2, or 3. If we want to work backwards,
then the URI is in either index -3, -4, or -6. The only segments guaranteed to
be present at any given index is the type. The situation is even more
complicated when considering that entries may have at a minimum 3 elements, and
at a maximum 12 or more elements.
In addition to parsing difficulty, :ref:`one-line-format` entries may only
specify a single suite and URI per entry, meaning that having two active mirrors
for a given source requires doubling the number of entries configured. You must
also create an extra set of duplicated entries for each suite you want to
configure. This can make tracking down duplicated entries difficult for users
and leads to longer-than-necessary configuration.
.. _deb822-format:
Deb822-style Format
===================
This format addresses the file-length, duplication, and machine-parsability
issues present in the :ref:`one-line-format`. Each source is configured in a
single stanza of configuration, with lines explicitly describing their
function for the source. They also allow for lists of values for most options,
meaning that mirrors or multiple suites can be defined within a single source. A
``#`` character at the beginning of a line marks the entire line as a comment.
Entries can again be disabled by commenting out each line within the stanza;
however, as a convenience this format also brings an ``Enabled:`` field which,
when set to ``no`` disables the entry as well. Removing this field or setting it
to ``yes`` re-enables it. Options have the same format as every other field,
thus a stanza may be parsed by checking the beginning of each line for a fixed
substring, and if the line doesn't match a known substring, it can be assumed
the line is an option and the line can be ignored. Unknown options are ignored
by all versions of APT. This has the unintentional side effect of adding
extensibility to the source; by selecting a carefully namespaced field name,
third-party applications and libraries can add their own fields to sources
without causing breakage by APT. This can include comments, version information,
and (as is the case with :ref:`repolib-module`, pretty, human-readable names.
From the ``sources.list(5)`` manual page:
This is a new format supported by apt itself since version 1.1. Previous
versions ignore such files with a notice message as described earlier. It is
intended to make this format gradually the default format, deprecating the
previously described one-line-style format, as it is easier to create,
extend and modify for humans and machines alike especially if a lot of
sources and/or options are involved.
.. _deb822-technical:
===================================
DEB822 Source Format Specifications
===================================
Following is a description of each field in the deb822 source format.
.. _deb822-field-enabled:
Enabled:
========
Enabled: (value: "yes" or "no", required: No, default: "yes")
Tells APT whether the source is enabled or not. Disabled sources are not
queried for package lists, effectively removing them from the system
sources while still allowing reference or re-enabling at any time.
.. _deb822-field-types:
Types:
======
Types: (value: "deb" or "deb-src", required: Yes)
Defines which types of packages to look for from a given source; either
binary: ``deb`` or source code: ``deb-src``. The ``deb`` type references a
typical two-level Debian archive providing packages containing pre-compiled
binary data intended for execution on user machines. The ``deb-src`` type
references a Debian distribution's source code in the same form as the ``deb``
type. A ``deb-src`` line is required to fetch source pacakge indices.
.. _deb822-field-uris:
URIs:
=====
URIs: (value: string(s), required: Yes)
The URI must specify the base of the Debian distribution archive, from which
APT finds the information it needs. There must be a URI component present in
order for the source to be valid; multipls URIs can be configured
simultaneously by adding a space-separated list of URIs.
A list of the current built-in URI Schemas supported by APT is available at
the `Debian sources.list manpage <https://manpages.debian.org/stretch/apt/sources.list.5.en.html#URI_SPECIFICATION>`_.
.. _deb822-field-suites:
Suites:
=======
Suites: (value: strings(s), required: Yes)
The Suite can specify an exact path in relation to the URI(s) provided, in
which case the :ref:`deb822-field-components` **must** be omitted and suite
**must** end with a slash ( ``/`` ). Alternatively, it may take the form of
a distribution version (e.g. a version codename like ``disco`` or ``artful``
). If the suite does not specify a path, at least one
:ref:`deb822-field-component` **must** be present.
.. _deb822-field-components:
Components:
===========
Components: (value: string(s), required: see :ref:`deb822-field-suites`)
Components specify different sections of one distribution version present in
a Suite. If :ref:`deb822-field-suites` specifies an exact path, then no
Components may be specified. Otherwise, a component **must** be present.
.. _deb822-options:
Options
=======
Sources may specify a number of options. These options and their values will
generally narrow a set of software to be available from the source or in some
other way control what software is downloaded from it. An exhaustive list of
options can be found at the
`Debian sources.list manpage <https://manpages.debian.org/stretch/apt/sources.list.5.en.html#THE_DEB_AND_DEB-SRC_TYPES:_OPTIONS>`_.
.. _deb822-fields-repolib:
RepoLib-Specific Deb822 Fields
==============================
RepoLib presently defines a single internal-use fields which it adds to deb822
sources that it modifies.
X-Repolib-Name:
---------------
X-Repolib-Name: (value: string, required: no, default: filename)
This field defines a formatted name for the source which is suitable for
inclusion within a graphical UI or other interface which presents source
information to an end-user. As a :ref:`repolib-module` specific field, this
is silently ignored by APT and other tools operating with deb822 sources and
is only intended to be utilized by :ref:`repolib-module` itself.
.. _deb822-examples:
========
Examples
========
The following specifies a binary and source-code source fetching from the
primary Ubuntu archive with multiple suites for updates as well as several components::
Enabled: yes
Types: deb deb-src
URIs: http://archive.ubuntu.com/ubuntu
Suites: disco disco-updates disco-security disco-backports
Components: main universe multiverse restricted
This is a source for fetching Google's Chrome browser, which specifies a CPU
architecture option and a RepoLib Name::
X-Repolib-Name: Google Chrome
Enabled: yes
URIs: http://dl.google.com/linux/chrome/deb
Suites: stable
Components: main
Architectures: amd64
This specifies a source for downloading packages for System76's Pop!_OS::
X-Repolib-Name: Pop!_OS PPA
Enabled: yes
Types: deb
URIs: http://ppa.launchpad.net/system76/pop/ubuntu
Suites: disco
Components: main
Following is a PPA source which has been disabled::
X-Repolib-Name: ZNC Stable
Enabled: no
Types: deb
URIs: http://ppa.launchpad.net/teward/znc/ubuntu
Suites: disco
Components: main

182
archive/docs/index.rst Normal file
View File

@ -0,0 +1,182 @@
.. RepoLib documentation master file, created by
sphinx-quickstart on Wed May 1 13:49:01 2019.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
===================================
Welcome to RepoLib's documentation!
===================================
RepoLib is a Python library and CLI tool-set for managing your software
system software repositories. It's currently set up to handle APT repositories
on Debian-based linux distributions.
RepoLib is intended to operate on DEB822-format sources as well as legacy
one-line format sources. It aims to provide feature parity with
software-properties for most commonly used functions. It also allows for
simple, automated conversion from legacy "one-line" style source to newer
DEB822 format sources. These sources will eventually deprecate the older
one-line sources and are much easier to parse for both human and machine. For a
detailed explanation of the DEB822 Source format, see :ref:`deb822-explanation`.
RepoLib provides much faster access to a subset of ``SoftwareProperties``
features than ``SoftwareProperties`` itself does. Its scope is somewhat more
limited because of this, but the performance improvements gained are substantial.
``SoftwareProperties`` also does not yet feature support for managing DEB822
format sources, and instead only manages one-line sources.
RepoLib is available under the GNU LGPL.
.. contents:: Table of Contents
:local:
.. toctree::
:maxdepth: 4
:caption: Developer Documentation
library/developer
.. toctree::
:maxdepth: 1
:caption: RepoLib Documentation
deb822-format
.. toctree::
:maxdepth: 1
:caption: apt-manage Documentation
aptmanage/aptmanage
============
Installation
============
There are a variety of ways to install RepoLib
From System Package Manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^
If your operating system packages repolib, you can install it by running::
sudo apt install python3-repolib
Uninstall
"""""""""
To uninstall, simply do::
sudo apt remove python3-repolib
From PyPI
^^^^^^^^^
Repolib is available on PyPI. You can install it for your current user with::
pip3 install repolib
Alternatively, you can install it system-wide using::
sudo pip3 install repolib
Uninstall
"""""""""
To uninstall, simply do::
sudo pip3 uninstall repolib
From Git
^^^^^^^^
First, clone the git repository onto your local system::
git clone https://github.com/isantop/repolib
cd repolib
Debian
^^^^^^
On debian based distributions, you can build a .deb package locally and install
it onto your system. You will need the following build-dependencies:
* debhelper (>= 11)
* dh-python
* lsb-release
* python3-all
* python3-dbus
* python3-debian
* python3-setuptools
* python3-distutils
* python3-pytest
* python3-gnupg
You can use this command to install these all in one go::
sudo apt install debhelper dh-python python3-all python3-setuptools python3-gnupg
Then build and install the package::
debuild -us -uc
cd ..
sudo dpkg -i python3-repolib_*.deb
Uninstall
"""""""""
To uninstall, simply do::
sudo apt remove python3-repolib
setuptools setup.py
^^^^^^^^^^^^^^^^^^^
You can build and install the package using python3-setuptools. First, install
the dependencies::
sudo apt install python3-all python3-setuptools
Then build and install the package::
sudo python3 ./setup.py install
Uninstall
"""""""""
You can uninstall RepoLib by removing the following files/directories:
* /usr/local/lib/python3.7/dist-packages/repolib/
* /usr/local/lib/python3.7/dist-packages/repolib-\*.egg-info
* /usr/local/bin/apt-manage
This command will remove all of these for you::
sudo rm -r /usr/local/lib/python3.7/dist-packages/repolib* /usr/local/bin/apt-manage
|
|
|
|
|
Copyright © 2019-2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.

View File

@ -0,0 +1,106 @@
============
Installation
============
There are a variety of ways to install RepoLib
From System Package Manager
^^^^^^^^^^^^^^^^^^^^^^^^^^^
If your operating system packages repolib, you can install it by running::
sudo apt install python3-repolib
Uninstall
"""""""""
To uninstall, simply do::
sudo apt remove python3-repolib
From PyPI
^^^^^^^^^
Repolib is available on PyPI. You can install it for your current user with::
pip3 install repolib
Alternatively, you can install it system-wide using::
sudo pip3 install repolib
Uninstall
"""""""""
To uninstall, simply do::
sudo pip3 uninstall repolib
From Git
^^^^^^^^
First, clone the git repository onto your local system::
git clone https://github.com/isantop/repolib
cd repolib
Debian
^^^^^^
On debian based distributions, you can build a .deb package locally and install
it onto your system. You will need the following build-dependencies:
* debhelper (>= 11)
* dh-python
* lsb-release
* python3-all
* python3-dbus
* python3-debian
* python3-setuptools
* python3-distutils
* python3-pytest
* python3-gnupg
You can use this command to install these all in one go::
sudo apt install debhelper dh-python python3-all python3-setuptools python3-gnupg
Then build and install the package::
debuild -us -uc
cd ..
sudo dpkg -i python3-repolib_*.deb
Uninstall
"""""""""
To uninstall, simply do::
sudo apt remove python3-repolib
setuptools setup.py
^^^^^^^^^^^^^^^^^^^
You can build and install the package using python3-setuptools. First, install
the dependencies::
sudo apt install python3-all python3-setuptools
Then build and install the package::
sudo python3 ./setup.py install
Uninstall
"""""""""
You can uninstall RepoLib by removing the following files/directories:
* /usr/local/lib/python3.7/dist-packages/repolib/
* /usr/local/lib/python3.7/dist-packages/repolib-\*.egg-info
* /usr/local/bin/apt-manage
This command will remove all of these for you::
sudo rm -r /usr/local/lib/python3.7/dist-packages/repolib* /usr/local/bin/apt-manage

View File

@ -0,0 +1,335 @@
.. _repolib_module:
=======
RepoLib
=======
The :ref:`repolib-module` module simplifies working with APT sources, especially
those stored in the DEB822 format. It translates these sources into Python
Objects for easy reading, parsing, and manipulation of them within Python
programs. The program takes a user-defined sources filename and reads the
contents, parsing it into a Python object which can then be iterated upon and
which uses native data types where practicable. The :ref:`repolib-module` module
also allows easy creation of new DEB822-style sources from user-supplied data.
.. toctree::
:maxdepth: 1
:caption: RepoLib
source/source
ppa-module
enums
==================
``repolib`` Module
==================
The ``repolib`` Module is the main module for the package. It allows interfacing
with the various Classes, Subclasses, and Functions provided by RepoLib.
Module-level Attributes
=======================
There are a couple of module-level attributes and functions provided directly in
the module.
.. _module-version
``VERSION``
-----------
repolib.VERSION
Provides the current version of the library.
.. _module_log_file_path
``LOG_FILE_PATH``
-----------------
repolib.LOG_FILE_PATH
Stores the current path to the log file
.. _module_log_level
repolib.LOG_LEVEL
Stores the current logging level. Note: Change this level using the
:ref:`module_set_logging_level` function.
.. _module_dirs
Configuration directories
=========================
repolib.KEYS_DIR
repolib.SOURCES_DIR
Stores the current :obj:`Pathlib.Path` pointing at the signing key and
sources directories, respectively. Used for building path names and reading
configuration.
.. _module_distro_codename:
DISTRO_CODENAME
===============
repolib.DISTRO_CODENAME
The current CODENAME field from LSB. If LSB is not available, it will
default to ``linux``.
.. _module_clean_chars
CLEAN_CHARS
===========
repolib.CLEAN_CHARS
A ``dict`` which maps invalid characters for the :ref:`ident` attributes
which cannot be used and their replacements. These limitations are based on
invalid characters in unix-compatible filenames.
.. _module_sources
sources
=======
repolib.sources
A :obj:`dict` storing all current sources configured on the system. To save
resources, this list is only loaded/parsed when
:ref:`module_load_all_sources` is called, since many simple operations don't
need the full list of currently-configured sources.
.. _module_files
files
=====
repolib.files
A :obj:`dict` containing any source file objects present in the configured
sources dir (See :ref:`module_dirs`). This list is empty until
:ref:`module_load_all_sources` is called, since many simple operations don't
need the full list of currently-installed config files.
.. _module_keys
keys
====
repolib.keys
A :obj`dict` containing any installed repository signing keys on the system.
This list is empty until :ref:`module_load_all_sources` is called, since
many simple operations don'tneed the full list of
currently-installed keys.
.. _module_compare_sources
compare_sources()
-----------------
repolib.compare_sources(source1, source2, excl_keys:list) -> bool
Compares two sources based on arbitrary criteria.
This looks at a given list of keys, and if the given keys between the two
given sources are identical, returns True.
Returns: :obj:`bool`
`True` if the sources are identical, otherwise `False`.
source1, source2
^^^^^^^^^^^^^^^^
The two source objects to compare.
excl_keys
^^^^^^^^^
:obj:`list` A list of DEB822key names to ignore when comparing. Even if these
items don't match, this function still returns true if all other keys match.
.. _module_combine_sources
combine_sources()
-----------------
repolib.combine_sources(source1, source2) -> None
Copies all of the data in `source2` and adds it to `source1`.
This avoids duplicating data and ensures that all of both sources' data are
present in the unified source
source1
^^^^^^^
The source into which both sources should be merged
source2
^^^^^^^
The source from which to copy to `source1`
.. _module_url_validator
url_validator()
---------------
repolib.url_validator(url: str) -> bool
Validates a given URL and attempts to see if it's a valid Debian respository
URL. Returns `True` if the URL appears to be valid and `False` if not.
url
^^^
:obj:`str`The url to validate
.. _module_validate_debline
validate_debline()
==================
repolib.util.validate_debline(line: str) -> bool
Validate if the provided debline ``line`` is valid or not.
Returns ``True`` if ``line`` is valid, otherwise ``False``.
line
^^^^
:obj:`str` The line to validate
.. _module_strip_hashes
strip_hashes()
--------------
repolib.strip_hashes(line: str) -> str
Strips leading hash (`#`) characters from a line and returns the result.
line
^^^^
:obj:`str` The line to strip
.. _module_load_all_sources
load_all_sources()
------------------
repolib.load_all_sources() -> None
Loads all sources from the current system configuration.
.. _module-set_testing
set_testing()
-------------
repolib.set_testing(testing: bool = True) -> None
Enables or disables testing mode in Repolib
When in testing mode, repolib will operate within a temporary directory
rather tan on your live system configuration. This can be used for testing
out changes to the program without worry about changes to the system config.
It's also used for unit testing.
testing
^^^^^^^
:obj:`bool` - Wether testing mode should be enabled (`True`) or not (`False`)
Example
=======
The following code as a Python program that creates in ``example.sources`` file
in `/etc/apt/sources.list.d` with some sample data, then modifies the suites
used by the source and prints it to the console, before finally saving the new,
modified source to disk::
import repolib
source = repolib.Source()
file = repolib.SourceFile(name='example')
source.ident = 'example-source'
source.name = 'Example Source'
source.types = [repolib.SourceType.BINARY]
source.uris = ['http://example.com/software']
source.suites = ['focal']
source.components = ['main']
file.add_source(source)
print(source.ui)
file.save()
When run with the appropriate arguments, it prints the contents of the source to
console and then saves a new ``example.sources`` file::
$
example-source:
Name: Example Source
Enabled: yes
Types: deb
URIs: http://example.com/software
Suites: focal
Components: main
$ ls -la /etc/apt/sources.list.d/example.sources
-rw-r--r-- 1 root root 159 May 1 15:21 /etc/apt/sources.list.d/example.sources
Below is a walkthrough of this example.
Creating Source and File Objects
--------------------------------
The first step in using :ref:`repolib_module` is creating :ref:`source_object`
and :ref:`file_object`::
source = repolib.Source()
file = repolib.SourceFile(name='example')
The :ref:`source_object` will hold all of the information for the source to be
created. The :ref:`file_object` represents the output file on disk, and allows
for advanced usage like multiple sources per file.
Adding and Manipulating Data
----------------------------
The :ref:`source_object` contains attributes which describe the connfiguration
aspects of the source required to fetch and install software. Generally, these
attributes are lists of strings which describe different aspects of the source.
They can be set or retrieved like any other attributes::
source.uris = ['http://example.com/software']
source.suites = ['focal']
This will add a ``focal`` suite to our source and add a URI from which to
download package lists.
:ref:`source_object` also presents a dict-like interface for setting and getting
configuration data. Key names are case-insensitive and their order within the
object are preserved.
Adding the Source to the File
-----------------------------
Before the :ref:`source_object` can be saved, it needs to be added to a
:ref:`file_object`::
file.add_source(source)
This will add `source` to the `file`'s lists of sources, as well as setting the
`source`'s file attribute to `file`.
Saving Data to Disk
-------------------
Once a source has the correct data and has been added to a file object, it can
be saved into the system configuration using :ref:`file-save`::
file.save()
When called, this writes the sources stored in the file to disk. This does not
destroy the object, so that it may be further manipulated by the program.
.. note::
While data within the source or file objects can be manipulated after
calling :ref:`file-save`, any subsequent changes will not be automatically
written to disk as well. The :ref:`file-save` method must be called to
commit changes to disk.
.. _source_object:

View File

@ -0,0 +1,45 @@
.. _repolib-enums:
=============
RepoLib Enums
=============
RepoLib uses a variety of Enums to help ensure that data values are consistent
when they need to be set to specific string values for compatibility.
.. _enum_sourceformat
SourceFormat
============
repolib.SourceFormat()
Encodes the two formats of source files, either ``.list`` for legacy format
files or ``.sources`` for DEB822-formatted files.
* ``DEFAULT`` - DEB822 formatting (value: ``"sources"``)
* ``LEGACY`` - Legacy, one-line formatting (value: ``"list"``)
.. _enum_sourcetype
SourceType
==========
repolib.SourceType()
Encodes the type of packages the repository provides (binary or source code).
* ``BINARY`` - Binary package source type (value: ``"deb"``)
* ``SOURCECODE`` - Source code package type (value : ``"deb-src"``)
.. _enum_aptsourceenabled:
AptSourceEnabled
================
repolib.AptSourceEnabled()
Used to encode whether the source is enabled or not.
* ``TRUE`` - The source should be enabled (value: ``"yes"``).
* ``FALSE`` - The source should not be enabled (value: ``"no"``).

View File

@ -0,0 +1,50 @@
.. _exceptions
==========
Exceptions
==========
RepoLib throws various forms of exceptions to halt execution of code when
incorrect input is detected or deviations from expected norms are encountered.
.. _exc_repoerror
RepoError()
-----------
Repolib.RepoError()
This is base exception thrown by RepoLib. All other exceptions are
subclasses of this exception type.
.. _exc_sourceerror
SourceError()
-------------
Repolib.Source.SourceError()
Raised when errors result from processing within the :obj:`source_object` or
one of it's subclasses.
.. _exc_debparseerror
DebParseError()
---------------
Repolib.parsedeb.DebParseError()
Raised due to problems parsing legacy Debian one-line repository lines.
.. _exc_sourcefileerror
SourceFileError()
-----------------
Repolib.file.SourceFileError()
Raised due to errors handling :ref:`file_object` objects.
.. _exc_keyfileerror
KeyFileError()
--------------
Repolib.key.KeyFileError()
Raised due to errors loading/finding key files or :ref:`key_object` objects.

View File

@ -0,0 +1,172 @@
.. _file_object:
===========
File object
===========
SourceFile objects are the interface between the OS storage and the repolib
:ref:`source_object` items. Files contain sources and comments and allow for
loading sources from and saving them to disk. They also assign idents to sources
in a way that ensures each source has a unique ident.
Attributes
==========
File objects contain the following attributes:
.. _file-name
name
----
SourceFile.name
The name of the file on disk. This does not include the file extension, as
that is stored in :ref:`file-format`.
.. _file-path
path
----
SourceFile.path
A ``Pathlib.Path`` object representing this file's actual path on disk. Note
that the file may not actually exist on disk yet.
.. _file-format
format
------
SourceFile.format
The format this file should be saved in. Saved as a :ref:`enum_sourceformat`.
.. _file-contents
contents
--------
SourceFile.contents
A :obj:`list` containing, in order, every comment line and source in this
file.
.. _file-sources
sources
-------
SourceFile.sources
A :obj:`list` containing, in order, only the sources in this file.
Methods
=======
.. _file-add_source
add_source()
------------
SourceFile.add_source(source) -> None
Adds a given source to the file. This correctly appends the source to both
the :ref:`file-contents` and :ref:`file-sources` lists, and sets the
:ref:`source-file` attribute of the source to this file.
source
^^^^^^
The source to add to the file.
.. _file-remove_source
remove_source()
---------------
SourceFile.remove_source(ident: str) -> None
Removes the source with a specified ident from this file.
ident
^^^^^
The ident of the source to remove from the file.
.. _file-get_source_by_ident
get_source_by_ident()
---------------------
SourceFile.get_source_by_ident(ident: str) -> :obj:`Source`
Finds a source within this file given a specified ident and returns the
:ref:`source_object` matching that ident. If the file does not contain a
Source matching the given ident, raises a :ref:`exc_sourcefileerror`.
ident
^^^^^
The ident to look up.
.. _file-reset_path
SourceFile.reset_path() -> None
Attempts to detect the full path to the file given the :ref:`file-name`
attribute for this file. If the ``name.sources`` exists on disk, the path
will be set to that, otherwise if the ``name.list`` exists, it will be set
to that instead. Failing both, the path will fallback to ``name.sources`` as
a default.
.. _file-load
load()
------
SourceFile.load() -> None
Loads the file specified by :ref:`path` from disk, creating sources and
comments and appending them in order to the :ref:`file-contents` and
:ref:`file-sources` lists as appropriate.
.. _file-save
save()
------
SourceFile.save() -> None
Saves the file and any sources currently configured to disk. This method
must be called to commit changes to disk. If the file currently contains no
sources, then the file will instead be deleted.
Output
======
There are four attributes which contain the output of the files stored as
strings and which are ready for full output in the specified format.
.. _file-deb822
deb822
------
SourceFile.deb822
Outputs the entire file as DEB822-formatted sources
.. _file-legacy
legacy
------
SourceFile.legacy
Outputs the entire file as one-line legacy-formatted deb lines
.. _file-ui
ui
--
SourceFile.ui
Outputs the file in a format for output through a UI (e.g. for preview or
external parsing.)
.. _file-output
output
------
SourceFile.output
Outputs the entire file in the format matching that configured in
:ref:`file-format`.

View File

@ -0,0 +1,121 @@
.. _key_object:
==========
Key object
==========
The SourceKey object is the representation for signing keys used to validate
software packages downloaded from repositories.
Attributes
==========
.. _key-path
path
----
SourceKey.path
A :obj:`Pathlib.Path` object pointing to this key's file location on disk.
.. _key-gpg
gpg
---
SourceKey.gpg
A :obj:`gnupg.GPG` object used for importing and manipulating GPG data.
.. _key-data
data
----
SourceKey.data
The binary data stored in this key, used to verify package signatures.
Methods
=======
.. _key-reset_path
reset_path()
------------
SourceKey.reset_path(name:str='', path:str='', suffix:str='archive-keyring') -> None
(Re)sets the path object for this key to the key file specified. If a ``name``
is given, then the file is expected to be located within the system default
signing key directory ``/etc/apt/keyrings``. If a ``path`` is spefified,
then it is assumed to be the full path to the keyring file on disk.
name
^^^^
The name of a keyring file located within the system signing key directory, less
any suffix or file extensions.
path
^^^^
The absolute path to a keyring file located at any readable location on disk.
suffix
^^^^^^
A suffix to append to the file name before the file extension. (Default:
``'archive-keyring'``)
.. _key-setup_gpg
setup_gpg()
-----------
SourceKey.setup_gpg() -> None
Sets up the :ref:`key-gpg` object for this key and loads key data from disk
(if present).
.. _key-save_gpg
save_gpg()
----------
SourceKey.save_gpg() -> None
Saves the GPG key data to disk.
.. _key-delete_key
delete_key()
------------
SourceKey.delete_key() -> None
Deletes the key file from disk.
.. _key-load_key_data
load_key_data()
---------------
SourceKey.load_key_data(raw=|ascii=|url=|fingerprint=) -> None
Fetches and loads a key from some remote source. Keys can be loaded from one
of:
* Raw internal data (:obj:`bytes`)
* ASCII-armored keys (:obj:`str`)
* Internet URL download (:obj:`str`)
* Public GPG Keyserver Fingerprint (:obj:`str`)
raw=data (:obj:`bytes`)
^^^^^^^^^^^^^^^^^^^^^^^
Load a key from raw binary data. This is ideal when importing a key which has
already been loaded from a binary data file or stream.
ascii=armor (:obj:`str`)
^^^^^^^^^^^^^^^^^^^^^^^^
Load a key from an ASCII-Armored string.
url=url (:obj:`str`)
^^^^^^^^^^^^^^^^^^^^
Download a key over an HTTPS connection. Note that keys should only be downloaded
from secure sources.
fingerprint=fingerprint (:obj:`str`)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Fetch the key specified by ``fingerprint`` from a public keyserver.

View File

@ -0,0 +1,56 @@
.. _shortcuts
====================
Repository Shortcuts
====================
Repolib supports adding repositories via abbreviated shortcuts (such as
``ppa:system76/pop`` or ``popdev:master``) where the format of the shortcut is
given in the form of a prefix, and the data required to expand the shortcut into
a full repository is provided, with a colon character separating the two parts.
These function as subclasses of the :ref:`source_object` with specialized
functionality. Currently there is support for Launchpad PPAs and Pop_OS
Development branches automatically.
Launchpad PPAs
==============
repolib.PPASource()
RepoLib has built-in support for adding Launchpad PPA shortcuts, similarly to
``add-apt-repository``. Launchpad shortcuts take the general form::
ppa:owner-name/ppa-name
If an internet connection is available, RepoLib can automatically fetch the
repository signing key as well as metadata like the description, human-readable
name, etc.
Pop_OS Development Branches
===========================
repolib.PopdevSource()
RepoLib can automatically add Pop_OS development branches for testing unstable
pre-release software before it is ready for general use. Development branch
shortcuts take the form::
popdev:branch-name
If an internet connection is available, then RepoLib will also add the signing
key for the branch (if it is not already added).
Adding Shortcut Repositories
============================
Shortcuts are generally added similarly to loading a different source from data,
the main difference being that in the call to :ref:`load_from_data` the only
element in the ``data`` list should be a string of the shortcut::
ppa_source = repolib.PPASource()
ppa_source.load_from_data(['ppa:system76/pop'])
popdev_source = repolib.PopdevSource()
popdev_source.load_from_data(['popdev:master'])

View File

@ -0,0 +1,281 @@
.. _source_object:
=============
Source object
=============
The Source object is the base class for all of the sources used within RepoLib.
The Source class itself is a subclass of the deb822() class from the Python
Debian module, which provides a dict-like interface for setting data as well as
several methods for dumping data to files and other helpful functions.
.. toctree::
:maxdepth: 2
:caption: Source
deb
legacy-deb
ppa-object
system
class repolib.Source (file=None)
Create a new :ref:`source_object`.
The Source object has the following attributes:
* :ref:`ident` - The system-identifier to use for this source.
* :ref:`name` - The human-readable name of the source. (default: '')
* :ref:`enabled` - Whether the source is enabled or not at creation.
(default: True)
* :ref:`types` - A list of the types that the source should use.
(default: [])
* :ref:`uris` - A list of URIs from which to fetch software or check for
updates. (default: [])
* :ref:`suites` - Suites in which to narrow available software. (default:
[])
* :ref:`components` - Components of the source repository to enable.
(default: [])
* :ref:`signed_by` - The path to the signing key for this source
* :ref:`file` - The :ref:`file_object` for this source
* :ref:`key` - The :ref:`key_object` for this source.
* :ref:`twin_source`: - This source should be saved with both binary and
source code types enabled.
The following decribe how each of these are used.
.. _ident:
ident
-----
The ``ident`` is the system-identifier for this source. This determines the
filename the source will use on-disk as well as the way to specify a source to
load.
.. _name:
name
----
This is a human-readable and nicely-formatted name to help a user recognize
what this source is. Any unicode character is allowed in this field. If a
source is opened which doesn't have a name field, the filename will be used.
:ref:`name` is a string value, set to ``''`` by default. If there is no name in
a loaded source, it will be set to the same as the filename (minus the
extension).
This field maps to the ``X-Repolib-Name:`` field in the .sources file, which
is ignored by Apt and other standards-compliant sources parsers.
.. _enabled:
enabled
-------
Apt sources can be disbaled without removing them entirely from the system. A
disabled source is excluded from updates and new software installs, but it can
be easily re-enabled at any time. It defaults to ``True``.
This field maps to the ``Enabled:`` field in the .sources file. This is optional
per the DEB822 standard, however if set to anything other than ``no``, the
source is considered enabled.
.. _types:
types
-----
Debian archives may contain either binary packages or source code packages, and
this value specifies which of those Apt should look for in the source. ``deb``
is used to look for binary packages, while ``deb-src`` looks for source code
packages. RepoLib stores this value as a list of :ref:`aptsourcetype-enum`s, and
defaults to ``[AptSourceType.BINARY]``.
This field maps to the ``Types:`` field in the sources file.
.. _uris:
uris
----
A list of string values describing the URIs from which to fetch package lists
and software for updates and installs. The currently recognized URI types are:
* file
* cdrom
* http
* ftp
* copy
* rsh
* ssh
DEB822 sources may directly contain an arbitrary number of URIs. Legacy sources
may also have multiple URIs; however, these require writing a new deb line for
each URI as the one-line format only allows a single URI per source.
.. note::
Although these are the currently-recognized official URI types, Apt can be
extended with additional URI schemes through extension packages. Thus it is
**not** recommended to parse URIs by type and instead rely on user input
being correct and to throw exceptions when that isn't the case.
.. _suites:
suites
------
The Suite, also known as the **distribution** specifies a subdirectory of the
main archive root in which to look for software. This is typically used to
differentiate versions for the same OS, e.g. ``disco`` or ``cosmic`` for Ubuntu,
or ``squeeze`` and ``unstable`` for Debian.
DEB822 Sources allow specifying an arbitrary number of suites. One-line sources
also support multiple suites, but require an additional repo line for each as
the one-line format only allows a single suite for each source.
This value maps to the ``Suites:`` field in the sources file.
.. _components:
components
----------
This value is a list of strings describing the enabled distro components to
download software from. Common values include ``main``, ``restricted``,
``nonfree``, etc.
.. _signed_by
signed_by
---------
The path to the keyring containing the signing key used to verify packages
downloaded from this repository. This should generally match the
:ref:`key-path` attribute for this source's :ref:`key` object.
.. _source-file
file
----
The :ref:`file_object` for the file which contains this source.
.. _key
key
---
The :ref:`key_object` for this source.
=======
Methods
=======
.. _get_description
Source.get_description() -> str
Returns a :obj:`str` containing a UI-compatible description of the source.
.. _reset_values:
reset_values()
-------------
Source.reset_values()
Initializes the Source's attributes with default data in-order. This is
recommended to ensure that the fields in the underlying deb822 Dict are
in order and correctly capitalized.
.. _load_from_data
load_from_data()
----------------
Source.load_from_data(data: list) -> None
Loads configuration information from a list of data, rather than using
manual entry. The data can either be a list of strings with DEB822 data, or
a single-element list containing a one-line legacy line.
data
^^^^
The data load load into the source. If this contains a legacy-format one-line
repository, it must be a single-element list. Otherwise, it should contain a
list of strings, each being a line from a DEB822 configuration.
.. _generate_default_ident
generate_default_ident()
------------------------
Source.generate_default_ident(prefix: str = '') -> str
Generates a suitable default ident, optionally with a prefix, and sets it.
The generated ident is also returned for processing convenience.
prefix
^^^^^^
The prefix to prepend to the ident.
.. _generate_default_name
generate_default_name()
_______________________
Source.generate_default_name() ->
Generates a default name for the source and sets it. The generated name is
also returned for convenience.
.. _load_key
load_key()
__________
Source.load_key(ignore_errors: bool = True) -> None
Finds the signing key for this source spefified in :ref:`signed_by` and
loads a :ref:`key_object` for it.
ignore_errors
^^^^^^^^^^^^^
If `False`, raise a :ref:`exc_sourceerror` if the key can't be loaded or doesn't
exist.
.. _source_save
save()
------
Source.save()
Proxy method for the :ref:`file-save` method for this source's
:ref:`file_object`.
Output Properties
=================
There are three output properties which contain the current source data for
output in a variety of formats.
.. _source_deb822
deb822
------
Source.deb822
A representation of the source data as a DEB822-formatted string
.. _source_legacy
legacy
------
Source.legacy
A one-line formatted string of the source. It :ref:`twin_source` is ``True``,
then there will additionally be a ``deb-src`` line following the primary
line.
.. _source_ui
ui
--
Source.ui
A representation of the source with certain key names translated to better
represent their use in a UI for display to a user.

144
archive/repolib/__init__.py Normal file
View File

@ -0,0 +1,144 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import logging.handlers as handlers
from pathlib import Path
from . import __version__
VERSION = __version__.__version__
from .file import SourceFile, SourceFileError
from .source import Source, SourceError
from .shortcuts import PPASource, PopdevSource, shortcut_prefixes
from .key import SourceKey, KeyFileError
from . import util
from . import system
LOG_FILE_PATH = '/var/log/repolib.log'
LOG_LEVEL = logging.WARNING
KEYS_DIR = util.KEYS_DIR
SOURCES_DIR = util.SOURCES_DIR
TESTING = util.TESTING
KEYSERVER_QUERY_URL = util.KEYSERVER_QUERY_URL
DISTRO_CODENAME = util.DISTRO_CODENAME
PRETTY_PRINT = util.PRETTY_PRINT
CLEAN_CHARS = util.CLEAN_CHARS
try:
from systemd.journal import JournalHandler
systemd_support = True
except ImportError:
systemd_support = False
## Setup logging
stream_fmt = logging.Formatter(
'%(name)-21s: %(levelname)-8s %(message)s'
)
file_fmt = logging.Formatter(
'%(asctime)s - %(name)-21s: %(levelname)-8s %(message)s'
)
log = logging.getLogger(__name__)
console_log = logging.StreamHandler()
console_log.setFormatter(stream_fmt)
console_log.setLevel(LOG_LEVEL)
# file_log = handlers.RotatingFileHandler(
# LOG_FILE_PATH, maxBytes=(1048576*5), backupCount=5
# )
# file_log.setFormatter(file_fmt)
# file_log.setLevel(LOG_LEVEL)
log.addHandler(console_log)
# log.addHandler(file_log)
log_level_map:dict = {
0: logging.WARNING,
1: logging.INFO,
2: logging.DEBUG
}
if systemd_support:
journald_log = JournalHandler() # type: ignore (this is handled by the wrapping if)
journald_log.setLevel(logging.INFO)
journald_log.setFormatter(stream_fmt)
log.addHandler(journald_log)
log.setLevel(logging.DEBUG)
def set_testing(testing:bool = True) -> None:
"""Puts Repolib into testing mode"""
global KEYS_DIR
global SOURCES_DIR
util.set_testing(testing=testing)
KEYS_DIR = util.KEYS_DIR
SOURCES_DIR = util.SOURCES_DIR
def set_logging_level(level:int) -> None:
"""Set the logging level for this current repolib
Accepts an integer between 0 and 2, with 0 being the default loglevel of
logging.WARNING, 1 being logging.INFO, and 2 being logging.DEBUG.
Values greater than 2 are clamped to 2. Values less than 0 are clamped to 0.
Note: This only affects console output. Log file output remains
at logging.INFO
Arguments:
level(int): A logging level from 0-2
"""
if level > 2:
level = 2
if level < 0:
level = 0
LOG_LEVEL = log_level_map[level]
console_log.setLevel(LOG_LEVEL)
RepoError = util.RepoError
SourceFormat = util.SourceFormat
SourceType = util.SourceType
AptSourceEnabled = util.AptSourceEnabled
scrub_filename = util.scrub_filename
url_validator = util.url_validator
prettyprint_enable = util.prettyprint_enable
validate_debline = util.validate_debline
strip_hashes = util.strip_hashes
compare_sources = util.compare_sources
combine_sources = util.combine_sources
sources = util.sources
files = util.files
keys = util.keys
errors = util.errors
valid_keys = util.valid_keys
options_inmap = util.options_inmap
options_outmap = util.options_outmap
true_values = util.true_values
load_all_sources = system.load_all_sources

View File

@ -0,0 +1,22 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
__version__ = "2.1.1"

View File

@ -0,0 +1,30 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from .argparser import get_argparser
from .add import Add
from .list import List
from .remove import Remove
from .modify import Modify
from .key import Key
parser = get_argparser()

View File

@ -0,0 +1,267 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from httplib2.error import ServerNotFoundError
from urllib.error import URLError
from .. import util
from ..source import Source, SourceError
from ..file import SourceFile, SourceFileError
from ..shortcuts import ppa, popdev, shortcut_prefixes
from .command import Command, RepolibCommandError
class Add(Command):
"""Add subcommand
Adds a new source into the system. Requests root, if not present.
Options:
--disable, d
--source-code, -s
--terse, -t
--name, -n
--identifier, -i
--format, -f
--skip-keys, -k
"""
@classmethod
def init_options(cls, subparsers):
""" Sets up the argument parser for this command.
Returns: argparse.subparser
The subparser for this command
"""
sub = subparsers.add_parser(
'add',
help='Add a new repository to the system'
)
sub.add_argument(
'deb_line',
nargs='*',
default=['822styledeb'],
help='The deb line of the repository to add'
)
sub.add_argument(
'-d',
'--disable',
action='store_true',
help='Add the repository and then set it to disabled.'
)
sub.add_argument(
'-s',
'--source-code',
action='store_true',
help='Also enable source code packages for the repository.'
)
sub.add_argument(
'-t',
'--terse',
action='store_true',
help='Do not display expanded info about a repository before adding it.'
)
sub.add_argument(
'-n',
'--name',
default='',
help='A name to set for the new repo'
)
sub.add_argument(
'-i',
'--identifier',
default='',
help='The filename to use for the new source'
)
sub.add_argument(
'-f',
'--format',
default='sources',
help='The source format to save as, `sources` or `list`'
)
sub.add_argument(
'-k',
'--skip-keys',
action='store_true',
help='Skip adding signing keys (not recommended!)'
)
return sub
def finalize_options(self, args):
super().finalize_options(args)
self.deb_line = ' '.join(args.deb_line)
self.terse = args.terse
self.source_code = args.source_code
self.disable = args.disable
self.log.debug(args)
try:
name = args.name.split()
except AttributeError:
name = args.name
try:
ident = args.identifier.split()
except AttributeError:
ident = args.identifier
self.name = ' '.join(name)
pre_ident:str = '-'.join(ident)
self.ident = util.scrub_filename(pre_ident)
self.skip_keys = args.skip_keys
self.format = args.format.lower()
def run(self) -> bool:
"""Run the command, return `True` if successful; otherwise `False`"""
if self.deb_line == '822styledeb':
self.parser.print_usage()
self.log.error('A repository is required')
return False
repo_is_url = self.deb_line.startswith('http')
repo_is_nospaced = len(self.deb_line.split()) == 1
if repo_is_url and repo_is_nospaced:
self.deb_line = f'deb {self.deb_line} {util.DISTRO_CODENAME} main'
print('Fetching repository information...')
self.log.debug('Adding line %s', self.deb_line)
new_source: Source = Source()
for prefix in shortcut_prefixes:
self.log.debug('Trying prefix %s', prefix)
if self.deb_line.startswith(prefix):
self.log.debug('Line is prefix: %s', prefix)
new_source = shortcut_prefixes[prefix]()
if not new_source.validator(self.deb_line):
self.log.error(
'The shortcut "%s" is malformed',
self.deb_line
)
# Try and get a suggested correction for common errors
try:
if self.deb_line[len(prefix)] != ':':
fixed_debline: str = self.deb_line.replace(
self.deb_line[len(prefix)],
":",
1
)
print(f'Did you mean "{fixed_debline}"?')
except IndexError:
pass
return False
try:
new_source.load_from_data([self.deb_line])
except (URLError, ServerNotFoundError) as err:
import traceback
self.log.debug(
'Exception info: %s \n %s \n %s',
type(err),
''.join(traceback.format_exception(err)),
err.args
)
self.log.error(
'System is offline. A connection is required to add '
'PPA and Popdev sources.'
)
return False
except Exception as err:
import traceback
self.log.debug(
'Exception info: %s \n %s \n %s',
type(err),
err.__traceback__,
err
)
self.log.error('An error ocurred: %s', err)
return False
break
if not new_source:
self.log.error(
f'Could not parse line "{self.deb_line}". Double-check the '
'spelling.'
)
valid_shortcuts: str = ''
for shortcut in shortcut_prefixes:
if shortcut.startswith('deb'):
continue
valid_shortcuts += f'{shortcut}, '
valid_shortcuts = valid_shortcuts.strip(', ')
print(f'Supported repository shortcuts:\n {valid_shortcuts}')
return False
new_source.twin_source = True
new_source.sourcecode_enabled = self.source_code
if self.name:
new_source.name = self.name
if self.ident:
new_source.ident = self.ident
if self.disable:
new_source.enabled = False
if not new_source.ident:
new_source.ident = new_source.generate_default_ident()
new_file = SourceFile(name=new_source.ident)
new_file.format = new_source.default_format
if self.format:
self.log.info('Setting new source format to %s', self.format)
for format in util.SourceFormat:
if self.format == format.value:
new_file.format = format
self.log.debug('New source format set to %s', format.value)
new_file.add_source(new_source)
new_source.file = new_file
self.log.debug('File format: %s', new_file.format)
self.log.debug('File path: %s', new_file.path)
self.log.debug('Sources in file %s:\n%s', new_file.path, new_file.sources)
if not self.terse:
print(
'Adding the following source: \n',
new_source.get_description(),
'\n\n',
new_source.ui
)
try:
input(
'Press ENTER to continue adding this source or Ctrl+C '
'to cancel'
)
except KeyboardInterrupt:
# Handled here to avoid printing the exception to console
exit(0)
new_file.save()
util.dbus_quit()
return True

View File

@ -0,0 +1,65 @@
#!/usr/bin/python3
"""
Copyright (c) 2020, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
Module for getting an argparser. Used by apt-manage
"""
import argparse
import inspect
from repolib import command as cmd
from .. import __version__
def get_argparser():
""" Get an argument parser with our arguments.
Returns:
An argparse.ArgumentParser.
"""
parser = argparse.ArgumentParser(
prog='apt-manage',
description='Manage software sources and repositories',
epilog='apt-manage version: {}'.format(__version__.__version__)
)
parser.add_argument(
'-b',
'--debug',
action='count',
help=argparse.SUPPRESS
)
subparsers = parser.add_subparsers(
help='...',
dest='action',
metavar='COMMAND'
)
subcommands = []
for i in inspect.getmembers(cmd, inspect.isclass):
obj = getattr(cmd, i[0])
subcommands.append(obj)
for i in subcommands:
i.init_options(subparsers)
return parser

View File

@ -0,0 +1,77 @@
"""
Copyright (c) 2020, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
Command line application helper class.
"""
from ..util import SOURCES_DIR
class RepolibCommandError(Exception):
""" Exceptions generated by Repolib CLIs."""
def __init__(self, *args, code=1, **kwargs):
"""Exception from a CLI
Arguments:
code (:obj:`int`, optional, default=1): Exception error code.
"""
super().__init__(*args, **kwargs)
self.code = code
class Command:
# pylint: disable=no-self-use,too-few-public-methods
# This is a base class for other things to inherit and give other programs
# a standardized interface for interacting with commands.
""" CLI helper class for developing command line applications."""
def __init__(self, log, args, parser):
self.log = log
self.parser = parser
self.sources_dir = SOURCES_DIR
self.finalize_options(args)
def finalize_options(self, args):
""" Base options parsing class.
Use this class in commands to set up the final options parsing and set
instance variables for later use.
"""
self.verbose = False
self.debug = False
if args.debug:
if args.debug > 1:
self.verbose = True
if args.debug != 0:
self.log.info('Debug mode set, not-saving any changes.')
self.debug = True
def run(self):
""" The initial base for running the command.
This needs to have a standardized format, argument list, and return
either True or False depending on whether the command was successful or
not.
Returns:
True if the command succeeded, otherwise False.
"""
# Since this is just the base, it should always return True (because
# you shouldn't fail at doing nothing).
return True

View File

@ -0,0 +1,325 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from pathlib import Path
from ..key import SourceKey
from .. import system, util
from .command import Command
KEYS_PATH = Path(util.KEYS_DIR)
class Key(Command):
"""Key subcommand.
Manages signing keys for repositories, allowing adding, removal, and
fetching of remote keys.
Options:
--name, -n
--path, -p
--url, -u
--ascii, -a
--fingerprint, -f
--keyserver, -s
--remove, -r
"""
@classmethod
def init_options(cls, subparsers) -> None:
"""Sets up this command's options parser"""
sub = subparsers.add_parser(
'key',
help='Manage repository signing keys',
epilog=(
'Note that no verification of key validity is performed when '
'managing key using apt-mange. Ensure that keys are valid to '
'avoid errors with updates.'
)
)
sub.add_argument(
'repository',
default=['x-repolib-none'],
help='Which repository to manage keys for.'
)
sub_group = sub.add_mutually_exclusive_group(
required=True
)
sub_group.add_argument(
'-n',
'--name',
help='The name of an existing key file to set.'
)
sub_group.add_argument(
'-p',
'--path',
help='Sets a path on disk to a signing key.'
)
sub_group.add_argument(
'-u',
'--url',
help='Download a key over HTTPS'
)
sub_group.add_argument(
'-a',
'--ascii',
help='Add an ascii-armored key'
)
sub_group.add_argument(
'-f',
'--fingerprint',
help=(
'Fetch a key via fingerprint from a keyserver. Use --keyserver '
'to specify the URL to the keyserver.'
)
)
sub_group.add_argument(
'-r',
'--remove',
action='store_true',
help=(
'Remove a signing key from the repo. If no other sources use '
'this key, its file will be deleted.'
)
)
sub.add_argument(
'-s',
'--keyserver',
help=(
'The keyserver from which to fetch the given fingerprint. '
'(Default: keyserver.ubuntu.com)'
)
)
def finalize_options(self, args):
super().finalize_options(args)
self.repo = args.repository
self.keyserver = args.keyserver
self.actions:dict = {}
self.system_source = False
for act in [
'name',
'path',
'url',
'ascii',
'fingerprint',
'remove',
]:
self.actions[act] = getattr(args, act)
system.load_all_sources()
def run(self):
"""Run the command"""
self.log.info('Modifying signing key settings for %s', self.repo)
if not self.repo:
self.log.error('No repository provided')
return False
try:
self.source = util.sources[self.repo]
if self.repo == 'system':
self.system_source = True
except KeyError:
self.log.error(
'The repository %s was not found. Check the spelling',
self.repo
)
return False
self.log.debug('Actions to take:\n%s', self.actions)
self.log.debug('Source before:\n%s', self.source)
rets = []
for action in self.actions:
if self.actions[action]:
self.log.debug('Running action: %s - (%s)', action, self.actions[action])
ret = getattr(self, action)(self.actions[action])
rets.append(ret)
self.log.debug('Results: %s', rets)
self.log.debug('Source after: \n%s', self.source)
if True in rets:
self.source.file.save()
return True
else:
self.log.warning('No valid changes specified, no actions taken.')
return False
def name(self, value:str) -> bool:
"""Sets the key file to a name in the key file directory"""
if not value:
return False
key_path = None
for ext in ['.gpg', '.asc']:
if value.endswith(ext):
path = KEYS_PATH / value
if path.exists():
key_path = path
break
else:
if 'archive-keyring' not in value:
value += '-archive-keyring'
path = KEYS_PATH / f'{value}{ext}'
if path.exists():
key_path = path
break
if not key_path:
self.log.error('The key file %s could not be found', value)
return False
return self.path(str(key_path))
def path(self, value:str) -> bool:
"""Sets the key file to the given path"""
if not value:
return False
key_path = Path(value)
if not key_path.exists():
self.log.error(
'The path %s does not exist', value
)
return False
self.source.signed_by = str(key_path)
self.source.load_key()
return True
def url(self, value:str) -> bool:
"""Downloads a key to use from a provided URL."""
if not value:
return False
if not value.startswith('https:'):
response = 'n'
self.log.warning(
'The key is not being downloaded over an encrypted connection, '
'and may be tampered with in-transit. Only add keys that you '
'trust, from sources which you trust.'
)
response = input('Do you want to continue? (y/N): ')
if response not in util.true_values:
return False
key = SourceKey(name=self.source.ident)
key.load_key_data(url=value)
self.source.key = key
self.source.signed_by = str(key.path)
self.source.load_key()
return True
def ascii(self, value:str) -> bool:
"""Loads the key from provided ASCII-armored data"""
if not value:
return False
response = 'n'
print('Only add signing keys from data you trust.')
response = input('Do you want to continue? (y/N): ')
if response not in util.true_values:
return False
key = SourceKey(name=self.source.ident)
key.load_key_data(ascii=value)
self.source.key = key
self.source.signed_by = str(key.path)
self.source.load_key()
return True
def fingerprint(self, value:str) -> bool:
"""Downloads the key with the given fingerprint from a keyserver"""
if not value:
return False
key = SourceKey(name=self.source.ident)
if self.keyserver:
key.load_key_data(fingerprint=value, keyserver=self.keyserver)
else:
key.load_key_data(fingerprint=value)
self.source.key = key
self.source.signed_by = str(key.path)
self.source.load_key()
return True
def remove(self, value:str) -> bool:
"""Removes the key from the source"""
if not self.source.key:
self.log.error(
'The source %s does not have a key configured.',
self.repo
)
return False
response = 'n'
print(
'If you remove the key, there may no longer be any verification '
'of software packages from this repository, including for future '
'updates. This may cause errors with your updates.'
)
response = input('Do you want to continue? (y/N): ')
if response not in util.true_values:
return False
old_key = self.source.key
self.source.key = None
self.source.signed_by = ''
for source in util.sources.values():
if source.key == old_key:
self.log.info(
'Key file %s in use with another key, not deleting',
old_key.path
)
return True
response = 'n'
print('No other sources were found which use this key.')
response = input('Do you want to remove it? (Y/n): ')
if response in util.true_values:
old_key.delete_key()
return True

View File

@ -0,0 +1,216 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import textwrap
import traceback
from ..file import SourceFile, SourceFileError
from ..source import Source, SourceError
from .. import RepoError, util, system
from .command import Command, RepolibCommandError
class List(Command):
"""List subcommand
Lists information about currently-configured sources on the system.
Options:
--legacy, -l
--verbose, -v
--all, -a
--no-names, -n
--file-names, -f
--no-indentation
"""
@classmethod
def init_options(cls, subparsers):
"""Sets up ths argument parser for this command.
Returns: argparse.subparser:
This command's subparser
"""
sub = subparsers.add_parser(
'list',
help=(
'List information for configured repostiories. If a repository '
'name is provided, details about that repository are printed.'
)
)
sub.add_argument(
'repository',
nargs='*',
default=['x-repolib-all-sources'],
help='The repository for which to list configuration'
)
sub.add_argument(
'-v',
'--verbose',
action='store_true',
help='Show additional information, if available.'
)
sub.add_argument(
'-a',
'--all',
action='store_true',
help='Display full configuration for all configured repositories.'
)
sub.add_argument(
'-l',
'--legacy',
action='store_true',
help='Include repositories configured in legacy sources.list file.'
)
sub.add_argument(
'-n',
'--no-names',
action='store_true',
dest='skip_names',
help=argparse.SUPPRESS
)
sub.add_argument(
'-f',
'--file-names',
action='store_true',
dest='print_files',
help="Don't print names of files"
)
sub.add_argument(
'--no-indentation',
action='store_true',
dest='no_indent',
help=argparse.SUPPRESS
)
def finalize_options(self, args):
super().finalize_options(args)
self.repo = ' '.join(args.repository)
self.verbose = args.verbose
self.all = args.all
self.legacy = args.legacy
self.skip_names = args.skip_names
self.print_files = args.print_files
self.no_indent = args.no_indent
def list_legacy(self, indent) -> None:
"""List the contents of the sources.list file.
Arguments:
list_file(Path): The sources.list file to try and parse.
indent(str): An indentation to append to the output
"""
try:
sources_list_file = util.SOURCES_DIR.parent / 'sources.list'
except FileNotFoundError:
sources_list_file = None
print('Legacy source.list sources:')
if sources_list_file:
with open(sources_list_file, mode='r') as file:
for line in file:
if 'cdrom' in line:
line = ''
try:
source = Source()
source.load_from_data([line])
print(textwrap.indent(source.ui, indent))
except SourceError:
pass
def list_all(self):
"""List all sources presently configured in the system
This may include the configuration data for each source as well
"""
indent = ' '
if self.no_indent:
indent = ''
if self.print_files:
print('Configured source files:')
for file in util.files:
print(f'{file.path.name}:')
for source in file.sources:
print(textwrap.indent(source.ui, indent))
if self.legacy:
self.list_legacy(indent)
else:
print('Configured Sources:')
for source in util.sources:
output = util.sources[source]
print(textwrap.indent(output.ui, indent))
if self.legacy:
self.list_legacy(indent)
if util.errors:
print('\n\nThe following files contain formatting errors:')
for err in util.errors:
print(err)
if self.verbose or self.debug:
print('\nDetails about failing files:')
for err in util.errors:
print(f'{err}: {util.errors[err].args[0]}')
return True
def run(self):
"""Run the command"""
system.load_all_sources()
self.log.debug("Current sources: %s", util.sources)
ret = False
if self.all:
return self.list_all()
if self.repo == 'x-repolib-all-sources' and not self.all:
if not self.skip_names:
print('Configured Sources:')
for source in util.sources:
line = source
if not self.skip_names:
line += f' - {util.sources[source].name}'
print(line)
return True
else:
try:
output = util.sources[self.repo]
print(f'Details for source {output.ui}')
return True
except KeyError:
self.log.error(
"Couldn't find the source file for %s, check the spelling",
self.repo
)
return False

View File

@ -0,0 +1,502 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from argparse import SUPPRESS
from .command import Command, RepolibCommandError
from .. import util, system
class Modify(Command):
"""Modify Subcommand
Makes modifications to the specified repository
Options:
--enable, -e
--disable, -d
--default-mirror
--name
--add-suite
--remove-suite
--add-component
--remove-component
--add-uri
--remove-uri
Hidden Options
--add-option
--remove-option
--default-mirror
"""
@classmethod
def init_options(cls, subparsers):
""" Sets up this command's options parser"""
sub = subparsers.add_parser(
'modify',
help='Change a configured repository.'
)
sub.add_argument(
'repository',
nargs='*',
default=['system'],
help='The repository to modify. Default is the system repository.'
)
modify_enable = sub.add_mutually_exclusive_group(
required=False
)
modify_enable.add_argument(
'-e',
'--enable',
action='store_true',
help='Enable the repository, if disabled.'
)
modify_enable.add_argument(
'-d',
'--disable',
action='store_true',
help=(
'Disable the repository, if enabled. The system repository cannot '
'be disabled.'
)
)
modify_source = sub.add_mutually_exclusive_group(
required=False
)
modify_source.add_argument(
'--source-enable',
action='store_true',
help='Enable source code for the repository, if disabled.'
)
modify_source.add_argument(
'--source-disable',
action='store_true',
help='Disable source code for the repository, if enabled.'
)
sub.add_argument(
'--default-mirror',
help=SUPPRESS
#help='Sets the default mirror for the system source.'
)
# Name
sub.add_argument(
'-n',
'--name',
help='Set the repository name to NAME'
)
# Suites
sub.add_argument(
'--add-suite',
metavar='SUITE[,SUITE]',
help=(
'Add the specified suite(s) to the repository. Multiple '
'repositories should be separated with commas. NOTE: Legacy deb '
'repositories may only contain one suite.'
)
)
sub.add_argument(
'--remove-suite',
metavar='SUITE[,SUITE]',
help=(
'Remove the specified suite(s) from the repository. Multiple '
'repositories should be separated with commas. NOTE: The last '
'suite in a repository cannot be removed.'
)
)
# Components
sub.add_argument(
'--add-component',
metavar='COMPONENT[,COMPONENT]',
help=(
'Add the specified component(s) to the repository. Multiple '
'repositories should be separated with commas.'
)
)
sub.add_argument(
'--remove-component',
metavar='COMPONENT[,COMPONENT]',
help=(
'Remove the specified component(s) from the repository. Multiple '
'repositories should be separated with commas. NOTE: The last '
'component in a repository cannot be removed.'
)
)
# URIs
sub.add_argument(
'--add-uri',
metavar='URI[,URI]',
help=(
'Add the specified URI(s) to the repository. Multiple '
'repositories should be separated with commas. NOTE: Legacy deb '
'repositories may only contain one uri.'
)
)
sub.add_argument(
'--remove-uri',
metavar='URI[,URI]',
help=(
'Remove the specified URI(s) from the repository. Multiple '
'repositories should be separated with commas. NOTE: The last '
'uri in a repository cannot be removed.'
)
)
# Options
sub.add_argument(
'--add-option',
metavar='OPTION=VALUE[,OPTION=VALUE]',
help=SUPPRESS
)
sub.add_argument(
'--remove-option',
metavar='OPTION=VALUE[,OPTION=VALUE]',
help=SUPPRESS
)
def finalize_options(self, args):
super().finalize_options(args)
self.count = 0
self.repo = ' '.join(args.repository)
self.enable = args.enable
self.disable = args.disable
self.source_enable = args.source_enable
self.source_disable = args.source_disable
self.actions:dict = {}
self.actions['endisable'] = None
for i in ['enable', 'disable']:
if getattr(args, i):
self.actions['endisable'] = i
self.actions['source_endisable'] = None
for i in ['source_enable', 'source_disable']:
if getattr(args, i):
self.actions['source_endisable'] = i
for arg in [
'default_mirror',
'name',
'add_uri',
'add_suite',
'add_component',
'remove_uri',
'remove_suite',
'remove_component',
]:
self.actions[arg] = getattr(args, arg)
system.load_all_sources()
def run(self):
"""Run the command"""
self.log.info('Modifying repository %s', self.repo)
self.system_source = False
try:
self.source = util.sources[self.repo]
except KeyError:
self.log.error(
'The source %s could not be found. Check the spelling',
self.repo
)
return False
if self.source.ident == 'system':
self.system_source = True
self.log.debug('Actions to take:\n%s', self.actions)
self.log.debug('Source before:\n%s', self.source)
rets = []
for action in self.actions:
ret = getattr(self, action)(self.actions[action])
rets.append(ret)
self.log.debug('Results: %s', rets)
self.log.debug('Source after: \n%s', self.source)
if True in rets:
self.source.file.save()
return True
else:
self.log.warning('No valid changes specified, no actions taken.')
return False
def default_mirror(self, value:str) -> bool:
"""Checks if this is the system source, then set the default mirror"""
if not value:
return False
if self.system_source:
self.source['X-Repolib-Default-Mirror'] = value
return True
return False
def name(self, value:str) -> bool:
"""Sets the source name"""
if not value:
return False
self.log.info('Setting name for %s to %s', self.repo, value)
self.source.name = value
return True
def endisable(self, value:str) -> bool:
"""Enable or disable the source"""
if not value:
return False
self.log.info('%sing source %s', value[:-1], self.repo)
if value == 'disable':
self.source.enabled = False
return True
self.source.enabled = True
return True
def source_endisable(self, value:str) -> bool:
"""Enable/disable source code for the repo"""
if not value:
return False
self.log.info('%sing source code for source %s', value[7:-1], self.repo)
if value == 'source_disable':
self.source.sourcecode_enabled = False
return True
self.source.sourcecode_enabled = True
return True
def add_uri(self, values:str) -> bool:
"""Adds URIs to the source, if not already present."""
if not values:
return False
self.log.info('Adding URIs: %s', values)
uris = self.source.uris
for uri in values.split():
if not util.url_validator(uri):
raise RepolibCommandError(
f'Cannot add URI {uri} to {self.repo}. The URI is '
'malformed'
)
if uri not in uris:
uris.append(uri)
self.log.debug('Added URI %s', uri)
else:
self.log.warning(
'The URI %s was already present in %s',
uri,
self.repo
)
if uris != self.source.uris:
self.source.uris = uris
return True
return False
def remove_uri(self, values:str) -> bool:
"""Remove URIs from the soruce, if they are added."""
if not values:
return False
self.log.info('Removing URIs %s from source %s', values, self.repo)
uris = self.source.uris
self.log.debug('Starting uris: %s', uris)
for uri in values.split():
try:
uris.remove(uri)
self.log.debug('Removed URI %s', uri)
except ValueError:
self.log.warning(
'The URI %s was not present in %s',
uri,
self.repo
)
if len(uris) == 0:
self.log.error(
'Cannot remove the last URI from %s. If you meant to delete the source, try REMOVE instead.',
self.repo
)
return False
if uris != self.source.uris:
self.source.uris = uris
return True
return False
def add_suite(self, values:str) -> bool:
"""Adds a suite to the source"""
if not values:
return False
self.log.info('Adding suites: %s', values)
suites = self.source.suites
for suite in values.split():
if suite not in suites:
suites.append(suite)
self.log.debug('Added suite %s', suite)
else:
self.log.warning(
'The suite %s was already present in %s',
suite,
self.repo
)
if suites != self.source.suites:
self.source.suites = suites
return True
return False
def remove_suite(self, values:str) -> bool:
"""Remove a suite from the source"""
if not values:
return False
self.log.info('Removing suites %s from source %s', values, self.repo)
suites = self.source.suites
self.log.debug('Starting suites: %s', suites)
for suite in values.split():
try:
suites.remove(suite)
self.log.debug('Removed suite %s', suite)
except ValueError:
self.log.warning(
'The suite %s was not present in %s',
suite,
self.repo
)
if len(suites) == 0:
self.log.error(
'Cannot remove the last suite from %s. If you meant to delete the source, try REMOVE instead.',
self.repo
)
return False
if suites != self.source.suites:
self.source.suites = suites
return True
return False
def add_component(self, values:str) -> bool:
"""Adds components to the source"""
if not values:
return False
self.log.info('Adding components: %s', values)
components = self.source.components
for component in values.split():
if component not in components:
components.append(component)
self.log.debug('Added component %s', component)
else:
self.log.warning(
'The component %s was already present in %s',
component,
self.repo
)
if len(components) > 1:
if self.source.file.format == util.SourceFormat.LEGACY:
self.log.warning(
'Adding multiple components to a legacy source is not '
'supported. Consider converting the source to DEB822 format.'
)
if components != self.source.components:
self.source.components = components
return True
return False
def remove_component(self, values:str) -> bool:
"""Removes components from the source"""
if not values:
return False
self.log.info('Removing components %s from source %s', values, self.repo)
components = self.source.components
self.log.debug('Starting components: %s', components)
for component in values.split():
try:
components.remove(component)
self.log.debug('Removed component %s', component)
except ValueError:
self.log.warning(
'The component %s was not present in %s',
component,
self.repo
)
if len(components) == 0:
self.log.error(
'Cannot remove the last component from %s. If you meant to delete the source, try REMOVE instead.',
self.repo
)
return False
if components != self.source.components:
self.source.components = components
return True
return False
def add_option(self, values) -> bool:
"""TODO: Support options"""
raise NotImplementedError(
'Options have not been implemented in this version of repolib yet. '
f'Please edit the file {self.source.file.path} manually.'
)
def remove_option(self, values) -> bool:
"""TODO: Support options"""
raise NotImplementedError(
'Options have not been implemented in this version of repolib yet. '
f'Please edit the file {self.source.file.path} manually.'
)

View File

@ -0,0 +1,128 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from .. import util, system
from .command import Command
class Remove(Command):
"""Remove subcommand
Removes configured sources from the system
Options:
--assume-yes, -y
"""
@classmethod
def init_options(cls, subparsers):
"""Sets up the argument parser for this command
Returns: argparse.subparser
This command's subparser
"""
sub = subparsers.add_parser(
'remove',
help='Remove a configured repository'
)
sub.add_argument(
'repository',
help='The identifier of the repository to remove. See LIST'
)
sub.add_argument(
'-y',
'--assume-yes',
action='store_true',
help='Remove without prompting for confirmation'
)
def finalize_options(self, args):
super().finalize_options(args)
system.load_all_sources()
self.source_name = args.repository
self.assume_yes = args.assume_yes
self.source = None
def run(self):
"""Run the command"""
self.log.info('Looking up %s for removal', self.source_name)
if self.source_name == 'system':
self.log.error('You cannot remove the system sources')
return False
if self.source_name not in util.sources:
self.log.error(
'Source %s was not found. Double-check the spelling',
self.source_name
)
source_list:list = []
for source in util.sources:
source_list.append(source)
suggested_source:str = self.source_name.replace(':', '-')
suggested_source = suggested_source.translate(util.CLEAN_CHARS)
if not suggested_source in source_list:
return False
response:str = input(f'Did you mean "{suggested_source}"? (Y/n) ')
if not response:
response = 'y'
if response not in util.true_values:
return False
self.source_name = suggested_source
self.source = util.sources[self.source_name]
self.key = self.source.key
self.file = self.source.file
print(f'This will remove the source {self.source_name}')
print(self.source.ui)
response:str = 'n'
if self.assume_yes:
response = 'y'
else:
response = input('Are you sure you want to do this? (y/N) ')
if response in util.true_values:
self.file.remove_source(self.source_name)
self.file.save()
system.load_all_sources()
for source in util.sources.values():
self.log.debug('Checking key for %s', source.ident)
try:
if source.key.path == self.key.path:
self.log.info('Source key in use with another source')
return True
except AttributeError:
pass
self.log.info('No other sources found using key, deleting key')
if self.key:
self.key.delete_key()
return True
else:
print('Canceled.')
return False

544
archive/repolib/file.py Normal file
View File

@ -0,0 +1,544 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from pathlib import Path
import dbus
from .source import Source, SourceError
from . import util
FILE_COMMENT = "## Added/managed by repolib ##"
class SourceFileError(util.RepoError):
""" Exception from a source file."""
def __init__(self, *args, code:int = 1, **kwargs):
"""Exception with a source file
Arguments:
:code int, optional, default=1: Exception error code.
"""
super().__init__(*args, **kwargs)
self.code: int = code
class SourceFile:
""" A Source File on disk
Attributes:
path(Pathlib.Path): the path for this file on disk
name(str): The name for this source (filename less the extension)
format(SourceFormat): The format used by this source file
contents(list): A list containing all of this file's contents
"""
def __init__(self, name:str='') -> None:
"""Initialize a source file
Arguments:
name(str): The filename within the sources directory to load
"""
self.log = logging.getLogger(__name__)
self.name:str = ''
self.path:Path = Path()
self.alt_path:Path = Path()
self.format:util.SourceFormat = util.SourceFormat.DEFAULT
self.contents:list = []
self.sources:list = []
self.contents.append(FILE_COMMENT)
self.contents.append('#')
if name:
self.name = name
self.reset_path()
if self.path.exists():
self.contents = []
self.load()
def __str__(self):
return self.output
def __repr__(self):
return f'SourceFile(name={self.name})'
def add_source(self, source:Source) -> None:
"""Adds a source to the file
Arguments:
source(Source): The source to add
"""
if source not in self.sources:
self.contents.append(source)
self.sources.append(source)
source.file = self
def remove_source(self, ident:str) -> None:
"""Removes a source from the file
Arguments:
ident(str): The ident of the source to remove
"""
source = self.get_source_by_ident(ident)
self.contents.remove(source)
self.sources.remove(source)
self.save()
## Remove sources prefs files/pin-priority
prefs_path = source.prefs
try:
if prefs_path.exists() and prefs_path.name:
prefs_path.unlink()
except AttributeError:
# No prefs path
pass
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.delete_prefs_file(str(prefs_path))
def get_source_by_ident(self, ident: str) -> Source:
"""Find a source within this file by its ident
Arguments:
ident(str): The ident to search for
Returns: Source
The located source
"""
self.log.debug(f'Looking up ident {ident} in {self.name}')
for source in self.sources:
if source.ident == ident:
self.log.debug(f'{ident} found')
return source
raise SourceFileError(
f'The file {self.path} does not contain the source {ident}'
)
def reset_path(self) -> None:
"""Attempt to detect the correct path for this File.
We default to DEB822 .sources format files, but if that file doesn't
exist, fallback to legacy .list format. If this also doesn't exist, we
swap back to DEB822 format, as this is likely a new file."""
self.log.debug('Resetting path')
default_path = util.SOURCES_DIR / f'{self.name}.sources'
legacy_path = util.SOURCES_DIR / f'{self.name}.list'
if default_path.exists():
self.path = default_path
self.alt_path = legacy_path
self.format = util.SourceFormat.DEFAULT
elif legacy_path.exists():
self.path = legacy_path
self.alt_path = default_path
self.format = util.SourceFormat.LEGACY
else:
self.path = default_path
self.alt_path = legacy_path
return
def find_unique_ident(self, source1:Source, source2:Source) -> bool:
"""Takes two sources with identical idents, and finds a new, unique
idents for them.
The rules for this are mildly complicated, and vary depending on the
situation:
* (DEB822) If the sources are identical other than some portion of
data, then the two will be combined into a single source.
* (legacy) If the two sources are identical other than source type
(common with legacy-format PPAs with source code) then the second
source will be dropped until export.
* (legacy) If the sources differ by URIs, Components, or Suites, then
the differing data will be appended to the sources' idents.
* (Either) If no other rules can be determined, then the sources will
have a number appended to them
Arguments:
source1(Source): The original source with the ident
source2(Source): The new colliding source with the ident
Returns: bool
`True` if the two sources were successfully deduped, `False` if the
second source should be discarded.
"""
ident_src1:str = source1.ident
ident_src2:str = source2.ident
self.log.debug(f'Idents {ident_src1} and {ident_src2} conflict')
if self.format == util.SourceFormat.DEFAULT:
util.combine_sources(source1, source2)
ident_src2 = ''
else:
excl_keys = [
'X-Repolib-Name',
'X-Repolib-ID',
'X-Repolib-Comment',
'Enabled',
'Types'
]
if len(source1.types) == 1 and len(source2.types) == 1:
if util.compare_sources(source1, source2, excl_keys):
util.combine_sources(source1, source2)
source1.types = [
util.SourceType.BINARY, util.SourceType.SOURCECODE
]
source1.twin_source = True
source1.sourcecode_enabled = source2.enabled
ident_src2 = ''
diffs = util.find_differences_sources(source1, source2, excl_keys)
if diffs:
for key in diffs:
raw_diffs:tuple = diffs[key]
diff1_list = raw_diffs[0].strip().split()
diff2_list = raw_diffs[1].strip().split()
for i in diff1_list:
if i not in diff2_list:
ident_src1 += f'-{i}'
break
for i in diff2_list:
if i not in diff1_list:
ident_src2 += f'-{i}'
break
if ident_src1 != ident_src2:
break
if ident_src2 and ident_src1 != ident_src2:
source1.ident = ident_src1
source2.ident = ident_src2
return True
elif ident_src2 and ident_src1 == ident_src2:
for source in self.sources:
src_index = self.sources.index(source)
source.ident = f'{self.name}-{src_index}'
return True
elif not ident_src2:
return False
return True
def load(self) -> None:
"""Loads the sources from the file on disk"""
self.log.debug(f'Loading source file {self.path}')
self.contents = []
self.sources = []
if not self.name:
raise SourceFileError('You must provide a filename to load.')
if not self.path.exists():
raise SourceFileError(f'The file {self.path} does not exist.')
with open(self.path, 'r') as source_file:
srcfile_data = source_file.readlines()
item:int = 0
raw822:list = []
parsing_deb822:bool = False
source_name:str = ''
commented:bool = False
idents:dict = {}
# Main file parsing loop
for line in srcfile_data:
comment_found:str = ''
name_line:bool = 'X-Repolib-Name' in line
if not parsing_deb822:
commented = line.startswith('#')
# Find commented out lines
if commented:
# Exclude disabled legacy deblines
valid_legacy = util.validate_debline(line.strip())
if not valid_legacy and not name_line:
# Found a standard comment
self.contents.append(line.strip())
elif valid_legacy:
if self.format != util.SourceFormat.LEGACY:
raise SourceFileError(
f'File {self.path.name} is an updated file, but '
'contains legacy-format sources. This is not '
'allowed. Please fix the file manually.'
)
new_source = Source()
new_source.load_from_data([line])
if source_name:
new_source.name = source_name
if not new_source.ident:
new_source.ident = self.name
to_add:bool = True
if new_source.ident in idents:
old_source = idents[new_source.ident]
idents.pop(old_source.ident)
to_add = self.find_unique_ident(old_source, new_source)
idents[old_source.ident] = old_source
idents[new_source.ident] = new_source
if to_add:
new_source.file = self
self.contents.append(new_source)
self.sources.append(new_source)
elif name_line:
source_name = ':'.join(line.split(':')[1:])
source_name = source_name.strip()
# Active legacy line
elif not commented:
if util.validate_debline(line.strip()):
if self.format != util.SourceFormat.LEGACY:
raise SourceFileError(
f'File {self.path.name} is an updated file, but '
'contains legacy-format sources. This is not '
'allowed. Please fix the file manually.'
)
new_source = Source()
new_source.load_from_data([line])
if source_name:
new_source.name = source_name
if not new_source.ident:
new_source.ident = self.name
to_add:bool = True
if new_source.ident in idents:
old_source = idents[new_source.ident]
idents.pop(old_source.ident)
to_add = self.find_unique_ident(old_source, new_source)
idents[old_source.ident] = old_source
idents[new_source.ident] = new_source
if to_add:
new_source.file = self
self.contents.append(new_source)
self.sources.append(new_source)
# Empty lines are treated as comments
if line.strip() == '':
self.contents.append('')
# Find 822 sources
# Valid sources can begin with any key:
for key in util.valid_keys:
if line.startswith(key):
if self.format == util.SourceFormat.LEGACY:
raise SourceFileError(
f'File {self.path.name} is a DEB822-format file, but '
'contains legacy sources. This is not allowed. '
'Please fix the file manually.'
)
parsing_deb822 = True
raw822.append(line.strip())
item += 1
elif parsing_deb822:
# Deb822 sources are terminated with an empty line
if line.strip() == '':
parsing_deb822 = False
new_source = Source()
new_source.load_from_data(raw822)
new_source.file = self
if source_name:
new_source.name = source_name
if not new_source.ident:
new_source.ident = self.name
if new_source.ident in idents:
old_source = idents[new_source.ident]
idents.pop(old_source.ident)
self.find_unique_ident(old_source, new_source)
idents[old_source.ident] = old_source
idents[new_source.ident] = new_source
new_source.file = self
self.contents.append(new_source)
self.sources.append(new_source)
raw822 = []
item += 1
self.contents.append('')
else:
raw822.append(line.strip())
if raw822:
parsing_deb822 = False
new_source = Source()
new_source.load_from_data(raw822)
new_source.file = self
if source_name:
new_source.name = source_name
if not new_source.ident:
new_source.ident = self.name
if new_source.ident in idents:
old_source = idents[new_source.ident]
idents.pop(old_source.ident)
self.find_unique_ident(old_source, new_source)
idents[old_source.ident] = old_source
idents[new_source.ident] = new_source
new_source.file = self
self.contents.append(new_source)
self.sources.append(new_source)
raw822 = []
item += 1
self.contents.append('')
for source in self.sources:
if not source.has_required_parts:
raise SourceFileError(
f'The file {self.path.name} is malformed and contains '
'errors. Maybe it has some extra new-lines?'
)
self.log.debug('File %s loaded', self.path)
def save(self) -> None:
"""Saves the source file to disk using the current format"""
self.log.debug(f'Saving source file to {self.path}')
for source in self.sources:
self.log.debug('New Source %s: \n%s', source.ident, source)
save_path = util.SOURCES_DIR / f'{self.name}.save'
for source in self.sources:
if source.key:
source.key.save_gpg()
source.tasks_save()
if not self.name or not self.format:
raise SourceFileError('There was not a complete filename to save')
if not util.SOURCES_DIR.exists():
try:
util.SOURCES_DIR.mkdir(parents=True)
except PermissionError:
self.log.error(
'Source destination path does not exist and cannot be created '
'Failures expected now.'
)
if len(self.sources) > 0:
self.log.debug('Saving, Main path %s; Alt path: %s', self.path, self.alt_path)
try:
with open(self.path, mode='w') as output_file:
output_file.write(self.output)
if self.alt_path.exists():
self.alt_path.rename(save_path)
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.output_file_to_disk(self.path.name, self.output)
self.log.debug('File %s saved', self.path)
else:
try:
self.path.unlink(missing_ok=True)
self.alt_path.unlink(missing_ok=True)
save_path.unlink(missing_ok=True)
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.delete_source_file(self.path.name)
self.log.debug('File %s removed', self.path)
## Attribute properties
@property
def format(self) -> util.SourceFormat: # type: ignore (We don't use str.format)
"""The format of the file on disk"""
return self._format
@format.setter
def format(self, format:util.SourceFormat) -> None: # type: ignore (We don't use str.format)
"""The path needs to be updated when the format changes"""
alt_format:util.SourceFormat = util.SourceFormat.LEGACY
self._format = format
self.path = util.SOURCES_DIR / f'{self.name}.{self._format.value}'
for format_ in util.SourceFormat:
if format != format_:
alt_format = format_
self.alt_path = util.SOURCES_DIR / f'{self.name}.{alt_format.value}'
## Output properties
@property
def legacy(self) -> str:
"""Outputs the file in the output_legacy format"""
legacy_output:str = ''
for item in self.contents:
try:
legacy_output += item.legacy
except AttributeError:
legacy_output += item
legacy_output += '\n'
return legacy_output
@property
def deb822(self) -> str:
"""Outputs the file in the output_822 format"""
deb822_output:str = ''
for item in self.contents:
try:
deb822_output += item.deb822
except AttributeError:
deb822_output += item
deb822_output += '\n'
return deb822_output
@property
def ui(self) -> str:
"""Outputs the file in the output_ui format"""
ui_output:str = ''
for item in self.contents:
try:
ui_output += item.ui
except AttributeError:
pass # Skip file comments in UI mode
ui_output += '\n'
return ui_output
@property
def output(self) -> str:
"""Outputs the file in the output format"""
default_output:str = ''
for item in self.contents:
try:
if self.format == util.SourceFormat.DEFAULT:
default_output += item.deb822
elif self.format == util.SourceFormat.LEGACY:
default_output += item.legacy
default_output += '\n'
except AttributeError:
default_output += item
default_output += '\n'
return default_output

204
archive/repolib/key.py Normal file
View File

@ -0,0 +1,204 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import shutil
import dbus
import gnupg
from pathlib import Path
from urllib import request
from . import util
SKS_KEYSERVER = 'https://keyserver.ubuntu.com/'
SKS_KEYLOOKUP_PATH = 'pks/lookup?op=get&options=mr&exact=on&search=0x'
class KeyFileError(util.RepoError):
""" Exceptions related to apt key files."""
def __init__(self, *args, code=1, **kwargs):
"""Exceptions related to apt key files.
Arguments:
code (:obj:`int`, optional, default=1): Exception error code.
"""
super().__init__(*args, **kwargs)
self.code = code
class SourceKey:
"""A signing key for an apt source."""
def __init__(self, name:str = '') -> None:
self.log = logging.getLogger(__name__)
self.tmp_path = Path()
self.path = Path()
self.gpg = gnupg.GPG()
self.data = b''
if name:
self.reset_path(name=name)
self.setup_gpg()
def reset_path(self, name: str = '', path:str = '', suffix: str = 'archive-keyring') -> None:
"""Set the path for this key
Arguments:
suffix(str): The suffix to append to the end of the name to get the
file name (default: 'archive-keyring')
name(str): The name of the source
path(str): The entire path to the key
"""
self.log.info('Setting path')
if not name and not path:
raise KeyFileError('A name is required to set the path for this key')
if name:
file_name = f'{name}-{suffix}.gpg'
self.tmp_path = util.TEMP_DIR / file_name
self.path = util.KEYS_DIR / file_name
elif path:
self.path = Path(path)
self.tmp_path = util.TEMP_DIR / self.path.name
self.setup_gpg()
self.log.debug('Key Path: %s', self.path)
self.log.debug('Temp Path: %s', self.tmp_path)
def setup_gpg(self) -> None:
"""Set up the GPG object for this key."""
self.log.info('Setting up GPG')
self.log.debug('Copying %s to %s', self.path, self.tmp_path)
try:
shutil.copy2(self.path, self.tmp_path)
except FileNotFoundError:
pass
self.gpg = gnupg.GPG(keyring=str(self.tmp_path))
self.log.debug('GPG Setup: %s', self.gpg.keyring)
def save_gpg(self) -> None:
"""Saves the key to disk."""
self.log.info('Saving key file %s from %s', self.path, self.tmp_path)
self.log.debug('Key contents: %s', self.gpg.list_keys())
self.log.debug('Temp key exists? %s', self.tmp_path.exists())
if not util.KEYS_DIR.exists():
try:
util.KEYS_DIR.mkdir(parents=True)
except PermissionError:
self.log.error(
'Key destination path does not exist and cannot be created '
'Failures expected now.'
)
try:
shutil.copy(self.tmp_path, self.path)
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.install_signing_key(
str(self.tmp_path),
str(self.path)
)
def delete_key(self) -> None:
"""Deletes the key file from disk."""
try:
self.tmp_path.unlink()
self.path.unlink()
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.delete_signing_key(str(self.path))
except FileNotFoundError:
pass
def load_key_data(self, **kwargs) -> None:
"""Loads the key data from disk into the object for processing.
Each of the keyword options specifies one place to look to import key
data. Once one is successfully imported, the method returns, so passing
multiple won't import multiple keys.
Keyword Arguments:
raw(bytes): Raw data to import to the keyring
ascii(str): ASCII-armored key data to import directly
url(str): A URL to download key data from
fingerprint(str): A key fingerprint to download from `keyserver`
keyserver(str): A keyserver to download from.
keypath(str): The path on the keyserver from which to download.
NOTE: The keyserver and keypath args only affect the operation of the
`fingerprint` keyword.
"""
if self.path.exists():
with open(self.path, mode='rb') as keyfile:
self.data = keyfile.read()
return
self.tmp_path.touch()
if 'raw' in kwargs:
self.data = kwargs['raw']
self.gpg.import_keys(self.data)
return
if 'ascii' in kwargs:
self.gpg.import_keys(kwargs['ascii'])
if self.tmp_path.exists():
with open(self.tmp_path, mode='rb') as keyfile:
self.data = keyfile.read()
return
if 'url' in kwargs:
req = request.Request(kwargs['url'])
with request.urlopen(req) as response:
self.data = response.read().decode('UTF-8')
self.gpg.import_keys(self.data)
return
if 'fingerprint' in kwargs:
if not 'keyserver' in kwargs:
kwargs['keyserver'] = SKS_KEYSERVER
if not 'keypath' in kwargs:
kwargs['keypath'] = SKS_KEYLOOKUP_PATH
key_url = kwargs['keyserver'] + kwargs['keypath'] + kwargs['fingerprint']
req = request.Request(key_url)
with request.urlopen(req) as response:
self.data = response.read().decode('UTF-8')
self.gpg.import_keys(self.data)
return
raise TypeError(
f'load_key_data() got an unexpected keyword argument "{kwargs.keys()}',
' Expected keyword arguments are: [raw, ascii, url, fingerprint]'
)

371
archive/repolib/parsedeb.py Normal file
View File

@ -0,0 +1,371 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from . import util
log = logging.getLogger(__name__)
class DebParseError(util.RepoError):
""" Exceptions related to parsing deb lines."""
def __init__(self, *args, code=1, **kwargs):
"""Exceptions related to parsing deb lines.
Arguments:
code (:obj:`int`, optional, default=1): Exception error code.
"""
super().__init__(*args, **kwargs)
self.code = code
def debsplit(line:str) -> list:
""" Improved string.split() with support for things like [] options.
Adapted from python-apt
Arguments:
line(str): The line to split up.
"""
line = line.strip()
line_list = line.split()
for i in line_list:
if util.url_validator(i):
line_list[line_list.index(i)] = decode_brackets(i)
line = ' '.join(line_list)
pieces:list = []
tmp:str = ""
# we are inside a [..] block
p_found = False
for char in line:
if char == '[':
p_found = True
tmp += char
elif char == ']':
p_found = False
tmp += char
elif char.isspace() and not p_found:
pieces.append(tmp)
tmp = ''
continue
else:
tmp += char
# append last piece
if len(tmp) > 0:
pieces.append(tmp)
return pieces
def encode_brackets(word:str) -> str:
""" Encodes any [ and ] brackets into URL-safe form
Technically we should never be recieving these, and there are other things
which should technically be encoded as well. However, square brackets
actively break the URL parsing, and must be strictly avoided.
Arguments:
word (str): the string to encode brackets in.
Returns:
`str`: the encoded string.
"""
word = word.replace('[', '%5B')
word = word.replace(']', '%5D')
return word
def decode_brackets(word:str) -> str:
""" Un-encodes [ and ] from the input
Since our downstream libraries should also be encoding these correctly, it
is better to retain these as the user entered, as that ensures they can
recognize it properly.
Arguments:
word (str): The string to decode.
Returns:
`str`: the decoded string.
"""
word = word.replace('%5B', '[')
word = word.replace('%5D', ']')
return word
def parse_name_ident(tail:str) -> tuple:
""" Find a Repolib name within the given comment string.
The name should be headed with "X-Repolib-Name:" and is not space terminated.
The ident should be headed with "X-Repolib-ID:" and is space terminated.
Either field ends at the end of a line, or at a subsequent definition of a
different field, or at a subsequent ' #' substring. Additionally, the ident
field ends with a subsequent space.
Arguments:
tail (str): The comment to search within.
Returns: tuple(name, ident, comment):
name (str): The detected name, or None
ident (str): The detected ident, or None
comment (str): The string with the name and ident removed
"""
tail = util.strip_hashes(tail)
# Used for sanity checking later
has_name = 'X-Repolib-Name' in tail
log.debug('Line name found: %s', has_name)
has_ident = 'X-Repolib-ID' in tail
log.debug('Line ident found: %s', has_ident)
parts: list = tail.split()
name_found = False
ident_found = False
name:str = ''
ident:str = ''
comment:str = ''
for item in parts:
log.debug("Checking line item: %s", item)
item_is_name = item.strip('#').strip().startswith('X-Repolib-Name')
item_is_ident = item.strip('#').strip().startswith('X-Repolib-ID')
if '#' in item and not item_is_name and not item_is_ident:
name_found = False
ident_found = False
elif item_is_name:
name_found = True
ident_found = False
continue
elif item_is_ident:
name_found = False
ident_found = True
continue
if name_found and not item_is_name:
name += f'{item} '
continue
elif ident_found and not item_is_ident:
ident += f'{item}'
ident_found = False
continue
elif not name_found and not ident_found:
c = item.strip('#')
comment += f'{c} '
name = name.strip()
ident = ident.strip()
comment = comment.strip()
if not name:
if ident:
name = ident
# Final sanity checking
if has_name and not name:
raise DebParseError(
f'Could not parse repository name from comment {comment}. Make sure '
'you have a space between the colon and the Name'
)
if has_ident and not ident:
raise DebParseError(
f'Could not parse repository ident from comment {comment}. Make sure '
'you have a space between the colon and the Ident'
)
return name, ident, comment
class ParseDeb:
""" Parsing for source entries.
Contains parsing helpers for one-line format sources.
"""
def __init__(self, debug:bool = False) -> None:
"""
Arguments:
debug (bool): In debug mode, the structured data is always returned
at the end, instead of checking for sanity (default: `False`)
"""
self.debug = debug
self.last_line: str = ''
self.last_line_valid: bool = False
self.curr_line: str = ''
self.curr_line_valid: bool = False
def parse_options(self, opt:str) -> dict:
""" Parses a string of options into a dictionary that repolib can use.
Arguments:
opt(str): The string with options returned from the line parser.
Returns:
`dict`: The dictionary of options with key:val pairs (may be {})
"""
opt = opt.strip()
opt = opt[1:-1].strip() # Remove enclosing brackets
options = opt.split()
parsed_options:dict = {}
for opt in options:
pre_key, values = opt.split('=')
values = values.split(',')
value:str = ' '.join(values)
try:
key:str = util.options_inmap[pre_key]
except KeyError:
raise DebParseError(
f'Could not parse line {self.curr_line}: option {opt} is '
'not a valid debian repository option or is unsupported.'
)
parsed_options[key] = value
return parsed_options
def parse_line(self, line:str) -> dict:
""" Parse a deb line into its individual parts.
Adapted from python-apt
Arguments:
line (str): The line input to parse
Returns:
(dict): a dict containing the requisite data.
"""
self.last_line = self.curr_line
self.last_line_valid = self.curr_line_valid
self.curr_line = line.strip()
parts:list = []
line_is_comment = self.curr_line == '#'
line_is_empty = self.curr_line == ''
if line_is_comment or line_is_empty:
raise DebParseError(f'Current line "{self.curr_line}" is empty')
line_parsed: dict = {}
line_parsed['enabled'] = True
line_parsed['name'] = ''
line_parsed['ident'] = ''
line_parsed['comments'] = []
line_parsed['repo_type'] = ''
line_parsed['uri'] = ''
line_parsed['suite'] = ''
line_parsed['components'] = []
line_parsed['options'] = {}
if line.startswith('#'):
line_parsed['enabled'] = False
line = util.strip_hashes(line)
parts = line.split()
if not parts[0] in ('deb', 'deb-src'):
raise DebParseError(f'Current line "{self.curr_line}" is invalid')
comments_index = line.find('#')
if comments_index > 0:
raw_comments:str = line[comments_index + 1:].strip()
(
line_parsed['name'],
line_parsed['ident'],
comments
) = parse_name_ident(raw_comments)
line_parsed['comments'].append(comments)
line = line[:comments_index]
parts = debsplit(line)
if len(parts) < 3: # We need at least a type, a URL, and a component
raise DebParseError(
f'The line "{self.curr_line}" does not have enough pieces to be'
'valid'
)
# Determine the type of the repo
repo_type:str = parts.pop(0)
if repo_type in ['deb', 'deb-src']:
line_parsed['repo_type'] = util.SourceType(repo_type)
else:
raise DebParseError(f'The line "{self.curr_line}" is of invalid type.')
# Determine the properties of our repo line
uri_index:int = 0
is_cdrom: bool = False
## The URI index is the vital piece of information we need to parse the
## deb line, as it's position determines what other components are
## present and where they are. This determines the location of the URI
## regardless of where it's at.
for part in parts:
if part.startswith('['):
if 'cdrom' in part:
is_cdrom = True
uri_index = parts.index(part)
else:
uri_index = 1
if is_cdrom:
# This could maybe change if the parser now differentiates between
# CDROM URIs and option lists
raise DebParseError('Repolib cannot currently accept CDROM Sources')
if uri_index != 0:
line_parsed['options'] = self.parse_options(parts.pop(0))
if len(line_parsed) < 2: # Should have at minimum a URI and a suite/path
raise DebParseError(
f'The line "{self.curr_line}" does not have enough pieces to be'
'valid'
)
line_uri = parts.pop(0)
if util.url_validator(line_uri):
line_parsed['uri'] = line_uri
else:
raise DebParseError(
f'The line "{self.curr_line}" has invalid URI: {line_uri}'
)
line_parsed['suite'] = parts.pop(0)
line_components:list = []
for comp in parts:
line_parsed['components'].append(comp)
has_type = line_parsed['repo_type']
has_uri = line_parsed['uri']
has_suite = line_parsed['suite']
if has_type and has_uri and has_suite:
# if we have these three minimum components, we can proceed and the
# line is valid. Otherwise, error out.
return line_parsed.copy()
if self.debug:
return line_parsed.copy()
raise DebParseError(
f'The line {self.curr_line} could not be parsed due to an '
'unknown error (Probably missing the repo type, URI, or a '
'suite/path).'
)

View File

@ -0,0 +1,32 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
from .popdev import PopdevSource
from .ppa import PPASource
from ..source import Source
shortcut_prefixes = {
'deb': Source,
'deb-src': Source,
ppa.prefix: ppa.PPASource,
popdev.prefix: popdev.PopdevSource
}

View File

@ -0,0 +1,171 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from pathlib import Path
import dbus
from repolib.key import SourceKey
from ..source import Source, SourceError
from ..file import SourceFile
from .. import util
BASE_FORMAT = util.SourceFormat.DEFAULT
BASE_URL = 'http://apt.pop-os.org/staging'
BASE_COMPS = 'main'
BASE_KEYURL = 'https://raw.githubusercontent.com/pop-os/pop/master/scripts/.iso.asc'
DEFAULT_FORMAT = util.SourceFormat.DEFAULT
prefix = 'popdev'
delineator = ':'
class PopdevSource(Source):
""" PopDev Source shortcut
These are given in the format popdev:branchname.
Arguments:
shortcut (str): The ppa: shortcut to process
"""
prefs_dir = Path('/etc/apt/preferences.d')
default_format = BASE_FORMAT
@staticmethod
def validator(shortcut:str) -> bool:
"""Determine whether a PPA shortcut is valid.
Arguments:
shortcut(str): The shortcut to validate
Returns: bool
`True` if the PPA is valid, otherwise False
"""
if '/' in shortcut:
return False
shortcut_split = shortcut.split(':')
try:
if not shortcut_split[1]:
return False
except IndexError:
return False
if shortcut.startswith(f'{prefix}:'):
shortlist = shortcut.split(':')
if len(shortlist) > 0:
return True
return False
def __init__(self, *args, line='', fetch_data=True, **kwargs):
if line:
if not line.startswith('ppa:'):
raise SourceError(f'The PPA shortcut {line} is malformed')
super().__init__(args, kwargs)
self.log = logging.getLogger(__name__)
self.line = line
self.twin_source:bool = True
self.prefs_path = None
self.branch_name:str = ''
self.branch_url:str = ''
if line:
self.load_from_shortcut(line)
def tasks_save(self, *args, **kwargs) -> None:
super().tasks_save(*args, **kwargs)
self.log.info('Saving prefs file for %s', self.ident)
prefs_contents = 'Package: *\n'
prefs_contents += f'Pin: release o=pop-os-staging-{self.branch_url}\n'
prefs_contents += 'Pin-Priority: 1002\n'
self.log.debug('%s prefs for pin priority:\n%s', self.ident, prefs_contents)
try:
with open(self.prefs, mode='w') as prefs_file:
prefs_file.write(prefs_contents)
except PermissionError:
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.output_prefs_to_disk(str(self.prefs), prefs_contents)
self.log.debug('Pin priority saved for %s', self.ident)
def get_description(self) -> str:
return f'Pop Development Staging branch'
def load_from_data(self, data: list) -> None:
self.log.debug('Loading line %s', data[0])
self.load_from_shortcut(shortcut=data[0])
def load_from_shortcut(self, shortcut:str='', meta:bool=True, get_key:bool=True) -> None:
"""Translates the shortcut line into a full repo.
Arguments:
shortcut(str): The shortcut to load, if one hasn't been loaded yet.
"""
self.reset_values()
if shortcut:
self.line = shortcut
if not self.line:
raise SourceError('No PPA shortcut provided')
if not self.validator(self.line):
raise SourceError(f'The line {self.line} is malformed')
self.log.debug('Loading shortcut %s', self.line)
self.info_parts = shortcut.split(delineator)
self.branch_url = ':'.join(self.info_parts[1:])
self.branch_name = util.scrub_filename(name=self.branch_url)
self.log.debug('Popdev branch name: %s', self.branch_name)
self.ident = f'{prefix}-{self.branch_name}'
if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
new_file = SourceFile(name=self.ident)
new_file.format = BASE_FORMAT
self.file = new_file
util.files[str(self.file.path)] = self.file
else:
self.file = util.files[str(self.file.path)]
self.file.add_source(self)
self.name = f'Pop Development Branch {self.branch_name}'
self.uris = [f'{BASE_URL}/{self.branch_url}']
self.suites = [util.DISTRO_CODENAME]
self.components = [BASE_COMPS]
key = SourceKey(name='popdev')
key.load_key_data(url=BASE_KEYURL)
self.key = key
self.signed_by = str(self.key.path)
self.prefs_path = self.prefs_dir / f'pop-os-staging-{self.branch_name}'
self.prefs = self.prefs_path
self.enabled = True

View File

@ -0,0 +1,274 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from repolib.key import SourceKey
from ..source import Source, SourceError
from ..file import SourceFile
from .. import util
try:
from launchpadlib.launchpad import Launchpad
from lazr.restfulclient.errors import BadRequest, NotFound, Unauthorized
except ImportError:
raise SourceError(
'Missing optional dependency "launchpadlib". Try `sudo apt install '
'python3-launchpadlib` to install it.'
)
BASE_FORMAT = util.SourceFormat.LEGACY
BASE_URL = 'http://ppa.launchpad.net'
BASE_DIST = 'ubuntu'
BASE_COMPS = 'main'
DEFAULT_FORMAT = util.SourceFormat.LEGACY
prefix = 'ppa'
delineator = ':'
class PPASource(Source):
""" PPA Source shortcut
These are given in the format ppa:owner/name. Much of this code is adapted
from SoftwareProperties.
Arguments:
shortcut (str): The ppa: shortcut to process
fetch_data (bool): Whether to try and fetch metadata from Launchpad.
"""
default_format = BASE_FORMAT
@staticmethod
def validator(shortcut:str) -> bool:
"""Determine whether a PPA shortcut is valid.
Arguments:
shortcut(str): The shortcut to validate
Returns: bool
`True` if the PPA is valid, otherwise False
"""
if shortcut.startswith(f'{prefix}:'):
shortlist = shortcut.split('/')
if len(shortlist) > 1:
return True
return False
def __init__(self, *args, line='', fetch_data=True, **kwargs):
if line:
if not line.startswith('ppa:'):
raise SourceError(f'The PPA shortcut {line} is malformed')
super().__init__(args, kwargs)
self.log = logging.getLogger(__name__)
self.line = line
self.ppa = None
self.twin_source = True
self._displayname = ''
self._description = ''
if line:
self.load_from_shortcut(self.line)
def get_description(self) -> str:
output:str = ''
output += self.displayname
output += '\n\n'
output += self.description
return output
def load_from_data(self, data: list) -> None:
self.load_from_shortcut(shortcut=data[0])
def load_from_shortcut(self, shortcut:str='', meta:bool=True, key:bool=True) -> None:
"""Translates the shortcut line into a full repo.
Arguments:
shortcut(str): The shortcut to load, if one hasn't been loaded yet.
meta(bool): Whether to fetch repo metadata from Launchpad
key(bool): Whether to fetch and install a signing key
"""
self.reset_values()
if shortcut:
self.line = shortcut
if not self.line:
raise SourceError('No PPA shortcut provided')
if not self.validator(self.line):
raise SourceError(f'The line {self.line} is malformed')
line = self.line.replace(prefix + delineator, '')
self.info_parts = line.split('/')
ppa_owner = self.info_parts[0]
ppa_name = self.info_parts[1]
self.ident = f'{prefix}-{ppa_owner}-{ppa_name}'
if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
new_file = SourceFile(name=self.ident)
new_file.format = BASE_FORMAT
self.file = new_file
util.files[str(self.file.path)] = self.file
else:
self.file = util.files[str(self.file.path)]
self.file.add_source(self)
self.name = self.ident
self.uris = [f'{BASE_URL}/{ppa_owner}/{ppa_name}/{BASE_DIST}']
self.suites = [util.DISTRO_CODENAME]
self.components = [BASE_COMPS]
if meta or key:
self.ppa = get_info_from_lp(ppa_owner, ppa_name)
self.displayname = self.ppa.displayname
self.description = self.ppa.description
if self.ppa and meta:
self.name = self.ppa.displayname
if self.ppa and key:
repo_key = SourceKey(name=self.ident)
if str(repo_key.path) not in util.keys:
repo_key.load_key_data(fingerprint=self.ppa.fingerprint)
util.keys[str(repo_key.path)] = repo_key
self.key:SourceKey = repo_key
else:
self.key = util.keys[repo_key.path]
self.signed_by = str(self.key.path)
self.enabled = True
@property
def displayname(self) -> str:
"""The name of the PPA provided by launchpad"""
if self._displayname:
return self._displayname
if self.ppa:
self._displayname = self.ppa.displayname
return self._displayname
@displayname.setter
def displayname(self, displayname) -> None:
"""Cache this for use without hitting LP"""
self._displayname = displayname
@property
def description(self) -> str:
"""The description of the PPA provided by Launchpad"""
if self._description:
return self._description
if self.ppa:
self._description = self.ppa.description
return self._description
@description.setter
def description(self, desc) -> None:
"""Cache this for use without hitting LP"""
self._description = desc
class PPA:
""" An object to fetch data from PPAs.
Portions of this class were adapted from Software Properties
"""
def __init__(self, teamname, ppaname):
self.teamname = teamname
self.ppaname = ppaname
self._lap = None
self._lpteam = None
self._lpppa = None
self._signing_key_data = None
self._fingerprint = None
@property
def lap(self):
""" The Launchpad Object."""
if not self._lap:
self._lap = Launchpad.login_anonymously(
f'{self.__module__}.{self.__class__.__name__}',
service_root='production',
version='devel'
)
return self._lap
@property
def lpteam(self):
""" The Launchpad object for the PPA's owner."""
if not self._lpteam:
try:
self._lpteam = self.lap.people(self.teamname) # type: ignore (This won't actually be unbound because of the property)
except NotFound as err:
msg = f'User/Team "{self.teamname}" not found'
raise SourceError(msg) from err
except Unauthorized as err:
msg = f'Invalid user/team name "{self.teamname}"'
raise SourceError(msg) from err
return self._lpteam
@property
def lpppa(self):
""" The Launchpad object for the PPA."""
if not self._lpppa:
try:
self._lpppa = self.lpteam.getPPAByName(name=self.ppaname)
except NotFound as err:
msg = f'PPA "{self.teamname}/{self.ppaname}"" not found'
raise SourceError(msg) from err
except BadRequest as err:
msg = f'Invalid PPA name "{self.ppaname}"'
raise SourceError(msg) from err
return self._lpppa
@property
def description(self) -> str:
"""str: The description of the PPA."""
return self.lpppa.description or ''
@property
def displayname(self) -> str:
""" str: the fancy name of the PPA."""
return self.lpppa.displayname or ''
@property
def fingerprint(self):
""" str: the fingerprint of the signing key."""
if not self._fingerprint:
self._fingerprint = self.lpppa.signing_key_fingerprint
return self._fingerprint
def get_info_from_lp(owner_name, ppa):
""" Attempt to get information on a PPA from launchpad over the internet.
Arguments:
owner_name (str): The Launchpad user owning the PPA.
ppa (str): The name of the PPA
Returns:
json: The PPA information as a JSON object.
"""
ppa = PPA(owner_name, ppa)
return ppa

920
archive/repolib/source.py Normal file
View File

@ -0,0 +1,920 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from pathlib import Path
from debian import deb822
from .parsedeb import ParseDeb
from .key import SourceKey
from . import util
DEFAULT_FORMAT = util.SourceFormat.LEGACY
class SourceError(util.RepoError):
""" Exception from a source object."""
def __init__(self, *args, code=1, **kwargs):
"""Exception with a source object
Arguments:
code (:obj:`int`, optional, default=1): Exception error code.
"""
super().__init__(*args, **kwargs)
self.code = code
class Source(deb822.Deb822):
"""A DEB822 object representing a single software source.
Attributes:
ident(str): The unique id for this source
name(str): The user-readable name for this source
enabled(bool): Whether or not the source is enabled
types([SourceType]): A list of repository types for this source
uris([str]): A list of possible URIs for this source
suites([str]): A list of enabled suites for this source
components([str]): A list of enabled components for this source
comments(str): Comments for this source
signed_by(Path): The path to this source's key file
file(SourceFile): The file this source belongs to
key(SourceKey): The key which signs this source
"""
default_format = DEFAULT_FORMAT
@staticmethod
def validator(shortcut:str) -> bool:
"""Determine whether a deb line is valid.
Arguments:
shortcut(str): The shortcut to validate
Returns: bool
`True` if the PPA is valid, otherwise False
"""
shortcut_list:list = shortcut.split()
if not shortcut.startswith('deb'):
return False
if not len(shortcut_list) > 3:
return False
if not util.validate_debline:
return False
if len(shortcut_list) == 3 and '/' not in shortcut_list[-1]:
return False
return True
def __init__(self, *args, file=None, **kwargs) -> None:
"""Initialize this source object"""
self.log = logging.getLogger(__name__)
super().__init__(*args, **kwargs)
self.reset_values()
self.file = file
self.twin_source = False
self.twin_enabled = False
def __repr__(self):
"""type: () -> str"""
# Append comments to the item
# if self.options:
if self.comments:
self['Comments'] = '# '
self['Comments'] += ' # '.join(self.comments)
rep:str = '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
rep:str = '{'
for key in self:
rep += f"{util.PRETTY_PRINT}'{key}': '{self[key]}', "
rep = rep[:-2]
rep += f"{util.PRETTY_PRINT.replace(' ', '')}"
rep += '}'
if self.comments:
self.pop('Comments')
return rep
def __bool__(self) -> bool:
has_uri:bool = len(self.uris) > 0
has_suite:bool = len(self.suites) > 0
has_component:bool = len(self.components) > 0
if has_uri and has_suite and has_component:
return True
return False
def get_description(self) -> str:
"""Get a UI-compatible description for a source.
Returns: (str)
The formatted description.
"""
return self.name
def reset_values(self) -> None:
"""Reset the default values for all attributes"""
self.log.info('Resetting source info')
self.ident = ''
self.name = ''
self.enabled = True
self.types = [util.SourceType.BINARY]
self.uris = []
self.suites = []
self.components = []
self.comments = []
self.signed_by = None
self.architectures = ''
self.languages = ''
self.targets = ''
self.pdiffs = ''
self.by_hash = ''
self.allow_insecure = ''
self.allow_weak = ''
self.allow_downgrade_to_insecure = ''
self.trusted = ''
self.signed_by = ''
self.check_valid_until = ''
self.valid_until_min = ''
self.valid_until_max = ''
self.prefs = ''
self._update_legacy_options()
self.file = None
self.key = None
def load_from_data(self, data:list) -> None:
"""Loads source information from the provided data
Should correctly load either a lecagy Deb line (optionally with
preceeding comment) or a DEB822 source.
Arguments:
data(list): the data to load into the source.
"""
self.log.info('Loading source from data')
self.reset_values()
if util.validate_debline(data[0]): # Legacy Source
if len(data) > 1:
raise SourceError(
f'The source is a legacy source but contains {len(data)} entries. '
'It may only contain one entry.'
)
deb_parser = ParseDeb()
parsed_debline = deb_parser.parse_line(data[0])
self.ident = parsed_debline['ident']
self.name = parsed_debline['name']
self.enabled = parsed_debline['enabled']
self.types = [parsed_debline['repo_type']]
self.uris = [parsed_debline['uri']]
self.suites = [parsed_debline['suite']]
self.components = parsed_debline['components']
for key in parsed_debline['options']:
self[key] = parsed_debline['options'][key]
self._update_legacy_options()
for comment in parsed_debline['comments']:
self.comments.append(comment)
if self.comments == ['']:
self.comments = []
if not self.name:
self.name = self.generate_default_name()
if self.signed_by:
self.load_key()
return
# DEB822 Source
super().__init__(sequence=data)
if self.signed_by:
self.load_key()
return
@property
def sourcecode_enabled(self) -> bool:
"""`True` if this source also provides source code, otherwise `False`"""
if util.SourceType.SOURCECODE in self.types:
return True
return False
@sourcecode_enabled.setter
def sourcecode_enabled(self, enabled) -> None:
"""Accept a variety of input values"""
types = [util.SourceType.BINARY]
if enabled in util.true_values:
types.append(util.SourceType.SOURCECODE)
self.types = types
def generate_default_ident(self, prefix='') -> str:
"""Generates a suitable ID for the source
Returns: str
A sane default-id
"""
ident:str = ''
if len(self.uris) > 0:
uri:str = self.uris[0].replace('/', ' ')
uri_list:list = uri.split()
uri_str:str = '-'.join(uri_list[1:])
branch_name:str = util.scrub_filename(uri_str)
ident = f'{prefix}{branch_name}'
ident += f'-{self.types[0].ident()}'
try:
if not self['X-Repolib-ID']:
self['X-Repolib-ID'] = ident
except KeyError:
self['X-Repolib-ID'] = ident
return ident
def generate_default_name(self) -> str:
"""Generate a default name based on the ident
Returns: str
A name based on the ident
"""
name:str = self.ident
try:
name = self['X-Repolib-Name']
except KeyError:
self['X-Repolib-Name'] = self.ident
return self['X-Repolib-Name']
if not name:
self['X-Repolib-Name'] = self.ident
return self['X-Repolib-Name']
def load_key(self, ignore_errors:bool = True) -> None:
"""Finds and loads the signing key from the system
Arguments:
ignore_errors(bool): If `False`, throw a SourceError exception if
the key can't be found/doesn't exist (Default: `True`)
"""
self.log.info('Finding key for source %s', self.ident)
if not self.signed_by:
if ignore_errors:
self.log.warning('No key configured for source %s', self.ident)
else:
raise SourceError('No key configured for source {self.ident}')
if self.signed_by not in util.keys:
new_key = SourceKey()
new_key.reset_path(path=self.signed_by)
self.key = new_key
util.keys[str(new_key.path)] = new_key
else:
self.key = util.keys[self.signed_by]
def save(self) -> None:
"""Proxy method to save the source"""
if not self.file == None:
self.file.save()
def output_legacy(self) -> str:
"""Outputs a legacy representation of this source
Note: this is expected to fail if there is more than one type, URI, or
Suite; the one-line format does not support having multiple of these
properties.
Returns: str
The source output formatted as Legacy
"""
return self.legacy
def output_822(self) -> str:
"""Outputs a DEB822 representation of this source
Returns: str
The source output formatted as Deb822
"""
return self.deb822
def output_ui(self) -> str:
"""Outputs a string representation of this source for use in UIs
Returns: str
The source output string
"""
return self.ui
def prop_append(self, prop:list, item:str) -> None:
"""Appends an item to a list property of this source.
NOTE: List properties are `types`, `uris`, `suites`, and `components`.
Arguments:
prop(list): The property on which to append the item.
item(str): The item to append to the propery
"""
_list = prop
_list.append(item)
prop = _list
def tasks_save(self, *args, **kwargs) -> None:
"""Extra tasks to perform when saving a source"""
return
## Properties are stored/retrieved from the underlying Deb822 dict
@property
def has_required_parts(self) -> bool:
"""(RO) True if all required attributes are set, otherwise false."""
required_parts = ['uris', 'suites', 'ident']
for attr in required_parts:
if len(getattr(self, attr)) < 1:
return False
return True
@property
def ident(self) -> str:
"""The ident for this source within the file"""
try:
return self['X-Repolib-ID']
except KeyError:
return ''
@ident.setter
def ident(self, ident: str) -> None:
ident = util.scrub_filename(ident)
self['X-Repolib-ID'] = ident
@property
def name(self) -> str:
"""The human-friendly name for this source"""
try:
_name = self['X-Repolib-Name']
except KeyError:
_name = ''
if not _name:
self.generate_default_name()
return self['X-Repolib-Name']
@name.setter
def name(self, name: str) -> None:
self['X-Repolib-Name'] = name
@property
def enabled(self) -> util.AptSourceEnabled:
"""Whether or not the source is enabled/active"""
try:
enabled = self['Enabled'] in util.true_values
except KeyError:
return util.AptSourceEnabled.FALSE
if enabled and self.has_required_parts:
return util.AptSourceEnabled.TRUE
return util.AptSourceEnabled.FALSE
@enabled.setter
def enabled(self, enabled) -> None:
"""For convenience, accept a wide varietry of input value types"""
self['Enabled'] = 'no'
if enabled in util.true_values:
self['Enabled'] = 'yes'
@property
def types(self) -> list:
"""The list of source types for this source"""
_types:list = []
try:
for sourcetype in self['types'].split():
_types.append(util.SourceType(sourcetype))
except KeyError:
pass
return _types
@types.setter
def types(self, types: list) -> None:
"""Turn this list into a string of values for storage"""
self['Types'] = ''
_types:list = []
for sourcetype in types:
if sourcetype not in _types:
_types.append(sourcetype)
for sourcetype in _types:
self['Types'] += f'{sourcetype.value} '
self['Types'] = self['Types'].strip()
@property
def uris(self) -> list:
"""The list of URIs for this source"""
try:
return self['URIs'].split()
except KeyError:
return []
@uris.setter
def uris(self, uris: list) -> None:
self['URIs'] = ' '.join(uris).strip()
@property
def suites(self) -> list:
"""The list of URIs for this source"""
try:
return self['Suites'].split()
except KeyError:
return []
@suites.setter
def suites(self, suites: list) -> None:
self['Suites'] = ' '.join(suites).strip()
@property
def components(self) -> list:
"""The list of URIs for this source"""
try:
return self['Components'].split()
except KeyError:
return []
@components.setter
def components(self, components: list) -> None:
self['Components'] = ' '.join(components).strip()
@property
def options(self) -> dict:
"""The options for this source"""
return self._options
@options.setter
def options(self, options:dict) -> None:
if 'Signed-By' in options:
self.signed_by = options['Signed-By']
if self.signed_by:
options.pop('Signed-By')
self._options = options
@property
def prefs(self):
"""The path to any apt preferences files for this source."""
try:
prefs = self['X-Repolib-Prefs']
except KeyError:
prefs = ''
if prefs:
return Path(prefs)
return Path()
@prefs.setter
def prefs(self, prefs):
"""Accept a str or a Path-like object"""
try:
del self['X-Repolib-Prefs']
except KeyError:
pass
if prefs:
prefs_str = str(prefs)
self['X-Repolib-Prefs'] = prefs_str
## Option properties
@property
def architectures (self) -> str:
"""architectures option"""
try:
return self['Architectures']
except KeyError:
return ''
@architectures.setter
def architectures(self, data) -> None:
try:
self.pop('Architectures')
except KeyError:
pass
if data:
self['Architectures'] = data
self._update_legacy_options()
@property
def languages (self) -> str:
"""languages option"""
try:
return self['Languages']
except KeyError:
return ''
@languages.setter
def languages(self, data) -> None:
try:
self.pop('Languages')
except KeyError:
pass
if data:
self['Languages'] = data
self._update_legacy_options()
@property
def targets (self) -> str:
"""targets option"""
try:
return self['Targets']
except KeyError:
return ''
@targets.setter
def targets(self, data) -> None:
try:
self.pop('Targets')
except KeyError:
pass
if data:
self['Targets'] = data
self._update_legacy_options()
@property
def pdiffs (self) -> str:
"""pdiffs option"""
try:
return self['Pdiffs']
except KeyError:
return ''
@pdiffs.setter
def pdiffs(self, data) -> None:
try:
self.pop('Pdiffs')
except KeyError:
pass
if data:
self['Pdiffs'] = data
self._update_legacy_options()
@property
def by_hash (self) -> str:
"""by_hash option"""
try:
return self['By-Hash']
except KeyError:
return ''
@by_hash.setter
def by_hash(self, data) -> None:
try:
self.pop('By-Hash')
except KeyError:
pass
if data:
self['By-Hash'] = data
self._update_legacy_options()
@property
def allow_insecure (self) -> str:
"""allow_insecure option"""
try:
return self['Allow-Insecure']
except KeyError:
return ''
@allow_insecure.setter
def allow_insecure(self, data) -> None:
try:
self.pop('Allow-Insecure')
except KeyError:
pass
if data:
self['Allow-Insecure'] = data
self._update_legacy_options()
@property
def allow_weak (self) -> str:
"""allow_weak option"""
try:
return self['Allow-Weak']
except KeyError:
return ''
@allow_weak.setter
def allow_weak(self, data) -> None:
try:
self.pop('Allow-Weak')
except KeyError:
pass
if data:
self['Allow-Weak'] = data
self._update_legacy_options()
@property
def allow_downgrade_to_insecure (self) -> str:
"""allow_downgrade_to_insecure option"""
try:
return self['Allow-Downgrade-To-Insecure']
except KeyError:
return ''
@allow_downgrade_to_insecure.setter
def allow_downgrade_to_insecure(self, data) -> None:
try:
self.pop('Allow-Downgrade-To-Insecure')
except KeyError:
pass
if data:
self['Allow-Downgrade-To-Insecure'] = data
self._update_legacy_options()
@property
def trusted (self) -> str:
"""trusted option"""
try:
return self['Trusted']
except KeyError:
return ''
@trusted.setter
def trusted(self, data) -> None:
try:
self.pop('Trusted')
except KeyError:
pass
if data:
self['Trusted'] = data
self._update_legacy_options()
@property
def signed_by (self) -> str:
"""signed_by option"""
try:
return self['Signed-By']
except KeyError:
return ''
@signed_by.setter
def signed_by(self, data) -> None:
try:
self.pop('Signed-By')
except KeyError:
pass
if data:
self['Signed-By'] = data
self._update_legacy_options()
@property
def check_valid_until (self) -> str:
"""check_valid_until option"""
try:
return self['Check-Valid-Until']
except KeyError:
return ''
@check_valid_until.setter
def check_valid_until(self, data) -> None:
try:
self.pop('Check-Valid-Until')
except KeyError:
pass
if data:
self['Check-Valid-Until'] = data
self._update_legacy_options()
@property
def valid_until_min (self) -> str:
"""valid_until_min option"""
try:
return self['Valid-Until-Min']
except KeyError:
return ''
@valid_until_min.setter
def valid_until_min(self, data) -> None:
try:
self.pop('Valid-Until-Min')
except KeyError:
pass
if data:
self['Valid-Until-Min'] = data
self._update_legacy_options()
@property
def valid_until_max (self) -> str:
"""valid_until_max option"""
try:
return self['Valid-Until-Max']
except KeyError:
return ''
@valid_until_max.setter
def valid_until_max(self, data) -> None:
try:
self.pop('Valid-Until-Max')
except KeyError:
pass
if data:
self['Valid-Until-Max'] = data
self._update_legacy_options()
@property
def default_mirror(self) -> str:
"""The default mirror/URI for the source"""
try:
return self['X-Repolib-Default-Mirror']
except KeyError:
return ''
@default_mirror.setter
def default_mirror(self, mirror) -> None:
if mirror:
self['X-Repolib-Default-Mirror'] = mirror
else:
self['X-Repolib-Default-Mirror'] = ''
## Output Properties
@property
def deb822(self) -> str:
"""The DEB822 representation of this source"""
self._update_legacy_options()
# comments get handled separately because they're a list, and list
# properties don't support .append()
if self.comments:
self['X-Repolib-Comments'] = '# '
self['X-Repolib-Comments'] += ' # '.join(self.comments)
_deb822 = self.dump()
if self.comments:
self.pop('X-Repolib-Comments')
if _deb822:
return _deb822
return ''
@property
def ui(self) -> str:
"""The UI-friendly representation of this source"""
self._update_legacy_options()
_ui_list:list = self.deb822.split('\n')
ui_output: str = f'{self.ident}:\n'
for line in _ui_list:
key = line.split(':')[0]
if key not in util.output_skip_keys:
if line:
ui_output += f'{line}\n'
for key in util.keys_map:
ui_output = ui_output.replace(key, util.keys_map[key])
return ui_output
@property
def legacy(self) -> str:
"""The legacy/one-line format representation of this source"""
self._update_legacy_options()
if str(self.prefs) != '.':
raise SourceError(
'Apt Preferences files can only be used with DEB822-format sources.'
)
sourcecode = self.sourcecode_enabled
if len(self.types) > 1:
self.twin_source = True
self.types = [util.SourceType.BINARY]
sourcecode = True
legacy = ''
legacy += self._generate_legacy_output()
if self.twin_source:
legacy += '\n'
legacy += self._generate_legacy_output(sourcecode=True, enabled=sourcecode)
return legacy
def _generate_legacy_output(self, sourcecode=False, enabled=True) -> str:
"""Generate a string of the current source in legacy format"""
legacy = ''
if len(self.types) > 1:
self.twin_source = True
self.types = [util.SourceType.BINARY]
for attr in ['types', 'uris', 'suites']:
if len(getattr(self, attr)) > 1:
msg = f'The source has too many {attr}.'
msg += f'Legacy-format sources support one {attr[:-1]} only.'
raise SourceError(msg)
if not self.enabled.get_bool() and not sourcecode:
legacy += '# '
if sourcecode and not enabled:
legacy += '# '
if sourcecode:
legacy += 'deb-src '
else:
legacy += self.types[0].value
legacy += ' '
options_string = self._legacy_options()
if options_string:
legacy += '['
legacy += options_string
legacy = legacy.strip()
legacy += '] '
legacy += f'{self.uris[0]} '
legacy += f'{self.suites[0]} '
for component in self.components:
legacy += f'{component} '
legacy += f' ## X-Repolib-Name: {self.name}'
legacy += f' # X-Repolib-ID: {self.ident}'
if self.comments:
for comment in self.comments:
legacy += f' # {comment}'
return legacy
def _legacy_options(self) -> str:
"""Turn the current options into a oneline-style string
Returns: str
The one-line-format options string
"""
options_str = ''
for key in self.options:
if self.options[key] != '':
options_str += f'{key}={self.options[key].replace(" ", ",")} '
return options_str
def _update_legacy_options(self) -> None:
"""Updates the current set of legacy options"""
self.options = {
'arch': self.architectures,
'lang': self.languages,
'target': self.targets,
'pdiffs': self.pdiffs,
'by-hash': self.by_hash,
'allow-insecure': self.allow_insecure,
'allow-weak': self.allow_weak,
'allow-downgrade-to-insecure': self.allow_downgrade_to_insecure,
'trusted': self.trusted,
'signed-by': self.signed_by,
'check-valid-until': self.check_valid_until,
'valid-until-min': self.valid_until_min,
'valid-until-max': self.valid_until_max,
}

73
archive/repolib/system.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from pathlib import Path
from . import util
from .file import SourceFile
from .source import Source
from .shortcuts import popdev, ppa
log = logging.getLogger(__name__)
def load_all_sources() -> None:
"""Loads all of the sources present on the system."""
log.info('Loading all sources')
util.sources.clear()
util.files.clear()
util.keys.clear()
util.errors.clear()
sources_path = Path(util.SOURCES_DIR)
sources_files = sources_path.glob('*.sources')
legacy_files = sources_path.glob('*.list')
for file in sources_files:
try:
sourcefile = SourceFile(name=file.stem)
log.debug('Loading %s', file)
sourcefile.load()
if file.name not in util.files:
util.files[file.name] = sourcefile
except Exception as err:
util.errors[file.name] = err
for file in legacy_files:
try:
sourcefile = SourceFile(name=file.stem)
sourcefile.load()
util.files[file.name] = sourcefile
except Exception as err:
util.errors[file.name] = err
for f in util.files:
file = util.files[f]
for source in file.sources:
if source.ident in util.sources:
source.ident = f'{file.name}-{source.ident}'
source.file.save()
util.sources[source.ident] = source

View File

View File

@ -0,0 +1,142 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import unittest
from ..key import SourceKey
from .. import util, system
from .. import set_testing
# System76 Signing PubKey, for test import
KEY_DATA = (
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFlL+3MBEADdNM9Xy2t3EtKU1i3R1o'
'1OCgJqLiDm8OZZq47InYID8oAPKRjd\n0UDVJTrvfsB4oJH97VRi2hGv2xmc19OaFE/NsQBZW/'
'7/3ypLr8eyaNgvscsmG/WN\ncM1cbMZtwd1b0JOr3bNTzp6WKRI3jo9uRw7duM8FwPjKm76Lbo'
'DQbAR+4Szm3O8x\n/om8Gs1MRPUkY2dVz5KzednFLHwy7qnUXR3WRB5K1L9EBZkFDDNqnyViUI'
'rE4bTm\nBC9mTg/Xfw/QXUFYz3t/YTYduAU0o1q2yei+8tVAJKh7H9t3PrQ95l3RUUcaAvba\n'
'A9zlCrI8fonpxu7eSpkqzT4uCkfxdLVwittl1DumKTEkSXDQ5txY21igbSZZQwBA\nZf9MnFhJ'
'fPsEIq2YHRc1FBcQxiAIpnGizv7FgYY5FxmZQ7592dMQOZ00h+lDSQug\nNMxloHCogaXR038u'
'IKGTQnQEVcT46FtTRkLMSvbigy+RVSchdu9MEBBPgD3vSv53\nNEobXsLiZ9hF6Hk7XI2WxP5j'
'1zWTPmzxvf9NDOWz2Sw9Z+ilf252LXoxZQaMngp8\nXL32uvw7q+mjB6F1W/qpe3b32uu7eGNr'
'DWJ5veE808hpXXj803TllmRUfMGUrtY9\nk7uUTQQWtrJ5uZ0QmsTk1oJHCPIUjjuiNtQfq28+'
'bfg8FEJ/F1N1mB0IvwARAQAB\ntCxQb3AgT1MgKElTTyBTaWduaW5nIEtleSkgPGluZm9Ac3lz'
'dGVtNzYuY29tPokC\nNwQTAQIAIgUCWUv7cwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA'
'CgkQIE3Y\nrsM6ev8kXw/4p/8fOH8wM59ZoU0t1+fv3O8dYaDdTVTVIEno9snrsx5A5tbMu59r'
'\nHoBaxGenv/PB0l8yANhRX+HVmU/l0Sj0lTlEkYzgH/IT2Ne60s1ETgI7DlgSuYyP\nH8wq61'
'85+2DyE2+R/XcXGq0I++QUq1Y6rS+B4KIyYcgpJotcVNFaZiwuJZE31uLg\nkVMZrm1oObHear'
'7P2JQTbgsENMZDJEhQBCGKVdnAfVdKUeUrd07syr0cDe3kwY9o\ncNc00bhIh23cLTJE2omok9'
'yCsXoeFJlPMyZw8WvEa5oaYWzP4Yw7nF8/27JTzZ70\nDjK2D2xoTkr0cP87LtZulS6FC3lxLu'
'Z6hSaxsqoBH8Dd1uyYVEzLDsIRMtSHsXk+\n3kLrr1p7/7/vjGShlYkbLtP4jWnlHc6vSxIzm/'
'MQmQMCfjeo3QH7GGw88mYtXngQ\n/Zna6wz0oL6pGM/4t90SCxTxRqCnoxMxzkcpt9n42bj79g'
'rESOMH4wm3ExfuPk7I\nDtY+SqzIq0QvoPbC3XJLusWVgwUsRF2FpTTRTHEiWEMjWDKDVEyT4K'
'1k1k3f/gi2\n6LdtXwqDwzUvJJU5HYwVFywt+0jt5F0ZlTlPizz3iHw4gMLOielRShl+gZrU2U'
'0O\naj1Hyts9LymEKMUvRQGwMqCZcXo6sGjs59tTsfeGX16PTOyBri8eoLkCDQRZS/tz\nARAA'
'pD9IWm4zS1AuBcOTuvh1E/ciKHGIUtW3JftD9ah8loEeckakgG5Xn9he1X6J\nyxPULpsptcCC'
'cKXlw853ZQK9PLQJX6moWLH+qf2Zo3UAn/YEsWk+KsHoxPXHNUds\nu/j6UKkqEk8c7H92hUo8'
'aWghO3p4HDVJ9KmGtueQ3jOv8Qun7Eh9cIo0A59cKmMv\njKUiYHLIJw8bkveQ8rVPul1ZHn56'
'ORiBi58vm3tzjI4UWHQMjiKxXT6H5eG/f5K6\nuaK8lljh6n6jhdnQCpBcdtSIbhE/6YRv2+Ig'
'L+BRssvprBtx4/sBwKjNNqzWPeGy\nUDHMiF88ETYqZ8DfukQ/e5XuaxjU41g/F8cw8BeVTBMv'
'eb1YTyOoWcWvTL+hoBfS\nqYc/lvDHmmJ7/IgeMvUE6KoByP4ub5wX52mJTqgMC4GMhA04BC60'
'B+NfVAXLh2pa\nTRJAHoWTDswOxbR6q9zPEFGZzV04B9Y96EavwMpT5IzG2fOPBwvdT0EDnt+v'
'Q/iB\nc9O7CvkRTROAV+RoNCLY2XU8yNc/XxuI66PCE4Q96jW4uDzHvi6sPW/glsfRi2NT\nRW'
'CO15KMVf0aypXeBpSbHIXIYGdXRQRpw980IW6PrElPpqZ5/DGbkXei5CuruF2R\nmltuu3MqYQ'
'jcUvP9T7s0e5GAFgQFrR/8q29nVULq8IF4vzUAEQEAAYkCHwQYAQIA\nCQUCWUv7cwIbDAAKCR'
'AgTdiuwzp6/wTGD/9Co4gEmTTOW++FneMMJo5K4WqeWVRg\ng1q5+yoVqgWq3k6lLsEC5kxR30'
'5BAAcvXo9XPKdo62ySYmhIFOpIz/TkeTUxDZaw\nsLtcBxXUME2L5j/1od1V9lxecUvLAgA11o'
'5Kb8TMKn5ZcmGhadtTLslWQcYsKqhw\nLaYQRlcxLDHYT8DXFkHgDhUMMbpt07dU5v5lIjgtGN'
'HRhdS7/lCmSWOBtYapwpAH\nGYSmahN0zO36VHzOB5uwFue0tSoQiBEvLrCV/8ZtT2S5NkXaSm'
'isz6B5Vr6DRtWI\nOamW5pMbSL8WQNQ99Kik05ctERjv2NgxI4JQo/a4KKthRrT4JlixXmrfJD'
'uPyDPp\nRuTu1Elo6snoqWKQNf1sEPKvcv7EviNxBOhbTKivWrJXMnbOme7+UlNLcq7VAFp3\n'
'x5hxk/ap0WqH/hs7+8jMBC8nS402MoM7EyLS0++kbOuEL/Prf3+JxFRqIu5Df77J\n+bUmTtKI'
'CV43ikiVWmnP5OuJj2JPSOTR+rLxAQYpyHmo7HKXE63FbH1FVLgsT88+\nEW6VtI01I7EYmKQX'
'EqQo52yfeHKDrQjGNVBWMKcXj0SVU+QQ1Ue/4yLwA+74VD2d\nfOyJI22NfTI+3SMAsMQ8L+WV'
'QI+58bu7+iEqoEfHCXikE8BtTbJAN4Oob1lrjfOe\n5utH/lMP9suRWw==\n=NL3f\n-----EN'
'D PGP PUBLIC KEY BLOCK-----\n'
)
class KeyTestCase(unittest.TestCase):
def setUp(self):
set_testing()
self.key_data = KEY_DATA
self.keys_dir = util.KEYS_DIR
self.key_id = '204DD8AEC33A7AFF'
self.key_uids = ['Pop OS (ISO Signing Key) <info@system76.com>']
self.key_length = '4096'
self.key_date = '1498151795'
def test_import_ascii(self):
key = SourceKey(name='popdev')
key.load_key_data(ascii=self.key_data)
key_dict = key.gpg.list_keys()[0]
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
self.assertEqual(key.path, key_path)
self.assertEqual(len(key.gpg.list_keys()), 1)
self.assertEqual(key_dict['keyid'], self.key_id)
self.assertEqual(key_dict['uids'], self.key_uids)
self.assertEqual(key_dict['length'], self.key_length)
self.assertEqual(key_dict['date'], self.key_date)
def test_key_save_load(self):
print(self.keys_dir)
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
if key_path.exists():
key_path.unlink()
self.assertFalse(key_path.exists())
key_save = SourceKey(name='popdev')
key_save.load_key_data(ascii=self.key_data)
key_save.save_gpg()
self.assertTrue(key_save.path.exists())
key_load = SourceKey()
key_load.reset_path(name='popdev')
key_dict = key_load.gpg.list_keys()[0]
self.assertEqual(key_load.path, key_path)
self.assertEqual(len(key_load.gpg.list_keys()), 1)
self.assertEqual(key_dict['keyid'], self.key_id)
self.assertEqual(key_dict['uids'], self.key_uids)
self.assertEqual(key_dict['length'], self.key_length)
self.assertEqual(key_dict['date'], self.key_date)
def test_delete_key(self):
key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
if key_path.exists():
key_path.unlink()
self.assertFalse(key_path.exists())
self.assertFalse(key_path.exists())
key_save = SourceKey(name='popdev')
key_save.load_key_data(ascii=self.key_data)
key_save.save_gpg()
self.assertTrue(key_save.path.exists())
key_load = SourceKey()
key_load.reset_path(name='popdev')
key_load.delete_key()
self.assertFalse(key_load.path.exists())

View File

@ -0,0 +1,188 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
This is a library for parsing deb lines into deb822-format data.
"""
import unittest
from ..source import Source
from .. import util
class DebTestCase(unittest.TestCase):
def test_normal_source(self):
source = Source()
source.load_from_data([
'deb http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.types, [util.SourceType.BINARY])
self.assertTrue(source.enabled.get_bool())
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.suites, ['suite'])
self.assertEqual(source.components, ['main'])
self.assertEqual(source.ident, 'example-com-binary')
def test_source_with_multiple_components(self):
source = Source()
source.load_from_data([
'deb http://example.com/ suite main nonfree'
])
source.generate_default_ident()
self.assertEqual(source.suites, ['suite'])
self.assertEqual(source.components, ['main', 'nonfree'])
def test_source_with_option(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64 ] http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.architectures, 'amd64')
def test_source_uri_with_brackets(self):
source = Source()
source.load_from_data([
'deb http://example.com/[release]/ubuntu suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/[release]/ubuntu'])
def test_source_options_with_colons(self):
source = Source()
source.load_from_data([
'deb [ arch=arm:2 ] http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.architectures, 'arm:2')
def test_source_with_multiple_option_values(self):
source = Source()
source.load_from_data([
'deb [ arch=armel,amd64 ] http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.architectures, 'armel amd64')
def test_source_with_multiple_options(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64 lang=en_US ] http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.architectures, 'amd64')
self.assertEqual(source.languages, 'en_US')
def test_source_with_multiple_options_with_multiple_values(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64,armel lang=en_US,en_CA ] '
'http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example.com/'])
self.assertEqual(source.architectures, 'amd64 armel')
self.assertEqual(source.languages, 'en_US en_CA')
def test_source_uri_with_brackets_and_options(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64 lang=en_US,en_CA ] '
'http://example][.com/[release]/ubuntu suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
self.assertEqual(source.architectures, 'amd64')
self.assertEqual(source.languages, 'en_US en_CA')
def test_source_uri_with_brackets_and_options_with_colons(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64,arm:2 lang=en_US,en_CA ] '
'http://example][.com/[release]/ubuntu suite main'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
self.assertEqual(source.architectures, 'amd64 arm:2')
self.assertEqual(source.languages, 'en_US en_CA')
def test_worst_case_sourcenario(self):
source = Source()
source.load_from_data([
'deb [ arch=amd64,arm:2,arm][ lang=en_US,en_CA ] '
'http://example][.com/[release:good]/ubuntu suite main restricted '
'nonfree not-a-component'
])
source.generate_default_ident()
self.assertEqual(source.uris, ['http://example][.com/[release:good]/ubuntu'])
self.assertEqual(source.suites, ['suite'])
self.assertEqual(source.components, [
'main', 'restricted', 'nonfree', 'not-a-component'
])
source.generate_default_ident()
self.assertEqual(source.architectures, 'amd64 arm:2 arm][')
self.assertEqual(source.languages, 'en_US en_CA')
def test_source_code_source(self):
source = Source()
source.load_from_data([
'deb-src http://example.com/ suite main'
])
source.generate_default_ident()
self.assertEqual(source.types, [util.SourceType.SOURCECODE])
def test_disabled_source(self):
source = Source()
source.load_from_data([
'# deb http://example.com/ suite main'
])
source.generate_default_ident()
self.assertFalse(source.enabled.get_bool())
def test_disabled_source_without_space(self):
source = Source()
source.load_from_data([
'#deb http://example.com/ suite main'
])
source.generate_default_ident()
self.assertFalse(source.enabled.get_bool())
def test_source_with_trailing_comment(self):
source = Source()
source.load_from_data([
'deb http://example.com/ suite main # This is a comment'
])
source.generate_default_ident()
self.assertEqual(source.suites, ['suite'])
self.assertEqual(source.components, ['main'])
def test_disabled_source_with_trailing_comment(self):
source = Source()
source.load_from_data([
'# deb http://example.com/ suite main # This is a comment'
])
source.generate_default_ident()
self.assertEqual(source.suites, ['suite'])
self.assertEqual(source.components, ['main'])

View File

@ -0,0 +1,43 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import unittest
from ..shortcuts import popdev
from .. import util
class PopdevTestCase(unittest.TestCase):
def test_ppa(self):
source = popdev.PopdevSource()
# Verification data
uris_test = ['http://apt.pop-os.org/staging/master']
signed_test = '/usr/share/keyrings/popdev-archive-keyring.gpg'
source.load_from_shortcut(shortcut='popdev:master')
self.assertEqual(source.uris, uris_test)
self.assertEqual(source.ident, 'popdev-master')
self.assertEqual(source.suites, [util.DISTRO_CODENAME])
self.assertEqual(source.components, ['main'])
self.assertEqual(source.types, [util.SourceType.BINARY])
self.assertTrue(source.signed_by.endswith(signed_test))

View File

@ -0,0 +1,42 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import unittest
from ..shortcuts import ppa
from .. import util
class PPATestCase(unittest.TestCase):
def test_ppa(self):
source = ppa.PPASource()
# Verification data
uris_test = ['http://ppa.launchpad.net/system76/pop/ubuntu']
signed_test = '/usr/share/keyrings/ppa-system76-pop-archive-keyring.gpg'
source.load_from_shortcut(shortcut='ppa:system76/pop', meta=False, key=False)
self.assertEqual(source.uris, uris_test)
self.assertEqual(source.ident, 'ppa-system76-pop')
self.assertEqual(source.suites, [util.DISTRO_CODENAME])
self.assertEqual(source.components, ['main'])
self.assertEqual(source.types, [util.SourceType.BINARY])

View File

@ -0,0 +1,245 @@
#!/usr/bin/python3
"""
Copyright (c) 2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import unittest
from .. import file, util, source
class SourceTestCase(unittest.TestCase):
def setUp(self):
util.set_testing()
self.source = source.Source()
self.source.ident = 'test'
self.source.name = 'Test Source'
self.source.enabled = True
self.source.types = [util.SourceType.BINARY, util.SourceType.SOURCECODE]
self.source.uris = ['http://example.com/ubuntu', 'http://example.com/mirror']
self.source.suites = ['suite', 'suite-updates']
self.source.components = ['main', 'contrib', 'nonfree']
self.source.architectures = 'amd64 armel'
self.source.languages = 'en_US en_CA'
self.file = file.SourceFile(name=self.source.ident)
self.file.add_source(self.source)
self.source.file = self.file
self.source_legacy = source.Source()
self.source_legacy.ident = 'test-legacy'
self.source_legacy.name = 'Test Legacy Source'
self.source_legacy.enabled = True
self.source_legacy.types = [util.SourceType.BINARY]
self.source_legacy.uris = ['http://example.com/ubuntu']
self.source_legacy.suites = ['suite']
self.source_legacy.components = ['main', 'contrib', 'nonfree']
self.source_legacy.architectures = 'amd64 armel'
self.source_legacy.languages = 'en_US en_CA'
self.source_legacy.file = file.SourceFile(name=self.source_legacy.ident)
self.source_legacy.file.format = util.SourceFormat.LEGACY
def test_default_source_data(self):
self.assertEqual(self.source.name, 'Test Source')
self.assertTrue(self.source.enabled.get_bool())
self.assertEqual(
self.source.types,
[util.SourceType.BINARY, util.SourceType.SOURCECODE]
)
self.assertTrue(self.source.sourcecode_enabled)
self.assertEqual(
self.source.uris,
['http://example.com/ubuntu', 'http://example.com/mirror']
)
self.assertEqual(
self.source.suites,
['suite', 'suite-updates']
)
self.assertEqual(
self.source.components,
['main', 'contrib', 'nonfree']
)
self.assertEqual(self.source.architectures, 'amd64 armel')
self.assertEqual(self.source.languages, 'en_US en_CA')
self.assertEqual(self.source.file.path.name, 'test.sources')
def test_output_822(self):
source_string = (
'X-Repolib-ID: test\n'
'X-Repolib-Name: Test Source\n'
'Enabled: yes\n'
'Types: deb deb-src\n'
'URIs: http://example.com/ubuntu http://example.com/mirror\n'
'Suites: suite suite-updates\n'
'Components: main contrib nonfree\n'
'Architectures: amd64 armel\n'
'Languages: en_US en_CA\n'
)
legacy_source_string = (
'X-Repolib-ID: test-legacy\n'
'X-Repolib-Name: Test Legacy Source\n'
'Enabled: yes\n'
'Types: deb\n'
'URIs: http://example.com/ubuntu\n'
'Suites: suite\n'
'Components: main contrib nonfree\n'
'Architectures: amd64 armel\n'
'Languages: en_US en_CA\n'
)
self.assertEqual(self.source.deb822, source_string)
self.assertEqual(self.source_legacy.deb822, legacy_source_string)
def test_output_ui(self):
source_string = (
'test:\n'
'Name: Test Source\n'
'Enabled: yes\n'
'Types: deb deb-src\n'
'URIs: http://example.com/ubuntu http://example.com/mirror\n'
'Suites: suite suite-updates\n'
'Components: main contrib nonfree\n'
'Architectures: amd64 armel\n'
'Languages: en_US en_CA\n'
''
)
legacy_source_string = (
'test-legacy:\n'
'Name: Test Legacy Source\n'
'Enabled: yes\n'
'Types: deb\n'
'URIs: http://example.com/ubuntu\n'
'Suites: suite\n'
'Components: main contrib nonfree\n'
'Architectures: amd64 armel\n'
'Languages: en_US en_CA\n'
)
self.assertEqual(self.source.ui, source_string)
self.assertEqual(self.source_legacy.ui, legacy_source_string)
def test_output_legacy(self):
source_string = (
'deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source # X-Repolib-ID: test-legacy'
)
self.assertEqual(self.source_legacy.legacy, source_string)
def test_enabled(self):
self.source.enabled = False
self.assertFalse(self.source.enabled.get_bool())
def test_sourcecode_enabled(self):
self.source.sourcecode_enabled = False
self.assertEqual(self.source.types, [util.SourceType.BINARY])
def test_dict_access(self):
self.assertEqual(self.source['X-Repolib-ID'], 'test')
self.assertEqual(self.source['X-Repolib-Name'], 'Test Source')
self.assertEqual(self.source['Enabled'], 'yes')
self.assertEqual(self.source['Enabled'], 'yes')
self.assertEqual(self.source['Types'], 'deb deb-src')
self.assertEqual(self.source['URIs'], 'http://example.com/ubuntu http://example.com/mirror')
self.assertEqual(self.source['Suites'], 'suite suite-updates')
self.assertEqual(self.source['Components'], 'main contrib nonfree')
self.assertEqual(self.source['Architectures'], 'amd64 armel')
self.assertEqual(self.source['Languages'], 'en_US en_CA')
def test_load(self):
load_source = source.Source()
load_source.load_from_data([
'X-Repolib-ID: load-test',
'X-Repolib-Name: Test Source Loading',
'Enabled: yes',
'Types: deb',
'URIs: http://example.com/ubuntu http://example.com/mirror',
'Suites: suite suite-updates',
'Components: main contrib nonfree',
'Architectures: amd64 armel',
'Languages: en_US en_CA',
])
self.assertEqual(load_source.ident, 'load-test')
self.assertEqual(load_source.name, 'Test Source Loading')
self.assertTrue(load_source.enabled.get_bool())
self.assertEqual(
load_source.types,
[util.SourceType.BINARY]
)
self.assertEqual(
load_source.uris,
['http://example.com/ubuntu', 'http://example.com/mirror']
)
self.assertEqual(
load_source.suites,
['suite', 'suite-updates']
)
self.assertEqual(
load_source.components,
['main', 'contrib', 'nonfree']
)
self.assertEqual(load_source.architectures, 'amd64 armel')
self.assertEqual(load_source.languages, 'en_US en_CA')
load_legacy_source = source.Source()
load_legacy_source.load_from_data(
['deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source Loading # X-Repolib-ID: test-load-legacy']
)
self.assertEqual(load_legacy_source.ident, 'test-load-legacy')
self.assertEqual(load_legacy_source.name, 'Test Legacy Source Loading')
self.assertTrue(load_legacy_source.enabled.get_bool())
self.assertEqual(
load_legacy_source.types,
[util.SourceType.BINARY]
)
self.assertEqual(
load_legacy_source.uris,
['http://example.com/ubuntu']
)
self.assertEqual(
load_legacy_source.suites,
['suite']
)
self.assertEqual(
load_legacy_source.components,
['main', 'contrib', 'nonfree']
)
self.assertEqual(load_legacy_source.architectures, 'amd64 armel')
self.assertEqual(load_legacy_source.languages, 'en_US en_CA')
def test_save_load(self):
self.source.file.save()
load_source_file = file.SourceFile(name='test')
load_source_file.load()
self.assertGreater(len(load_source_file.sources), 0)
self.assertGreater(
len(load_source_file.contents), len(load_source_file.sources)
)
load_source = load_source_file.sources[0]
self.assertEqual(load_source.ident, self.source.ident)
self.assertEqual(load_source.name, self.source.name)
self.assertEqual(load_source.enabled, self.source.enabled)
self.assertEqual(load_source.types, self.source.types)
self.assertEqual(load_source.sourcecode_enabled, self.source.sourcecode_enabled)
self.assertEqual(load_source.uris, self.source.uris)
self.assertEqual(load_source.suites, self.source.suites)
self.assertEqual(load_source.components, self.source.components)
self.assertEqual(load_source.architectures, self.source.architectures)
self.assertEqual(load_source.languages, self.source.languages)
self.assertEqual(load_source.file.name, self.source.file.name)

455
archive/repolib/util.py Normal file
View File

@ -0,0 +1,455 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2022, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
import atexit
import logging
import re
import tempfile
from enum import Enum
from pathlib import Path
from urllib.parse import urlparse
from urllib import request, error
import dbus
SOURCES_DIR = Path('/etc/apt/sources.list.d')
KEYS_DIR = Path('/etc/apt/keyrings/')
TESTING = False
KEYSERVER_QUERY_URL = 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x'
log = logging.getLogger(__name__)
class RepoError(Exception):
""" Exception from this module."""
def __init__(self, *args, code=1, **kwargs):
"""Exception with a source object
Arguments:
code (:obj:`int`, optional, default=1): Exception error code.
"""
super().__init__(*args, **kwargs)
self.code = code
try:
import distro
DISTRO_CODENAME = distro.codename()
except ImportError:
DISTRO_CODENAME = 'linux'
class SourceFormat(Enum):
"""Enum of SourceFile Formats"""
DEFAULT = "sources"
LEGACY = "list"
class SourceType(Enum):
"""Enum of repository types"""
BINARY = 'deb'
SOURCECODE = 'deb-src'
def ident(self) -> str:
"""Used for getting a version of the format for idents"""
ident = f'{self.value}'
ident = ident.replace('deb-src', 'source')
ident = ident.replace('deb', 'binary')
return ident
class AptSourceEnabled(Enum):
""" Helper Enum to translate between bool data and the Deb822 format. """
TRUE = 'yes'
FALSE = 'no'
def get_bool(self):
""" Return a bool based on the value. """
# pylint: disable=comparison-with-callable
# This doesnt seem to actually be a callable in this case.
if self.value == "yes":
return True
return False
valid_keys = [
'X-Repolib-Name:',
'X-Repolib-ID:',
'X-Repolib-Default-Mirror:',
'X-Repolib-Comment',
'X-Repolib-Prefs',
'Enabled:',
'Types:',
'URIs:',
'Suites:',
'Components:',
'Architectures:',
'Languages:',
'Targets:',
'PDiffs:',
'By-Hash:',
'Allow-Insecure:',
'Allow-Weak:',
'Allow-Downgrade-To-Insecure:',
'Trusted:',
'Signed-By:',
'Check-Valid-Until:',
'Valid-Until-Min:',
'Valid-Until-Max:',
]
output_skip_keys = [
'X-Repolib-Prefs',
'X-Repolib-ID',
]
options_inmap = {
'arch': 'Architectures',
'lang': 'Languages',
'target': 'Targets',
'pdiffs': 'PDiffs',
'by-hash': 'By-Hash',
'allow-insecure': 'Allow-Insecure',
'allow-weak': 'Allow-Weak',
'allow-downgrade-to-insecure': 'Allow-Downgrade-To-Insecure',
'trusted': 'Trusted',
'signed-by': 'Signed-By',
'check-valid-until': 'Check-Valid-Until',
'valid-until-min': 'Valid-Until-Min',
'valid-until-max': 'Valid-Until-Max'
}
options_outmap = {
'Architectures': 'arch',
'Languages': 'lang',
'Targets': 'target',
'PDiffs': 'pdiffs',
'By-Hash': 'by-hash',
'Allow-Insecure': 'allow-insecure',
'Allow-Weak': 'allow-weak',
'Allow-Downgrade-To-Insecure': 'allow-downgrade-to-insecure',
'Trusted': 'trusted',
'Signed-By': 'signed-by',
'Check-Valid-Until': 'check-valid-until',
'Valid-Until-Min': 'valid-until-min',
'Valid-Until-Max': 'valid-until-max'
}
true_values = [
True,
'True',
'true',
'Yes',
'yes',
'YES',
'y',
'Y',
AptSourceEnabled.TRUE,
1
]
keys_map = {
'X-Repolib-Name: ': 'Name: ',
'X-Repolib-ID: ': 'Ident: ',
'X-Repolib-Comments: ': 'Comments: ',
'X-Repolib-Default-Mirror: ': 'Default Mirror: ',
}
PRETTY_PRINT = '\n '
_KEYS_TEMPDIR = tempfile.TemporaryDirectory()
TEMP_DIR = Path(_KEYS_TEMPDIR.name)
options_re = re.compile(r'[^@.+]\[([^[]+.+)\]\ ')
uri_re = re.compile(r'\w+:(\/?\/?)[^\s]+')
CLEAN_CHARS = {
33: None,
64: 45,
35: 45,
36: 45,
37: 45,
94: 45,
38: 45,
42: 45,
41: None,
40: None,
43: 45,
61: 45,
91: None,
92: None,
93: None,
123: None,
125: None,
124: 95,
63: None,
47: 95,
46: 45,
60: 95,
62: 95,
44: 95,
96: None,
126: None,
32: 95,
58: None,
59: None,
}
sources:dict = {}
files:dict = {}
keys:dict = {}
errors:dict = {}
def scrub_filename(name: str = '') -> str:
""" Clean up a string intended for a filename.
Arguments:
name (str): The prospective name to scrub.
Returns: str
The cleaned-up name.
"""
return name.translate(CLEAN_CHARS)
def set_testing(testing:bool=True) -> None:
"""Sets Repolib in testing mode where changes will not be saved.
Arguments:
testing(bool): Whether testing mode should be enabled or disabled
(Defaul: True)
"""
global KEYS_DIR
global SOURCES_DIR
testing_tempdir = tempfile.TemporaryDirectory()
if not testing:
KEYS_DIR = '/usr/share/keyrings'
SOURCES_DIR = '/etc/apt/sources.list.d'
return
testing_root = Path(testing_tempdir.name)
KEYS_DIR = testing_root / 'usr' / 'share' / 'keyrings'
SOURCES_DIR = testing_root / 'etc' / 'apt' / 'sources.list.d'
def _cleanup_temsps() -> None:
"""Clean up our tempdir"""
_KEYS_TEMPDIR.cleanup()
# _TESTING_TEMPDIR.cleanup()
atexit.register(_cleanup_temsps)
def dbus_quit():
bus = dbus.SystemBus()
privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
privileged_object.exit()
def compare_sources(source1, source2, excl_keys:list) -> bool:
"""Compare two sources based on arbitrary criteria.
This looks at a given list of keys, and if the given keys between the two
given sources are identical, returns True.
Arguments:
source1, source2(Source): The two sources to compare
excl_keys([str]): Any keys to exclude from the comparison
Returns: bool
`True` if the sources are identical, otherwise `False`.
"""
for key in source1:
if key in excl_keys:
continue
if key in source2:
if source1[key] != source2[key]:
return False
else:
continue
else:
return False
for key in source2:
if key in excl_keys:
continue
if key in source1:
if source1[key] != source2[key]:
return False
else:
continue
else:
return False
return True
def find_differences_sources(source1, source2, excl_keys:list) -> dict:
"""Find key-value pairs which differ between two sources.
Arguments:
source1, source2(Source): The two sources to compare
excl_keys([str]): Any keys to exclude from the comparison
Returns: dict{'key': ('source1[key]','source2[key]')}
The dictionary of different keys, with the key values from each source.
"""
differing_keys:dict = {}
for key in source1:
if key in excl_keys:
continue
if key in source2:
if source1[key] == source2[key]:
continue
differing_keys[key] = (source1[key], source2[key])
differing_keys[key] = (source1[key], '')
for key in source2:
if key in excl_keys:
continue
if key in source1:
if source1[key] == source2[key]:
continue
differing_keys[key] = ('', source2[key])
return differing_keys
def combine_sources(source1, source2) -> None:
"""Combine the data in two sources into one.
Arguments:
source1(Source): The source to be merged into
source2(Source): The source to merge from
"""
for key in source1:
if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
continue
if key in source2:
source1[key] += f' {source2[key]}'
for key in source2:
if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
continue
if key in source1:
source1[key] += f' {source2[key]}'
# Need to deduplicate the list
for key in source1:
vals = source1[key].strip().split()
newvals = []
for val in vals:
if val not in newvals:
newvals.append(val)
source1[key] = ' '.join(newvals)
for key in source2:
vals = source2[key].strip().split()
newvals = []
for val in vals:
if val not in newvals:
newvals.append(val)
source2[key] = ' '.join(newvals)
def prettyprint_enable(enabled: bool = True) -> None:
"""Easy helper to enable/disable pretty-printing for object reprs.
Can also be used as an easy way to reset to defaults.
Arguments:
enabled(bool): Whether or not Pretty Printing should be enabled
"""
global PRETTY_PRINT
if enabled:
PRETTY_PRINT = '\n '
else:
PRETTY_PRINT = ''
def url_validator(url):
""" Validate a url and tell if it's good or not.
Arguments:
url (str): The URL to validate.
Returns:
`True` if `url` is not malformed, otherwise `False`.
"""
try:
# pylint: disable=no-else-return,bare-except
# A) We want to return false if the URL doesn't contain those parts
# B) We need this to not throw any exceptions, regardless what they are
result = urlparse(url)
if not result.scheme:
return False
if result.scheme == 'x-repolib-name':
return False
if result.netloc:
# We need at least a scheme and a netlocation/hostname or...
return all([result.scheme, result.netloc])
elif result.path:
# ...a scheme and a path (this allows file:/// URIs which are valid)
return all([result.scheme, result.path])
return False
except:
return False
def validate_debline(valid):
""" Basic checks to see if a given debline is valid or not.
Arguments:
valid (str): The line to validate.
Returns:
True if the line is valid, False otherwise.
"""
comment:bool = False
if valid.startswith('#'):
comment = True
valid = valid.replace('#', '')
valid = valid.strip()
if valid.startswith("deb"):
words = valid.split()
for word in words:
if url_validator(word):
return True
elif valid.startswith("ppa:"):
if "/" in valid:
return True
else:
if valid.endswith('.flatpakrepo'):
return False
if len(valid.split()) == 1 and not comment:
return url_validator(valid)
return False
def strip_hashes(line:str) -> str:
""" Strips the leading #'s from the given line.
Arguments:
line (str): The line to strip.
Returns:
(str): The input line without any leading/trailing hashes or
leading/trailing whitespace.
"""
while True:
line = line.strip('#')
line = line.strip()
if not line.startswith('#'):
break
return line

4
archive/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
dbus-python
distro
pytest-pylint
python-debian

198
archive/setup.py Executable file
View File

@ -0,0 +1,198 @@
#!/usr/bin/python3
"""
Copyright (c) 2019-2020, Ian Santopietro
All rights reserved.
This file is part of RepoLib.
RepoLib 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 of the License, or
(at your option) any later version.
RepoLib 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 RepoLib. If not, see <https://www.gnu.org/licenses/>.
"""
#pylint: skip-file
# We don't need to check these in setup
import os
import subprocess
from setuptools import setup, find_packages, Command
def get_version():
""" Get the program version. """
#pylint: disable=exec-used
# Just getting the version.
version = {}
with open(os.path.join('repolib', '__version__.py')) as fp:
exec(fp.read(), version)
return version['__version__']
with open("README.rst", "r") as fh:
long_description = fh.read()
classifiers = [
'Environment :: Console',
'Intended Audience :: System Administrators',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Natural Language :: English',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System'
]
class Release(Command):
""" Generate a release and push it to git."""
description = "Generate a release and push it to git."
user_options = [
('dry-run', None, 'Skip the actual release and do a dry run instead.'),
('skip-deb', None, 'Skip doing a debian update for this release.'),
('skip-git', None, 'Skip committing to git at the end.'),
('prerelease=', None, 'Release a pre-release version (alpha,beta,rc)'),
('increment=', None, 'Manually specify the desired increment (MAJOR, MINOR, PATCH)')
]
def initialize_options(self):
self.dry_run = False
self.skip_deb = False
self.skip_git = False
self.prerelease = None
self.increment = None
def finalize_options(self):
pass
def run(self):
cz_command = ['cz', 'bump', '--yes']
ch_command = ['dch']
git_command = ['git', 'add', '.']
def capture_version(sp_complete):
output = sp_complete.stdout.decode('UTF-8').split('\n')
print('\n'.join(output))
for line in output:
if 'tag to create' in line:
version_line = line
try:
return version_line.split()[-1].replace('v', '')
except UnboundLocalError:
stderr = sp_complete.stderr.decode('UTF-8')
print("WARNING: Couldn't get updated version! Using current.")
print(stderr)
return get_version()
if self.dry_run:
print('Dry run: Not making actual changes')
cz_command.append('--dry-run')
if self.prerelease:
if self.prerelease.lower() not in ['alpha', 'beta', 'rc']:
raise Exception(
f'{self.prerelease} is not a valid prerelease type. Please '
'use one of "alpha", "beta", or "rc".'
)
cz_command.append('--prerelease')
cz_command.append(self.prerelease.lower())
if self.increment:
if self.increment.upper() not in ['MAJOR', 'MINOR', 'PATCH']:
raise Exception(
f'{self.increment} is not a valid increments. Please use '
'one of MAJOR, MINOR, or PATCH.'
)
cz_command.append('--increment')
cz_command.append(self.increment.upper())
# We need to get the new version from CZ, as the file hasn't been
# updated yet.
version_command = cz_command.copy()
version_command.append('--dry-run')
version_complete = subprocess.run(version_command, capture_output=True)
version = capture_version(version_complete)
print(f'Old Version: {get_version()}')
print(f'New version: {version}')
ch_command.append(f'-v{version}')
if not self.skip_deb:
print(ch_command)
if not self.dry_run:
subprocess.run(ch_command)
subprocess.run(['dch', '-r', '""'])
if not self.skip_git:
print(git_command)
if not self.dry_run:
subprocess.run(git_command)
print(' '.join(cz_command))
if not self.dry_run:
subprocess.run(cz_command)
class Test(Command):
""" Run pyflakes and pytest"""
description = 'Run pyflakes, pytest, and pylint'
user_options = [
('run-flakes', None, 'Run pyflakes'),
('skip-test', None, 'Skip running pytest'),
('skip-lint', None, 'Skip running pylint')
]
def initialize_options(self):
self.run_flakes = True
self.skip_test = False
self.skip_lint = False
def finalize_options(self):
pass
def run(self):
pytest_command = ['pytest-3']
flakes_command = ['pyflakes3', 'repolib']
lint_command = ['pylint', 'repolib']
if not self.skip_test:
subprocess.run(pytest_command)
if not self.run_flakes:
subprocess.run(flakes_command)
if not self.skip_lint:
subprocess.run(lint_command)
setup(
name='repolib',
version=get_version(),
author='Ian Santopietro',
author_email='ian@system76.com',
url='https://github.com/pop-os/repolib',
description='Easily manage software sources',
download_url='https://github.com/pop-os/repolib/releases',
long_description=long_description,
tests_require=['pytest'],
license='LGPLv3',
packages=['repolib', 'repolib/command', 'repolib/shortcuts', 'repolib/unittest'],
cmdclass={'release': Release, 'test': Test},
scripts=['bin/apt-manage'],
data_files=[
('share/bash-completion/completions', ['data/bash-completion/apt-manage']),
('share/zsh/vendor-completions', ['data/zsh-completion/_apt-manage']),
('/usr/share/dbus-1/system-services', ['data/org.pop_os.repolib.service']),
('/usr/share/polkit-1/actions', ['data/org.pop_os.repolib.policy']),
('/etc/dbus-1/system.d/', ['data/org.pop_os.repolib.conf']),
('/usr/lib/repolib', ['data/service.py', 'bin/add-apt-repository'])
]
)

1
orig.source.txt Normal file
View File

@ -0,0 +1 @@
Source is this git's root