recover: recover Malachite v3.0.0
parent
527507fc62
commit
7a7375f176
@ -1,8 +0,0 @@
|
||||
/target
|
||||
.idea
|
||||
.direnv
|
||||
|
||||
examples/workspace/*
|
||||
examples/repository/*
|
||||
!examples/workspace/mlc.toml
|
||||
!examples/repository/mlc.toml
|
@ -1,17 +0,0 @@
|
||||
image: "rust:latest"
|
||||
|
||||
cargo:version:
|
||||
script:
|
||||
- rustc --version && cargo --version
|
||||
|
||||
cargo:clippy:
|
||||
before_script:
|
||||
- rustup component add clippy
|
||||
script:
|
||||
- cargo clippy --no-deps -- -D clippy::all
|
||||
|
||||
cargo:fmt:
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
script:
|
||||
- cargo fmt --check
|
@ -0,0 +1,24 @@
|
||||
# Key:
|
||||
# {{ image }}: will be replaced with `base.image` from .mlc/config.toml
|
||||
# {{ pkg }} : will be replaced with the name of the package being built
|
||||
# Post-build, the contents of /out will be copied to the host at `repo.out` from .mlc/config.toml
|
||||
|
||||
FROM {{ image }}
|
||||
|
||||
RUN mkdir /out
|
||||
COPY {{ pkg }} /tmp/{{ pkg }}
|
||||
|
||||
RUN pacman -Syu --noconfirm
|
||||
RUN pacman -S --noconfirm --needed base-devel
|
||||
|
||||
RUN useradd -m -G wheel build-user
|
||||
RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
|
||||
RUN chown -R build-user /tmp/{{ pkg }}
|
||||
|
||||
USER build-user
|
||||
WORKDIR /tmp/{{ pkg }}
|
||||
|
||||
RUN makepkg -s {{ flags }} --noconfirm
|
||||
|
||||
USER root
|
||||
RUN cp *.pkg.tar.* /out
|
File diff suppressed because it is too large
Load Diff
@ -1,40 +1,46 @@
|
||||
[package]
|
||||
name = "Malachite"
|
||||
version = "2.1.1"
|
||||
authors = ["michal <michal@tar.black>"]
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
description = "Packaging tool for pacman repositories"
|
||||
repository = "https://github.com/crystal-linux/malachite"
|
||||
license-file = "LICENSE"
|
||||
keywords = ["pacman", "repository", "packaging"]
|
||||
categories = ["filesystem", "development-tools"]
|
||||
|
||||
[package.metadata]
|
||||
codename = "Mi Goreng"
|
||||
|
||||
[[bin]]
|
||||
name = "mlc"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
incremental = true
|
||||
debug = false
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
clap = { version = "3.2.8", features = ["derive", "suggestions"] }
|
||||
toml = { version = "0.5.9", default-features = false }
|
||||
serde = { version = "1.0.139", default-features = false }
|
||||
serde_derive = { version = "1.0.139", default-features = false }
|
||||
libc = { version = "0.2.126", default-features = false }
|
||||
colored = { version = "2.0.0", default-features = false }
|
||||
tabled = { version = "0.8.0", default-features = false, features = [
|
||||
libc = "0.2.137"
|
||||
clap = { version = "4.0.18", features = ["derive", "wrap_help"] }
|
||||
rust-embed = "6.4.2"
|
||||
thiserror = "1.0.37"
|
||||
i18n-embed = { version = "0.13.4", features = [
|
||||
"fluent-system",
|
||||
"desktop-requester",
|
||||
] }
|
||||
i18n-embed-fl = "0.6.4"
|
||||
color-eyre = { version = "0.6.2", features = ["issue-url", "url"] }
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.7.2"
|
||||
serde = { version = "1.0.147", default-features = false, features = [
|
||||
"derive",
|
||||
"color",
|
||||
"serde_derive",
|
||||
] }
|
||||
crossterm = { version = "0.25.0", default-features = false }
|
||||
regex = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
spinoff = { version = "0.5.4", default-features = false }
|
||||
rm_rf = { version = "0.6.2", default-features = false }
|
||||
podman-api = "0.8.0"
|
||||
names = { version = "0.14.0", default-features = false }
|
||||
futures-util = "0.3.25"
|
||||
tokio = { version = "1.21.2", features = ["full"] }
|
||||
fs_extra = "1.2.0"
|
||||
tar = "0.4.38"
|
||||
glob = "0.3.0"
|
||||
compress-tools = "0.14.0"
|
||||
gpgme = "0.11.0"
|
||||
tabled = { version = "0.10.0", features = ["color"] }
|
||||
liquid = "0.26.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
mimalloc = { version = "0.1.29" }
|
||||
[build-dependencies]
|
||||
cargo_toml = "0.15.2"
|
||||
serde = { version = "1.0.147", default-features = false, features = ["derive"] }
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Key:
|
||||
# {{ image }}: will be replaced with `base.image` from .mlc/config.toml
|
||||
# {{ name }} : will be replaced with `repo.name` from .mlc/config.toml
|
||||
# Post-build, the contents of /repo will be copied to the host at `repo.repo` from .mlc/config.toml
|
||||
# If `repo.security` is set to true, all resultant *.pkg.tar.* files will be GPG signed by the host
|
||||
|
||||
FROM {{ image }}
|
||||
|
||||
RUN mkdir /{{ repo }}
|
||||
COPY out /tmp/{{ name }}
|
||||
|
||||
RUN pacman -Syu --noconfirm
|
||||
RUN pacman -S --noconfirm --needed pacman-contrib
|
||||
|
||||
RUN useradd -m -G wheel generate-user
|
||||
RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
|
||||
RUN chown -R generate-user /tmp/{{ name }}
|
||||
|
||||
USER generate-user
|
||||
WORKDIR /tmp/{{ name }}
|
||||
|
||||
RUN repo-add {{ name }}.db.tar.gz *.pkg.tar.*
|
||||
|
||||
USER root
|
||||
RUN cp /tmp/{{ name }}/* /{{ repo }}
|
@ -1,674 +0,0 @@
|
||||
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>.
|
@ -1,62 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/crystal-linux/Malachite">
|
||||
<img src="https://getcryst.al/site/assets/other/logo.png" alt="Logo" width="150" height="150">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center">Malachite</h2>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/crystal-linux/.github/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-GPL--3.0-blue.svg" alt="License">
|
||||
<a href="https://github/crystal-linux/malachite"><img alt="GitHub isses" src="https://img.shields.io/github/issues-raw/crystal-linux/malachite"></a>
|
||||
<a href="https://github/crystal-linux/malachite"><img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr-raw/crystal-linux/malachite"></a><br>
|
||||
<a href="https://discord.gg/hYJgu8K5aA"><img alt="Discord" src="https://img.shields.io/discord/825473796227858482?color=blue&label=Discord&logo=Discord&logoColor=white"> </a>
|
||||
<a href="https://github.com/ihatethefrench"> <img src="https://img.shields.io/badge/Maintainer-@ihatethefrench-brightgreen" alt="The maintainer of this repository" href="https://github.com/ihatethefrench"></a><br>
|
||||
<a href="https://fosstodon.org/@crystal_linux"><img alt="Mastodon Follow" src="https://img.shields.io/mastodon/follow/108618426259408142?domain=https%3A%2F%2Ffosstodon.org">
|
||||
<a href="https://twitter.com/crystal_linux"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/crystal_linux"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
Malachite is a simple yet useful workspace and local repository management tool, made for packagers of Arch Linux based distributions.
|
||||
<h2 align="center"><a href="docs/GETTING_STARTED.md">--> Detailed Usage Guide <--</a></h2>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
### Basic Usage Guide
|
||||
|
||||
| Action | Command |
|
||||
|--------------------------------------------------------|-------------------------------------------|
|
||||
| Build a package | mlc build \<package\> [all if left empty] |
|
||||
| Generate local repository | mlc repo-gen |
|
||||
| Update local repos/PKGBUILDs | mlc pull/update [all if left empty] |
|
||||
| Create and/or open config file | mlc conf |
|
||||
| Initialises repo/workspace based on config in mlc.toml | mlc clone/init |
|
||||
| Displays information about a Malachite repository | mlc info/status |
|
||||
|
||||
### Pacman Repository Creation
|
||||
|
||||
- `mlc config` to create the config (and also populate it)
|
||||
- `mlc init` to build repository base from config file
|
||||
- `mlc build <package>` to either build individual packages, or don't specify package names to build all packages in mlc.toml
|
||||
- `build` typically automatically updates the repository unless `--no-regen` is passed, if so:
|
||||
- `mlc repo-gen` to generate functional pacman repository at \<name\>/\<name\>.db from built packages
|
||||
|
||||
|
||||
## How to build:
|
||||
|
||||
Tested on latest Cargo (1.60.0-nightly)
|
||||
|
||||
### Debug/development builds
|
||||
|
||||
- `cargo build`
|
||||
|
||||
### Optimised/release builds
|
||||
|
||||
- `cargo build --release`
|
||||
|
||||
### AUR
|
||||
|
||||
- https://aur.archlinux.org/packages/malachite
|
@ -0,0 +1,20 @@
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cargo_toml::Manifest;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct Metadata {
|
||||
codename: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let manifest = Manifest::<Metadata>::from_path_with_metadata(PathBuf::from("Cargo.toml"))
|
||||
.expect("Failed to read manifest (Cargo.toml)");
|
||||
|
||||
if let Some(package) = manifest.package {
|
||||
if let Some(metadata) = package.metadata {
|
||||
println!("cargo:rustc-env=MALACHITE_CODENAME={}", metadata.codename);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
# Common Features Between Modes
|
||||
As [mode]us, shared of between uh… repositories… or something.
|
||||
|
||||
### What you need to know
|
||||
Malachite is fairly fleshed out in Repository mode, and not so much in Workspace mode.
|
||||
|
||||
This isn't of course because I'm lazy and hate Workspace mode or anything, there's just not
|
||||
a lot *to add*.
|
||||
|
||||
Without further ado, let's take a look at this example config file.
|
||||
|
||||
```toml
|
||||
# mlc.toml
|
||||
|
||||
[base]
|
||||
mode = "workspace"
|
||||
smart_pull = true
|
||||
|
||||
[mode.workspace]
|
||||
git_info = true
|
||||
colorblind = true
|
||||
|
||||
[repositories]
|
||||
repos = [
|
||||
"foo:repo1:2",
|
||||
"foo:repo2/testing",
|
||||
"bar:baz!",
|
||||
"bar:qux/testing!:1",
|
||||
]
|
||||
|
||||
[repositories.urls]
|
||||
foo = "https://example.org/{}.git"
|
||||
bar = "https://example.org/other/{}.git"
|
||||
```
|
||||
|
||||
Now, this is going to look really confusing at first, but bear with me.
|
||||
|
||||
In this document, we'll cover only what is required to know for **both** modes.
|
||||
More specialized tutorials will be linked for each mode at the bottom of this page.
|
||||
|
||||
Let's start with the base(ics).
|
||||
|
||||
|
||||
### Base Config
|
||||
The base config defines a few common parameters between all the Malachite modes.
|
||||
|
||||
```toml
|
||||
[base]
|
||||
mode = "workspace"
|
||||
smart_pull = true
|
||||
```
|
||||
|
||||
In this snippet, we define `mode` to be `"workspace"`.
|
||||
|
||||
`base.mode` in Malachite can only ever be one of `"workspace"` or `"repository"`, and defines, drumroll…
|
||||
The mode in which it operates. If it is set to anything but those 2 modes, it crashes.
|
||||
|
||||
Also defined in this snippet is `smart_pull`, which controls whether to pull… smartly.
|
||||
|
||||
What that actually means is that instead of just performing a simple `git pull` in each repository, Malachite
|
||||
will:
|
||||
|
||||
- First run `git remote update` to fetch new remotes from origin
|
||||
- Then run `git status` and parse the output to see if the current branch is behind
|
||||
- If the current branch is behind, it runs a regular `git pull`, which will take advantage of the remotes
|
||||
already being updated.
|
||||
|
||||
Theoretically, this only actually speeds things up by a minute amount (think milliseconds, really). Where this feature shines however is in repository mode,
|
||||
where it enables helpful automation features such as `build_on_update`.
|
||||
|
||||
Regardless, it's recommended to keep this enabled for the slight speed-up, and only disable it if it causes issues.
|
||||
I've never personally had issues with it in the past, but who knows what could happen. This is Git we're talking about.
|
||||
|
||||
|
||||
### Repositories Config
|
||||
|
||||
The repositories config is realistically what makes Malachite churn repo butter internally. It's the whole
|
||||
purpose of what it does, and because of that we've tried to come up with a neat little system to help
|
||||
facilitate many packages without having to type each URL out a million times.
|
||||
|
||||
```toml
|
||||
[repositories]
|
||||
repos = [
|
||||
"foo:repo1:2",
|
||||
"foo:repo2/testing",
|
||||
"bar:baz!",
|
||||
"bar:qux/testing!:1",
|
||||
]
|
||||
|
||||
[repositories.urls]
|
||||
foo = "https://example.org/{}.git"
|
||||
bar = "https://example.org/other/{}.git"
|
||||
```
|
||||
|
||||
The way this works is simple:
|
||||
- We have 2 URLs in the `repositories.urls` key.
|
||||
- Each `repo` in the `repositories.repos` key is prefixed with an identifier.
|
||||
- If the number is `foo`, it'll insert the URL with the id `foo`.
|
||||
- Specifically, in the URL, it'll insert the defined `repo`'s name in place of the `%repo%` substring.
|
||||
|
||||
#### Hang on, what are the special symbols????
|
||||
|
||||
I'm glad you asked!
|
||||
- If you want to clone a specific branch, simply use the `/` delimiter. To clone repository `foo` on branch `bar`, use `id:foo/bar`.
|
||||
- If you want a specific package to build first, use instances of `!` to set priority. This is explained later in the [Repository Mode](REPOSITORY_MODE.md) page
|
||||
|
||||
The last `:` delimiter is entirely optional, and behaves differently depending on the mode:
|
||||
- In Repository mode, it defines the desired commit hash/rev/tag to checkout on repository clone
|
||||
- In Workspace mode, it defines the desired depth to clone the repository, useful with large git repositories, such as `nixpkgs`.
|
||||
|
||||
That's literally it!
|
||||
|
||||
|
||||
### Mode-Specific Config
|
||||
|
||||
For mode-specific config, avert your eyes to the following links!
|
||||
|
||||
- [Workspace Mode](WORKSPACE_MODE.md)
|
||||
- [Repository Mode](REPOSITORY_MODE.md)
|
||||
|
||||
### Examples
|
||||
|
||||
Functioning config examples for both modes are available in the [examples](../examples) directory!
|
||||
|
||||
### Usage
|
||||
|
||||
Alternatively, you can look at the [Usage](USAGE.md) guide!
|
||||
|
@ -1,47 +0,0 @@
|
||||
# Getting Started With Malachite
|
||||
Baby's first Malachite repository!
|
||||
|
||||
### What you need to know
|
||||
|
||||
Malachite is:
|
||||
- A pacman repository manager
|
||||
- A workspace manager
|
||||
- ~~Awesome~~
|
||||
|
||||
Malachite isn't:
|
||||
- The end-all solution for all pacman repositories
|
||||
- Perfect
|
||||
|
||||
|
||||
### With that out of the way
|
||||
|
||||
Hi! My name is Michal, and I wrote this tool pretty much on my own for [Crystal Linux](https://getcryst.al);
|
||||
but it is not at all exclusive to Crystal. This tool should and will work on and for any pacman-based
|
||||
distribution (so long as it packages all of Malachite's dependencies, of course).
|
||||
|
||||
Throughout this tutorial, I'll explain each little feature of Malachite in what I hope to be bite-sized and
|
||||
programmatic chunks.
|
||||
|
||||
Without further ado, let's begin with the first, most important question:
|
||||
|
||||
|
||||
### Modes
|
||||
|
||||
What mode are you using malachite in?
|
||||
|
||||
Currently, malachite supports 2 modes:
|
||||
|
||||
#### Repository Mode
|
||||
- Allows the user to configure and manage a remote (or local) pacman-based package repository
|
||||
- Allows for customisability in repository name, signing preferences, signing key etc.
|
||||
- Allows for basic levels of automation, by using features such as build_on_update
|
||||
|
||||
#### Workspace Mode
|
||||
- The most basic functionality of Malachite
|
||||
- Just clones git directories into a "Workspace" directory for easier management
|
||||
- Allows for basic pulling operations to keep your repositories up-to-date
|
||||
|
||||
These modes essentially dictate everything about how Malachite functions, so much so that I now need to
|
||||
split this page off before it gets too long!
|
||||
|
||||
For more info, get started with the [Common Features](COMMON_FEATURES.md) page!
|
@ -1,41 +0,0 @@
|
||||
# Repository Mode
|
||||
PacManage your repositories in style!
|
||||
|
||||
### Repository Config
|
||||
|
||||
As opposed to the rather barren Workspace mode, the Repository mode config is rather fleshed out;
|
||||
and we have a few options to choose from.
|
||||
|
||||
Let's take an example section from a Repository mode config,
|
||||
|
||||
```toml
|
||||
[mode.repository]
|
||||
name = "example"
|
||||
build_on_update = true
|
||||
|
||||
[mode.repository.signing]
|
||||
enabled = true
|
||||
key = "you@example.org"
|
||||
on_gen = true
|
||||
```
|
||||
|
||||
### Basic Repository Config
|
||||
|
||||
To start with, there are 2 main config keys to Repository mode:
|
||||
- `name`: Defines what pacman calls your repository.
|
||||
- `build_on_update`: In conjunction with `smart_pull`, defines whether to rebuild packages automatically when an update is detected.
|
||||
|
||||
### Signing
|
||||
|
||||
Malachite also supports, and encourages, the signing of packages.
|
||||
GPG Signing packages ensures that the user receives exactly what you packaged, without any chance of tampering.
|
||||
|
||||
Calling back to the example above, we can see 3 config keys:
|
||||
|
||||
- `enabled`: Defines whether to sign packages (heavily encouraged).
|
||||
- `key`: Defines the GPG key ID to use for signing.
|
||||
- `on_gen`: Defines whether to sign packages when they are built, or all at once on repository generation (this is also recommended).
|
||||
|
||||
---
|
||||
|
||||
You can return to [Getting Started](GETTING_STARTED.md) page here!
|
@ -1,32 +0,0 @@
|
||||
# Detailed Usage
|
||||
Work it harder, make it better!
|
||||
|
||||
### Global Flags
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--verbose`, `-v` | Prints lots of debug information to `stderr`. If something doesn't go right, sending us the output with this enabled will help greatly |
|
||||
| `--exclude`, `-x` | Excludes the supplied package from the current operation. Can be used multiple times. |
|
||||
|
||||
### Basic Commands
|
||||
|
||||
| Action | Command | Extra Flags |
|
||||
|-----------------------------------------------------------------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||
| Build a package/packages. | `mlc build <package>` [all if left empty] | `--no-regen`: Doesn't regenerate repository after build |
|
||||
| Generate pacman repository | `mlc repo-gen` | |
|
||||
| Update local repos/PKGBUILDs | `mlc pull/update` [all if left empty] | `--no-regen`: If `mode.repository.build_on_update` is `true`, Do not regenerate repository after package rebuild |
|
||||
| Create and/or open config file | `mlc conf` | |
|
||||
| Initialises repo/workspace based on config in mlc.toml | `mlc clone/init` | |
|
||||
| Displays an info panel/overview of the current repo | `mlc info/status` | |
|
||||
| Resets Malachite repository by deleting all directories, omitting `mlc.toml` and `.git` | `mlc clean/reset` | `--force`: Remove dirty directories (unstaged, untracked, etc) |
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| AppExitCode (named Enum) | Exit code (i32) | Error Description |
|
||||
|--------------------------|-----------------|--------------------------------------------------------------------------------------------------------|
|
||||
| `RunAsRoot` | `1` | Malachite was run as root. This is highly discouraged. So highly, in fact, that it will refuse to run. |
|
||||
| `PkgsNotFound` | `2` | No packages were specified/found for the desired operation |
|
||||
| `DirNotEmpty` | `3` | The creation of a Malachite repository was attempted in a non-empty directory |
|
||||
| `ConfigParseError` | `4` | The config file could not be parsed |
|
||||
| `RepoParseError` | `5` | The repository info could not be parsed |
|
||||
| `RepoNotClean` | `6` | The git repository is not clean and cannot be removed without `--force` |
|
@ -1,31 +0,0 @@
|
||||
# Workspace Mode
|
||||
You'll never have to work(space) another day in your life!
|
||||
|
||||
### Workspace Config
|
||||
|
||||
Taking an example section from the Workspace mode config,
|
||||
|
||||
```toml
|
||||
[mode.workspace]
|
||||
git_info = true
|
||||
colorblind = true
|
||||
```
|
||||
|
||||
Currently, Workspace mode only has 2 options, both pertaining to the display of information. (`mlc info`)
|
||||
|
||||
The first key is `git_info`, which is a boolean value. If it is true, the git information will be displayed alongside repository information.
|
||||
|
||||
This information will be formatted as so: `D Pl Ps <Latest Commit Hash>`
|
||||
|
||||
The key for the values is as follows:
|
||||
- D: Whether the repository is dirty or not (unstaged changes)
|
||||
- Pl: Whether there are unpulled changes at the remote
|
||||
- Ps: Whether there are unpushed changes in your local repository
|
||||
|
||||
These will be typically displayed in either Green (Clean) or Red (Dirty)
|
||||
|
||||
However, if `colorblind` is set to true, the colors will instead be set to Blue (Clean) or Dark Red (Dirty), to be more discernible to colorblind users.
|
||||
|
||||
---
|
||||
|
||||
You can return to [Getting Started](GETTING_STARTED.md) page here!
|
@ -1,26 +0,0 @@
|
||||
[base]
|
||||
mode = "repository"
|
||||
smart_pull = true
|
||||
|
||||
[mode.repository]
|
||||
name = "repository-test"
|
||||
build_on_update = true
|
||||
|
||||
[mode.repository.signing]
|
||||
enabled = true
|
||||
key = "michal@tar.black"
|
||||
on_gen = true
|
||||
|
||||
[repositories]
|
||||
repos = [
|
||||
"crs:malachite/development:0a5bdc9", # Note, in this example, these two
|
||||
"mic:apod:v.1.1.2", # will fail to build.
|
||||
"pkg:pfetch!",
|
||||
"nms:rpass" # This too
|
||||
]
|
||||
|
||||
[repositories.urls]
|
||||
crs = "https://github.com/crystal-linux/{}"
|
||||
pkg = "https://github.com/crystal-linux/pkgbuild.{}"
|
||||
mic = "https://git.tar.black/michal/{}"
|
||||
nms = "https://github.com/not-my-segfault/{}"
|
@ -1,20 +0,0 @@
|
||||
[base]
|
||||
mode = "workspace"
|
||||
smart_pull = true
|
||||
|
||||
[mode.workspace]
|
||||
git_info = true
|
||||
colorblind = false
|
||||
|
||||
[repositories]
|
||||
repos = [
|
||||
"crs:amethyst",
|
||||
"crs:malachite/development!",
|
||||
"aur:notop-git",
|
||||
"nix:nixpkgs/nixos-unstable:1",
|
||||
]
|
||||
|
||||
[repositories.urls]
|
||||
crs = "https://github.com/crystal-linux/{}"
|
||||
aur = "https://aur.archlinux.org/{}"
|
||||
nix = "https://github.com/nixos/{}"
|
@ -0,0 +1,4 @@
|
||||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
@ -0,0 +1,23 @@
|
||||
# src/args.rs
|
||||
|
||||
about = A scriptable and declarative Pacman repository management tool
|
||||
|
||||
help-verbose = Set amount of logging output
|
||||
help-exclude = Exclude given repositories from the operation
|
||||
|
||||
help-init = Initializes empty Malachite repository in current directory
|
||||
|
||||
help-pull = Clone and/or update Git repositories
|
||||
help-pull-rebuild = Rebuild packages if new commits are found
|
||||
help-pull-concurrent = Number of concurrent rebuilds
|
||||
|
||||
help-build = Build given package(s)
|
||||
help-build-packages = Package(s) to build
|
||||
help-build-concurrent = Number of concurrent builds
|
||||
|
||||
help-clean = Remove Git repositories no longer present in the configuration
|
||||
help-clean-prune = Keep X latest versions of each package, removing any that are older
|
||||
|
||||
help-info = Display information about the current Malachite repository
|
||||
|
||||
help-generate = Generate Pacman repository from built packages
|
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env just --justfile
|
||||
|
||||
release:
|
||||
cargo build --release
|
||||
|
||||
lint:
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo fmt -- --check
|
||||
|
||||
test:
|
||||
if [ -d example ]; then rm -rf example; fi
|
||||
mkdir example
|
||||
|
||||
pushd example && cargo run -- init && cargo run -- pull && popd
|
@ -1,75 +1,75 @@
|
||||
use clap::{ArgAction, Parser, Subcommand};
|
||||
|
||||
use crate::fl;
|
||||
|
||||
static VERSION: &str = concat!(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (",
|
||||
env!("MALACHITE_CODENAME"),
|
||||
")"
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(name = "Malachite", version = env ! ("CARGO_PKG_VERSION"), about = env ! ("CARGO_PKG_DESCRIPTION"))]
|
||||
#[command(bin_name = "mlc", name = "Malachite", version = VERSION, about = fl!("about"), infer_subcommands = true)]
|
||||
pub struct Args {
|
||||
#[clap(subcommand)]
|
||||
pub subcommand: Option<Operation>,
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Operation,
|
||||
|
||||
/// Sets the level of verbosity
|
||||
#[clap(long, short, global(true), action = ArgAction::SetTrue)]
|
||||
pub verbose: bool,
|
||||
#[arg(long, short, action = ArgAction::Count, global = true, help = fl!("help-verbose"))]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Excludes packages from given operation, if applicable
|
||||
#[clap(short = 'x', long = "exclude", action = ArgAction::Append, takes_value = true)]
|
||||
#[arg(long, short = 'x', action = ArgAction::Append, global = true, help = fl!("help-exclude"))]
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Operation {
|
||||
/// Builds the given packages
|
||||
#[clap(name = "build", aliases = & ["b"])]
|
||||
Build {
|
||||
/// The packages to operate on
|
||||
#[clap(name = "package(s)", action = ArgAction::Append, index = 1)]
|
||||
packages: Vec<String>,
|
||||
#[command(bin_name = "mlc", name = "init", short_flag = 'I', about = fl!("help-init"))]
|
||||
Init,
|
||||
|
||||
#[command(bin_name = "mlc", name = "pull", short_flag = 'P', about = fl!("help-pull"))]
|
||||
Pull {
|
||||
#[arg(long, short, action = ArgAction::SetTrue, help = fl!("help-pull-rebuild"))]
|
||||
rebuild: bool,
|
||||
|
||||
/// Does not regenerate repository after building given package(s)
|
||||
#[clap(short = 'n', long = "no-regen", action = ArgAction::SetTrue)]
|
||||
no_regen: bool,
|
||||
#[arg(long, short, help = fl!("help-pull-concurrent"))]
|
||||
concurrent: Option<u8>,
|
||||
},
|
||||
|
||||
/// Generates Pacman repository from built packages
|
||||
#[clap(name = "repo-gen", aliases = & ["repo", "r"])]
|
||||
RepoGen,
|
||||
#[command(bin_name = "mlc", name = "build", short_flag = 'B', about = fl!("help-build"))]
|
||||
Build {
|
||||
#[arg(required = true, help = fl!("help-build-packages"))]
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Clones all git repositories from mlc.toml branching from current directory
|
||||
#[clap(name = "clone", aliases = & ["init", "i", "c"])]
|
||||
Clone,
|
||||
#[arg(long, short, help = fl!("help-build-concurrent"))]
|
||||
concurrent: Option<u8>,
|
||||
},
|
||||
|
||||
/// Removes everything in directory except for mlc.toml
|
||||
#[clap(name = "clean", aliases = & ["clean", "cl", "reset"])]
|
||||
#[command(bin_name = "mlc", name = "clean", short_flag = 'C', about = fl!("help-clean"))]
|
||||
Clean {
|
||||
/// Force removes everything, even if git directory is dirty or has unpushed changes or changes at remote
|
||||
#[clap(short = 'f', long = "force", action = ArgAction::SetTrue)]
|
||||
force: bool,
|
||||
#[arg(long, short, help = fl!("help-clean-prune"))]
|
||||
prune: Option<u8>,
|
||||
},
|
||||
|
||||
/// Removes all but the latest 3 versions of each package in a repository
|
||||
#[clap(name = "prune", aliases = & ["prune", "p"])]
|
||||
Prune,
|
||||
|
||||
/// Shows an info panel/overview about the current repository
|
||||
#[clap(name = "info", aliases = & ["status", "s", "i"])]
|
||||
#[command(bin_name = "mlc", name = "info", short_flag = 'i', about = fl!("help-info"))]
|
||||
Info,
|
||||
|
||||
/// Pulls in git repositories from mlc.toml branching from current directory
|
||||
#[clap(name = "pull", aliases = & ["u"])]
|
||||
Pull {
|
||||
/// The packages to operate on
|
||||
#[clap(name = "package(s)", help = "The packages to operate on", action = ArgAction::Append, index = 1)]
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Does not regenerate repository after pulling given package(s). This only applies if build_on_update is set to true in repository config
|
||||
#[clap(short = 'n', long = "no-regen", action = ArgAction::SetTrue)]
|
||||
no_regen: bool,
|
||||
#[command(bin_name = "mlc", name = "generate", short_flag = 'G', about = fl!("help-generate"))]
|
||||
Generate,
|
||||
}
|
||||
|
||||
/// Will prompt for confirmation before rebuilding a package
|
||||
#[clap(long, action = ArgAction::SetTrue)]
|
||||
interactive: bool,
|
||||
},
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct GlobalArgs {
|
||||
pub verbosity: u8,
|
||||
pub exclude: Vec<String>,
|
||||
}
|
||||
|
||||
/// Create and/or open local config file
|
||||
#[clap(name = "config", aliases = & ["conf"])]
|
||||
Config,
|
||||
impl GlobalArgs {
|
||||
pub fn new() -> Self {
|
||||
let args: Args = Args::parse();
|
||||
Self {
|
||||
verbosity: args.verbose,
|
||||
exclude: args.exclude,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,188 @@
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fs_extra::dir::{self, CopyOptions};
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use glob::glob;
|
||||
use names::{Generator, Name};
|
||||
use podman_api::opts::ContainerCreateOpts;
|
||||
use podman_api::opts::ImageBuildOpts;
|
||||
use podman_api::Podman;
|
||||
|
||||
use crate::errors::AppResult;
|
||||
use crate::utils::{uid, ShellCommand};
|
||||
|
||||
pub const BUILDFILE: &str = include_str!("../Buildfile");
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BuildKind {
|
||||
Podman { image: String },
|
||||
Host,
|
||||
}
|
||||
|
||||
impl BuildKind {
|
||||
pub fn image(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Podman { image } => Some(image),
|
||||
Self::Host => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Build {
|
||||
pub kind: BuildKind,
|
||||
pub name: String,
|
||||
pub pkg: String,
|
||||
pub out: PathBuf,
|
||||
pub flags: Vec<String>,
|
||||
}
|
||||
|
||||
impl Build {
|
||||
pub fn new<S: ToString, P: Into<PathBuf>>(
|
||||
kind: BuildKind,
|
||||
pkg: S,
|
||||
out: P,
|
||||
flags: Vec<String>,
|
||||
) -> AppResult<Self> {
|
||||
let mut generator = Generator::with_naming(Name::Numbered);
|
||||
|
||||
let pod = Self {
|
||||
kind,
|
||||
name: generator.next().unwrap(),
|
||||
pkg: pkg.to_string(),
|
||||
out: out.into(),
|
||||
flags,
|
||||
};
|
||||
|
||||
Ok(pod)
|
||||
}
|
||||
|
||||
pub async fn build(&self) -> AppResult<()> {
|
||||
fs::create_dir_all(&self.out)?;
|
||||
|
||||
match &self.kind {
|
||||
BuildKind::Podman { .. } => self.podman_build().await?,
|
||||
BuildKind::Host => self.host_build().await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn podman_build(&self) -> AppResult<()> {
|
||||
let uid = uid();
|
||||
|
||||
let podman = Podman::new(format!("unix:///var/run/user/{}/podman/podman.sock", uid))?;
|
||||
let image = self.kind.image().unwrap();
|
||||
|
||||
let buildfile = fs::read_to_string(".mlc/Buildfile")?;
|
||||
let buildfile_template = liquid::ParserBuilder::with_stdlib()
|
||||
.build()?
|
||||
.parse(&buildfile)?;
|
||||
|
||||
let buildfile_values = liquid::object!({
|
||||
"image": image,
|
||||
"pkg": &self.pkg,
|
||||
"flags": &self.flags.join(" "),
|
||||
});
|
||||
|
||||
let buildfile = buildfile_template.render(&buildfile_values)?;
|
||||
|
||||
let build_path = std::env::temp_dir().join(&self.name);
|
||||
|
||||
fs::create_dir_all(&build_path)?;
|
||||
fs::write(build_path.join("Buildfile"), buildfile)?;
|
||||
|
||||
dir::copy(&self.pkg, &build_path, &CopyOptions::new())?;
|
||||
dir::copy(".mlc/store", &build_path, &CopyOptions::new())?;
|
||||
|
||||
let opts = &ImageBuildOpts::builder(build_path.to_string_lossy())
|
||||
.dockerfile("Buildfile")
|
||||
.tag(&self.name)
|
||||
.build();
|
||||
|
||||
let mut log_file = File::create(&format!(".mlc/logs/{}-{}.log", &self.pkg, &self.name))?;
|
||||
|
||||
let images = podman.images();
|
||||
match images.build(opts) {
|
||||
Ok(mut build_stream) => {
|
||||
while let Some(chunk) = build_stream.next().await {
|
||||
match chunk {
|
||||
Ok(chunk) => {
|
||||
println!("{}", chunk.stream);
|
||||
let _ = log_file.write(chunk.stream.as_bytes())?;
|
||||
}
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
|
||||
podman
|
||||
.containers()
|
||||
.create(
|
||||
&ContainerCreateOpts::builder()
|
||||
.image(&self.name)
|
||||
.name(&self.name)
|
||||
.build(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes = podman
|
||||
.containers()
|
||||
.get(&self.name)
|
||||
.copy_from("/out")
|
||||
.try_concat()
|
||||
.await?;
|
||||
|
||||
let mut archive = tar::Archive::new(&bytes[..]);
|
||||
archive.unpack(self.out.parent().unwrap())?;
|
||||
|
||||
podman.images().get(&self.name).remove().await?;
|
||||
|
||||
fs::remove_dir_all(build_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn host_build(&self) -> AppResult<()> {
|
||||
let build_path = std::env::temp_dir().join(&self.name);
|
||||
|
||||
fs::create_dir_all(&build_path)?;
|
||||
dir::copy(&self.pkg, &build_path, &CopyOptions::new())?;
|
||||
dir::copy(".mlc/store", &build_path, &CopyOptions::new())?;
|
||||
|
||||
ShellCommand::makepkg()
|
||||
.args(["-s", "--noconfirm"])
|
||||
.args(&self.flags)
|
||||
.cwd(&build_path.join(&self.pkg))
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
|
||||
let glob = glob(
|
||||
&build_path
|
||||
.join(&self.pkg)
|
||||
.join("*.pkg.tar.*")
|
||||
.to_string_lossy(),
|
||||
)?;
|
||||
for entry in glob {
|
||||
match entry {
|
||||
Ok(path) => {
|
||||
fs_extra::copy_items(
|
||||
&[path.to_string_lossy().to_string()],
|
||||
&self.out,
|
||||
&CopyOptions::new(),
|
||||
)?;
|
||||
}
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_dir_all(build_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
use crate::errors::AppResult;
|
||||
use crate::utils::ShellCommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitCloneBuilder {
|
||||
pub url: String,
|
||||
pub branch: Option<String>,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitCloneBuilder {
|
||||
pub fn new<S, P>(url: S, path: P) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
Self {
|
||||
url: url.to_string(),
|
||||
branch: None,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch(mut self, branch: String) -> Self {
|
||||
self.branch = Some(branch);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clone(&self) -> AppResult<ShellCommand> {
|
||||
let mut args = vec![
|
||||
"clone".to_string(),
|
||||
self.url.clone(),
|
||||
self.path.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
if let Some(branch) = &self.branch {
|
||||
args.push("--branch".to_string());
|
||||
args.push(branch.clone());
|
||||
}
|
||||
|
||||
let command = ShellCommand::git().args(args);
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitPullBuilder {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitPullBuilder {
|
||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
|
||||
pub fn pull(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git().cwd(self.path.clone()).args(["pull"]);
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitCheckoutBuilder {
|
||||
pub path: PathBuf,
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
impl GitCheckoutBuilder {
|
||||
pub fn new<S, P>(path: P, branch: S) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
Self {
|
||||
path: path.into(),
|
||||
branch: branch.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkout(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.arg("checkout")
|
||||
.arg(self.branch.clone());
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitStatusBuilder {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitStatusBuilder {
|
||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git().cwd(self.path.clone()).arg("status");
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitInitBuilder {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitInitBuilder {
|
||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
|
||||
pub fn init(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git().cwd(self.path.clone()).arg("init");
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitAddBuilder {
|
||||
pub path: PathBuf,
|
||||
pub refspecs: Vec<String>,
|
||||
}
|
||||
|
||||
impl GitAddBuilder {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
refspecs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn refspec<S: ToString>(mut self, refspec: S) -> Self {
|
||||
self.refspecs.push(refspec.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn refspecs<S, V>(mut self, refspecs: V) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
V: IntoIterator<Item = S>,
|
||||
{
|
||||
self.refspecs
|
||||
.extend(refspecs.into_iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.arg("add")
|
||||
.args(self.refspecs.clone());
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitFetchBuilder {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitFetchBuilder {
|
||||
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||
Self { path: path.into() }
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> AppResult<ShellCommand> {
|
||||
let command = ShellCommand::git().cwd(self.path.clone()).arg("fetch");
|
||||
|
||||
Ok(command)
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ops::Not;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::build::BUILDFILE;
|
||||
use crate::builders::{GitAddBuilder, GitInitBuilder};
|
||||
use crate::errors::ConfigError;
|
||||
use crate::generate::GENERATEFILE;
|
||||
use crate::lock::LOCKFILE_VERSION;
|
||||
use crate::AppError;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub base: BaseConfig,
|
||||
pub repo: RepoConfig,
|
||||
pub repositories: Repositories,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BaseConfig {
|
||||
pub src: PathBuf,
|
||||
pub podman: bool,
|
||||
pub image: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RepoConfig {
|
||||
pub name: String,
|
||||
pub out: PathBuf,
|
||||
pub repo: PathBuf,
|
||||
pub security: SecurityConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SecurityConfig {
|
||||
pub sign: bool,
|
||||
pub sign_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Repositories {
|
||||
pub names: Vec<GitRepo>,
|
||||
pub keys: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(into = "String", try_from = "String")]
|
||||
pub struct GitRepo {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub rev: Option<String>,
|
||||
}
|
||||
|
||||
impl From<String> for GitRepo {
|
||||
fn from(s: String) -> Self {
|
||||
let (key, repo) = s.split_once(':').unwrap();
|
||||
let (name, rev) = repo.split_once('@').unwrap_or((repo, ""));
|
||||
|
||||
Self {
|
||||
key: key.to_string(),
|
||||
name: name.to_string(),
|
||||
rev: rev.is_empty().not().then_some(rev.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitRepo> for String {
|
||||
fn from(repo: GitRepo) -> Self {
|
||||
if let Some(rev) = repo.rev {
|
||||
format!("{}:{}@{}", repo.key, repo.name, rev)
|
||||
} else {
|
||||
format!("{}:{}", repo.key, repo.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRepo {
|
||||
pub fn expand(&self, config: &Config) -> Result<String, ConfigError> {
|
||||
let url = config
|
||||
.repositories
|
||||
.keys
|
||||
.get(&self.key)
|
||||
.ok_or_else(|| ConfigError::Expand(format!("Unknown key: {}", self.key)))?;
|
||||
|
||||
Ok(url.replace("{}", &self.name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let pwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let name = pwd
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::from("mlc".to_string()));
|
||||
Self {
|
||||
base: BaseConfig {
|
||||
src: pwd.clone(),
|
||||
podman: false,
|
||||
image: "archlinux:latest".to_string(),
|
||||
},
|
||||
repo: RepoConfig {
|
||||
name: name.to_string(),
|
||||
out: pwd.join("out"),
|
||||
repo: pwd.join("repo"),
|
||||
security: SecurityConfig {
|
||||
sign: false,
|
||||
sign_key: None,
|
||||
},
|
||||
},
|
||||
repositories: Repositories {
|
||||
names: vec![
|
||||
GitRepo {
|
||||
key: "crs".to_string(),
|
||||
name: "malachite".to_string(),
|
||||
rev: Some("mlc3-rewrite".to_string()),
|
||||
},
|
||||
GitRepo {
|
||||
key: "aur".to_string(),
|
||||
name: "ame".to_string(),
|
||||
rev: None,
|
||||
},
|
||||
],
|
||||
keys: HashMap::from([
|
||||
(
|
||||
"crs".to_string(),
|
||||
"https://git.getcryst.al/crystal/software/{}.git".to_string(),
|
||||
),
|
||||
(
|
||||
"aur".to_string(),
|
||||
"https://aur.archlinux.org/{}.git".to_string(),
|
||||
),
|
||||
]),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn init() -> Result<(), AppError> {
|
||||
let pwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let config = Config::default();
|
||||
|
||||
if pwd.read_dir()?.count() > 0 {
|
||||
return Err(AppError::Config(ConfigError::Init(
|
||||
"Directory is not empty".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
std::fs::create_dir(".mlc")?;
|
||||
std::fs::write(
|
||||
".mlc/config.toml",
|
||||
toml::to_string_pretty(&config)
|
||||
.map_err(|e| AppError::Config(ConfigError::Serialize(e)))?,
|
||||
)?;
|
||||
std::fs::write(".mlc/Buildfile", BUILDFILE)?;
|
||||
std::fs::write(".mlc/Generatefile", GENERATEFILE)?;
|
||||
|
||||
std::fs::create_dir(".mlc/conf.d")?;
|
||||
std::fs::create_dir(".mlc/store")?;
|
||||
std::fs::create_dir(".mlc/logs")?;
|
||||
|
||||
std::fs::write(
|
||||
".mlc/mlc.lock",
|
||||
format!("[lockfile]\nversion = '{}'\n\n[remote]\n", LOCKFILE_VERSION),
|
||||
)?;
|
||||
std::fs::write(".mlc/conf.d/.gitkeep", "\n")?;
|
||||
std::fs::write(".mlc/store/.gitkeep", "\n")?;
|
||||
std::fs::write(".mlc/logs/.gitkeep", "\n")?;
|
||||
std::fs::write(".gitignore", "/*/\n!/.mlc")?;
|
||||
|
||||
GitInitBuilder::new(pwd.clone()).init()?.silent()?;
|
||||
|
||||
GitAddBuilder::new(pwd)
|
||||
.refspecs([".gitignore", ".mlc/**"])
|
||||
.add()?
|
||||
.silent()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Config, AppError> {
|
||||
let pwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let config = toml::from_str(&std::fs::read_to_string(pwd.join(".mlc/config.toml"))?)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Malachite should not be run as root")]
|
||||
RunAsRoot,
|
||||
|
||||
#[error(transparent)]
|
||||
Config(#[from] ConfigError),
|
||||
|
||||
#[error(transparent)]
|
||||
Glob(#[from] glob::PatternError),
|
||||
|
||||
#[error(transparent)]
|
||||
Git(#[from] GitError),
|
||||
|
||||
#[error(transparent)]
|
||||
LibArchive(#[from] compress_tools::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Gpg(#[from] gpgme::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Podman(#[from] podman_api::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
|
||||
#[error(transparent)]
|
||||
Liquid(#[from] liquid::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum IoError {
|
||||
#[error("IO Error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
FsExtra(#[from] fs_extra::error::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GitError {
|
||||
#[error("Invalid Git repository: {0}")]
|
||||
InvalidRepository(String),
|
||||
|
||||
#[error("Unable to merge: {0}")]
|
||||
MergeError(String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("Failed to deserialize config: {0}")]
|
||||
Deserialize(#[from] toml::de::Error),
|
||||
|
||||
#[error("Failed to serialize config: {0}")]
|
||||
Serialize(#[from] toml::ser::Error),
|
||||
|
||||
#[error("Failed to expand repositories: {0}")]
|
||||
Expand(String),
|
||||
|
||||
#[error("Failed to initialize config: {0}")]
|
||||
Init(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
AppError::Io(IoError::Io(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fs_extra::error::Error> for AppError {
|
||||
fn from(e: fs_extra::error::Error) -> Self {
|
||||
AppError::Io(IoError::FsExtra(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for AppError {
|
||||
fn from(e: toml::de::Error) -> Self {
|
||||
AppError::Config(ConfigError::Deserialize(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for AppError {
|
||||
fn from(e: toml::ser::Error) -> Self {
|
||||
AppError::Config(ConfigError::Serialize(e))
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
use std::env::set_current_dir;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use fs_extra::dir;
|
||||
use futures_util::StreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use glob::glob;
|
||||
use gpgme::{Context, Protocol, SignMode};
|
||||
use names::Generator;
|
||||
use podman_api::opts::ContainerCreateOpts;
|
||||
use podman_api::opts::ImageBuildOpts;
|
||||
use podman_api::Podman;
|
||||
|
||||
use crate::config::SecurityConfig;
|
||||
use crate::errors::AppResult;
|
||||
use crate::utils::{uid, ShellCommand};
|
||||
|
||||
pub const GENERATEFILE: &str = include_str!("../Generatefile");
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateKind {
|
||||
Podman { image: String },
|
||||
Host,
|
||||
}
|
||||
|
||||
impl GenerateKind {
|
||||
pub fn image(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Podman { image } => Some(image),
|
||||
Self::Host => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PacmanRepository {
|
||||
pub kind: GenerateKind,
|
||||
pub name: String,
|
||||
pub tempdir: String,
|
||||
pub out: PathBuf,
|
||||
pub repo: PathBuf,
|
||||
pub security: SecurityConfig,
|
||||
}
|
||||
|
||||
impl PacmanRepository {
|
||||
pub fn new<S: ToString, P: Into<PathBuf>>(
|
||||
kind: GenerateKind,
|
||||
name: S,
|
||||
out: P,
|
||||
repo: P,
|
||||
security: SecurityConfig,
|
||||
) -> Self {
|
||||
let mut generator = Generator::with_naming(names::Name::Numbered);
|
||||
|
||||
Self {
|
||||
kind,
|
||||
name: name.to_string(),
|
||||
tempdir: generator.next().unwrap(),
|
||||
out: out.into(),
|
||||
repo: repo.into(),
|
||||
security,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate(&self) -> AppResult<()> {
|
||||
fs::create_dir_all(&self.repo)?;
|
||||
|
||||
match &self.kind {
|
||||
GenerateKind::Podman { .. } => self.podman_generate().await?,
|
||||
GenerateKind::Host => self.host_generate()?,
|
||||
}
|
||||
|
||||
if self.security.sign {
|
||||
let packages: Vec<PathBuf> = glob(&format!("{}/*.pkg.tar.*", self.repo.display()))?
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
let mut gpg_ctx = Context::from_protocol(Protocol::OpenPgp)?;
|
||||
gpg_ctx.set_armor(true);
|
||||
|
||||
for package in packages {
|
||||
let contents = fs::read(&package)?;
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
gpg_ctx.sign(SignMode::Detached, &contents, &mut buffer)?;
|
||||
|
||||
let sigfile = format!("{}.sig", package.display());
|
||||
fs::write(sigfile, buffer)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn host_generate(&self) -> AppResult<()> {
|
||||
if self.repo.exists() {
|
||||
fs::remove_dir_all(&self.repo)?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(&self.repo)?;
|
||||
|
||||
let generate_path = std::env::temp_dir().join(&self.tempdir);
|
||||
fs::create_dir_all(&generate_path)?;
|
||||
|
||||
let copy_opts = dir::CopyOptions {
|
||||
content_only: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
dir::copy(&self.out, &generate_path, ©_opts)?;
|
||||
|
||||
let mlc_dir = std::env::current_dir()?;
|
||||
set_current_dir(&generate_path)?;
|
||||
|
||||
let files: Vec<String> = glob("*.pkg.tar.*")?
|
||||
.filter_map(Result::ok)
|
||||
.map(|p| p.display().to_string())
|
||||
.collect();
|
||||
|
||||
ShellCommand::repo_add()
|
||||
.arg(format!("{}.db.tar.gz", self.name))
|
||||
.args(files)
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
|
||||
set_current_dir(mlc_dir)?;
|
||||
|
||||
let copy_opts = dir::CopyOptions {
|
||||
content_only: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
dir::copy(&generate_path, &self.repo, ©_opts)?;
|
||||
|
||||
fs::remove_dir_all(&generate_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub async fn podman_generate(&self) -> AppResult<()> {
|
||||
let uid = uid();
|
||||
|
||||
let podman = Podman::new(format!("unix:///run/user/{}/podman/podman.sock", uid))?;
|
||||
let image = self.kind.image().unwrap();
|
||||
|
||||
let generatefile = fs::read_to_string(".mlc/Generatefile")?;
|
||||
let generatefile_template = liquid::ParserBuilder::with_stdlib()
|
||||
.build()?
|
||||
.parse(&generatefile)?;
|
||||
|
||||
let generatefile_values = liquid::object!({
|
||||
"image": image,
|
||||
"name": &self.name,
|
||||
"repo": self.repo.file_name().unwrap().to_str().unwrap(),
|
||||
});
|
||||
|
||||
let generatefile = generatefile_template.render(&generatefile_values)?;
|
||||
|
||||
let generate_path = std::env::temp_dir().join(&self.tempdir);
|
||||
fs::create_dir_all(&generate_path)?;
|
||||
fs::write(generate_path.join("Generatefile"), generatefile)?;
|
||||
|
||||
dir::copy(&self.out, &generate_path, &dir::CopyOptions::new())?;
|
||||
|
||||
let opts = &ImageBuildOpts::builder(generate_path.to_string_lossy())
|
||||
.dockerfile("Generatefile")
|
||||
.tag(&self.tempdir)
|
||||
.build();
|
||||
|
||||
let images = podman.images();
|
||||
match images.build(opts) {
|
||||
Ok(mut generate_stream) => {
|
||||
while let Some(chunk) = generate_stream.next().await {
|
||||
match chunk {
|
||||
Ok(chunk) => println!("{:?}", chunk),
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
|
||||
podman
|
||||
.containers()
|
||||
.create(
|
||||
&ContainerCreateOpts::builder()
|
||||
.image(&self.tempdir)
|
||||
.name(&self.tempdir)
|
||||
.build(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes = podman
|
||||
.containers()
|
||||
.get(&self.tempdir)
|
||||
.copy_from(format!(
|
||||
"/{}",
|
||||
self.repo.file_name().unwrap().to_str().unwrap()
|
||||
))
|
||||
.try_concat()
|
||||
.await?;
|
||||
|
||||
let mut archive = tar::Archive::new(&bytes[..]);
|
||||
archive.unpack(self.repo.parent().unwrap())?;
|
||||
|
||||
podman.images().get(&self.tempdir).remove().await?;
|
||||
|
||||
fs::remove_dir_all(&generate_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::builders::{GitCheckoutBuilder, GitCloneBuilder, GitPullBuilder};
|
||||
use crate::errors::AppResult;
|
||||
use crate::utils::ShellCommand;
|
||||
|
||||
pub struct GitRepository {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl GitRepository {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn clone_repo(path: PathBuf, url: &str, branch: Option<String>) -> AppResult<()> {
|
||||
let mut builder = GitCloneBuilder::new(url, path);
|
||||
|
||||
if let Some(branch) = branch {
|
||||
builder = builder.branch(branch);
|
||||
}
|
||||
|
||||
builder.clone()?.silent()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pull(&self) -> AppResult<()> {
|
||||
let builder = GitPullBuilder::new(self.path.clone());
|
||||
|
||||
builder.pull()?.silent()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_origin(&self, url: &str) -> AppResult<()> {
|
||||
ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.args(["remote", "set-url", "origin"])
|
||||
.arg(url)
|
||||
.silent()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> AppResult<String> {
|
||||
let output = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> AppResult<String> {
|
||||
let output = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.args(["remote", "get-url", "origin"])
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn current_branch(&self) -> AppResult<String> {
|
||||
let output = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.output()?;
|
||||
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn default_branch(&self) -> AppResult<String> {
|
||||
let output = ShellCommand::git()
|
||||
.cwd(self.path.clone())
|
||||
.args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?.trim().to_string();
|
||||
|
||||
let (_, branch) = stdout.split_once('/').unwrap();
|
||||
|
||||
Ok(branch.to_string())
|
||||
}
|
||||
|
||||
pub fn checkout(&self, branch: &str) -> AppResult<()> {
|
||||
GitCheckoutBuilder::new(self.path.clone(), branch)
|
||||
.checkout()?
|
||||
.silent()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DesktopLanguageRequester,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n"]
|
||||
struct Localizations;
|
||||
|
||||
fn read() -> FluentLanguageLoader {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
let req_langs = DesktopLanguageRequester::requested_languages();
|
||||
i18n_embed::select(&loader, &Localizations, &req_langs).unwrap();
|
||||
loader
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LANG_LOADER: FluentLanguageLoader = read();
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::i18n::LANG_LOADER, $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::i18n::LANG_LOADER, $message_id, $($args), *)
|
||||
}};
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::builders::{GitFetchBuilder, GitStatusBuilder};
|
||||
use tabled::Tabled;
|
||||
|
||||
use crate::errors::AppResult;
|
||||
use crate::git::GitRepository;
|
||||
|
||||
pub const BOLD: &str = "\x1b[1m";
|
||||
pub const CLEAN: &str = "\x1b[34m";
|
||||
pub const DIRTY: &str = "\x1b[33m";
|
||||
pub const UNKNOWN: &str = "\x1b[37m";
|
||||
pub const RESET: &str = "\x1b[0m";
|
||||
|
||||
#[derive(Debug, Clone, Tabled)]
|
||||
pub enum GitStatus {
|
||||
Clean,
|
||||
Dirty,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
fn color(&self) -> String {
|
||||
match self {
|
||||
Self::Clean => format!("{}{}", BOLD, CLEAN),
|
||||
Self::Dirty => format!("{}{}", BOLD, DIRTY),
|
||||
Self::Unknown => format!("{}{}", BOLD, UNKNOWN),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for GitStatus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Clean => write!(f, "✔"),
|
||||
Self::Dirty => write!(f, "✘"),
|
||||
Self::Unknown => write!(f, "?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RichInfo {
|
||||
output: String,
|
||||
pub push: GitStatus,
|
||||
pub pull: GitStatus,
|
||||
pub dirty: GitStatus,
|
||||
}
|
||||
|
||||
impl Display for RichInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}↑ {}↓ {}*{}",
|
||||
self.push.color(),
|
||||
self.pull.color(),
|
||||
self.dirty.color(),
|
||||
RESET
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Tabled)]
|
||||
pub struct GitInfo {
|
||||
#[tabled(skip)]
|
||||
pub path: PathBuf,
|
||||
#[tabled(rename = "Repository")]
|
||||
pub name: String,
|
||||
#[tabled(rename = "Rich Info")]
|
||||
pub rich: RichInfo,
|
||||
#[tabled(rename = "Commit")]
|
||||
pub commit: String,
|
||||
}
|
||||
|
||||
impl GitInfo {
|
||||
pub fn new(path: PathBuf) -> AppResult<Self> {
|
||||
let output = GitStatusBuilder::new(path.clone()).status()?.output()?;
|
||||
let output = String::from_utf8(output.stdout)?;
|
||||
|
||||
GitFetchBuilder::new(path.clone())
|
||||
.fetch()?
|
||||
.arg("--all")
|
||||
.silent()?;
|
||||
|
||||
let mut repo = Self {
|
||||
path: path.clone(),
|
||||
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
rich: RichInfo {
|
||||
output,
|
||||
push: GitStatus::Unknown,
|
||||
pull: GitStatus::Unknown,
|
||||
dirty: GitStatus::Unknown,
|
||||
},
|
||||
commit: GitRepository::new(path).hash()?,
|
||||
};
|
||||
|
||||
repo.push()?;
|
||||
repo.pull()?;
|
||||
repo.dirty()?;
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
fn push(&mut self) -> AppResult<()> {
|
||||
self.rich.push = if self.rich.output.contains("ahead") {
|
||||
GitStatus::Dirty
|
||||
} else {
|
||||
GitStatus::Clean
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pull(&mut self) -> AppResult<()> {
|
||||
self.rich.pull = if self.rich.output.contains("behind") {
|
||||
GitStatus::Dirty
|
||||
} else {
|
||||
GitStatus::Clean
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dirty(&mut self) -> AppResult<()> {
|
||||
self.rich.dirty = if self.rich.output.contains("Untracked files")
|
||||
|| self.rich.output.contains("Changes not staged for commit")
|
||||
|| self.rich.output.contains("Changes to be committed")
|
||||
{
|
||||
GitStatus::Dirty
|
||||
} else {
|
||||
GitStatus::Clean
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
pub enum AppExitCode {
|
||||
#[cfg(target_os = "linux")]
|
||||
RunAsRoot = 1,
|
||||
PkgsNotFound = 2,
|
||||
DirNotEmpty = 3,
|
||||
ConfigParseError = 4,
|
||||
RepoParseError = 5,
|
||||
RepoNotClean = 6,
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
pub use exit_codes::*;
|
||||
pub use read::*;
|
||||
|
||||
mod exit_codes;
|
||||
mod read;
|
||||
pub mod strings;
|
||||
pub mod structs;
|
@ -1,119 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::internal::structs::{Config, Repo, SplitRepo, UnexpandedConfig};
|
||||
use crate::internal::AppExitCode;
|
||||
use crate::{crash, log};
|
||||
|
||||
pub fn parse_cfg(verbose: bool) -> Config {
|
||||
// Crash if mlc.toml doesn't exist
|
||||
if !Path::exists("mlc.toml".as_ref()) {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Config file not found (mlc.toml)"
|
||||
);
|
||||
}
|
||||
|
||||
// Reading the config file to an UnexpandedConfig struct
|
||||
let file = fs::read_to_string("mlc.toml").unwrap();
|
||||
let config: UnexpandedConfig = toml::from_str(&file).unwrap_or_else(|e| {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Error parsing config file: {}",
|
||||
e
|
||||
);
|
||||
// This is unreachable, but rustc complains about it otherwise
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
log!(verbose, "Config file read: {:?}", config);
|
||||
|
||||
// Crash if incorrect mode is set
|
||||
if config.base.mode != "workspace" && config.base.mode != "repository" {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Invalid mode in mlc.toml, must be either \"repository\" or \"workspace\""
|
||||
);
|
||||
}
|
||||
|
||||
let mut expanded_repos: Vec<Repo> = vec![];
|
||||
|
||||
// Parsing repos from the config file
|
||||
for x in config.repositories.repos {
|
||||
log!(verbose, "Parsing repo: {:?}", x);
|
||||
// Splits the repo name and index into a SplitRepo struct
|
||||
let split: Vec<&str> = x.split(':').collect();
|
||||
let split_struct = if split.len() > 2 {
|
||||
SplitRepo {
|
||||
id: split[0].parse().unwrap(),
|
||||
name: split[1].parse().unwrap(),
|
||||
extra: Some(split[2].parse().unwrap()),
|
||||
}
|
||||
} else {
|
||||
SplitRepo {
|
||||
id: split[0].parse().unwrap(),
|
||||
name: split[1].parse().unwrap(),
|
||||
extra: None,
|
||||
}
|
||||
};
|
||||
log!(verbose, "Split repo: {:?}", split_struct);
|
||||
|
||||
// Parses all necessary values for expanding the repo to a Repo struct
|
||||
let id = split_struct.id;
|
||||
|
||||
// If a branch is defined, parse it
|
||||
let branch = if split_struct.name.contains('/') {
|
||||
log!(verbose, "Branch defined: {}", split_struct.name);
|
||||
Some(
|
||||
split_struct.name.split('/').collect::<Vec<&str>>()[1]
|
||||
.to_string()
|
||||
.replace('!', ""),
|
||||
)
|
||||
} else {
|
||||
log!(verbose, "No branch defined");
|
||||
None
|
||||
};
|
||||
|
||||
// Strip branch and priority info from the name, if present
|
||||
let name = if split_struct.name.contains('/') {
|
||||
split_struct.name.split('/').collect::<Vec<&str>>()[0].to_string()
|
||||
} else {
|
||||
split_struct.name.to_string().replace('!', "")
|
||||
};
|
||||
|
||||
// Substitutes the name into the url
|
||||
let urls = &config.repositories.urls;
|
||||
let mut urls_vec = vec![];
|
||||
for (i, url) in urls {
|
||||
if i == &id {
|
||||
log!(verbose, "Substituting url: {:?}", url);
|
||||
urls_vec.push(url);
|
||||
}
|
||||
}
|
||||
let url = urls_vec[0].replace("{}", &name);
|
||||
|
||||
// Counts instances of ! in the name, and totals a priority accordingly
|
||||
let priority = &split_struct.name.matches('!').count();
|
||||
|
||||
// Creates and pushes Repo struct to expanded_repos
|
||||
let repo = Repo {
|
||||
name,
|
||||
url,
|
||||
branch,
|
||||
extra: split_struct.extra,
|
||||
priority: *priority,
|
||||
};
|
||||
log!(verbose, "Expanded repo: {:?}", repo);
|
||||
expanded_repos.push(repo);
|
||||
}
|
||||
|
||||
// Returns parsed config file
|
||||
let conf = Config {
|
||||
base: config.base,
|
||||
mode: config.mode,
|
||||
repositories: expanded_repos,
|
||||
};
|
||||
log!(verbose, "Config: {:?}", conf);
|
||||
|
||||
conf
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
use colored::Colorize;
|
||||
use std::process::exit;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use crate::internal::AppExitCode;
|
||||
|
||||
const LOGO_SYMBOL: &str = "μ";
|
||||
const ERR_SYMBOL: &str = "❌";
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)+) => {
|
||||
$crate::internal::strings::info_fn(&format!($($arg)+));
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($verbose:expr, $($arg:tt)+) => {
|
||||
$crate::internal::strings::log_fn(&format!("[{}:{}] {}", file!(), line!(), format!($($arg)+)), $verbose);
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! crash {
|
||||
($exit_code:expr, $($arg:tt)+) => {
|
||||
$crate::internal::strings::crash_fn(&format!("[{}:{}] {}", file!(), line!(), format!($($arg)+)), $exit_code)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! prompt {
|
||||
(default $default:expr, $($arg:tt)+) => {
|
||||
$crate::internal::strings::prompt_fn(&format!($($arg)+), $default)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn info_fn(msg: &str) {
|
||||
println!("{} {}", LOGO_SYMBOL.green(), msg.bold());
|
||||
}
|
||||
|
||||
pub fn log_fn(msg: &str, verbose: bool) {
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crash_fn(msg: &str, exit_code: AppExitCode) {
|
||||
println!("{} {}", ERR_SYMBOL.red(), msg.bold());
|
||||
exit(exit_code as i32);
|
||||
}
|
||||
|
||||
pub fn prompt_fn(msg: &str, default: bool) -> bool {
|
||||
let yn = if default { "[Y/n]" } else { "[y/N]" };
|
||||
print!("{} {} {}", "?".bold().green(), msg.bold(), yn);
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
|
||||
let input = input.trim().to_lowercase();
|
||||
|
||||
if input == "y" || input == "yes" {
|
||||
true
|
||||
} else if input == "n" || input == "no" {
|
||||
false
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
//// Config structs
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub base: ConfigBase,
|
||||
pub mode: ConfigMode,
|
||||
pub repositories: Vec<Repo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UnexpandedConfig {
|
||||
pub base: ConfigBase,
|
||||
pub mode: ConfigMode,
|
||||
pub repositories: ConfigRepositories,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigBase {
|
||||
pub mode: String,
|
||||
pub smart_pull: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigMode {
|
||||
pub repository: Option<ConfigModeRepository>,
|
||||
pub workspace: Option<ConfigModeWorkspace>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigModeRepository {
|
||||
pub name: String,
|
||||
pub build_on_update: bool,
|
||||
pub signing: ConfigModeRepositorySigning,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigModeRepositorySigning {
|
||||
pub enabled: bool,
|
||||
pub key: Option<String>,
|
||||
pub on_gen: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigModeWorkspace {
|
||||
pub git_info: bool,
|
||||
pub colorblind: bool,
|
||||
/* pub backup: bool,
|
||||
pub backup_dir: Option<String>, TODO: Implement backup
|
||||
*/
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigRepositories {
|
||||
pub repos: Vec<String>,
|
||||
pub urls: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigRepositoriesExpanded {
|
||||
pub repos: Vec<Repo>,
|
||||
}
|
||||
|
||||
//// Repository structs
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Repo {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub branch: Option<String>,
|
||||
pub extra: Option<String>,
|
||||
pub priority: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SplitRepo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub extra: Option<String>,
|
||||
}
|
||||
|
||||
//// Build operation structs
|
||||
#[derive(Debug)]
|
||||
pub struct ErroredPackage {
|
||||
pub name: String,
|
||||
pub code: i32,
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::errors::{AppError, ConfigError};
|
||||
use crate::git::GitRepository;
|
||||
|
||||
pub const LOCKFILE_VERSION: &str = "1.0";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Lockfile {
|
||||
pub lockfile: Meta,
|
||||
pub remote: HashMap<String, Remote>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Meta {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Remote {
|
||||
pub origin: String,
|
||||
pub commit: String,
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
impl Lockfile {
|
||||
pub fn new(config: &Config) -> Result<Self, ConfigError> {
|
||||
let path = config.base.src.join(".mlc").join("mlc.lock");
|
||||
|
||||
let lockfile = match std::fs::read_to_string(path) {
|
||||
Ok(lockfile) => toml::from_str(&lockfile)?,
|
||||
Err(_) => Self {
|
||||
lockfile: Meta {
|
||||
version: LOCKFILE_VERSION.to_string(),
|
||||
},
|
||||
remote: HashMap::new(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(lockfile)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, config: &Config) -> Result<&mut Self, AppError> {
|
||||
for repo in &config.repositories.names {
|
||||
let git = GitRepository::new(config.base.src.join(&repo.name));
|
||||
let config_url = repo.expand(config)?;
|
||||
let remote_url = git.origin()?;
|
||||
|
||||
if config_url != remote_url {
|
||||
git.update_origin(&config_url)?;
|
||||
git.pull()?;
|
||||
}
|
||||
|
||||
if let Some(branch) = &repo.rev {
|
||||
if branch != &git.current_branch()? {
|
||||
git.checkout(branch)?;
|
||||
}
|
||||
}
|
||||
|
||||
if repo.rev.is_none() && git.current_branch()? != git.default_branch()? {
|
||||
git.checkout(git.default_branch()?.as_str())?;
|
||||
}
|
||||
|
||||
let remote = Remote {
|
||||
origin: config_url,
|
||||
commit: git.hash()?,
|
||||
branch: git.current_branch()?,
|
||||
};
|
||||
|
||||
self.remote.insert(repo.name.clone(), remote);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), AppError> {
|
||||
let lockfile = toml::to_string_pretty(&self)
|
||||
.map_err(|e| AppError::Config(ConfigError::Serialize(e)))?;
|
||||
std::fs::write(".mlc/mlc.lock", lockfile)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,88 +1,237 @@
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||
#![allow(clippy::too_many_lines)]
|
||||
|
||||
use clap::Parser;
|
||||
use futures_util::future;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use crate::args::{Args, Operation};
|
||||
use crate::internal::parse_cfg;
|
||||
use crate::internal::AppExitCode;
|
||||
use args::Args;
|
||||
use args::GlobalArgs;
|
||||
use args::Operation;
|
||||
use errors::AppError;
|
||||
use errors::AppResult;
|
||||
use git::GitRepository;
|
||||
use utils::uid;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
use crate::build::{Build, BuildKind};
|
||||
use crate::config::Config;
|
||||
use crate::generate::GenerateKind;
|
||||
use crate::info::GitInfo;
|
||||
use crate::info::{BOLD, CLEAN, DIRTY, RESET, UNKNOWN};
|
||||
use crate::lock::Lockfile;
|
||||
use crate::prune::PackageFiles;
|
||||
|
||||
mod args;
|
||||
mod internal;
|
||||
mod operations;
|
||||
mod repository;
|
||||
mod build;
|
||||
mod builders;
|
||||
mod config;
|
||||
mod errors;
|
||||
mod generate;
|
||||
mod git;
|
||||
mod i18n;
|
||||
mod info;
|
||||
mod lock;
|
||||
mod prune;
|
||||
mod utils;
|
||||
|
||||
lazy_static!(
|
||||
#[derive(Clone, Debug)]
|
||||
pub static ref GLOBAL_ARGS: GlobalArgs = GlobalArgs::new();
|
||||
);
|
||||
|
||||
fn repository(verbose: bool) -> bool {
|
||||
// Parse config
|
||||
let config = parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
#[tokio::main]
|
||||
pub async fn main() -> AppResult<()> {
|
||||
color_eyre::install().unwrap();
|
||||
if uid() == 0 {
|
||||
return Err(AppError::RunAsRoot);
|
||||
}
|
||||
|
||||
// Get repository mode status
|
||||
let repository = config.base.mode == "repository";
|
||||
log!(verbose, "Repository Mode: {:?}", repository);
|
||||
let args: Args = Args::parse();
|
||||
|
||||
// Return repository mode status
|
||||
repository
|
||||
match args.subcommand {
|
||||
Operation::Build {
|
||||
packages,
|
||||
concurrent,
|
||||
} => cmd_build(packages, concurrent).await?,
|
||||
Operation::Generate => cmd_generate().await?,
|
||||
Operation::Init => cmd_init()?,
|
||||
Operation::Pull {
|
||||
rebuild,
|
||||
concurrent,
|
||||
} => cmd_pull(rebuild, concurrent).await?,
|
||||
Operation::Clean { prune } => cmd_clean(prune).await?,
|
||||
Operation::Info => cmd_info()?,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
if unsafe { libc::geteuid() } == 0 {
|
||||
crash!(AppExitCode::RunAsRoot, "Running malachite as root is disallowed as it can lead to system breakage. Instead, malachite will prompt you when it needs superuser permissions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get required variables
|
||||
let args: Args = Args::parse();
|
||||
let exclude = &args.exclude;
|
||||
let verbose = args.verbose;
|
||||
log!(verbose, "Args: {:?}", args);
|
||||
log!(verbose, "Exclude: {:?}", exclude);
|
||||
log!(verbose, "Verbose: You guess. :)");
|
||||
|
||||
// Arg matching
|
||||
match args.subcommand.unwrap_or(Operation::Clone) {
|
||||
Operation::Clone => operations::clone(verbose),
|
||||
Operation::Build {
|
||||
packages, no_regen, ..
|
||||
} => {
|
||||
if !repository(verbose) {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Cannot build packages in workspace mode"
|
||||
);
|
||||
fn cmd_init() -> AppResult<()> {
|
||||
Config::init()?;
|
||||
Ok(())
|
||||
}
|
||||
operations::build(&packages, exclude.clone(), no_regen, verbose);
|
||||
|
||||
async fn cmd_pull(rebuild: bool, concurrent: Option<u8>) -> AppResult<()> {
|
||||
let config = Config::load()?;
|
||||
|
||||
for repository in &config.repositories.names {
|
||||
let expanded = repository.expand(&config)?;
|
||||
let path = config.base.src.join(&repository.name);
|
||||
|
||||
if path.exists() {
|
||||
GitRepository::new(path).pull()?;
|
||||
} else {
|
||||
GitRepository::clone_repo(path, &expanded, repository.rev.clone())?;
|
||||
}
|
||||
Operation::Pull {
|
||||
packages,
|
||||
no_regen,
|
||||
interactive,
|
||||
..
|
||||
} => operations::pull(packages, exclude, verbose, no_regen, interactive),
|
||||
Operation::RepoGen => {
|
||||
if !repository(verbose) {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Cannot generate repository in workspace mode"
|
||||
);
|
||||
}
|
||||
repository::generate(verbose);
|
||||
|
||||
let mut lock = Lockfile::new(&config)?;
|
||||
let old_lock = lock.clone();
|
||||
lock.update(&config)?;
|
||||
|
||||
if rebuild {
|
||||
let mut packages_to_rebuild = vec![];
|
||||
|
||||
for (name, remote) in &old_lock.remote {
|
||||
if let Some(new_remote) = lock.remote.get(name) {
|
||||
if remote.commit != new_remote.commit {
|
||||
packages_to_rebuild.push(name.clone());
|
||||
}
|
||||
}
|
||||
Operation::Config => operations::config(verbose),
|
||||
Operation::Prune => {
|
||||
if !repository(verbose) {
|
||||
crash!(
|
||||
AppExitCode::ConfigParseError,
|
||||
"Cannot prune packages in workspace mode"
|
||||
);
|
||||
}
|
||||
operations::prune(verbose);
|
||||
|
||||
if !packages_to_rebuild.is_empty() {
|
||||
cmd_build(packages_to_rebuild, concurrent).await?;
|
||||
}
|
||||
}
|
||||
|
||||
lock.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_clean(prune: Option<u8>) -> AppResult<()> {
|
||||
let config = Config::load()?;
|
||||
|
||||
let excluded = [
|
||||
&config.repo.repo,
|
||||
&config.repo.out,
|
||||
&config.base.src.join(".mlc"),
|
||||
&config.base.src.join(".git"),
|
||||
];
|
||||
|
||||
let mut to_delete = Vec::new();
|
||||
for entry in std::fs::read_dir(&config.base.src)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let name = path.file_name().map(|s| s.to_string_lossy()).unwrap();
|
||||
if !config.repositories.names.iter().any(|x| x.name == name)
|
||||
&& !excluded.contains(&&path)
|
||||
{
|
||||
to_delete.push(path);
|
||||
}
|
||||
}
|
||||
Operation::Clean { force, .. } => operations::clean(verbose, force),
|
||||
Operation::Info => operations::info(verbose),
|
||||
}
|
||||
|
||||
for path in to_delete {
|
||||
std::fs::remove_dir_all(path)?;
|
||||
}
|
||||
|
||||
if let Some(prune) = prune {
|
||||
let mut packages = PackageFiles::new();
|
||||
packages.scan(&config.repo.out)?;
|
||||
let pruned = packages.prune(prune)?;
|
||||
if pruned > 0 {
|
||||
cmd_generate().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_build(packages: Vec<String>, concurrent: Option<u8>) -> AppResult<()> {
|
||||
let config = Config::load()?;
|
||||
|
||||
let kind = match config.base.podman {
|
||||
true => BuildKind::Podman {
|
||||
image: config.base.image,
|
||||
},
|
||||
false => BuildKind::Host,
|
||||
};
|
||||
|
||||
let concurrent = if let Some(concurrent) = concurrent {
|
||||
concurrent
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let out = config.repo.out;
|
||||
|
||||
let sem = Arc::new(Semaphore::new(concurrent as usize));
|
||||
let results = future::join_all(packages.into_iter().map(|name| async {
|
||||
let sem = sem.clone();
|
||||
let out = out.clone();
|
||||
let kind = kind.clone();
|
||||
|
||||
let perm = sem.acquire().await.unwrap();
|
||||
let build = Build::new(kind, name, out, vec![])?;
|
||||
build.build().await?;
|
||||
drop(perm);
|
||||
|
||||
Ok::<_, AppError>(())
|
||||
}))
|
||||
.await;
|
||||
|
||||
for result in results {
|
||||
result?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_generate() -> AppResult<()> {
|
||||
let config = Config::load()?;
|
||||
|
||||
let kind = match config.base.podman {
|
||||
true => GenerateKind::Podman {
|
||||
image: config.base.image,
|
||||
},
|
||||
false => GenerateKind::Host,
|
||||
};
|
||||
|
||||
generate::PacmanRepository::new(
|
||||
kind,
|
||||
config.repo.name,
|
||||
config.repo.out,
|
||||
config.repo.repo,
|
||||
config.repo.security,
|
||||
)
|
||||
.generate()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_info() -> AppResult<()> {
|
||||
let config = Config::load()?;
|
||||
let repositories = config.repositories.names;
|
||||
|
||||
let mut overview = Vec::new();
|
||||
|
||||
for repo in repositories {
|
||||
let path = config.base.src.join(&repo.name);
|
||||
|
||||
overview.push(GitInfo::new(path)?);
|
||||
}
|
||||
|
||||
let mut table = tabled::Table::new(overview);
|
||||
table.with(tabled::Style::rounded());
|
||||
|
||||
println!("{}", table);
|
||||
println!(
|
||||
" {}Key:{} {}Clean {}Unknown {}Dirty{}",
|
||||
BOLD, RESET, CLEAN, UNKNOWN, DIRTY, RESET
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,140 +0,0 @@
|
||||
use crate::internal::structs::{ErroredPackage, Repo};
|
||||
use crate::internal::AppExitCode;
|
||||
use crate::{crash, info, log, repository};
|
||||
|
||||
pub fn build(packages: &[String], exclude: Vec<String>, no_regen: bool, verbose: bool) {
|
||||
// Read config struct from mlc.toml
|
||||
let config = crate::internal::parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
// Check if any packages were passed, if not, imply all
|
||||
let all = packages.is_empty();
|
||||
log!(verbose, "All: {:?}", all);
|
||||
|
||||
// Read signing
|
||||
let signing = config.mode.repository.as_ref().unwrap().signing.enabled;
|
||||
|
||||
// Read on_gen
|
||||
let on_gen = config.mode.repository.as_ref().unwrap().signing.on_gen;
|
||||
|
||||
// Parse whether to sign on build or not
|
||||
let sign = if signing && on_gen.is_some() && on_gen.unwrap() {
|
||||
false
|
||||
} else {
|
||||
signing
|
||||
};
|
||||
log!(verbose, "Signing: {:?}", sign);
|
||||
|
||||
// Get list of repos and subtract exclude
|
||||
let mut repos: Vec<Repo> = config.repositories;
|
||||
log!(verbose, "{} Repos: {:?}", repos.len(), repos);
|
||||
if !exclude.is_empty() {
|
||||
log!(verbose, "Exclude not empty: {:?}", exclude);
|
||||
for ex in exclude {
|
||||
repos.retain(|x| *x.name != ex);
|
||||
}
|
||||
}
|
||||
|
||||
log!(
|
||||
verbose,
|
||||
"Exclusions parsed. Now {} Repos: {:?}",
|
||||
repos.len(),
|
||||
repos
|
||||
);
|
||||
|
||||
// If packages is not empty and all isn't specified, build specified packages
|
||||
let mut errored: Vec<ErroredPackage> = vec![];
|
||||
if !packages.is_empty() && !all {
|
||||
log!(verbose, "Packages not empty: {:?}", packages);
|
||||
for pkg in packages.iter() {
|
||||
// If repo is not in config, crash, otherwise, build
|
||||
if repos.iter().map(|x| x.name.clone()).any(|x| x == *pkg) {
|
||||
// Otherwise, build
|
||||
log!(verbose, "Building {}", pkg);
|
||||
|
||||
let code = repository::build(pkg, sign, verbose);
|
||||
log!(
|
||||
verbose,
|
||||
"Package {} finished with exit code: {:?}",
|
||||
pkg,
|
||||
code
|
||||
);
|
||||
|
||||
if code != 0 {
|
||||
let error = ErroredPackage {
|
||||
name: pkg.to_string(),
|
||||
code,
|
||||
};
|
||||
errored.push(error);
|
||||
}
|
||||
} else {
|
||||
crash!(
|
||||
AppExitCode::PkgsNotFound,
|
||||
"Package repo {} not found in in mlc.toml",
|
||||
pkg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all is specified, attempt to build a package from all repos
|
||||
if all {
|
||||
log!(verbose, "Proceeding to build all");
|
||||
|
||||
// Sort by package priority
|
||||
log!(verbose, "Sorting by priority: {:?}", repos);
|
||||
repos.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||
log!(verbose, "Sorted: {:?}", repos);
|
||||
for pkg in repos {
|
||||
log!(verbose, "Building {}", pkg.name);
|
||||
|
||||
let code = repository::build(&pkg.name, sign, verbose);
|
||||
log!(
|
||||
verbose,
|
||||
"Package {} finished with exit code: {:?}",
|
||||
pkg.name,
|
||||
code
|
||||
);
|
||||
|
||||
if code != 0 {
|
||||
let error = ErroredPackage {
|
||||
name: pkg.name,
|
||||
code,
|
||||
};
|
||||
errored.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all is not specified, but packages is empty, crash
|
||||
if !all && packages.is_empty() {
|
||||
log!(verbose, "Packages empty. Crashing");
|
||||
crash!(AppExitCode::PkgsNotFound, "No packages specified");
|
||||
}
|
||||
|
||||
// If no_regen is passed, do not generate a repository
|
||||
if !no_regen {
|
||||
log!(verbose, "Generating repository");
|
||||
repository::generate(verbose);
|
||||
}
|
||||
|
||||
// Map errored packages to a string for display
|
||||
let error_strings: Vec<String> = errored
|
||||
.iter()
|
||||
.map(|x| format!("{}: Returned {}", x.name, x.code))
|
||||
.collect();
|
||||
|
||||
// If errored is not empty, let the user know which packages failed
|
||||
if !errored.is_empty() {
|
||||
log!(verbose, "Errored packages: \n{:?}", error_strings);
|
||||
info!(
|
||||
"The following packages build jobs returned a non-zero exit code: \n {}",
|
||||
error_strings.join("\n ")
|
||||
);
|
||||
info!("Please check `man 8 makepkg` for more information");
|
||||
// Check if code 63 appeared at all
|
||||
if errored.iter().any(|x| x.code == 63) {
|
||||
log!(verbose, "Code 63 found");
|
||||
info!("Note: Code 63 is an internal Malachite exit code, and specifies that no PKGBUILD was found.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
use crate::{crash, info, internal::AppExitCode, log};
|
||||
|
||||
pub fn clean(verbose: bool, force: bool) {
|
||||
info!("Resetting mlc repo, deleting all directories");
|
||||
// Get a vec of all files/dirs in the current directory
|
||||
let dir_paths = std::fs::read_dir(".").unwrap();
|
||||
log!(verbose, "Paths: {:?}", dir_paths);
|
||||
let mut dirs = dir_paths
|
||||
.map(|x| x.unwrap().path().display().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Remove mlc.toml and .git from output
|
||||
dirs.retain(|x| *x != "./mlc.toml" && *x != ".\\mlc.toml");
|
||||
dirs.retain(|x| *x != "./.git" && *x != ".\\.git");
|
||||
dirs.retain(|x| *x != "./.gitignore" && *x != ".\\.gitignore");
|
||||
dirs.retain(|x| *x != "./.gitmodules" && *x != ".\\.gitmodules");
|
||||
dirs.retain(|x| *x != "./README.md" && *x != ".\\README.md");
|
||||
|
||||
let mut unclean_dirs = vec![];
|
||||
|
||||
// Enter each directory and check git status
|
||||
for dir in &dirs {
|
||||
let root_dir = std::env::current_dir().unwrap();
|
||||
|
||||
log!(verbose, "Entering directory: {}", dir);
|
||||
std::env::set_current_dir(dir).unwrap();
|
||||
|
||||
let status = std::process::Command::new("git")
|
||||
.arg("status")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let output = std::string::String::from_utf8(status.stdout).unwrap();
|
||||
log!(verbose, "Git status: {}", output);
|
||||
|
||||
if output.contains("Your branch is up to date with")
|
||||
&& !output.contains("Untracked files")
|
||||
&& !output.contains("Changes not staged for commit")
|
||||
{
|
||||
log!(verbose, "Directory {} is clean", dir);
|
||||
} else {
|
||||
unclean_dirs.push(dir);
|
||||
}
|
||||
|
||||
std::env::set_current_dir(&root_dir).unwrap();
|
||||
log!(verbose, "Current directory: {}", root_dir.display());
|
||||
}
|
||||
|
||||
if !unclean_dirs.is_empty() && !force && crate::parse_cfg(verbose).base.mode == "workspace" {
|
||||
crash!(
|
||||
AppExitCode::RepoNotClean,
|
||||
"The following directories are not clean: \n {}\n\
|
||||
If you are sure no important changes are staged, run `mlc clean` with the `--force` flag to delete them.",
|
||||
unclean_dirs.iter().map(|x| (*x).to_string().replace("./", "").replace(".\\", "")).collect::<Vec<String>>().join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs);
|
||||
for dir in &dirs {
|
||||
log!(verbose, "Deleting directory: {}", dir);
|
||||
rm_rf::remove(dir).unwrap();
|
||||
}
|
||||
info!(
|
||||
"Reset complete, dirs removed: \n \
|
||||
{}",
|
||||
dirs.iter()
|
||||
.map(|x| x.replace("./", "").replace(".\\", ""))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n ")
|
||||
);
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{info, log};
|
||||
|
||||
pub fn clone(verbose: bool) {
|
||||
// Read config struct from mlc.toml
|
||||
let config = crate::internal::parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
// Parse repositories from config
|
||||
let repos = &config.repositories;
|
||||
log!(verbose, "Repos: {:?}", repos);
|
||||
|
||||
// Get a vector of all files/dirs in the current directory, excluding config file
|
||||
let dir_paths = std::fs::read_dir(".").unwrap();
|
||||
let mut dirs = dir_paths
|
||||
.map(|x| x.unwrap().path().display().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Remove mlc.toml and .git from output
|
||||
dirs.retain(|x| *x != "./mlc.toml" && *x != ".\\mlc.toml");
|
||||
dirs.retain(|x| *x != "./.git" && *x != ".\\.git");
|
||||
|
||||
// If mode is repository, also exclude repository mode directories
|
||||
if config.mode.repository.is_some() {
|
||||
dirs.retain(|x| {
|
||||
*x != format!("./{}", config.mode.repository.as_ref().unwrap().name)
|
||||
&& *x != format!(".\\{}", config.mode.repository.as_ref().unwrap().name)
|
||||
});
|
||||
dirs.retain(|x| *x != "./out" && *x != ".\\out");
|
||||
}
|
||||
log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs);
|
||||
|
||||
// Creates a vector of the difference between cloned repos and repos defined in config
|
||||
let mut repo_diff = vec![];
|
||||
for repo in repos {
|
||||
let name = &repo.name;
|
||||
|
||||
if !dirs.contains(&format!("./{}", name)) && !dirs.contains(&format!(".\\{}", name)) {
|
||||
repo_diff.push(repo);
|
||||
}
|
||||
}
|
||||
|
||||
// Diff logic
|
||||
if repo_diff.is_empty() {
|
||||
// No diff, do nothing
|
||||
log!(verbose, "No diff");
|
||||
info!("All repos are already cloned");
|
||||
} else {
|
||||
log!(verbose, "Diff: {:?}", repo_diff);
|
||||
// This is just for pretty display purposes
|
||||
let display = repo_diff
|
||||
.iter()
|
||||
.map(|x| x.name.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ");
|
||||
info!("New/missing repos to clone: {}", display);
|
||||
|
||||
// Clone all diff repos
|
||||
for r in repo_diff {
|
||||
log!(verbose, "Depth: {:?}", r.extra);
|
||||
log!(verbose, "Cloning {}", r.name);
|
||||
|
||||
if r.extra.is_some() && config.base.mode == "workspace" {
|
||||
info!(
|
||||
"Cloning ({} mode): {} with `--depth {}`",
|
||||
config.base.mode,
|
||||
r.name,
|
||||
r.extra.as_ref().unwrap()
|
||||
);
|
||||
} else if r.extra.is_some() && config.base.mode == "repository" {
|
||||
info!(
|
||||
"Cloning ({} mode): {} at {}",
|
||||
config.base.mode,
|
||||
r.name,
|
||||
r.extra.as_ref().unwrap()
|
||||
);
|
||||
} else {
|
||||
info!("Cloning ({} mode): {}", config.base.mode, r.name);
|
||||
}
|
||||
|
||||
if r.extra.is_some() && config.base.mode == "workspace" {
|
||||
// Clone with specified extra depth
|
||||
Command::new("git")
|
||||
.args(&["clone", &r.url, &r.name])
|
||||
// If a branch is specified, clone that specific branch
|
||||
.args(if r.branch.is_some() {
|
||||
vec!["-b", r.branch.as_ref().unwrap()]
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
.args(if r.extra.is_some() {
|
||||
vec!["--depth", r.extra.as_ref().unwrap()]
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
} else if config.base.mode == "repository" {
|
||||
// Clone and checkout specified hash
|
||||
// Create an empty directory with repo.name and enter it
|
||||
let root_dir = env::current_dir().unwrap();
|
||||
|
||||
// Git clone the repo
|
||||
Command::new("git")
|
||||
.args(&["clone", &r.url, &r.name])
|
||||
.args(if r.branch.is_some() {
|
||||
vec!["-b", r.branch.as_ref().unwrap()]
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
|
||||
std::env::set_current_dir(&r.name).unwrap();
|
||||
log!(verbose, "Entered directory: {}", r.name);
|
||||
|
||||
// Git checkout the PKGBUILD from the hash
|
||||
if r.extra.is_some() {
|
||||
Command::new("git")
|
||||
.args(&["checkout", r.extra.as_ref().unwrap()])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Return to the root directory
|
||||
std::env::set_current_dir(root_dir).unwrap();
|
||||
log!(verbose, "Returned to root directory");
|
||||
} else {
|
||||
// Clone normally
|
||||
Command::new("git")
|
||||
.args(&["clone", &r.url, &r.name])
|
||||
.args(if r.branch.is_some() {
|
||||
vec!["-b", r.branch.as_ref().unwrap()]
|
||||
} else {
|
||||
vec![]
|
||||
})
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::{log, repository::create};
|
||||
|
||||
pub fn config(verbose: bool) {
|
||||
// Generate new config file if not already present
|
||||
if !Path::exists("mlc.toml".as_ref()) {
|
||||
log!(verbose, "Creating mlc.toml");
|
||||
create(verbose);
|
||||
}
|
||||
|
||||
// Open config file in user's editor of choice
|
||||
let editor = env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
|
||||
log!(verbose, "Opening mlc.toml in {}", editor);
|
||||
Command::new(editor)
|
||||
.arg("mlc.toml")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
use colored::Colorize;
|
||||
use spinoff::{Color, Spinner, Spinners};
|
||||
use std::env;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tabled::Tabled;
|
||||
|
||||
use crate::{crash, info, internal::AppExitCode, log};
|
||||
|
||||
// For displaying the table of contents
|
||||
#[derive(Clone, tabled::Tabled, Debug)]
|
||||
struct RepoDisplayGit {
|
||||
#[tabled(rename = "Name")]
|
||||
name: String,
|
||||
#[tabled(rename = "URL")]
|
||||
url: String,
|
||||
#[tabled(skip)]
|
||||
priority: usize,
|
||||
#[tabled(rename = "Git Info")]
|
||||
git_info: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, tabled::Tabled, Debug)]
|
||||
struct RepoDisplay {
|
||||
#[tabled(rename = "Name")]
|
||||
name: String,
|
||||
#[tabled(rename = "URL")]
|
||||
url: String,
|
||||
#[tabled(skip)]
|
||||
priority: usize,
|
||||
}
|
||||
|
||||
pub fn git_status(verbose: bool, repo: &str, colorblind: bool) -> String {
|
||||
let dir = env::current_dir().unwrap();
|
||||
log!(
|
||||
verbose,
|
||||
"Current directory: {}",
|
||||
env::current_dir().unwrap().display()
|
||||
);
|
||||
env::set_current_dir(&repo).unwrap_or_else(|e| {
|
||||
crash!(
|
||||
AppExitCode::RepoParseError,
|
||||
"Failed to enter directory {} for Git info: {}, Have you initialized the repo?",
|
||||
repo,
|
||||
e.to_string()
|
||||
);
|
||||
});
|
||||
log!(verbose, "Current directory: {}", repo);
|
||||
|
||||
let output = Command::new("git").arg("status").output().unwrap();
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
log!(verbose, "Git status: {}", output);
|
||||
|
||||
let unstaged = output.contains("Changes not staged for commit")
|
||||
|| output.contains("Changes to be committed");
|
||||
let untracked = output.contains("Untracked files");
|
||||
let dirty = unstaged || untracked;
|
||||
|
||||
let pull = output.contains("Your branch is behind");
|
||||
let push = output.contains("Your branch is ahead");
|
||||
|
||||
let latest_commit = Command::new("git")
|
||||
.args(&["log", "--pretty=%h", "-1"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let mut latest_commit = String::from_utf8(latest_commit.stdout).unwrap();
|
||||
latest_commit.retain(|c| !c.is_whitespace());
|
||||
|
||||
let output = if colorblind {
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
if dirty { "D".red() } else { "D".bright_blue() },
|
||||
if pull { "Pl".red() } else { "Pl".bright_blue() },
|
||||
if push { "Ps".red() } else { "Ps".bright_blue() },
|
||||
latest_commit
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
if dirty { "D".red() } else { "D".green() },
|
||||
if pull { "Pl".red() } else { "Pl".green() },
|
||||
if push { "Ps".red() } else { "Ps".green() },
|
||||
latest_commit
|
||||
)
|
||||
};
|
||||
env::set_current_dir(&dir).unwrap();
|
||||
log!(verbose, "Current directory: {}", dir.display());
|
||||
output
|
||||
}
|
||||
|
||||
pub fn info(verbose: bool) {
|
||||
log!(verbose, "Showing Info");
|
||||
// Parse config from mlc.toml
|
||||
let config = crate::internal::parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
|
||||
// Check for git_info
|
||||
let git_info = if config.mode.workspace.is_some() {
|
||||
config.mode.workspace.as_ref().unwrap().git_info
|
||||
} else {
|
||||
false
|
||||
};
|
||||
log!(verbose, "Git info: {}", git_info);
|
||||
|
||||
// Check for colorblind mode
|
||||
let colorblind = if config.mode.workspace.is_some() {
|
||||
config.mode.workspace.as_ref().unwrap().colorblind
|
||||
} else {
|
||||
false
|
||||
};
|
||||
log!(verbose, "Colorblind: {}", colorblind);
|
||||
|
||||
// Add the branch to the name if it's not the default branch for said repository
|
||||
let repos_unparsed = config.repositories;
|
||||
let mut repos = vec![];
|
||||
let mut repos_git = vec![];
|
||||
|
||||
if git_info {
|
||||
// Crash early if directories are not found for git_info
|
||||
for repo in &repos_unparsed {
|
||||
if !Path::new(&repo.name).exists() {
|
||||
crash!(
|
||||
AppExitCode::RepoParseError,
|
||||
"Failed to check directory {} for Git info, have you initialized the repo?",
|
||||
repo.name,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Start the spinner
|
||||
let sp = Spinner::new(
|
||||
Spinners::Line,
|
||||
format!("{}", "Parsing Git Info...".bold()),
|
||||
Color::Green,
|
||||
);
|
||||
|
||||
// Construct bash script to run git remote upgrade on all repos asynchronously
|
||||
// This helps speed up the operation when, for example, you have a lot of repositories and you store your SSH key as a subkey of your GPG key on a yubikey
|
||||
// This took my `mlc info` time down from 17s to 8s (i have the above described setup)
|
||||
let mut bash_script = String::new();
|
||||
bash_script.push_str(
|
||||
"\n\
|
||||
#!/usr/bin/env bash\n\
|
||||
\n\
|
||||
# This script will run `git remote update` in all repositories\n\
|
||||
pull() { cd $1; git remote update; cd -; }\n\
|
||||
\n",
|
||||
);
|
||||
for repo in &repos_unparsed {
|
||||
writeln!(bash_script, "pull {} &", repo.name).unwrap();
|
||||
}
|
||||
bash_script.push_str("wait\n");
|
||||
|
||||
log!(verbose, "Bash script: {}", bash_script);
|
||||
|
||||
// Run the bash script
|
||||
Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(bash_script)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// Stop the spinner with a success message
|
||||
let text = format!("{}", "Parsing Git Info... Done".bold());
|
||||
let symbol = format!("{}", "✔".bold().green());
|
||||
|
||||
sp.stop_and_persist(&symbol, &text);
|
||||
log!(verbose, "Repos: {:?}", repos);
|
||||
}
|
||||
|
||||
// Iterate over all repositories
|
||||
for repo in repos_unparsed {
|
||||
// Get name with branch, '/' serving as the delimiter
|
||||
let name = if repo.branch.is_some() {
|
||||
format!("{}/{}", repo.name, repo.branch.unwrap())
|
||||
} else {
|
||||
repo.name.clone()
|
||||
};
|
||||
|
||||
// Get git info, if applicable
|
||||
let git_info_string = if git_info {
|
||||
let info = Some(git_status(
|
||||
verbose,
|
||||
&repo.name,
|
||||
config.mode.workspace.as_ref().unwrap().colorblind,
|
||||
));
|
||||
info
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Push to the correct vector, we're using a separate vector for git info because
|
||||
// the struct we're displaying is different
|
||||
if git_info {
|
||||
repos_git.push(RepoDisplayGit {
|
||||
name,
|
||||
url: repo.url.clone(),
|
||||
priority: repo.priority,
|
||||
git_info: git_info_string.unwrap(),
|
||||
});
|
||||
} else {
|
||||
repos.push(RepoDisplay {
|
||||
name,
|
||||
url: repo.url.clone(),
|
||||
priority: repo.priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
repos.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||
repos_git.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||
if git_info {
|
||||
log!(verbose, "Repos Sorted: {:?}", repos_git);
|
||||
} else {
|
||||
log!(verbose, "Repos Sorted: {:?}", repos);
|
||||
}
|
||||
|
||||
// Displaying basic info about the Malachite Repository
|
||||
let internal_name = if config.mode.repository.is_none()
|
||||
|| config.mode.repository.as_ref().unwrap().name.is_empty()
|
||||
{
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
} else {
|
||||
config.mode.repository.unwrap().name
|
||||
};
|
||||
let name = format!(
|
||||
"{} \"{}\":",
|
||||
// Sidenote: It should NOT be this convoluted to capitalise the first character of a string in rust. What the fuck.
|
||||
String::from_utf8_lossy(&[config.base.mode.as_bytes()[0].to_ascii_uppercase()])
|
||||
+ &config.base.mode[1..],
|
||||
internal_name
|
||||
);
|
||||
|
||||
// Get terminal width for table formatting
|
||||
let width = match crossterm::terminal::size() {
|
||||
Ok((w, _)) => w,
|
||||
Err(_) => 80,
|
||||
};
|
||||
|
||||
// Create table for displaying info
|
||||
let table = if git_info {
|
||||
tabled::Table::new(&repos_git)
|
||||
.with(tabled::Style::modern())
|
||||
.with(tabled::Width::wrap(width as usize).keep_words())
|
||||
.to_string()
|
||||
} else {
|
||||
tabled::Table::new(&repos)
|
||||
.with(tabled::Style::modern())
|
||||
.with(tabled::Width::wrap(width as usize).keep_words())
|
||||
.to_string()
|
||||
};
|
||||
|
||||
// Get length of Vec for displaying in the table
|
||||
let len = if git_info {
|
||||
repos_git.len()
|
||||
} else {
|
||||
repos.len()
|
||||
};
|
||||
|
||||
// Print all of the info
|
||||
info!("{}", name);
|
||||
info!("Total Repositories: {}", len.to_string().green());
|
||||
println!("{}", table);
|
||||
if config.mode.workspace.is_some() && config.mode.workspace.as_ref().unwrap().git_info {
|
||||
info!(
|
||||
"D: Dirty - Unstaged Changes \n \
|
||||
Pl: Pull - Changes at Remote \n \
|
||||
Ps: Push - Unpushed Changes \n \
|
||||
{}: Dirty, {}: Clean",
|
||||
" ".on_red(),
|
||||
if config.mode.workspace.unwrap().colorblind {
|
||||
" ".on_bright_blue()
|
||||
} else {
|
||||
" ".on_green()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
pub use build::*;
|
||||
pub use clean::*;
|
||||
pub use clone::*;
|
||||
pub use config::*;
|
||||
pub use info::*;
|
||||
pub use prune::*;
|
||||
pub use pull::*;
|
||||
|
||||
mod build;
|
||||
mod clean;
|
||||
mod clone;
|
||||
mod config;
|
||||
mod info;
|
||||
mod prune;
|
||||
mod pull;
|
@ -1,139 +0,0 @@
|
||||
use colored::Colorize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::info;
|
||||
use crate::log;
|
||||
use crate::parse_cfg;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PackageFile {
|
||||
name: String,
|
||||
ver: String,
|
||||
ext: String,
|
||||
}
|
||||
|
||||
pub fn prune(verbose: bool) {
|
||||
// Read config struct from mlc.toml
|
||||
let config = parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
|
||||
// Read current directory
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
log!(verbose, "Current dir: {:?}", current_dir);
|
||||
|
||||
// Enter out directory
|
||||
env::set_current_dir("out").unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
|
||||
// Read all files from . into a Vec<PathBuf>, except for .sig files
|
||||
let mut files: Vec<PathBuf> = vec![];
|
||||
for entry in fs::read_dir(".").unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().unwrap() != "sig" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
log!(verbose, "Files: {:?}", files);
|
||||
|
||||
// Split files into Vec<PackageFile>, turning package-name-1.0.0-1-x86_64.tar.gz into PackageFile { name: "package-name", ver: "1.0.0-1", ext: "x86_64.tar.gz" }
|
||||
let mut packages: Vec<PackageFile> = vec![];
|
||||
for file in files {
|
||||
// Regex, splits package-name-1.0.0-1-x86_64.tar.gz into 3 groups: package-name, -1.0.0-1, -x86_64.tar.gz
|
||||
let re = regex::Regex::new(r"^(.+)(-.+-.+)(-.+\..+\..+\.+..+)$").unwrap();
|
||||
|
||||
// Get file name to string
|
||||
let file = file.to_str().unwrap();
|
||||
|
||||
// Match file name against regex
|
||||
for cap in re.captures_iter(file) {
|
||||
// Collect regex captures
|
||||
let name = cap[1].to_string();
|
||||
let mut ver = cap[2].to_string();
|
||||
let mut ext = cap[3].to_string();
|
||||
|
||||
// Strip leading - from ver and ext
|
||||
ver.remove(0).to_string();
|
||||
ext.remove(0).to_string();
|
||||
|
||||
let package = PackageFile { name, ver, ext };
|
||||
log!(verbose, "Package: {:?}", package);
|
||||
packages.push(package);
|
||||
}
|
||||
}
|
||||
|
||||
// Split packages into a Vector of Vectors by unique name
|
||||
let mut packages_by_name: Vec<Vec<&PackageFile>> = vec![];
|
||||
for package in &packages {
|
||||
log!(verbose, "Sorting Package: {:?}", package);
|
||||
let name = &package.name;
|
||||
let mut found = false;
|
||||
// Check if name is already present in packages_by_name
|
||||
for p in &mut packages_by_name {
|
||||
if &p[0].name == name {
|
||||
log!(verbose, "Found {}", name);
|
||||
found = true;
|
||||
p.push(package);
|
||||
}
|
||||
}
|
||||
// If not, create a new vector and push to it
|
||||
if !found {
|
||||
log!(verbose, "Creating {}", name);
|
||||
packages_by_name.push(vec![package]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each Vector of Vectors by version
|
||||
for p in &mut packages_by_name {
|
||||
log!(verbose, "Sorting {:?}", p);
|
||||
p.sort_by(|a, b| b.ver.cmp(&a.ver));
|
||||
}
|
||||
|
||||
// Pushes all but the 3 most recent versions of each package into a new Vector of PackageFiles
|
||||
let mut packages_to_delete: Vec<PackageFile> = vec![];
|
||||
for p in &packages_by_name {
|
||||
let mut to_delete = vec![];
|
||||
for (i, _) in p.iter().enumerate() {
|
||||
if i >= 3 {
|
||||
log!(verbose, "Deleting {:?}", p[i]);
|
||||
to_delete.push(p[i].clone());
|
||||
}
|
||||
}
|
||||
packages_to_delete.extend(to_delete);
|
||||
}
|
||||
log!(verbose, "Packages to delete: {:?}", packages_to_delete);
|
||||
|
||||
// Delete all packages in packages_to_delete
|
||||
for p in &packages_to_delete {
|
||||
let path = format!("{}-{}-{}", p.name, p.ver, p.ext);
|
||||
log!(verbose, "Deleting {}", path);
|
||||
std::process::Command::new("bash")
|
||||
.args(&["-c", &format!("rm -rf ./{} ./{}.sig", path, path)])
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Return to current directory
|
||||
env::set_current_dir(current_dir).unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
|
||||
// Print which packages were deleted
|
||||
if packages_to_delete.is_empty() {
|
||||
info!("No packages were deleted.");
|
||||
} else {
|
||||
info!("Deleted the following packages:");
|
||||
for p in &mut packages_to_delete {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" {}-{}",
|
||||
p.name.replace("./", "").replace(".\\", ""),
|
||||
p.ver
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::info;
|
||||
use crate::{crash, internal::AppExitCode, log, prompt};
|
||||
|
||||
struct PullParams {
|
||||
smart_pull: bool,
|
||||
build_on_update: bool,
|
||||
no_regen: bool,
|
||||
}
|
||||
|
||||
fn do_the_pulling(repos: Vec<String>, verbose: bool, params: &PullParams, interactive: bool) {
|
||||
for repo in repos {
|
||||
// Set root dir to return after each git pull
|
||||
let root_dir = env::current_dir().unwrap();
|
||||
log!(verbose, "Root dir: {:?}", root_dir);
|
||||
|
||||
// Enter repo dir
|
||||
info!("Entering working directory: {}", &repo);
|
||||
env::set_current_dir(&repo).unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
|
||||
let mut packages_to_rebuild: Vec<String> = vec![];
|
||||
|
||||
// Pull logic
|
||||
log!(verbose, "Pulling");
|
||||
if params.smart_pull {
|
||||
// Update the remote
|
||||
log!(verbose, "Smart pull");
|
||||
Command::new("git")
|
||||
.args(&["remote", "update"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
|
||||
// Check the repository status
|
||||
let output = Command::new("git").arg("status").output().unwrap();
|
||||
|
||||
// If there are changes, pull normally
|
||||
if String::from_utf8(output.stdout)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("Your branch is behind")
|
||||
{
|
||||
info!("Branch out of date, pulling changes");
|
||||
Command::new("git")
|
||||
.arg("pull")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
|
||||
// If build_on_update is set, rebuild package
|
||||
if params.build_on_update {
|
||||
if interactive {
|
||||
let cont = prompt!(default true, "Rebuild package {}?", &repo);
|
||||
if cont {
|
||||
info!("Package {} updated, staging for rebuild", &repo);
|
||||
log!(verbose, "Pushing package {} to be rebuilt", &repo);
|
||||
packages_to_rebuild.push(repo);
|
||||
} else {
|
||||
info!("Not rebuilding package {}", &repo);
|
||||
}
|
||||
} else {
|
||||
packages_to_rebuild.push(repo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there are no changes, alert the user
|
||||
info!("No changes to pull");
|
||||
}
|
||||
} else {
|
||||
// Pull normally
|
||||
log!(verbose, "Normal pull");
|
||||
Command::new("git")
|
||||
.arg("pull")
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
// Return to root dir
|
||||
env::set_current_dir(&root_dir).unwrap();
|
||||
log!(
|
||||
verbose,
|
||||
"Returned to root dir: {:?}",
|
||||
env::current_dir().unwrap()
|
||||
);
|
||||
|
||||
// Rebuild packages if necessary
|
||||
if !packages_to_rebuild.is_empty() && params.build_on_update {
|
||||
info!("Rebuilding packages: {}", &packages_to_rebuild.join(", "));
|
||||
log!(verbose, "Rebuilding packages: {:?}", &packages_to_rebuild);
|
||||
|
||||
// Push to build
|
||||
crate::operations::build(&packages_to_rebuild, vec![], params.no_regen, verbose);
|
||||
|
||||
// Ensure you are in root dir
|
||||
env::set_current_dir(root_dir).unwrap();
|
||||
log!(
|
||||
verbose,
|
||||
"Returned to root dir: {:?}",
|
||||
env::current_dir().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pull(
|
||||
packages: Vec<String>,
|
||||
exclude: &[String],
|
||||
verbose: bool,
|
||||
no_regen: bool,
|
||||
interactive: bool,
|
||||
) {
|
||||
// Read config file
|
||||
let config = crate::parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
|
||||
// If no packages are specified, imply all
|
||||
let all = packages.is_empty();
|
||||
log!(verbose, "All: {}", all);
|
||||
|
||||
// Read smart_pull from config
|
||||
let smart_pull = config.base.smart_pull;
|
||||
log!(verbose, "Smart pull: {}", smart_pull);
|
||||
|
||||
// Read build_on_update from config
|
||||
let build_on_update = if config.mode.repository.is_some() {
|
||||
config.mode.repository.unwrap().build_on_update
|
||||
} else {
|
||||
false
|
||||
};
|
||||
log!(verbose, "Build on update: {}", build_on_update);
|
||||
|
||||
// Read repos from config
|
||||
let repos = config
|
||||
.repositories
|
||||
.iter()
|
||||
.map(|x| x.name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
log!(verbose, "Repos: {:?}", repos);
|
||||
|
||||
// Set repos_applicable for next function
|
||||
let mut repos_applicable = if all { repos } else { packages };
|
||||
log!(verbose, "Repos applicable: {:?}", repos_applicable);
|
||||
|
||||
// Subtract exclude from repos_applicable
|
||||
if !exclude.is_empty() {
|
||||
for ex in exclude.iter() {
|
||||
repos_applicable.retain(|x| *x != *ex);
|
||||
}
|
||||
}
|
||||
log!(verbose, "Exclude: {:?}", exclude);
|
||||
log!(verbose, "Repos applicable excluded: {:?}", repos_applicable);
|
||||
|
||||
// If all is not specified and packages is empty, crash
|
||||
if repos_applicable.is_empty() {
|
||||
crash!(AppExitCode::PkgsNotFound, "No packages specified");
|
||||
}
|
||||
|
||||
// Sort repos_applicable by priority
|
||||
repos_applicable.sort_by(|a, b| {
|
||||
config
|
||||
.repositories
|
||||
.iter()
|
||||
.find(|x| x.name == *a)
|
||||
.unwrap()
|
||||
.priority
|
||||
.cmp(
|
||||
&config
|
||||
.repositories
|
||||
.iter()
|
||||
.find(|x| x.name == *b)
|
||||
.unwrap()
|
||||
.priority,
|
||||
)
|
||||
});
|
||||
|
||||
log!(verbose, "Pulling {:?}", repos_applicable);
|
||||
|
||||
// If any repos are not in the config, run a clone
|
||||
for repo in &repos_applicable {
|
||||
if !std::path::Path::new(repo).exists() {
|
||||
info!(
|
||||
"Repo {} does not exist, ensuring all repos are cloned",
|
||||
repo
|
||||
);
|
||||
crate::operations::clone(verbose);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pull!
|
||||
do_the_pulling(
|
||||
repos_applicable,
|
||||
verbose,
|
||||
&PullParams {
|
||||
smart_pull,
|
||||
build_on_update,
|
||||
no_regen,
|
||||
},
|
||||
interactive,
|
||||
);
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::AppResult;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageFile {
|
||||
pub path: String,
|
||||
pub package: Package,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PackageFiles {
|
||||
files: HashMap<String, Vec<PackageFile>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Package {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn new(name: String, version: String) -> Self {
|
||||
Self { name, version }
|
||||
}
|
||||
|
||||
pub fn read(path: &Path) -> AppResult<PackageFile> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
compress_tools::uncompress_archive_file(&mut file, &mut buffer, ".PKGINFO")?;
|
||||
let pkginfo = String::from_utf8(buffer)?;
|
||||
|
||||
let mut pkgname = String::new();
|
||||
let mut pkgver = String::new();
|
||||
|
||||
for line in pkginfo.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("pkgname") {
|
||||
pkgname = line.split('=').nth(1).unwrap().trim().to_string();
|
||||
} else if line.starts_with("pkgver") {
|
||||
pkgver = line.split('=').nth(1).unwrap().trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PackageFile {
|
||||
path: path.display().to_string(),
|
||||
package: Package::new(pkgname, pkgver),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageFiles {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
files: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, file: PackageFile) {
|
||||
self.files
|
||||
.entry(file.clone().package.name)
|
||||
.or_default()
|
||||
.push(file);
|
||||
}
|
||||
|
||||
pub fn scan(&mut self, path: &Path) -> AppResult<()> {
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let path_display = path.display().to_string();
|
||||
if path.is_file()
|
||||
&& path_display.contains(".pkg.tar.")
|
||||
&& !path_display.ends_with(".sig")
|
||||
{
|
||||
let file = Package::read(&path)?;
|
||||
self.add(file);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prune(&mut self, keep: u8) -> AppResult<usize> {
|
||||
let mut to_delete = Vec::new();
|
||||
for (_, files) in self.files.iter_mut() {
|
||||
files.sort_by(|a, b| b.package.version.cmp(&a.package.version));
|
||||
to_delete.extend(files.iter().skip(keep as usize));
|
||||
}
|
||||
|
||||
for file in &to_delete {
|
||||
fs::remove_file(&file.path)?;
|
||||
}
|
||||
|
||||
Ok(to_delete.len())
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::internal::AppExitCode;
|
||||
use crate::{crash, log};
|
||||
|
||||
const DEFAULT_CONFIG: &str = r#"
|
||||
[base]
|
||||
# Either "repository" or "workspace"
|
||||
mode = ""
|
||||
# Better left as true, but can be set to false if it causes issues with branches
|
||||
smart_pull = true
|
||||
|
||||
[mode.repository]
|
||||
# Decides what to call the repository and relevant files
|
||||
name = ""
|
||||
# Decides whether to build packages if package repo is updated on pull
|
||||
build_on_update = false
|
||||
|
||||
[mode.repository.signing]
|
||||
# Decides whether or not to sign packages
|
||||
enabled = true
|
||||
|
||||
[mode.workspace]
|
||||
# Whether to show rich git info for repositories
|
||||
git_info = true
|
||||
# Colorblind mode toggle
|
||||
colorblind = false
|
||||
|
||||
[repositories]
|
||||
# List of repositories formatted as id:name (priority is decided by the ! suffix, and decides package build order)
|
||||
repos = [
|
||||
"aur:hello!",
|
||||
"crs:malachite"
|
||||
]
|
||||
|
||||
[repositories.urls]
|
||||
# URL keys for repositories, with {} where the repository name would go
|
||||
crs = "https://github.com/crystal-linux/{}"
|
||||
aur = "https://aur.archlinux.org/{}"
|
||||
"#;
|
||||
|
||||
pub fn create(verbose: bool) {
|
||||
// Ensure current directory is empty
|
||||
if env::current_dir()
|
||||
.unwrap()
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
crash!(
|
||||
AppExitCode::DirNotEmpty,
|
||||
"Directory is not empty, please only create a repository in an empty directory"
|
||||
);
|
||||
}
|
||||
log!(verbose, "Creating config file");
|
||||
|
||||
// If config file exists, create it
|
||||
if !Path::exists("mlc.toml".as_ref()) {
|
||||
let mut file = File::create("mlc.toml").unwrap();
|
||||
file.write_all(DEFAULT_CONFIG.as_ref()).unwrap();
|
||||
}
|
||||
log!(verbose, "Config file created");
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
pub use config::*;
|
||||
pub use package::*;
|
||||
pub use repo::*;
|
||||
|
||||
mod config;
|
||||
mod package;
|
||||
mod repo;
|
@ -1,87 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
use crate::internal::AppExitCode;
|
||||
use crate::{crash, log};
|
||||
|
||||
pub fn build(pkg: &str, sign: bool, verbose: bool) -> i32 {
|
||||
log!(verbose, "Building {}", pkg);
|
||||
log!(verbose, "Signing: {}", sign);
|
||||
|
||||
// Set root dir to return after build
|
||||
let dir = env::current_dir().unwrap();
|
||||
log!(verbose, "Root dir: {:?}", dir);
|
||||
|
||||
// Create out dir if not already present
|
||||
if !Path::exists("out".as_ref()) {
|
||||
log!(verbose, "Creating out dir");
|
||||
fs::create_dir_all("out").unwrap();
|
||||
}
|
||||
|
||||
// If package directory is not found, crash
|
||||
if !Path::exists(pkg.as_ref()) {
|
||||
crash!(
|
||||
AppExitCode::PkgsNotFound,
|
||||
"Repo for package {} not found, aborting",
|
||||
pkg
|
||||
);
|
||||
}
|
||||
|
||||
// Enter build directory
|
||||
env::set_current_dir(pkg).unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
|
||||
// If PKGBUILD is not found, return 63 and break
|
||||
if !Path::exists("PKGBUILD".as_ref()) {
|
||||
env::set_current_dir(&dir).unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
return 63;
|
||||
}
|
||||
|
||||
// Parse extra flags from envvar
|
||||
let extra_flags = env::var("MAKEPKG_FLAGS").unwrap_or_else(|_| "".to_string());
|
||||
let extra_flags = extra_flags.split(' ').collect::<Vec<&str>>();
|
||||
|
||||
// Default set of flags
|
||||
let default_args = vec![
|
||||
"-sf",
|
||||
"--skippgpcheck",
|
||||
if sign { "--sign" } else { "--nosign" },
|
||||
"--noconfirm",
|
||||
];
|
||||
|
||||
// Build each package
|
||||
let a = Command::new("makepkg")
|
||||
.args(
|
||||
default_args
|
||||
.iter()
|
||||
.chain(extra_flags.iter())
|
||||
.map(std::string::ToString::to_string),
|
||||
)
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
log!(verbose, "{} Build job returned: {:?}", pkg, a);
|
||||
|
||||
// Copy built package to out dir
|
||||
Command::new("bash")
|
||||
.args(&["-c", "cp *.pkg.tar* ../out/"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
log!(verbose, "Copied built package to out dir");
|
||||
|
||||
// Return to root dir
|
||||
env::set_current_dir(dir).unwrap();
|
||||
log!(
|
||||
verbose,
|
||||
"Returned to root dir: {:?}",
|
||||
env::current_dir().unwrap()
|
||||
);
|
||||
|
||||
// Return exit code
|
||||
a.code().unwrap()
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
use crate::{crash, info, internal::parse_cfg, internal::AppExitCode, log};
|
||||
|
||||
pub fn generate(verbose: bool) {
|
||||
// Read config struct from mlc.toml
|
||||
let config = parse_cfg(verbose);
|
||||
log!(verbose, "Config: {:?}", config);
|
||||
|
||||
// Get signing from config
|
||||
let signing = &config.mode.repository.as_ref().unwrap().signing.enabled;
|
||||
log!(verbose, "Signing: {:?}", signing);
|
||||
|
||||
// Get repository name from config
|
||||
let name = &config.mode.repository.as_ref().unwrap().name;
|
||||
log!(verbose, "Name: {}", name);
|
||||
|
||||
// Read on_gen from config
|
||||
let on_gen = &config.mode.repository.as_ref().unwrap().signing.on_gen;
|
||||
log!(verbose, "On gen: {:?}", on_gen);
|
||||
|
||||
// Read key from config
|
||||
let key = &config.mode.repository.as_ref().unwrap().signing.key;
|
||||
log!(verbose, "Key: {:?}", key);
|
||||
|
||||
info!("Generating repository: {}", name);
|
||||
|
||||
// If repository exists, empty it
|
||||
if Path::exists(name.as_ref()) {
|
||||
log!(verbose, "Deleting contents of {}", name);
|
||||
Command::new("bash")
|
||||
.args(&["-c", &format!("rm -rf {}/*", &name)])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap(); // Dirty temp hack but oh well
|
||||
} else {
|
||||
// Create dir if it doesn't exist
|
||||
fs::create_dir_all(&name).unwrap();
|
||||
log!(verbose, "Created {}", name);
|
||||
}
|
||||
|
||||
// Copy out packages to repository directory
|
||||
Command::new("bash")
|
||||
.args(&["-c", &format!("cp -v out/* {}/", &name)])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
log!(verbose, "Copied out packages to {}", name);
|
||||
|
||||
// Enter repository directory
|
||||
env::set_current_dir(&name).unwrap();
|
||||
log!(verbose, "Current dir: {:?}", env::current_dir().unwrap());
|
||||
|
||||
// Sign all package files in repository if signing and on_gen are true
|
||||
if *signing && on_gen.is_some() && on_gen.unwrap() {
|
||||
// Get a list of all .tar.* files in repository
|
||||
let files = fs::read_dir(".").unwrap();
|
||||
|
||||
for file in files {
|
||||
// Get file name
|
||||
let file = file.unwrap();
|
||||
let path = file.path();
|
||||
|
||||
let sign_command = if key.is_some() && !key.as_ref().unwrap().is_empty() {
|
||||
format!(
|
||||
"gpg --default-key {} --detach-sign {}",
|
||||
key.as_ref().unwrap(),
|
||||
path.to_str().unwrap()
|
||||
)
|
||||
} else {
|
||||
format!("gpg --detach-sign {}", path.to_str().unwrap())
|
||||
};
|
||||
|
||||
// If extension is either .zst or .xz, sign it
|
||||
if path.extension().unwrap() == "zst" || path.extension().unwrap() == "xz" {
|
||||
log!(verbose, "Signing {}", path.display());
|
||||
Command::new("bash")
|
||||
.arg("-c")
|
||||
.args(&[&sign_command])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
log!(verbose, "Signed repository");
|
||||
}
|
||||
|
||||
let db = format!("{}.db", &name);
|
||||
let files = format!("{}.files", &name);
|
||||
|
||||
// Check if package files end with .tar.zst or .tar.xz
|
||||
let zst = Command::new("bash")
|
||||
.args(&["-c", "ls *.tar.zst"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
let xz = Command::new("bash")
|
||||
.args(&["-c", "ls *.tar.xz"])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
|
||||
// This should never happen, crash and burn if it does
|
||||
if zst.success() && xz.success() {
|
||||
crash!(
|
||||
AppExitCode::RepoParseError,
|
||||
"Both .tar.zst and .tar.xz files found in repository. You've done something wrong. Aborting"
|
||||
);
|
||||
}
|
||||
|
||||
// Ensuring aarch64/ALARM support for the future
|
||||
let aarch64_mode = if zst.success() {
|
||||
false
|
||||
} else if xz.success() {
|
||||
true
|
||||
} else {
|
||||
crash!(
|
||||
AppExitCode::PkgsNotFound,
|
||||
"No .zst or .xz packages found in repository"
|
||||
);
|
||||
// This should theoretically never be reached, but let's just give the compiler what it wants
|
||||
false
|
||||
};
|
||||
let suffix = if aarch64_mode { "xz" } else { "zst" };
|
||||
|
||||
// Create repo.db and repo.files using repo-add
|
||||
Command::new("bash")
|
||||
.args(&[
|
||||
"-c",
|
||||
&format!(
|
||||
"GLOBIGNORE=\"*.sig\" repo-add {}.tar.gz *.pkg.tar.{}",
|
||||
db, suffix
|
||||
),
|
||||
])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
log!(verbose, "Created {} and {}", db, files);
|
||||
|
||||
// Replace repo.{db,files}.tar.gz with just repo.{db,files}
|
||||
Command::new("bash")
|
||||
.args(&[
|
||||
"-c",
|
||||
&format!("mv {}.tar.gz {}; mv {}.tar.gz {}", db, db, files, files),
|
||||
])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
log!(
|
||||
verbose,
|
||||
"Renamed {}.tar.gz to {} and {}.tar.gz to {}",
|
||||
db,
|
||||
db,
|
||||
files,
|
||||
files
|
||||
);
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, ExitStatus};
|
||||
|
||||
use crate::errors::{AppError, AppResult};
|
||||
|
||||
pub fn uid() -> i32 {
|
||||
(unsafe { libc::geteuid() } as i32)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShellCommand {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ShellCommand {
|
||||
pub fn repo_add() -> Self {
|
||||
Self::new("repo-add")
|
||||
}
|
||||
|
||||
pub fn git() -> Self {
|
||||
Self::new("git")
|
||||
}
|
||||
|
||||
pub fn makepkg() -> Self {
|
||||
Self::new("makepkg")
|
||||
}
|
||||
|
||||
pub fn new<S: ToString>(command: S) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
args: vec![],
|
||||
cwd: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg<S: ToString>(mut self, arg: S) -> Self {
|
||||
self.args.push(arg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn args<S, V>(mut self, args: V) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
V: IntoIterator<Item = S>,
|
||||
{
|
||||
self.args.extend(args.into_iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cwd<P: Into<PathBuf>>(mut self, cwd: P) -> Self {
|
||||
self.cwd = Some(cwd.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn wait(&self) -> AppResult<ExitStatus> {
|
||||
let mut child = self.spawn()?;
|
||||
|
||||
child.wait().map_err(AppError::from)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn wait_with_output(&self) -> AppResult<std::process::Output> {
|
||||
let child = self.spawn()?;
|
||||
|
||||
child.wait_with_output().map_err(AppError::from)
|
||||
}
|
||||
|
||||
pub fn silent(&self) -> AppResult<()> {
|
||||
let _ = self.output()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn output(&self) -> AppResult<std::process::Output> {
|
||||
let mut command = Command::new(&self.command);
|
||||
command.args(&self.args);
|
||||
|
||||
if let Some(cwd) = &self.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
let output = command.output()?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn spawn(&self) -> AppResult<Child> {
|
||||
let mut command = Command::new(&self.command);
|
||||
command.args(&self.args);
|
||||
|
||||
if let Some(cwd) = &self.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
let child = command.spawn()?;
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue