镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 05:42:44 +08:00
ParlzPackageManger (PMM): cross-platform package manager + mirror
- C11, static MinGW build, single exe (pmm/pdm) - apt-style multi-mirror install with sha256/sha1 verify - .pdm package format (pdm pack/install/remove), deb-like - github/gitlab/gitea/forgejo release install with --git/--github etc. - python/pip-style version selection (==,>=,<=,>,< , comma) - auto platform selection (windows/linux variants) - Windows Installed Apps registration + PATH auto-add - download progress bar (tqdm-style, ASCII-safe) - mirror/ : zero-dep Node.js registry server (variants, multi-version) - Licensed under GNU GPL v3
这个提交包含在:
+20
@@ -0,0 +1,20 @@
|
||||
# build artifacts
|
||||
*.exe
|
||||
*.o
|
||||
/dist/
|
||||
|
||||
# packaged binaries / large upstream files (keep them out of git)
|
||||
*.pdm
|
||||
*.msi
|
||||
*.tar.xz
|
||||
*.tar.gz
|
||||
*.zip
|
||||
|
||||
# mirror runtime data (the browser-station packages are regenerated at deploy)
|
||||
mirror/server.log
|
||||
mirror/packages/
|
||||
mirror/files/
|
||||
|
||||
# editors
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,674 @@
|
||||
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>.
|
||||
@@ -0,0 +1,29 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O2 -Wall -static -Wextra -std=c11
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
CORE = src/main.c src/json.c src/ini.c src/pmm.c src/http.c src/repo.c \
|
||||
src/install.c src/sha256.c src/sha1.c src/mirrors.c src/pdm.c
|
||||
|
||||
all: pmm pdm
|
||||
|
||||
pmm: $(CORE) $(wildcard src/*.h)
|
||||
$(CC) $(CFLAGS) -o $@ $(CORE)
|
||||
|
||||
PDM_DEPS = src/pdm_main.c src/pdm.c src/pmm.c src/sha256.c src/install.c \
|
||||
src/http.c src/json.c src/ini.c src/mirrors.c src/sha1.c
|
||||
|
||||
pdm: $(PDM_DEPS) $(wildcard src/*.h)
|
||||
$(CC) $(CFLAGS) -o $@ $(PDM_DEPS)
|
||||
|
||||
install: all
|
||||
install -m 755 pmm $(PREFIX)/bin/pmm
|
||||
install -m 755 pdm $(PREFIX)/bin/pdm
|
||||
|
||||
uninstall:
|
||||
rm -f $(PREFIX)/bin/pmm $(PREFIX)/bin/pdm
|
||||
|
||||
clean:
|
||||
rm -f pmm pdm pmm.exe pdm.exe
|
||||
|
||||
.PHONY: all install uninstall clean
|
||||
@@ -0,0 +1,247 @@
|
||||
# ParlzPackageManger (PMM)
|
||||
|
||||
用 C 语言编写的跨平台包管理器(Windows / Linux / macOS),零第三方库依赖
|
||||
(下载通过系统自带的 `curl` 完成,Windows 10+、macOS、主流 Linux 均预装)。
|
||||
|
||||
## 构建
|
||||
|
||||
```sh
|
||||
make # Linux / macOS(或任何有 make 的环境),生成 pmm + pdm 两个二进制
|
||||
build.bat # Windows(只需 MinGW gcc),同样生成 pmm.exe + pdm.exe
|
||||
make install # 安装到 /usr/local/bin(可选)
|
||||
```
|
||||
|
||||
产物是单文件 `pmm` / `pdm`(或 `.exe`),无 DLL/第三方库依赖。
|
||||
|
||||
> 重要:Windows 构建须用**静态链接**(`-static`)。否则产物会依赖 MinGW 的
|
||||
> `libmcfgthread-2.dll`,在没装 MinGW 的普通 PowerShell 里会因为找不到该 DLL 而
|
||||
> **静默不输出**(加载即退出)。`Makefile`/`build.bat` 已默认加 `-static`。
|
||||
|
||||
## 使用
|
||||
|
||||
```sh
|
||||
# 从镜像站(apt 式多镜像)安装
|
||||
pmm install <pkg> # 按优先级从 registry 镜像查找并安装
|
||||
pmm install <file.pdm> # 安装本地 .pdm 包
|
||||
pmm pack <dir> [out.pdm] # 把文件夹打包成 .pdm(模拟 dpkg deb)
|
||||
pmm pdm install|list|remove|info ... # 管理 .pdm 包
|
||||
|
||||
# 从 git 托管仓库的最新 release 安装
|
||||
pmm install --git https://host/owner/repo.git # 任意 git 仓库(自动探测 API 形状)
|
||||
pmm install --github owner/repo # GitHub
|
||||
pmm install --gitlab owner/repo # GitLab
|
||||
pmm install --gitea https://git.example.com/owner/repo # Gitea/Forgejo(含私人部署)
|
||||
pmm install --git owner/repo --host forgejo # 强制指定主机类型
|
||||
|
||||
pmm mirror list / add <名> <源> / use <名> / remove <名>
|
||||
pmm list
|
||||
pmm help / version
|
||||
pmm install <pkg> -pd # 安装到 D:\.pmm(可换成 -pc 回到 C 盘,-px 任意盘)
|
||||
```
|
||||
|
||||
## 换安装盘(-p<drive>,避免塞爆 C 盘)
|
||||
|
||||
**默认安装在 `D:\.pmm`**(config + cache + bin + root 全在这里,避免占满 C 盘)。
|
||||
用 `-p<盘符>` 可以改变 PMM 的**整个家目录**:
|
||||
|
||||
- 默认(不带 `-p`)→ `D:\.pmm\`(node 等 .exe 装到 `D:\.pmm\bin`,`.pdm` 装到 `D:\.pmm\root`)
|
||||
- `pmm install xxx -pc` → 回到用户目录 `C:\Users\<你>\.pmm`
|
||||
- `pmm install xxx -pe` → 任意其它盘(`E:\.pmm\`);也支持 `-p D`(空格分隔)写法
|
||||
|
||||
**记忆规则**:第一次用某个 `-p<盘>` 会被记住(写入 `~/.pmm/install-drive`),
|
||||
之后的命令不写也会自动用那个盘;想换盘用新的 `-p<盘>` 即可。
|
||||
|
||||
> 注意:换盘会把配置/镜像源也挪到对应盘的 `.pmm`(如 `D:\.pmm\mirror.ini`)。
|
||||
> 切换后如要装包,先在该盘目录放好镜像配置,或重新 `pmm mirror add ...`。
|
||||
|
||||
`--git` 对任意 git 仓库地址自动探测 API 形状(Gitea/Forgejo → GitLab → GitHub),
|
||||
GitHub.com、GitLab.com、Gitea、Forgejo 及一切私有部署实例都支持。
|
||||
|
||||
## 下载校验(SHA-256 / SHA-1)
|
||||
|
||||
每次下载完成后自动计算并打印 SHA-256 与 SHA-1;若 release 资源旁有
|
||||
`<资源名>.sha256` 或 `<资源名>.sha1` 伴随文件,会自动核对,不一致时拒绝安装
|
||||
并删除缓存文件。registry 条目里若带 `sha256` 字段,也会作为独立校验来源。
|
||||
SHA 实现为纯 C 内置(FIPS 180-4),无外部依赖。
|
||||
|
||||
## .pdm 包格式(模拟 dpkg 的 deb)
|
||||
|
||||
`pdm pack ./目录` 会把目录打包成一个 `.pdm`(一个 tar 归档,含
|
||||
`control.tar.gz` / `data.tar.gz` / `sha256sums` 三个成员,与 deb 的
|
||||
`control.tar`+`data.tar` 思路一致):
|
||||
|
||||
- 目录里需有一个 `pdm-control` 文件,格式为 `KEY: value` 行:
|
||||
`Package` / `Version` / `Architecture` / `Description` / `Maintainer`
|
||||
- 其余文件全部作为文件有效载荷,安装时解包到安装根目录(`~/.pmm/root`)
|
||||
- `pdm install <pkg.pdm>` / `pmm install <pkg.pdm>` 安装,`pdm remove` 卸载
|
||||
- 安装记录写入 `~/.pmm/installed/<包名>.info`,含文件清单,用于卸载
|
||||
|
||||
## apt 式镜像(mirror.ini / mirror.conf)
|
||||
|
||||
镜像配置文件位于 `~/.pmm/`(Windows 为 `%USERPROFILE%\.pmm\`),查找顺序
|
||||
`mirror.ini` → `mirror.conf`。每个 `[名]` 是一个镜像,字段如下:
|
||||
|
||||
```ini
|
||||
# ~/.pmm/mirror.ini
|
||||
[官方]
|
||||
registry = https://mirror.example.com/packages # registry 索引:{registry}/{包}.json
|
||||
download = https://ghfast.example-mirror.dev # 下载前缀(gh-proxy 风格,接到原始 URL 前)
|
||||
api = https://api.mirror.example # git 主机 API 基地址(整体替换)
|
||||
priority = 10 # 越小越优先(apt 式)
|
||||
default = true
|
||||
|
||||
[自建]
|
||||
registry = https://git.example.local/packages
|
||||
priority = 20
|
||||
```
|
||||
|
||||
- `pmm install <包>` 会**按 priority 从小到大依次尝试**每个镜像的
|
||||
`{registry}/{包}.json`,第一个成功的镜像命中,否则回退到下一个(apt 的逻辑)。
|
||||
- 下载文件时也会把镜像的 `download` 前缀按优先级排在原始 URL 之前逐个尝试。
|
||||
- 每家的 `priority` 相同则按出现顺序;`pmm mirror use <名>` 可强制优先用某个镜像。
|
||||
|
||||
## 镜像站(mirror/ 网页版)
|
||||
|
||||
`mirror/` 目录是一个零依赖的 Node.js 镜像站,提供网页浏览、搜索、按平台筛选、
|
||||
上传包并自动生成 registry 索引。
|
||||
|
||||
**默认站点:`https://pmm.parlz.com/mirror`**(上传生成的下载链接和 `mirror.ini`
|
||||
都指向它)。本地开发/测试可用 `PMM_BASE_URL` 覆盖:
|
||||
|
||||
```sh
|
||||
cd mirror && node server.js # 生产默认 https://pmm.parlz.com/mirror
|
||||
PMM_BASE_URL=http://127.0.0.1:8080 node server.js # 本地调试
|
||||
PMM_PORT=9000 node server.js # 仅改监听端口
|
||||
```
|
||||
|
||||
- 网页 `/`:浏览/搜索包、按 Windows/Linux/macOS 筛选、复制安装命令
|
||||
- `POST /upload`:上传 `.pdm`/`.deb`/`.exe`/`.dmg`/`.tar.gz` 等,自动算 sha256/sha1
|
||||
- 上传带 `x-pkg-name`、`x-pkg-version`、`x-pkg-os`、`x-pkg-desc` 头(或表单包名)
|
||||
会写 registry。**包文件存到 `packages/<包>/<版本>.pdm`,元数据 `packages/<包>/<版本>.json`,
|
||||
`<包>.json` 是 latest 指针(含 `versions` 列表)**;不再有独立的 `files/` 目录。
|
||||
- `GET /packages.json`:registry 聚合索引(只列 latest,供 `pmm install` 无版本时用)
|
||||
- `GET /mirror.ini`:可直接复制到 `~/.pmm/mirror.ini` 的镜像配置
|
||||
- 目录约定:`packages/<包>.json`(latest)+ `packages/<包>/<版本>.pdm|.json`
|
||||
|
||||
把 `mirror.ini` 指向这个镜像后,`pmm install <包>` 装 latest,也支持
|
||||
**Python/pip 风格的版本限定**:
|
||||
|
||||
```sh
|
||||
pmm install nodejs # latest
|
||||
pmm install nodejs==24.20.0 # 精确版本
|
||||
pmm install nodejs>=24,<25 # 版本区间(选出范围内最高)
|
||||
pmm install nodejs<=24.20.0 # <= / >= / < / > / != , 逗号组合
|
||||
pmm install "nodejs>=24,<25" # 带空格/组合时加引号
|
||||
```
|
||||
|
||||
## Release 资源与平台对应
|
||||
|
||||
| 系统 | 优先匹配的扩展名 |
|
||||
|---|---|
|
||||
| windows | `.exe` `.msi` `.zip` `.7z` |
|
||||
| linux | `.deb` `.rpm` `.appimage` `.tar.gz` `.tgz` `.tar.xz` `.tar.bz2` `.pkg.tar.zst` |
|
||||
| macos | `.dmg` `.pkg` `.app.zip` `.tar.gz` `.tgz` |
|
||||
|
||||
校验文件 / 源码包(`.sha256`、`checksum*`、`-src` 等)会被自动跳过。
|
||||
|
||||
## 主机支持
|
||||
|
||||
- **GitHub**:`https://api.github.com/repos/{repo}/releases/latest`
|
||||
- **GitLab**:`https://gitlab.com/projects/{repo}/releases/permalink/latest`
|
||||
- **Gitea / Forgejo**(含私人部署):`{origin}/api/v1/repos/{repo}/releases/latest`
|
||||
|
||||
镜像源会整体替换 API 基地址,加速代理和私有实例都走同一条路。
|
||||
|
||||
## 安装行为(下载后自动安装)
|
||||
|
||||
下载完成并通过校验后,pmm 会自动执行对应平台的安装动作,无需再手动操作:
|
||||
|
||||
- `.deb` → `sudo dpkg -i`;`.rpm` → `sudo rpm -Uvh`(Linux)
|
||||
- `.msi` → `msiexec /i /qn`(Windows,静默安装)
|
||||
- `.exe` → 自动运行安装器(Windows,带常见静默旗标 `/S /VERYSILENT /quiet --silent`)
|
||||
- `.dmg` → `hdiutil attach` + `sudo cp` 复制 `.app` 到 `/Applications`;`.pkg` → `sudo installer`(macOS)
|
||||
- `.tar.*` / `.zip` / `.7z` → 解压到安装目录(`~/.pmm/bin`)
|
||||
- `.pdm` → 通过内置 .pdm 管理器解包到 `~/.pmm/root`;`.appimage` → 加执行权限后放入安装目录
|
||||
|
||||
> Windows 下 pmm 通过系统 `cmd.exe` 执行命令,因此 `.exe` 用 cmd 兼容的单条静默命令。
|
||||
|
||||
## 已安装软件登记到注册表(Installed Apps)
|
||||
|
||||
安装 `.pdm` 包后,pmm 会模仿 **MSI/NSIS 安装器** 的做法,把软件写入 **每用户
|
||||
注册表卸载项** `HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\<包名>`,
|
||||
于是它会出现在 Windows「应用 / Installed Apps」里,带:
|
||||
|
||||
- `DisplayName` / `DisplayVersion` / `Publisher`(取自控制信息)
|
||||
- `DisplayIcon`(包内找到的第一个 `.exe`)
|
||||
- `UninstallString` → `"<pmm.exe>" remove <包名>`,让「卸载」按钮真正调用我们
|
||||
- `InstallLocation`、`EstimatedSize`、`InstallDate`、`NoModify`、`NoRepair`
|
||||
|
||||
`pmm remove <包>`(或控制面板"卸载"按钮)会删除文件并清理该注册表项。
|
||||
(每用户 HKCU 写入,无需管理员权限,重启终端/刷新设置可见。)
|
||||
|
||||
## 自动加入 PATH
|
||||
|
||||
每次成功安装后,pmm/pdm 会把 PMM 目录 **自动加入用户 PATH**(幂等,不重复):
|
||||
|
||||
- 始终加入 `~/.pmm/bin` 与 `~/.pmm/root`;
|
||||
- 还会扫描 `~/.pmm/root/` 下含有可执行文件的子目录并加入,例如安装 Node.js 后
|
||||
会自动加入 `~/.pmm/root/nodejs`;对 `pkg/cmd` 布局(如 Git/MinGit 的
|
||||
`git/cmd/git.exe`)会识别到第二层并加入 `~/.pmm/root/git/cmd`。
|
||||
- Windows 通过用户注册表 PATH 持久化(`setx`),新开终端即生效;Linux/macOS
|
||||
写 `~/.bashrc`(重新加载 shell 生效)。
|
||||
|
||||
## 现成的 .pdm 包(dist/)
|
||||
|
||||
- `nodejs-24.20.0.pdm` — Node.js 24.20.0(Windows,源自官方 MSI)
|
||||
- `nodejs-linux-24.20.0.pdm` — Node.js 24.20.0(Linux x64,源自官方 .tar.xz)
|
||||
- `git-2.55.0.5.pdm` — Git for Windows 2.55.0.5(MinGit 官方发行版)
|
||||
- `pureftpd-1.0.51.pdm` — **Pure-FTPd 1.0.51(Linux)**:源码分发,装到 Linux 的 `~/.pmm/root`
|
||||
后需 `./configure && make && sudo make install` 编译使用(官方只发布源码)。
|
||||
|
||||
## 自动识别平台
|
||||
|
||||
镜像站把同一软件做成**多平台变体**(`variants` 数组,每项带 `os`)。客户端安装时
|
||||
**自动按当前系统选平台**:
|
||||
|
||||
```sh
|
||||
pdmm install nodejs # Windows 上装 node.exe 版,Linux 上自动装 ELF 版
|
||||
pmm install nodejs
|
||||
```
|
||||
|
||||
- Windows 机 `install nodejs` → 拉取 `os=windows` 变体(`packages/nodejs/24.20.0-windows.pdm`)
|
||||
- Linux 机 `install nodejs` → 拉取 `os=linux` 变体(`packages/nodejs/24.20.0-linux.pdm`)
|
||||
- 版本语义仍支持 `==` / `>=` / `<=` / `,` 组合,且在**匹配当前平台**的版本里选最高。
|
||||
|
||||
## 跨环境可靠性
|
||||
|
||||
`.pdm`/`.deb` 等 tar 相关安装动作先切换到 PMM 目录再用**相对路径**执行,避免
|
||||
git-bash 的 MSYS GNU tar(认 `/c/...`)与 Windows 自带的 bsdtar(认 `C:\...`)
|
||||
对盘符路径解析不一致的问题。
|
||||
|
||||
## registry 格式(pmm install xxx)
|
||||
|
||||
每个镜像的 `{registry}/{包名}.json` 描述一个包;`url` + `file` 指向发布文件,
|
||||
`sha256`(可选)作为该文件的独立校验来源:
|
||||
|
||||
```json
|
||||
{ "version": "1.2.3", "url": "https://.../foo-1.2.3-win64.exe",
|
||||
"file": "foo-1.2.3-win64.exe", "sha256": "..." }
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/main.c 命令入口(pmm)
|
||||
src/json.c/.h 内置 JSON 解析器
|
||||
src/ini.c/.h INI/.conf 解析器
|
||||
src/http.c/.h 系统 curl 封装
|
||||
src/repo.c/.h 仓库适配(GitHub/GitLab/Gitea/Forgejo + 通用 URL 自动探测)
|
||||
src/sha256.c/.h SHA-256(FIPS 180-4)
|
||||
src/sha1.c/.h SHA-1(FIPS 180-4)
|
||||
src/mirrors.c/.h apt 式镜像列表(priority 排序 + 下载回退)
|
||||
src/pdm.c/.h .pdm 打包/安装/卸载(模拟 deb)
|
||||
src/pdm_main.c 独立 pdm 工具
|
||||
src/install.c/.h 下载、校验与各平台安装
|
||||
mirror/ 镜像站(Node.js 网页版)
|
||||
examples/ 配置文件示例
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
rem One-shot Windows build: produces pmm.exe and pdm.exe (needs MinGW gcc + curl in PATH)
|
||||
set SRC=src\main.c src\json.c src\ini.c src\pmm.c src\http.c src\repo.c src\install.c src\sha256.c src\sha1.c src\mirrors.c src\pdm.c
|
||||
gcc -O2 -Wall -static -Wextra -std=c11 -o pmm.exe %SRC%
|
||||
if errorlevel 1 (echo pmm build failed & exit /b 1)
|
||||
gcc -O2 -Wall -static -Wextra -std=c11 -o pdm.exe src\pdm_main.c src\pdm.c src\pmm.c src\sha256.c src\install.c src\http.c src\json.c src\ini.c src\mirrors.c src\sha1.c
|
||||
if errorlevel 1 (echo pdm build failed & exit /b 1)
|
||||
echo built pmm.exe and pdm.exe
|
||||
@@ -0,0 +1,7 @@
|
||||
# PMM 镜像源配置 (与 mirror.ini 格式完全相同)
|
||||
[ghfast]
|
||||
api = https://ghfast.example-mirror.dev
|
||||
default = true
|
||||
|
||||
[mygitea]
|
||||
api = https://git.mycompany.local
|
||||
@@ -0,0 +1,14 @@
|
||||
# PMM 镜像源配置 (mirror.conf 与本文件格式相同)
|
||||
# apt 式:priority 越小越优先;registry 为 pmm install <包> 的索引镜像;
|
||||
# download 为下载前缀;api 替换 git 主机 API 基地址。
|
||||
#
|
||||
[官方]
|
||||
registry = https://pmm.parlz.com/mirror/packages
|
||||
download = https://pmm.parlz.com/mirror/files
|
||||
priority = 1
|
||||
default = true
|
||||
|
||||
[自建Gitea]
|
||||
registry = https://git.mycompany.local/packages
|
||||
api = https://git.mycompany.local
|
||||
priority = 20
|
||||
@@ -0,0 +1,5 @@
|
||||
Package: hello
|
||||
Version: 1.0.0
|
||||
Architecture: all
|
||||
Description: A demo .pdm package
|
||||
Maintainer: PMM example <example@pmm.dev>
|
||||
@@ -0,0 +1,3 @@
|
||||
# PMM 配置 (与 pmm.json / pmm.ini 等价)
|
||||
registry = https://packages.example.com/pmm
|
||||
mirror = ghfast
|
||||
@@ -0,0 +1,3 @@
|
||||
# PMM 配置 (与 pmm.json 等价,二选一,查找顺序: pmm.json > pmm.ini > pmm.conf)
|
||||
registry = https://packages.example.com/pmm
|
||||
mirror = ghfast
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"registry": "https://packages.example.com/pmm",
|
||||
"mirror": "ghfast"
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PMM 镜像站</title>
|
||||
<style>
|
||||
:root { --bg:#0f172a; --card:#1e293b; --acc:#38bdf8; --txt:#e2e8f0; --muted:#94a3b8; --pos:#4ade80; --neg:#f87171; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Microsoft YaHei",sans-serif;
|
||||
background:var(--bg); color:var(--txt); }
|
||||
header { padding:22px 28px; border-bottom:1px solid #334155; display:flex; align-items:center; gap:16px; flex-wrap:wrap; }
|
||||
header h1 { font-size:20px; margin:0; display:flex; align-items:center; gap:10px; }
|
||||
header h1 .dot { width:12px; height:12px; border-radius:50%; background:var(--acc); box-shadow:0 0 12px var(--acc); }
|
||||
.crumb { color:var(--muted); font-size:14px; }
|
||||
main { padding:20px 28px; max-width:1080px; margin:0 auto; }
|
||||
.toolbar { display:flex; gap:12px; align-items:center; flex-wrap:wrap; margin-bottom:20px; }
|
||||
input,select,button { padding:9px 12px; border-radius:8px; border:1px solid #334155; background:var(--card); color:var(--txt); font-size:14px; }
|
||||
input:focus,select:focus { outline:2px solid var(--acc); border-color:transparent; }
|
||||
button.primary { background:var(--acc); border:none; color:#022c3b; font-weight:600; cursor:pointer; }
|
||||
button.primary:hover { filter:brightness(1.1); }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:16px; }
|
||||
.card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; transition:.15s; }
|
||||
.card:hover { border-color:var(--acc); transform:translateY(-2px); }
|
||||
.card .name { font-weight:700; font-size:16px; display:flex; justify-content:space-between; align-items:center; gap:8px; }
|
||||
.tag { font-size:12px; padding:2px 8px; border-radius:20px; background:#0f172a; color:var(--muted); border:1px solid #334155; }
|
||||
.tag.win { color:#60a5fa; } .tag.lin { color:#4ade80; } .tag.mac { color:#f472b6; } .tag.any { color:var(--muted); }
|
||||
.card .desc { color:var(--muted); font-size:13px; margin:8px 0; min-height:36px; }
|
||||
.card .meta { font-size:12px; color:var(--muted); display:flex; justify-content:space-between; }
|
||||
.card .actions { margin-top:12px; display:flex; gap:8px; }
|
||||
.card a.btn { text-decoration:none; font-size:13px; padding:6px 10px; border-radius:7px; }
|
||||
.a-ok { background:#14532d; color:#bbf7d0; } .a-mut { background:#1e293b; border:1px solid #334155; color:var(--txt); }
|
||||
.hint { color:var(--muted); font-size:13px; }
|
||||
code { background:#0f172a; border:1px solid #334155; padding:1px 5px; border-radius:5px; font-size:12px; }
|
||||
.upload { margin:22px 0; background:var(--card); border:1px dashed #475569; border-radius:12px; padding:18px; }
|
||||
.upload h3 { margin:0 0 10px; font-size:15px; }
|
||||
.upload .row { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
|
||||
.upload .row label { font-size:13px; color:var(--muted); }
|
||||
.error { color:var(--neg); }
|
||||
.success { color:var(--pos); }
|
||||
footer { color:var(--muted); font-size:12px; text-align:center; padding:26px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><span class="dot"></span>PMM 镜像站</h1>
|
||||
<span class="crumb" id="mirror-url"></span>
|
||||
</header>
|
||||
<main>
|
||||
<div class="toolbar">
|
||||
<input id="q" placeholder="搜索包名 / 描述..." autocomplete="off">
|
||||
<select id="os">
|
||||
<option value="all">全部平台</option>
|
||||
<option value="windows">Windows</option>
|
||||
<option value="linux">Linux</option>
|
||||
<option value="macos">macOS</option>
|
||||
<option value="any">跨平台</option>
|
||||
</select>
|
||||
<span class="hint" id="count"></span>
|
||||
</div>
|
||||
|
||||
<div class="upload">
|
||||
<h3>上传包(.pdm / .deb / .exe / .dmg / .tar.gz ...)</h3>
|
||||
<div class="row">
|
||||
<label>包名 <input id="u-pkg" placeholder="myapp" size="18"></label>
|
||||
<label>版本 <input id="u-ver" placeholder="1.0.0" size="10"></label>
|
||||
<label>文件 <input type="file" id="u-file" required></label>
|
||||
<button class="primary" id="u-go">上传并索引</button>
|
||||
</div>
|
||||
<div class="hint" id="u-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid" id="grid"></div>
|
||||
<br>
|
||||
<div class="hint" style="text-align:center">
|
||||
客户端使用:<code>pmm mirror use main</code> 后 <code>pmm install <包名></code>,
|
||||
或复制本页 <a href="/mirror.ini" style="color:var(--acc)">mirror.ini</a> 到 <code>~/.pmm/</code>。
|
||||
</div>
|
||||
</main>
|
||||
<footer>ParlzPackageManger · 镜像站</footer>
|
||||
|
||||
<script>
|
||||
const SELF = location.origin;
|
||||
document.getElementById('mirror-url').textContent = SELF;
|
||||
|
||||
let ALL = [];
|
||||
async function load() {
|
||||
try {
|
||||
const r = await fetch(SELF + '/api/packages.json');
|
||||
const j = await r.json();
|
||||
ALL = j.packages || [];
|
||||
render();
|
||||
} catch (e) { console.warn('packages.json:%o', e); }
|
||||
}
|
||||
function platformOf(m) {
|
||||
const osv = (m.os || '').toLowerCase();
|
||||
if (osv === 'all' || osv === 'any' || !osv) return 'any';
|
||||
if (/win/.test(osv)) return 'windows';
|
||||
if (/mac|darwin/.test(osv)) return 'macos';
|
||||
return 'linux';
|
||||
}
|
||||
function render() {
|
||||
const q = document.getElementById('q').value.trim().toLowerCase();
|
||||
const osf = document.getElementById('os').value;
|
||||
const list = ALL.filter(m => {
|
||||
const match = !q || (m.name||'').toLowerCase().includes(q) || (m.description||'').toLowerCase().includes(q);
|
||||
const okOs = osf === 'all' || platformOf(m) === osf || platformOf(m) === 'any';
|
||||
return match && okOs;
|
||||
});
|
||||
document.getElementById('count').textContent = `${list.length} / ${ALL.length} 包`;
|
||||
const grid = document.getElementById('grid');
|
||||
grid.innerHTML = '';
|
||||
const tagCls = { windows:'win', linux:'lin', macos:'mac', any:'any' };
|
||||
for (const m of list) {
|
||||
const p = platformOf(m);
|
||||
const name = m.name || m.file || '?';
|
||||
const ver = m.version || '';
|
||||
const file = m.file || '';
|
||||
const size = m.size ? fmtSize(m.size) : '';
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.innerHTML = `
|
||||
<div class="name"><span>${esc(name)}</span><span class="tag ${tagCls[p]||''}">${p}</span></div>
|
||||
<div class="desc">${esc(m.description || '暂无描述')}</div>
|
||||
<div class="meta"><span>v${esc(ver)}</span><span>${size}</span></div>
|
||||
<div class="actions">
|
||||
<a class="btn a-ok" href="${esc(m.url||'#')}" download>下载</a>
|
||||
<a class="btn a-mut" href="#" data-copy="${esc('pmm install ' + name)}">复制安装命令</a>
|
||||
</div>`;
|
||||
card.querySelector('[data-copy]').addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
navigator.clipboard && navigator.clipboard.writeText(e.target.getAttribute('data-copy'));
|
||||
e.target.textContent = '已复制';
|
||||
setTimeout(()=>e.target.textContent='复制安装命令',1200);
|
||||
});
|
||||
grid.appendChild(card);
|
||||
}
|
||||
}
|
||||
function fmtSize(n) {
|
||||
if (n >= 1048576) return (n/1048576).toFixed(1) + ' MB';
|
||||
if (n >= 1024) return (n/1024).toFixed(1) + ' KB';
|
||||
return n + ' B';
|
||||
}
|
||||
function esc(s){ return String(s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
document.getElementById('q').addEventListener('input', render);
|
||||
document.getElementById('os').addEventListener('change', render);
|
||||
|
||||
document.getElementById('u-go').addEventListener('click', async () => {
|
||||
const pkg = document.getElementById('u-pkg').value.trim();
|
||||
const ver = document.getElementById('u-ver').value.trim();
|
||||
const f = document.getElementById('u-file').files[0];
|
||||
const status = document.getElementById('u-status');
|
||||
if (!f) { status.textContent = '请选择文件'; return; }
|
||||
status.textContent = '上传中...'; status.className='hint';
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
const hdr = {};
|
||||
if (pkg) hdr['x-pkg-name'] = pkg;
|
||||
if (ver) hdr['x-pkg-version'] = ver;
|
||||
try {
|
||||
const r = await fetch(SELF + '/upload', { method:'POST', body: fd, headers: hdr });
|
||||
const j = await r.json();
|
||||
status.textContent = `完成:${j.file}(sha256 ${(j.sha256||'').slice(0,16)}…)` + (j.pkg?`,注册包 ${j.pkg}`:'');
|
||||
status.className='success';
|
||||
load();
|
||||
} catch (e) { status.textContent='上传失败 '+e; status.className='error'; }
|
||||
});
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
// PMM 镜像站(网页版,Node.js 无依赖)
|
||||
//
|
||||
// 用自带的 Node 运行,或把这些静态文件放到任意 web 服务器(nginx/静态托管)上。
|
||||
//
|
||||
// node server.js // 默认 http://localhost:8080
|
||||
// PMM_PORT=9000 node server.js // 自定义端口
|
||||
//
|
||||
// 目录约定(见 README):
|
||||
// packages/ registry 索引(packages.json 聚合 + <pkg>.json 单包元数据)
|
||||
// files/ 实际发布文件(deb/exe/dmg/pdm/tar.gz ...)
|
||||
// mirror.ini 提供给 pmm 的镜像配置(server.js 会生成)
|
||||
//
|
||||
// 网页 / /index.html 浏览器浏览、搜索、按平台筛选
|
||||
// /api/packages.json registry 索引
|
||||
// /api/packages/<pkg>.json 单包元数据
|
||||
// /packages.json 同 /api/packages.json(让 pmm 直接用 / 也行)
|
||||
// /files/<name> 文件下载
|
||||
// /mirror.ini 可直接复制到 ~/.pmm/mirror.ini
|
||||
// /upload POST 上传(multipart 或 raw)
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
|
||||
const ROOT = __dirname;
|
||||
const PKG_DIR = path.join(ROOT, 'packages');
|
||||
const PORT = process.env.PMM_PORT ? parseInt(process.env.PMM_PORT, 10) : 8080;
|
||||
const HOST = process.env.PMM_HOST || '127.0.0.1';
|
||||
/* Default public base for generated download URLs / mirror.ini.
|
||||
* Override with PMM_BASE_URL (e.g. http://127.0.0.1:8080 for local testing). */
|
||||
const BASE_URL = (process.env.PMM_BASE_URL || 'https://pmm.parlz.com/mirror').replace(/\/+$/, '');
|
||||
|
||||
function ensureDirs() {
|
||||
if (!fs.existsSync(PKG_DIR)) fs.mkdirSync(PKG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/* ---- parse the <pkg>.json latest pointers into an index ---- */
|
||||
function loadIndex() {
|
||||
const index = { generated: new Date().toISOString(), packages: [] };
|
||||
if (!fs.existsSync(PKG_DIR)) return index;
|
||||
for (const f of fs.readdirSync(PKG_DIR)) {
|
||||
if (!f.endsWith('.json')) continue; /* <pkg>.json only (list, not pkg/ dir) */
|
||||
if (fs.statSync(path.join(PKG_DIR, f)).isDirectory()) continue;
|
||||
try {
|
||||
const meta = JSON.parse(fs.readFileSync(path.join(PKG_DIR, f), 'utf8'));
|
||||
if (!meta.variants) continue;
|
||||
index.packages.push(meta);
|
||||
} catch (e) { /* skip corrupt */ }
|
||||
}
|
||||
index.packages.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
||||
return index;
|
||||
}
|
||||
|
||||
/* ---- helpers ---- */
|
||||
function sha256(p) { return crypto.createHash('sha256').update(p).digest('hex'); }
|
||||
function sha1(p) { return crypto.createHash('sha1').update(p).digest('hex'); }
|
||||
|
||||
function baseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
/* ---- upload a file into packages/<pkg>/<ver>.pdm, then write registry metadata ---- */
|
||||
function handleUpload(req, res) {
|
||||
let body = [];
|
||||
req.on('data', c => body.push(c));
|
||||
req.on('end', () => {
|
||||
body = Buffer.concat(body);
|
||||
let fname = null;
|
||||
const ct = req.headers['content-type'] || '';
|
||||
if (/multipart\/form-data/.test(ct)) {
|
||||
const b = body.toString('latin1');
|
||||
const disp = /filename="([^"]+)"/.exec(b);
|
||||
fname = disp ? disp[1] : null;
|
||||
const marker = `\r\n\r\n`;
|
||||
const idx = b.indexOf(marker);
|
||||
body = idx >= 0 ? Buffer.from(b.substring(idx + marker.length, b.length - 4 || b.length), 'latin1') : body;
|
||||
} else {
|
||||
fname = req.headers['x-file-name'] || urlQuery(req.url, 'file') || 'file.pdm';
|
||||
}
|
||||
if (!fname) { res.writeHead(400); return res.end('no filename'); }
|
||||
fname = path.basename(fname.replace(/[\\/]/g, '/'));
|
||||
const pkg = (req.headers['x-pkg-name'] || urlQuery(req.url, 'pkg') || '').trim();
|
||||
let ver = (req.headers['x-pkg-version'] || urlQuery(req.url, 'ver') || '').trim();
|
||||
if (!pkg) { res.writeHead(400); return res.end('no x-pkg-name'); }
|
||||
if (!ver) ver = sha256(body).slice(0, 8);
|
||||
|
||||
/* store packages/<pkg>/<ver>-<os>.pdm (os lets one package carry multiple platforms) */
|
||||
const dir = path.join(PKG_DIR, pkg);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const os = (req.headers['x-pkg-os'] || urlQuery(req.url, 'os') || 'any').trim();
|
||||
const storeName = `${ver}-${os}.pdm`;
|
||||
const outPath = path.join(dir, storeName);
|
||||
fs.writeFileSync(outPath, body);
|
||||
|
||||
const info = {
|
||||
size: body.length,
|
||||
sha256: sha256(body),
|
||||
sha1: sha1(body),
|
||||
file: storeName,
|
||||
url: `${baseUrl()}/packages/${encodeURIComponent(pkg)}/${encodeURIComponent(storeName)}`,
|
||||
os,
|
||||
description: (req.headers['x-pkg-desc'] || urlQuery(req.url, 'desc') || `Uploaded via PMM mirror (${baseUrl()})`).trim(),
|
||||
};
|
||||
writeRegistryEntry(pkg, ver, info);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, file: storeName, version: ver, size: info.size,
|
||||
sha256: info.sha256, sha1: info.sha1, pkg, url: info.url, mirror: baseUrl() }));
|
||||
});
|
||||
}
|
||||
|
||||
function writeRegistryEntry(pkg, ver, info) {
|
||||
const meta = {
|
||||
name: pkg, version: ver,
|
||||
file: info.file, url: info.url, sha256: info.sha256,
|
||||
os: info.os || 'any',
|
||||
description: info.description || `Uploaded via PMM mirror (${baseUrl()})`,
|
||||
};
|
||||
/* per-version-per-os metadata: packages/<pkg>/<ver>-<os>.json */
|
||||
fs.writeFileSync(path.join(PKG_DIR, pkg, `${ver}-${info.os}.json`), JSON.stringify(meta, null, 2));
|
||||
/* latest pointer: packages/<pkg>.json with a versions list */
|
||||
const latestPath = path.join(PKG_DIR, pkg + '.json');
|
||||
let last = {};
|
||||
if (fs.existsSync(latestPath)) {
|
||||
try { last = JSON.parse(fs.readFileSync(latestPath, 'utf8')); } catch (e) { last = {}; }
|
||||
}
|
||||
/* variants: one entry per (version, os) so the client can pick by platform */
|
||||
let variants = Array.isArray(last.variants) ? last.variants : [];
|
||||
variants = variants.filter(v => !(v.version === ver && v.os === (info.os || 'any')));
|
||||
variants.push({ version: ver, os: info.os || 'any', file: info.file, url: info.url,
|
||||
sha256: info.sha256, description: info.description || '' });
|
||||
const latest = Object.assign({}, meta, { variants });
|
||||
fs.writeFileSync(latestPath, JSON.stringify(latest, null, 2));
|
||||
console.log(` [registry] ${pkg} @ ${ver} (os=${meta.os}) -> packages/${pkg}/${ver}-${meta.os}.pdm`);
|
||||
}
|
||||
|
||||
function urlQuery(url, key) {
|
||||
const q = url.split('?')[1] || '';
|
||||
for (const kv of q.split('&')) {
|
||||
const [k, v] = kv.split('=');
|
||||
if (k === key) return decodeURIComponent(v || '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json',
|
||||
'.ini': 'text/plain', '.txt': 'text/plain', '.css': 'text/css', '.pdm': 'application/octet-stream' };
|
||||
|
||||
function serveStatic(res, p) {
|
||||
const ext = path.extname(p);
|
||||
fs.readFile(p, (err, data) => {
|
||||
if (err) { res.writeHead(404); return res.end('not found'); }
|
||||
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream',
|
||||
'Content-Length': data.length }); /* lets the client show total size */
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
function generateMirrorIni() {
|
||||
return `# 由 PMM 镜像站 ${baseUrl()} 自动生成
|
||||
# 复制到 ~/.pmm/mirror.ini(或 mirror.conf)即可用 pmm install 从本镜像安装
|
||||
[main]
|
||||
registry = ${baseUrl()}/packages
|
||||
priority = 1
|
||||
`;
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, baseUrl());
|
||||
const pathname = decodeURIComponent(u.pathname);
|
||||
|
||||
// CORS for browser-based tools
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Headers', '*');
|
||||
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
|
||||
|
||||
if (req.method === 'POST' && pathname === '/upload') return handleUpload(req, res);
|
||||
|
||||
if (pathname === '/' || pathname === '/index.html') return serveStatic(res, path.join(ROOT, 'index.html'));
|
||||
if (pathname === '/packages.json' || pathname === '/api/packages.json')
|
||||
return serveJson(res, loadIndex());
|
||||
if (pathname === '/mirror.ini' || pathname === '/mirror.conf') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
return res.end(generateMirrorIni());
|
||||
}
|
||||
const pm = /^\/api\/packages\/([^/]+)\.json$/.exec(pathname) || /^\/packages\/([^/]+)\.json$/.exec(pathname);
|
||||
if (pm) {
|
||||
const f = path.join(PKG_DIR, pm[1] + '.json');
|
||||
if (fs.existsSync(f)) return serveStatic(res, f);
|
||||
res.writeHead(404); return res.end('not found');
|
||||
}
|
||||
// versioned metadata / package file:
|
||||
// /packages/<pkg>/<ver>-<os>.json -> packages/<pkg>/<ver>-<os>.json
|
||||
// /packages/<pkg>/<ver>-<os>.pdm -> packages/<pkg>/<ver>-<os>.pdm (download)
|
||||
const pv = /^\/api\/packages\/([^/]+)\/([^/]+)\.(json|pdm)$/.exec(pathname) ||
|
||||
/^\/packages\/([^/]+)\/([^/]+)\.(json|pdm)$/.exec(pathname);
|
||||
if (pv) {
|
||||
const ext = '.' + pv[3];
|
||||
const f = path.join(PKG_DIR, pv[1], pv[2] + ext);
|
||||
if (fs.existsSync(f)) return serveStatic(res, f);
|
||||
res.writeHead(404); return res.end('not found');
|
||||
}
|
||||
if (pathname === '/api/health') { res.writeHead(200); return res.end('ok'); }
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('not found');
|
||||
});
|
||||
|
||||
function serveJson(res, obj) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj, null, 2)); }
|
||||
|
||||
ensureDirs();
|
||||
console.log(`PMM mirror (web) serving ${ROOT}`);
|
||||
console.log(` web UI: ${baseUrl()}/`);
|
||||
console.log(` registry: ${baseUrl()}/packages.json`);
|
||||
console.log(` mirror.ini: ${baseUrl()}/mirror.ini`);
|
||||
server.listen(PORT, HOST);
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
/* http.c - HTTP(S) via the system curl executable */
|
||||
#include "http.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#define PMM_POPEN _popen
|
||||
#define PMM_PCLOSE _pclose
|
||||
#define PMM_POPEN_READ(cmd) _popen(cmd, "r") /* text: header/body capture */
|
||||
#define PMM_PCLOSE_READ(p) _pclose(p)
|
||||
#define PMM_POPEN_READ_X(cmd) _popen(cmd, "rb") /* binary: download streams */
|
||||
#define PMM_PCLOSE_READ_X(p) _pclose(p)
|
||||
#define STDERR_FD _fileno(stderr)
|
||||
#define IS_TTY(fd) _isatty(fd)
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define PMM_POPEN popen
|
||||
#define PMM_PCLOSE pclose
|
||||
#define PMM_POPEN_READ(cmd) popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ(p) pclose(p)
|
||||
#define PMM_POPEN_READ_X(cmd) popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ_X(p) pclose(p)
|
||||
#define STDERR_FD fileno(stderr)
|
||||
#define IS_TTY(fd) isatty(fd)
|
||||
#endif
|
||||
|
||||
/* Read all output of `cmd` into a malloc'd buffer. */
|
||||
static char *run_capture(const char *cmd, size_t *out_len) {
|
||||
FILE *f = PMM_POPEN(cmd, "rb");
|
||||
if (!f) return NULL;
|
||||
size_t cap = 64 * 1024, len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) { PMM_PCLOSE(f); return NULL; }
|
||||
for (;;) {
|
||||
if (len + 4096 > cap) {
|
||||
cap *= 2;
|
||||
char *nb = realloc(buf, cap);
|
||||
if (!nb) { free(buf); PMM_PCLOSE(f); return NULL; }
|
||||
buf = nb;
|
||||
}
|
||||
size_t n = fread(buf + len, 1, 4096, f);
|
||||
len += n;
|
||||
if (n == 0) break;
|
||||
}
|
||||
int rc = PMM_PCLOSE(f);
|
||||
if (rc != 0 && len == 0) { free(buf); return NULL; }
|
||||
if (out_len) *out_len = len;
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Run `curl <args>` with the URL appended; return captured output. */
|
||||
static char *curl_capture(const char *args, const char *url, size_t *out_len) {
|
||||
size_t cmdlen = strlen(args) + strlen(url) * 3 + 64;
|
||||
char *cmd = malloc(cmdlen);
|
||||
if (!cmd) return NULL;
|
||||
/* wrap URL in double quotes, escaping embedded quotes */
|
||||
snprintf(cmd, cmdlen, "curl %s \"", args);
|
||||
char *p = cmd + strlen(cmd);
|
||||
for (const char *u = url; *u && (size_t)(p - cmd) < cmdlen - 4; u++) {
|
||||
if (*u == '"') *p++ = '\\';
|
||||
if (*u == '$') *p++ = '\\';
|
||||
*p++ = *u;
|
||||
}
|
||||
*p++ = '"';
|
||||
*p = '\0';
|
||||
char *res = run_capture(cmd, out_len);
|
||||
free(cmd);
|
||||
return res;
|
||||
}
|
||||
|
||||
char *http_get(const char *url, int *status) {
|
||||
/* -w appends the HTTP status code on its own last line */
|
||||
size_t len = 0;
|
||||
char *body = curl_capture("-sSL --max-time 60 -w \\n__PMM_HTTP__%{http_code}", url, &len);
|
||||
if (!body) return NULL;
|
||||
char *marker = body + len - 1;
|
||||
while (marker > body && *marker != '\n') marker--;
|
||||
if (strncmp(marker + 1, "__PMM_HTTP__", 12) == 0) {
|
||||
int code = atoi(marker + 13);
|
||||
*marker = '\0'; /* strip marker + newline */
|
||||
if (code <= 0) { /* 000 = failed transfer (reset/refused/DNS), not success */
|
||||
if (status) *status = -1;
|
||||
free(body);
|
||||
return NULL;
|
||||
}
|
||||
if (status) *status = code;
|
||||
} else {
|
||||
if (status) *status = -1;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/* ---------- python/tqdm-style download progress ---------- */
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
static unsigned long long now_ms(void) { return (unsigned long long)GetTickCount64(); }
|
||||
#else
|
||||
#include <time.h>
|
||||
static unsigned long long now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (unsigned long long)ts.tv_sec * 1000 + (unsigned long long)ts.tv_nsec / 1000000; }
|
||||
#endif
|
||||
|
||||
/* human size: B / KB / MB / GB with 1 decimal */
|
||||
static void hs(unsigned long long v, char *out, size_t n) {
|
||||
const char *u[] = { "B", "KB", "MB", "GB", "TB" };
|
||||
int i = 0; double d = (double)v;
|
||||
while (d >= 1024.0 && i < 4) { d /= 1024.0; i++; }
|
||||
if (i == 0) snprintf(out, n, "%llu B", v);
|
||||
else snprintf(out, n, "%.1f%s", d, u[i]);
|
||||
}
|
||||
|
||||
static void hms(double sec, char *out, size_t n) {
|
||||
int s = (int)sec;
|
||||
if (s >= 3600) snprintf(out, n, "%02d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60);
|
||||
else snprintf(out, n, "%02d:%02d", s / 60, s % 60);
|
||||
}
|
||||
|
||||
/* python-style bar, ASCII-safe (no unicode block glyphs, so it can't mojibake
|
||||
* on GBK/936 or other non-UTF-8 Windows console codepages):
|
||||
* 93%|##############--------| 36.1MB/38.8MB [01:30<00:05, 560.7KB/s] */
|
||||
static void render_progress(unsigned long long fetched, unsigned long long total,
|
||||
double elapsed, double speed, int done) {
|
||||
int w = 28;
|
||||
char line[240];
|
||||
if (total == 0) { /* unknown total */
|
||||
char f[32], sp[32], et[32];
|
||||
hs(fetched, f, sizeof(f));
|
||||
hs(speed > 0 ? speed : 0, sp, sizeof(sp));
|
||||
hms(elapsed, et, sizeof(et));
|
||||
snprintf(line, sizeof(line), "\r %s [%s, %s/s]", f, et, sp);
|
||||
} else {
|
||||
double pct = (double)fetched / (double)total * 100.0;
|
||||
int fill = (int)(pct / 100.0 * w);
|
||||
char bar[32];
|
||||
for (int i = 0; i < w; i++) bar[i] = (i < fill) ? '#' : ' ';
|
||||
bar[w] = '\0';
|
||||
char f[32], t[32], sp[32], e[32], eta[32];
|
||||
hs(fetched, f, sizeof(f));
|
||||
hs(total, t, sizeof(t));
|
||||
hs(speed > 0 ? speed : 0, sp, sizeof(sp));
|
||||
hms(elapsed, e, sizeof(e));
|
||||
double rem = speed > 0 ? ((double)total - (double)fetched) / speed : 0;
|
||||
hms(rem, eta, sizeof(eta));
|
||||
snprintf(line, sizeof(line), "\r%3.0f%%|%s| %s/%s [%s<%s, %s/s]",
|
||||
pct >= 100.0 ? 100.0 : pct, bar, f, t, e, eta, sp);
|
||||
}
|
||||
/* pad to a fixed width that ALWAYS fits in a normal terminal (~72 cols) so
|
||||
* the frame overwrites in place; ≥200 spaces would wrap on narrow consoles
|
||||
* and that is what makes it look like the bar is spamming many lines. */
|
||||
int L = (int)strlen(line);
|
||||
while (L < 72) line[L++] = ' ';
|
||||
line[L] = '\0';
|
||||
fprintf(stderr, "%s", line);
|
||||
if (done) fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
/* fetch Content-Length (bytes) via HEAD; returns 0 if unknown */
|
||||
static unsigned long long remote_size(const char *url) {
|
||||
unsigned long long sz = 0;
|
||||
char cmd[2100];
|
||||
snprintf(cmd, sizeof(cmd), "curl -sIL --max-time 30 \"%s\" 2>nul", url);
|
||||
FILE *f = PMM_POPEN_READ(cmd);
|
||||
if (f) {
|
||||
char line[1024];
|
||||
char low[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
strncpy(low, line, sizeof(low) - 1); low[sizeof(low) - 1] = '\0';
|
||||
for (char *p = low; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32;
|
||||
char *v = strstr(low, "content-length:");
|
||||
if (v) { sz = (unsigned long long)strtoull(v + 15, NULL, 10); break; }
|
||||
}
|
||||
PMM_PCLOSE_READ(f);
|
||||
}
|
||||
return sz;
|
||||
}
|
||||
|
||||
int http_download(const char *url, const char *out_path) {
|
||||
int tty = IS_TTY(STDERR_FD);
|
||||
if (getenv("PMM_FORCE_PROGRESS")) tty = 1; /* debug/testing override */
|
||||
|
||||
if (!tty) {
|
||||
/* background/redirect: silent, simple */
|
||||
size_t cmdlen = strlen(url) * 3 + strlen(out_path) * 3 + 160;
|
||||
char *cmd = malloc(cmdlen);
|
||||
if (!cmd) return -1;
|
||||
snprintf(cmd, cmdlen, "curl -L --fail --retry 3 --max-time 3600 -S --silent -o \"%s\" \"%s\"", out_path, url);
|
||||
int rc = system(cmd);
|
||||
free(cmd);
|
||||
return (rc == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* terminal: stream via popen and paint a python-style bar */
|
||||
unsigned long long total = remote_size(url);
|
||||
char cmd[2100];
|
||||
snprintf(cmd, sizeof(cmd), "curl -sL --fail --retry 3 --max-time 3600 -o - \"%s\"", url);
|
||||
FILE *pf = PMM_POPEN_READ_X(cmd); /* binary read */
|
||||
if (!pf) return -1;
|
||||
FILE *outf = fopen(out_path, "wb");
|
||||
if (!outf) { PMM_PCLOSE_READ_X(pf); return -1; }
|
||||
|
||||
char buf[65536];
|
||||
size_t r;
|
||||
unsigned long long fetched = 0, last = 0, last_n = now_ms();
|
||||
double speed = 0, el = 0;
|
||||
double start = (double)now_ms();
|
||||
while ((r = fread(buf, 1, sizeof(buf), pf)) > 0) {
|
||||
fwrite(buf, 1, r, outf);
|
||||
fetched += r;
|
||||
unsigned long long n = now_ms();
|
||||
double dt = (n - last_n) / 1000.0;
|
||||
if (dt >= 0.25) {
|
||||
double inst = (fetched - last) / dt;
|
||||
speed = (speed <= 0) ? inst : (speed * 0.7 + inst * 0.3); /* EMA smooth */
|
||||
last = fetched; last_n = n;
|
||||
}
|
||||
el = ((double)n - start) / 1000.0;
|
||||
render_progress(fetched, total, el, speed, 0);
|
||||
}
|
||||
fclose(outf);
|
||||
int rc = PMM_PCLOSE_READ_X(pf);
|
||||
if (rc != 0) { remove(out_path); return -1; }
|
||||
render_progress(fetched, total, el, speed, 1);
|
||||
return 0;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/* http.h - HTTP(S) via system curl (curl is preinstalled on Win10+/macOS/most Linux) */
|
||||
#ifndef PMM_HTTP_H
|
||||
#define PMM_HTTP_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* GET a URL, return malloc'd body or NULL on failure. Sets *status (may be NULL). */
|
||||
char *http_get(const char *url, int *status);
|
||||
|
||||
/* Download URL to file path. Returns 0 on success. */
|
||||
int http_download(const char *url, const char *out_path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
/* ini.c - tiny INI / .conf parser */
|
||||
#include "ini.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static char *trim(char *s) {
|
||||
while (*s && isspace((unsigned char)*s)) s++;
|
||||
char *end = s + strlen(s);
|
||||
while (end > s && isspace((unsigned char)end[-1])) *--end = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
Ini *ini_load(const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return NULL;
|
||||
Ini *ini = calloc(1, sizeof(Ini));
|
||||
if (!ini) { fclose(f); return NULL; }
|
||||
char line[4096];
|
||||
char section[512] = "";
|
||||
IniEntry *tail = NULL;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *s = trim(line);
|
||||
if (*s == '\0' || *s == '#' || *s == ';') continue;
|
||||
if (*s == '[') {
|
||||
char *close = strchr(s, ']');
|
||||
if (close) {
|
||||
*close = '\0';
|
||||
strncpy(section, s + 1, sizeof(section) - 1);
|
||||
section[sizeof(section) - 1] = '\0';
|
||||
char *t = trim(section);
|
||||
memmove(section, t, strlen(t) + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
char *eq = strchr(s, '=');
|
||||
if (!eq) eq = strchr(s, ':');
|
||||
if (!eq) continue;
|
||||
*eq = '\0';
|
||||
char *key = trim(s);
|
||||
char *value = trim(eq + 1);
|
||||
IniEntry *e = calloc(1, sizeof(IniEntry));
|
||||
e->section = strdup(*section ? section : "");
|
||||
e->key = strdup(key);
|
||||
e->value = strdup(value);
|
||||
if (tail) tail->next = e; else ini->head = e;
|
||||
tail = e;
|
||||
}
|
||||
fclose(f);
|
||||
return ini;
|
||||
}
|
||||
|
||||
void ini_free(Ini *ini) {
|
||||
if (!ini) return;
|
||||
IniEntry *e = ini->head;
|
||||
while (e) {
|
||||
IniEntry *n = e->next;
|
||||
free(e->section); free(e->key); free(e->value); free(e);
|
||||
e = n;
|
||||
}
|
||||
free(ini);
|
||||
}
|
||||
|
||||
const char *ini_get(const Ini *ini, const char *section, const char *key) {
|
||||
if (!ini) return NULL;
|
||||
if (!section) section = "";
|
||||
for (IniEntry *e = ini->head; e; e = e->next)
|
||||
if (strcmp(e->section, section) == 0 && strcmp(e->key, key) == 0)
|
||||
return e->value;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *ini_find_key_by_value(const Ini *ini, const char *section, const char *value) {
|
||||
if (!ini) return NULL;
|
||||
if (!section) section = "";
|
||||
for (IniEntry *e = ini->head; e; e = e->next)
|
||||
if (strcmp(e->section, section) == 0 && strcmp(e->value, value) == 0)
|
||||
return e->key;
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* ini.h - tiny INI / .conf parser for PMM.
|
||||
* Format:
|
||||
* [section]
|
||||
* key = value (# or ; comment)
|
||||
* Also accepts key = value lines without a section (section = "").
|
||||
*/
|
||||
#ifndef PMM_INI_H
|
||||
#define PMM_INI_H
|
||||
|
||||
typedef struct IniEntry {
|
||||
char *section;
|
||||
char *key;
|
||||
char *value;
|
||||
struct IniEntry *next;
|
||||
} IniEntry;
|
||||
|
||||
typedef struct {
|
||||
IniEntry *head;
|
||||
} Ini;
|
||||
|
||||
Ini *ini_load(const char *path);
|
||||
void ini_free(Ini *ini);
|
||||
/* Lookup value; section may be NULL or "" for top-level. Returns NULL if absent. */
|
||||
const char *ini_get(const Ini *ini, const char *section, const char *key);
|
||||
/* First key in given section whose value matches; returns key name or NULL. */
|
||||
const char *ini_find_key_by_value(const Ini *ini, const char *section, const char *value);
|
||||
|
||||
#endif
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
/* install.c - download + per-OS install logic
|
||||
*
|
||||
* Install methods:
|
||||
* windows: .exe -> run installer silently where known, else copy to bin
|
||||
* .msi -> msiexec /i /qn
|
||||
* .zip/.7z -> extract into install dir
|
||||
* linux: .deb -> dpkg -i .rpm -> rpm -Uvh
|
||||
* .appimage -> chmod +x, copy to bin
|
||||
* .tar.gz/.tgz/.tar.xz -> extract into install dir
|
||||
* macos: .dmg -> hdiutil attach, copy .app to /Applications, detach
|
||||
* .pkg -> installer -pkg
|
||||
* .tar.gz/.tgz -> extract into install dir
|
||||
*/
|
||||
#include "install.h"
|
||||
#include "pmm.h"
|
||||
#include "http.h"
|
||||
#include "json.h"
|
||||
#include "sha256.h"
|
||||
#include "sha1.h"
|
||||
#include "mirrors.h"
|
||||
#include "pdm.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#define PMM_MKDIR(p) _mkdir(p)
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#define PMM_MKDIR(p) mkdir((p), 0755)
|
||||
#endif
|
||||
|
||||
static int has_suffix(const char *s, const char *suf) {
|
||||
size_t sl = strlen(s), xl = strlen(suf);
|
||||
return sl >= xl && strcasecmp(s + sl - xl, suf) == 0;
|
||||
}
|
||||
|
||||
static const char *base_name(const char *path) {
|
||||
const char *a = strrchr(path, '/');
|
||||
const char *b = strrchr(path, '\\');
|
||||
const char *p = (a > b) ? a : b;
|
||||
return p ? p + 1 : path;
|
||||
}
|
||||
|
||||
static int run_cmd_quiet(const char *cmd) {
|
||||
int rc = system(cmd);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Strip "<algo> *path" style prefix from checksum files; keep last token's hex. */
|
||||
static void extract_hex(const char *text, const char *filename, char *hex, size_t hexsize) {
|
||||
hex[0] = '\0';
|
||||
const char *line = text;
|
||||
while (line && *line) {
|
||||
const char *eol = strchr(line, '\n');
|
||||
size_t ll = eol ? (size_t)(eol - line) : strlen(line);
|
||||
char buf[1024];
|
||||
if (ll >= sizeof(buf)) ll = sizeof(buf) - 1;
|
||||
memcpy(buf, line, ll);
|
||||
buf[ll] = '\0';
|
||||
/* trim */
|
||||
char *s = buf;
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r') s++;
|
||||
size_t sl = strlen(s);
|
||||
while (sl && (s[sl-1] == ' ' || s[sl-1] == '\t' || s[sl-1] == '\r')) s[--sl] = '\0';
|
||||
/* does this line mention the file (or is a bare hash)? */
|
||||
char *hend = s;
|
||||
while (*hend && *hend != ' ' && *hend != '\t' && *hend != '*') hend++;
|
||||
int is_hex_prefix = 1;
|
||||
for (char *p = s; p < hend; p++)
|
||||
if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')))
|
||||
{ is_hex_prefix = 0; break; }
|
||||
if (is_hex_prefix && hend > s) {
|
||||
/* bare "<hex>" or "<hex> *file" */
|
||||
if (!*hend || strstr(hend, filename)) {
|
||||
snprintf(hex, hexsize, "%.*s", (int)(hend - s), s);
|
||||
return;
|
||||
}
|
||||
} else if (strstr(s, filename)) {
|
||||
/* "hash file" reversed order unlikely; take first token */
|
||||
char tok[256]; size_t t = 0;
|
||||
while (s[t] && s[t] != ' ' && s[t] != '\t' && t < sizeof(tok) - 1) { tok[t] = s[t]; t++; }
|
||||
tok[t] = '\0';
|
||||
int ok = *tok;
|
||||
for (char *p = tok; *p; p++)
|
||||
if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')))
|
||||
{ ok = 0; break; }
|
||||
if (ok) { snprintf(hex, hexsize, "%s", tok); return; }
|
||||
}
|
||||
line = eol ? eol + 1 : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Best-effort sha256/sha1 verification against sidecar checksum files
|
||||
* (<url>.sha256 / <url>.sha1). Returns 0 on ok-or-no-checksum, -1 on mismatch. */
|
||||
static int verify_checksums(const char *url, const char *path, const char *name) {
|
||||
char hex[128];
|
||||
struct { const char *ext; int algo; } algos[] = {
|
||||
{ ".sha256", 256 }, { ".sha1", 1 }, { NULL, 0 }
|
||||
};
|
||||
for (int i = 0; algos[i].ext; i++) {
|
||||
char csum_url[2400];
|
||||
snprintf(csum_url, sizeof(csum_url), "%s%s", url, algos[i].ext);
|
||||
int status = 0;
|
||||
char *text = http_get(csum_url, &status);
|
||||
if (!text || (status != 200 && status != 0)) { free(text); continue; }
|
||||
if (algos[i].algo == 256) {
|
||||
if (pmm_sha256_file(path, hex) != 0) { free(text); return -1; }
|
||||
} else {
|
||||
if (pmm_sha1_file(path, hex) != 0) { free(text); return -1; }
|
||||
}
|
||||
char expect[128];
|
||||
extract_hex(text, name, expect, sizeof(expect));
|
||||
free(text);
|
||||
if (!expect[0]) {
|
||||
printf("pmm: warning: could not parse %s checksum file\n", algos[i].ext);
|
||||
continue;
|
||||
}
|
||||
/* case-insensitive compare */
|
||||
if (strcasecmp(expect, hex) != 0) {
|
||||
fprintf(stderr, "pmm: CHECKSUM MISMATCH (%s)!\n expected: %s\n actual: %s\n",
|
||||
algos[i].ext, expect, hex);
|
||||
return -1;
|
||||
}
|
||||
printf("pmm: %s checksum OK: %s\n", algos[i].algo == 256 ? "sha256" : "sha1", hex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int install_file(const char *url, const char *name) {
|
||||
PmmOS os = pmm_detect_os();
|
||||
char cache[1024], path[1200], cmd[2600];
|
||||
pmm_cache_dir(cache, sizeof(cache));
|
||||
snprintf(path, sizeof(path), "%s/%s", cache, name);
|
||||
|
||||
/* apt-style fallback: mirrors (by priority) first, then the origin URL */
|
||||
MirrorList *ml = mirrors_load();
|
||||
int ncand = 0;
|
||||
char **cands = mirrors_download_candidates(ml, url, &ncand);
|
||||
int ok = -1;
|
||||
for (int i = 0; i < ncand; i++) {
|
||||
printf("pmm: downloading %s%s\n", cands[i],
|
||||
i < ncand - 1 ? " (mirror)" : "");
|
||||
if (http_download(cands[i], path) == 0) { ok = 0; break; }
|
||||
fprintf(stderr, "pmm: download failed, trying next source...\n");
|
||||
remove(path);
|
||||
}
|
||||
for (int i = 0; i < ncand; i++) free(cands[i]);
|
||||
free(cands);
|
||||
mirrors_free(ml);
|
||||
if (ok != 0) {
|
||||
fprintf(stderr, "pmm: all download sources failed: %s\n", url);
|
||||
return -1;
|
||||
}
|
||||
printf("pmm: downloaded %s\n", path);
|
||||
|
||||
/* integrity: sha256 + sha1, verified against sidecar checksums when present */
|
||||
char hex[128];
|
||||
if (pmm_sha256_file(path, hex) == 0)
|
||||
printf("pmm: sha256: %s\n", hex);
|
||||
if (pmm_sha1_file(path, hex) == 0)
|
||||
printf("pmm: sha1: %s\n", hex);
|
||||
if (verify_checksums(url, path, name) != 0) {
|
||||
fprintf(stderr, "pmm: refusing to install: checksum verification failed\n");
|
||||
remove(path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *bname = base_name(name);
|
||||
char dest[1024];
|
||||
pmm_install_dir(dest, sizeof(dest));
|
||||
|
||||
/* forward-slash clone of the cached path (native exes/tools accept it and the
|
||||
* git-bash shell won't mangle a command word like "C:/...") */
|
||||
char pwin[1200];
|
||||
snprintf(pwin, sizeof(pwin), "%s", path);
|
||||
for (char *q = pwin; *q; q++) if (*q == '\\') *q = '/';
|
||||
|
||||
/* .pdm packages are installed through the deb-like manager */
|
||||
if (has_suffix(bname, ".pdm") || has_suffix(bname, ".PDM")) {
|
||||
extern int pdm_install_file(const char *path);
|
||||
printf("pmm: installing .pdm %s ...\n", bname);
|
||||
if (pdm_install_file(path) != 0) {
|
||||
fprintf(stderr, "pmm: .pdm install failed: %s\n", bname);
|
||||
return -1;
|
||||
}
|
||||
printf("pmm: installed .pdm %s\n", bname);
|
||||
pmm_add_to_path();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (has_suffix(bname, ".deb") && os == OS_LINUX) {
|
||||
snprintf(cmd, sizeof(cmd), "sudo dpkg -i \"%s\"", path);
|
||||
} else if (has_suffix(bname, ".rpm") && os == OS_LINUX) {
|
||||
snprintf(cmd, sizeof(cmd), "sudo rpm -Uvh \"%s\"", path);
|
||||
} else if (has_suffix(bname, ".appimage") && os == OS_LINUX) {
|
||||
snprintf(cmd, sizeof(cmd), "chmod +x \"%s\" && mv -f \"%s\" \"%s/\"", path, path, dest);
|
||||
} else if ((has_suffix(bname, ".tar.gz") || has_suffix(bname, ".tgz") ||
|
||||
has_suffix(bname, ".tar.xz") || has_suffix(bname, ".tar.bz2") ||
|
||||
has_suffix(bname, ".zip") || has_suffix(bname, ".7z"))) {
|
||||
if (has_suffix(bname, ".zip"))
|
||||
snprintf(cmd, sizeof(cmd), "unzip -o \"%s\" -d \"%s\"", path, dest);
|
||||
else if (has_suffix(bname, ".7z"))
|
||||
snprintf(cmd, sizeof(cmd), "7z x -y -o\"%s\" \"%s\"", dest, path);
|
||||
else
|
||||
snprintf(cmd, sizeof(cmd), "tar -xf \"%s\" -C \"%s\"", path, dest);
|
||||
} else if (has_suffix(bname, ".dmg") && os == OS_MACOS) {
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"h=$(hdiutil attach \"%s\" | grep -o '/Volumes/.*' | head -1); "
|
||||
"sudo cp -R \"$h\"/*.app /Applications/ 2>/dev/null; hdiutil detach \"$h\"",
|
||||
path);
|
||||
} else if (has_suffix(bname, ".pkg") && os == OS_MACOS) {
|
||||
snprintf(cmd, sizeof(cmd), "sudo installer -pkg \"%s\" -target /", path);
|
||||
} else if (has_suffix(bname, ".msi") && os == OS_WINDOWS) {
|
||||
/* silent install; note system() runs cmd.exe on Windows, so use single
|
||||
* slashes and a forward-slash path (cmd accepts both) */
|
||||
snprintf(cmd, sizeof(cmd), "msiexec /i \"%s\" /qn", pwin);
|
||||
} else if (has_suffix(bname, ".exe") && os == OS_WINDOWS) {
|
||||
/* auto-run the installer. system() runs cmd.exe, which chokes on shell
|
||||
* control operators (|| / &) here, so issue ONE silent invocation
|
||||
* (common silent flags; vendors ignore the ones they don't know). */
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"\"%s\" /S /VERYSILENT /quiet --silent 2>nul", pwin);
|
||||
} else {
|
||||
/* unknown type for this OS: just leave in cache and copy if a single file */
|
||||
snprintf(cmd, sizeof(cmd), "cp -f \"%s\" \"%s/\" 2>/dev/null || true", path, dest);
|
||||
}
|
||||
|
||||
printf("pmm: installing %s ...\n", bname);
|
||||
if (run_cmd_quiet(cmd) != 0) {
|
||||
fprintf(stderr, "pmm: install command failed: %s\n", cmd);
|
||||
return -1;
|
||||
}
|
||||
printf("pmm: installed %s\n", bname);
|
||||
pmm_add_to_path();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- python-style version selection (==,>=,<=,>,<,!= , comma list) ---------- */
|
||||
|
||||
/* Compare dotted version strings numerically ("24.20.0" vs "24.2.0"). */
|
||||
static int vcmp(const char *a, const char *b) {
|
||||
const char *pa = a, *pb = b;
|
||||
while (*pa || *pb) {
|
||||
double va = 0, vb = 0; int ha = 0, hb = 0;
|
||||
while (*pa && *pa != '.' && *pa != '-') { va = va * 10 + (*pa - '0'); pa++; ha = 1; }
|
||||
while (*pb && *pb != '.' && *pb != '-') { vb = vb * 10 + (*pb - '0'); pb++; hb = 1; }
|
||||
if (ha != hb) return ha < hb ? -1 : 1;
|
||||
if (va != vb) return va < vb ? -1 : 1;
|
||||
if (*pa == '.') pa++; if (*pb == '.') pb++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Does `ver` satisfy one `cond` like ">=24.20.0"? (bare "24.20.0" means ==) */
|
||||
static int cond_match(const char *ver, const char *cond) {
|
||||
while (*cond == ' ' || *cond == '\t') cond++;
|
||||
char op[3] = {0}; int oi = 0;
|
||||
if (cond[0] == '=' && cond[1] == '=') { op[0] = '='; op[1] = '='; oi = 2; }
|
||||
else if (cond[0] == '!' && cond[1] == '=') { op[0] = '!'; op[1] = '='; oi = 2; }
|
||||
else if (cond[0] == '>' && cond[1] == '=') { op[0] = '>'; op[1] = '='; oi = 2; }
|
||||
else if (cond[0] == '<' && cond[1] == '=') { op[0] = '<'; op[1] = '='; oi = 2; }
|
||||
else if (cond[0] == '>') { op[0] = '>'; oi = 1; }
|
||||
else if (cond[0] == '<') { op[0] = '<'; oi = 1; }
|
||||
else { op[0] = '='; op[1] = '='; oi = 2; } /* bare version == */
|
||||
const char *num = cond + oi;
|
||||
while (*num == ' ' || *num == '\t') num++;
|
||||
int c = vcmp(ver, num);
|
||||
if (oi == 2 && op[0] == '=' && op[1] == '=') return c == 0;
|
||||
if (oi == 2 && op[0] == '!' && op[1] == '=') return c != 0;
|
||||
if (oi == 2 && op[0] == '>' && op[1] == '=') return c >= 0;
|
||||
if (oi == 2 && op[0] == '<' && op[1] == '=') return c <= 0;
|
||||
if (op[0] == '>') return c > 0;
|
||||
if (op[0] == '<') return c < 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ver satisfies a comma-separated spec like ">=24,<25" */
|
||||
static int spec_match(const char *ver, const char *spec) {
|
||||
char tmp[256];
|
||||
strncpy(tmp, spec, 255);
|
||||
tmp[255] = '\0';
|
||||
char *p = tmp;
|
||||
while (*p) {
|
||||
while (*p == ' ' || *p == '\t' || *p == ',') p++;
|
||||
if (!*p) break;
|
||||
char *start = p;
|
||||
while (*p && *p != ',') p++;
|
||||
char hold = *p;
|
||||
*p = '\0';
|
||||
if (!cond_match(ver, start)) return 0;
|
||||
if (hold) *p = hold;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Pick the HIGHEST variant matching current OS + version spec from a `variants`
|
||||
* array; returns the selected variant JsonValue* or NULL. */
|
||||
static const char *cur_os_name(void) {
|
||||
const char *ov = getenv("PMM_OS_OVERRIDE"); /* debug/testing */
|
||||
if (ov && *ov) return ov;
|
||||
switch (pmm_detect_os()) { case OS_WINDOWS: return "windows"; case OS_LINUX: return "linux";
|
||||
case OS_MACOS: return "macos"; default: return "any"; }
|
||||
}
|
||||
|
||||
int install_from_registry(const char *name, const char *spec, const char *mirror_active) {
|
||||
if (!name || !*name) return -1;
|
||||
MirrorList *ml = mirrors_load();
|
||||
int has_reg = 0;
|
||||
for (int i = 0; i < ml->count; i++)
|
||||
if (ml->items[i].registry && *ml->items[i].registry) has_reg = 1;
|
||||
|
||||
if (!has_reg) {
|
||||
fprintf(stderr,
|
||||
"pmm: no registry mirror configured. Add to ~/.pmm/mirror.ini:\n"
|
||||
" [name]\n registry = https://host/pmm\n"
|
||||
" (apt-style: mirrors are tried by priority; the first with the\n"
|
||||
" package wins, else plain 'pmm install --git owner/repo')\n");
|
||||
mirrors_free(ml);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Build registry base order: active mirror first, then by priority, deduped. */
|
||||
char *bases[128];
|
||||
int nb = 0;
|
||||
int seen[128] = {0};
|
||||
if (mirror_active && *mirror_active) {
|
||||
for (int i = 0; i < ml->count; i++)
|
||||
if (ml->items[i].registry && *ml->items[i].registry &&
|
||||
strcmp(ml->items[i].name, mirror_active) == 0)
|
||||
{ bases[nb++] = ml->items[i].registry; seen[i] = 1; break; }
|
||||
}
|
||||
for (int i = 0; i < ml->count && nb < 128; i++)
|
||||
if (!seen[i] && ml->items[i].registry && *ml->items[i].registry)
|
||||
bases[nb++] = ml->items[i].registry;
|
||||
|
||||
if (nb == 0) { fprintf(stderr, "pmm: no registry mirrors\n"); mirrors_free(ml); return -1; }
|
||||
|
||||
/* fetch the <pkg>.json latest pointer (carries the variants list) */
|
||||
char url[2048];
|
||||
int status = 0;
|
||||
char *body = NULL;
|
||||
char *used_base = NULL;
|
||||
for (int i = 0; i < nb; i++) {
|
||||
snprintf(url, sizeof(url), "%s/%s.json", bases[i], name);
|
||||
printf("pmm: looking up %s in mirror %s\n", name, bases[i]);
|
||||
body = http_get(url, &status);
|
||||
if (body && status == 200) { used_base = bases[i]; break; }
|
||||
free(body); body = NULL;
|
||||
}
|
||||
if (!body) {
|
||||
fprintf(stderr, "pmm: package '%s' not found in any registry mirror\n", name);
|
||||
mirrors_free(ml);
|
||||
return -1;
|
||||
}
|
||||
JsonValue *meta = json_parse(body);
|
||||
free(body);
|
||||
if (!meta || meta->type != JSON_OBJECT) {
|
||||
fprintf(stderr, "pmm: bad registry entry for '%s' (%s)\n", name, used_base);
|
||||
json_free(meta);
|
||||
mirrors_free(ml);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *osn = cur_os_name();
|
||||
JsonValue *variants = json_get(meta, "variants");
|
||||
const char *dl = NULL, *file = NULL;
|
||||
char *want_sha = NULL;
|
||||
const char *chosen = NULL;
|
||||
if (variants && variants->count > 0) {
|
||||
/* pick highest variant that matches OS (or 'any') + version spec */
|
||||
const char *bestver = NULL;
|
||||
for (int i = 0; i < variants->count; i++) {
|
||||
JsonValue *v = json_at(variants, i);
|
||||
if (!v || v->type != JSON_OBJECT) continue;
|
||||
const char *vos = json_str(v, "os"); if (!vos) continue;
|
||||
if (strcmp(vos, osn) != 0 && strcmp(vos, "any") != 0) continue;
|
||||
const char *ver = json_str(v, "version"); if (!ver) continue;
|
||||
if (spec && *spec && !spec_match(ver, spec)) continue;
|
||||
if (!bestver || vcmp(ver, bestver) > 0) { bestver = ver; chosen = ver;
|
||||
dl = json_str(v, "url"); file = json_str(v, "file");
|
||||
const char *s = json_str(v, "sha256"); free(want_sha); want_sha = s ? strdup(s) : NULL; }
|
||||
}
|
||||
if (!chosen) {
|
||||
fprintf(stderr, "pmm: no version of '%s' for os=%s%s%s\n", name, osn,
|
||||
(spec && *spec) ? " satisfying '" : "", (spec && *spec) ? spec : "");
|
||||
json_free(meta); mirrors_free(ml); return -1;
|
||||
}
|
||||
printf("pmm: selected %s@%s (os=%s)\n", name, chosen, osn);
|
||||
} else {
|
||||
/* legacy single-platform entry */
|
||||
dl = json_str(meta, "url"); file = json_str(meta, "file");
|
||||
const char *s = json_str(meta, "sha256"); want_sha = s ? strdup(s) : NULL;
|
||||
printf("pmm: selected %s@%s (os=%s)\n", name, json_str(meta, "version"), osn);
|
||||
}
|
||||
if (!dl) {
|
||||
fprintf(stderr, "pmm: registry entry for '%s' has no url\n", name);
|
||||
json_free(meta); mirrors_free(ml); return -1;
|
||||
}
|
||||
char *url_cp = strdup(dl);
|
||||
char *file_cp = file ? strdup(file) : strdup(base_name(url_cp));
|
||||
json_free(meta);
|
||||
mirrors_free(ml);
|
||||
|
||||
int rc = install_file(url_cp, file_cp);
|
||||
|
||||
if (want_sha && rc == 0) {
|
||||
char cache[1024], path[1200], hex[128];
|
||||
pmm_cache_dir(cache, sizeof(cache));
|
||||
snprintf(path, sizeof(path), "%s/%s", cache, file_cp);
|
||||
if (pmm_sha256_file(path, hex) == 0) {
|
||||
if (strcasecmp(want_sha, hex) != 0) {
|
||||
fprintf(stderr, "pmm: CHECKSUM MISMATCH (%s registry entry)!\n"
|
||||
" expected: %s\n actual: %s\n", name, want_sha, hex);
|
||||
remove(path);
|
||||
rc = -1;
|
||||
} else {
|
||||
printf("pmm: registry sha256 OK: %s\n", hex);
|
||||
}
|
||||
}
|
||||
}
|
||||
free(url_cp); free(file_cp); free(want_sha);
|
||||
return rc;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/* install.h */
|
||||
#ifndef PMM_INSTALL_H
|
||||
#define PMM_INSTALL_H
|
||||
|
||||
/* Download `url` into cache and install it according to file type.
|
||||
* name is the asset file name used to decide the install method. */
|
||||
int install_file(const char *url, const char *name);
|
||||
|
||||
/* Install from a remote registry (apt-style multi-mirror fallback):
|
||||
* looks up package `name` in the configured registry mirrors by priority.
|
||||
* If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise
|
||||
* the latest pointer {registry}/{name}.json.
|
||||
* mirror_active is the name of the explicitly selected mirror, or NULL. */
|
||||
int install_from_registry(const char *name, const char *version, const char *mirror_active);
|
||||
|
||||
#endif
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/* json.c - minimal recursive-descent JSON parser (no external deps) */
|
||||
#include "json.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
typedef struct { const char *p; const char *end; } Parser;
|
||||
|
||||
static JsonValue *parse_value(Parser *ps);
|
||||
|
||||
static JsonValue *jnew(JsonType t) {
|
||||
JsonValue *v = calloc(1, sizeof(JsonValue));
|
||||
if (!v) { fprintf(stderr, "pmm: out of memory\n"); exit(1); }
|
||||
v->type = t;
|
||||
return v;
|
||||
}
|
||||
|
||||
void json_free(JsonValue *v) {
|
||||
if (!v) return;
|
||||
free(v->string);
|
||||
for (int i = 0; i < v->count; i++) {
|
||||
if (v->keys) free(v->keys[i]);
|
||||
json_free(v->items[i]);
|
||||
}
|
||||
free(v->keys);
|
||||
free(v->items);
|
||||
free(v);
|
||||
}
|
||||
|
||||
static void jadd(JsonValue *v, char *key, JsonValue *item) {
|
||||
if (v->count >= v->capacity) {
|
||||
v->capacity = v->capacity ? v->capacity * 2 : 8;
|
||||
v->items = realloc(v->items, v->capacity * sizeof(JsonValue *));
|
||||
if (v->type == JSON_OBJECT)
|
||||
v->keys = realloc(v->keys, v->capacity * sizeof(char *));
|
||||
if (!v->items || (v->type == JSON_OBJECT && !v->keys)) {
|
||||
fprintf(stderr, "pmm: out of memory\n"); exit(1);
|
||||
}
|
||||
}
|
||||
if (v->type == JSON_OBJECT) v->keys[v->count] = key;
|
||||
v->items[v->count++] = item;
|
||||
}
|
||||
|
||||
static void skip_ws(Parser *ps) {
|
||||
while (ps->p < ps->end && isspace((unsigned char)*ps->p)) ps->p++;
|
||||
}
|
||||
|
||||
static void fail(const char *msg) {
|
||||
fprintf(stderr, "pmm: JSON parse error: %s\n", msg);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static char *parse_string_raw(Parser *ps) {
|
||||
if (ps->p >= ps->end || *ps->p != '"') fail("expected string");
|
||||
ps->p++;
|
||||
size_t cap = 16, len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) fail("out of memory");
|
||||
while (ps->p < ps->end && *ps->p != '"') {
|
||||
char c = *ps->p++;
|
||||
char out[4]; int n = 1;
|
||||
if (c == '\\') {
|
||||
if (ps->p >= ps->end) fail("bad escape");
|
||||
char e = *ps->p++;
|
||||
switch (e) {
|
||||
case '"': out[0] = '"'; break;
|
||||
case '\\': out[0] = '\\'; break;
|
||||
case '/': out[0] = '/'; break;
|
||||
case 'b': out[0] = '\b'; break;
|
||||
case 'f': out[0] = '\f'; break;
|
||||
case 'n': out[0] = '\n'; break;
|
||||
case 'r': out[0] = '\r'; break;
|
||||
case 't': out[0] = '\t'; break;
|
||||
case 'u': {
|
||||
if (ps->end - ps->p < 4) fail("bad \\u escape");
|
||||
unsigned cp = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
char h = *ps->p++;
|
||||
cp <<= 4;
|
||||
if (h >= '0' && h <= '9') cp |= (unsigned)(h - '0');
|
||||
else if (h >= 'a' && h <= 'f') cp |= (unsigned)(h - 'a' + 10);
|
||||
else if (h >= 'A' && h <= 'F') cp |= (unsigned)(h - 'A' + 10);
|
||||
else fail("bad \\u escape");
|
||||
}
|
||||
/* UTF-8 encode (surrogate pairs not combined: good enough for names/URLs) */
|
||||
if (cp < 0x80) { out[0] = (char)cp; n = 1; }
|
||||
else if (cp < 0x800) { out[0] = (char)(0xC0 | (cp >> 6)); out[1] = (char)(0x80 | (cp & 0x3F)); n = 2; }
|
||||
else { out[0] = (char)(0xE0 | (cp >> 12)); out[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); out[2] = (char)(0x80 | (cp & 0x3F)); n = 3; }
|
||||
break;
|
||||
}
|
||||
default: fail("bad escape");
|
||||
}
|
||||
} else {
|
||||
out[0] = c;
|
||||
}
|
||||
if (len + (size_t)n + 1 > cap) {
|
||||
cap *= 2;
|
||||
buf = realloc(buf, cap);
|
||||
if (!buf) fail("out of memory");
|
||||
}
|
||||
memcpy(buf + len, out, (size_t)n);
|
||||
len += (size_t)n;
|
||||
}
|
||||
if (ps->p >= ps->end) fail("unterminated string");
|
||||
ps->p++; /* closing quote */
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static JsonValue *parse_number(Parser *ps) {
|
||||
char *endp;
|
||||
double d = strtod(ps->p, &endp);
|
||||
if (endp == ps->p) fail("bad number");
|
||||
ps->p = endp;
|
||||
JsonValue *v = jnew(JSON_NUMBER);
|
||||
v->number = d;
|
||||
return v;
|
||||
}
|
||||
|
||||
static JsonValue *parse_value(Parser *ps) {
|
||||
skip_ws(ps);
|
||||
if (ps->p >= ps->end) fail("unexpected end");
|
||||
char c = *ps->p;
|
||||
if (c == '{') {
|
||||
ps->p++;
|
||||
JsonValue *v = jnew(JSON_OBJECT);
|
||||
skip_ws(ps);
|
||||
if (ps->p < ps->end && *ps->p == '}') { ps->p++; return v; }
|
||||
for (;;) {
|
||||
skip_ws(ps);
|
||||
char *key = parse_string_raw(ps);
|
||||
skip_ws(ps);
|
||||
if (ps->p >= ps->end || *ps->p != ':') fail("expected ':'");
|
||||
ps->p++;
|
||||
JsonValue *item = parse_value(ps);
|
||||
jadd(v, key, item);
|
||||
skip_ws(ps);
|
||||
if (ps->p < ps->end && *ps->p == ',') { ps->p++; continue; }
|
||||
if (ps->p < ps->end && *ps->p == '}') { ps->p++; return v; }
|
||||
fail("expected ',' or '}'");
|
||||
}
|
||||
} else if (c == '[') {
|
||||
ps->p++;
|
||||
JsonValue *v = jnew(JSON_ARRAY);
|
||||
skip_ws(ps);
|
||||
if (ps->p < ps->end && *ps->p == ']') { ps->p++; return v; }
|
||||
for (;;) {
|
||||
JsonValue *item = parse_value(ps);
|
||||
jadd(v, NULL, item);
|
||||
skip_ws(ps);
|
||||
if (ps->p < ps->end && *ps->p == ',') { ps->p++; continue; }
|
||||
if (ps->p < ps->end && *ps->p == ']') { ps->p++; return v; }
|
||||
fail("expected ',' or ']'");
|
||||
}
|
||||
} else if (c == '"') {
|
||||
JsonValue *v = jnew(JSON_STRING);
|
||||
v->string = parse_string_raw(ps);
|
||||
return v;
|
||||
} else if (c == 't') {
|
||||
if (ps->end - ps->p < 4 || strncmp(ps->p, "true", 4)) fail("bad literal");
|
||||
ps->p += 4;
|
||||
JsonValue *v = jnew(JSON_BOOL); v->boolean = 1; return v;
|
||||
} else if (c == 'f') {
|
||||
if (ps->end - ps->p < 5 || strncmp(ps->p, "false", 5)) fail("bad literal");
|
||||
ps->p += 5;
|
||||
JsonValue *v = jnew(JSON_BOOL); return v;
|
||||
} else if (c == 'n') {
|
||||
if (ps->end - ps->p < 4 || strncmp(ps->p, "null", 4)) fail("bad literal");
|
||||
ps->p += 4;
|
||||
return jnew(JSON_NULL);
|
||||
} else {
|
||||
return parse_number(ps);
|
||||
}
|
||||
}
|
||||
|
||||
JsonValue *json_parse(const char *text) {
|
||||
Parser ps = { text, text + strlen(text) };
|
||||
JsonValue *v = parse_value(&ps);
|
||||
return v;
|
||||
}
|
||||
|
||||
char *read_whole_file(const char *path, long *out_len) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len < 0) { fclose(f); return NULL; }
|
||||
char *buf = malloc((size_t)len + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
size_t rd = fread(buf, 1, (size_t)len, f);
|
||||
fclose(f);
|
||||
buf[rd] = '\0';
|
||||
if (out_len) *out_len = (long)rd;
|
||||
return buf;
|
||||
}
|
||||
|
||||
JsonValue *json_parse_file(const char *path) {
|
||||
char *text = read_whole_file(path, NULL);
|
||||
if (!text) return NULL;
|
||||
JsonValue *v = json_parse(text);
|
||||
free(text);
|
||||
return v;
|
||||
}
|
||||
|
||||
JsonValue *json_get(const JsonValue *obj, const char *key) {
|
||||
if (!obj || obj->type != JSON_OBJECT) return NULL;
|
||||
for (int i = 0; i < obj->count; i++)
|
||||
if (strcmp(obj->keys[i], key) == 0) return obj->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JsonValue *json_at(const JsonValue *arr, int i) {
|
||||
if (!arr || arr->type != JSON_ARRAY || i < 0 || i >= arr->count) return NULL;
|
||||
return arr->items[i];
|
||||
}
|
||||
|
||||
const char *json_str(const JsonValue *obj, const char *key) {
|
||||
JsonValue *v = json_get(obj, key);
|
||||
return (v && v->type == JSON_STRING) ? v->string : NULL;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/* json.h - minimal JSON parser for PMM */
|
||||
#ifndef PMM_JSON_H
|
||||
#define PMM_JSON_H
|
||||
|
||||
typedef enum {
|
||||
JSON_NULL, JSON_BOOL, JSON_NUMBER, JSON_STRING, JSON_ARRAY, JSON_OBJECT
|
||||
} JsonType;
|
||||
|
||||
typedef struct JsonValue JsonValue;
|
||||
struct JsonValue {
|
||||
JsonType type;
|
||||
int boolean;
|
||||
double number;
|
||||
char *string; /* decoded string (JSON_STRING) */
|
||||
JsonValue **items; /* array/object members (values) */
|
||||
char **keys; /* object member keys */
|
||||
int count;
|
||||
int capacity;
|
||||
};
|
||||
|
||||
JsonValue *json_parse(const char *text);
|
||||
JsonValue *json_parse_file(const char *path);
|
||||
void json_free(JsonValue *v);
|
||||
/* Object member lookup by key; returns NULL if missing or not an object. */
|
||||
JsonValue *json_get(const JsonValue *obj, const char *key);
|
||||
/* Array index; returns NULL if out of range. */
|
||||
JsonValue *json_at(const JsonValue *arr, int i);
|
||||
/* Convenience: nested get, e.g. json_path(v, "assets", 0-ish) not supported; use chain. */
|
||||
const char *json_str(const JsonValue *obj, const char *key); /* NULL if absent */
|
||||
|
||||
#endif
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
/* main.c - ParlzPackageManger (PMM)
|
||||
*
|
||||
* Usage:
|
||||
* pmm install <pkg> install from configured registry
|
||||
* pmm install --git <owner/repo> install latest release asset for this OS
|
||||
* pmm install --git <url> full URL: github/gitlab/gitea/forgejo
|
||||
* pmm install --host <host> --git <slug> force host (github|gitlab|gitea|forgejo)
|
||||
* pmm mirror list list configured mirrors
|
||||
* pmm mirror add <name> <api-url> add a mirror (writes mirror.ini)
|
||||
* pmm mirror remove <name> remove a mirror
|
||||
* pmm mirror use <name> set active mirror
|
||||
* pmm list list installed packages
|
||||
* pmm version | help
|
||||
*
|
||||
* Config: ~/.pmm/pmm.json | pmm.ini | pmm.conf (first found wins)
|
||||
* Mirrors: ~/.pmm/mirror.ini | mirror.conf
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pmm.h"
|
||||
#include "json.h"
|
||||
#include "ini.h"
|
||||
#include "http.h"
|
||||
#include "repo.h"
|
||||
#include "install.h"
|
||||
#include "pdm.h"
|
||||
|
||||
typedef struct {
|
||||
char *registry_url; /* package registry base URL */
|
||||
char *mirror_name; /* active mirror name ("" = none) */
|
||||
} PmmConfig;
|
||||
|
||||
typedef struct {
|
||||
char *name; /* active mirror name or NULL */
|
||||
char *api_base; /* mirror API base, NULL = use default host URLs */
|
||||
} MirrorSel;
|
||||
|
||||
static char *xstrdup(const char *s) { return s ? strdup(s) : NULL; }
|
||||
|
||||
/* ---------- config loading (json / ini / conf) ---------- */
|
||||
|
||||
static void load_config(PmmConfig *cfg) {
|
||||
char dir[1024];
|
||||
pmm_config_dir(dir, sizeof(dir));
|
||||
memset(cfg, 0, sizeof(*cfg));
|
||||
char *path = pmm_find_config(dir, "pmm");
|
||||
if (!path) return;
|
||||
if (strstr(path, ".json")) {
|
||||
JsonValue *root = json_parse_file(path);
|
||||
if (root) {
|
||||
const char *u = json_str(root, "registry");
|
||||
if (u) cfg->registry_url = xstrdup(u);
|
||||
const char *m = json_str(root, "mirror");
|
||||
if (m) cfg->mirror_name = xstrdup(m);
|
||||
json_free(root);
|
||||
}
|
||||
} else {
|
||||
Ini *ini = ini_load(path);
|
||||
if (ini) {
|
||||
const char *u = ini_get(ini, NULL, "registry");
|
||||
if (!u) u = ini_get(ini, "pmm", "registry");
|
||||
if (u) cfg->registry_url = xstrdup(u);
|
||||
const char *m = ini_get(ini, NULL, "mirror");
|
||||
if (!m) m = ini_get(ini, "pmm", "mirror");
|
||||
if (m) cfg->mirror_name = xstrdup(m);
|
||||
ini_free(ini);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char *mirror_section_name(void) {
|
||||
#if defined(_WIN32)
|
||||
return pmm_os_name(pmm_detect_os()); /* per-OS section falls back below */
|
||||
#else
|
||||
return pmm_os_name(pmm_detect_os());
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Load mirrors from mirror.ini / mirror.conf; pick active mirror (by cfg
|
||||
* name, first "default=true", or first entry). Format:
|
||||
* [name]
|
||||
* api = https://mirror.example.com (rewrites github api; or gitea api base)
|
||||
* default = true|false
|
||||
*/
|
||||
static void load_mirror(PmmConfig *cfg, MirrorSel *sel) {
|
||||
char dir[1024];
|
||||
pmm_config_dir(dir, sizeof(dir));
|
||||
memset(sel, 0, sizeof(*sel));
|
||||
char *path = pmm_find_config(dir, "mirror");
|
||||
if (!path) return;
|
||||
Ini *ini = ini_load(path);
|
||||
if (!ini) return;
|
||||
|
||||
/* discover section names (order of appearance) */
|
||||
const char *first = NULL, *chosen = NULL;
|
||||
for (IniEntry *e = ini->head; e; e = e->next) {
|
||||
if (!*e->section) continue;
|
||||
if (!first) first = e->section;
|
||||
const char *dflt = ini_get(ini, e->section, "default");
|
||||
if (dflt && (strcmp(dflt, "true") == 0 || strcmp(dflt, "1") == 0) && !chosen)
|
||||
chosen = e->section;
|
||||
}
|
||||
const char *want = (cfg->mirror_name && *cfg->mirror_name) ? cfg->mirror_name : chosen;
|
||||
if (!want) want = first;
|
||||
if (!want) { ini_free(ini); return; }
|
||||
|
||||
sel->name = xstrdup(want);
|
||||
const char *api = ini_get(ini, want, "api");
|
||||
if (!api) api = ini_get(ini, mirror_section_name(), "api");
|
||||
sel->api_base = xstrdup(api);
|
||||
ini_free(ini);
|
||||
}
|
||||
|
||||
/* ---------- commands ---------- */
|
||||
|
||||
static const char *host_name(PmmHost h) {
|
||||
switch (h) {
|
||||
case HOST_GITHUB: return "github";
|
||||
case HOST_GITLAB: return "gitlab";
|
||||
case HOST_GITEA: return "gitea";
|
||||
case HOST_FORGEJO: return "forgejo";
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
static PmmHost host_from_name(const char *h) {
|
||||
if (strcmp(h, "github") == 0) return HOST_GITHUB;
|
||||
if (strcmp(h, "gitlab") == 0) return HOST_GITLAB;
|
||||
if (strcmp(h, "gitea") == 0) return HOST_GITEA;
|
||||
if (strcmp(h, "forgejo") == 0) return HOST_FORGEJO;
|
||||
return HOST_UNKNOWN;
|
||||
}
|
||||
|
||||
/* <flag> <repo-ish> [--host <name>] */
|
||||
static int cmd_install_git(int argc, char **argv, const char *flag) {
|
||||
const char *repo = NULL;
|
||||
PmmHost host = HOST_AUTO;
|
||||
PmmConfig cfg; MirrorSel mirror;
|
||||
load_config(&cfg);
|
||||
load_mirror(&cfg, &mirror);
|
||||
|
||||
if (strcmp(flag, "github") == 0) host = HOST_GITHUB;
|
||||
else if (strcmp(flag, "gitlab") == 0) host = HOST_GITLAB;
|
||||
else if (strcmp(flag, "gitea") == 0) host = HOST_GITEA;
|
||||
else if (strcmp(flag, "forgejo") == 0) host = HOST_FORGEJO;
|
||||
|
||||
for (int i = 0; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
||||
host = host_from_name(argv[++i]);
|
||||
if (host == HOST_UNKNOWN) {
|
||||
fprintf(stderr, "pmm: unknown host '%s'\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
} else if (!repo) {
|
||||
repo = argv[i];
|
||||
}
|
||||
}
|
||||
if (!repo) {
|
||||
fprintf(stderr,
|
||||
"pmm: usage:\n"
|
||||
" pmm install --git <repo-url.git> any git host (auto-detected API)\n"
|
||||
" pmm install --github <owner/repo> GitHub\n"
|
||||
" pmm install --gitlab <owner/repo> GitLab\n"
|
||||
" pmm install --gitea <url/owner/repo> Gitea / Forgejo (incl. self-hosted)\n"
|
||||
" [--host github|gitlab|gitea|forgejo] force host type\n");
|
||||
return 1;
|
||||
}
|
||||
if (host == HOST_AUTO) host = repo_detect_host(repo);
|
||||
|
||||
RepoContext *ctx = repo_open(host, repo, mirror.api_base);
|
||||
if (!ctx) return 1;
|
||||
printf("pmm: host=%s repo=%s%s%s\n", host_name(host), repo,
|
||||
mirror.name ? " mirror=" : "", mirror.name ? mirror.name : "");
|
||||
|
||||
PmmOS os = pmm_detect_os();
|
||||
ReleaseAsset *asset = repo_latest_asset(ctx, os);
|
||||
if (!asset && host == HOST_AUTO)
|
||||
fprintf(stderr, "pmm: could not reach a known release API for %s "
|
||||
"(tried gitea/gitlab/github shapes)\n", repo);
|
||||
repo_close(ctx);
|
||||
if (!asset) {
|
||||
if (host != HOST_AUTO)
|
||||
fprintf(stderr, "pmm: no suitable %s asset found in latest release of %s\n",
|
||||
pmm_os_name(os), repo);
|
||||
return 1;
|
||||
}
|
||||
printf("pmm: %s -> %s (tag %s)\n", pmm_os_name(os), asset->name, asset->tag);
|
||||
int rc = install_file(asset->url, asset->name);
|
||||
repo_asset_free(asset);
|
||||
return rc == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
static int cmd_mirror(int argc, char **argv) {
|
||||
char dir[1024], path[1200];
|
||||
pmm_config_dir(dir, sizeof(dir));
|
||||
snprintf(path, sizeof(path), "%s/mirror.ini", dir);
|
||||
|
||||
if (argc == 0 || strcmp(argv[0], "list") == 0) {
|
||||
Ini *ini = ini_load(path);
|
||||
if (!ini || !ini->head) { printf("(no mirrors configured)\n"); ini_free(ini); return 0; }
|
||||
printf("mirrors in %s:\n", path);
|
||||
/* one line per unique section */
|
||||
char lastsec[512] = "";
|
||||
for (IniEntry *e = ini->head; e; e = e->next) {
|
||||
if (!*e->section) continue;
|
||||
if (strcmp(e->section, lastsec) == 0) continue;
|
||||
strncpy(lastsec, e->section, sizeof(lastsec) - 1);
|
||||
const char *api = ini_get(ini, e->section, "api");
|
||||
const char *reg = ini_get(ini, e->section, "registry");
|
||||
const char *dl = ini_get(ini, e->section, "download");
|
||||
const char *dflt = ini_get(ini, e->section, "default");
|
||||
printf(" [%s] registry=%s api=%s download=%s%s\n", e->section,
|
||||
reg ? reg : "-", api ? api : "-", dl ? dl : "-",
|
||||
dflt && strcmp(dflt, "true") == 0 ? " (default)" : "");
|
||||
}
|
||||
ini_free(ini);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[0], "add") == 0 && argc >= 3) {
|
||||
FILE *f = fopen(path, "a");
|
||||
if (!f) { fprintf(stderr, "pmm: cannot write %s\n", path); return 1; }
|
||||
fprintf(f, "\n[%s]\napi = %s\n", argv[1], argv[2]);
|
||||
fclose(f);
|
||||
printf("pmm: mirror '%s' added\n", argv[1]);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[0], "use") == 0 && argc >= 2) {
|
||||
/* set mirror=<name> in pmm.conf (always writable text format) */
|
||||
char cfgpath[1200];
|
||||
snprintf(cfgpath, sizeof(cfgpath), "%s/pmm.conf", dir);
|
||||
FILE *rf = fopen(cfgpath, "r");
|
||||
char lines[256][1024]; int n = 0;
|
||||
if (rf) {
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), rf) && n < 256) {
|
||||
if (strncmp(line, "mirror", 6) != 0) strncpy(lines[n++], line, sizeof(lines[0]) - 1);
|
||||
}
|
||||
fclose(rf);
|
||||
}
|
||||
FILE *wf = fopen(cfgpath, "w");
|
||||
if (!wf) { fprintf(stderr, "pmm: cannot write %s\n", cfgpath); return 1; }
|
||||
int wrote = 0;
|
||||
for (int i = 0; i < n; i++) fputs(lines[i], wf);
|
||||
fprintf(wf, "mirror = %s\n", argv[1]);
|
||||
(void)wrote;
|
||||
fclose(wf);
|
||||
printf("pmm: active mirror set to '%s'\n", argv[1]);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[0], "remove") == 0 && argc >= 2) {
|
||||
Ini *ini = ini_load(path);
|
||||
if (!ini) { fprintf(stderr, "pmm: no mirror file\n"); return 1; }
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) { fprintf(stderr, "pmm: cannot write %s\n", path); ini_free(ini); return 1; }
|
||||
char cursec[512] = "";
|
||||
for (IniEntry *e = ini->head; e; e = e->next) {
|
||||
if (strcmp(e->section, cursec) != 0) {
|
||||
strncpy(cursec, e->section, sizeof(cursec) - 1);
|
||||
if (strcmp(e->section, argv[1]) != 0)
|
||||
fprintf(f, "\n[%s]\n", e->section);
|
||||
}
|
||||
if (strcmp(e->section, argv[1]) != 0)
|
||||
fprintf(f, "%s = %s\n", e->key, e->value);
|
||||
}
|
||||
fclose(f);
|
||||
ini_free(ini);
|
||||
printf("pmm: mirror '%s' removed\n", argv[1]);
|
||||
return 0;
|
||||
}
|
||||
fprintf(stderr, "pmm: usage: pmm mirror list|add|use|remove ...\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cmd_list(void) {
|
||||
char dir[1024];
|
||||
pmm_install_dir(dir, sizeof(dir));
|
||||
printf("install dir: %s\n", dir);
|
||||
#ifdef _WIN32
|
||||
char cmd[1200];
|
||||
snprintf(cmd, sizeof(cmd), "dir /b \"%s\" 2>nul", dir);
|
||||
#else
|
||||
char cmd[1200];
|
||||
snprintf(cmd, sizeof(cmd), "ls -1 \"%s\" 2>/dev/null", dir);
|
||||
#endif
|
||||
return system(cmd) == 0 ? 0 : 0;
|
||||
}
|
||||
|
||||
static void print_help(void) {
|
||||
printf("ParlzPackageManger (PMM) v%s\n\n", PMM_VERSION);
|
||||
printf("usage:\n");
|
||||
printf(" pmm install <pkg> install from registry mirrors (apt-style)\n");
|
||||
printf(" pmm install <file.pdm> install a local .pdm package\n");
|
||||
printf(" pmm pack <dir> [out.pdm] pack a folder into a .pdm\n");
|
||||
printf(" pmm pdm install|list|remove|info ... manage .pdm packages\n");
|
||||
printf(" pmm install --git <repo-url.git> any git host, API auto-detected\n");
|
||||
printf(" pmm install --github <owner/repo> GitHub latest release\n");
|
||||
printf(" pmm install --gitlab <owner/repo> GitLab latest release\n");
|
||||
printf(" pmm install --gitea <url/owner/repo> Gitea/Forgejo (incl. self-hosted)\n");
|
||||
printf(" [--host github|gitlab|gitea|forgejo]\n");
|
||||
printf(" pmm mirror list|add <n> <api>|use <n>|remove <n>\n");
|
||||
printf(" pmm list list installed files\n");
|
||||
printf(" pmm version | help\n\n");
|
||||
printf("options:\n");
|
||||
printf(" -p<drive> install under <DRIVE>:\\\\.pmm (e.g. -pd -> D:\\\\.pmm, -pc -> C:\\\\.pmm)\n");
|
||||
printf(" 第一个 -p 会被记住,后续命令无需再写;用 -pc 切回 C 盘\n\n");
|
||||
printf("config: <base>/pmm.json | pmm.ini | pmm.conf (base = <drive>:\\\\.pmm 或 ~/.pmm)\n");
|
||||
printf("mirrors: <base>/mirror.ini | mirror.conf\n");
|
||||
printf("asset mapping: windows=exe/msi/zip/7z linux=deb/rpm/appimage/tar.* macos=dmg/pkg\n");
|
||||
}
|
||||
|
||||
/* Parse "-p<drive>" / "-p <drive>" flags (e.g. -pd -> D:\.pmm) anywhere in
|
||||
* argv, record the drive, and compact the argument list so subcommands don't
|
||||
* see the flag. Returns new argc. */
|
||||
static int consume_drive_flag(int argc, char **argv) {
|
||||
int w = 1; /* argv[0] is the program name */
|
||||
for (int r = 1; r < argc; r++) {
|
||||
const char *a = argv[r];
|
||||
if (a[0] == '-' && a[1] == 'p' && (a[2] >= 'A' && a[2] <= 'Z' || a[2] >= 'a' && a[2] <= 'z')) {
|
||||
char dr[2] = { a[2], '\0' };
|
||||
pmm_set_install_drive(dr);
|
||||
continue;
|
||||
}
|
||||
if (strcmp(a, "-p") == 0 && r + 1 < argc) {
|
||||
const char *nxt = argv[r + 1];
|
||||
if (nxt[0] && (nxt[0] >= 'A' && nxt[0] <= 'Z' || nxt[0] >= 'a' && nxt[0] <= 'z') && nxt[1] == '\0') {
|
||||
char dr[2] = { nxt[0], '\0' };
|
||||
pmm_set_install_drive(dr);
|
||||
r++; /* consume the drive arg too */
|
||||
continue;
|
||||
}
|
||||
}
|
||||
argv[w++] = argv[r];
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { print_help(); return 0; }
|
||||
pmm_set_self_path(argv[0]);
|
||||
argc = consume_drive_flag(argc, argv);
|
||||
if (argc < 2) { print_help(); return 0; }
|
||||
|
||||
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
|
||||
print_help(); return 0;
|
||||
}
|
||||
if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "--version") == 0) {
|
||||
printf("pmm %s (%s)\n", PMM_VERSION, pmm_os_name(pmm_detect_os()));
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[1], "list") == 0) return cmd_list();
|
||||
if (strcmp(argv[1], "mirror") == 0) return cmd_mirror(argc - 2, argv + 2);
|
||||
/* pmm remove <pkg> (also used by the Windows "Uninstall" registration) */
|
||||
if (strcmp(argv[1], "remove") == 0) {
|
||||
if (argc < 3) { fprintf(stderr, "pmm: usage: pmm remove <pkg>\n"); return 1; }
|
||||
return pdm_remove(argv[2]) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "install") == 0) {
|
||||
if (argc >= 3 && (strcmp(argv[2], "--git") == 0 || strcmp(argv[2], "--github") == 0 ||
|
||||
strcmp(argv[2], "--gitlab") == 0 || strcmp(argv[2], "--gitea") == 0 ||
|
||||
strcmp(argv[2], "--forgejo") == 0))
|
||||
return cmd_install_git(argc - 3, argv + 3, argv[2] + 2);
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "pmm: usage: pmm install <pkg|file.pdm> | pmm install --git <repo>\n");
|
||||
return 1;
|
||||
}
|
||||
/* parse package name + optional version spec:
|
||||
* pmm install nodejs==24.20.0
|
||||
* pmm install "nodejs>=24,<25"
|
||||
* pmm install nodejs -v 24.20.0 / --version ">=24,<25" */
|
||||
const char *raw_pkg = NULL, *version = NULL;
|
||||
for (int i = 2; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
if (strncmp(a, "-v", 2) == 0 && a[2]) { version = a + 2; continue; }
|
||||
if (strcmp(a, "-v") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
|
||||
if (strcmp(a, "--version") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
|
||||
if (strncmp(a, "--version=", 10) == 0) { version = a + 10; continue; }
|
||||
if (!raw_pkg && a[0] != '-') { raw_pkg = a; continue; }
|
||||
}
|
||||
if (!raw_pkg) raw_pkg = argv[2];
|
||||
/* split "nodejs>=24,<25" -> pkg="nodejs", spec=">=24,<25"; else pkg only */
|
||||
char pkg[256], spec[256];
|
||||
if (version && *version) {
|
||||
strncpy(pkg, raw_pkg, sizeof(pkg) - 1);
|
||||
strncpy(spec, version, sizeof(spec) - 1);
|
||||
} else {
|
||||
const char *ops[] = { ">=", "<=", "==", "!=", ">", "<", NULL };
|
||||
const char *pos = NULL;
|
||||
for (int i = 0; ops[i]; i++) { pos = strstr(raw_pkg, ops[i]); if (pos) break; }
|
||||
if (pos) {
|
||||
size_t n = (size_t)(pos - raw_pkg);
|
||||
if (n >= sizeof(pkg)) n = sizeof(pkg) - 1;
|
||||
memcpy(pkg, raw_pkg, n); pkg[n] = '\0';
|
||||
strncpy(spec, pos, sizeof(spec) - 1);
|
||||
} else {
|
||||
strncpy(pkg, raw_pkg, sizeof(pkg) - 1);
|
||||
spec[0] = '\0';
|
||||
}
|
||||
}
|
||||
pkg[sizeof(pkg) - 1] = '\0'; spec[sizeof(spec) - 1] = '\0';
|
||||
/* local .pdm file? */
|
||||
size_t la = strlen(pkg);
|
||||
if (la > 4 && (strcmp(pkg + la - 4, ".pdm") == 0 || strcmp(pkg + la - 4, ".PDM") == 0))
|
||||
return pdm_install_file(pkg) == 0 ? 0 : 1;
|
||||
PmmConfig cfg; MirrorSel mirror;
|
||||
load_config(&cfg);
|
||||
load_mirror(&cfg, &mirror);
|
||||
/* apt-style: mirrors.first-wins by priority, active mirror if named */
|
||||
int rc = install_from_registry(pkg, spec[0] ? spec : NULL, mirror.name);
|
||||
free(cfg.registry_url); free(cfg.mirror_name);
|
||||
free(mirror.name); free(mirror.api_base);
|
||||
return rc == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "pack") == 0) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "pmm: usage: pmm pack <dir> [output.pdm]\n");
|
||||
return 1;
|
||||
}
|
||||
return pdm_pack(argv[2], argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "pdm") == 0) {
|
||||
/* pmm pdm install/list/remove/info (sugar for the standalone pdm tool) */
|
||||
if (argc >= 3 && strcmp(argv[2], "install") == 0)
|
||||
return pdm_install_file(argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
|
||||
if (argc >= 3 && strcmp(argv[2], "list") == 0) { pdm_list_installed(); return 0; }
|
||||
if (argc >= 3 && strcmp(argv[2], "remove") == 0)
|
||||
return pdm_remove(argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
|
||||
if (argc >= 3 && strcmp(argv[2], "info") == 0)
|
||||
return pdm_info(argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
|
||||
fprintf(stderr, "pmm: usage: pmm pdm install|list|remove|info ...\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "pmm: unknown command '%s' (try 'pmm help')\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/* mirrors.c - apt-style mirror handling
|
||||
*
|
||||
* mirror.ini / mirror.conf format:
|
||||
* [name]
|
||||
* api = https://mirror.example.com # replaces git-host API base
|
||||
* download = https://ghfast.example # gh-proxy style download prefix
|
||||
* registry = https://mirror.example/pmm # registry mirror ({base}/{pkg}.json)
|
||||
* priority = 1 # lower tried first (apt-like order)
|
||||
*/
|
||||
#include "mirrors.h"
|
||||
#include "ini.h"
|
||||
#include "pmm.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static char *xstrdup_or(const char *s, const char *dflt) {
|
||||
return strdup(s ? s : dflt);
|
||||
}
|
||||
|
||||
static int cmp_priority(const void *a, const void *b) {
|
||||
const Mirror *ma = a, *mb = b;
|
||||
return ma->priority - mb->priority;
|
||||
}
|
||||
|
||||
MirrorList *mirrors_load(void) {
|
||||
char dir[1024];
|
||||
pmm_config_dir(dir, sizeof(dir));
|
||||
char *path = pmm_find_config(dir, "mirror");
|
||||
if (!path) return calloc(1, sizeof(MirrorList));
|
||||
Ini *ini = ini_load(path);
|
||||
MirrorList *list = calloc(1, sizeof(MirrorList));
|
||||
if (!ini) return list;
|
||||
|
||||
int cap = 0;
|
||||
char cursec[512] = "";
|
||||
for (IniEntry *e = ini->head; e; e = e->next) {
|
||||
if (strcmp(e->section, cursec) != 0) {
|
||||
strncpy(cursec, e->section, sizeof(cursec) - 1);
|
||||
/* new mirror section begins */
|
||||
if (list->count >= cap) {
|
||||
cap = cap ? cap * 2 : 8;
|
||||
list->items = realloc(list->items, cap * sizeof(Mirror));
|
||||
}
|
||||
Mirror *m = &list->items[list->count++];
|
||||
memset(m, 0, sizeof(*m));
|
||||
m->name = xstrdup_or(e->section, "");
|
||||
m->priority = 100;
|
||||
}
|
||||
Mirror *m = list->count ? &list->items[list->count - 1] : NULL;
|
||||
if (!m) continue;
|
||||
if (strcmp(e->key, "api") == 0) m->api = xstrdup_or(e->value, "");
|
||||
else if (strcmp(e->key, "download") == 0) m->download = xstrdup_or(e->value, "");
|
||||
else if (strcmp(e->key, "registry") == 0) m->registry = xstrdup_or(e->value, "");
|
||||
else if (strcmp(e->key, "priority") == 0) m->priority = atoi(e->value);
|
||||
}
|
||||
ini_free(ini);
|
||||
qsort(list->items, list->count, sizeof(Mirror), cmp_priority);
|
||||
return list;
|
||||
}
|
||||
|
||||
void mirrors_free(MirrorList *list) {
|
||||
if (!list) return;
|
||||
for (int i = 0; i < list->count; i++) {
|
||||
free(list->items[i].name);
|
||||
free(list->items[i].api);
|
||||
free(list->items[i].download);
|
||||
free(list->items[i].registry);
|
||||
}
|
||||
free(list->items);
|
||||
free(list);
|
||||
}
|
||||
|
||||
Mirror *mirrors_active(const MirrorList *list, const char *active_name) {
|
||||
if (!active_name || !*active_name) return NULL;
|
||||
for (int i = 0; i < list->count; i++)
|
||||
if (strcmp(list->items[i].name, active_name) == 0)
|
||||
return &list->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char **mirrors_download_candidates(const MirrorList *list, const char *url, int *out_n) {
|
||||
int cap = list->count + 1, n = 0;
|
||||
char **cands = malloc(cap * sizeof(char *));
|
||||
for (int i = 0; i < list->count && n < cap - 1; i++) {
|
||||
const char *dl = list->items[i].download;
|
||||
if (!dl || !*dl) continue;
|
||||
size_t len = strlen(dl) + strlen(url) + 2;
|
||||
cands[n] = malloc(len);
|
||||
/* gh-proxy convention: prefix + full original URL */
|
||||
if (dl[strlen(dl) - 1] == '=' || dl[strlen(dl) - 1] == '/')
|
||||
snprintf(cands[n], len, "%s%s", dl, url);
|
||||
else
|
||||
snprintf(cands[n], len, "%s/%s", dl, url);
|
||||
n++;
|
||||
}
|
||||
cands[n++] = strdup(url); /* direct last, like apt falling back to origin */
|
||||
*out_n = n;
|
||||
return cands;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/* mirrors.h - apt-style mirror list with priorities and download fallback */
|
||||
#ifndef PMM_MIRRORS_H
|
||||
#define PMM_MIRRORS_H
|
||||
|
||||
typedef struct Mirror {
|
||||
char *name;
|
||||
char *api; /* API base (rewrites github/gitlab/gitea API endpoints) */
|
||||
char *download; /* download prefix (gh-proxy style: prefix + original URL) */
|
||||
char *registry; /* apt-style registry mirror: serves {registry}/{pkg}.json */
|
||||
int priority; /* lower = tried first (default 100) */
|
||||
} Mirror;
|
||||
|
||||
typedef struct {
|
||||
Mirror *items;
|
||||
int count;
|
||||
} MirrorList;
|
||||
|
||||
/* Load mirrors from ~/.pmm/mirror.ini / mirror.conf, sorted by priority. */
|
||||
MirrorList *mirrors_load(void);
|
||||
void mirrors_free(MirrorList *list);
|
||||
/* Mirror whose name matches the active selection in pmm config, or NULL. */
|
||||
Mirror *mirrors_active(const MirrorList *list, const char *active_name);
|
||||
|
||||
/* Build candidate download URLs for `url` (caller frees each string and array).
|
||||
* Order: prefix-style mirrors by priority, then the original URL. */
|
||||
char **mirrors_download_candidates(const MirrorList *list, const char *url, int *out_n);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,528 @@
|
||||
/* pdm.c - .pdm pack/install/remove (deb-like, built on system tar) */
|
||||
#include "pdm.h"
|
||||
#include "pmm.h"
|
||||
#include "sha256.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <inttypes.h>
|
||||
#define PMM_MKDIR(p) _mkdir(p)
|
||||
#define RMDIR(p) _rmdir(p)
|
||||
#define PMM_POPEN_READ(cmd) _popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ(p) _pclose(p)
|
||||
#define GETCWD(p,n) (_getcwd((p),(n)))
|
||||
#define CHDIR(p) _chdir(p)
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#define PMM_MKDIR(p) mkdir((p), 0755)
|
||||
#define RMDIR(p) rmdir(p)
|
||||
#define PMM_POPEN_READ(cmd) popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ(p) pclose(p)
|
||||
#define GETCWD(p,n) getcwd((p),(n))
|
||||
#define CHDIR(p) chdir(p)
|
||||
#endif
|
||||
|
||||
/* ---------- small helpers ---------- */
|
||||
|
||||
static int ends_with(const char *s, const char *suf) {
|
||||
size_t sl = strlen(s), xl = strlen(suf);
|
||||
return sl >= xl && strcmp(s + sl - xl, suf) == 0;
|
||||
}
|
||||
|
||||
static const char *base_name(const char *path) {
|
||||
const char *a = strrchr(path, '/');
|
||||
const char *b = strrchr(path, '\\');
|
||||
const char *p = (a > b) ? a : b;
|
||||
return p ? p + 1 : path;
|
||||
}
|
||||
|
||||
static void mkdir_p(const char *path) {
|
||||
char tmp[1024];
|
||||
strncpy(tmp, path, sizeof(tmp) - 1);
|
||||
tmp[sizeof(tmp) - 1] = '\0';
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
char c = *p; *p = '\0'; PMM_MKDIR(tmp); *p = c;
|
||||
}
|
||||
}
|
||||
PMM_MKDIR(tmp);
|
||||
}
|
||||
|
||||
/* Convert backslashes to forward slashes (so cmd/bash don't treat "C:\x" as host). */
|
||||
static void rmtree(const char *dir) {
|
||||
/* Pure-C recursive delete: no shell, so no path-flavour (bsdtar/GNU) issues. */
|
||||
#ifdef _WIN32
|
||||
char patt[1200];
|
||||
snprintf(patt, sizeof(patt), "%s\\*", dir);
|
||||
struct _finddata_t fd;
|
||||
intptr_t h = _findfirst(patt, &fd);
|
||||
if (h != -1) {
|
||||
do {
|
||||
if (strcmp(fd.name, ".") == 0 || strcmp(fd.name, "..") == 0) continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s\\%s", dir, fd.name);
|
||||
if (fd.attrib & _A_SUBDIR) rmtree(full);
|
||||
else remove(full);
|
||||
} while (_findnext(h, &fd) == 0);
|
||||
_findclose(h);
|
||||
}
|
||||
RMDIR(dir);
|
||||
#else
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) rmtree(full);
|
||||
else remove(full);
|
||||
}
|
||||
closedir(d);
|
||||
RMDIR(dir);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* KEY: value lookup in control text; returns malloc'd value or NULL. */
|
||||
static char *control_get(const char *text, const char *key) {
|
||||
size_t klen = strlen(key);
|
||||
const char *line = text;
|
||||
while (line && *line) {
|
||||
const char *eol = strchr(line, '\n');
|
||||
size_t ll = eol ? (size_t)(eol - line) : strlen(line);
|
||||
if (ll > klen && strncasecmp(line, key, klen) == 0 && line[klen] == ':') {
|
||||
const char *v = line + klen + 1;
|
||||
while (v < line + ll && (*v == ' ' || *v == '\t')) v++;
|
||||
size_t vl = (size_t)(line + ll - v);
|
||||
while (vl && (v[vl-1] == '\r' || v[vl-1] == ' ')) vl--;
|
||||
char *out = malloc(vl + 1);
|
||||
memcpy(out, v, vl);
|
||||
out[vl] = '\0';
|
||||
return out;
|
||||
}
|
||||
line = eol ? eol + 1 : NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *read_file(const char *path, long *len) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
char *buf = malloc((size_t)n + 1);
|
||||
size_t rd = fread(buf, 1, (size_t)n, f);
|
||||
fclose(f);
|
||||
buf[rd] = '\0';
|
||||
if (len) *len = (long)rd;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ---------- pack ---------- */
|
||||
|
||||
int pdm_pack(const char *dir, const char *out) {
|
||||
char cp_[1100];
|
||||
snprintf(cp_, sizeof(cp_), "%s/pdm-control", dir);
|
||||
char *control = read_file(cp_, NULL);
|
||||
if (!control) {
|
||||
fprintf(stderr, "pdm: %s has no pdm-control file\n", dir);
|
||||
return -1;
|
||||
}
|
||||
char *pkg = control_get(control, "Package");
|
||||
char *ver = control_get(control, "Version");
|
||||
if (!pkg || !*pkg) {
|
||||
fprintf(stderr, "pdm: pdm-control missing 'Package:' field\n");
|
||||
free(control); return -1;
|
||||
}
|
||||
if (!ver || !*ver) ver = strdup("0.0.0");
|
||||
|
||||
/* Output defaults to <Package>_<Version>.pdm in the current directory.
|
||||
* Everything below uses paths relative to the CWD so the shell never sees a
|
||||
* Windows drive-letter path (which MSYS bash would mangle). */
|
||||
char outbuf[1100];
|
||||
if (!out) {
|
||||
snprintf(outbuf, sizeof(outbuf), "%s_%s.pdm", pkg, ver);
|
||||
out = outbuf;
|
||||
}
|
||||
|
||||
const char *stage = ".pdm-stage";
|
||||
char cmd[2800];
|
||||
rmtree(stage);
|
||||
PMM_MKDIR(stage);
|
||||
|
||||
/* control.tar.gz (contains pdm-control; packed from the source dir) */
|
||||
snprintf(cmd, sizeof(cmd), "tar -czf \"%s/control.tar.gz\" -C \"%s\" pdm-control", stage, dir);
|
||||
if (system(cmd) != 0) { fprintf(stderr, "pdm: tar (control) failed\n"); rmtree(stage); return -1; }
|
||||
|
||||
/* data.tar.gz (everything except pdm-control and the scratch dir) */
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"tar -czf \"%s/data.tar.gz\" -C \"%s\" --exclude pdm-control --exclude %s .",
|
||||
stage, dir, stage);
|
||||
if (system(cmd) != 0) { fprintf(stderr, "pdm: tar (data) failed\n"); rmtree(stage); return -1; }
|
||||
|
||||
/* sha256sums of the two members */
|
||||
char sums_path[1100], hex[128];
|
||||
snprintf(sums_path, sizeof(sums_path), "%s/sha256sums", stage);
|
||||
FILE *sf = fopen(sums_path, "w");
|
||||
if (sf) {
|
||||
char mp[1200];
|
||||
snprintf(mp, sizeof(mp), "%s/control.tar.gz", stage);
|
||||
if (pmm_sha256_file(mp, hex) == 0) fprintf(sf, "%s control.tar.gz\n", hex);
|
||||
snprintf(mp, sizeof(mp), "%s/data.tar.gz", stage);
|
||||
if (pmm_sha256_file(mp, hex) == 0) fprintf(sf, "%s data.tar.gz\n", hex);
|
||||
fclose(sf);
|
||||
}
|
||||
|
||||
/* outer archive */
|
||||
snprintf(cmd, sizeof(cmd), "tar -cf \"%s\" -C \"%s\" control.tar.gz data.tar.gz sha256sums", out, stage);
|
||||
int rc = system(cmd);
|
||||
rmtree(stage);
|
||||
if (rc != 0) { fprintf(stderr, "pdm: failed to write %s\n", out); return -1; }
|
||||
|
||||
printf("pdm: packed %s (%s %s) -> %s\n", dir, pkg, ver, out);
|
||||
free(control); free(pkg);
|
||||
if (strcmp(ver, "0.0.0") != 0) free(ver);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- install ---------- */
|
||||
|
||||
static void db_dirs(char *db, size_t dbs, char *root, size_t roots) {
|
||||
char home[900];
|
||||
pmm_config_dir(home, sizeof(home) - 32);
|
||||
snprintf(db, dbs, "%s/installed", home);
|
||||
mkdir_p(db);
|
||||
snprintf(root, roots, "%s/root", home);
|
||||
mkdir_p(root);
|
||||
}
|
||||
|
||||
static int copy_file(const char *src, const char *dst) {
|
||||
FILE *in = fopen(src, "rb");
|
||||
if (!in) return -1;
|
||||
FILE *out = fopen(dst, "wb");
|
||||
if (!out) { fclose(in); return -1; }
|
||||
char buf[65536];
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
|
||||
if (fwrite(buf, 1, n, out) != n) { fclose(in); fclose(out); return -1; }
|
||||
fclose(in);
|
||||
fclose(out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Recursively find the first .exe under dir (for the app icon); malloc'd path or NULL. */
|
||||
static char *find_first_exe(const char *dir) {
|
||||
#ifdef _WIN32
|
||||
char patt[1300];
|
||||
snprintf(patt, sizeof(patt), "%s\\*", dir);
|
||||
struct _finddata_t fd;
|
||||
intptr_t h = _findfirst(patt, &fd);
|
||||
if (h != -1) {
|
||||
do {
|
||||
if (strcmp(fd.name, ".") == 0 || strcmp(fd.name, "..") == 0) continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s\\%s", dir, fd.name);
|
||||
if ((fd.attrib & _A_SUBDIR)) {
|
||||
char *r = find_first_exe(full);
|
||||
if (r) { _findclose(h); return r; }
|
||||
} else {
|
||||
size_t l = strlen(fd.name);
|
||||
if (l > 4 && strcasecmp(fd.name + l - 4, ".exe") == 0) {
|
||||
char *r = malloc(1400);
|
||||
if (r) snprintf(r, 1400, "%s\\%s", dir, fd.name);
|
||||
_findclose(h);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
} while (_findnext(h, &fd) == 0);
|
||||
_findclose(h);
|
||||
}
|
||||
#else
|
||||
DIR *d = opendir(dir);
|
||||
if (d) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (e->d_name[0] == '.') continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) { char *r = find_first_exe(full); if (r) { closedir(d); return r; } }
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Recursively sum file sizes under dir, in bytes. */
|
||||
static unsigned long long dir_size(const char *dir) {
|
||||
unsigned long long total = 0;
|
||||
#ifdef _WIN32
|
||||
char patt[1300];
|
||||
snprintf(patt, sizeof(patt), "%s\\*", dir);
|
||||
struct _finddata_t fd;
|
||||
intptr_t h = _findfirst(patt, &fd);
|
||||
if (h != -1) {
|
||||
do {
|
||||
if (strcmp(fd.name, ".") == 0 || strcmp(fd.name, "..") == 0) continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s\\%s", dir, fd.name);
|
||||
if (fd.attrib & _A_SUBDIR) total += dir_size(full);
|
||||
else total += (unsigned long long)fd.size;
|
||||
} while (_findnext(h, &fd) == 0);
|
||||
_findclose(h);
|
||||
}
|
||||
#else
|
||||
DIR *d = opendir(dir);
|
||||
if (d) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (e->d_name[0] == '.') continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) total += dir_size(full);
|
||||
else total += (unsigned long long)st.st_size;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
#endif
|
||||
return total;
|
||||
}
|
||||
|
||||
static char saved_cwd[1400];
|
||||
static int chdir_save(const char *dir) {
|
||||
if (!GETCWD(saved_cwd, sizeof(saved_cwd))) saved_cwd[0] = '\0';
|
||||
return CHDIR(dir) == 0 ? 0 : -1;
|
||||
}
|
||||
static void chdir_restore(void) {
|
||||
if (saved_cwd[0]) CHDIR(saved_cwd);
|
||||
}
|
||||
|
||||
int pdm_info(const char *pdmfile) {
|
||||
printf("pdm: %s\n", base_name(pdmfile));
|
||||
/* Copy into the pm dir + chdir, then use relative names (works with both
|
||||
* MSYS GNU tar and System32 bsdtar). */
|
||||
char home[1024];
|
||||
pmm_config_dir(home, sizeof(home));
|
||||
char info_tmp[1400];
|
||||
snprintf(info_tmp, sizeof(info_tmp), "%s/_pmm_info.pdm", home);
|
||||
if (copy_file(pdmfile, info_tmp) != 0) return -1;
|
||||
if (chdir_save(home) != 0) { remove(info_tmp); return -1; }
|
||||
char cmd[2800];
|
||||
snprintf(cmd, sizeof(cmd), "tar -xOf _pmm_info.pdm control.tar.gz | tar -xzOf - pdm-control");
|
||||
int rc = system(cmd);
|
||||
chdir_restore();
|
||||
remove(info_tmp);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int pdm_install_file(const char *pdmfile) {
|
||||
if (!ends_with(pdmfile, ".pdm")) {
|
||||
fprintf(stderr, "pdm: '%s' is not a .pdm file\n", pdmfile);
|
||||
return -1;
|
||||
}
|
||||
char home[1024], db[1024], root[1024], cmd[2600];
|
||||
pmm_config_dir(home, sizeof(home));
|
||||
snprintf(db, sizeof(db), "%s/installed", home);
|
||||
mkdir_p(db);
|
||||
snprintf(root, sizeof(root), "%s/root", home);
|
||||
mkdir_p(root);
|
||||
|
||||
/* Copy the package into the PM dir, chdir there, and run tar with ONLY
|
||||
* relative paths. Relative paths are understood identically by MSYS GNU tar
|
||||
* and System32 bsdtar, so we don't have to guess a drive-letter style. */
|
||||
char tmpname[1400], tmprel[] = "_pmm_install.pdm";
|
||||
snprintf(tmpname, sizeof(tmpname), "%s/_pmm_install.pdm", home);
|
||||
if (copy_file(pdmfile, tmpname) != 0) {
|
||||
fprintf(stderr, "pdm: cannot read %s\n", pdmfile);
|
||||
return -1;
|
||||
}
|
||||
if (chdir_save(home) != 0) { remove(tmpname); return -1; }
|
||||
if (system(NULL) == 0) { fprintf(stderr, "pdm: cannot run tar\n"); chdir_restore(); remove(tmpname); return -1; }
|
||||
|
||||
const char *stage = "installed/.stage";
|
||||
rmtree(stage);
|
||||
PMM_MKDIR(stage);
|
||||
|
||||
/* extract members (all relative) */
|
||||
snprintf(cmd, sizeof(cmd), "tar -xf \"%s\" -C \"%s\"", tmprel, stage);
|
||||
if (system(cmd) != 0) {
|
||||
fprintf(stderr, "pdm: not a valid .pdm archive: %s\n", pdmfile);
|
||||
chdir_restore(); remove(tmpname); return -1;
|
||||
}
|
||||
|
||||
/* verify member checksums (relative to pm dir) */
|
||||
char hex[128];
|
||||
FILE *sf = fopen("installed/.stage/sha256sums", "r");
|
||||
if (sf) {
|
||||
char line[256], fname[128], expect[128];
|
||||
while (fgets(line, sizeof(line), sf)) {
|
||||
if (sscanf(line, "%127s %127s", expect, fname) != 2) continue;
|
||||
char mp[1200];
|
||||
snprintf(mp, sizeof(mp), "installed/.stage/%s", fname);
|
||||
if (pmm_sha256_file(mp, hex) != 0 || strcasecmp(hex, expect) != 0) {
|
||||
fprintf(stderr, "pdm: CHECKSUM MISMATCH for %s in %s\n", fname, pdmfile);
|
||||
fclose(sf); chdir_restore(); remove(tmpname); return -1;
|
||||
}
|
||||
}
|
||||
fclose(sf);
|
||||
printf("pdm: sha256 checksums OK\n");
|
||||
} else {
|
||||
printf("pdm: warning: no sha256sums member, skipping integrity check\n");
|
||||
}
|
||||
|
||||
/* read control */
|
||||
snprintf(cmd, sizeof(cmd), "tar -xzOf \"%s/control.tar.gz\" pdm-control", stage);
|
||||
FILE *cf = PMM_POPEN_READ(cmd);
|
||||
if (!cf) {
|
||||
fprintf(stderr, "pdm: missing control.tar.gz in %s\n", pdmfile);
|
||||
chdir_restore(); remove(tmpname); return -1;
|
||||
}
|
||||
char ctl[4096];
|
||||
size_t got = fread(ctl, 1, sizeof(ctl) - 1, cf);
|
||||
PMM_PCLOSE_READ(cf);
|
||||
ctl[got] = '\0';
|
||||
char *pkg = control_get(ctl, "Package");
|
||||
char *ver = control_get(ctl, "Version");
|
||||
if (!pkg || !*pkg) {
|
||||
fprintf(stderr, "pdm: package has no Package: field\n");
|
||||
chdir_restore(); remove(tmpname); return -1;
|
||||
}
|
||||
|
||||
/* extract data into root */
|
||||
snprintf(cmd, sizeof(cmd), "tar -xzf \"%s/data.tar.gz\" -C \"root\"", stage);
|
||||
if (system(cmd) != 0) {
|
||||
fprintf(stderr, "pdm: data extraction failed\n");
|
||||
chdir_restore(); remove(tmpname); return -1;
|
||||
}
|
||||
|
||||
/* record file list + control in db */
|
||||
snprintf(cmd, sizeof(cmd), "%s/%s.info", db, pkg);
|
||||
FILE *dbf = fopen(cmd, "wb"); /* binary: keep \n, avoid Windows CRLF translation */
|
||||
if (dbf) {
|
||||
fprintf(dbf, "Package: %s\nVersion: %s\nSource: %s\n\n%s\n",
|
||||
pkg, ver ? ver : "0.0.0", base_name(pdmfile), ctl);
|
||||
fprintf(dbf, "Files:\n");
|
||||
char ltcmd[2600];
|
||||
snprintf(ltcmd, sizeof(ltcmd), "tar -tzf \"%s/data.tar.gz\"", stage);
|
||||
FILE *tf = PMM_POPEN_READ(ltcmd);
|
||||
if (tf) {
|
||||
char ln[2048];
|
||||
while (fgets(ln, sizeof(ln), tf))
|
||||
fputs(ln, dbf);
|
||||
PMM_PCLOSE_READ(tf);
|
||||
} else {
|
||||
fprintf(stderr, "pdm: warning: could not record file list\n");
|
||||
}
|
||||
fclose(dbf);
|
||||
}
|
||||
|
||||
rmtree(stage);
|
||||
chdir_restore();
|
||||
remove(tmpname);
|
||||
printf("pdm: installed %s %s into %s\n", pkg, ver ? ver : "0.0.0", root);
|
||||
|
||||
/* register in the Windows per-user "Installed Apps" (Uninstall) hive,
|
||||
* like an MSI installer would, so it shows in Settings > Installed apps */
|
||||
{
|
||||
char rootpkg[1400];
|
||||
snprintf(rootpkg, sizeof(rootpkg), "%s/%s", root, pkg);
|
||||
char *icon = find_first_exe(rootpkg);
|
||||
const char *pub = control_get(ctl, "Maintainer");
|
||||
const char *self = pmm_self_path();
|
||||
pmm_reg_uninstall(pkg, ver ? ver : "", pub ? pub : "",
|
||||
root, icon, self && *self ? self : "pmm",
|
||||
dir_size(rootpkg));
|
||||
if (pub) free((char *)pub);
|
||||
free(icon);
|
||||
}
|
||||
|
||||
pmm_add_to_path(); /* auto-put PMM dirs (e.g. root/nodejs) on PATH */
|
||||
|
||||
#ifndef _WIN32
|
||||
/* On Linux/macOS, mark anything in <root>/bin executable. Windows doesn't
|
||||
* record unix exec bits, so ELF/scripts packaged on Windows lose +x; restore
|
||||
* them at install time so e.g. a Linux Node.js binary actually runs. */
|
||||
{
|
||||
char cx[1600];
|
||||
snprintf(cx, sizeof(cx), "chmod +x \"%s/bin\"/* 2>/dev/null || true", root);
|
||||
system(cx);
|
||||
}
|
||||
#endif
|
||||
|
||||
free(pkg); if (ver) free(ver);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- list / remove ---------- */
|
||||
|
||||
void pdm_list_installed(void) {
|
||||
char db[1024], root[1024];
|
||||
db_dirs(db, sizeof(db), root, sizeof(root));
|
||||
#ifdef _WIN32
|
||||
char cmd[1200];
|
||||
snprintf(cmd, sizeof(cmd), "dir /b \"%s\\*.info\" 2>nul", db);
|
||||
#else
|
||||
char cmd[1200];
|
||||
snprintf(cmd, sizeof(cmd), "ls -1 \"%s\"/*.info 2>/dev/null", db);
|
||||
#endif
|
||||
printf("installed .pdm packages (root: %s):\n", root);
|
||||
fflush(stdout);
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
int pdm_remove(const char *name) {
|
||||
char db[1024], root[1024];
|
||||
db_dirs(db, sizeof(db), root, sizeof(root));
|
||||
char ipath[1100];
|
||||
snprintf(ipath, sizeof(ipath), "%s/%s.info", db, name);
|
||||
long len = 0;
|
||||
char *info = read_file(ipath, &len);
|
||||
if (!info) {
|
||||
fprintf(stderr, "pdm: package '%s' is not installed\n", name);
|
||||
return -1;
|
||||
}
|
||||
char *files = strstr(info, "Files:\n");
|
||||
int n = 0;
|
||||
if (files) {
|
||||
char path[1024];
|
||||
char *line = files + 7;
|
||||
while (*line) {
|
||||
char *eol = strchr(line, '\n');
|
||||
if (!eol) eol = line + strlen(line);
|
||||
size_t ll = (size_t)(eol - line);
|
||||
while (ll && (line[ll-1] == '\r' || line[ll-1] == ' ')) ll--;
|
||||
if (ll > 0 && ll < sizeof(path)) {
|
||||
memcpy(path, line, ll); path[ll] = '\0';
|
||||
/* normalize "./x" -> "x" */
|
||||
char *p = path;
|
||||
while (p[0] == '.' && p[1] == '/') p += 2;
|
||||
char full[1200];
|
||||
snprintf(full, sizeof(full), "%s/%s", root, p);
|
||||
remove(full);
|
||||
n++;
|
||||
}
|
||||
line = *eol ? eol + 1 : eol;
|
||||
}
|
||||
}
|
||||
remove(ipath);
|
||||
free(info);
|
||||
printf("pdm: removed %s (%d files)\n", name, n);
|
||||
pmm_reg_uninstall_clear(name); /* drop it from Installed Apps */
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* pdm.h - .pdm package format (deb-like)
|
||||
*
|
||||
* A .pdm file is a tar archive with three members:
|
||||
* control.tar.gz -> contains "pdm-control" text file (Package/Version/...)
|
||||
* data.tar.gz -> the package payload, extracted into the pm root
|
||||
* sha256sums -> "<hex> control.tar.gz" lines for both members
|
||||
*
|
||||
* pdm-control fields (KEY: value lines):
|
||||
* Package: name
|
||||
* Version: 1.0.0
|
||||
* Architecture: all | windows | linux | macos
|
||||
* Description: text
|
||||
* Maintainer: text
|
||||
*
|
||||
* The folder passed to `pdm pack ./dir` must contain a "pdm-control" file;
|
||||
* everything else in the folder becomes data.tar.gz.
|
||||
*/
|
||||
#ifndef PMM_PDM_H
|
||||
#define PMM_PDM_H
|
||||
|
||||
/* Pack directory (containing pdm-control) into out.pdm. out may be NULL
|
||||
* (defaults to <Package>_<Version>.pdm next to the dir). */
|
||||
int pdm_pack(const char *dir, const char *out);
|
||||
|
||||
/* Install a local .pdm into root dir (~/.pmm/root), record in db (~/.pmm/installed). */
|
||||
int pdm_install_file(const char *pdmfile);
|
||||
|
||||
/* Remove installed package by name (dpkg -r style: delete data files). */
|
||||
int pdm_remove(const char *name);
|
||||
|
||||
/* Print installed packages from the db. */
|
||||
void pdm_list_installed(void);
|
||||
|
||||
/* Print control fields of a .pdm file. */
|
||||
int pdm_info(const char *pdmfile);
|
||||
|
||||
#endif
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/* pdm_main.c - standalone `pdm` tool (pack/install/remove/info/list)
|
||||
*
|
||||
* usage:
|
||||
* pdm pack <dir> [output.pdm] pack a folder (with pdm-control) into .pdm
|
||||
* pdm install <file.pdm> install local .pdm
|
||||
* pdm info <file.pdm> show package control info
|
||||
* pdm list list installed packages
|
||||
* pdm remove <name> remove installed package
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "pdm.h"
|
||||
#include "pmm.h"
|
||||
#include "install.h"
|
||||
|
||||
static void usage(void) {
|
||||
printf("pdm - Parlz .pdm package tool\n\n"
|
||||
"usage:\n"
|
||||
" pdm pack <dir> [output.pdm] pack folder (needs pdm-control) into .pdm\n"
|
||||
" pdm install <file.pdm> install local .pdm\n"
|
||||
" pdm install <pkg> [-v <ver>] install from registry (pmm mirror)\n"
|
||||
" pdm info <file.pdm> show control info\n"
|
||||
" pdm list list installed packages\n"
|
||||
" pdm remove <name> remove installed package\n"
|
||||
"options: -p<drive> install under <DRIVE>:\\.pdm (e.g. -pd -> D:\\.pdm)\n");
|
||||
}
|
||||
|
||||
/* Parse "-p<drive>" / "-p <drive>" and compact argv; records the drive so
|
||||
* pdm installs under <DRIVE>:\.pmm. */
|
||||
static int consume_drive_flag(int argc, char **argv) {
|
||||
int w = 1;
|
||||
for (int r = 1; r < argc; r++) {
|
||||
const char *a = argv[r];
|
||||
if (a[0] == '-' && a[1] == 'p' && (a[2] >= 'A' && a[2] <= 'Z' || a[2] >= 'a' && a[2] <= 'z')) {
|
||||
char dr[2] = { a[2], '\0' };
|
||||
pmm_set_install_drive(dr);
|
||||
continue;
|
||||
}
|
||||
if (strcmp(a, "-p") == 0 && r + 1 < argc) {
|
||||
const char *nxt = argv[r + 1];
|
||||
if (nxt[0] && (nxt[0] >= 'A' && nxt[0] <= 'Z' || nxt[0] >= 'a' && nxt[0] <= 'z') && nxt[1] == '\0') {
|
||||
char dr[2] = { nxt[0], '\0' };
|
||||
pmm_set_install_drive(dr);
|
||||
r++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
argv[w++] = argv[r];
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { usage(); return 0; }
|
||||
pmm_set_self_path(argv[0]);
|
||||
argc = consume_drive_flag(argc, argv);
|
||||
if (argc < 2) { usage(); return 0; }
|
||||
if (strcmp(argv[1], "pack") == 0) {
|
||||
if (argc < 3) { fprintf(stderr, "pdm: usage: pdm pack <dir> [output.pdm]\n"); return 1; }
|
||||
return pdm_pack(argv[2], argc >= 4 ? argv[3] : NULL) == 0 ? 0 : 1;
|
||||
}
|
||||
if (strcmp(argv[1], "install") == 0 || strcmp(argv[1], "-i") == 0) {
|
||||
if (argc < 3) { fprintf(stderr, "pdm: usage: pdm install <file.pdm|pkg> [-v ver]\n"); return 1; }
|
||||
/* parse [pkg] [ -v <ver> ] ; pkg may be "nodejs==24.20.0" / "nodejs>=24,<25" */
|
||||
const char *raw = NULL, *version = NULL;
|
||||
for (int i = 2; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
if (strncmp(a, "-v", 2) == 0 && a[2]) { version = a + 2; continue; }
|
||||
if (strcmp(a, "-v") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
|
||||
if (strcmp(a, "--version") == 0 && i + 1 < argc) { version = argv[++i]; continue; }
|
||||
if (strncmp(a, "--version=", 10) == 0) { version = a + 10; continue; }
|
||||
if (!raw && a[0] != '-') { raw = a; continue; }
|
||||
}
|
||||
if (!raw) raw = argv[2];
|
||||
char pkg[256], spec[256];
|
||||
if (version && *version) {
|
||||
strncpy(pkg, raw, sizeof(pkg) - 1);
|
||||
strncpy(spec, version, sizeof(spec) - 1);
|
||||
} else {
|
||||
const char *ops[] = { ">=", "<=", "==", "!=", ">", "<", NULL };
|
||||
const char *pos = NULL;
|
||||
for (int i = 0; ops[i]; i++) { pos = strstr(raw, ops[i]); if (pos) break; }
|
||||
if (pos) { size_t n = (size_t)(pos - raw); if (n >= sizeof(pkg)) n = sizeof(pkg) - 1; memcpy(pkg, raw, n); pkg[n] = '\0'; strncpy(spec, pos, sizeof(spec) - 1); }
|
||||
else { strncpy(pkg, raw, sizeof(pkg) - 1); spec[0] = '\0'; }
|
||||
}
|
||||
pkg[sizeof(pkg) - 1] = '\0'; spec[sizeof(spec) - 1] = '\0';
|
||||
size_t l = strlen(pkg);
|
||||
if (l > 4 && (strcmp(pkg + l - 4, ".pdm") == 0 || strcmp(pkg + l - 4, ".PDM") == 0))
|
||||
return pdm_install_file(pkg) == 0 ? 0 : 1;
|
||||
/* otherwise treat as a registry package name */
|
||||
return install_from_registry(pkg, spec[0] ? spec : NULL, NULL) == 0 ? 0 : 1;
|
||||
}
|
||||
if (strcmp(argv[1], "info") == 0) {
|
||||
if (argc < 3) { fprintf(stderr, "pdm: usage: pdm info <file.pdm>\n"); return 1; }
|
||||
return pdm_info(argv[2]) == 0 ? 0 : 1;
|
||||
}
|
||||
if (strcmp(argv[1], "list") == 0 || strcmp(argv[1], "-l") == 0) {
|
||||
pdm_list_installed();
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[1], "remove") == 0 || strcmp(argv[1], "-r") == 0) {
|
||||
if (argc < 3) { fprintf(stderr, "pdm: usage: pdm remove <name>\n"); return 1; }
|
||||
return pdm_remove(argv[2]) == 0 ? 0 : 1;
|
||||
}
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/* pmm.c - platform & config-dir helpers */
|
||||
#include "pmm.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <inttypes.h>
|
||||
#define PMM_MKDIR(p) _mkdir(p)
|
||||
#define PMM_POPEN_READ(cmd) _popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ(p) _pclose(p)
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#include <dirent.h>
|
||||
#include <strings.h> /* strcasecmp/strncasecmp */
|
||||
#define PMM_MKDIR(p) mkdir((p), 0755)
|
||||
#define PMM_POPEN_READ(cmd) popen(cmd, "r")
|
||||
#define PMM_PCLOSE_READ(p) pclose(p)
|
||||
#endif
|
||||
|
||||
static char g_self_path[1400] = "";
|
||||
void pmm_set_self_path(const char *argv0) {
|
||||
if (!argv0 || !*argv0) return;
|
||||
#ifdef _WIN32
|
||||
if (_fullpath(g_self_path, argv0, sizeof(g_self_path)) == NULL)
|
||||
snprintf(g_self_path, sizeof(g_self_path), "%s", argv0);
|
||||
#else
|
||||
if (realpath(argv0, g_self_path) == NULL)
|
||||
snprintf(g_self_path, sizeof(g_self_path), "%s", argv0);
|
||||
#endif
|
||||
}
|
||||
const char *pmm_self_path(void) {
|
||||
return g_self_path[0] ? g_self_path : NULL;
|
||||
}
|
||||
|
||||
PmmOS pmm_detect_os(void) {
|
||||
#if defined(_WIN32)
|
||||
return OS_WINDOWS;
|
||||
#elif defined(__APPLE__)
|
||||
return OS_MACOS;
|
||||
#elif defined(__linux__)
|
||||
return OS_LINUX;
|
||||
#else
|
||||
return OS_UNKNOWN;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *pmm_os_name(PmmOS os) {
|
||||
switch (os) {
|
||||
case OS_WINDOWS: return "windows";
|
||||
case OS_LINUX: return "linux";
|
||||
case OS_MACOS: return "macos";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void mkdir_p(const char *path) {
|
||||
char tmp[1024];
|
||||
strncpy(tmp, path, sizeof(tmp) - 1);
|
||||
tmp[sizeof(tmp) - 1] = '\0';
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
char c = *p;
|
||||
*p = '\0';
|
||||
PMM_MKDIR(tmp);
|
||||
*p = c;
|
||||
}
|
||||
}
|
||||
PMM_MKDIR(tmp);
|
||||
}
|
||||
|
||||
static const char *home_dir(void) {
|
||||
const char *h = getenv("USERPROFILE"); /* windows */
|
||||
if (!h || !*h) h = getenv("HOME");
|
||||
return (h && *h) ? h : ".";
|
||||
}
|
||||
|
||||
/* ---- install-drive override (e.g. "pmm install x -pd" -> D:\.pmm) ----
|
||||
* The chosen drive is persisted in the always-reachable home dir so later
|
||||
* commands remember it without needing the flag again. */
|
||||
static char g_drive[4] = ""; /* "D", or "" = use home */
|
||||
|
||||
static const char *drive_state_path(char *buf, size_t size) {
|
||||
snprintf(buf, size, "%s/.pmm/install-drive", home_dir());
|
||||
return buf;
|
||||
}
|
||||
|
||||
void pmm_set_install_drive(const char *letter) {
|
||||
if (!letter || !*letter) { g_drive[0] = '\0'; return; }
|
||||
char c = (char)((*letter >= 'a' && *letter <= 'z') ? *letter - 'a' + 'A' : *letter);
|
||||
if (c < 'A' || c > 'Z') return; /* only a single A-Z drive letter */
|
||||
g_drive[0] = c;
|
||||
g_drive[1] = '\0';
|
||||
/* persist so it becomes the default for later runs */
|
||||
char state[1024];
|
||||
drive_state_path(state, sizeof(state));
|
||||
char dir[1024];
|
||||
snprintf(dir, sizeof(dir), "%s/.pmm", home_dir());
|
||||
mkdir_p(dir);
|
||||
FILE *f = fopen(state, "w");
|
||||
if (f) { fprintf(f, "%c", c); fclose(f); }
|
||||
printf("pmm: install drive set to %c:\n", c);
|
||||
}
|
||||
|
||||
/* Resolve the .pmm base dir: explicit drive override > persisted drive >
|
||||
* home/.pmm (when that drive is picked) > DEFAULT = D:\.pmm.
|
||||
* Defaulting to D:\ spares the C: drive (user's primary diskspace concern);
|
||||
* use -pc to go back to the user-home .pmm, or -p<drive> for any other drive. */
|
||||
static char home_drive_letter(void) {
|
||||
const char *h = home_dir();
|
||||
if (h[0] && h[1] == ':' && ((h[0] >= 'A' && h[0] <= 'Z') || (h[0] >= 'a' && h[0] <= 'z')))
|
||||
return (char)((h[0] >= 'a' && h[0] <= 'z') ? h[0] - 'a' + 'A' : h[0]);
|
||||
return 0; /* not a drive-letter home (unix) */
|
||||
}
|
||||
|
||||
static const char *pmm_base_dir(char *buf, size_t size) {
|
||||
char drive[4] = "";
|
||||
if (g_drive[0]) {
|
||||
drive[0] = g_drive[0];
|
||||
} else {
|
||||
char state[1024];
|
||||
drive_state_path(state, sizeof(state));
|
||||
FILE *f = fopen(state, "r");
|
||||
if (f) {
|
||||
char c = (char)fgetc(f);
|
||||
fclose(f);
|
||||
if (c >= 'A' && c <= 'Z') drive[0] = c;
|
||||
else if (c >= 'a' && c <= 'z') drive[0] = (char)(c - 'a' + 'A');
|
||||
}
|
||||
}
|
||||
char hd = home_drive_letter();
|
||||
if (drive[0] && hd && drive[0] != hd) {
|
||||
snprintf(buf, size, "%c:\\.pmm", drive[0]); /* -p<other> */
|
||||
} else if (drive[0] && hd && drive[0] == hd) {
|
||||
snprintf(buf, size, "%s/.pmm", home_dir()); /* -pc -> home */
|
||||
} else {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, size, "D:\\.pmm"); /* Windows DEFAULT -> D: */
|
||||
#else
|
||||
snprintf(buf, size, "%s/.pmm", home_dir()); /* unix default -> ~/.pmm */
|
||||
#endif
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *pmm_config_dir(char *buf, size_t size) {
|
||||
pmm_base_dir(buf, size);
|
||||
mkdir_p(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *pmm_cache_dir(char *buf, size_t size) {
|
||||
char base[1024];
|
||||
pmm_base_dir(base, sizeof(base));
|
||||
snprintf(buf, size, "%s/cache", base);
|
||||
mkdir_p(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *pmm_install_dir(char *buf, size_t size) {
|
||||
const char *env = getenv("PMM_INSTALL_DIR");
|
||||
if (env && *env) { snprintf(buf, size, "%s", env); }
|
||||
else {
|
||||
char base[1024];
|
||||
pmm_base_dir(base, sizeof(base));
|
||||
snprintf(buf, size, "%s/bin", base);
|
||||
}
|
||||
mkdir_p(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
char *pmm_find_config(const char *dir, const char *base) {
|
||||
static const char *exts[] = { ".json", ".ini", ".conf", NULL };
|
||||
static char out[1024];
|
||||
for (int i = 0; exts[i]; i++) {
|
||||
snprintf(out, sizeof(out), "%s/%s%s", dir, base, exts[i]);
|
||||
struct stat st;
|
||||
if (stat(out, &st) == 0 && !S_ISDIR(st.st_mode))
|
||||
return out;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------- PATH auto-add ---------- */
|
||||
|
||||
static int dir_has_exec(const char *dir) {
|
||||
#ifdef _WIN32
|
||||
char patt[1300];
|
||||
snprintf(patt, sizeof(patt), "%s\\*", dir);
|
||||
struct _finddata_t fd;
|
||||
intptr_t h = _findfirst(patt, &fd);
|
||||
if (h == -1) return 0;
|
||||
int found = 0;
|
||||
do {
|
||||
if (fd.attrib & _A_SUBDIR) continue;
|
||||
size_t l = strlen(fd.name);
|
||||
if ((l > 4 && strcasecmp(fd.name + l - 4, ".exe") == 0) ||
|
||||
(l > 4 && strcasecmp(fd.name + l - 4, ".cmd") == 0) ||
|
||||
(l > 4 && strcasecmp(fd.name + l - 4, ".bat") == 0)) { found = 1; break; }
|
||||
} while (_findnext(h, &fd) == 0);
|
||||
_findclose(h);
|
||||
return found;
|
||||
#else
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return 0;
|
||||
struct dirent *e;
|
||||
int found = 0;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
if (e->d_name[0] == '.') continue;
|
||||
char full[1300];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) == 0 && !S_ISDIR(st.st_mode)) { found = 1; break; }
|
||||
}
|
||||
closedir(d);
|
||||
return found;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* case-insensitive "dir" present in a ';'-separated PATH (win) / ':' (unix) */
|
||||
static int path_has(const char *path, const char *dir, char sep) {
|
||||
if (!path || !*path) return 0;
|
||||
size_t dl = strlen(dir);
|
||||
const char *p = path;
|
||||
for (;;) {
|
||||
const char *e = strchr(p, sep);
|
||||
size_t n = e ? (size_t)(e - p) : strlen(p);
|
||||
while (n && (p[n-1]==' '||p[n-1]=='"')) n--;
|
||||
const char *q = p;
|
||||
while (*q==' '||*q=='"') q++;
|
||||
if (n && strncasecmp(q, dir, dl) == 0 && (n == dl)) return 1;
|
||||
if (!e) break;
|
||||
p = e + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Add a dir to the candidate list if not already listed. */
|
||||
static void add_path_candidate(char list[][1200], int *n, int max, const char *dir) {
|
||||
for (int i = 0; i < *n; i++)
|
||||
if (strcmp(list[i], dir) == 0) return;
|
||||
if (*n < max) snprintf(list[(*n)++], 1200, "%s", dir);
|
||||
}
|
||||
|
||||
void pmm_add_to_path(void) {
|
||||
char home[1024];
|
||||
pmm_config_dir(home, sizeof(home));
|
||||
char base[1024];
|
||||
snprintf(base, sizeof(base), "%s/root", home);
|
||||
|
||||
/* collect dirs: ~/.pmm/bin, ~/.pmm/root, and root subdirs holding executables */
|
||||
char list[160][1200];
|
||||
int n = 0;
|
||||
char bin[1100];
|
||||
snprintf(bin, sizeof(bin), "%s/bin", home);
|
||||
mkdir_p(bin);
|
||||
snprintf(list[n++], sizeof(list[0]), "%s", bin);
|
||||
snprintf(list[n++], sizeof(list[0]), "%s", base);
|
||||
mkdir_p(base);
|
||||
|
||||
#ifdef _WIN32
|
||||
{
|
||||
char patt[1600];
|
||||
snprintf(patt, sizeof(patt), "%s\\*", base);
|
||||
char cmd[1700];
|
||||
snprintf(cmd, sizeof(cmd), "dir /b /ad \"%s\" 2>nul", patt);
|
||||
FILE *f = PMM_POPEN_READ(cmd);
|
||||
if (f) {
|
||||
char sub[1200];
|
||||
while (fgets(sub, sizeof(sub), f)) {
|
||||
size_t l = strlen(sub);
|
||||
while (l && (sub[l-1]=='\r'||sub[l-1]=='\n')) sub[--l] = 0;
|
||||
if (!l) continue;
|
||||
char full[1300];
|
||||
snprintf(full, sizeof(full), "%s/%s", base, sub);
|
||||
if (dir_has_exec(full)) {
|
||||
add_path_candidate(list, &n, 160, full);
|
||||
} else {
|
||||
/* depth-2: pkg/cmd convention (e.g. git/cmd/git.exe) */
|
||||
char cmddir[1400];
|
||||
snprintf(cmddir, sizeof(cmddir), "%s/cmd", full);
|
||||
if (dir_has_exec(cmddir))
|
||||
add_path_candidate(list, &n, 160, cmddir);
|
||||
}
|
||||
}
|
||||
PMM_PCLOSE_READ(f);
|
||||
}
|
||||
}
|
||||
#else
|
||||
{
|
||||
char cmd[1700];
|
||||
snprintf(cmd, sizeof(cmd), "ls -1 \"%s\" 2>/dev/null", base);
|
||||
FILE *f = PMM_POPEN_READ(cmd);
|
||||
if (f) {
|
||||
char sub[1200];
|
||||
while (fgets(sub, sizeof(sub), f)) {
|
||||
size_t l = strlen(sub);
|
||||
while (l && (sub[l-1]=='\r'||sub[l-1]=='\n')) sub[--l] = 0;
|
||||
char full[1300];
|
||||
snprintf(full, sizeof(full), "%s/%s", base, sub);
|
||||
if (dir_has_exec(full)) {
|
||||
add_path_candidate(list, &n, 160, full);
|
||||
} else {
|
||||
char cmddir[1400];
|
||||
snprintf(cmddir, sizeof(cmddir), "%s/cmd", full);
|
||||
if (dir_has_exec(cmddir))
|
||||
add_path_candidate(list, &n, 160, cmddir);
|
||||
}
|
||||
}
|
||||
PMM_PCLOSE_READ(f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* read current user PATH */
|
||||
char cur[16384] = "";
|
||||
#ifdef _WIN32
|
||||
{
|
||||
FILE *f = PMM_POPEN_READ("reg query \"HKCU\\Environment\" /v Path 2>nul");
|
||||
if (f) {
|
||||
char line[8192];
|
||||
char *val = NULL;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *r = strstr(line, "REG_SZ");
|
||||
if (r) { val = r + 6; while (*val==' '||*val=='\t'||*val=='\r'||*val=='\n') val++; break; }
|
||||
}
|
||||
PMM_PCLOSE_READ(f);
|
||||
if (val) {
|
||||
size_t v = strlen(val);
|
||||
while (v && (val[v-1]=='\r'||val[v-1]=='\n')) val[--v] = 0;
|
||||
snprintf(cur, sizeof(cur), "%s", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
{
|
||||
const char *env = getenv("PATH");
|
||||
if (env) snprintf(cur, sizeof(cur), "%s", env);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* append missing dirs */
|
||||
char next[17000] = "";
|
||||
snprintf(next, sizeof(next), "%s", cur);
|
||||
int added = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (!list[i][0]) continue;
|
||||
if (path_has(next, list[i], ';') || path_has(next, list[i], ':')) continue;
|
||||
size_t l = strlen(next);
|
||||
if (l && next[l-1] != ';' && next[l-1] != ':') strncat(next, ";", sizeof(next)-l-1);
|
||||
strncat(next, list[i], sizeof(next)-strlen(next)-1);
|
||||
added++;
|
||||
printf("pmm: adding to PATH: %s\n", list[i]);
|
||||
}
|
||||
if (!added) { printf("pmm: PATH already contains PMM dirs\n"); return; }
|
||||
|
||||
#ifdef _WIN32
|
||||
{
|
||||
char cmd[17001];
|
||||
/* drive letters and ';' are fine for setx; quote the value */
|
||||
snprintf(cmd, sizeof(cmd), "setx Path \"%s\" >nul 2>&1", next);
|
||||
system(cmd);
|
||||
printf("pmm: PATH updated for new shells (reopen a terminal or run: setx) (user-level)\n");
|
||||
}
|
||||
#else
|
||||
{
|
||||
const char *she = getenv("SHELL");
|
||||
const char *rc = "~/.bashrc";
|
||||
char rcp[1100];
|
||||
snprintf(rcp, sizeof(rcp), "%s/.bashrc", home_dir());
|
||||
FILE *f = fopen(rcp, "a");
|
||||
if (f) {
|
||||
fprintf(f, "\n# PMM PATH\n");
|
||||
for (int i = 0; i < n; i++) if (list[i][0]) fprintf(f, "export PATH=\"%s:$PATH\"\n", list[i]);
|
||||
fclose(f);
|
||||
}
|
||||
(void)she;
|
||||
printf("pmm: updated %s (reload your shell)\n", rcp);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---------- register installed packages in the Windows "apps" (Uninstall) ----------
|
||||
* Mimics what an MSI installer writes: HKCU\Software\Microsoft\
|
||||
* Windows\CurrentVersion\Uninstall\<pkg>, so the package appears in
|
||||
* "Installed apps" / Add-Remove Programs and can be uninstalled via pmm. */
|
||||
|
||||
/* Escape a string for a single-quoted PowerShell literal (backslashes are
|
||||
* literal there; only a single quote needs doubling: ' -> ''). */
|
||||
static void ps_squote(const char *in, char *out, size_t outsz) {
|
||||
size_t o = 0;
|
||||
for (const char *p = in; *p && o + 3 < outsz; p++) {
|
||||
if (*p == '\'') { out[o++] = '\''; out[o++] = '\''; }
|
||||
out[o++] = *p;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
int pmm_reg_uninstall(const char *pkg, const char *ver, const char *pub,
|
||||
const char *install_location, const char *icon,
|
||||
const char *uninstaller, unsigned long long size_bytes) {
|
||||
#ifndef _WIN32
|
||||
(void)pkg; (void)ver; (void)pub; (void)install_location; (void)icon;
|
||||
(void)uninstaller; (void)size_bytes;
|
||||
return 0; /* only Windows registers software in the Uninstall hive */
|
||||
#else
|
||||
if (!pkg || !*pkg) return -1;
|
||||
char base[1024];
|
||||
pmm_config_dir(base, sizeof(base));
|
||||
char cfg[1100];
|
||||
snprintf(cfg, sizeof(cfg), "%s\\cache", base);
|
||||
mkdir_p(base); mkdir_p(cfg);
|
||||
char ps1[1300];
|
||||
snprintf(ps1, sizeof(ps1), "%s\\pmm-uninst-%s.ps1", cfg, pkg);
|
||||
/* PowerShell-escape the values */
|
||||
char e_nm[700], e_ver[700], e_pub[700], e_loc[2400], e_icon[2600], e_un[4200];
|
||||
ps_squote(pkg, e_nm, sizeof(e_nm));
|
||||
ps_squote(ver ? ver : "", e_ver, sizeof(e_ver));
|
||||
ps_squote(pub ? pub : "", e_pub, sizeof(e_pub));
|
||||
ps_squote(install_location ? install_location : "", e_loc, sizeof(e_loc));
|
||||
ps_squote(icon ? icon : "", e_icon, sizeof(e_icon));
|
||||
char uninst[2048];
|
||||
if (uninstaller && *uninstaller) snprintf(uninst, sizeof(uninst), "\"%s\" remove %s", uninstaller, pkg);
|
||||
else snprintf(uninst, sizeof(uninst), "pmm remove %s", pkg);
|
||||
ps_squote(uninst, e_un, sizeof(e_un));
|
||||
|
||||
FILE *f = fopen(ps1, "wb");
|
||||
if (!f) return -1;
|
||||
fprintf(f, "$ErrorActionPreference='SilentlyContinue'\r\n");
|
||||
fprintf(f, "$k='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s'\r\n", pkg);
|
||||
fprintf(f, "New-Item -Path $k -Force | Out-Null\r\n");
|
||||
fprintf(f, "Set-ItemProperty -Path $k -Name DisplayName -Value '%s' -Force\r\n", e_nm);
|
||||
if (ver && *ver) fprintf(f, "Set-ItemProperty -Path $k -Name DisplayVersion -Value '%s' -Force\r\n", e_ver);
|
||||
if (pub && *pub) fprintf(f, "Set-ItemProperty -Path $k -Name Publisher -Value '%s' -Force\r\n", e_pub);
|
||||
if (icon && *icon) fprintf(f, "Set-ItemProperty -Path $k -Name DisplayIcon -Value '%s' -Force\r\n", e_icon);
|
||||
fprintf(f, "Set-ItemProperty -Path $k -Name UninstallString -Value '%s' -Force\r\n", e_un);
|
||||
if (install_location && *install_location)
|
||||
fprintf(f, "Set-ItemProperty -Path $k -Name InstallLocation -Value '%s' -Force\r\n", e_loc);
|
||||
fprintf(f, "New-ItemProperty -Path $k -Name EstimatedSize -Value %llu -PropertyType DWord -Force | Out-Null\r\n",
|
||||
(unsigned long long)(size_bytes / 1024ULL));
|
||||
{ /* InstallDate YYYYMMDD */
|
||||
time_t t = time(NULL); struct tm *tm = localtime(&t);
|
||||
char date[32];
|
||||
if (tm) snprintf(date, sizeof(date), "%04d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
|
||||
else snprintf(date, sizeof(date), "0");
|
||||
fprintf(f, "Set-ItemProperty -Path $k -Name InstallDate -Value '%s' -Force\r\n", date);
|
||||
}
|
||||
fprintf(f, "New-ItemProperty -Path $k -Name NoModify -Value 1 -PropertyType DWord -Force | Out-Null\r\n");
|
||||
fprintf(f, "New-ItemProperty -Path $k -Name NoRepair -Value 1 -PropertyType DWord -Force | Out-Null\r\n");
|
||||
fclose(f);
|
||||
|
||||
char cmd[2600];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"%%SystemRoot%%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%s\" >nul 2>&1",
|
||||
ps1);
|
||||
int rc = system(cmd);
|
||||
remove(ps1);
|
||||
if (rc != 0)
|
||||
fprintf(stderr, "pmm: warning: could not register '%s' in Installed Apps\n", pkg);
|
||||
else
|
||||
printf("pmm: registered '%s' %s in Installed Apps (uninstall via pmm remove %s)\n",
|
||||
pkg, ver ? ver : "", pkg);
|
||||
return rc == 0 ? 0 : -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void pmm_reg_uninstall_clear(const char *pkg) {
|
||||
#ifndef _WIN32
|
||||
(void)pkg;
|
||||
#else
|
||||
if (!pkg || !*pkg) return;
|
||||
char cmd[2200];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"%%SystemRoot%%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -Command \"Remove-Item -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s' -Recurse -Force -ErrorAction SilentlyContinue\" 2>nul",
|
||||
pkg);
|
||||
system(cmd);
|
||||
printf("pmm: removed '%s' from Installed Apps\n", pkg);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/* pmm.h - shared definitions */
|
||||
#ifndef PMM_H
|
||||
#define PMM_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Add PMM install dirs (~/.pmm/bin, ~/.pmm/root, and executable subdirs like
|
||||
* root/nodejs) to the user's PATH if not already present. Idempotent.
|
||||
* On Windows persists via the user registry PATH; on Unix appends to shell rc. */
|
||||
void pmm_add_to_path(void);
|
||||
|
||||
/* Set the install location by drive letter ("D" -> D:\.pmm). An empty string
|
||||
* clears back to the user-home default. Persisted across runs. */
|
||||
void pmm_set_install_drive(const char *letter);
|
||||
|
||||
/* File-association record written to the per-user registry on install. */
|
||||
typedef struct {
|
||||
const char *progid; /* e.g. "Node.JSFile" */
|
||||
const char *icon; /* e.g. "C:\...\node.exe,0" or NULL */
|
||||
const char *cmd; /* open command, e.g. "C:\..\node.exe" "%1" */
|
||||
const char **exts; /* NULL-terminated list of ".js", ".mjs", ... */
|
||||
} PmmAssoc;
|
||||
|
||||
/* Write HKCU\Software\Classes associations for the given package.
|
||||
* Backs up any pre-existing per-user entries so clear() can restore them. */
|
||||
int pmm_assoc_apply(const PmmAssoc *a, const char *backup_dir);
|
||||
|
||||
/* Remove the associations for a package; restores backups if present. */
|
||||
int pmm_assoc_clear(const PmmAssoc *a, const char *backup_dir);
|
||||
|
||||
/* Register an installed package in the Windows per-user "Uninstall" (Installed
|
||||
* Apps) hive, mimicking what an MSI installer writes. uninstaller is the
|
||||
* absolute path to the pmm/pdm executable used to uninstall. */
|
||||
int pmm_reg_uninstall(const char *pkg, const char *ver, const char *pub,
|
||||
const char *install_location, const char *icon,
|
||||
const char *uninstaller, unsigned long long size_bytes);
|
||||
void pmm_reg_uninstall_clear(const char *pkg);
|
||||
|
||||
/* Record the running executable path (argv[0], resolved to absolute) so
|
||||
* registration can point the "Uninstall" button back at us. */
|
||||
void pmm_set_self_path(const char *argv0);
|
||||
const char *pmm_self_path(void);
|
||||
|
||||
#define PMM_VERSION "0.1.0"
|
||||
|
||||
typedef enum { OS_WINDOWS, OS_LINUX, OS_MACOS, OS_UNKNOWN } PmmOS;
|
||||
|
||||
typedef enum { HOST_GITHUB, HOST_GITLAB, HOST_GITEA, HOST_FORGEJO, HOST_AUTO, HOST_UNKNOWN } PmmHost;
|
||||
|
||||
PmmOS pmm_detect_os(void);
|
||||
const char *pmm_os_name(PmmOS os);
|
||||
|
||||
/* Resolve ~/.pmm or %USERPROFILE%\.pmm into buf; returns buf or NULL. */
|
||||
const char *pmm_config_dir(char *buf, size_t size);
|
||||
const char *pmm_cache_dir(char *buf, size_t size);
|
||||
const char *pmm_install_dir(char *buf, size_t size);
|
||||
|
||||
/* Find first existing config file among given extensions inside dir. */
|
||||
char *pmm_find_config(const char *dir, const char *base); /* malloc'd path or NULL */
|
||||
|
||||
#endif
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/* repo.c - git hosting adapters: GitHub / GitLab / Gitea / Forgejo
|
||||
*
|
||||
* API endpoints used (latest release):
|
||||
* GitHub: {api}/repos/{repo}/releases/latest
|
||||
* GitLab: {api}/projects/{url-encoded repo}/releases/permalink/latest
|
||||
* Gitea/Forgejo: {api}/api/v1/repos/{repo}/releases/latest
|
||||
*
|
||||
* Asset selection by OS (first match wins, in priority order):
|
||||
* windows: .exe .msi .zip .7z
|
||||
* linux: .deb .rpm .appimage .tar.gz .tgz .tar.xz
|
||||
* macos: .dmg .pkg .app.zip .tar.gz
|
||||
*/
|
||||
#include "repo.h"
|
||||
#include "http.h"
|
||||
#include "json.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static char *strdup_or(const char *s, const char *dflt) {
|
||||
return strdup(s ? s : dflt);
|
||||
}
|
||||
|
||||
static char *url_encode(const char *s) {
|
||||
size_t cap = strlen(s) * 3 + 1;
|
||||
char *out = malloc(cap);
|
||||
size_t o = 0;
|
||||
for (const char *p = s; *p && o + 4 < cap; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
out[o++] = (char)c;
|
||||
} else if (c == '/') {
|
||||
out[o++] = '%'; out[o++] = '2'; out[o++] = 'F';
|
||||
} else {
|
||||
char tmp[8];
|
||||
snprintf(tmp, sizeof(tmp), "%%%02X", c);
|
||||
out[o++] = tmp[0]; out[o++] = tmp[1]; out[o++] = tmp[2];
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Normalize "https://host/owner/repo(.git)" or "owner/repo" into "owner/repo". */
|
||||
static char *normalize_repo(const char *in) {
|
||||
const char *s = in;
|
||||
int is_url = 0;
|
||||
if (strncmp(s, "https://", 8) == 0) { s += 8; is_url = 1; }
|
||||
else if (strncmp(s, "http://", 7) == 0) { s += 7; is_url = 1; }
|
||||
/* skip hostname only for full URLs */
|
||||
if (is_url) {
|
||||
const char *slash = strchr(s, '/');
|
||||
if (slash && slash != s) s = slash + 1;
|
||||
}
|
||||
size_t len = strlen(s);
|
||||
if (len > 4 && strcmp(s + len - 4, ".git") == 0) len -= 4;
|
||||
/* strip trailing slashes */
|
||||
while (len > 0 && s[len - 1] == '/') len--;
|
||||
char *out = malloc(len + 1);
|
||||
memcpy(out, s, len);
|
||||
out[len] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
PmmHost repo_detect_host(const char *repo_or_url) {
|
||||
if (strstr(repo_or_url, "github.com")) return HOST_GITHUB;
|
||||
if (strstr(repo_or_url, "gitlab.")) return HOST_GITLAB;
|
||||
if (strstr(repo_or_url, "gitea.")) return HOST_GITEA;
|
||||
if (strstr(repo_or_url, "forgejo.")) return HOST_FORGEJO;
|
||||
/* full URL with unknown host: try to infer from path; default gitea-compatible */
|
||||
if (strncmp(repo_or_url, "http", 4) == 0) return HOST_GITEA;
|
||||
return HOST_GITHUB; /* bare slug defaults to github */
|
||||
}
|
||||
|
||||
/* Extract "scheme://host[:port]" from a URL; empty string if none. */
|
||||
static void extract_origin(const char *repo, char *origin, size_t size) {
|
||||
const char *s = repo;
|
||||
origin[0] = '\0';
|
||||
if (strncmp(s, "https://", 8) == 0) s += 8;
|
||||
else if (strncmp(s, "http://", 7) == 0) { s += 7; }
|
||||
else return; /* bare slug: no origin */
|
||||
s -= (strncmp(repo, "https://", 8) == 0) ? 8 : 7;
|
||||
const char *slash = strchr(s + 8, '/');
|
||||
if (!slash) slash = s + strlen(s);
|
||||
size_t n = (size_t)(slash - s);
|
||||
if (n >= size) n = size - 1;
|
||||
memcpy(origin, s, n);
|
||||
origin[n] = '\0';
|
||||
}
|
||||
|
||||
RepoContext *repo_open(PmmHost host, const char *repo, const char *mirror_api_base) {
|
||||
RepoContext *ctx = calloc(1, sizeof(RepoContext));
|
||||
if (!ctx) return NULL;
|
||||
ctx->host = host;
|
||||
char *norm = normalize_repo(repo);
|
||||
char api[1024];
|
||||
ctx->repo_norm = strdup(norm);
|
||||
|
||||
char origin[512];
|
||||
extract_origin(repo, origin, sizeof(origin));
|
||||
ctx->origin = strdup(origin);
|
||||
|
||||
const char *base = NULL;
|
||||
if (mirror_api_base && *mirror_api_base) {
|
||||
base = mirror_api_base; /* mirror overrides host entirely */
|
||||
} else {
|
||||
switch (host) {
|
||||
case HOST_GITHUB: base = "https://api.github.com"; break;
|
||||
case HOST_GITLAB: base = "https://gitlab.com"; break;
|
||||
default: base = NULL; break; /* gitea/forgejo/auto need a full URL */
|
||||
}
|
||||
}
|
||||
|
||||
if (host == HOST_AUTO) {
|
||||
/* Generic git URL: api_url is built per-candidate in repo_latest_asset */
|
||||
ctx->page_url = strdup_or(origin[0] ? origin : "", "");
|
||||
ctx->api_url = NULL;
|
||||
free(norm);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
if (!base) {
|
||||
/* Gitea/Forgejo require a full URL in the repo argument, e.g.
|
||||
* https://git.example.com/owner/repo or configured in mirror file. */
|
||||
if (!origin[0])
|
||||
strncpy(origin, "http://localhost:3000", sizeof(origin) - 1);
|
||||
snprintf(api, sizeof(api), "%s/api/v1/repos/%s", origin, norm);
|
||||
ctx->page_url = strdup_or(origin, "");
|
||||
} else {
|
||||
switch (host) {
|
||||
case HOST_GITHUB:
|
||||
snprintf(api, sizeof(api), "%s/repos/%s", base, norm);
|
||||
break;
|
||||
case HOST_GITLAB: {
|
||||
char *enc = url_encode(norm);
|
||||
snprintf(api, sizeof(api), "%s/projects/%s", base, enc);
|
||||
free(enc);
|
||||
break;
|
||||
}
|
||||
default: /* gitea/forgejo via mirror base */
|
||||
snprintf(api, sizeof(api), "%s/api/v1/repos/%s", base, norm);
|
||||
break;
|
||||
}
|
||||
ctx->page_url = strdup(base);
|
||||
}
|
||||
ctx->api_url = strdup(api);
|
||||
free(norm);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void repo_close(RepoContext *ctx) {
|
||||
if (!ctx) return;
|
||||
free(ctx->api_url);
|
||||
free(ctx->page_url);
|
||||
free(ctx->origin);
|
||||
free(ctx->repo_norm);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
static int has_ext(const char *name, const char *ext) {
|
||||
size_t nl = strlen(name), el = strlen(ext);
|
||||
if (nl < el) return 0;
|
||||
const char *tail = name + nl - el;
|
||||
if (strcasecmp(tail, ext) != 0) return 0;
|
||||
/* ".tar.gz" style: for ".gz" only match after ".tar"? keep simple: exact suffix */
|
||||
return 1;
|
||||
}
|
||||
|
||||
int asset_matches_os(const char *filename, PmmOS os) {
|
||||
static const char *win_exts[] = { ".exe", ".msi", ".zip", ".7z", NULL };
|
||||
static const char *linux_exts[] = { ".deb", ".rpm", ".appimage", ".tar.gz", ".tgz", ".tar.xz", ".tar.bz2", ".pkg.tar.zst", NULL };
|
||||
static const char *mac_exts[] = { ".dmg", ".pkg", ".app.zip", ".tar.gz", ".tgz", NULL };
|
||||
const char **exts = NULL;
|
||||
switch (os) {
|
||||
case OS_WINDOWS: exts = win_exts; break;
|
||||
case OS_LINUX: exts = linux_exts; break;
|
||||
case OS_MACOS: exts = mac_exts; break;
|
||||
default: return 0;
|
||||
}
|
||||
for (int i = 0; exts[i]; i++)
|
||||
if (has_ext(filename, exts[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Reject common junk assets like sources, checksums, sigs. */
|
||||
static int asset_is_junk(const char *name) {
|
||||
static const char *junk[] = { ".sha256", ".sha512", ".sig", ".asc", ".sbom",
|
||||
".json", ".txt", ".yml", ".yaml", ".xml", ".sb", ".pem", NULL };
|
||||
for (int i = 0; junk[i]; i++)
|
||||
if (has_ext(name, junk[i])) return 1;
|
||||
if (strstr(name, "checksum") || strstr(name, "source") || strstr(name, "-src")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ReleaseAsset *asset_from_json(const JsonValue *a, const char *tag) {
|
||||
ReleaseAsset *ra = calloc(1, sizeof(ReleaseAsset));
|
||||
const char *name = json_str(a, "name");
|
||||
const char *url = json_str(a, "browser_download_url");
|
||||
if (!url) url = json_str(a, "direct_asset_url");
|
||||
ra->name = strdup_or(name, "asset");
|
||||
ra->url = strdup_or(url, "");
|
||||
ra->tag = strdup_or(tag, "");
|
||||
return ra;
|
||||
}
|
||||
|
||||
static ReleaseAsset *pick_from_assets_array(const JsonValue *assets, const char *tag, PmmOS os) {
|
||||
for (int i = 0; i < assets->count; i++) {
|
||||
JsonValue *a = json_at(assets, i);
|
||||
if (!a) continue;
|
||||
const char *name = json_str(a, "name");
|
||||
if (!name || asset_is_junk(name)) continue;
|
||||
if (!asset_matches_os(name, os)) continue;
|
||||
return asset_from_json(a, tag);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ReleaseAsset *fetch_latest(const char *url, PmmHost host, PmmOS os) {
|
||||
int status = 0;
|
||||
char *body = http_get(url, &status);
|
||||
if (!body || (status != 200 && status != 0)) {
|
||||
free(body);
|
||||
return NULL;
|
||||
}
|
||||
JsonValue *root = json_parse(body);
|
||||
free(body);
|
||||
if (!root) return NULL;
|
||||
|
||||
const char *tag = json_str(root, "tag_name");
|
||||
if (!tag) tag = json_str(root, "tag");
|
||||
|
||||
ReleaseAsset *found = NULL;
|
||||
|
||||
/* GitHub: assets is at root; Gitea: same; GitLab: assets.links[] */
|
||||
JsonValue *assets = json_get(root, "assets");
|
||||
if (host == HOST_GITLAB && assets) {
|
||||
JsonValue *links = json_get(assets, "links");
|
||||
if (links) {
|
||||
for (int i = 0; i < links->count && !found; i++) {
|
||||
JsonValue *a = json_at(links, i);
|
||||
const char *name = json_str(a, "name");
|
||||
if (!name) name = json_str(a, "direct_asset_url");
|
||||
if (!name || asset_is_junk(name) || !asset_matches_os(name, os)) continue;
|
||||
found = calloc(1, sizeof(ReleaseAsset));
|
||||
found->name = strdup(name);
|
||||
found->url = strdup_or(json_str(a, "direct_asset_url"), json_str(a, "url") ? json_str(a, "url") : "");
|
||||
found->tag = strdup_or(tag, "");
|
||||
}
|
||||
}
|
||||
} else if (assets) {
|
||||
found = pick_from_assets_array(assets, tag, os);
|
||||
}
|
||||
|
||||
json_free(root);
|
||||
if (found && (!found->url || !*found->url)) {
|
||||
repo_asset_free(found);
|
||||
return NULL;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
ReleaseAsset *repo_latest_asset(RepoContext *ctx, PmmOS os) {
|
||||
char url[2048];
|
||||
|
||||
if (ctx->host == HOST_AUTO) {
|
||||
/* Generic git repo: probe known API shapes on the given origin. */
|
||||
const char *origin = (ctx->origin && *ctx->origin) ? ctx->origin : NULL;
|
||||
const char *norm = ctx->repo_norm;
|
||||
ReleaseAsset *a = NULL;
|
||||
if (origin) {
|
||||
snprintf(url, sizeof(url), "%s/api/v1/repos/%s/releases/latest", origin, norm);
|
||||
if ((a = fetch_latest(url, HOST_GITEA, os))) { ctx->host = HOST_GITEA; return a; }
|
||||
char *enc = url_encode(norm);
|
||||
snprintf(url, sizeof(url), "%s/api/v4/projects/%s/releases/permalink/latest", origin, enc);
|
||||
free(enc);
|
||||
if ((a = fetch_latest(url, HOST_GITLAB, os))) { ctx->host = HOST_GITLAB; return a; }
|
||||
if (strstr(origin, "github.com")) {
|
||||
snprintf(url, sizeof(url), "https://api.github.com/repos/%s/releases/latest", norm);
|
||||
if ((a = fetch_latest(url, HOST_GITHUB, os))) { ctx->host = HOST_GITHUB; return a; }
|
||||
}
|
||||
} else {
|
||||
snprintf(url, sizeof(url), "https://api.github.com/repos/%s/releases/latest", norm);
|
||||
if ((a = fetch_latest(url, HOST_GITHUB, os))) { ctx->host = HOST_GITHUB; return a; }
|
||||
snprintf(url, sizeof(url), "https://gitlab.com/api/v4/projects/%s/releases/permalink/latest", url_encode(norm));
|
||||
if ((a = fetch_latest(url, HOST_GITLAB, os))) { ctx->host = HOST_GITLAB; return a; }
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (ctx->host) {
|
||||
case HOST_GITLAB:
|
||||
snprintf(url, sizeof(url), "%s/releases/permalink/latest", ctx->api_url);
|
||||
break;
|
||||
default:
|
||||
snprintf(url, sizeof(url), "%s/releases/latest", ctx->api_url);
|
||||
break;
|
||||
}
|
||||
return fetch_latest(url, ctx->host, os);
|
||||
}
|
||||
|
||||
void repo_asset_free(ReleaseAsset *a) {
|
||||
if (!a) return;
|
||||
free(a->url); free(a->name); free(a->tag);
|
||||
free(a);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/* repo.h - git hosting adapters: GitHub / GitLab / Gitea / Forgejo */
|
||||
#ifndef PMM_REPO_H
|
||||
#define PMM_REPO_H
|
||||
|
||||
#include "pmm.h"
|
||||
|
||||
typedef struct {
|
||||
char *url; /* asset download URL */
|
||||
char *name; /* asset file name */
|
||||
char *tag; /* release tag */
|
||||
} ReleaseAsset;
|
||||
|
||||
typedef struct {
|
||||
PmmHost host; /* resolved host (HOST_AUTO is resolved during latest_asset) */
|
||||
char *api_url; /* resolved API endpoint (after mirror rewriting) */
|
||||
char *page_url; /* resolved raw/home endpoint */
|
||||
char *origin; /* scheme://host for full URLs (HOST_AUTO probing) */
|
||||
char *repo_norm; /* normalized "owner/repo" */
|
||||
} RepoContext;
|
||||
|
||||
/* Detect host from a repo slug like "owner/repo" or a full URL. */
|
||||
PmmHost repo_detect_host(const char *repo_or_url);
|
||||
|
||||
/* Build context for host + repo. repo may be "owner/repo" or full URL. */
|
||||
RepoContext *repo_open(PmmHost host, const char *repo, const char *mirror_api_base);
|
||||
|
||||
/* Get the download URL of the best asset for the current OS from the latest
|
||||
* release. Returns NULL if none found. Caller frees via repo_asset_free. */
|
||||
ReleaseAsset *repo_latest_asset(RepoContext *ctx, PmmOS os);
|
||||
|
||||
void repo_asset_free(ReleaseAsset *a);
|
||||
void repo_close(RepoContext *ctx);
|
||||
|
||||
/* Asset-extension <-> OS matching used by repo_latest_asset (exposed for tests). */
|
||||
int asset_matches_os(const char *filename, PmmOS os);
|
||||
|
||||
#endif
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/* sha1.c - FIPS 180-4 SHA-1 */
|
||||
#include "sha1.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ROTL(x,n) (((x) << (n)) | ((x) >> (32 - (n))))
|
||||
|
||||
static void transform(PmmSha1 *c, const unsigned char block[64]) {
|
||||
unsigned int w[80], a, b, d, e, f, k, tmp;
|
||||
unsigned int cc = c->state[2]; /* avoid shadowing */
|
||||
for (int i = 0, j = 0; i < 16; i++, j += 4)
|
||||
w[i] = ((unsigned int)block[j] << 24) | ((unsigned int)block[j+1] << 16) |
|
||||
((unsigned int)block[j+2] << 8) | (unsigned int)block[j+3];
|
||||
for (int i = 16; i < 80; i++)
|
||||
w[i] = ROTL(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
|
||||
a = c->state[0]; b = c->state[1]; unsigned int c2 = cc; d = c->state[3]; e = c->state[4];
|
||||
for (int i = 0; i < 80; i++) {
|
||||
if (i < 20) { f = (b & c2) | ((~b) & d); k = 0x5A827999; }
|
||||
else if (i < 40) { f = b ^ c2 ^ d; k = 0x6ED9EBA1; }
|
||||
else if (i < 60) { f = (b & c2) | (b & d) | (c2 & d); k = 0x8F1BBCDC; }
|
||||
else { f = b ^ c2 ^ d; k = 0xCA62C1D6; }
|
||||
tmp = ROTL(a, 5) + f + e + k + w[i];
|
||||
e = d; d = c2; c2 = ROTL(b, 30); b = a; a = tmp;
|
||||
}
|
||||
c->state[0]+=a; c->state[1]+=b; c->state[2]+=c2; c->state[3]+=d; c->state[4]+=e;
|
||||
}
|
||||
|
||||
void pmm_sha1_init(PmmSha1 *c) {
|
||||
c->count = 0;
|
||||
c->state[0]=0x67452301; c->state[1]=0xEFCDAB89; c->state[2]=0x98BADCFE;
|
||||
c->state[3]=0x10325476; c->state[4]=0xC3D2E1F0;
|
||||
}
|
||||
|
||||
void pmm_sha1_update(PmmSha1 *c, const unsigned char *data, size_t len) {
|
||||
size_t i = 0, j = (size_t)(c->count & 63);
|
||||
c->count += len;
|
||||
if (j + len > 63) {
|
||||
size_t fill = 64 - j;
|
||||
memcpy(c->buffer + j, data, fill);
|
||||
transform(c, c->buffer);
|
||||
for (i = fill; i + 63 < len; i += 64)
|
||||
transform(c, data + i);
|
||||
j = 0;
|
||||
}
|
||||
memcpy(c->buffer + j, data + i, len - i);
|
||||
}
|
||||
|
||||
void pmm_sha1_final(PmmSha1 *c, unsigned char out[20]) {
|
||||
unsigned char finalcount[8];
|
||||
unsigned long long bits = c->count * 8; /* bit length before padding */
|
||||
for (int i = 0; i < 8; i++)
|
||||
finalcount[i] = (unsigned char)((bits >> (56 - 8 * i)) & 0xFF);
|
||||
unsigned char pad = 0x80;
|
||||
pmm_sha1_update(c, &pad, 1);
|
||||
pad = 0x00;
|
||||
while ((c->count & 63) != 56)
|
||||
pmm_sha1_update(c, &pad, 1);
|
||||
pmm_sha1_update(c, finalcount, 8);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
out[i*4] = (unsigned char)(c->state[i] >> 24);
|
||||
out[i*4+1] = (unsigned char)(c->state[i] >> 16);
|
||||
out[i*4+2] = (unsigned char)(c->state[i] >> 8);
|
||||
out[i*4+3] = (unsigned char)(c->state[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void pmm_sha1_hex(const unsigned char digest[20], char *hex) {
|
||||
static const char *d = "0123456789abcdef";
|
||||
for (int i = 0; i < 20; i++) {
|
||||
hex[i*2] = d[digest[i] >> 4];
|
||||
hex[i*2+1] = d[digest[i] & 15];
|
||||
}
|
||||
hex[40] = '\0';
|
||||
}
|
||||
|
||||
int pmm_sha1_file(const char *path, char *hex) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
PmmSha1 c;
|
||||
unsigned char buf[65536], digest[20];
|
||||
size_t n;
|
||||
pmm_sha1_init(&c);
|
||||
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
|
||||
pmm_sha1_update(&c, buf, n);
|
||||
fclose(f);
|
||||
pmm_sha1_final(&c, digest);
|
||||
pmm_sha1_hex(digest, hex);
|
||||
return 0;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/* sha1.h */
|
||||
#ifndef PMM_SHA1_H
|
||||
#define PMM_SHA1_H
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
unsigned int state[5];
|
||||
unsigned long long count;
|
||||
unsigned char buffer[64];
|
||||
} PmmSha1;
|
||||
|
||||
void pmm_sha1_init(PmmSha1 *c);
|
||||
void pmm_sha1_update(PmmSha1 *c, const unsigned char *data, size_t len);
|
||||
void pmm_sha1_final(PmmSha1 *c, unsigned char out[20]);
|
||||
int pmm_sha1_file(const char *path, char *hex); /* 41 bytes incl NUL */
|
||||
void pmm_sha1_hex(const unsigned char digest[20], char *hex);
|
||||
|
||||
#endif
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/* sha256.c - FIPS 180-4 SHA-256 */
|
||||
#include "sha256.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const unsigned int K[64] = {
|
||||
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
||||
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
||||
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
||||
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
||||
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
||||
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
||||
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
||||
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
||||
};
|
||||
|
||||
#define ROTR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||
#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||||
#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||
#define EP0(x) (ROTR(x,2) ^ ROTR(x,13) ^ ROTR(x,22))
|
||||
#define EP1(x) (ROTR(x,6) ^ ROTR(x,11) ^ ROTR(x,25))
|
||||
#define SIG0(x) (ROTR(x,7) ^ ROTR(x,18) ^ ((x) >> 3))
|
||||
#define SIG1(x) (ROTR(x,17) ^ ROTR(x,19) ^ ((x) >> 10))
|
||||
|
||||
static void transform(PmmSha256 *c, const unsigned char data[64]) {
|
||||
unsigned int m[64], a, b, cc, d, e, f, g, h, t1, t2;
|
||||
for (int i = 0, j = 0; i < 16; i++, j += 4)
|
||||
m[i] = ((unsigned int)data[j] << 24) | ((unsigned int)data[j+1] << 16) |
|
||||
((unsigned int)data[j+2] << 8) | (unsigned int)data[j+3];
|
||||
for (int i = 16; i < 64; i++)
|
||||
m[i] = SIG1(m[i-2]) + m[i-7] + SIG0(m[i-15]) + m[i-16];
|
||||
a=c->state[0]; b=c->state[1]; cc=c->state[2]; d=c->state[3];
|
||||
e=c->state[4]; f=c->state[5]; g=c->state[6]; h=c->state[7];
|
||||
for (int i = 0; i < 64; i++) {
|
||||
t1 = h + EP1(e) + CH(e,f,g) + K[i] + m[i];
|
||||
t2 = EP0(a) + MAJ(a,b,cc);
|
||||
h=g; g=f; f=e; e=d+t1; d=cc; cc=b; b=a; a=t1+t2;
|
||||
}
|
||||
c->state[0]+=a; c->state[1]+=b; c->state[2]+=cc; c->state[3]+=d;
|
||||
c->state[4]+=e; c->state[5]+=f; c->state[6]+=g; c->state[7]+=h;
|
||||
}
|
||||
|
||||
void pmm_sha256_init(PmmSha256 *c) {
|
||||
c->datalen = 0;
|
||||
c->bitlen = 0;
|
||||
c->state[0]=0x6a09e667; c->state[1]=0xbb67ae85; c->state[2]=0x3c6ef372; c->state[3]=0xa54ff53a;
|
||||
c->state[4]=0x510e527f; c->state[5]=0x9b05688c; c->state[6]=0x1f83d9ab; c->state[7]=0x5be0cd19;
|
||||
}
|
||||
|
||||
void pmm_sha256_update(PmmSha256 *c, const unsigned char *data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
c->data[c->datalen++] = data[i];
|
||||
if (c->datalen == 64) {
|
||||
transform(c, c->data);
|
||||
c->bitlen += 512;
|
||||
c->datalen = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pmm_sha256_final(PmmSha256 *c, unsigned char out[32]) {
|
||||
unsigned int i = c->datalen;
|
||||
if (c->datalen < 56) {
|
||||
c->data[i++] = 0x80;
|
||||
while (i < 56) c->data[i++] = 0x00;
|
||||
} else {
|
||||
c->data[i++] = 0x80;
|
||||
while (i < 64) c->data[i++] = 0x00;
|
||||
transform(c, c->data);
|
||||
memset(c->data, 0, 56);
|
||||
}
|
||||
c->bitlen += (unsigned long long)c->datalen * 8;
|
||||
for (int j = 0; j < 8; j++)
|
||||
c->data[63 - j] = (unsigned char)(c->bitlen >> (8 * j));
|
||||
transform(c, c->data);
|
||||
for (int j = 0; j < 4; j++)
|
||||
for (int k = 0; k < 8; k++)
|
||||
out[j + 4*k] = (unsigned char)(c->state[k] >> (24 - j * 8));
|
||||
}
|
||||
|
||||
void pmm_sha256_hex(const unsigned char digest[32], char *hex) {
|
||||
static const char *d = "0123456789abcdef";
|
||||
for (int i = 0; i < 32; i++) {
|
||||
hex[i*2] = d[digest[i] >> 4];
|
||||
hex[i*2+1] = d[digest[i] & 15];
|
||||
}
|
||||
hex[64] = '\0';
|
||||
}
|
||||
|
||||
int pmm_sha256_file(const char *path, char *hex) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
PmmSha256 c;
|
||||
unsigned char buf[65536], digest[32];
|
||||
size_t n;
|
||||
pmm_sha256_init(&c);
|
||||
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
|
||||
pmm_sha256_update(&c, buf, n);
|
||||
fclose(f);
|
||||
pmm_sha256_final(&c, digest);
|
||||
pmm_sha256_hex(digest, hex);
|
||||
return 0;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/* sha256.h */
|
||||
#ifndef PMM_SHA256_H
|
||||
#define PMM_SHA256_H
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
unsigned int state[8];
|
||||
unsigned long long bitlen;
|
||||
unsigned char data[64];
|
||||
unsigned int datalen;
|
||||
} PmmSha256;
|
||||
|
||||
void pmm_sha256_init(PmmSha256 *c);
|
||||
void pmm_sha256_update(PmmSha256 *c, const unsigned char *data, size_t len);
|
||||
void pmm_sha256_final(PmmSha256 *c, unsigned char out[32]);
|
||||
/* Hex digest (65 bytes incl NUL). Returns 0 on success. */
|
||||
int pmm_sha256_file(const char *path, char *hex);
|
||||
void pmm_sha256_hex(const unsigned char digest[32], char *hex);
|
||||
|
||||
#endif
|
||||
在新工单中引用
屏蔽一个用户