<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:wfw="http://wellformedweb.org/CommentAPI/"
     >
  <channel>
    <title>Gregory Szorc's Digital Home</title>
    <link>http://gregoryszorc.com/blog</link>
    <description>Rambling on</description>
    <pubDate>Tue, 03 Dec 2024 19:27:50 GMT</pubDate>
    <generator>Blogofile</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Transferring Python Build Standalone Stewardship to Astral</title>
      <link>http://gregoryszorc.com/blog/2024/12/03/transferring-python-build-standalone-stewardship-to-astral</link>
      <pubDate>Tue, 03 Dec 2024 11:30:00 PST</pubDate>
      <category><![CDATA[Python]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2024/12/03/transferring-python-build-standalone-stewardship-to-astral</guid>
      <description>Transferring Python Build Standalone Stewardship to Astral</description>
      <content:encoded><![CDATA[<p>My <a href="/docs/python-build-standalone/main/">Python Standalone Builds</a> (PBS)
<a href="https://github.com/indygreg/python-build-standalone">project</a> provides
self-contained Python distributions that aim to <em>just work</em>.</p>
<p>PBS Python distributions are used by
<a href="https://github.com/astral-sh/uv">uv</a>, <a href="https://github.com/astral-sh/rye">Rye</a>,
<a href="https://github.com/bazelbuild/rules_python">Bazel's rules_python</a>, and
other tools in the Python ecosystem. The GitHub release artifacts have been
downloaded over 70,000,000 times.</p>
<p>With ruff and uv, Charlie Marsh and the folks at <a href="https://astral.sh/">Astral</a>
have been speedrunning building world class tooling for the Python ecosystem.</p>
<p>As I wrote in <a href="/blog/2024/03/17/my-shifting-open-source-priorities/">My Shifting Open Source Priorities</a>
in March 2024, changes in my life have resulted in me paring back my
open source activities.</p>
<p>Since that post, uv has exploded onto the scene. There have been over 50
million downloads of PBS release assets since March.</p>
<p>Charlie and Astral have stepped up and been outstanding open source
citizens when it comes to evolving PBS to support their work. When I
told Charlie I could use assistance supporting PBS, Astral employees
started contributing to the project. They have built out various
functionality, including Python 3.13 support (including free-threaded
builds), turnkey automated release publishing, and debug symbol stripped
builds to further reduce the download/install size. Multiple Astral
employees now have GitHub permissions to approve/merge PRs and publish
releases. All <a href="https://github.com/indygreg/python-build-standalone/releases">releases</a>
since April have been performed by Astral employees.</p>
<p>PBS today is effectively an Astral maintained project and has been that
way for months. As far as I can tell there have been only positive side effects
of this transition. When I ask myself why I should continue being the GitHub
maintainer, every answer distills down to personal pride or building a personal
brand. I need neither.</p>
<p>I agree with Charlie that formally transferring stewardship of my standalone
Python builds project to Astral is in the best interest of not only the
project itself but of the wider Python community.</p>
<p>On 2024-12-17 I will transfer
<a href="https://github.com/indygreg/python-build-standalone">indygreg/python-build-standalone</a>
into the <a href="https://github.com/astral-sh">astral-sh</a> GitHub organization. From
there, Astral will lead its development and evolution.</p>
<p>I will retain my GitHub permissions on the project and hope to stay involved
in its development, if nothing more than a periodic advisor.</p>
<p>Astral has clearly demonstrated competency for execution and has the needs of
Python developers at heart. I have no doubt that PBS will thrive under Astral's
stewardship. I can't wait to see what they do next.</p>
<p>Astral has <a href="https://astral.sh/blog/python-build-standalone">also blogged</a> about
this announcement and I encourage interested parties to read it as well.</p>]]></content:encoded>
    </item>
    <item>
      <title>My Shifting Open Source Priorities</title>
      <link>http://gregoryszorc.com/blog/2024/03/17/my-shifting-open-source-priorities</link>
      <pubDate>Sun, 17 Mar 2024 21:00:00 PDT</pubDate>
      <category><![CDATA[Personal]]></category>
      <category><![CDATA[PyOxidizer]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2024/03/17/my-shifting-open-source-priorities</guid>
      <description>My Shifting Open Source Priorities</description>
      <content:encoded><![CDATA[<p>I'm a maintainer of a handful of open source projects, some of which have
millions of downloads and/or are used in important workloads, including in
production.</p>
<p>I have a full time job as a software engineer and my open source work is
effectively a side job. (Albeit one I try very hard to not let intersect
with my day job.)</p>
<p>Historically, my biggest contributions to my open source projects have come
when I'm not working full time:</p>
<ul>
<li><a href="https://github.com/indygreg/python-zstandard">python-zstandard</a> was started
  when I was on medical leave, recovering from a surgery.</li>
<li><a href="https://github.com/indygreg/python-build-standalone">python-build-standalone</a>
  and <a href="https://github.com/indygreg/PyOxidizer">PyOxidizer</a> were mainly built
  when I was between jobs, after leaving Mozilla.</li>
<li><a href="https://github.com/indygreg/apple-platform-rs/tree/main/apple-codesign">apple-codesign</a>
  was built in the height of COVID when I took a voluntary leave of absence
  from work to reconstitute my mental and physical health.</li>
</ul>
<p>When working full time, my time to contribute to open source has been carved out
of weekday nights and weekends, especially in the winter months. I believe
that code is an art form and programming a form of creative expression. My open
source contributions provide a relaxing avenue for me to express my artistic
creativity, when able.</p>
<p>My open source contributions reflect my personal priorities of where and what
to spend my free time on.</p>
<p>The only constant in life is change.</p>
<p>In the middle of 2022, I switched job roles and found myself reinvigorated by my
new role - Infrastructure Performance - which is at the intersection of some of
my strongest technical and professional skills. I found myself willingly
pouring more energy and time into my day job. That had the side effect of
reducing my open source contributions.</p>
<p>In 2023Q1 I got married. In the months leading up to and after, I chose to
prioritize spending time with my now wife and all the time commitments that
entails. This also reduced the amount of time available for open source
contributions.</p>
<p>In 2023Q4 I became a father to a beautiful baby girl. While on my employer's
generous-for-the-United-States fourteen week paternity leave, I somehow found
some time to contribute to open source. As refreshing as that was, it didn't
last. My <em>man cave</em> where my desktop computer resides has been converted into
a nursery. And for the past few months it has been occupied by my mother-in-law,
who has been generously effectively serving as a live-in nanny. Even when I'm
able to sit down at my desktop, it's hard to get into a state of flow due to
the added entropy from the additional three people now living with me.</p>
<p>After realizing the new normal in 2024Q1, I purchased a <a href="https://www.wahoofitness.com/devices/indoor-cycling/bike-trainers/kickr-move-buy">Wahoo KICKR MOVE</a>
bicycle trainer and now spend considerable time doing virtual bicycle rides on
<a href="https://www.zwift.com/">Zwift</a> because its one of the few <em>leisure</em> activities
I can do at home without drawing scrutiny from my wife and mother-in-law (but
98% my mother-in-law because I've observed that my wife is effectively
infallible). I now get excited about virtually summiting famous climbs instead of
contributing to open source. (Today's was
<a href="https://pjammcycling.com/climb/40.Mont-Ventoux-Bedoin">Mont Ventoux</a> - an
absolute beast of a climb that reminded me a lot of my real world ride up
<a href="https://pjammcycling.com/climb/11.Pikes-Peak">Pike's Peak</a> in 2020.)</p>
<p>Various changes in the past eighteen or so months have created additional
time constraints and prioritization changes that have resulted in my open
source contributions withering.</p>
<p>In addition, my technical interests have been shifting.</p>
<p>I've always gravitated to more systems-level areas of computers. My degree
is in Computer Engineering and I have a stereotypical engineer mindset: I
have an insatiable curiosity about how things work and interact and I want
to always be tinkering. I prefer to be closer to hardware instead of abstracted
far away from it. I enjoy interacting with the building blocks of software
ecosystems: operating systems, filesystems, runtimes, file formats, compilers,
etc.</p>
<p>Historically, my open source contributions to my preferred areas of
computing were limited. Again, to me open source is an enjoyable form of
creative expression. That means I do it for <em>fun</em>. Historically, the
<em>systems-level</em> programming space was limited to languages like C and
C++, which I consider frustrating and painful to use. If I'm going to subject
myself to misery when programming, you are going to have to pay me well to
do it.</p>
<p>As part of creating PyOxidizer, I learned Rust.</p>
<p>When I became proficient in Rust, I realized that Rust unlocks all kinds of
systems-level problems that were effectively off-limits for my open source
contributions. Would I <a href="/blog/2022/01/03/rust-implementation-of-debian-packaging-primitives/">implement</a>
Debian packaging primitives in Python? Or a <a href="/blog/2022/01/09/bulk-analyze-linux-packages-with-linux-package-analyzer/">tool</a>
to bulk analyze Linux packages and peek inside ELF binaries for insights
about what compiler/linker features are used in the wild in Python/C/C++?
Not unless you pay me to do it!</p>
<p>As I learned Rust, I also found myself being drawn away from Python, my
prior go-to language. As I wrote in <a href="/blog/2021/04/13/rust-is-for-professionals/">Rust is for Professionals</a>,
Rust feels surprisingly high level. It isn't as terse as Python but it is
a lot closer than I thought it would be. And Rust gives you vastly stronger
compile-time guarantees and run-time performance than Python. I felt like
Rust's tooling ecosystem was supporting me instead of standing in my way. I
felt that when you consider the overall software development lifecycle - not
just the edit-build-run loop that people tend to fixate on, likely because
it is the easiest to measure - Rust was vastly more productive and a joy to
work with than Python. All those countless hours debugging, fixing, and
authoring tests for <code>TypeError</code> and <code>ValueError</code> Python exceptions you see
in production just don't happen with Rust and that time can be better spent
iterating on core functionality, which is what actually matters. </p>
<p>On top of the Rust undercurrents, I've also become somewhat disenchanted with
the Python ecosystem. As I wrote in 2020's <a href="/blog/2020/01/13/mercurial's-journey-to-and-reflections-on-python-3/">Mercurial's Journey to and Reflections on
Python 3</a>,
the Python 3 transition was bungled and resulted in years - if not a full
decade - of lost opportunity. As I wrote in 2023's <a href="/blog/2023/10/30/my-user-experience-porting-off-setup.py/">My User Experience Porting Off
setup.py</a>, the Python
packaging story feels as discombobulated and frustrating as ever. PyOxidizer
additionally brushed up against <a href="/docs/pyoxidizer/0.24.0/pyoxidizer_technotes.html#desired-changes-from-python-to-aid-pyoxidizer">several limitations</a>
in how Python is designed and implemented, many of which are not trivially
fixable. As a <em>systems-level guy</em>, I am frequently questioning various aspects
of the Python ecosystem which I have contrasting opinions on, including the
importance of correctness and performance.</p>
<p>Starting in 2021, I started gravitating towards writing more Rust code and solving
problems in the systems domain that were previously off-limits to me, like Apple
code signing. Initially the work was in support of PyOxidizer: I was going to
implement all these packaging primitives in pure Rust and enable people to
distribute Python applications without requiring access to a Windows or macOS
machine! Over time, this work consumed me. Apple code signing turned into a major
time sink because of its complexity and the fact I was having to reverse
engineer a lot of its internals. But I was having a ton of fun doing it: more
fun than swimming upstream against decades of encrusted technical debts in the
Python ecosystem.</p>
<p>By late 2021, I realized I made a series of <em>mistakes</em> with PyOxidizer.</p>
<p>I started PyOxidizer as a science experiment to see if it was possible to achieve
a single file executable Python application without requiring a <em>temporary</em>
filesystem at run-time. I succeeded. But the cost was compatibility with the
larger pre-built Python package ecosystem. I built all this complexity into
PyOxidizer to allow people to tweak how Python resources are packaged so they
could choose to build a single file application if they wanted. This ballooned
into a hot mess and was obviously not user-friendly. It violated various
personal principles about optimizing for end-user experience.</p>
<p>Armed with knowledge of all the pitfalls, I realized that there was a 90%
use case for Python application packaging that was simple for end users and
technically achievable using all the code primitives - like the
<a href="https://crates.io/crates/pyembed">pyembed Rust crate</a> - that I built out for
PyOxidizer.</p>
<p>Thus the <a href="/blog/2022/05/10/announcing-the-pyoxy-python-runner/">PyOxy project</a> was born
and released in May 2022.</p>
<p>While I believe PyOxy is already a generally useful primitive to have in the
Python ecosystem, I had bigger goals in mind.</p>
<p>My intent with PyOxy was to build in a simplified and opinionated <em>PyOxidizer
lite</em> mode. The <code>pyoxy</code> executable is already a chameleon: if you rename it to
<code>python</code> it behaves like a <code>python</code> executable. I wanted to extend this so you
could do something like <code>pyoxy build-app</code> and it would collect all dependencies,
assemble a
<a href="https://gregoryszorc.com/docs/pyoxidizer/0.24.0/oxidized_importer_packed_resources.html">Python packed resources</a>
blob, and embed that in a copy of the <code>pyoxy</code> binary as an ELF, Mach-O, or PE
segment. Then at run-time, the variant executable binary would load the application
configuration and Python resources metadata from its own binary and execute the
application. Essentially, PyOxy would evolve into a self-packaging Python
application. I <em>just</em> needed to evolve the Python packed resources format,
implement a very crude ELF, Mach-O, and PE <em>linker</em> to append resources data to an
executable, and teach <code>pyembed</code> to read resources data from an ELF, Mach-O, or
PE segment. All within my sphere of technical competency. And I was excited to
build it and forever alter people's perceptions of how easy it could be to produce
a distributable Python application.</p>
<p>Then the roller coaster of my personal life took over. I felt newly invigorated
with a new job role. I got engaged and married. I became a father.</p>
<p>By early 2023, it was clear my ability to contribute to open source would be
vastly diminished for the foreseeable future. PyOxidizer and PyOxy fell into a
state of neglect. Weeks went by without me even tinkering on my local computer,
much less push commits or publish a release. Weeks turned into months. Months
into quarters. At this point, I haven't pushed a commit to
<a href="https://github.com/indygreg/PyOxidizer">indygreg/PyOxidizer</a> since January 2023.
And I'm not sure when I next will, if ever.</p>
<p>In my limited open source contribution time, I've prioritized other projects
over PyOxidizer.</p>
<p><a href="https://github.com/indygreg/python-build-standalone">python-build-standalone</a>
has gained a life outside PyOxidizer. It is now used by
<a href="https://github.com/astral-sh/rye">rye</a>, Bazel's
<a href="https://github.com/bazelbuild/rules_python">rules_python</a>,
<a href="https://beeware.org/project/projects/tools/briefcase/">briefcase</a>, and a myriad
of other consumers. The release assets have been downloaded over 23 million
times and the download rate appears to be accelerating. I still actively
support python-build-standalone and intend for the project to be actively
supported for the indefinite future: it has become too important to abandon. I'm
actively recruiting assistance to help maintain the project and I'm not concerned
about its future.</p>
<p>Apple code signing still actively draws my engagement. What I love about the
project is it either works or it doesn't: there's limited extra features we can
add to it since Apple mostly dictates the feature set. And I perceive the current
project to be mostly <em>done</em>.</p>
<p><a href="https://github.com/indygreg/python-zstandard">python-zstandard</a> is downloaded
~8 million times per month. The project is long overdue for some modernization.
I'm sitting on a pile of commits to improve it, but progress has been slow. I
just learned this weekend that the maintainer of the other popular zstandard
Python package deleted their GitHub account recently and now users are looking to
onboard to my package. Nothing quite like unanticipated distractions!</p>
<p><strong>That's a very long-winded way of saying that PyOxidizer and all the projects under
its umbrella are effectively in a zombie state.</strong> I'm hesitant to say <em>dead</em> because
if I suddenly found myself with lots of free time I'd love to brush off the cobwebs
and bring the projects back to life. But who am I kidding: they are effectively dead
at the moment because with everything happening in my personal life, I don't see where
I find the time to resuscitate the project. And that assumes I even want to: again,
I've become somewhat disenchanted by the state of Python. The main thing that draws
me to it is the size of the community and the potential for impact. But to realize
that impact I feel like I'd be pushing Python in directions it isn't well-equipped
to go in. Quite franky - and, yes, selfishly - I don't want to subject myself to
the misery unless I'm being well paid to do it. Again, I view my open source
contributions as a <em>fun</em> outlet for my creative expression and nudging Python
packaging in directions it is obviously ill-equipped to go in just isn't <em>fun</em>.</p>
<p><strong>If anyone reading has an interest in taking ownership or maintenance responsibilities
of PyOxidizer, any projects under its umbrella, or any of my other open source projects,
I'm receptive to proposals.</strong> Send me an <a href="mailto:gregory.szorc@gmail.com">email</a>
or create an issue or discussion on GitHub if you want to do it publicly.</p>
<p>But I'm going to assume that PyOxidizer is going to wither and die - or at least
incur some massive backwards incompatible breaks if it continues to live. I've already
filed issues against python-build-standalone - such as
<a href="https://github.com/indygreg/python-build-standalone/issues/221">removing Windows static builds</a> -
to make the project easier to support and less work for future maintainers.</p>
<p>If I have one regret about how this has played out, it is my failure to
communicate developments in my open source commitments / expectations in a timely manner.
I knew the future was bleak in early 2023 but didn't publicly say anything.
I still thought there was a chance that things were going to change and I didn't
want to make a hard decision prematurely. Writing this post has been on my mind
since the middle of 2023 but I just couldn't bring myself to write it. And -
surprise - having a newborn at home is a giant time and mental commitment! I'm
writing this now because people are (finally!) noticing my lack of contributions to
PyOxidizer and asking questions. And I'm home alone for a few days and actually
have time to sit down and compose this post. (Yes, I'm that stretched for time in
my personal life.)</p>
<p>In 2023, I struggled with the idea of letting people down by declaring PyOxidizer
<em>dead</em>. But when I wake up every morning, walk into the nursery, and cause my daughter
to smile and flail her arms and legs with unbridled excitement when she sees me, I'd
have it no other way. When it comes to choosing between open source and family, I
choose family.</p>
<p>It feels appropriate to end this post with a link to <a href="https://xkcd.com/2347/">XKCD 2347: Dependency</a>.
But I'm not the <em>random person in Nebraska</em>: I'm a husband and father.</p>]]></content:encoded>
    </item>
    <item>
      <title>My User Experience Porting Off setup.py</title>
      <link>http://gregoryszorc.com/blog/2023/10/30/my-user-experience-porting-off-setup.py</link>
      <pubDate>Mon, 30 Oct 2023 06:00:00 PDT</pubDate>
      <category><![CDATA[Python]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2023/10/30/my-user-experience-porting-off-setup.py</guid>
      <description>My User Experience Porting Off setup.py</description>
      <content:encoded><![CDATA[<p>In the past week I went to add Python 3.12 support to my
<a href="https://github.com/indygreg/python-zstandard">zstandard</a> Python package.
A few hours into the unexpected yak shave / rat hole, I decided to start
chronicling my experience so that I may share it with the broader Python
community. My hope is that by sharing my (unfortunately painful) end-user
experience that I can draw attention to aspects of Python packaging that
are confusing so that better informed and empowered people can improve
matters and help make future Python packaging decisions to help scenarios
like what I'm about to describe.</p>
<p>This blog post is purposefully verbose and contains a very lightly edited
stream of my mental thoughts. Think of it as a self-assessed user experience
study of Python packaging.</p>
<h2>Some Background</h2>
<p>I'm no stranger to the Python ecosystem or Python packaging. I've been
programming Python for 10+ years. I've even authored a Python application
packaging tool, <a href="https://gregoryszorc.com/docs/pyoxidizer/main/">PyOxidizer</a>.</p>
<p>When programming, I strive to understand how things work. I try to not blindly
copy-paste or cargo cult patterns unless I understand how they work. This
means I often scope bloat myself and slow down velocity in the short term.
But I justify this practice because I find it often pays dividends in the long
term because I actually understand how things work.</p>
<p>I also have a passion for security and supply chain robustness. After you've
helped maintain complex CI systems for multiple companies, you learn the hard
way that it is important to do things like transitively pin dependencies and
reduce surface area for failures so that build automation breaks in reaction
to code changes in your version control, not spooky-action-at-a-distance when
state on a third party server changes (e.g. a new package version is uploaded).</p>
<p>I've been aware of the emergence of <code>pyproject.toml</code>. But I've largely sat on
the sidelines and held off adopting them, mainly for <em>if it isn't broken, don't
fix it</em> reasons. Plus, my perception has been that the tooling still hasn't
stabilized: I'm not going to incur work now if it is going to invite avoidable
churn that could be avoided by sitting on my hands a little longer.</p>
<p>Now, on to my user experience of adding Python 3.12 to python-zstandard and
the epic packaging yak shave that entailed.</p>
<h2>The Journey Begins</h2>
<p>When I attempted to run CI against Python 3.12 on GitHub Actions, running
<code>python setup.py</code>complained that <code>setuptools</code> couldn't be imported.</p>
<p>Huh? I thought <code>setuptools</code> was installed in pretty much every Python
distribution by default? It was certainly installed in all previous Python
versions by the <a href="https://github.com/actions/setup-python">actions/setup-python</a>
GitHub Action. I was aware <code>distutils</code> was removed from the Python 3.12
standard library. But setuptools and distutils are not the same! Why did
<code>setuptools</code> disappear?</p>
<p>I look at the CI logs for the passing Python 3.11 job and notice a message:</p>
<div class="pygments_murphy"><pre><span></span>********************************************************************************
Please avoid running ``setup.py`` directly.
Instead, use pypa/build, pypa/installer or other
standards-based tools.

See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.
********************************************************************************
</pre></div>

<p>I had several immediate reactions:</p>
<ol>
<li>OK, maybe this is a sign I should be modernizing to <code>pyproject.toml</code> and
   moving away from <code>python setup.py</code>. Maybe the missing <code>setuptools</code> in the
   3.12 CI environment is a side-effect of this <em>policy</em> shift?</li>
<li>What are <code>pypa/build</code> and <code>pypa/installer</code>? I've never heard of them. I know
   <code>pypa</code> is the Python Packaging Authority (I suspect most Python developers
   don't know this). Are these GitHub <em>org/repo</em> identifiers?</li>
<li>What exactly is a <em>standards-based tool</em>? Is pip not a <em>standards-based tool</em>?</li>
<li>Speaking of pip, why isn't it mentioned? I thought pip was the de facto
   packaging tool and had been for a while!</li>
<li>It's linking a URL for more info. But why is this a link to what looks like
   an individual's blog and not to some more official site, like the setuptools
   or pip docs? Or anything under python.org?</li>
</ol>
<h2>Learning That I Shouldn't Invoke <code>python setup.py</code></h2>
<p>I open <a href="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html">https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html</a>
in my browser and see a 4,000+ word blog post. Oof. Do I really want/need to read
this? Fortunately, the author included a tl;dr and linked to a
<a href="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary">summary</a>
section telling me a lot of useful information! It informs me (my commentary in
parentheses):</p>
<ol>
<li><em>The setuptools project has stopped maintaining all direct invocations of setup.py
   years ago</em>. (What?!)</li>
<li><em>There are undoubtedly many ways that your setup.py-based system is broken today,
   even if it's not failing loudly or obviously.</em>. (What?! Surely this can't be
   true. I didn't see any warnings from tooling until recently. How was I supposed
   to know this?)</li>
<li><em>PEP 517, 518 and other standards-based packaging are the future of the Python
   ecosystem</em>. (A ha - a definition of <em>standards-based</em> tooling. I guess I have
   to look at PEP 517 and PEP 518 in more detail. I'm pretty sure these are
   the PEPs that define <code>pyproject.toml</code>.)</li>
<li><em>At this point you may be expecting me to give you a canonical list of the right
   way to do everything that setup.py used to do, and unfortunately the answer here
   is that it's complicated.</em> (You are telling me that we had a working
   <code>python setup.py</code> solution for 10+ years, this workflow is now quasi deprecated,
   and the recommended replacement is <em>it's complicated</em>?! I'm just trying to get my
   package modernized. Why does that need to be <em>complicated</em>?)</li>
<li><em>That said, I can give you some simple "works for most people" recommendations for
   some of the common commands.</em> (Great, this is exactly what I was looking for!)</li>
</ol>
<p>Then I look at the table mapping old ways to new ways. In the <em>new</em> column, it
references the following tools: <a href="https://pypa-build.readthedocs.io/en/stable/">build</a>,
pytest, <a href="https://tox.wiki/en/latest/">tox</a>, <a href="https://tox.wiki/en/latest/">nox</a>,
pip, and <a href="https://twine.readthedocs.io/en/latest/">twine</a>. That's quite the
tooling salad! (And that <code>build</code> tool must be the <em>pypa/build</em> referenced in the
setuptools warning message. One mystery solved!)</p>
<p>I scroll back to the top of the article and notice the date: October 2021. Two
years old. The summary section also mentioned that there's been a lot of
activity around packaging tooling occurring. So now I'm wondering if this blog
post is outdated. Either way, it is clear I have to perform some additional
research to figure out how to migrate off <code>python setup.py</code> so I can be
compliant with the new world order.</p>
<h2>Learning About <code>pyproject.toml</code> and Build Systems</h2>
<p>I had pre-existing knowledge of <code>pyproject.toml</code> as the modern way to define
build system metadata. So I decide to start my research by Googling
<code>pyproject.toml</code>. The first results are:</p>
<ol>
<li><a href="https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/">https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/</a></li>
<li><a href="https://stackoverflow.com/questions/62983756/what-is-pyproject-toml-file-for">https://stackoverflow.com/questions/62983756/what-is-pyproject-toml-file-for</a></li>
<li><a href="https://python-poetry.org/docs/pyproject/">https://python-poetry.org/docs/pyproject/</a></li>
<li><a href="https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html">https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html</a></li>
<li><a href="https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/">https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/</a></li>
<li><a href="https://towardsdatascience.com/pyproject-python-9df8cc092f61">https://towardsdatascience.com/pyproject-python-9df8cc092f61</a></li>
</ol>
<p>I click pip's documentation first because pip is known to me and it seems a
canonical source. Pip's documentation proceeds to link to
<a href="https://peps.python.org/pep-0518/">PEP-518</a>, <a href="https://peps.python.org/pep-0517/">PEP-517</a>,
<a href="https://peps.python.org/pep-0621/">PEP-621</a>, and <a href="https://peps.python.org/pep-0660/">PEP-660</a>
before telling me how projects with <code>pyproject.toml</code> are built, without giving
me - a package maintainer - much useful advice for what to do or how to port from
<code>setup.py</code>. This seems like a dead end.</p>
<p>Then I look at the Stack Overflow link. Again, telling me a lot of what I don't
really care about. (I've somewhat lost faith in Stack Overflow and only really
skimmed this page: I would much prefer to get an answer from a first party
source.)</p>
<p>I click on the <a href="https://python-poetry.org/docs/pyproject/">Poetry</a> link. It
documents TOML fields. But only for the <code>[tool.poetry]</code> section. While I've
heard about Poetry, I know that I probably don't want to scope bloat myself
to learn how Poetry works so I can use it. (No offence meant to the Poetry
project here but I don't perceive my project as needing whatever features
Poetry provides: I'm <em>just</em> trying to publish a simple library package.) I
go back to the search results.</p>
<p>I click on the <a href="https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html">setuptools</a>
link. I'm using setuptools via <code>setup.py</code> so this content looks promising! It gives
me a nice example TOML of how to configure a <code>[build-system]</code> and <code>[project]</code>
metadata. It links to PyPA's <a href="https://packaging.python.org/en/latest/specifications/declaring-project-metadata/">Declaring project metadata</a>
content, which I open in a new tab, as the content seems useful. I continue
reading setuptools documentation. I land on its
<a href="https://setuptools.pypa.io/en/latest/userguide/quickstart.html">Quickstart</a>
documentation, which seems useful. I start reading it and it links to the
<a href="https://pypa-build.readthedocs.io/en/latest/">build</a> tool documentation.
That's the second link to the <code>build</code> tool. So I open that in a new tab.</p>
<p>At this point, I think I have all the documentation on <code>pyproject.toml</code>. But
I'm still trying to figure out what to replace <code>python setup.py</code> with. The
<code>build</code> tool certainly seems like a contender since I've seen multiple
references to it. But I'm still looking for <em>modern</em>, actively maintained
documentation pointing me in a blessed direction.</p>
<p>The next Google link is <a href="https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/">A Practical Guide to Setuptools and Pyproject.toml</a>.
I start reading that. I'm immediately confused because it is recommending I
put setuptools metadata in <code>setup.cfg</code> files. But I just read all about defining
this metadata in <code>pyproject.toml</code> files in setuptools' own documentation! Is
this blog post out of date? March 12, 2022. Seems pretty modern. I look at the
setuptools documentation again and see the <code>pyproject.toml</code> metadata pieces are
in version 61.0.0 and newer. I go to
<a href="https://github.com/pypa/setuptools/releases/tag/v61.0.0">https://github.com/pypa/setuptools/releases/tag/v61.0.0</a>
and see version 61.0.0 was released on March 25, 2022. So the fifth Google link
was seemingly obsoleted 13 days after it was published. Good times. I pretend I
never read this content because it seems out of date.</p>
<p>The next Google link is
<a href="https://towardsdatascience.com/pyproject-python-9df8cc092f61">https://towardsdatascience.com/pyproject-python-9df8cc092f61</a>.
I click through. But Medium wants me to log in to read it all and it is unclear
it is going to tell me anything important, so I back out.</p>
<h2>Learning About the <code>build</code> Tool</h2>
<p>I give up on Google for the moment and start reading up on the <code>build</code> tool
from its docs.</p>
<p>The only usage documentation for the <code>build</code> tool is on its
<a href="https://pypa-build.readthedocs.io/en/latest/index.html">root documentation page</a>.
And that documentation basically prints what <code>python -m build --help</code> would
print: says what the tool does but doesn't give any guidance or where I should
be using it or how to replace existing tools (like <code>python setup.py</code> invocations).
Yes, I can piece the parts together and figure out that <code>python -m build</code> can be
used as a replacement for <code>python setup.py sdist</code> and <code>python setup.py bdist_wheel</code>
(and maybe <code>pip wheel</code>?). But <em>should</em> it be the replacement I choose? I make
use of <code>python setup.py develop</code> and the aforementioned blog post recommended
replacing that with <code>python -m pip install -e</code>. Perhaps I can use <code>pip</code> as the
singular replacement for building source distributions and binary wheels so I
have N-1 packaging tools? I keep researching.</p>
<h2>Exploring the Python Packaging User Guide</h2>
<p>I had previously opened <a href="https://packaging.python.org/en/latest/specifications/declaring-project-metadata/">https://packaging.python.org/en/latest/specifications/declaring-project-metadata/</a>
in a browser tab without really looking at it. On second glance, I see it is part
of a broader <a href="https://packaging.python.org/en/latest/">Python Packaging User Guide</a>.
Oh, this looks promising! A guide on how to do what I'm seeking maintained by the
Python Packaging Authority (PyPA), the group who I know to be the, well, authorities
on Python packaging. It is is published under the canonical <code>python.org</code> domain.
Surely the answer will be here.</p>
<p>I immediately click on the link to
<a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/">Packaging Python Projects</a>
to hopefully see what the PyPA folks are recommending.</p>
<h3>Is Hatch the Answer?</h3>
<p>I skim through. I see recommendations to use a <code>pyproject.toml</code> with a
<code>[build-system]</code> to define the build backend. This matches my expectations.
But they are using <em>Hatchling</em> as their build backend. Another tool I don't
really know about. I click through some inline links and eventually arrive
at <a href="https://github.com/pypa/hatch">https://github.com/pypa/hatch</a>. (I'm kind of
confused why the PyPA tutorial said <em>Hatchling</em> when the project and tool is
apparently named <em>Hatch</em>. But whatever.)</p>
<p>I skim Hatch's GitHub README. It looks like a unified packaging tool. Build
system. Package uploading/publishing. Environment management (sounds like a
virtualenv alternative?). This tool actually seems quite nice! I start skimming
the docs. Like Poetry, it seems like this is yet another new tool that I'd need
to learn and would require me to blow up my existing <code>setup.py</code> in order to
adopt. Do I really want to put in that effort? I'm just trying to get
python-zstandard back on the paved road and avoid seemingly deprecated workflows:
I'm not looking to adopt new tooling stacks.</p>
<p>I'm also further confused by the existence of Hatch under the
<a href="https://github.com/pypa">PyPA GitHub Organization</a>. That's the same
GitHub organization hosting the Python packaging tools that are known to
me, namely
<a href="https://github.com/pypa/build">build</a>, <a href="https://github.com/pypa/pip">pip</a>,
and <a href="https://github.com/pypa/setuptools">setuptools</a>. Those three projects
are pinned repositories. (The other three pinned repositories are
<a href="https://github.com/pypa/virtualenv">virtualenv</a>,
<a href="https://github.com/pypa/wheel">wheel</a>, and
<a href="https://github.com/pypa/twine">twine</a>.) Hatch is seemingly a replacement
for pip, setuptools, virtualenv, twine, and possibly other tools. But it isn't
a pinned repository. Yet it is the default tool used in the PyPA maintained
<a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/">Packaging Python Projects</a>
guide. (That guide also suggests using other tools like setuptools,
<a href="https://github.com/pypa/flit">flit</a>, and
<a href="https://github.com/pdm-project/pdm/.">pdm</a>. But the default is Hatch and that has me asking questions. Also,
I didn't initially notice that
<a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/#creating-pyproject-toml">Creating pyproject.toml</a>
has multiple tabs for different backends.)</p>
<p>While Hatch looks interesting, I'm just not getting a strong signal that Hatch
is sufficiently stable or warrants my time investment to switch to. So I go
back to reading the
<a href="https://packaging.python.org/en/latest/">Python Packaging User Guide</a>.</p>
<h3>The PyPA User Guide Search Continues</h3>
<p>As I click around the User Guide, it is clear the PyPA folks really want me
to use <code>pyproject.toml</code> for packaging. I suppose that's the future and that's
a fair ask. But I'm still confused how I should migrate my <code>setup.py</code> to it.
What are the risks with replacing my <code>setup.py</code> with <code>pyproject.toml</code>? Could
I break someone installing my package on an old Linux distribution or old
virtualenv using an older version of setuptools or pip? Will my adoption of
build, hatch, poetry, whatever constitute a one way door where I lock out
users in older environments? My package is downloaded over one million times
per month and if I break packaging <em>someone</em> is likely to complain.</p>
<p>I'm desperately looking for guidance from the PyPA at
<a href="https://packaging.python.org/">https://packaging.python.org/</a> on how to
manage this migration. But I just... can't find it.
<a href="https://packaging.python.org/en/latest/guides/">Guides</a> surprisingly has
nothing on the topic.</p>
<h3>Outdated Tool Recommendations from the PyPA</h3>
<p>Finally I find <a href="https://packaging.python.org/en/latest/guides/tool-recommendations/">Tool recommendations</a>
in the PyPA User Guide. Under
<a href="https://packaging.python.org/en/latest/guides/tool-recommendations/#packaging-tool-recommendations">Packaging tool recommendations</a>
it says:</p>
<ul>
<li><em>Use setuptools to define projects.</em></li>
<li><em>Use build to create Source Distributions and wheels.</em></li>
<li><em>If you have binary extensions and want to distribute wheels for multiple
  platforms, use cibuildwheel as part of your CI setup to build distributable
  wheels.</em></li>
<li><em>Use twine for uploading distributions to PyPI.</em></li>
</ul>
<p>Finally, some canonical documentation from the PyPA that comes out and
suggests what to use!</p>
<p>But my relief immediately turns to questioning whether this tooling
recommendations documentation is up to date:</p>
<ol>
<li>If setuptools is recommended, why does the
   <a href="https://packaging.python.org/en/latest/tutorials/packaging-projects/">Packaging Python Projects</a>
   tutorial use Hatch?</li>
<li>How exactly should I be using setuptools to define projects? Is this
   referring to setuptools as a <code>[build-system]</code> backend? The existence of
   <em>define</em> seemingly implies using <code>setup.py</code> or <code>setup.cfg</code> to define metadata.
   But I thought these distutils/setuptools specific mechanisms were deprecated
   in favor of the more generic <code>pyproject.toml</code>?</li>
<li>Why aren't other tools like Hatch, pip, poetry, flit, and pdm mentioned on
   this page? Where's the guidance on when to use these alternative tools?</li>
<li>There are footnotes referencing <code>distutils</code> as if it is still a modern
   practice. No mention that it was removed from the standard library in
   Python 3.12.</li>
<li>But the <code>build</code> tool is referenced and that tool is relatively new. So the
   docs have to be somewhat up-to-date, right?</li>
</ol>
<p>Sadly, I reach the conclusion that this
<a href="https://packaging.python.org/en/latest/guides/tool-recommendations/">Tool recommendations</a>
documentation is inconsistent with newer documentation and can't be trusted.
But it did mention the <code>build</code> tool and we now have multiple independent
sources steering me in the direction of the <code>build</code> tool (at least for source
distribution and wheel building), so it seems like we have a winner on our
hands.</p>
<h2>Initial Failures Running <code>build</code></h2>
<p>So let's use the <code>build</code> tool. I remember docs saying to invoke it with
<code>python -m build</code>, so I try that:</p>
<div class="pygments_murphy"><pre><span></span>$ python3.12 -m build --help
No module named build.__main__; &#39;build&#39; is a package and cannot be directly executed
</pre></div>

<p>So the <code>build</code> package exists but it doesn't have a <code>__main__</code>. Ummm.</p>
<div class="pygments_murphy"><pre><span></span>$ python3.12R
Python 3.12.0 (main, Oct 23 2023, 19:58:35) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
&gt;&gt;&gt; import build
&gt;&gt;&gt; build.__spec__
ModuleSpec(name=&#39;build&#39;, loader=&lt;_frozen_importlib_external.NamespaceLoader object at 0x10d403bc0&gt;, submodule_search_locations=_NamespacePath([&#39;/Users/gps/src/python-zstandard/build&#39;]))
</pre></div>

<p>Oh, it picked up the <code>build</code> directory from my source checkout because
<code>sys.path</code> has the current directory by default. Good times.</p>
<div class="pygments_murphy"><pre><span></span>$ (cd ~ &amp;&amp; python3.12 -m build)
/Users/gps/.pyenv/versions/3.12.0/bin/python3.12: No module named build
</pre></div>

<p>I guess <code>build</code> isn't installed in my Python distribution / environment.
You used to be able to build packages using just the Python standard library.
I guess this battery is no longer included in the stdlib. I shrug and continue.</p>
<h2>Installing <code>build</code></h2>
<p>I go to the <a href="https://pypa-build.readthedocs.io/en/latest/installation.html">Build installation docs</a>.
It says to <code>pip install build</code>. (I thought I read years ago that one should
use <code>python3 -m pip</code> to invoke pip. Strange that a PyPA maintained tool is
telling me to invoke <code>pip</code> directly since I'm pretty sure a lot of the reasons
to use <code>python -m</code> to invoke tools are still valid. But I digress.)</p>
<p>I follow the instructions, installing it to the global <code>site-packages</code>
because I figure I'll use this tool a lot and I'm not a virtual environment
purist:</p>
<div class="pygments_murphy"><pre><span></span>$ python3.12 -m pip install build
Collecting build
  Obtaining dependency information for build from https://files.pythonhosted.org/packages/93/dd/b464b728b866aaa62785a609e0dd8c72201d62c5f7c53e7c20f4dceb085f/build-1.0.3-py3-none-any.whl.metadata
  Downloading build-1.0.3-py3-none-any.whl.metadata (4.2 kB)
Collecting packaging&gt;=19.0 (from build)
  Obtaining dependency information for packaging&gt;=19.0 from https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl.metadata
  Downloading packaging-23.2-py3-none-any.whl.metadata (3.2 kB)
Collecting pyproject_hooks (from build)
  Using cached pyproject_hooks-1.0.0-py3-none-any.whl (9.3 kB)
Using cached build-1.0.3-py3-none-any.whl (18 kB)
Using cached packaging-23.2-py3-none-any.whl (53 kB)
Installing collected packages: pyproject_hooks, packaging, build
Successfully installed build-1.0.3 packaging-23.2 pyproject_hooks-1.0.0
</pre></div>

<p>That downloads and installs wheels for <code>build</code>, <code>packaging</code>, and
<code>pyproject_hooks</code>.</p>
<p>At this point the security aware part of my brain is screaming because we
didn't pin versions or SHA-256 digests of any of these packages
anywhere. So if a malicious version of any of these packages is somehow
uploaded to PyPI that's going to be a nightmare software supply chain
vulnerability having similar industry impact as
<a href="https://en.wikipedia.org/wiki/Log4Shell">log4shell</a>. Nowhere in build's
documentation does it mention this or say how to securely install build.
I suppose you have to just know about the supply chain gotchas with
<code>pip install</code> in order to mitigate this risk for yourself.</p>
<h2>Initial Results With <code>build</code> Are Promising</h2>
<p>After getting <code>build</code> installed, <code>python3.12 -m build --help</code> works now
and I can build a wheel:</p>
<div class="pygments_murphy"><pre><span></span>$ python3.12 -m build --wheel .
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools &gt;= 40.8.0, wheel)
* Getting build dependencies for wheel...
...
* Installing packages in isolated environment... (wheel)
* Building wheel...
running bdist_wheel
running build
running build_py
...
Successfully built zstandard-0.22.0.dev0-cp312-cp312-macosx_14_0_x86_64.whl
</pre></div>

<p>That looks promising! It seems to have invoked my <code>setup.py</code> without me
having to define a <code>[build-system]</code> in my <code>pyproject.toml</code>! Yay for backwards
compatibility.</p>
<h2>The Mystery of the Missing <code>cffi</code> Package</h2>
<p>But I notice something.</p>
<p>My <code>setup.py</code> script conditionally builds a <code>zstandard._cffi</code> extension
module if <code>import cffi</code> succeeds. Building with <code>build</code> isn't building this
extension module.</p>
<p>Before using <code>build</code>, I had to run <code>setup.py</code> using a <code>python</code> having the
<code>cffi</code> package installed, usually a project-local virtualenv. So let's try
that:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install build cffi
...
$ venv/bin/python -m build --wheel .
...
</pre></div>

<p>And I get the same behavior: no CFFI extension module.</p>
<p>Staring at the output, I see what looks like a smoking gun:</p>
<div class="pygments_murphy"><pre><span></span>* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools &gt;= 40.8.0, wheel)
* Getting build dependencies for wheel...
...
* Installing packages in isolated environment... (wheel)
</pre></div>

<p>OK. So it looks like <code>build</code> is creating its own isolated environment
(disregarding the invoked Python environment having <code>cffi</code> installed),
installing <code>setuptools &gt;= 40.8.0</code> and <code>wheel</code> into it, and then executing
the build from that environment.</p>
<p>So <code>build</code> sandboxes builds in an ephemeral build environment. This actually
seems like a useful feature to help with deterministic and reproducible
builds: I like it! But at this moment it stands in the way of progress. So
I run <code>python -m build --help</code>, spot a <code>--no-isolation</code> argument and do the
obvious:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m build --wheel --no-isolation .
...
building &#39;zstandard._cffi&#39; extension
...
</pre></div>

<p>Success!</p>
<p>And I don't see any deprecation warnings either. So I <em>think</em> I'm all good.</p>
<p>But obviously I've ventured off the paved road here, as we had to violate
the default constraints of <code>build</code> to get things to work. I'll get back to that
later.</p>
<h2>Reproducing Working Wheel Builds With <code>pip</code></h2>
<p>Just for good measure, let's see if we can use <code>pip wheel</code> to produce wheels,
as I've seen references that this is a supported mechanism for building wheels.</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip wheel .
Processing /Users/gps/src/python-zstandard
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: zstandard
  Building wheel for zstandard (pyproject.toml) ... done
  Created wheel for zstandard: filename=zstandard-0.22.0.dev0-cp312-cp312-macosx_14_0_x86_64.whl size=407841 sha256=a2e1cc1ad570ab6b2c23999695165a71c8c9e30823f915b88db421443749f58e
  Stored in directory: /Users/gps/Library/Caches/pip/wheels/eb/6b/3e/89aae0b17b638c9cdcd2015d98b85ee7fb3ef00325bb44a572
Successfully built zstandard
</pre></div>

<p>That output is a bit terse, since the setuptools build logs are getting swallowed.
That's fine. Rather than run with <code>-v</code> to get those logs, I manually inspect
the built wheel:</p>
<div class="pygments_murphy"><pre><span></span>$ unzip -lv zstandard-0.22.0.dev0-cp312-cp312-macosx_14_0_x86_64.whl
Archive:  zstandard-0.22.0.dev0-cp312-cp312-macosx_14_0_x86_64.whl
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
    7107  Defl:N     2490  65% 10-23-2023 08:36 7bb42fff  zstandard/__init__.py
   13938  Defl:N     2498  82% 10-23-2023 08:36 8d8d1316  zstandard/__init__.pyi
  919352  Defl:N   366631  60% 10-26-2023 08:28 3aeefc48  zstandard/backend_c.cpython-312-darwin.so
  152430  Defl:N    32528  79% 10-26-2023 05:37 fc1a3c0c  zstandard/backend_cffi.py
       0  Defl:N        2   0% 12-26-2020 16:12 00000000  zstandard/py.typed
    1484  Defl:N      784  47% 10-26-2023 08:28 facba579  zstandard-0.22.0.dev0.dist-info/LICENSE
    2863  Defl:N      847  70% 10-26-2023 08:28 b8d80875  zstandard-0.22.0.dev0.dist-info/METADATA
     111  Defl:N      106   5% 10-26-2023 08:28 878098e6  zstandard-0.22.0.dev0.dist-info/WHEEL
      10  Defl:N       12 -20% 10-26-2023 08:28 a5f38e4e  zstandard-0.22.0.dev0.dist-info/top_level.txt
     841  Defl:N      509  40% 10-26-2023 08:28 e9a804ae  zstandard-0.22.0.dev0.dist-info/RECORD
--------          -------  ---                            -------
 1098136           406407  63%                            10 files
</pre></div>

<p>(Python wheels are just zip files with certain well-defined paths having special
meanings. I know this because I wrote Rust code for <em>parsing</em> wheels as part of
developing PyOxidizer.)</p>
<p>Looks like the <code>zstandard/_cffi.cpython-312-darwin.so</code> extension module is missing.
Well, at least <code>pip</code> is consistent with <code>build</code>! Although somewhat confusingly I don't
see any reference to a separate build environment in the pip output. But I suspect
it is there because <code>cffi</code> is installed in the virtual environment I invoke pip from!</p>
<p>Reading pip help output, I find the relevant argument to not spawn a new
environment and try again:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip wheel --no-build-isolation .
&lt;same exact output except the wheel size and digest changes&gt;

$ unzip -lv zstandard-0.22.0.dev0-cp312-cp312-macosx_14_0_x86_64.whl
...
 1002664  Defl:N   379132  62% 10-26-2023 08:33 48afe5ba  zstandard/_cffi.cpython-312-darwin.so
...
</pre></div>

<p>(I'm happy to see <code>build</code> and <code>pip</code> agreeing on the <em>no isolation</em> terminology.)</p>
<p>OK, so I got <code>build</code> and <code>pip</code> to behave nearly identically. I feel like I
finally understand this!</p>
<p>I also run <code>pip -v wheel</code> and <code>pip -vv wheel</code> to peek under the covers and see
what it's doing. Interestingly, I don't see any hint of a virtual environment
or temporary directory until I go to <code>-vv</code>. I find it interesting that <code>build</code>
presents details about this by default but you have to put <code>pip</code> in very verbose
mode to get it. I'm glad I used <code>build</code> first because the ephemeral build
environment was the source of my missing dependency and <code>pip</code> buried this
important detail behind a ton of other output in <code>-vv</code>, making it much harder
to discover!</p>
<h2>Understanding How <code>setuptools</code> Gets Installed</h2>
<p>When looking at pip's verbose output, I also see references to installing the
<code>setuptools</code> and <code>wheel</code> packages:</p>
<div class="pygments_murphy"><pre><span></span>Processing /Users/gps/src/python-zstandard
  Running command pip subprocess to install build dependencies
  Collecting setuptools&gt;=40.8.0
    Using cached setuptools-68.2.2-py3-none-any.whl.metadata (6.3 kB)
  Collecting wheel
    Using cached wheel-0.41.2-py3-none-any.whl.metadata (2.2 kB)
  Using cached setuptools-68.2.2-py3-none-any.whl (807 kB)
  Using cached wheel-0.41.2-py3-none-any.whl (64 kB)
  Installing collected packages: wheel, setuptools
  Successfully installed setuptools-68.2.2 wheel-0.41.2
  Installing build dependencies ... done
</pre></div>

<p>There's that <code>setuptools&gt;=40.8.0</code> constraint again. (We also saw it in <code>build</code>.)
I <code>rg 40.8.0</code> my source checkout (note: the <code>.</code> in there are wildcard characters
since <code>40.8.0</code> is a regexp so this could over match) and come up with nothing.
If it's not coming from my code, where is it coming from?</p>
<p>In the pip documentation, <a href="https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/#fallback-behaviour">Fallback behaviour</a>
says that a missing <code>[build-system]</code> from <code>pyproject.toml</code> is implicitly
translated to the following:</p>
<div class="pygments_murphy"><pre><span></span><span class="k">[build-system]</span>
<span class="n">requires</span> <span class="o">=</span> <span class="k">[&quot;setuptools&gt;=40.8.0&quot;, &quot;wheel&quot;]</span>
<span class="n">build-backend</span> <span class="o">=</span> <span class="s">&quot;setuptools.build_meta:__legacy__&quot;</span>
</pre></div>

<p>For <code>build</code>, I go to the source code and discover that
<a href="https://github.com/pypa/build/commit/7504e2a7a8ac956b8f2991ae28aa45d8d73b9740">similar functionality</a>
was added in May 2020.</p>
<p>I'm not sure if this default behavior is specified in a PEP or what. But
<code>build</code> and <code>pip</code> seem to be agreeing on the behavior of adding
<code>setuptools&gt;=40.8.0</code> and <code>wheel</code> to their ephemeral build environments and
invoking <code>setuptools.build_meta:__legacy__</code> as the build backend as
implicit defaults if your <code>pyproject.toml</code> lacks a <code>[build-system]</code>. OK.</p>
<h2>Being Explicit About The Build System</h2>
<p>Perhaps I should consider defining <code>[build-system]</code> and being explicit
about things? After all, the tools aren't printing anything indicating they
are assuming implicit defaults and for all I know the defaults could change
in a backwards incompatible manner in any release and break my build. (Although
I would hope to see a deprecation warning before that occurs.)</p>
<p>So I modify my <code>pyproject.toml</code> accordingly:</p>
<div class="pygments_murphy"><pre><span></span><span class="k">[build-system]</span>
<span class="n">requires</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">&quot;cffi==1.16.0&quot;</span><span class="p">,</span>
    <span class="s">&quot;setuptools==68.2.2&quot;</span><span class="p">,</span>
    <span class="s">&quot;wheel==0.41.2&quot;</span><span class="p">,</span>
<span class="p">]</span>
<span class="n">build-backend</span> <span class="o">=</span> <span class="s">&quot;setuptools.build_meta:__legacy__&quot;</span>
</pre></div>

<p>I pinned all the dependencies to specific versions because I like determinism
and reproducibility. I really don't like when the upload of a new package version
breaks my builds!</p>
<h2>Software Supply Chain Weaknesses in <code>pyproject.toml</code></h2>
<p>When I pinned dependencies in <code>[build-system]</code> in <code>pyproject.toml</code>, the
security part of my brain is screaming over the lack of SHA-256
digest pinning.</p>
<p>How am I sure that we're using well-known, trusted versions of
these dependencies? Are all the transitive dependencies even pinned?</p>
<p>Before <code>pyproject.toml</code>, I used <code>pip-compile</code> from
<a href="https://github.com/jazzband/pip-tools">pip-tools</a> to generate a <code>requirements.txt</code>
containing SHA-256 digests for all transitive dependencies. I would use
<code>python3 -m venv</code> to create a virtualenv,
<code>venv/bin/python -m pip install -r requirements.txt</code> to materialize a (highly
deterministic) set of packages, then run <code>venv/bin/python setup.py</code> to invoke
a build in this stable and securely created environment. (Some) software supply chain
risks averted! But, uh, how do I do that with <code>pyproject.toml</code>
<code>build-system.requires</code>? Does it even support pinning SHA-256 digests?</p>
<p>I skim the PEPs related to <code>pyproject.toml</code> and don't see anything. Surely
I'm missing something.</p>
<p>In desperation I check the pip-tools project and sure enough they
<a href="https://github.com/jazzband/pip-tools#requirements-from-pyprojecttoml">document pyproject.toml integration</a>.
However, they tell you how to feed <code>requirements.txt</code> files into the dynamic
dependencies consumed by the build backend: there's nothing on how to securely
install the build backend itself.</p>
<p>As far as I can tell <code>pyproject.toml</code> has no facilities for securely
installing (read: pinning content digests for all transitive dependencies)
the build backend itself. This is left as an exercise to the reader. But,
um, the build frontend (which I was also instructed to download insecurely
via <code>python -m pip install</code>) is the thing installing the build backend. How am
I supposed to subvert the build frontend to securely install the build backend?
Am I supposed to disable default behavior of using an ephemeral environment
in order to get secure backend installs? Doesn't the ephemeral environment
give me additional, desired protections for build determinism and
reproducibility? That seems <em>wrong</em>.</p>
<p>It kind of looks like <code>pyproject.toml</code> wasn't designed with software supply
chain risk mitigation as a criteria. This is extremely surprising for a build
system abstraction designed in the past few years. I shrug my shoulders and
move on.</p>
<h2>Porting <code>python setup.py develop</code> Invocations</h2>
<p>Now that I figure I have a working <code>pyproject.toml</code>, I move onto removing
<code>python setup.py</code> invocations.</p>
<p>First up is a <code>python setup.py develop --rust-backend</code> invocation.</p>
<p>My <code>setup.py</code> performs <a href="https://github.com/indygreg/python-zstandard/blob/f17569c645618a786ad11a3d51c3baa0b49a311c/setup.py#L65">very crude scanning</a>
of <code>sys.argv</code> looking for command arguments like <code>--system-zstd</code> and
<code>--rust-backend</code> as a way to influence the build. We just sniff these special
arguments and remove them from <code>sys.argv</code> so they don't confuse the setuptools
options parser. (I don't believe this is a blessed way of doing custom options
handling in distutils/setuptools. But it is simple and has worked since I
introduced the pattern in 2016.)</p>
<h2>Is <code>--global-option</code> the Answer?</h2>
<p>With <code>python setup.py</code> invocations going away and a build frontend invoking
<code>setup.py</code>, I need to find an alternative mechanism to pass settings into my
<code>setup.py</code>.</p>
<p><a href="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary">Why you shouldn't invoke setup.py directly</a>
tells me I should use <code>pip install -e</code>. I'm guessing there's a way to instruct
<code>pip install</code> to pass arguments to <code>setup.py</code>.</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --help
...
  -C, --config-settings &lt;settings&gt;
                              Configuration settings to be passed to the PEP 517 build backend. Settings take the form KEY=VALUE. Use multiple --config-settings options to pass multiple keys to the backend.
  --global-option &lt;options&gt;   Extra global options to be supplied to the setup.py call before the install or bdist_wheel command.
...
</pre></div>

<p>Hmmm. Not really sure which of these to use. But<code>--global-option</code> mentions
<code>setup.py</code> and I'm using <code>setup.py</code>. So I try that:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --global-option --rust-backend -e .
Usage:
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] &lt;requirement specifier&gt; [package-index-options] ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] -r &lt;requirements file&gt; [package-index-options] ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] [-e] &lt;vcs project url&gt; ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] [-e] &lt;local project path&gt; ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] &lt;archive url/path&gt; ...

no such option: --rust-backend
</pre></div>

<p>Oh, duh, <code>--rust-backend</code> looks like an argument and makes pip's own argument
parsing ambiguous as to how to handle it. Let's try that again with
<code>--global-option=--rust-backend</code>:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --global-option=--rust-backend -e .
DEPRECATION: --build-option and --global-option are deprecated. pip 24.0 will enforce this behaviour change. A possible replacement is to use --config-settings. Discussion can be found at https://github.com/pypa/pip/issues/11859
WARNING: Implying --no-binary=:all: due to the presence of --build-option / --global-option.
Obtaining file:///Users/gps/src/python-zstandard
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: zstandard
  WARNING: Ignoring --global-option when building zstandard using PEP 517
  Building editable for zstandard (pyproject.toml) ... done
  Created wheel for zstandard: filename=zstandard-0.22.0.dev0-0.editable-cp312-cp312-macosx_14_0_x86_64.whl size=4379 sha256=05669b0a5fd8951cac711923d687d9d4192f6a70a8268dca31bdf39012b140c8
  Stored in directory: /private/var/folders/dd/xb3jz0tj133_hgnvdttctwxc0000gn/T/pip-ephem-wheel-cache-6amdpg21/wheels/eb/6b/3e/89aae0b17b638c9cdcd2015d98b85ee7fb3ef00325bb44a572
Successfully built zstandard
Installing collected packages: zstandard
Successfully installed zstandard-0.22.0.dev0
</pre></div>

<p>I immediately see the three <code>DEPRECATION</code> and <code>WARNING</code> lines (which are color
highlighted in my terminal, yay):</p>
<div class="pygments_murphy"><pre><span></span>DEPRECATION: --build-option and --global-option are deprecated. pip 24.0 will enforce this behaviour change. A possible replacement is to use --config-settings. Discussion can be found at https://github.com/pypa/pip/issues/11859
WARNING: Implying --no-binary=:all: due to the presence of --build-option / --global-option.
WARNING: Ignoring --global-option when building zstandard using PEP 517
</pre></div>

<p>Yikes. It looks like <code>--global-option</code> is deprecated and will be removed in pip 24.0.
And, later it says <code>--global-option</code> was ignored. Is that true?!</p>
<div class="pygments_murphy"><pre><span></span>$ ls -al zstandard/*cpython-312*.so
-rwxr-xr-x  1 gps  staff  1002680 Oct 27 11:35 zstandard/_cffi.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff   919352 Oct 27 11:35 zstandard/backend_c.cpython-312-darwin.so
</pre></div>

<p>Not seeing a <code>backend_rust</code> library like I was expecting. So, yes, it does look
like <code>--global-option</code> was ignored.</p>
<p>This behavior is actually pretty concerning to me. It certainly
seems like at one time <code>--global-option</code> (and a <code>--build-option</code> which doesn't
exist on the <code>pip install</code> command I guess) did get threaded through to <code>setup.py</code>.
However, it no longer does.</p>
<p>I find an entry in the <a href="https://pip.pypa.io/en/stable/news/#v23-1">pip 23.1 changelog</a>:
<code>Deprecate --build-option and --global-option. Users are invited to switch
to --config-settings. (#11859)</code>. <em>Deprecate</em>. What is pip's definition of
<em>deprecate</em>? I click the link to <a href="https://github.com/pypa/pip/pull/11859">#11859</a>.
An open issue with a lot of comments. I scan the issue history to find
referenced PRs and click on <a href="https://github.com/pypa/pip/pull/11861">#11861</a>.
OK, it is just an advertisement. Maybe <code>--global-option</code> never got threaded
through to <code>setup.py</code>? But its help usage text clearly says it is related to
<code>setup.py</code>! Maybe the presence of <code>[build-system]</code> in <code>pyproject.toml</code> is
somehow engaging different semantics that result in <code>--global-option</code> not
being passed to <code>setup.py</code>? The warning message did say
<code>Ignoring --global-option when building zstandard using PEP 517</code>.</p>
<p>I try commenting out the <code>[build-system]</code> section in my <code>pyproject.toml</code>
and trying again. Same result. Huh? Reading the <code>pip install --help</code> output,
I see <code>--no-use-pep517</code> and try it:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --global-option=--rust-backend --no-use-pep517 -e .
...
$ ls -al zstandard/*cpython-312*.so
-rwxr-xr-x  1 gps  staff  1002680 Oct 27 11:35 zstandard/_cffi.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff   919352 Oct 27 11:35 zstandard/backend_c.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff  2727920 Oct 27 11:53 zstandard/backend_rust.cpython-312-darwin.so
</pre></div>

<p>Ahh, so pip's default PEP-517 build mode is causing <code>--global-option</code> to get
ignored. So I guess older versions of pip honored <code>--global-option</code> and when
pip switched to PEP-517 build mode by default <code>--global-option</code> just stopped
working and emitted a warning instead. That's quite the backwards incompatible
behavior break! I really wish tools would fail fast when making these kinds of
breaks or at least offer a <code>--warnings-as-errors</code> mode so I can opt into fatal
errors when these kinds of breaks / deprecations are introduced. I would 100%
opt into this since these warnings are often the figurative needle in a haystack
of CI logs and easy to miss. Especially if the build environment is
non-deterministic and new versions of tools like pip get installed <em>randomly</em>
without a version control commit.</p>
<p>Pip's allowing me to specify <code>--global-option</code> but then only issuing a
warning when it is ignored doesn't sit well with me. But what can I do?</p>
<p>It is obvious <code>--global-option</code> is a non-starter here.</p>
<h2>Attempts at Using <code>--config-setting</code></h2>
<p>Fortunately, pip's deprecation message suggests a path forward:</p>
<div class="pygments_murphy"><pre><span></span>A possible replacement is to use --config-settings. Discussion can be found
at https://github.com/pypa/pip/issues/11859
</pre></div>

<p>First, kudos for actionable warning messages. However, the wording says
<em>possible replacement</em>. Are there other alternatives I didn't see in the
<code>pip install --help</code> output?</p>
<p>Anyway, I decide to go with that <code>--config-settings</code> suggestion.</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --config-settings=--rust-backend -e .

Usage:
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] &lt;requirement specifier&gt; [package-index-options] ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] -r &lt;requirements file&gt; [package-index-options] ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] [-e] &lt;vcs project url&gt; ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] [-e] &lt;local project path&gt; ...
  /Users/gps/src/python-zstandard/venv/bin/python -m pip install [options] &lt;archive url/path&gt; ...

Arguments to --config-settings must be of the form KEY=VAL
</pre></div>

<p>Hmmm. Let's try adding a trailing <code>=</code>?</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --config-settings=--rust-backend= -e .
Obtaining file:///Users/gps/src/python-zstandard
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: zstandard
  Building editable for zstandard (pyproject.toml) ... done
  Created wheel for zstandard: filename=zstandard-0.22.0.dev0-0.editable-cp312-cp312-macosx_14_0_x86_64.whl size=4379 sha256=619db9806bc4c39e973c3197a0ddb9b03b49fff53cd9ac3d7df301318d390b5e
  Stored in directory: /private/var/folders/dd/xb3jz0tj133_hgnvdttctwxc0000gn/T/pip-ephem-wheel-cache-gtsvw78d/wheels/eb/6b/3e/89aae0b17b638c9cdcd2015d98b85ee7fb3ef00325bb44a572
Successfully built zstandard
Installing collected packages: zstandard
  Attempting uninstall: zstandard
    Found existing installation: zstandard 0.22.0.dev0
    Uninstalling zstandard-0.22.0.dev0:
      Successfully uninstalled zstandard-0.22.0.dev0
Successfully installed zstandard-0.22.0.dev0
</pre></div>

<p>No warnings or deprecations. That's promising. Did it work?</p>
<div class="pygments_murphy"><pre><span></span>$ ls -al zstandard/*cpython-312*.so
-rwxr-xr-x  1 gps  staff  1002680 Oct 27 12:11 zstandard/_cffi.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff   919352 Oct 27 12:11 zstandard/backend_c.cpython-312-darwin.so
</pre></div>

<p>No <code>backend_rust</code> extension module. Boo. So what actually happened?</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip -v install --config-settings=--rust-backend= -e .
</pre></div>

<p>I don't see <code>--rust-backend</code> anywhere in that log output. I try with
more verbosity:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip -vvvvv install --config-settings=--rust-backend= -e .
</pre></div>

<p>Still nothing!</p>
<p>Maybe That <code>--</code> prefix is wrong?</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip -vvvvv install --config-settings=rust-backend= -e .
</pre></div>

<p>Still nothing!</p>
<p>I have no clue how <code>--config-settings=</code> is getting passed
to <code>setup.py</code> nor where it is seemingly getting dropped on the floor.</p>
<h2>How Does setuptools Handle <code>--config-settings</code>?</h2>
<p>This must be documented in the setuptools project. So I open those docs in
my web browser and do a <a href="https://setuptools.pypa.io/en/latest/search.html?q=settings&amp;check_keywords=yes&amp;area=default">search for settings</a>.
I open the first three results in separate tabs:</p>
<ol>
<li><a href="https://setuptools.pypa.io/en/latest/deprecated/commands.html?highlight=settings">Running setuptools commands</a></li>
<li><a href="https://setuptools.pypa.io/en/latest/deprecated/commands.html?highlight=settings#configuration-file-options">Configuration File Options</a></li>
<li><a href="https://setuptools.pypa.io/en/latest/deprecated/commands.html?highlight=settings#develop-deploy-the-project-source-in-development-mode">develop - Deploy the project source in "Development Mode"</a></li>
</ol>
<p>That first link has docs on the deprecated setuptools commands and how to
invoke <code>python setup.py</code> directly. (Note: there is a warning box here saying
that <code>python setup.py</code> is deprecated. I guess I somehow missed this document
when looking at setuptools documentation earlier! In hindsight, it appears to
be buried at the figurative bottom of the docs tree as the last item under
a <code>Backward compatibility &amp; deprecated practice section</code>. Talk about burying
the lede!) These docs aren't useful.</p>
<p>The second link also takes me to deprecated documentation related to direct
<code>python setup.py</code> command invocations.</p>
<p>The third link is also useless.</p>
<p>I continue opening search results in new tabs. Surely the answer is in here.</p>
<p>I find an <a href="https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-arguments">Adding Arguments</a>
section telling me that <code>Adding arguments to setup is discouraged as such
arguments are only supported through imperative execution and not supported
through declarative config.</code>. I <em>think</em> that's an obtuse of saying that
<code>sys.argv</code> arguments are only supported via <code>python setup.py</code> invocations
and not via <code>setup.cfg</code> or <code>pyproject.toml</code>? But the example only shows me
how to use <code>setup.cfg</code> and doesn't have any mention of <code>pyproject.toml</code>. So
is this documentation even relevant to <code>pyproject.toml</code>?</p>
<p>Eventually I stumble across
<a href="https://setuptools.pypa.io/en/latest/build_meta.html">Build System Support</a>.
In the <a href="https://setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks">Dynamic build dependencies and other build_meta tweaks</a>
section, I notice the following example code:</p>
<div class="pygments_murphy"><pre><span></span><span class="kn">from</span> <span class="nn">setuptools</span> <span class="kn">import</span> <span class="n">build_meta</span> <span class="k">as</span> <span class="n">_orig</span>
<span class="kn">from</span> <span class="nn">setuptools.build_meta</span> <span class="kn">import</span> <span class="o">*</span>

<span class="k">def</span> <span class="nf">get_requires_for_build_wheel</span><span class="p">(</span><span class="n">config_settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">_orig</span><span class="o">.</span><span class="n">get_requires_for_build_wheel</span><span class="p">(</span><span class="n">config_settings</span><span class="p">)</span> <span class="o">+</span> <span class="p">[</span><span class="o">...</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">get_requires_for_build_sdist</span><span class="p">(</span><span class="n">config_settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">_orig</span><span class="o">.</span><span class="n">get_requires_for_build_sdist</span><span class="p">(</span><span class="n">config_settings</span><span class="p">)</span> <span class="o">+</span> <span class="p">[</span><span class="o">...</span><span class="p">]</span>
</pre></div>

<p><code>config_settings=None</code>. OK, this might be the <code>--config-settings</code>
values passed to the build frontend getting fed into the build backend.
I Google <code>get_requires_for_build_wheel</code>. One of the top results is
<a href="https://peps.python.org/pep-0517/">PEP-517</a>, which I click on.</p>
<p>I see that the <a href="https://peps.python.org/pep-0517/#build-backend-interface">Build backend interface</a>
consists of a handful of functions that are invoked by the build frontend.
These functions all seem to take a <code>config_settings=None</code> argument. Great,
now I know the interface between build frontends and backends at the Python
API level. Where was I in this yak shave?</p>
<p>I remember from <code>pyproject.toml</code> that one of the lines is
<code>build-backend = "setuptools.build_meta:__legacy__"</code>. That
<code>setuptools.build_meta:__legacy__</code> bit looks like a Python symbol reference.
Since the setuptools documentation didn't answer my question on how to
thread <code>--config-settings</code> into <code>setup.py</code> invocations, I
<a href="https://github.com/pypa/setuptools/blob/2384d915088b960999ca74fb81ce70bffd17b082/setuptools/build_meta.py">open the build_meta.py source code</a>.
(Aside: experience has taught me that when in doubt on how something works,
consult the source code: code doesn't lie.)</p>
<p>I search for <code>config_settings</code>. I immediately see
<code>class _ConfigSettingsTranslator:</code> whose purported job is
<code>Translate config_settings into distutils-style command arguments.
Only a limited number of options is currently supported.</code> Oh, this looks
relevant. But there's a fair bit of code in here. Do I really need to grok
it all? I keep scanning the source.</p>
<p>In a <code>def _build_with_temp_dir()</code> I spot the following code:</p>
<div class="pygments_murphy"><pre><span></span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span> <span class="o">=</span> <span class="p">[</span>
    <span class="o">*</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[:</span><span class="mi">1</span><span class="p">],</span>
    <span class="o">*</span><span class="bp">self</span><span class="o">.</span><span class="n">_global_args</span><span class="p">(</span><span class="n">config_settings</span><span class="p">),</span>
    <span class="o">*</span><span class="n">setup_command</span><span class="p">,</span>
    <span class="s2">&quot;--dist-dir&quot;</span><span class="p">,</span>
    <span class="n">tmp_dist_dir</span><span class="p">,</span>
    <span class="o">*</span><span class="bp">self</span><span class="o">.</span><span class="n">_arbitrary_args</span><span class="p">(</span><span class="n">config_settings</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>

<p>Ahh, cool. It looks to be calling <code>self._global_args()</code> and
<code>self._arbitrary_args()</code> and adding the arguments those functions return
to <code>sys.argv</code> before evaluating <code>setup.py</code> in the current interpreter.</p>
<p>I look at the definition of <code>_arbitrary_args()</code> and I'm onto something:</p>
<div class="pygments_murphy"><pre><span></span><span class="k">def</span> <span class="nf">_arbitrary_args</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">config_settings</span><span class="p">:</span> <span class="n">_ConfigSettings</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Iterator</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
  <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">  Users may expect to pass arbitrary lists of arguments to a command</span>
<span class="sd">  via &quot;--global-option&quot; (example provided in PEP 517 of a &quot;escape hatch&quot;).</span>
<span class="sd">  ...</span>
<span class="sd">  &quot;&quot;&quot;</span>
  <span class="n">args</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">(</span><span class="s2">&quot;--global-option&quot;</span><span class="p">,</span> <span class="n">config_settings</span><span class="p">)</span>
  <span class="n">global_opts</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_valid_global_options</span><span class="p">()</span>
  <span class="n">bad_args</span> <span class="o">=</span> <span class="p">[]</span>

  <span class="k">for</span> <span class="n">arg</span> <span class="ow">in</span> <span class="n">args</span><span class="p">:</span>
      <span class="k">if</span> <span class="n">arg</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">&quot;-&quot;</span><span class="p">)</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">global_opts</span><span class="p">:</span>
          <span class="n">bad_args</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
          <span class="k">yield</span> <span class="n">arg</span>

  <span class="k">yield from</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_config</span><span class="p">(</span><span class="s2">&quot;--build-option&quot;</span><span class="p">,</span> <span class="n">config_settings</span><span class="p">)</span>

  <span class="k">if</span> <span class="n">bad_args</span><span class="p">:</span>
      <span class="n">SetuptoolsDeprecationWarning</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span>
          <span class="s2">&quot;Incompatible `config_settings` passed to build backend.&quot;</span><span class="p">,</span>
          <span class="sa">f</span><span class="s2">&quot;&quot;&quot;</span>
<span class="s2">          The arguments </span><span class="si">{bad_args!r}</span><span class="s2"> were given via `--global-option`.</span>
<span class="s2">          Please use `--build-option` instead,</span>
<span class="s2">          `--global-option` is reserved for flags like `--verbose` or `--quiet`.</span>
<span class="s2">          &quot;&quot;&quot;</span><span class="p">,</span>
          <span class="n">due_date</span><span class="o">=</span><span class="p">(</span><span class="mi">2023</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">26</span><span class="p">),</span>  <span class="c1"># Warning introduced in v64.0.1, 11/Aug/2022.</span>
      <span class="p">)</span>
</pre></div>

<p>It looks to peek inside <code>config_settings</code> and handle <code>--global-option</code>
and <code>--build-option</code> specially. But we clearly see <code>--global-option</code> is
deprecated in favor of <code>--build-option</code>.</p>
<p>So is the <code>--config-settings</code> key name <code>--build-option</code> and its value
the <code>setup.py</code> argument we want to insert?</p>
<p>I try that:</p>
<div class="pygments_murphy"><pre><span></span>$ venv/bin/python -m pip install --config-settings=--build-option=--rust-backend -e .
...
$ ls -al zstandard/*cpython-312*.so
-rwxr-xr-x  1 gps  staff  1002680 Oct 27 12:54 zstandard/_cffi.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff   919352 Oct 27 12:53 zstandard/backend_c.cpython-312-darwin.so
-rwxr-xr-x  1 gps  staff  2727920 Oct 27 12:54 zstandard/backend_rust.cpython-312-darwin.so
</pre></div>

<p>It worked!</p>
<h2>Disbelief Over <code>--config-settings=--build-option=</code></h2>
<p>But, um, <code>--config-settings=--build-option=--rust-backend</code>. We've triple encoded
command arguments here. This feels exceptionally weird. Is that really the
supported/preferred interface? Surely there's something simpler.</p>
<p><code>def _arbitrary_args()</code>'s docstring mentioned <em>escape hatch</em> in the context
of PEP-517. I open PEP-517 and search for that term, finding
<a href="https://peps.python.org/pep-0517/#config-settings">Config settings</a>. Sure
enough, it is describing the mechanism I just saw the source code to. And its
pip example is using <code>pip install</code>'s <code>--global-option</code> and <code>--build-option</code>
arguments. So this all seems to check out. (Although these pip arguments are
deprecated in favor of <code>-C/--config-settings</code>.)</p>
<p>Thinking I missed some obvious documentation, I search the setuptools
documentation for <a href="https://setuptools.pypa.io/en/latest/search.html?q=%22--build-option%22&amp;check_keywords=yes&amp;area=default#">--build-option</a>.
The only hits are in the <a href="https://setuptools.pypa.io/en/latest/history.html#v64-0-0">v64.0.0 changelog entry</a>.
So you are telling me this feature of passing arbitrary config settings into
<code>setup.py</code> via PEP-517 build frontends is only documented in the <em>changelog</em>?!</p>
<p>Ok, I know my <code>setup.py</code> is abusing <code>sys.argv</code>. I'm off the paved road for
passing settings into <code>setup.py</code>. What is the preferred <code>pyproject.toml</code>
era mechanism for passing settings into <code>setup.py</code>? These settings can't
be file based because they are dynamic. There must be a <code>config_settings</code>
mechanism to thread dynamic settings into <code>setup.py</code> that doesn't rely on
these magical <code>--build-option</code> and <code>--global-option</code> settings keys.</p>
<p>I stare and stare at the
<a href="https://github.com/pypa/setuptools/blob/2384d915088b960999ca74fb81ce70bffd17b082/setuptools/build_meta.py">build_meta.py source code</a>
looking for find an answer. But all I see is the <code>def _build_with_temp_dir()</code>
calling into <code>self._global_args()</code> and <code>self._arbitrary_args()</code> to append
arguments to <code>sys.argv</code>. Huh? Surely this isn't the only solution. Surely
there's a simpler way. The setuptools documentation said <em>Adding arguments
to setup is discouraged</em>, seemingly implying a better way of doing it. And
yet the only code I'm seeing in <code>build_meta.py</code> for passing custom
<code>config_settings</code> values in is literally via additional <code>setup.py</code> process
arguments. This can't be right.</p>
<p>I start unwinding my mental stack and browser tabs trying to come across
something I missed.</p>
<p>I again look at
<a href="https://setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks">Dynamic build dependencies and other build_meta tweaks</a>
and see its code is defining a custom <code>[build-system]</code> backend that
does a <code>from setuptools.build_meta import *</code> and defines some custom
build backend interface APIs (which receive <code>config_settings</code>) and then
proxy into the original implementations. While the example is related to
build metadata, I'm thinking <em>do I need to implement my own setuptools
wrapping build backend that implements a custom
<a href="https://peps.python.org/pep-0517/#build-wheel">def build_wheel()</a> to
intercept <code>config_settings</code></em>? Surely this is avoidable complexity.</p>
<h2>Pip's Eager Deprecations</h2>
<p>I keep unwinding context and again notice pip's warning message telling
me <em>A possible replacement is to use <code>--config-settings</code>. Discussion can
be found at https://github.com/pypa/pip/issues/11859</em>.</p>
<p>I open <a href="https://github.com/pypa/pip/issues/11859">pip issue #11859</a>.
Oh, that's the same issue tracking the <code>--global-option</code> deprecation I
encountered earlier. I again scan the issue timeline. It is mostly
references from other GitHub projects. Telltale sign that this
deprecation is creating waves.</p>
<p>The issue is surprisingly light on comments for how many references it
has.</p>
<p>The comment with
<a href="https://github.com/pypa/pip/issues/11859#issuecomment-1664649395">the most emoji reactions</a>
says:</p>
<div class="pygments_murphy"><pre><span></span>Is there an example showing how to use --config-settings with setup.py
and/or newer alternatives? The setuptools documentation is awful and the
top search results are years/decades out-of-date and wildly contradictory.`
</pre></div>

<p>I don't know who you are, @alexchandel, but we're on the same wavelength.</p>
<p>Then the next comment says:</p>
<div class="pygments_murphy"><pre><span></span>Something like this seems to work to pass global options to setuptools.

pip -vv install   --config-setting=&quot;--global-option=--verbose&quot;  .

Passing --build-option in the same way does not work, as setuptools
attempts to pass these to the egg_info command where they are not supported.
</pre></div>

<p>So there it seemingly is, confirmation that my independently derived
solution of <code>--config-settings=--build-option=-...</code> is in fact the way to
go. But this commenter says to use <code>--global-option</code>, which appears to
be deprecated in modern setuptools. Oof.</p>
<p>The next comment links to <a href="https://github.com/pypa/setuptools/issues/3896">pypa/setuptools#3896</a>
where apparently there's been an ongoing conversation since April about how
setuptools should <em>design and document a stable mechanism to pass <code>config_settings</code>
to PEP517 backend</em>.</p>
<p>If I'm interpreting this correctly, it looks like distutils/setuptools - the
primary way to define Python packages for the better part of twenty years -
doesn't have a stable mechanism for passing configuration settings from
modern <code>pyproject.toml</code> <code>[build-system]</code> frontends. Meanwhile pip is deprecating
long-working mechanisms to pass options to <code>setup.py</code> and forcing people to
use a mechanism that setuptools doesn't explicitly document much less say is
stable. This is all taking place <a href="https://mail.python.org/pipermail/distutils-sig/2017-September/031548.html">six years</a>
after PEP-517 was accepted.</p>
<p>I'm kind of at a loss for words here. I understand pip's desire to delete some
legacy code and standardize on the new way of doing things. But it really looks
like they are breaking backwards compatibility for <code>setup.py</code> a bit too
eagerly. That's a <em>questionable</em> decision in my mind, so I
<a href="https://github.com/pypa/pip/issues/11859#issuecomment-1778620671">write a detailed comment on the pip issue</a>
explaining how the interface works and asking the pip folks to hold off on
deprecation until setuptools has a stable, documented solution. Time will
tell what happens.</p>
<h2>In Summary</h2>
<p>What an adventure that Python packaging yak shave was! I feel
like I just learned a whole lot of things that I shouldn't have needed to learn
in order to keep my Python package building without deprecation warnings.
Yes, I scope bloated myself to understanding how things worked because
that's my ethos. But even without that extra work, there's a lot here that I
feel I shouldn't have needed to do, like figure out the undocumented
<code>--config-settings=--build-option=</code> interface.</p>
<p>Despite having ported my <code>python setup.py</code> invocation to modern, PEP-517 build
frontends (<code>build</code> and <code>pip</code>) and gotten rid of various deprecation messages
and warnings, I'm still not sure the implications of that transition. I really
want to understand the trade-offs for adopting <code>pyproject.toml</code> and using the
modern build frontends for doing things. But I couldn't find any documentation
on this anywhere! I don't know basic things like whether my adoption of
<code>pyproject.toml</code> will break end-users stuck on older Python versions or what.
I still haven't ported my project metadata from <code>setup.py</code> to <code>pyproject.toml</code>
because I don't understand the implications. I feel like I'm flying blind and
am bound to make mistakes with undesirable impacts to end-users of my package.</p>
<p>But at least I was able to remove deprecation warnings from my packaging CI
with <em>just</em> several hours of work.</p>
<p>I recognize this post is light on constructive feedback and suggestions
for how to improve matters.</p>
<p>One reason is that I think a lot of the improvements are self-explanatory -
clearer warning messages, better documentation, not deprecating things
prematurely, etc. I prefer to just submit PRs instead of long blog posts. But
I just don't know what is appropriate in some cases: one of the themes of this
post is I just don't grok the state of Python packaging right now.</p>
<p>This post did initially contain a few thousand words expanding on what all I
thought was broken and how it should be fixed. But I stripped the content
because I didn't want my (likely controversial) opinions to distract from
the self-assessed user experience study documented in this post. This content
is probably better posted to a PyPA mailing list anyway, otherwise I'm just
another guy complaining on the Internet.</p>
<p>I've <a href="https://discuss.python.org/t/user-experience-with-porting-off-setup-py/37502">posted a link</a>
to this post to the
<a href="https://discuss.python.org/c/packaging/14">packaging category</a> on
<a href="https://discuss.python.org">discuss.python.org</a> so the PyPA (and other
subscribed parties) are aware of all the issues I stumbled over. Hopefully
people with more knowledge of the state of Python packaging see this post,
empathize with my struggles, and enact meaningful improvements so others
can port off <code>setup.py</code> with a fraction of the effort as it took me.</p>]]></content:encoded>
    </item>
    <item>
      <title>Achieving A Completely Open Source Implementation of Apple Code Signing and Notarization</title>
      <link>http://gregoryszorc.com/blog/2022/08/08/achieving-a-completely-open-source-implementation-of-apple-code-signing-and-notarization</link>
      <pubDate>Mon, 08 Aug 2022 08:08:08 PDT</pubDate>
      <category><![CDATA[Apple]]></category>
      <category><![CDATA[Rust]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2022/08/08/achieving-a-completely-open-source-implementation-of-apple-code-signing-and-notarization</guid>
      <description>Achieving A Completely Open Source Implementation of Apple Code Signing and Notarization</description>
      <content:encoded><![CDATA[<p>As I've previously blogged in
<a href="/blog/2021/04/14/pure-rust-implementation-of-apple-code-signing/">Pure Rust Implementation of Apple Code Signing</a>
(2021-04-14) and
<a href="/blog/2022/04/25/expanding-apple-ecosystem-access-with-open-source,-multi-platform-code-signing/">Expanding Apple Ecosystem Access with Open Source, Multi Platform Code signing</a>
(2022-04-25), I've been hacking on an open source implementation of Apple code
signing and notarization using the Rust programming language. This takes the form
of the <code>apple-codesign</code> crate / library and its <code>rcodesign</code> CLI executable.
(<a href="https://gregoryszorc.com/docs/apple-codesign/stable/">Documentation</a> /
<a href="https://github.com/indygreg/apple-platform-rs/tree/main/apple-codesign">GitHub project</a> /
<a href="https://crates.io/crates/apple-codesign">crates.io</a>).</p>
<p>As of that most recent post in April, I was pretty happy with the relative
stability of the implementation: we were able to sign, notarize, and staple
Mach-O binaries, directory bundles (<code>.app</code>, <code>.framework</code> bundles, etc), XAR
archives / flat packages / <code>.pkg</code> installers, and DMG disk images. Except for
the <a href="https://gregoryszorc.com/docs/apple-codesign/0.17.0/apple_codesign_quirks.html">known limitations</a>,
if Apple's official <code>codesign</code> and <code>notarytool</code> tools support it, so do we.
<strong>This allows people to sign, notarize, and release Apple software from non-Apple
operating systems like Linux and Windows.</strong> This opens up new avenues for
Apple platform access.</p>
<p>A major limitation in previous versions of the <code>apple-codesign</code> crate was our
reliance on Apple's <a href="https://help.apple.com/itc/transporteruserguide/">Transporter</a>
tool for notarization. Transporter is a Java application made available for macOS,
Linux, and Windows that speaks to Apple's servers and can upload assets to their
notarization service. I used this tool at the time because it seemed to
be officially supported by Apple and the path of least resistance to standing
up notarization. But Transporter was a bit wonky to use and an extra
dependency that you needed to install.</p>
<p>At WWDC 2022, Apple <a href="https://developer.apple.com/videos/play/wwdc2022/10109/">announced</a>
a new <a href="https://developer.apple.com/documentation/notaryapi">Notary API</a> as
part of the App Store Connect API. In what felt like a wink directly at me,
Apple themselves even calls out the possibility for leveraging this API to
notarize from Linux! I knew as soon as I saw this that it was only a matter
of time before I would be able to replace Transporter with a pure Rust client
for the new HTTP API. (I was already thinking about using the unpublished HTTP
API that <code>notarytool</code> uses. And from the limited reversing notes I have from
before WWDC it looks like the new official Notary API is very similar - possibly
identical to - what <code>notarytool</code> uses. So kudos to Apple for opening up this
access!)</p>
<p><strong>I'm very excited to announce that we now have a pure Rust implementation
of a client for Apple's Notary API in the <code>apple-codesign</code> crate. This means we
can now notarize Apple software from any machine where you can get the Rust
crate to compile. This means we no longer have a dependency on the 3rd party
Apple Transporter application. Notarization, like code signing, is 100% open
source Rust code.</strong></p>
<p>As excited as I am to announce this new feature, <strong>I'm even more excited that
it was largely implemented by a contributor, Robin Lambertz /
<a href="https://github.com/roblabla">@roblabla</a>!</strong> They
<a href="https://github.com/indygreg/PyOxidizer/issues/591">filed a GitHub feature request</a>
while WWDC 2022 was still ongoing and then <a href="https://github.com/indygreg/PyOxidizer/pull/593">submitted a PR</a>
a few days later. It took me a few months to get around to reviewing it
(I try to avoid computer screens during summers), but it was a fantastic
PR given the scope of the change. It never ceases to bring joy to me when
someone randomly contributes greatness to open source.</p>
<p>So, as of the just-released <a href="https://github.com/indygreg/PyOxidizer/releases/tag/apple-codesign%2F0.17.0">0.17 release</a>
of the <code>apple-codesign</code> Rust crate and its corresponding <code>rcodesign</code> CLI tool, you can now
<code>rcodesign notary-submit</code> to speak to Apple's Notary API using a pure Rust client. No
more requirements on 3rd party, proprietary software. All you need to sign and
notarize Apple applications is the self-contained <code>rcodesign</code> executable and a Linux,
Windows, macOS, BSD, etc machine to run it on.</p>
<p>I'm stoked to finally achieve this milestone! There are probably thousands of
companies and individuals who have wanted to release Apple software from
non-macOS operating systems. (The existence and popularity of tools like
<a href="https://fastlane.tools/">fastlane</a> seems to confirm this.) The historical
lack of an Apple code signing and notarization solution that worked outside
macOS has prevented this. Well, that barrier has officially fallen.</p>
<p>Release notes, documentation, and (self-signed) pre-built executables of the
<code>rcodesign</code> executable for major platforms are available on the
<a href="https://github.com/indygreg/PyOxidizer/releases/tag/apple-codesign%2F0.17.0">0.17 release page</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Announcing the PyOxy Python Runner</title>
      <link>http://gregoryszorc.com/blog/2022/05/10/announcing-the-pyoxy-python-runner</link>
      <pubDate>Tue, 10 May 2022 08:00:00 PDT</pubDate>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[PyOxidizer]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2022/05/10/announcing-the-pyoxy-python-runner</guid>
      <description>Announcing the PyOxy Python Runner</description>
      <content:encoded><![CDATA[<p>I'm pleased to announce the initial release of
<a href="https://pyoxidizer.readthedocs.io/en/latest/pyoxy.html">PyOxy</a>.
Binaries are <a href="https://github.com/indygreg/PyOxidizer/releases/tag/pyoxy%2F0.1.0">available on GitHub</a>.</p>
<p>(Yes, I used my <a href="/blog/2022/04/25/expanding-apple-ecosystem-access-with-open-source,-multi-platform-code-signing/">pure Rust Apple code signing implementation</a>
to remotely sign the macOS binaries from GitHub Actions using a YubiKey
plugged into my Windows desktop: that experience still feels magical
to me.)</p>
<p>PyOxy is all of the following:</p>
<ul>
<li>An executable program used for running Python interpreters.</li>
<li>A single file and highly portable (C)Python distribution.</li>
<li>An alternative <code>python</code> driver providing more control over the
  interpreter than what <code>python</code> itself provides.</li>
<li>A way to make some of PyOxidizer's technology more broadly available without
  using PyOxidizer.</li>
</ul>
<p>Read the following sections for more details.</p>
<h2><code>pyoxy</code> Acts Like <code>python</code></h2>
<p>The <code>pyoxy</code> executable has a <code>run-python</code> sub-command that will essentially
do what <code>python</code> would do:</p>
<pre><code>$ pyoxy run-python
Python 3.9.12 (main, May  3 2022, 03:29:54)
[Clang 14.0.3 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
&gt;&gt;&gt;
</code></pre>
<p>A Python REPL. That's familiar!</p>
<p>You can even pass <code>python</code> arguments to it:</p>
<pre><code>$ pyoxy run-python -- -c 'print("hello, world")'
hello, world
</code></pre>
<p>When a <code>pyoxy</code> executable is renamed to any filename beginning with <code>python</code>,
it implicitly behaves like <code>pyoxy run-python --</code>.</p>
<pre><code>$ mv pyoxy python3.9
$ ls -al python3.9
-rwxrwxr-x  1 gps gps 120868856 May 10  2022 python3.9

$ ./python3.9
Python 3.9.12 (main, May  3 2022, 03:29:54)
[Clang 14.0.3 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
&gt;&gt;&gt;
</code></pre>
<h2>Single File Python Distributions</h2>
<p>The official <code>pyoxy</code> executables are built with PyOxidizer and leverage the
Python distributions provided by my
<a href="https://github.com/indygreg/python-build-standalone/">python-build-standalone</a>
project. On Linux and macOS, a fully featured Python interpreter and its library
dependencies are statically linked into <code>pyoxy</code>. The <code>pyoxy</code> executable also embeds
a copy of the Python standard library and imports it from memory using the
<a href="https://pyoxidizer.readthedocs.io/en/latest/oxidized_importer.html">oxidized_importer</a>
Python extension module.</p>
<p>What this all means is that the official <code>pyoxy</code> executables can function as
single file CPython distributions! Just download a <code>pyoxy</code> executable, rename it
to <code>python</code>, <code>python3</code>, <code>python3.9</code>, etc and it should behave just like a normal
<code>python</code> would!</p>
<p>Your Python installation has never been so simple. And fast: <code>pyoxy</code> should be
a few milliseconds faster to initialize a Python interpreter mostly because of
<code>oxidized_importer</code> and it avoiding filesystem overhead to look for and load
<code>.py[c]</code> files.</p>
<h2>Low-Level Control Over the Python Interpreter with YAML</h2>
<p>The <code>pyoxy run-yaml</code> command is takes the path to a YAML file defining the
embedded Python interpreter configuration and then launches that Python
interpreter in-process:</p>
<pre><code>$ cat &gt; hello_world.yaml &lt;&lt;EOF
---
allocator_debug: true
interpreter_config:
  run_command: 'print("hello, world")'
...
EOF

$ pyoxy run-yaml hello_world.yaml
hello, world
</code></pre>
<p>Under the hood, PyOxy uses the
<a href="https://docs.rs/pyembed/0.20.0/pyembed/">pyembed</a> Rust crate to manage embedded
Python interpreters. The YAML document that PyOxy uses is simply deserialized
into a
<a href="https://docs.rs/pyembed/0.20.0/pyembed/struct.OxidizedPythonInterpreterConfig.html">pyembed::OxidizedPythonInterpreterConfig</a>
Rust struct, which <code>pyembed</code> uses to spawn a Python interpreter. This Rust struct
offers near complete control over how the embedded Python interpreter behaves: it
even allows you to tweak settings that are impossible to change from environment
variables or <code>python</code> command arguments! (Beware: this power means you can
easily cause the interpreter to crash if you feed it a bad configuration!)</p>
<h2>YAML Based Python Applications</h2>
<p><code>pyoxy run-yaml</code> ignores all file content before the YAML <code>---</code> start document
delimiter. This means that on UNIX-like platforms
you can create <em>executable YAML</em> files defining your Python application. e.g.</p>
<pre><code>$ mkdir -p myapp
$ cat &gt; myapp/__main__.py &lt;&lt; EOF
print("hello from myapp")
EOF

$ cat &gt; say_hello &lt;&lt;"EOF"
#!/bin/sh
"exec" "`dirname $0`/pyoxy" run-yaml "$0" -- "$@"
---
interpreter_config:
  run_module: 'myapp'
  module_search_paths: ["$ORIGIN"]
...
EOF

$ chmod +x say_hello

$ ./say_hello
hello from myapp
</code></pre>
<p>This means that to <em>distribute</em> a Python application, you can drop a copy
of <code>pyoxy</code> in a directory then define an executable YAML file masquerading
as a shell script and you can run Python code with as little as two files!</p>
<h2>The Future of PyOxy</h2>
<p>PyOxy is very young. I hacked it together on a weekend in September 2021.
I wanted to shore up some functionality before releasing it then. But I
got perpetually sidetracked and never did the work. I figured it would be
better to make a smaller splash with a lesser-baked product now than wait
even longer. Anyway...</p>
<p>As part of building
<a href="https://pyoxidizer.readthedocs.io/en/stable/pyoxidizer.html">PyOxidizer</a> I've
built some peripheral technology:</p>
<ul>
<li>Standalone and highly distributable Python builds via the
  <a href="https://github.com/indygreg/python-build-standalone">python-build-standalone</a>
  project.</li>
<li>The <a href="https://docs.rs/pyembed/0.20.0/pyembed/">pyembed</a> Rust crate for managing
  an embedded Python interpreter.</li>
<li>The <a href="https://pyoxidizer.readthedocs.io/en/stable/oxidized_importer.html">oxidized_importer</a>
  Python package/extension for importing modules from memory, among other
  things.</li>
<li>The <a href="https://pyoxidizer.readthedocs.io/en/latest/oxidized_importer_packed_resources.html">Python packed resources</a>
  data format for representing a collection of Python modules and resource
  files for efficient loading (by <code>oxidized_importer</code>).</li>
</ul>
<p>I conceived PyOxy as a vehicle to enable people to leverage PyOxidizer's
technology without imposing PyOxidizer onto them. I feel that PyOxidizer's
broader technology is generally useful and too valuable to be gated behind
using PyOxidizer.</p>
<p>PyOxy is only officially released for Linux and macOS for the moment.
It definitely builds on Windows. However, I want to improve the single file
executable experience before officially releasing PyOxy on Windows. This
requires an extensive overhaul to <code>oxidized_importer</code> and the way it
serializes Python resources to be loaded from memory.</p>
<p>I'd like to add a sub-command to produce a
<a href="https://pyoxidizer.readthedocs.io/en/latest/oxidized_importer_packed_resources.html">Python packed resources</a>
payload. With this, you could bundle/distribute a Python application as
<code>pyoxy</code> plus a file containing your application's packed resources alongside
YAML configuring the Python interpreter. Think of this as a more modern and
faster version of the venerable <code>zipapp</code> approach. This would enable PyOxy to
satisfy packaging scenarios provided by tools like Shiv, PEX, and XAR.
However, unlike Shiv and PEX, <code>pyoxy</code> also provides an embedded Python
interpreter, so applications are much more portable since there isn't
reliance on the host machine having a Python interpreter installed.</p>
<p>I'm really keen to see how others want to use <code>pyoxy</code>.</p>
<p>The YAML based control over the Python interpreter could be super useful for
testing, benchmarking, and general Python interpreter configuration
experimentation. It essentially opens the door to things previously only
possible if you wrote code interfacing with Python's C APIs.</p>
<p>I can also envision tools that hide the existence of Python wanting to
leverage the single file Python distribution property of <code>pyoxy</code>. For
example, tools like Ansible could copy <code>pyoxy</code> to a remote machine to provide
a well-defined Python execution environment without having to rely on what
packages are installed. Or <code>pyoxy</code> could be copied into a container or
other sandboxed/minimal environment to provide a Python interpreter.</p>
<p>And that's PyOxy. I hope you find it useful. Please file any bug reports
or feature requests in <a href="https://github.com/indygreg/PyOxidizer/issues">PyOxidizer's issue tracker</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Expanding Apple Ecosystem Access with Open Source, Multi Platform Code Signing</title>
      <link>http://gregoryszorc.com/blog/2022/04/25/expanding-apple-ecosystem-access-with-open-source,-multi-platform-code-signing</link>
      <pubDate>Mon, 25 Apr 2022 08:00:00 PDT</pubDate>
      <category><![CDATA[Apple]]></category>
      <category><![CDATA[Rust]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2022/04/25/expanding-apple-ecosystem-access-with-open-source,-multi-platform-code-signing</guid>
      <description>Expanding Apple Ecosystem Access with Open Source, Multi Platform Code Signing</description>
      <content:encoded><![CDATA[<p>A little over one year ago, I
<a href="/blog/2021/04/14/pure-rust-implementation-of-apple-code-signing/">announced a project to implement Apple code signing in pure Rust</a>.
There have been quite a number of developments since that post and I thought
a blog post was in order. So here we are!</p>
<p>But first, some background on why we're here.</p>
<h2>Background</h2>
<p>(Skip this section if you just want to get to the technical bits.)</p>
<p>Apple runs some of the largest and most profitable software application
ecosystems in existence. Gaining access to these ecosystems has traditionally
required the use of macOS and membership in the Apple Developer Program.</p>
<p>For the most part this makes sense: if you want to develop applications for
Apple operating systems you will likely utilize Apple's operating systems
and Apple's official tooling for development and distribution. Sticking to
the paved road is a good default!</p>
<p>But many people want more... flexibility. Open source developers, for example,
often want to distribute cross-platform applications with minimal effort.
There are entire programming language ecosystems where the operating system
you are running on is abstracted away as an implementation detail for many
applications. <strong>By creating a de facto requirement that macOS, iOS, etc
development require the direct access to macOS and (often above market priced)
Apple hardware, the distribution requirements imposed by Apple's software
ecosystems are effectively exclusionary and prevent interested parties
from contributing to the ecosystem.</strong></p>
<p>One of the aspects of software distribution on Apple platforms that trips
a lot of people up is code signing and notarization. Essentially, you need
to:</p>
<ol>
<li>Embed a cryptographic signature in applications that effectively attests
   to its authenticity from an Apple Developer Program associated account.
   (This is signing.)</li>
<li>Upload your application to Apple so they can inspect it, verify it meets
   requirements, likely store a copy of it. Apple then issues their own
   cryptographic signature called a <em>notarization ticket</em> which then needs
   to be <em>stapled</em>/attached to the application being distributed so Apple
   operating systems can trust it. (This is notarization.)</li>
</ol>
<p>Historically, these steps required Apple proprietary software run exclusively
from macOS. This means that even if you are in a software ecosystem like Rust,
Go, or the web platform where you can cross-compile apps without direct access
to macOS (testing is obviously a different story), you would still need macOS
somewhere if you wanted to sign and notarize your application. And signing and
notarization is effectively required on macOS due to default security settings.
On mobile platforms like iOS, it is impossible to distribute applications that
aren't signed and notarized unless you are running a jailbreaked device.</p>
<p>A lot of people (myself included) have grumbled at these requirements.
Why should I be forced to involve an Apple machine as part of my software
release process if I don't need macOS to build my application? Why do I have
to go through a convoluted dance to sign and notarize my application at
release time - can't it be more streamlined?</p>
<p>When I looked at this space last year, I saw some obvious inefficiencies
and room to improve. So as I said then, I <em>foolishly</em> set out to reimplement
Apple code signing so developers would have more flexibility and opportunity
for distributing applications to Apple's ecosystems.</p>
<p><strong>The ultimate goal of this work is to expand Apple ecosystem access to more
developers.</strong> A year later, I believe I'm delivering a product capable of
doing this.</p>
<h2>One Year Later</h2>
<p><strong>Foremost, I'm excited to announce release of
<a href="https://github.com/indygreg/PyOxidizer/releases/tag/apple-codesign/0.14.0">rcodesign 0.14.0</a>.
This is the first time I'm publishing pre-built binaries (Linux, Windows, and macOS)
of <code>rcodesign</code>. This reflects my confidence in the relative maturity of the
software.</strong></p>
<p>In case you are wondering, yes, the macOS <code>rcodesign</code> executable is self-signed:
it was signed by a GitHub Actions Linux runner using a code signing certificate
exclusive to a YubiKey. That YubiKey was plugged into a Windows 11 desktop next to
my desk. The <code>rcodesign</code> executable was not copied between machines as part of the
signing operation. Read on to learn about the sorcery that made this possible.</p>
<p>A lot has changed in the <a href="https://github.com/indygreg/apple-platform-rs/tree/main/apple-codesign">apple-codesign</a>
project / Rust crate in the last year! Just look at the
<a href="https://github.com/indygreg/apple-platform-rs/blob/main/apple-codesign/CHANGELOG.rst">changelog</a>!</p>
<p>The project was renamed from <code>tugger-apple-codesign</code>.</p>
<p>(If you installed via <code>cargo install</code>, you'll need to
<code>cargo install --force apple-codesign</code> to force Cargo to overwrite the <code>rcodesign</code>
executable with one from a different crate.)</p>
<p>The <code>rcodesign</code> CLI executable is still there and more powerful than ever.
You can still sign Apple applications from Linux, Windows, macOS, and any other
platform you can get the Rust program to compile on.</p>
<p>There is now <a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign.html">Sphinx documentation for the project</a>.
This is published on readthedocs.io alongside PyOxidizer's documentation (because
I'm using a monorepo). There's some general documentation in there, such as a
guide on how to
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_custom_assessment_policies.html">selectively bypass Gatekeeper</a>
by deploying your own alternative code signing PKI to parallel Apple's. (This
seems like something many companies would want but for whatever reason I'm
not aware of anyone doing this - possibly because very few people understand
how these systems work.)</p>
<p>There are bug fixes galore. When I look back at the state of <code>rcodesign</code>
when I first blogged about it, I think of how naive I was. There were a myriad
of applications that wouldn't pass notarization because of a long tail of bugs.
There are still known issues. But <strong>I believe many applications will
successfully sign and notarize now.</strong> I consider failures novel and worthy of
bug reports - so please <a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_debugging.html">report them</a>!</p>
<p>Read on to learn about some of the notable improvements in the past year (many
of them occurring in the last two months).</p>
<h2>Support for Signing Bundles, DMGs, and <code>.pkg</code> Installers</h2>
<p>When I announced this project last year, only Mach-O binaries and trivially
simple <code>.app</code> bundles were signable. And even then there were a ton of subtle
issues.</p>
<p><code>rcodesign sign</code> can now sign more complex bundles, including many nested
bundles. There are reports of iOS app bundles signing correctly! (However, we
don't yet have good end-user documentation for signing iOS apps. I will gladly
accept PRs to improve the documentation!)</p>
<p>The tool also gained support for signing <code>.dmg</code> disk image files and <code>.pkg</code>
flat package installers.</p>
<p>Known limitations with signing are now
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_quirks.html">documented</a>
in the Sphinx docs.</p>
<p><strong>I believe <code>rcodesign</code> now supports signing all the major file formats used
for Apple software distribution.</strong> If you find something that doesn't sign
and it isn't documented as a known issue with an existing GitHub issue tracking
it, please report it!</p>
<h2>Support for Notarization on Linux, Windows, and macOS</h2>
<p>Apple publishes a Java tool named
<a href="https://help.apple.com/itc/transporteruserguide/">Transporter</a> that enables you
to upload artifacts to Apple for notarization. They make this tool available for
Linux, Windows, and of course macOS.</p>
<p>While this tool isn't open source (as far as I know), usage of this tool enables
you to notarize from Linux and Windows while still using Apple's official
tooling for communicating with their servers.</p>
<p><code>rcodesign</code> now has support for invoking Transporter and uploading artifacts
to Apple for notarization. We now support notarizing bundles, <code>.dmg</code> disk
images, and <code>.pkg</code> flat installer packages. I've successfully notarized all
of these application types from Linux.</p>
<p>(I'm capable of implementing
an alternative uploader in pure Rust but without assurances that Apple won't
bring down the ban hammer for violating terms of use, this is a bridge I'm
not yet willing to cross. The requirement to use Transporter is literally the
only thing standing in the way of making <code>rcodesign</code> an all-in-one single
file executable tool for signing and notarizing Apple software and I <strong>really</strong>
wish I could deliver this user experience win without reprisal.)</p>
<p><strong>With support for both signing and notarizing all application types, it is
now possible to release Apple software without macOS involved in your release
process.</strong></p>
<h2>YubiKey Integration</h2>
<p>I try to use my YubiKeys as much as possible because a secret or private key
stored on a YubiKey is likely more secure than a secret or private key sitting
around on a filesystem somewhere. If you hack my machine, you can likely
gain access to my private keys. But you will need physical access to my
YubiKey and to compel or coerce me into unlocking it in order to gain access
to its private keys.</p>
<p><strong><code>rcodesign</code> now has support for using YubiKeys for signing operations.</strong></p>
<p>This does require an off-by-default <code>smartcard</code> Cargo feature. So if
building manually you'll need to e.g.
<code>cargo install --features smartcard apple-codesign</code>.</p>
<p>The YubiKey integration comes courtesy of the amazing
<a href="https://crates.io/crates/yubikey">yubikey</a> Rust crate. This crate will speak
directly to the smartcard APIs built into macOS and Windows. So if you have an
<code>rcodesign</code> build with YubiKey support enabled, YubiKeys should
<em>just work</em>. Try it by plugging in your YubiKey and running
<code>rcodesign smartcard-scan</code>.</p>
<p>YubiKey integration has its
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_smartcard.html">own documentation</a>.</p>
<p>I even implemented some commands to make it easy to manage the code signing
certificates on your YubiKey. For example, you can run
<code>rcodesign smartcard-generate-key --smartcard-slot 9c</code> to generate a new private
key directly on the device and then
<code>rcodesign generate-certificate-signing-request --smartcard-slot 9c --csr-pem-path csr.pem</code>
to export that certificate to a Certificate Signing Request (CSR), which you can
exchange for an Applie-issued signing certificate at developer.apple.com. <strong>This
means you can easily create code signing certificates whose private key was
generated directly on the hardware device and can never be exported.</strong>
Generating keys this way is widely considered to be more secure than storing
keys in software vaults, like Apple's Keychains.</p>
<h2>Remote Code Signing</h2>
<p>The feature I'm most excited about is what I'm calling
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing.html">remote code signing</a>.</p>
<p>Remote code signing allows you to delegate the low-level cryptographic signature
operations in code signing to a separate machine.</p>
<p>It's probably easiest to just demonstrate what it can do.</p>
<p><strong>Earlier today I signed a macOS universal Mach-O executable from a GitHub-hosted
Linux GitHub Actions runner using a YubiKey physically attached to the
Windows 11 machine next to my desk at home. The signed application was not
copied between machines.</strong></p>
<p>Here's how I did it.</p>
<p>I have a GitHub Actions workflow that calls <code>rcodesign sign --remote-signer</code>.
I manually triggered that workflow and started watching the near real time
job output with my browser. Here's a screenshot of the job logs:</p>
<p><img alt="GitHub Actions initiating remote code signing" src="https://raw.githubusercontent.com/indygreg/PyOxidizer/058f718641ad47b39ccf54346f0f0ad6e91bd09b/apple-codesign/docs/apple_codesign_actions_sjs_join.png" /></p>
<p><code>rcodesign sign --remote-signer</code> prints out some instructions (including a
wall of base64 encoded data) for what to do next. Importantly, it requests that
someone else run <code>rcodesign remote-sign</code> to continue the signing process.</p>
<p>And here's a screenshot of me doing that from the Windows terminal:</p>
<p><img alt="Windows terminal output from running remote-sign command" src="https://raw.githubusercontent.com/indygreg/PyOxidizer/058f718641ad47b39ccf54346f0f0ad6e91bd09b/apple-codesign/docs/apple_codesign_actions_signer_output.png" /></p>
<p>This log shows us connecting and authenticating with the YubiKey along
with some status updates regarding speaking to a remote server.</p>
<p>Finally, here's a screenshot of the GitHub Actions job output after
I ran that command on my Windows machine:</p>
<p><img alt="GitHub Actions initiating machine output" src="https://raw.githubusercontent.com/indygreg/PyOxidizer/058f718641ad47b39ccf54346f0f0ad6e91bd09b/apple-codesign/docs/apple_codesign_actions_initiator_output.png" /></p>
<p><em>Remote signing</em> enabled me to sign a macOS application from a GitHub Actions
runner operated by GitHub while using a code signing certificate securely
stored on my YubiKey plugged into a Windows machine hundreds of kilometers away
from the GitHub Actions runner. Magic, right?</p>
<p>What's happening here is the 2 <code>rcodesign</code> processes are communicating
with each other via websockets bridged by a central relay server.
(I operate a
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing_design.html#default-remote-code-signing-server">default server free of charge</a>.
The server is open source and a Terraform module is available if you want
to run your own server with hopefully just a few minutes of effort.)
When the initiating machine wants to create a signature, it sends a
message back to the <em>signer</em> requesting a cryptographic signature. The
signature is then sent back to the initiator, who incorporates it.</p>
<p><strong>I designed this feature with automated releases from CI systems (like
GitHub Actions) in mind. I wanted a way where I could streamline the
code signing and release process of applications without having to give
a low trust machine in CI ~unlimited access to my private signing key.
But the more I thought about it the more I realized there are likely
many other scenarios where this could be useful. Have you ever emailed
or Dropboxed an application for someone else to sign because you don't
have an Apple issued code signing certificate? Now you have an alternative
solution that doesn't require copying files around!</strong> As long as you
can see the log output from the initiating machine or have that output
communicated to you (say over a chat application or email), you can
remotely sign files on another machine!</p>
<h3>An Aside on the Security of Remote Signing</h3>
<p>At this point, I'm confident the more security conscious among you have
been grimacing for a few paragraphs now. Websockets through a central
server operated by a 3rd party?! Giving remote machines access to perform
code signing against arbitrary content?! Your fears and skepticism are
100% justified: I'd be thinking the same thing!</p>
<p>I fully recognize that a service that facilitates remote code signing makes
for a very lucrative attack target! If abused, it could be used to coerce
parties with valid code signing certificates to sign unwanted code, like
malware. There are many, many, many <em>wrong</em> ways to implement such a feature.
I pondered for hours about the threat modeling and how to make this feature
as secure as possible.</p>
<p><a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing_design.html">Remote Code Signing Design and Security Considerations</a>
captures some of my high level design goals and security assessments.
And <a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing_protocol.html">Remote Code Signing Protocol</a>
goes into detail about the communications protocol, including the
crypto (actual cryptography, not the fad) involved. The key takeaways are
the protocol and server are designed such that a malicious server or
man-in-the-middle can not forge signature requests. Signing sessions expire
after a few minutes and 3rd parties (or the server) can't inject malicious
messages that would result in unwanted signatures. There is an initial
handshake to derive a session ephemeral shared encryption key and from
there symmetric encryption keys are used so all meaningful messages between
peers are end-to-end encrypted. About the worst a malicious server could do
is conduct a denial of service. This is by design.</p>
<p>As I argue in <a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing_design.html#security-analysis-in-the-bigger-picture">Security Analysis in the Bigger Picture</a>,
I believe that my implementation of <em>remote signing</em> is <strong>more</strong> secure than
many common practices because common practices today entail making copies
of private keys and giving low trust machines (like CI workers) access to
private keys. Or files are copied around without cryptographic chain-of-custody
to prove against tampering. Yes, <em>remote signing</em> introduces a vector for remote
access to <em>use</em> signing keys. But practiced as I intended, <em>remote signing</em> can
eliminate the need to copy private keys or grant ~unlimited access to them.
From a threat modeling perspective, I think the net restriction in key
access makes <em>remote signing</em> more secure than the private key management
practices by many today.</p>
<p><strong>All that being said, the giant asterisk here is I implemented my own
cryptosystem to achieve end-to-end message security. If there are bugs in
the design or implementation, that cryptosystem could come crashing down,
bringing defenses against message forgery with it.</strong> At that point, a
malicious server or privileged network actor could potentially coerce
someone into signing unwanted software. But this is likely the extent of
the damage: an offline attack against the signing key should not be
possible since signing requires presence and since the private key is
never transmitted over the wire. Even without the end-to-end encryption,
the system is <em>arguably</em> more secure than leaving your private key
lingering around as an easily exfiltrated CI secret (or similar).</p>
<p>(I apologize to every cryptographer I worked with at Mozilla who beat into me
the commandment that <em>thou shall not roll their own crypto</em>: I have sinned
and I feel remorseful.) </p>
<p>Cryptography is hard. And I'm sure I made plenty of subtle mistakes.
<a href="https://github.com/indygreg/PyOxidizer/issues/552">Issue #552</a> tracks
getting an audit of this protocol and code performed. And the aforementioned
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing_protocol.html">protocol design docs</a>
call out some of the places where I question decisions I've made.</p>
<p><strong>If you would be interested in doing a security review on this feature,
please get in touch on issue #552 or
<a href="mailto:gregory.szorc@gmail.com">send me an email</a>. If there's one immediate
outcome I'd like from this blog post it would be for some white hat^Hknight
to show up and give me peace of mind about the cryptosystem implementation.</strong></p>
<p><strong>Until then, please assume the end-to-end encryption is completely flawed.</strong>
Consider asking someone with <em>security</em> or <em>cryptographer</em> in their job title
for their opinion on whether this feature is safe for you to use. Hopefully
we'll get a security review done soon and this caveat can go away!</p>
<p>If you do want to use this feature,
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_remote_signing.html">Remote Code Signing</a>
contains some usage documentation, including how to use it with GitHub
Actions. (I could also use some help productionizing a reusable GitHub Action
to make this more turnkey! Although I'm hesitant to do it before I know the
cryptosystem is sound.)</p>
<p>That was a long introduction to <em>remote code signing</em>. But I couldn't
in good faith present the feature without addressing the security aspect.
Hopefully I didn't scare you away! <strong>Traditional / local signing should
have no security concerns</strong> (beyond the willingness to run software written
by somebody you probably don't know, of course).</p>
<h2>Apple Keychain Support</h2>
<p>As of today's 0.14 release we now have early support for signing with code signing
certificates stored in Apple Keychains! If you created your Apple code signing
certificates in Keychain Access or Xcode, this is probably where you code
signing certificates live.</p>
<p>I held off implementing this for the longest time because I didn't perceive
there to be a benefit: if you are on macOS, just use Apple's official tooling.
But with <code>rcodesign</code> gaining support for remote code signing and some other
features that could make it a compelling replacement for Apple tooling on
all platforms, I figured we should provide the feature so we stop discouraging
people to export private keys from Keychains.</p>
<p>This integration is very young and there's still a lot that can be done,
such as automatically using an appropriate signing certificate based on
what you are signing. Please file feature request issues if there's
a must-have feature you are missing!</p>
<h2>Better Debugging of Failures</h2>
<p>Apple's code signing is complex. It is easy for there to be subtle differences
between Apple's tooling and <code>rcodesign</code>.</p>
<p><code>rcodesign</code> now has <code>print-signature-info</code> and <code>diff-signatures</code> commands to
dump and compare YAML metadata pertinent to code signing to make it easier to
compare behavior between code signing implementations and even multiple
signing operations.</p>
<p>The documentation around
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_debugging.html">debugging and reporting bugs</a>
now emphasizes using these tools to help identify bugs.</p>
<h2>A Request For Users and Feedback</h2>
<p>I now believe <code>rcodesign</code> to be generally usable. I've thrown a lot of
random software at it and I feel like most of the big bugs and major missing
features are behind us.</p>
<p>But I also feel it hasn't yet received wide enough attention to have confidence
in that assessment.</p>
<p><strong>If you want to help the development of this tool, the most important
actions you can take are to attempt signing / notarization operations with it
and report your results.</strong></p>
<p>Does <code>rcodesign</code> spark joy? Please leave a comment in the
<a href="https://github.com/indygreg/PyOxidizer/discussions/556">GitHub discussion for the latest release</a>!</p>
<p>Does <code>rcodesign</code> not work? I would very much appreciate a bug report!
Details on how to file good bugs are
<a href="https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_debugging.html">in the docs</a>.</p>
<p>Have general feedback? UI is confusing? Documentation is insufficient?
Leave a comment in the aforementioned discussion. Or
<a href="https://github.com/indygreg/PyOxidizer/issues/new">create a GitHub issue</a> if
you think it is actionable. I can't fix what I don't know about!</p>
<p>Have private feedback? <a href="mailto:gregory.szorc@gmail.com">Send me an email</a>.</p>
<h2>Conclusion</h2>
<p>I could write thousands of words about all I learned from hacking on this
project.</p>
<p>I've learned way too much about too many standards and specifications in the
crypto space. RFCs 2986, 3161, 3280, 3281, 3447, 4210, 4519, 5280, 5480,
5652, 5869, 5915, 5958, and 8017 plus probably a few more. How cryptographic
primitives are stored and expressed: ASN.1, OIDs, BER, DER, PEM, SPKI,
PKCS#1, PKCS#8. You can show me the raw parse tree for an ASN.1 data structure
and I can probably tell you what RFC defines it. I'm not proud of this. But
I will say actually knowing what every field in an X.509 certificate does
or the many formats that cryptographic keys are expressed in seems empowering.
Before, I would just search for the <code>openssl</code> incantation to do something.
Now, I know which ASN.1 data structures are involved and how to manipulate
the fields within.</p>
<p>I've learned way too much around minutia around how Apple code signing
actually works. The mechanism is way too complex for something in the security
space. There was at least one high profile Gatekeeper bug in the past year
allowing improperly signed code to run. I suspect there will be more: the
surface area to exploit is just too large.</p>
<p><strong>I think I'm proud of building an open source implementation of Apple's code
signing. To my knowledge nobody else has done this outside of Apple. At least
not to the degree I have.</strong> Then factor in that I was able to do this without
access (or willingness) to look at Apple source code and much of the progress was
achieved by diffing and comparing results with Apple's tooling. Hours of
staring at diffoscope and comparing binary data structures. Hours of trying
to find the magical settings that enabled a SHA-1 or SHA-256 digest to agree.
It was tedious work for sure. I'll likely never see a financial return on
the time equivalent it took me to develop this software. But, I suppose I
can nerd brag that I was able to implement this!</p>
<p><strong>But the real reward for this work will be if it opens up avenues to more
(open source) projects distributing to the Apple ecosystems.</strong> This has
historically been challenging for multiple reasons and many open source
projects have avoided official / proper distribution channels to avoid the
pain (or in some cases because of philosophical disagreements with the premise
of having a walled software garden in the first place). I suspect things
will only get worse, as I feel it is inevitable Apple clamps down on signing
and notarization requirements on macOS due to the rising costs of malware
and ransomware. <strong>So having an alternative, open source, and multi-platform
implementation of Apple code signing seems like something important that
should exist in order to provide opportunities to otherwise excluded
developers. I would be humbled if my work empowers others. And this is
all the reward I need.</strong></p>]]></content:encoded>
    </item>
    <item>
      <title>Bulk Analyze Linux Packages with Linux Package Analyzer</title>
      <link>http://gregoryszorc.com/blog/2022/01/09/bulk-analyze-linux-packages-with-linux-package-analyzer</link>
      <pubDate>Sun, 09 Jan 2022 21:10:00 PST</pubDate>
      <category><![CDATA[packaging]]></category>
      <category><![CDATA[Rust]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2022/01/09/bulk-analyze-linux-packages-with-linux-package-analyzer</guid>
      <description>Bulk Analyze Linux Packages with Linux Package Analyzer</description>
      <content:encoded><![CDATA[<p>I've frequently wanted to ask random questions about Linux packages and
binaries:</p>
<ul>
<li>Which packages provide a file X?</li>
<li>Which binaries link against a library X?</li>
<li>Which libraries/packages define/export a symbol X?</li>
<li>Which binaries reference an undefined symbol X?</li>
<li>Which features of ELF are in common use?</li>
<li>Which x86 instructions are in a binary?</li>
<li>What are the most common ELF section names and their sizes?</li>
<li>What are the differences between package X in distributions A and B?</li>
<li>Which ELF binaries have the most relocations?</li>
<li>And many more.</li>
</ul>
<p>So, I built
<a href="https://github.com/indygreg/PyOxidizer/tree/main/linux-package-analyzer">Linux Package Analyzer</a>
to facilitate answering questions like this.</p>
<p>Linux Package Analyzer is a Rust crate providing the <code>lpa</code> CLI tool. <code>lpa</code> currently
supports importing Debian and RPM package repositories (the most popular Linux
packaging formats) into a local SQLite database so subsequent analysis can be
efficiently performed offline. In essence:</p>
<ol>
<li>Run <code>lpa import-debian-repository</code> or <code>lpa import-rpm-repository</code> and point
   the tool at the base URL of a Linux package repository.</li>
<li>Package indices are fetched.</li>
<li>Discovered <code>.deb</code> and <code>.rpm</code> files are downloaded.</li>
<li>Installed files within each package archive are inspected.</li>
<li>Binary/ELF files have their executable sections disassembled.</li>
<li>Results are stored in a local SQLite database for subsequent analysis.</li>
</ol>
<p>The LPA-built database currently stores the following:</p>
<ul>
<li>Imported packages (name, version, source URL).</li>
<li>Installed files within each package (path, size).</li>
<li>ELF file information (parsed fields from header, number of relocations,
  important metadata from the <code>.dynamic</code> section, etc).</li>
<li>ELF sections (number, type, flags, address, file offset, etc).</li>
<li>Dynamic libraries required by ELF files.</li>
<li>ELF symbols (name, demangled name, type constant, binding, visibility,
  version string, etc).</li>
<li>For x86 architectures, counts of unique instructions in each ELF file and
  counts of instructions referencing specific registers.</li>
</ul>
<p>Using a command like <code>lpa import-debian-repository --components main,multiverse,restricted,universe
--architectures amd64 http://us.archive.ubuntu.com/ubuntu impish</code>, I can import
the (currently) ~96 GB of package data from 63,720 packages defining Ubuntu 21.10
to a local ~12 GB SQLite database and answer tons of random questions. Interesting
insights yielded so far include:</p>
<ul>
<li>The entirety of the package ecosystem for amd64 consists of
  63,720 packages providing 6,704,222 files (168,730 of them ELF binaries)
  comprising 355,700,362,973 bytes in total.</li>
<li>Within the 168,730 ELF binaries are 5,286,210 total sections having
  606,175 distinct names. There are also 116,688,943 symbols
  in symbol tables (debugging info is not included and local symbols not
  imported or exported are often not present in symbol tables) across
  19,085,540 distinct symbol names. The sum of all the unique symbol names
  is 1,263,441,355 bytes and 4,574,688,289 bytes if you count occurrences
  across all symbol tables (this might be an over count due to how ELF string
  tables work).</li>
<li>The longest demangled ELF symbol is 271,800 characters and is defined in
  the file <code>usr/lib/x86_64-linux-gnu/libmshr.so.2019.2.0.dev0</code> provided by
  the <code>libmshr2019.2</code> package.</li>
<li>The longest non-mangled ELF symbol is 5,321 characters and is defined in
  multiple files/packages, as it is part of a library provided by GCC.</li>
<li>Only 145 packages have files with
  <a href="https://sourceware.org/glibc/wiki/GNU_IFUNC">indirect functions (IFUNCs)</a>.
  If you discard duplicates (mainly from GCC and glibc), you are left with
  ~11 packages. This does not appear to be a popular ELF feature!</li>
<li>With 54,764 references in symbol tables, <code>strlen</code> appears to be the most
  (recognized) widely used libc symbol. It even bests <code>memcpy</code> (52,726) and
  <code>free</code> (42,603).</li>
<li><code>MOV</code> is the most frequent x86 instruction, followed by <code>CALL</code>. (I could
  write an entire blog post about observations about x86 instruction use.)</li>
</ul>
<p>There's a trove of data in the SQLite database and the <code>lpa</code> commands only
expose a fraction of it. I reckon a lot of interesting tweets, blog posts,
research papers, and more could be derived from the data that <code>lpa</code> assembles.</p>
<p><code>lpa</code> does all of its work in-process using pure Rust. The Debian and RPM
repository interaction is handled via the
<a href="https://crates.io/crates/debian-packaging">debian-packaging</a> and
<a href="https://crates.io/crates/rpm-repository">rpm-repository</a> crates (which I
wrote). ELF file parsing is handled by the (amazing)
<a href="https://crates.io/crates/object">object</a> crate. And x86 disassembling via
the <a href="https://crates.io/crates/iced-x86">iced-x86</a> crate. Many tools similar
to <code>lpa</code> call out to other processes to interface with <code>.deb</code>/.<code>rpm</code> packages,
parse ELF files, disassemble x86, etc. Doing this in pure Rust makes life so
much simpler as all the functionality is self-contained and I don't have to
worry about run-time dependencies for random tools. This means that <code>lpa</code>
should <em>just work</em> from Windows, macOS, and other non-Linux environments.</p>
<p>Linux Package Analyzer is very much in its infancy. And I don't really
have a grand vision for it. (I built it and some of the packaging code it
is built on) in support of some even grander projects I have cooking.) Please
file bugs, feature requests, and pull requests
<a href="https://github.com/indygreg/PyOxidizer">in GitHub</a>. The project is currently
part of the PyOxidizer repo (because I like monorepos). But I may pull it and
other os/packaging/toolchain code into a new monorepo since target audiences
are different.</p>
<p>I hope others find this tool useful!</p>]]></content:encoded>
    </item>
    <item>
      <title>Rust Implementation of Debian Packaging Primitives</title>
      <link>http://gregoryszorc.com/blog/2022/01/03/rust-implementation-of-debian-packaging-primitives</link>
      <pubDate>Mon, 03 Jan 2022 16:00:00 PST</pubDate>
      <category><![CDATA[packaging]]></category>
      <category><![CDATA[Rust]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2022/01/03/rust-implementation-of-debian-packaging-primitives</guid>
      <description>Rust Implementation of Debian Packaging Primitives</description>
      <content:encoded><![CDATA[<p>Does your Linux distribution use tools with <code>apt</code> in their name to manage
system packages? If so, your system packages are using Debian packaging.</p>
<p>Most tools interfacing with Debian packages (<code>.deb</code> files) and repositories
use functionality provided by the <a href="https://salsa.debian.org/apt-team/apt">apt</a>
repository. This repository provides libraries like <code>libapt</code> as well as
tools like <code>apt-get</code> and <code>apt</code>. Most of the functionality is implemented in
C++.</p>
<p>I wanted to raise awareness that I've begun implementing Debian packaging
primitives in pure Rust. The <code>debian-packaging</code> crate is
<a href="https://crates.io/crates/debian-packaging">published on crates.io</a>. For
now, it is developed inside the
<a href="https://github.com/indygreg/PyOxidizer">PyOxidizer repository</a> (because I
like monorepos).</p>
<p>So far, a handful of useful functionality is implemented:</p>
<ul>
<li>Parsing and serializing <a href="https://www.debian.org/doc/debian-policy/ch-controlfields.html">control files</a></li>
<li>Reading repository indices files and parsing their content.</li>
<li>Reading HTTP hosted repositories.</li>
<li>Publishing repositories to the filesystem and S3.</li>
<li>Writing changelog files.</li>
<li>Reading and writing <code>.deb</code> files.</li>
<li>Copying repositories.</li>
<li>Creating repositories.</li>
<li>PGP signing and verification operations.</li>
<li>Parsing and sorting version strings.</li>
<li>Dependency syntax parsing.</li>
<li>Dependency resolution.</li>
</ul>
<p>Hopefully the <a href="https://docs.rs/debian-packaging/0.9.0/debian_packaging/">documentation</a>
contains all you would want to know for how to use the crate.</p>
<p>The crate is designed to be used as a library so any Rust program can
(hopefully) easily tap the power of the Debian packaging ecosystem.</p>
<p>As with most software, there are likely several bugs and many features not yet
implemented. But I have bulk downloaded the entirety of some distribution's
repositories without running into obvious parse/reading failures. So I'm
reasonably confident that important parts of the code (like control file parsing,
repository indices file handling, and <code>.deb</code> file reading) work as advertised.</p>
<p>Hopefully someone out there finds this work useful!</p>]]></content:encoded>
    </item>
    <item>
      <title>Why You Shouldn't Use Git LFS</title>
      <link>http://gregoryszorc.com/blog/2021/05/12/why-you-shouldn't-use-git-lfs</link>
      <pubDate>Wed, 12 May 2021 10:30:00 PDT</pubDate>
      <category><![CDATA[Mercurial]]></category>
      <category><![CDATA[Git]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2021/05/12/why-you-shouldn't-use-git-lfs</guid>
      <description>Why You Shouldn't Use Git LFS</description>
      <content:encoded><![CDATA[<p>I have long held the opinion that you should avoid Git LFS if possible.
Since people keeping asking me why, I figured I'd capture my thoughts
in a blog post so I have something to refer them to.</p>
<p>Here are my reasons for not using Git LFS.</p>
<h2>Git LFS is a Stop Gap Solution</h2>
<p>Git LFS was developed outside the official Git project to fulfill a
very real market need that Git didn't/doesn't handle large files very
well.</p>
<p>I believe it is inevitable that Git will gain better support for
handling of large files, as this seems like a critical feature for
a popular version control tool.</p>
<p>If you make this long bet, LFS is only an interim solution and its
value proposition disappears after Git has better native support
for large files.</p>
<p>LFS as a stop gap solution would be tolerable except for the fact
that...</p>
<h2>Git LFS is a One Way Door</h2>
<p>The adoption or removal of Git LFS in a repository is an irreversible
decision that requires rewriting history and losing your original 
commit SHAs.</p>
<p>In some contexts, rewriting history is tolerable. In many others, it
is an extremely expensive proposition. My experience maintaining
version control in professional contexts aligns with the opinion
that rewriting history is expensive and should only be considered a
measure of last resort. Maybe if tools made it easier to rewrite
history without the negative consequences (e.g. GitHub would redirect
references to old SHA1 in URLs and API calls) I would change my opinion
here. Until that day, the drawbacks of losing history are just too
high to stomach for many.</p>
<p>The reason adoption or removal of LFS is irreversible is due to the
way Git LFS works. What LFS does is change the blob content that a
Git commit/tree references. Instead of the content itself, it stores
a pointer to the content. At checkout and commit time, LFS blobs/records
are treated specially via a mechanism in Git that allows content to be
rewritten as it moves between Git's core storage and its materialized
representation. (The same filtering mechanism is responsible for
normalizing line endings in text files. Although that feature is built
into the core Git product and doesn't work exactly the same way. But
the principles are the same.)</p>
<p>Since the LFS pointer is part of the Merkle tree that a Git commit
derives from, you can't add or remove LFS from a repo without rewriting
existing Git commit SHAs.</p>
<p>I want to explicitly call out that even if a rewrite is acceptable in
the short term, things may change in the future. If you adopt LFS
today, you are committing to a) running an LFS server forever b)
incurring a history rewrite in the future in order to remove LFS from
your repo, or c) ceasing to provide an LFS server and locking out people
from using older Git commits. I don't think any of these are great
options: I would prefer if there were a way to offboard from LFS in
the future with minimal disruption. This is theoretically possible, but
it requires the Git core product to recognize LFS blobs/records natively.
There's no guarantee this will happen. So adoption of Git LFS is a one
way door that can't be easily reversed.</p>
<h2>LFS is More Complexity</h2>
<p>LFS is more complex for Git end users.</p>
<p>Git users have to install, configure, and sometimes know about the
existence of Git LFS. Version control should <em>just work</em>. Large file
handling should <em>just work</em>. End-users shouldn't have to care that
large files are handled slightly differently from small files.</p>
<p>The usability of Git LFS is generally pretty good. However, there's an
upper limit on that usability as long as LFS exists outside the core
Git product. And LFS will likely never be integrated into the core Git
product because the Git maintainers know that LFS is only a stop gap
solution. They would rather solve large files storage <em>correctly</em> than
~forever carry the legacy baggage of having to support LFS in the core
product.</p>
<p>LFS is more complexity for Git server operators as well. Instead of
a self-contained Git repository and server to support, you now have
to support a likely separate HTTP server to facilitate LFS access.
This isn't the hardest thing in the world, especially since we're
talking about key-value blob storage, which is arguably a solved problem.
But it's another piece of infrastructure to support and secure and it
increases the surface area of complexity instead of minimizing it.
As a server operator, I would much prefer if the large file storage
were integrated into the core Git product and I simply needed to provide
some settings for it to <em>just work</em>.</p>
<h2>Mercurial Does LFS Slightly Better</h2>
<p>Since I'm a maintainer of the Mercurial version control tool, I thought
I'd throw out how Mercurial handles large file storage better than
Git. Mercurial's large file handling isn't great, but I believe it
is strictly better with regards to the trade-offs of adopting large
file storage.</p>
<p>In Mercurial, use of LFS is a dynamic feature that server/repo operators
can choose to enable or disable whenever they want. When the Mercurial
server sends file content to a client, presence of external/LFS storage
is a <em>flag</em> set on that file revision. Essentially, the flag says <em>the
data you are receiving is an LFS record, not the file content itself</em> and
the client knows how to resolve that record into content.</p>
<p>Conceptually, this is little different from Git LFS records in terms of
content resolution. However, the big difference is this flag is part
of the repository interchange data, not the core repository data as it
is with Git. Since this flag isn't part of the Merkle tree used to
derive the commit SHA, adding, removing, or altering the content of the
LFS records doesn't require rewriting commit SHAs. The tracked content
SHA - the data now stored in LFS - is still tracked as part of the Merkle
tree, so the integrity of the commit / repository can still be verified.</p>
<p>In Mercurial, the choice of whether to use LFS and what to use LFS for
is made by the server operator and settings can change over time. For
example, you could start with no use of LFS and then one day decide to
use LFS for all file revisions larger than 10 MB. Then a year later
you lower that to all revisions larger than 1 MB. Then a year after
that Mercurial gains better <em>native</em> support for large files and
you decide to stop using LFS altogether.</p>
<p>Also in Mercurial, it is possible for clients to push a large file
<em>inline</em> as part of the push operation. When the server sees that
large file, it can be like <em>this is a large file: I'm going to add
it to the blob store and advertise it as LFS</em>. Because the large
file record isn't part of the Merkle tree, you can have nice things
like this.</p>
<p>I suspect it is only a matter of time before Git's wire protocol learns
the ability to dynamically advertise <em>remote servers</em> for content
retrieval and this feature will be leveraged for better large file
handling. Until that day, I suppose we're stuck with having to
rewrite history with LFS and/or funnel large blobs through Git natively,
with all the pain that entails.</p>
<h2>Conclusion</h2>
<p>This post summarized reasons to avoid Git LFS. Are there justifiable
scenarios for using LFS? Absolutely! If you insist on using Git and
insist on tracking many <em>large</em> files in version control, you
should definitely consider LFS. (Although, if you are a heavy user
of large files in version control, I would consider Plastic SCM instead,
as they seem to have the most mature solution for large files handling.)</p>
<p>The main point of this post is to highlight some drawbacks with
using Git LFS because Git LFS is most definitely not a magic bullet. If
you can stomach the short and long term effects of Git LFS adoption, by
all means, use Git LFS. But please make an informed decision either way.</p>]]></content:encoded>
    </item>
    <item>
      <title>Pure Rust Implementation of Apple Code Signing</title>
      <link>http://gregoryszorc.com/blog/2021/04/14/pure-rust-implementation-of-apple-code-signing</link>
      <pubDate>Wed, 14 Apr 2021 13:45:00 PDT</pubDate>
      <category><![CDATA[PyOxidizer]]></category>
      <category><![CDATA[Apple]]></category>
      <category><![CDATA[Rust]]></category>
      <guid isPermaLink="true">http://gregoryszorc.com/blog/2021/04/14/pure-rust-implementation-of-apple-code-signing</guid>
      <description>Pure Rust Implementation of Apple Code Signing</description>
      <content:encoded><![CDATA[<p>A few weeks ago I (foolishly?) set out to implement Apple code signing
(what Apple's <code>codesign</code> tool does) in pure Rust.</p>
<p>I wanted to quickly announce on this blog the existence of the project and
the news that as of a few minutes ago, the <code>tugger-apple-codesign</code> crate
implementing the code signing functionality is now
<a href="https://crates.io/crates/tugger-apple-codesign">published on crates.io</a>!</p>
<p>So, you can now sign Apple binaries and bundles on non-Apple hardware by
doing something like this:</p>
<pre><code>$ cargo install tugger-apple-codesign
$ rcodesign sign /path/to/input /path/to/output
</code></pre>
<p>Current features include:</p>
<ul>
<li>Robust support for parsing embedded signatures and most related data
  structures. <code>rcodesign extract</code> can be used to extract various signature
  data in raw or human readable form.</li>
<li>Parse and verify RFC 5652 Cryptographic Message Syntax (CMS) signature
  data.</li>
<li>Sign binaries. If a code signing certificate key pair is provided,
  a CMS signature will be created. This includes support for Time-Stamp Protocol
  (TSP) / RFC 3161 tokens. If no key pair is provided, you get an ad-hoc
  signature.</li>
<li>Signing bundles. Nested bundles and binaries will automatically be signed.
  Non-code resources will be digested and a <code>CodeResources</code> XML file will be
  produced.</li>
</ul>
<p>The most notable missing features are:</p>
<ul>
<li>No support for obtaining signing keys from keychains. If you want to sign
  with a cryptographic key pair, you'll need to point the tool at a PEM encoded
  key pair and CA chain.</li>
<li>No support for parsing the Code Signing Requirements language. We can parse the
  binary encoding produced by <code>csreq -b</code> and convert it back to this DSL. But we
  don't parse the human friendly language.</li>
<li>No support for notarization.</li>
</ul>
<p>All of these could likely be implemented. However, I am not actively working on
any of these features. If you would like to contribute support, make noise in
the <a href="https://github.com/indygreg/apple-platform-rs/issues">GitHub issue tracker</a>.</p>
<p>The Rust API, CLI, and documentation are still a bit rough around the edges. I
haven't performed thorough QA on aspects of the functionality. However, the
tool is able to produce signed binaries that Apple's canonical <code>codesign</code> tool
says are well-formed. So I'm reasonably confident some of the functionality
works as intended. If you find bugs or missing features, please
<a href="https://github.com/indygreg/apple-platform-rs/issues">report them on GitHub</a>. Or even
better: submit pull requests!</p>
<p>As part of this project, I also created and published the
<a href="https://crates.io/crates/cryptographic-message-syntax">cryptographic-message-syntax</a>
crate, which is a pure Rust partial implementation of RFC 5652, which defines
the cryptographic message signing mechanism. This RFC is a bit dated and seems
to have been superseded by RPKI. So you may want to look elsewhere before
inventing new signing mechanisms that use this format.</p>
<p>Finally, it appears the Windows code signing mechanism (Authenticode) also uses
RFC 5652 (or a variant thereof) for cryptographic signatures. So by implementing
Apple code signatures, I believe I've done most of the legwork to implement
Windows/PE signing! I'll probably implement Windows signing in a new crate whenever
I hook up automatic code signing to PyOxidizer, which was the impetus for this work
(I want to make it possible to build distributable Apple programs without Apple
hardware, using as many open source Rust components as possible).</p>]]></content:encoded>
    </item>
  </channel>
</rss>
