<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>david alfonso</title><link href="https://davidalfonso.es/" rel="alternate"></link><link href="https://davidalfonso.es/feed/atom" rel="self"></link><id>https://davidalfonso.es/</id><updated>2024-05-29T00:00:00+02:00</updated><entry><title>Book review: Performance Testing</title><link href="https://davidalfonso.es/posts/performance-testing-book" rel="alternate"></link><published>2024-05-29T00:00:00+02:00</published><updated>2024-05-29T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2024-05-29:/posts/performance-testing-book</id><summary type="html">&lt;p&gt;My review of the book "Performance Testing", written by Keith Yorkston.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Performance testing book cover" src="/images/book-performance-testing.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Title&lt;/strong&gt;: &lt;a href="https://www.goodreads.com/book/show/58335292-performance-testing"&gt;Performance Testing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subtitle&lt;/strong&gt;: An ISTQB Certified Tester Foundation Level Specialist Certification Review&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Author&lt;/strong&gt;: Keith Yorkston&lt;/p&gt;
&lt;h2&gt;Review&lt;/h2&gt;
&lt;p&gt;The book is oriented towards obtaining the &lt;a href="https://www.istqb.org/certifications/performance-tester"&gt;ISTQB Certified Performance Tester certification&lt;/a&gt; and uses &lt;a href="https://istqb-main-web-prod.s3.amazonaws.com/media/documents/ISTQB-CT-PT_Syllabus_v1.0_2018.pdf"&gt;its syllabus&lt;/a&gt; as a guide to organize the contents.&lt;/p&gt;
&lt;p&gt;The author does a good job of explaining the certification syllabus, expanding it with information from other resources, adding his own experiences, and including summary boxes to favour studying. It does not feel like other certification books, where understanding and learning are disregarded in favour of an extreme focus on passing the exam. &lt;/p&gt;
&lt;p&gt;On the other hand, it only scratches the technical surface of systems performance, sometimes feels repetitive, and assumes you have a testing background (which seems reasonable if you're trying to get the certificate).&lt;/p&gt;
&lt;p&gt;It's a good read, even if you're not planning on taking the exam (like me), but I'd recommend skimming over the chapters related to formal processes and to testing tools.&lt;/p&gt;
&lt;h2&gt;Key Insights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The problem with performance is where the cut-off between “good” and “bad” performance exists.&lt;/li&gt;
&lt;li&gt;Performance is a component of a user’s “good experience” and forms part of an acceptable quality level.&lt;/li&gt;
&lt;li&gt;Quality attributes of "Performance Efficiency" as per ISO 25010 (which superseded ISO 9126): Time behavior, Resource utilisation, and Capacity.&lt;/li&gt;
&lt;li&gt;Performance testing is any kind of testing that focuses on the performance (responsiveness) of a system.&lt;/li&gt;
&lt;li&gt;Performance testing can be performed in all phases of the software development lifecycle.&lt;/li&gt;
&lt;li&gt;Components of a typical load test: Test controller, Load generators, Scripts (virtual users), SUT, and Monitoring.&lt;/li&gt;
&lt;li&gt;There are multiple causes for bad performance. For example:&lt;ul&gt;
&lt;li&gt;Resource saturation (cpu, memory, disk i/o, network i/o)&lt;/li&gt;
&lt;li&gt;Differences between testing and production (e.g. missing a load balancer)&lt;/li&gt;
&lt;li&gt;Graceless error handling (e.g. resource pools, queues, timeouts)&lt;/li&gt;
&lt;li&gt;Database design&lt;/li&gt;
&lt;li&gt;Networking issues&lt;/li&gt;
&lt;li&gt;Unexpected background loads&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Performance test goals are defined and achieved by the result metrics gathered during the test. Metrics are defined with measurements.&lt;/li&gt;
&lt;li&gt;Metrics are aggregated or derived data that provide insights and summarize the performance (e.g. peak CPU usage, p95 response time).&lt;/li&gt;
&lt;li&gt;Measurements are raw, quantitative data points collected directly during performance tests (e.g. individual response times, CPU usage readings).&lt;/li&gt;
&lt;li&gt;Metrics and measurements should be defined to be meaningful in the context of the requirements/user stories.&lt;/li&gt;
&lt;li&gt;Measurements should be accurate and captured with a level of precision defined by the context.&lt;ul&gt;
&lt;li&gt;Accuracy refers to how close the measurement is to the real value.&lt;/li&gt;
&lt;li&gt;Precision refers to how exact the measurement is (i.e. how close are the measurements to each other).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Typical measurements and metrics are similar to those seen in systems performance analysis.&lt;/li&gt;
&lt;li&gt;Consider the Goal-Question-Metric (GQM) approach to select what metrics to use.&lt;/li&gt;
&lt;li&gt;A deductive approach conducts multiple test iterations with changes to single items.&lt;/li&gt;
&lt;li&gt;A diagnostic approach prefers to capture more information earlier and analyze the results data if there is a problem. &lt;/li&gt;
&lt;li&gt;Avoid the probe effect by considering the precision required and the measurement load on the system.&lt;/li&gt;
&lt;li&gt;Metrics can be categorized depending on the context where they are needed: Technical, Business, and Operational.&lt;/li&gt;
&lt;li&gt;Key sources of metrics: Testing tools, Monitoring tools, and Log analysis tools.&lt;/li&gt;
&lt;li&gt;Consider adding a tolerance to time-related requirements to avoid white or black results.&lt;/li&gt;
&lt;li&gt;Failing to plan is planning to fail... Have a test plan.&lt;/li&gt;
&lt;li&gt;Test planning involves business and technical stakeholders, reviews requirements and user stories, performs a volumetric analysis, and defines the testing environment.&lt;/li&gt;
&lt;li&gt;Performance test cases are created in modular form to be used as the building blocks of larger performance tests.&lt;/li&gt;
&lt;li&gt;Categories of test data: Master data (base data), User-defined data (test input data), Transactional data (created when executing the test).&lt;/li&gt;
&lt;li&gt;Different types of system architectures have different types of performance risks.&lt;/li&gt;
&lt;li&gt;Transactions describe a set of activities from initiation to process completion, to be measured as part of the performance test to identify issues.&lt;/li&gt;
&lt;li&gt;Think time is used to simulate a real user performing actions. The response time plus the think time is the elapsed time.&lt;/li&gt;
&lt;li&gt;Operational profiles describe a user’s interaction with the system. Multiple operational profiles are combined to form a load profile (scenario) to fulfill a test objective.&lt;/li&gt;
&lt;li&gt;A load profile combines operational profiles with groups of virtual users ramping up, running for a duration, and ramping down to replicate production load.&lt;/li&gt;
&lt;li&gt;Consider throughput and concurrency when modeling operational and load profiles.&lt;/li&gt;
&lt;li&gt;System throughput is the amount of transactions  processed in a given time. It defines the load on the system. Concurrency represents parallel sessions consuming resources.&lt;/li&gt;
&lt;li&gt;Performance scripts should simulate the real system user, in the same order and at the same speed.&lt;/li&gt;
&lt;li&gt;Data typically analyzed after a performance test: Status of (virtual) users, Transaction response time, Transactions per second, Transaction failures, Hits (request) per second, Network throughput (amount of data received by users), HTTP responses.&lt;/li&gt;
&lt;li&gt;The goal of analyzing the test results is twofold:&lt;ul&gt;
&lt;li&gt;Develop cause-effect relationships chains to uncover root causes of performance issues.&lt;/li&gt;
&lt;li&gt;Prove the SUT has achieved the stated performance requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In performance testing, minimum outliers are better removed as they were probably gathered when the load was still low.&lt;/li&gt;
&lt;li&gt;Time, relative or absolute, is the easiest way to correlate metrics.&lt;/li&gt;
&lt;/ul&gt;</content><category term="posts"></category><category term="book-review"></category><category term="performance"></category><category term="testing"></category></entry><entry><title>Certbot webroot auth with Ansible</title><link href="https://davidalfonso.es/posts/certbot-webroot-auth-with-ansible" rel="alternate"></link><published>2024-03-26T00:00:00+01:00</published><updated>2024-03-26T00:00:00+01:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2024-03-26:/posts/certbot-webroot-auth-with-ansible</id><summary type="html">&lt;p&gt;How to make Certbot, Apache, and Ansible, work together to use webroot authentication.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://certbot.eff.org/"&gt;Certbot&lt;/a&gt; is the recommended software to generate &lt;a href="https://letsencrypt.org/getting-started/"&gt;Let's Encrypt certificates&lt;/a&gt; and prove you control the domain. It does so by implementing the &lt;a href="https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment"&gt;ACME protocol&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html#webroot"&gt;webroot&lt;/a&gt; &lt;em&gt;authenticator plugin&lt;/em&gt; allows to renew certificates without stopping the web server. It works by writing a &lt;em&gt;challenge file&lt;/em&gt; to the domain's document root path that will be requested by the Let's Encrypt validation service. This file will live in a hidden folder named &lt;code&gt;.well-known/acme-challenge/&lt;/code&gt; and will be accessed through the standard HTTP port.&lt;/p&gt;
&lt;h2&gt;Apache configuration&lt;/h2&gt;
&lt;p&gt;Let's start from the end by making sure that Apache will serve files in the expected path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;VirtualHost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;*:80&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# Avoid redirecting to HTTPS if it&amp;#39;s an acme challenge request&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;RewriteEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;On&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;RewriteCond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;%{REQUEST_URI}&lt;span class="w"&gt; &lt;/span&gt;!^/\.well\-known/acme\-challenge/
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;RewriteRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;^(.*)$&lt;span class="w"&gt; &lt;/span&gt;https://%{HTTP_HOST}$1&lt;span class="w"&gt; &lt;/span&gt;[R=301,L]
&lt;span class="nt"&gt;&amp;lt;/VirtualHost&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We now face a chicken-egg problem where we need a web server in the authentication process but Apache will fail to start if a referenced file does not exist.&lt;/p&gt;
&lt;p&gt;A solution is to add a conditional clause to the HTTPS virtual host so that it's only active when the certificate exists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;IfFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/letsencrypt/live/your-domain.com/fullchain.pem&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;VirtualHost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;*:443&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;SSLCertificateFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sx"&gt;/etc/letsencrypt/live/your-domain.com/fullchain.pem&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/VirtualHost&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/IfFile&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This allows to start Apache serving only on the HTTP port, but we'll need to reload the configuration after the certificate is generated.&lt;/p&gt;
&lt;h2&gt;Ansible orchestration&lt;/h2&gt;
&lt;p&gt;Let's assume we're managing multiple domains and have these variables defined:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;apache_cert_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/etc/letsencrypt/live&lt;/span&gt;
&lt;span class="nt"&gt;apache_sites&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;domain1.com&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# other info for domain1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;domain2.com&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# other info for domain2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;We need to first install and start Apache with the appropriate configuration. We could use the &lt;a href="https://github.com/geerlingguy/ansible-role-apache"&gt;geerlingguy apache role&lt;/a&gt; for that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Use geerlingguy apache role&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.include_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;geerlingguy.apache&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# our config...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we're using a firewall like UFW, we have to open the ports, but note that it won't restart and apply the new config &lt;em&gt;until the end of the play&lt;/em&gt;:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;UFW - Allow HTTP/HTTPS connections&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;community.general.ufw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;allow&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Apache Full&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Restart ufw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Now, before setting up certbot, let's check if there are any certificates missing and, if that's the case, force a UFW restart (reload is not supported) to make sure the HTTP port will be opened for the authentication process.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Certbot - Check if certificates exist&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.stat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apache_cert_path&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item.hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/fullchain.pem&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apache_sites&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;register&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;certbot_certs_stat&lt;/span&gt;

&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Certbot - Force UFW restart&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ufw&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;restarted&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;certbot_certs_stat.results | rejectattr(&amp;#39;stat&amp;#39;, &amp;#39;defined&amp;#39;) != []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, using &lt;a href="https://github.com/geerlingguy/ansible-role-certbot"&gt;geerlingguy certbot role&lt;/a&gt;, create the certificates and reload the Apache config, if necessary.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Certbot - Include certbot role&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.include_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;geerlingguy.certbot&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;certbot_create_method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;webroot&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;certbot_create_if_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# other settings...&lt;/span&gt;

&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Certbot - Force Apache config reload after creating new certificates&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;apache2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;reloaded&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;certbot_certs_stat.results | rejectattr(&amp;#39;stat&amp;#39;, &amp;#39;defined&amp;#39;) != []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="posts"></category><category term="ansible"></category><category term="apache"></category><category term="certbot"></category></entry><entry><title>Discourse dev setup instructions</title><link href="https://davidalfonso.es/posts/discourse-dev-setup-instructions" rel="alternate"></link><published>2024-02-04T00:00:00+01:00</published><updated>2024-02-04T00:00:00+01:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2024-02-04:/posts/discourse-dev-setup-instructions</id><summary type="html">&lt;p&gt;Additional steps I had to take to setup a development environment for Discourse.&lt;/p&gt;</summary><content type="html">&lt;p&gt;The &lt;a href="https://github.com/discourse/discourse/blob/ac53e5a9622bd118fffa37684c5b0f894e97b943/docs/DEVELOPER-ADVANCED.md"&gt;installation steps&lt;/a&gt; for developing &lt;a href="https://discourse.org"&gt;Discourse&lt;/a&gt; aren't updated and don't match the &lt;a href="https://github.com/discourse/discourse/blob/ac53e5a9622bd118fffa37684c5b0f894e97b943/docs/INSTALL.md"&gt;minimal requirements&lt;/a&gt;. Most are figured out easily, but some require some lookup.&lt;/p&gt;
&lt;h2&gt;Redis&lt;/h2&gt;
&lt;p&gt;For example, &lt;strong&gt;Redis&lt;/strong&gt; uses Chris Lea's ppa, which doesn't contain packages for &lt;strong&gt;Ubuntu 22.04 (jammy)&lt;/strong&gt;. If you had already added it, remove it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;add-apt-repository&lt;span class="w"&gt; &lt;/span&gt;--remove&lt;span class="w"&gt; &lt;/span&gt;ppa:chris-lea/redis-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and add the &lt;a href="https://redis.io/docs/install/install-redis/install-redis-on-linux/"&gt;official Redis Apt repo&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-fsSL&lt;span class="w"&gt; &lt;/span&gt;https://packages.redis.io/gpg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--dearmor&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;/usr/share/keyrings/redis-archive-keyring.gpg
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release&lt;span class="w"&gt; &lt;/span&gt;-cs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/sources.list.d/redis.list
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This repository contains the most recent stable versions of Redis, which can be listed with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-cache&lt;span class="w"&gt; &lt;/span&gt;policy&lt;span class="w"&gt; &lt;/span&gt;redis
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And installed with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is the latest 7.2.x available, which is supposedly&lt;/span&gt;
&lt;span class="c1"&gt;# compatible with Discourse.&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;:7.2.4-1rl1~jammy1

&lt;span class="c1"&gt;# Enable it&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;redis-server
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;redis-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Miscelanea&lt;/h2&gt;
&lt;p&gt;There's also &lt;strong&gt;oxipng&lt;/strong&gt;, which can be easily installed using &lt;a href="https://doc.rust-lang.org/cargo/"&gt;Rust's cargo&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cargo&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;oxipng@8.0.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once inside the &lt;code&gt;discourse&lt;/code&gt; source folder, there are some more things to do beyond those described in the instructions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the PostgreSQL client libraries: &lt;code&gt;sudo apt install libpq-dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use a Node.js version that's compatible, e.g. 18.19: &lt;code&gt;echo "18.19" &amp;gt; .nvmrc &amp;amp;&amp;amp; nvm use&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Enable &lt;a href="https://yarnpkg.com/corepack"&gt;corepack&lt;/a&gt;, which is the preferred way to install yarn: &lt;code&gt;corepack enable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install the js dependencies: &lt;code&gt;yarn install&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we can run all those &lt;code&gt;bundle&lt;/code&gt; commands without errors.&lt;/p&gt;
&lt;h2&gt;Mailpit&lt;/h2&gt;
&lt;p&gt;Finally, because MailHog seems to be abandoned, I went for &lt;a href="https://github.com/axllent/mailpit"&gt;Mailpit&lt;/a&gt; and installed it with Docker, using some specific options to avoid dealing with authentication in a local environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--restart&lt;span class="w"&gt; &lt;/span&gt;unless-stopped&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="o"&gt;=&lt;/span&gt;mailpit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/var/mailpit:/data&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MP_DATA_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/data/mailpit.db&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MP_SMTP_AUTH_ACCEPT_ANY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MP_SMTP_AUTH_ALLOW_INSECURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8025&lt;/span&gt;:8025&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1025&lt;/span&gt;:1025&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;axllent/mailpit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="posts"></category><category term="discourse"></category><category term="ubuntu"></category></entry><entry><title>Removing all my tweets from X/Twitter</title><link href="https://davidalfonso.es/posts/removing-all-my-tweets-from-twitter" rel="alternate"></link><published>2023-11-06T00:00:00+01:00</published><updated>2023-11-06T00:00:00+01:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2023-11-06:/posts/removing-all-my-tweets-from-twitter</id><summary type="html">&lt;p&gt;How I batch deleted all my tweets using the free API tier.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I want to remove all my tweets. It's been too many years since I've used Twitter, either as a reader or as a writer, and it's about time to leave for good &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;. This post explains the steps I've done to achieve it.&lt;/p&gt;
&lt;h2&gt;The API is useless, long live the API&lt;/h2&gt;
&lt;p&gt;The &lt;a href="API tiers: https://developer.twitter.com/en/portal/products/free"&gt;free Twitter API tier&lt;/a&gt; has been super restrictive &lt;a href="https://twitter.com/XDevelopers/status/1641222786894135296"&gt;since a few months ago&lt;/a&gt;. Some developers (who don't make money with it) have started &lt;a href="https://github.com/lemon24/reader/issues/310"&gt;dropping support&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;OK, but what about my use case? It turns out I can still &lt;em&gt;delete&lt;/em&gt; (and &lt;em&gt;post&lt;/em&gt;) tweets! But wait, there are some caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rate-limit&lt;/strong&gt;: 50 api requests/day, which is a bit too scarce.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unknown tweet IDs&lt;/strong&gt;: No free endpoint available to list your own tweets and get &lt;a href="https://developer.twitter.com/en/docs/twitter-ids"&gt;their ID&lt;/a&gt;, which is needed to delete them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first issue is a matter of abiding by the limits and deleting just 50 tweets per day. I can live with that, but if you have tweets in the order of thousands, you will need some patience. For example, for 10.000 tweets, you'd need 200 days to delete them all :O Maybe you'd be better off deleting your account.&lt;/p&gt;
&lt;p&gt;To bypass the second issue, I downloaded a &lt;a href="https://twitter.com/settings/download_your_data"&gt;complete account backup&lt;/a&gt; file that contains all my tweets as a JavaScript dictionary, including their IDs.&lt;/p&gt;
&lt;h2&gt;Create a new app in the Twitter Developer Portal&lt;/h2&gt;
&lt;p&gt;To be able to authenticate yourself in the Twitter API, you need to have a &lt;strong&gt;project app&lt;/strong&gt; created in the &lt;a href="https://developer.twitter.com/en/portal/dashboard"&gt;Developer Portal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The first time I filled out the request, I never got a response (was it denied? I don't know). The second time, I tried to explain the intent of the app with more detail. Somehow, it quickly got accepted.&lt;/p&gt;
&lt;p&gt;Once I had the app, I needed to generate the following keys and tokens required for &lt;a href="https://oauth.net/1/"&gt;OAuth 1&lt;/a&gt; authentication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Consumer keys&lt;/strong&gt;: These identify your app so that the API knows what app is doing the request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access token and secret&lt;/strong&gt;: These are user-specific credentials, which is perfect because we want to delete our tweets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Delete 50 tweets per day&lt;/h2&gt;
&lt;p&gt;I implemented a &lt;a href="https://github.com/davidag/scripts/blob/master/twitter-delete-all.py"&gt;Python script&lt;/a&gt; that issues a &lt;code&gt;DELETE&lt;/code&gt; method request to the &lt;code&gt;/2/tweets&lt;/code&gt; API endpoint up to 50 times, using the backup file as the source of Tweet IDs.&lt;/p&gt;
&lt;p&gt;The script has the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It receives one command-line argument: the path to the Twitter backup (unzipped).&lt;/li&gt;
&lt;li&gt;It reads credentials from environment variables.&lt;/li&gt;
&lt;li&gt;Tweets are deleted sequentially in the order they appear in &lt;code&gt;data/tweets.js&lt;/code&gt; inside the backup.&lt;/li&gt;
&lt;li&gt;The number of deleted tweets is stored in &lt;code&gt;~/.twitter-delete-all-start-position&lt;/code&gt; to continue where it left it.&lt;/li&gt;
&lt;li&gt;Use the position file as a marker filer to wait &lt;em&gt;at least&lt;/em&gt; 24 hours after its last modification time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Running the script daily&lt;/h2&gt;
&lt;p&gt;Given the features and requirements of the script, I want it to run whenever I turn on my computer. Because the script won't call the API unless at least 24 hours have passed since the last time, we can run it multiple times each day without hitting the rate limit.&lt;/p&gt;
&lt;p&gt;I'm using Ubuntu 22.04, so I'll go with &lt;a href="https://wiki.archlinux.org/title/Systemd/User"&gt;Systemd&lt;/a&gt; as the mechanism to run the script.&lt;/p&gt;
&lt;p&gt;First, create the user service file &lt;code&gt;~/.config/systemd/user/twitter-delete.service&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[Service]
Type=oneshot
RemainAfterExit=true
StandardOutput=journal

# Define DISPLAY for desktop notifications and remember to provide
# all the tokens (you might want to use a more secure alternative
# if you have the need for it)
Environment=DISPLAY=&amp;#39;:0&amp;#39; CONSUMER_KEY= CONSUMER_SECRET= ACCESS_TOKEN= ACCESS_TOKEN_SECRET=

# I use pyenv to manage my virtual envs and have one named `scripts`
# just for my scripts.
ExecStart=%h/.pyenv/versions/scripts/bin/python3 %h/scripts/twitter-delete-all.py %h/backups/twitter-2023-10-18.zip

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, enable the service and restart the computer to make sure it's working:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;twitter-delete.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, every time my computer is on, the script will run in the background, sleep if necessary, and delete up to 50 tweets of my account.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Well, I'm not going to completely delete my account (for now) as it would be quickly taken by someone that could impersonate me or just be confusing to people that knows me.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;It is known that &lt;a href="https://www.theguardian.com/technology/2023/nov/05/elon-musk-unveils-grok-an-ai-chatbot-with-a-rebellious-streak?trk=public_post_comment-text"&gt;xAI uses all tweets in Twitter to train its new chatbot&lt;/a&gt; and when a tweet is deleted it's actually &lt;em&gt;soft deleted&lt;/em&gt;, so it remains in Twitter servers. In fact, they are part of the backup archive. In any case, could deleting my Twitter account stop xAI from using my tweets? Most certainly no.&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="posts"></category><category term="python"></category><category term="scripts"></category></entry><entry><title>E-mail privacy in GitHub</title><link href="https://davidalfonso.es/posts/make-your-e-mail-private-in-github-repos" rel="alternate"></link><published>2023-10-17T00:00:00+02:00</published><updated>2023-10-17T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2023-10-17:/posts/make-your-e-mail-private-in-github-repos</id><summary type="html">&lt;p&gt;How to make your e-mail private in GitHub repositories.&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Future commits&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Check both &lt;code&gt;Keep e-mail private&lt;/code&gt; and &lt;code&gt;Block command-line pushes&lt;/code&gt; options in &lt;a href="https://github.com/settings/emails"&gt;your GitHub e-mail settings&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set your &lt;a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address"&gt;GitHub private e-mail address&lt;/a&gt; globally of per-project:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# current project&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;user.email&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;username@users.noreply.github.com&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# global&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;--global&lt;span class="w"&gt; &lt;/span&gt;user.email&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;username@users.noreply.github.com&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Past commits&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;WARNING&lt;/em&gt;&lt;/strong&gt;: These steps rewrite your repository history. &lt;a href="https://www.mankier.com/1/git-filter-repo#Discussion"&gt;Read about collaboration and rewriting history&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Last commit&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;--amend&lt;span class="w"&gt; &lt;/span&gt;--reset-author
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Bulk edition&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href="https://github.com/newren/git-filter-repo"&gt;&lt;code&gt;git-filter-repo&lt;/code&gt;&lt;/a&gt; which is the official &lt;code&gt;git filter-branch&lt;/code&gt; replacement.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Clone repo (bare or not)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;--bare&lt;span class="w"&gt; &lt;/span&gt;https://hostname/user/repo.git
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;repo.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Update e-mail and/or name from all commits matching your &lt;code&gt;old@email.com&lt;/code&gt; note the usage of &lt;code&gt;bytes&lt;/code&gt;) &lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# update e-mail only&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git-filter-repo&lt;span class="w"&gt; &lt;/span&gt;--email-callback&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;return email if email != b&amp;quot;old@email.com&amp;quot; else b&amp;quot;username@users.noreply.github.com&amp;quot;&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;# update both e-mail and name&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git-filter-repo&lt;span class="w"&gt; &lt;/span&gt;--name-callback&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;return b&amp;quot;Your Name&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--email-callback&lt;span class="w"&gt; &lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Re-add remote repository (&lt;code&gt;git-filter-repo&lt;/code&gt; deletes remotes as a safety measure to avoid unintended history rewrites!)&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;remote-url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Force push changes (!)&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;--all
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/897586/does-git-publicly-expose-my-e-mail-address"&gt;Stackoverflow - Keep your e-mail address private&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository"&gt;GitHub Docs - Removing sensitive data using git-filter-repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="posts"></category><category term="git"></category><category term="github"></category></entry><entry><title>Updating my Neovim config from upstream</title><link href="https://davidalfonso.es/posts/updating-neovim-config-from-upstream" rel="alternate"></link><published>2023-09-27T00:00:00+02:00</published><updated>2023-09-27T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2023-09-27:/posts/updating-neovim-config-from-upstream</id><summary type="html">&lt;p&gt;How to pull changes from the upstream Neovim configuration repository.&lt;/p&gt;</summary><content type="html">&lt;p&gt;When &lt;a href="https://davidalfonso.es/posts/using-a-lua-based-neovim-config"&gt;I picked a reference Neovim config&lt;/a&gt;, one of the main criteria was that it should be updated regularly. This note explains how to bring updates from the upstream repository (i.e. &lt;code&gt;LunarVim/Launch.nvim&lt;/code&gt;) to my own config repo.&lt;/p&gt;
&lt;h2&gt;Bringing changes from upstream to main branch&lt;/h2&gt;
&lt;p&gt;Creating the repo from a GitHub template only makes it reference the original repo but it's not &lt;em&gt;a real fork&lt;/em&gt; and commits are completely unrelated. This means that I can't use any GitHub feature to deal with forked repositories and the process will be manual.&lt;/p&gt;
&lt;p&gt;Let's see it step by step.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add upstream as a remote to the local clone, if not done already:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;upstream&lt;span class="w"&gt; &lt;/span&gt;https://github.com/LunarVim/Launch.nvim
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Fetch changes from upstream&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;--all
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Merge remote &lt;code&gt;master&lt;/code&gt; branch with local &lt;code&gt;master&lt;/code&gt; branch&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;master

&lt;span class="c1"&gt;# The first time you merge from upstream the unrelated histories flag is&lt;/span&gt;
&lt;span class="c1"&gt;# required&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;merge&lt;span class="w"&gt; &lt;/span&gt;upstream/master&lt;span class="w"&gt; &lt;/span&gt;--allow-unrelated-histories

&lt;span class="c1"&gt;# After the first time, we can just merge&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;merge&lt;span class="w"&gt; &lt;/span&gt;upstream/master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Rebase my &lt;code&gt;mine&lt;/code&gt; branch (the one which contains my changes) with &lt;code&gt;master&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Note: This could be a merge but I don't want to preserve the commits, as I'm the sole user of this repo.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;mine

$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rebase&lt;span class="w"&gt; &lt;/span&gt;master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Push changes to remote &lt;code&gt;origin&lt;/code&gt; repo&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Force should not be required&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;master

&lt;span class="c1"&gt;# We have rebased mine, so let&amp;#39;s force push&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;mine
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Updating the submodule commit in my dotfiles repo&lt;/h2&gt;
&lt;p&gt;Now that my &lt;code&gt;nvim-config&lt;/code&gt; repo is updated, I can do the same with my &lt;code&gt;dotfiles&lt;/code&gt; repo which references the former with a submodule.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cd home, because my &lt;code&gt;dotfiles&lt;/code&gt; are &lt;a href="https://www.unixsheikh.com/tutorials/keeping-your-home-in-git.html"&gt;my home directory&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Pull changes from the just updated remote &lt;code&gt;nvim-config&lt;/code&gt; into the local submodule clone.&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Note: The commit referenced in the &lt;code&gt;mine&lt;/code&gt; branch is the one we want our submodule to point to.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;fetch&lt;span class="w"&gt; &lt;/span&gt;--recurse-submodules&lt;span class="w"&gt; &lt;/span&gt;origin
Fetching&lt;span class="w"&gt; &lt;/span&gt;submodule&lt;span class="w"&gt; &lt;/span&gt;.config/nvim
From&lt;span class="w"&gt; &lt;/span&gt;https://github.com/davidag/nvim-config
&lt;span class="w"&gt;   &lt;/span&gt;ea52394..2d88040&lt;span class="w"&gt;  &lt;/span&gt;master&lt;span class="w"&gt;     &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;origin/master
&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;f9f407d...e51cad0&lt;span class="w"&gt; &lt;/span&gt;mine&lt;span class="w"&gt;       &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;origin/mine&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;forced&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Make the submodule HEAD point to the new commit&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# -C: Cd to the submodule local path before running the Git command&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;.config/nvim&lt;span class="w"&gt; &lt;/span&gt;reset&lt;span class="w"&gt; &lt;/span&gt;origin/mine
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Push submodule change&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Only a file is modified when updating a submodule&lt;/span&gt;
&lt;span class="c1"&gt;# Verify that the new commit is the one shown in the step above&lt;/span&gt;
&lt;span class="c1"&gt;# (!) If the hash ends in -dirty, you need to enter ~/.config/nvim and clean the repo&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;diff
diff&lt;span class="w"&gt; &lt;/span&gt;--git&lt;span class="w"&gt; &lt;/span&gt;a/.config/nvim&lt;span class="w"&gt; &lt;/span&gt;b/.config/nvim
index&lt;span class="w"&gt; &lt;/span&gt;f9f407d..e51cad0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;160000&lt;/span&gt;
---&lt;span class="w"&gt; &lt;/span&gt;a/.config/nvim
+++&lt;span class="w"&gt; &lt;/span&gt;b/.config/nvim
@@&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="w"&gt; &lt;/span&gt;+1&lt;span class="w"&gt; &lt;/span&gt;@@
-Subproject&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;f9f407df596755ad6e03a63e83d6d8ca5c58dcc2
+Subproject&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;e51cad0f5dffef66a4a401a1558712f67884c33d

&lt;span class="c1"&gt;# This force is required because my `.gitignore` contains `*` (this is not&lt;/span&gt;
&lt;span class="c1"&gt;# related to the process described here)&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;.config/nvim

&lt;span class="c1"&gt;# Commit the change&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;commit

&lt;span class="c1"&gt;# Update origin&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;origin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;This setup has several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upstream changes are brought into the &lt;code&gt;nvim-config master&lt;/code&gt; branch without conflicts&lt;/li&gt;
&lt;li&gt;My modifications to the original config live in a different branch, &lt;code&gt;mine&lt;/code&gt;, and are &lt;a href="https://github.com/davidag/nvim-config/compare/master..mine"&gt;easily viewable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;I can work on my &lt;code&gt;nvim-config&lt;/code&gt; directly in the submodule and quickly try new config options&lt;/li&gt;
&lt;li&gt;Multiple commits in my neovim config can be translated into one commit in the &lt;code&gt;dotfiles&lt;/code&gt; repo, leading to different Git history granularities&lt;/li&gt;
&lt;/ul&gt;</content><category term="posts"></category><category term="neovim"></category><category term="git"></category></entry><entry><title>Using a Lua-based Neovim config</title><link href="https://davidalfonso.es/posts/using-a-lua-based-neovim-config" rel="alternate"></link><published>2023-08-14T00:00:00+02:00</published><updated>2023-08-14T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2023-08-14:/posts/using-a-lua-based-neovim-config</id><summary type="html">&lt;p&gt;Taking another step in my Neovim journey by using Lua-based configuration.&lt;/p&gt;</summary><content type="html">&lt;p&gt;When I migrated to Neovim some time ago, I took the easy path of just using my existing vim configuration (in VimL). It was fine and it worked. But if you want to experiment all the benefits of Neovim, you have to go Lua.&lt;/p&gt;
&lt;h2&gt;Choosing a base configuration&lt;/h2&gt;
&lt;p&gt;These are the Lua-based configurations that I considered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/LunarVim/Launch.nvim"&gt;LunarVim/Launch.nvim&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1k stars, uses lazy.nvim instead of packer, that's the newest trend, updated&lt;/li&gt;
&lt;li&gt;requires installing multiple tools to work&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/tokiory/neovim-boilerplate"&gt;tokiory/neovim-boilerplate&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;simpler than lunarvim's, uses less plugins&lt;/li&gt;
&lt;li&gt;both use lazy.nvim to manage plugins&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/frans-johansson/lazy-nvim-starter"&gt;frans-johansson/lazy-nvim-starter&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;single-file but with examples to move to multiple files&lt;/li&gt;
&lt;li&gt;documented, modular, small&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I decided to use &lt;strong&gt;LunarVim's Launch.nvim&lt;/strong&gt; as it seemed the most powerful, clean and updated.&lt;/p&gt;
&lt;h2&gt;Learning some Lua&lt;/h2&gt;
&lt;p&gt;Lua is well known for its simplicity and power. It's also designed to be embedded in applications which make it the perfect option to support user plugins and customizations.&lt;/p&gt;
&lt;p&gt;Besides &lt;a href="https://www.lua.org/docs.html"&gt;the official docs&lt;/a&gt;, just read the &lt;a href="https://learnxinyminutes.com/docs/lua/"&gt;learnxinyminutes quick tutorial&lt;/a&gt; and have the Neovim references at hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neovim.io/doc/user/lua.html"&gt;Lua in Neovim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neovim.io/doc/user/lua-guide.html"&gt;Lua guide to config Neovim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Creating my config from the template&lt;/h2&gt;
&lt;p&gt;I decided to keep my Neovim config in a separate repo from &lt;a href="https://github.com/davidag/dotfiles"&gt;my dotfiles&lt;/a&gt;, as I want to keep it in sync with upstream and have more freedom to branch and test new things.&lt;/p&gt;
&lt;p&gt;GitHub uses the concept of &lt;strong&gt;templates&lt;/strong&gt; to allow a "soft" clone where you don't inherit all the Git history. Instead, a new repo with the latest state of the template contents is created &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The new repo still points to the original stating:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;generated from LunarVim/Launch.nvim&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My config repo has a &lt;code&gt;master&lt;/code&gt; branch that mirrors upstream and a new &lt;code&gt;mine&lt;/code&gt; branch to keep my customizations.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;-b&lt;span class="w"&gt; &lt;/span&gt;mine

$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;mine
To&lt;span class="w"&gt; &lt;/span&gt;github.com:davidag/nvim-config.git
&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;new&lt;span class="w"&gt; &lt;/span&gt;branch&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;mine&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;mine
Branch&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mine&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;track&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;branch&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mine&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;origin&amp;#39;&lt;/span&gt;.

$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;branch&lt;span class="w"&gt; &lt;/span&gt;-vv
&lt;span class="w"&gt;  &lt;/span&gt;master&lt;span class="w"&gt; &lt;/span&gt;ea52394&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;origin/master&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Initial&lt;span class="w"&gt; &lt;/span&gt;commit
*&lt;span class="w"&gt; &lt;/span&gt;mine&lt;span class="w"&gt;   &lt;/span&gt;ea52394&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;origin/mine&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Initial&lt;span class="w"&gt; &lt;/span&gt;commit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Referencing Neovim's config from my dotfiles repository&lt;/h2&gt;
&lt;p&gt;Since my dotfiles repo no longer contains my Neovim's config, there must be some mechanism in place to download it in the proper directory. This is a good use case for &lt;a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules"&gt;Git submodules&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# remove previous contents&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;.config/nvim

&lt;span class="c1"&gt;# add the submodule&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;submodule&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;-b&lt;span class="w"&gt; &lt;/span&gt;mine&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;https://github.com/davidag/nvim-config&lt;span class="w"&gt; &lt;/span&gt;.config/nvim
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We don't want absolute paths in &lt;code&gt;.gitmodules&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[submodule &amp;quot;nvim&amp;quot;]
  path = .config/nvim
  url = https://github.com/davidag/nvim-config
  branch = mine
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
Continue setting up the local repo (unnecessary if you clone my dotfiles with &lt;code&gt;--recurse-submodules&lt;/code&gt;):
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;submodule&lt;span class="w"&gt; &lt;/span&gt;init
Submodule&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nvim&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;https://github.com/davidag/nvim-config&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;registered&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.config/nvim&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
When there are changes in my Neovim's config repo, I can update to these changes (since Git 1.8) with:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# This will update the commit referenced in `~/.config/nvim`&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;submodule&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;--remote&lt;span class="w"&gt; &lt;/span&gt;--merge

&lt;span class="c1"&gt;# Commit the new commit reference&lt;/span&gt;
~&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;commit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;The commit that removes my old vim config and adds a submodule with the new Lua-based Neovim config can be viewed &lt;a href="https://github.com/davidag/dotfiles/commit/4f2642f60ec40bd0cdc4c644fd07c21a369b7d84"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template"&gt;GitHub docs: Creating a repository from a template&lt;/a&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="posts"></category><category term="neovim"></category><category term="lua"></category></entry><entry><title>Signing PDF documents</title><link href="https://davidalfonso.es/posts/sign-pdf-document" rel="alternate"></link><published>2023-07-29T00:00:00+02:00</published><updated>2023-07-29T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2023-07-29:/posts/sign-pdf-document</id><summary type="html">&lt;p&gt;How to "sign" a PDF document using an image (no cryptography involved)&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have to "sign" a pdf document with my signature, which it's in a png file.&lt;/p&gt;
&lt;p&gt;Let's use &lt;a href="https://github.com/benwinding/pdfstamp"&gt;pdfstamp&lt;/a&gt;, which depends on &lt;a href="https://imagemagick.org/index.php"&gt;ImageMagick&lt;/a&gt; and &lt;a href="https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/"&gt;the PDF Toolkit&lt;/a&gt; (&lt;code&gt;pdftk&lt;/code&gt;).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# install dependencies if needed&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;imagemagick&lt;span class="w"&gt; &lt;/span&gt;pdftk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;pdfstamp is available as a node package publised to the npm registry.&lt;/p&gt;
&lt;p&gt;Assuming you have a npm/npx working environment (I use &lt;a href="https://github.com/nvm-sh/nvm"&gt;nvm&lt;/a&gt; to manage node versions):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;## install globally with --global/-g&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;npm&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pdfstamp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pdfstamp&lt;span class="w"&gt; &lt;/span&gt;doctor
running&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;which&lt;span class="w"&gt; &lt;/span&gt;convert
running&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;which&lt;span class="w"&gt; &lt;/span&gt;pdftk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Sign the pdf:&lt;/p&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pdfstamp&lt;span class="w"&gt; &lt;/span&gt;stamp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--input&lt;span class="w"&gt; &lt;/span&gt;./file.pdf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--signature&lt;span class="w"&gt; &lt;/span&gt;./signature.png&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--output&lt;span class="w"&gt; &lt;/span&gt;./signed.pdf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--page&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--zoom&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--left&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;95&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--bottom&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
If you get an error that states: "attempt to perform an operation not allowed by the security policy", it's because of a &lt;a href="https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion"&gt;security vulnerability in Ghostscript&lt;/a&gt;, just comment the following line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/\(&amp;lt;policy domain=&amp;quot;coder&amp;quot; rights=&amp;quot;none&amp;quot; pattern=&amp;quot;PDF&amp;quot; \/&amp;gt;\)/&amp;lt;!-- \1 --&amp;gt;/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/ImageMagick-6/policy.xml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="posts"></category><category term="command-line"></category></entry><entry><title>Debugging PHP in a Docker container</title><link href="https://davidalfonso.es/posts/debugging-php-in-docker-container" rel="alternate"></link><published>2022-09-03T00:00:00+02:00</published><updated>2022-09-03T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2022-09-03:/posts/debugging-php-in-docker-container</id><summary type="html">&lt;p&gt;How to debug PHP code running in a Docker container using xdebug and VS Code.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm migrating a local development setup from Vagrant to Docker and I've had to setup debugging to deal with some problems.&lt;/p&gt;
&lt;p&gt;I've used &lt;a href="https://xdebug.org/"&gt;xdebug&lt;/a&gt;, PHP's debugger, which &lt;a href="https://phptherightway.com/#xdebug"&gt;supports remote debugging&lt;/a&gt;. This is needed because VS Code runs in the host machine and PHP in a Docker container.&lt;/p&gt;
&lt;h2&gt;Build Docker container with xdebug support&lt;/h2&gt;
&lt;p&gt;Note: I'm assuming a repository with a &lt;code&gt;docker/&lt;/code&gt; subfolder where all the Docker-related files reside.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install and enable &lt;code&gt;xdebug&lt;/code&gt; in the &lt;code&gt;docker/Dockerfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# Assuming the official php docker image&lt;/span&gt;
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
#&lt;span class="w"&gt; &lt;/span&gt;...
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pecl&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;xdebug-3.1.5&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker-php-ext-enable&lt;span class="w"&gt; &lt;/span&gt;xdebug&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
#&lt;span class="w"&gt; &lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure &lt;code&gt;xdebug&lt;/code&gt; in a file &lt;code&gt;docker/conf.d/xdebug.ini&lt;/code&gt; that will be mapped inside the container
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="na"&gt;zend_extension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xdebug&lt;/span&gt;

&lt;span class="k"&gt;[xdebug]&lt;/span&gt;
&lt;span class="na"&gt;xdebug.mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;develop,debug&lt;/span&gt;
&lt;span class="na"&gt;xdebug.client_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;host.docker.internal&lt;/span&gt;
&lt;span class="na"&gt;xdebug.start_with_request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add alias for docker host in &lt;code&gt;docker-compose.yaml&lt;/code&gt; because &lt;a href="https://github.com/docker/for-linux/issues/264#issuecomment-964620100"&gt;I'm on Linux&lt;/a&gt;.
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;services:
  web:
    extra_hosts:
      - host.docker.internal:host-gateway
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Configure VS Code&lt;/h2&gt;
&lt;ol start="4"&gt;
&lt;li&gt;
&lt;p&gt;Install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug"&gt;official PHP Debug extension&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;launch.json&lt;/code&gt; with the default PHP config and include a path mapping from server to host paths:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; &amp;quot;configurations&amp;quot;: [
  {
   &amp;quot;name&amp;quot;: &amp;quot;Listen for Xdebug&amp;quot;,
   &amp;quot;type&amp;quot;: &amp;quot;php&amp;quot;,
   &amp;quot;request&amp;quot;: &amp;quot;launch&amp;quot;,
   &amp;quot;pathMappings&amp;quot;: {
    &amp;quot;/var/www/html&amp;quot;: &amp;quot;/home/username/project/html&amp;quot;
   },
   &amp;quot;port&amp;quot;: 9003
  },
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Start debugging with "Listen for Xdebug" and set some breakpoint.&lt;/li&gt;
&lt;li&gt;Open the container url where Apache is listening and wait for the breakpoint to hit.&lt;/li&gt;
&lt;/ol&gt;</content><category term="posts"></category><category term="php"></category><category term="docker"></category></entry><entry><title>Default resource values in the Python CDK</title><link href="https://davidalfonso.es/posts/default-resource-values-in-the-python-cdk" rel="alternate"></link><published>2021-06-02T00:00:00+02:00</published><updated>2021-06-02T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2021-06-02:/posts/default-resource-values-in-the-python-cdk</id><summary type="html">&lt;p&gt;Finding CDK default values using two different methods.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Imagine that we want find out what is the default subnet configuration created when instantiating a VPC like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;aws_lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aws_ec2&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Stack&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# there is an optional `subnet_configuration` parameter&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;MyVpc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_azs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Method 1: Synthetizing&lt;/h2&gt;
&lt;p&gt;There is one straightforward way to know what resources will be created with a stack: &lt;strong&gt;synthetizing it&lt;/strong&gt;. This means creating the CloudFormation template that would be provisioned if this stack was ever deployed.&lt;/p&gt;
&lt;p&gt;You can do this by executing &lt;code&gt;cdk synth&lt;/code&gt; and greping for &lt;code&gt;MyVpc&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;cdk&lt;span class="w"&gt; &lt;/span&gt;synth&lt;span class="w"&gt; &lt;/span&gt;--json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;jq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.Resources | map_values(.Type)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;MyVpc&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcF9F0CA6F&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::VPC&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcIGW5C4A4F63&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::InternetGateway&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet1DefaultRouteA8CDE2FA&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Route&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet1RouteTable8819E6E2&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::RouteTable&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet1RouteTableAssociation56D38C7E&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::SubnetRouteTableAssociation&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet1Subnet5057CF7E&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Subnet&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet2DefaultRoute9CE96294&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Route&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet2RouteTableAssociation86A610DA&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::SubnetRouteTableAssociation&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet2RouteTableCEDCEECE&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::RouteTable&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPrivateSubnet2Subnet0040C983&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Subnet&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1DefaultRoute95FDF9EB&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Route&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1EIP096967CB&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::EIP&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1NATGatewayAD3400C1&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::NatGateway&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::SubnetRouteTableAssociation&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1RouteTableC46AB2F4&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::RouteTable&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet1SubnetF6608456&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Subnet&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2DefaultRoute052936F6&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Route&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2EIP8CCBA239&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::EIP&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2NATGateway91BFBEC9&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::NatGateway&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2RouteTable1DF17386&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::RouteTable&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2RouteTableAssociation227DE78D&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::SubnetRouteTableAssociation&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcPublicSubnet2Subnet492B6BFB&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::Subnet&amp;quot;&lt;/span&gt;,
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;MyVpcVPCGW488ACE0D&amp;quot;&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AWS::EC2::VPCGatewayAttachment&amp;quot;&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case, all associated resources begin with the same name, but in more complex cases it might be better to grep in the raw synthethized output and find out dependent resources.&lt;/p&gt;
&lt;h2&gt;Method 2: Looking at the CDK library resource&lt;/h2&gt;
&lt;p&gt;By synthetizing our stack, we've learnt that two private and two public subnets, along a couple of NAT gateways, would be created. But where is this defined?&lt;/p&gt;
&lt;p&gt;The key is that all supported CDK libraries end up calling the TypeScript CDK library. This is done through the &lt;a href="https://pypi.org/project/jsii/"&gt;jsii&lt;/a&gt; library:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;jsii allows code in any language to naturally interact with JavaScript classes. It is the technology that enables the AWS Cloud Development Kit to deliver polyglot libraries from a single codebase!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Looking at the Python definition of &lt;code&gt;aws_ec2.Vpc&lt;/code&gt; we can see the actual name to look for in the &lt;a href="https://github.com/aws/aws-cdk"&gt;CDK library&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@jsii&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;implements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IVpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_Resource_45bc6135&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metaclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jsii&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSIIMeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jsii_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;aws-cdk-lib.aws_ec2.Vpc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# This is the class in the aws-cdk-lib&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We just have to open the appropriate file. In this case, &lt;code&gt;packages/aws-cdk/aws-ec2/lib/vpc.ts&lt;/code&gt; (&lt;a href="https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-ec2/lib/vpc.ts"&gt;link&lt;/a&gt;) and search for &lt;code&gt;class Vpc&lt;/code&gt;. These are the relevant contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Vpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;extends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;VpcBase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cm"&gt;/**&lt;/span&gt;
&lt;span class="cm"&gt;   * The default subnet configuration&lt;/span&gt;
&lt;span class="cm"&gt;   *&lt;/span&gt;
&lt;span class="cm"&gt;   * 1 Public and 1 Private subnet per AZ evenly split&lt;/span&gt;
&lt;span class="cm"&gt;   */&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;readonly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_SUBNETS&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;SubnetConfiguration&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;subnetType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;SubnetType.PUBLIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;defaultSubnetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;subnetType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;SubnetType.PRIVATE_WITH_NAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;defaultSubnetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SubnetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE_WITH_NAT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cm"&gt;/**&lt;/span&gt;
&lt;span class="cm"&gt;   * Vpc creates a VPC that spans a whole region.&lt;/span&gt;
&lt;span class="cm"&gt;   * It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone.&lt;/span&gt;
&lt;span class="cm"&gt;   * Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway.&lt;/span&gt;
&lt;span class="cm"&gt;   * Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ).&lt;/span&gt;
&lt;span class="cm"&gt;   */&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kr"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;VpcProps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;defaultSubnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;natGateways&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Vpc.DEFAULT_SUBNETS_NO_NAT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Vpc.DEFAULT_SUBNETS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnetConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ifUndefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnetConfiguration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;defaultSubnet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And there it is, the explanation of why two public and two private subnets along two NAT gateways were being defined.&lt;/p&gt;</content><category term="posts"></category><category term="python"></category><category term="aws"></category></entry><entry><title>Forking Watson: License</title><link href="https://davidalfonso.es/posts/forking-watson-license" rel="alternate"></link><published>2020-06-05T00:00:00+02:00</published><updated>2020-06-05T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2020-06-05:/posts/forking-watson-license</id><summary type="html">&lt;p&gt;The process and rationale behind updating a compatible license (MIT/Expat) to the GNU GPL.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;DISCLAIMER&lt;/strong&gt;: I'm not an expert in software licensing by any means, so please don't take this post as legal advice.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://davidalfonso.es/posts/forking-watson-motivations"&gt;As described in a previous post&lt;/a&gt;, I'm doing a fork of Watson, a time tracking command-line tool. You can find in there my motivations for doing a fork instead of other alternatives. The subject of this post is describing why and how I've switched the forked project license from MIT/Expat to GNU GPL&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;, what's the REUSE project and how to apply it to help in the migration process.&lt;/p&gt;
&lt;h2&gt;Why would anyone ever want to modify the license?&lt;/h2&gt;
&lt;p&gt;Both MIT and GPL are &lt;a href="https://opensource.org/licenses"&gt;OSI approved licenses&lt;/a&gt;, meaning both are accepted by the Open Source Initiative as complying with their &lt;a href="https://opensource.org/osd"&gt;definition of Open Source Software&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.gnu.org/licenses/licenses.html#GPL"&gt;GNU General Public License (GNU GPL)&lt;/a&gt; is a license created by the &lt;a href="https://fsf.org/"&gt;Free Software Foundation&lt;/a&gt;, which defines free software using &lt;a href="https://www.gnu.org/philosophy/free-sw.html"&gt;a four-point definition&lt;/a&gt;. You can find arguments in favor and against the OSI and the FSF definitions in their &lt;a href="https://www.gnu.org/philosophy/open-source-misses-the-point.html"&gt;respective&lt;/a&gt; &lt;a href="https://opensource.org/faq#free-software"&gt;sites&lt;/a&gt; and all over the Internet.&lt;/p&gt;
&lt;p&gt;The GPL, in its third revision (GPLv3), &lt;a href="https://www.gnu.org/licenses/quick-guide-gplv3.html"&gt;adds some interesting features&lt;/a&gt;, though not very relevant for my project. What matters most, is that the GPL is &lt;a href="https://www.gnu.org/copyleft/"&gt;a copyleft license&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Copyleft is a general method for making a program (or other work) free (in the sense of freedom, not “zero price”), and requiring all modified and extended versions of the program to be free as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is not the case of &lt;a href="https://tldrlegal.com/license/mit-license"&gt;the MIT/Expat license&lt;/a&gt;, which allows you to distribute the software (and any modifications) under a non-free license, ie. not complying with any or all of the four free software points. This is why it is said that the MIT license is more permissive than the GPL.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/license-recommendations.html"&gt;Choosing one license or another&lt;/a&gt; is a matter of &lt;a href="https://choosealicense.com/"&gt;circumstances, strategy, and convictions&lt;/a&gt;. In a small project like this, &lt;a href="https://www.gnu.org/licenses/why-not-lgpl.en.html"&gt;not distributed as a library&lt;/a&gt;, it's a conviction that takes most of the weight.&lt;/p&gt;
&lt;h2&gt;Switching from MIT to GPL&lt;/h2&gt;
&lt;p&gt;First of all, it's &lt;strong&gt;important&lt;/strong&gt; to note that &lt;em&gt;I'm not changing the license of the original project&lt;/em&gt;. Watson is a different piece of software and is still MIT-licensed. It is the forked software (&lt;em&gt;xtimetracker&lt;/em&gt;) license that is being changed. &lt;a href="https://www.gnu.org/licenses/license-compatibility.en.html"&gt;Relicensing&lt;/a&gt; a software project with many contributors can be &lt;a href="https://github.com/getpelican/pelican/issues/1397"&gt;quite challenging&lt;/a&gt; and is not the subject of this post.&lt;/p&gt;
&lt;p&gt;Second, we can take this upgrade path because the MIT license is &lt;a href="https://www.gnu.org/licenses/gpl-faq.en.html#WhatDoesCompatMean"&gt;GPL-compatible&lt;/a&gt;, meaning we can combine code under the MIT and GPL licenses in one larger program and distribute it as GPL.&lt;/p&gt;
&lt;p&gt;Finally, the original project only stated the license in a &lt;code&gt;LICENSE&lt;/code&gt; file in the project root. However, according to &lt;a href="https://www.gnu.org/licenses/gpl-faq.en.html#LicenseCopyOnly"&gt;the GPL FAQ&lt;/a&gt;, license notices &lt;em&gt;should&lt;/em&gt; be included in all relevant files, &lt;a href="http://softwarefreedom.org/resources/2007/gpl-non-gpl-collaboration.html"&gt;especially when mixing permissive and GPL-licensed files&lt;/a&gt; and, in this case, also for honesty's sake. This means all files should be modified to state copyright and license information.&lt;/p&gt;
&lt;h2&gt;Enter REUSE&lt;/h2&gt;
&lt;p&gt;Applying multiple licenses to the same files is not an easy task, legally speaking; that's the reason I found the REUSE initiative so appealing to the problem at hand.&lt;/p&gt;
&lt;p&gt;REUSE defines a set of recommendations to apply copyright and licensing information to software projects. It was created by the &lt;a href="https://fsfe.org/"&gt;Free Software Foundation Europe (FSFE)&lt;/a&gt; to make licensing clear both to humans and computers. That's the reason &lt;a href="https://github.com/fsfe/reuse-tool"&gt;there is a tool&lt;/a&gt; to automate many tasks involved in applying the REUSE recommendations, including compliance validation according to &lt;a href="https://reuse.software/spec"&gt;the latest REUSE specification&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reality is that &lt;a href="https://github.com/fsfe/reuse-tool/issues/169"&gt;other tools are not yet compatible with the REUSE recommendations&lt;/a&gt; and that the &lt;code&gt;reuse&lt;/code&gt; tool itself is under continuous development to handle the large number of special cases that can arise in software projects. This means you'll probably have to manually handle and verify most of your files (which is recommended anyway).&lt;/p&gt;
&lt;h2&gt;How to declare multiple licenses&lt;/h2&gt;
&lt;p&gt;The current situation is such that most project files have already been modified by me. This means that both MIT and GPL licenses should apply to them. Specifically, the parts that I've coded are licensed under the GPL license, and the parts from the original project fall under the MIT license. However, this distinction is &lt;a href="https://reuse.software/faq/#partial-license"&gt;difficult to state explicitly&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When &lt;a href="https://reuse.software/faq/#multi-licensing"&gt;multi-licenses apply&lt;/a&gt;, each file should list what licenses apply in separate &lt;code&gt;SPDX-License-Identifier&lt;/code&gt; tags (wait, &lt;a href="https://spdx.org/ids"&gt;what's an SPDX identifier?&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In the project properties (e.g. your &lt;code&gt;setup()&lt;/code&gt; function in a Python package), the &lt;code&gt;AND&lt;/code&gt; operator should be used because the project is governed by two different licenses:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# setup.py
license=&amp;quot;GPL-3.0-or-later AND MIT&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
You would use the &lt;code&gt;OR&lt;/code&gt; operator when the user can &lt;em&gt;choose&lt;/em&gt; between them.&lt;/p&gt;
&lt;h2&gt;Using the &lt;code&gt;reuse&lt;/code&gt; tool to include licensing information&lt;/h2&gt;
&lt;p&gt;Let's run &lt;code&gt;reuse&lt;/code&gt; to change all files with &lt;a href="https://github.com/fsfe/reuse-tool/issues/224"&gt;supported comment styles&lt;/a&gt; in bulk. Note that we could apply multiple copyrights and licenses at once, but, because the years are different, we have to run two commands:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# OLD COPYRIGHT
$ git ls-files | xargs reuse addheader --skip-unrecognised \
    --copyright=&amp;quot;Tailordev&amp;quot; --license=MIT --year=2015-2019

# NEW COPYRIGHT
$ git ls-files | xargs reuse addheader --skip-unrecognised \
    --copyright=&amp;quot;David Alfonso&amp;quot; --license=GPL-3.0-or-later --year=2020
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Now we can list the files missing licensing information and deal with each of them on a case-by-case basis:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ reuse lint
### MISSING COPYRIGHT AND LICENSING INFORMATION

The following files have no copyright and licensing information:
* .flake8
* MANIFEST.in
* requirements-dev.txt
* tests/resources/autocompletion/frames
* tests/resources/sample_data/frames
* tt.completion
* tt.zsh-completion

The following files have no licensing information:
* README.rst
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;If we know the comment style of any of these files, we can force the &lt;code&gt;reuse&lt;/code&gt; tool to use it. For example:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ reuse addheader --copyright=&amp;quot;David Alfonso&amp;quot; \
    --license=GLP-3.0-or-later --year=2020 \
    --style=python tt.completion
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
You can find the style shorthands inspecting &lt;a href="https://github.com/fsfe/reuse-tool/blob/v0.10.0/src/reuse/_comment.py"&gt;the reuse code&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Licensing non-code files&lt;/h2&gt;
&lt;p&gt;According to the &lt;a href="https://reuse.software/tutorial/"&gt;REUSE tutorial&lt;/a&gt;, files which can be considered &lt;em&gt;insignificant&lt;/em&gt;, like configuration files, might be licensed differently. One alternative is the &lt;a href="https://creativecommons.org/publicdomain/zero/1.0/"&gt;CC0 license&lt;/a&gt;, which is very similar to releasing the file to the public domain. However, because I'm already using the MIT license, and to &lt;a href="https://flameeyes.blog/2020/04/23/reuse-simplifying-code-licensing/"&gt;avoid blocking contributors&lt;/a&gt;, I prefer to use the MIT license for this kind of files.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;This post has tried to explain the findings and tools I've found along the way of switching the license of a forked FOSS project.&lt;/p&gt;
&lt;p&gt;Of special importance was the &lt;a href="https://reuse.software/"&gt;REUSE website&lt;/a&gt; which has proved to be a great resource for solving licensing doubts, together with the &lt;a href="https://www.gnu.org/licenses/gpl-faq.en.html"&gt;GPL FAQ&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, the &lt;code&gt;reuse&lt;/code&gt; tool has turned out very effective in automating the process of adding copyright and license information to all files.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;When I write GPL, please assume GNU GPLv3 or later. &lt;a href="https://www.gnu.org/licenses/identify-licenses-clearly.html"&gt;Being forward-compatible is relevant&lt;/a&gt;.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="posts"></category><category term="foss"></category></entry><entry><title>POSIX Shell Scripting</title><link href="https://davidalfonso.es/posts/posix-shell-scripting" rel="alternate"></link><published>2020-05-07T00:00:00+02:00</published><updated>2020-05-07T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2020-05-07:/posts/posix-shell-scripting</id><summary type="html">&lt;p&gt;Resources for developing POSIX-compliant scripts.&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Portable Operating System Interface (POSIX) family of standards are specified by the IEEE to maintain OS interoperability&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The standardized user command line and scripting interface were based on the UNIX System V shell. But not only these are standardized, also are common utility programs.&lt;/p&gt;
&lt;p&gt;POSIX.1-2008 combined most parts of previous POSIX standards into one. Latest revision is POSIX.1-2017&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9699919799/nframe.html"&gt;POSIX.1-2017 Specification with search box and frames&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9699919799/xrat/contents.html"&gt;POSIX.1-2017 Rationale TOC&lt;/a&gt; (e.g. to be strictly POSIX compliant there must be NO #! at the beginning of the file)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html"&gt;The Shell Command Language Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html"&gt;POSIX Utility Conventions&lt;/a&gt;: Naming of utilities, specification of options, option-arguments, and operands (The &lt;code&gt;getopt()&lt;/code&gt; function assists in this matter).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://wiki.bash-hackers.org/scripting/nonportable"&gt;Shell Portability Guidelines&lt;/a&gt;: &lt;em&gt;If scripts need to be portable, some of the BASH-specific syntax elements should be avoided. Others should be avoided for all scripts, e.g. if there is a corresponding POSIX®-compatible syntax.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dylanaraps/pure-sh-bible"&gt;Pure sh Bible&lt;/a&gt;: a collection of pure POSIX alternatives to external processes.&lt;/p&gt;
&lt;h2&gt;Shells&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://developer.ibm.com/tutorials/l-linux-shells/"&gt;Evolution of Shells in Linux&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Comparison_of_command_shells"&gt;Comparison of command shells&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://hyperpolyglot.org/unix-shells"&gt;Hyperpolyglot: Unix Shells (bash, fish, ksh, tcsh, zsh)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://wiki.ubuntu.com/DashAsBinSh"&gt;Dash as default /bin/sh shell in Ubuntu&lt;/a&gt;: Dash is &lt;a href="https://unix.stackexchange.com/questions/148035/is-dash-or-some-other-shell-faster-than-bash"&gt;more efficient than Bash&lt;/a&gt;. For system scripts this matters. It was changed in Ubuntu 6.10 (long time ago). Includes a list of "bashisms"&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;. It was Ubuntu who migrated first to Dash, and then Debian decided to adopt it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Almquist_shell"&gt;ash&lt;/a&gt;, the original Kenneth Almquist shell. It's a clone of the &lt;a href="https://en.wikipedia.org/wiki/Bourne_shell"&gt;Bourne Shell&lt;/a&gt; to avoid licensing issues. It's the default shell in &lt;a href="https://www.busybox.net/"&gt;BusyBox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://gondor.apana.org.au/~herbert/dash/"&gt;dash&lt;/a&gt;: The Debian Almquist Shell is a modern POSIX-compliant implementation of &lt;code&gt;/bin/sh&lt;/code&gt;.  It has a low memory footprint compared to Bash. It's used in Debian as the default non-interactive shell.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://mrsh.sh/"&gt;mrsh&lt;/a&gt;: a minimal POSIX shell.&lt;/p&gt;
&lt;h2&gt;Tooling&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/koalaman/shellcheck"&gt;shellcheck&lt;/a&gt;: a static analysis tool for shell scripts. Use &lt;code&gt;shellcheck -s sh&lt;/code&gt; (or if script starts with &lt;code&gt;#!/bin/sh&lt;/code&gt;) to check for POSIX and warn on portability issues.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/thinkerbot/ts"&gt;ts(1) -- test script&lt;/a&gt;: &lt;em&gt;ts provides functions for writing tests in shell. The test scripts can be run individually or in a batch format using ts as a command. ts makes a test directory available on a per-test basis so it's easy to sandbox tests that write or manipulate files. ts tries to use POSIX exclusively and so should (hopefully) work with any POSIX-compliant shell.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://linux.die.net/man/1/checkbashisms"&gt;checkbashisms&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Examples&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.cs.stevens.edu/~jschauma/615/examples/shexamples"&gt;A shell script to demonstrate various POSIX features related to shell variables&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/kisslinux/kiss"&gt;KISS package manager&lt;/a&gt;: Tiny and straightforward package manager for &lt;a href="https://k1ss.org/"&gt;KISS&lt;/a&gt; (a Linux distribution) written in 600 lines of POSIX sh. &lt;a href="https://k1ss.org/software"&gt;Related&lt;/a&gt; &lt;a href="https://k1ss.org/community"&gt;software&lt;/a&gt;. Script related &lt;a href="https://k1ss.org/guidestones"&gt;guidestones&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;All shell code must be written in a safe way, pass the shellcheck linter and match the style of any existing code.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;All distribution tooling and shell code must be written in a portable way. Otherwise, the user will be locked into a single coreutils and shell.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;One exception is made for &lt;code&gt;sed -i&lt;/code&gt; as it is too useful to let go of. The &lt;code&gt;-i&lt;/code&gt; flag has rather good support across implementations regardless.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://www.romanzolotarev.com/ssg.html"&gt;Static site generator (180LoC)&lt;/a&gt;, written in shell.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.romanzolotarev.com/rssg.html"&gt;RSS feed generator (148LoC)&lt;/a&gt;, written in shell.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dspinellis/git-issue"&gt;git-issue&lt;/a&gt;: Git-based decentralized issue management. Interesting shellchecks to support, e.g. local variables, which are not standard but supported by most modern shells.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/asdf-vm/asdf"&gt;asdf-vm&lt;/a&gt;: Extendable version manager. Supports many shells.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/huyng/bashmarks"&gt;bashmarks&lt;/a&gt;: Save and jump to commonly used directories. Has tab completion embedded in the same script (zsh and bash).&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/POSIX"&gt;https://en.wikipedia.org/wiki/POSIX&lt;/a&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;bashism: "a shell feature that is not required to be supported by POSIX".&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="posts"></category><category term="linux"></category><category term="references"></category></entry><entry><title>Forking Watson: Motivations</title><link href="https://davidalfonso.es/posts/forking-watson-motivations" rel="alternate"></link><published>2020-05-05T00:00:00+02:00</published><updated>2020-05-05T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2020-05-05:/posts/forking-watson-motivations</id><summary type="html">&lt;p&gt;About forking and the reasoning behind my decision to fork the Watson project.&lt;/p&gt;</summary><content type="html">&lt;p&gt;This story began about a year ago when I started &lt;a href="https://github.com/TailorDev/Watson/pulls?q=is%3Apr+author%3Adavidag+sort%3Acreated-asc"&gt;contributing&lt;/a&gt; to the &lt;a href="https://davidalfonso.es/posts/codebase-watson"&gt;Watson project&lt;/a&gt;. It was a good experience and I learnt a lot, especially about collaborating in a FOSS project. Nevertheless, along the way, I noticed some patterns and signals that hinted me that this project was not for me. But let's start from the beginning.&lt;/p&gt;
&lt;h2&gt;What is a fork?&lt;/h2&gt;
&lt;p&gt;There are (at least) two types of forks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A &lt;strong&gt;development fork&lt;/strong&gt; is just a copy of the original project where a contributor makes some changes and submits those back to the main project. This meaning of fork is &lt;a href="https://drewdevault.com/2019/05/24/What-is-a-fork.html"&gt;controversial&lt;/a&gt; as it's mostly associated to the GitHub concept of Fork, which is far in intention and meaning from the &lt;a href="https://en.wikipedia.org/wiki/The_Cathedral_and_the_Bazaar#%22The_Cathedral_and_the_Bazaar%22"&gt;Bazaar model&lt;/a&gt; where contributors just &lt;em&gt;clone&lt;/em&gt; the original repository, make some changes and send them back (usually in &lt;a href="https://www.kernel.org/doc/html/v4.10/process/submitting-patches.html"&gt;patch form using e-mail&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A &lt;strong&gt;hard fork&lt;/strong&gt; is when some people (or even just one person) decides to part ways with an existing project (the forked project) and start its own &lt;em&gt;independent&lt;/em&gt; project (the fork) based on the former. This can be done for a myriad of reasons, in multiple ways and with infinite purposes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post refers to the second meaning and I will use the term "fork" meaning "hard fork" through the rest of the text.&lt;/p&gt;
&lt;h2&gt;Why forking?&lt;/h2&gt;
&lt;p&gt;The general advice is to fork a project only as a last resort measure, as it effectively splits the efforts weakening both the fork and the forked projects. Still and all, there are multiple reasons to fork a FOSS project and one or more can apply at the same time. Let's review some of them in no particular order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The project leadership does not align with your personal values&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As a contributor, you should evaluate the value system of the leadership and make a personal determination as to whether or not it aligns with your own. If it does, participate. If it does not, find an alternative or fork the project.
&lt;em&gt;&lt;a href="https://drewdevault.com/2020/01/17/Effective-project-governance.html"&gt;Drew DeVault - Effective project governance&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You have a different vision of the direction the project should follow&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some forks are due to amicable but irreconcilable disagreements about the direction of the project; perhaps more are due to both technical disagreements and interpersonal conflicts. Of course, it's not always possible to tell the difference between the two, as technical arguments may involve personal elements as well. What all forks have in common is that one group of developers (or sometimes even just one developer) has decided that the costs of working with some or all of the others now outweigh the benefits.
&lt;em&gt;&lt;a href="https://producingoss.com/en/forks.html#who-is-the-fork"&gt;Producing OSS - Forks&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The project leadership has left or lost the original motivation, ie. the project is abandoned/unmaintained&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In my opinion, there are two good ways to abandon a project: the fork it option and the hand-off option. The former is faster and easier, and you can pick this if you want to wash your hands of the project ASAP, but has a larger effect on the community. The latter is not always possible, requires more work on your part, and takes longer, but it has a minimal impact on the community.
&lt;em&gt;&lt;a href="https://drewdevault.com/2018/12/04/How-to-abandon-a-FLOSS-project.html"&gt;Drew DeVault - How to abandon a FLOSS project&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You will come up with more if you try, but these seem to me like the most common reasons to fork a project.&lt;/p&gt;
&lt;h2&gt;The case of Watson&lt;/h2&gt;
&lt;p&gt;Watson is a small tool to track your time from the command line. It was built by the French company &lt;a href="https://tailordev.fr/"&gt;TailorDev&lt;/a&gt;. As part of their open core values, they developed a variety of very interesting projects that had quite an impact on the global software development audience. They were an inspiration to many people (among which I include myself).&lt;/p&gt;
&lt;p&gt;Nevertheless, their activity started to slow down since 2018 and their opensource projects switched to maintenance mode, not because they were complete, but because it was no longer a priority for the people involved. Despite this, they still care and answer questions from the community, always in a kind and friendly manner.&lt;/p&gt;
&lt;p&gt;For me, the problem is that these projects (and Watson specifically) have many traits of what I've called a &lt;a href="https://davidalfonso.es/posts/foss-zombies"&gt;FOSS zombie project&lt;/a&gt;. It sounds worse than the maintainers deserve, but I couldn't find a better metaphor to convey the situation as I see it: &lt;em&gt;a project that is alive, but dead&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I'd also like to say that after speaking privately with the maintainers, there is a disposition to improve the current situation to some extent; though they are OK with the current state of things.&lt;/p&gt;
&lt;p&gt;To sum it all up, as I haven't seen a clear path to keep participating in Watson, I've prefered to take the situation as a challenge to learn about FOSS maintenance and simplify and customise the software to fit my needs (and hopefully the ones of others too).&lt;/p&gt;</content><category term="posts"></category><category term="foss"></category></entry><entry><title>Khal: Codebase review</title><link href="https://davidalfonso.es/posts/codebase-khal" rel="alternate"></link><published>2020-04-05T00:00:00+02:00</published><updated>2020-04-05T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2020-04-05:/posts/codebase-khal</id><summary type="html">&lt;p&gt;Code review of Khal, a CLI and terminal calendar program, able to synchronize with CalDAV servers.&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Project information&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Code repository: &lt;a href="https://github.com/pimutils/khal"&gt;https://github.com/pimutils/khal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;License: Expat/MIT License (license and copyright notice)&lt;/li&gt;
&lt;li&gt;Type of program: Python cli and terminal application&lt;/li&gt;
&lt;li&gt;Supported Python versions: 3.4+ (see setup.py or tox.ini)&lt;/li&gt;
&lt;li&gt;Version reviewed: v0.10.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Documentation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lostpackets.de/khal/"&gt;Official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pimutils/khal/wiki/release"&gt;Release procedure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;High-level analysis&lt;/h2&gt;
&lt;p&gt;This analysis is based on &lt;code&gt;setup.py&lt;/code&gt; contents and on a first look at the code.&lt;/p&gt;
&lt;h3&gt;Concepts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;iCalendar&lt;/strong&gt;: A standard specification for Internet Calendaring and Scheduling. It defines a MIME type and files that comply with this specification usually have an extension of &lt;code&gt;.icf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;vdir&lt;/strong&gt;: A &lt;a href="https://vdirsyncer.readthedocs.io/en/stable/vdir.html"&gt;storage format&lt;/a&gt; for storing calendars and contacts in the filesystem with the goal of being easy to implement. It uses the &lt;em&gt;iCalendar&lt;/em&gt; and &lt;em&gt;vCard&lt;/em&gt; formats, where each file contains one contact or event.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;etag&lt;/strong&gt; and &lt;strong&gt;ctag&lt;/strong&gt;: The concept of &lt;a href="https://en.wikipedia.org/wiki/HTTP_ETag"&gt;ETag is used in HTTP&lt;/a&gt; as a mechanism to provide web cache validation. In Khal, both &lt;strong&gt;etag&lt;/strong&gt; and &lt;strong&gt;ctag&lt;/strong&gt; are used for synchronizing the SQLite database cache with the actual events and calendars.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dependencies&lt;/h3&gt;
&lt;p&gt;Runtime dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;click&lt;/code&gt;: composable command line interface toolkit&lt;/li&gt;
&lt;li&gt;&lt;code&gt;click_log&lt;/code&gt;: logging integration for Click&lt;/li&gt;
&lt;li&gt;&lt;code&gt;icalendar&lt;/code&gt;: iCalendar parser/generator&lt;/li&gt;
&lt;li&gt;&lt;code&gt;urwid&lt;/code&gt;: a full-featured console (xterm et al.) user interface library&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyxdg&lt;/code&gt;: implementations of freedesktop.org standards in Python&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytz&lt;/code&gt;: world timezone definitions, modern and historical&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python-dateutil&lt;/code&gt;: extensions to the standard Python datetime module&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configobj&lt;/code&gt;: config file reading, writing and validation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;atomicwrites&lt;/code&gt;: atomic file writes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tzlocal&lt;/code&gt;: tzinfo object for the local timezone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;freezegun&lt;/code&gt;: let tests travel through time by mocking the datetime module&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vdirsyncer&lt;/code&gt;: synchronize calendars and contacts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Extra dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;proctitle&lt;/code&gt;: context manager to set/reset the current process name&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Package organization&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;khal&lt;/code&gt;: The main package. It contains all CLI-related code based in &lt;code&gt;Click&lt;/code&gt; that handles the creation of the &lt;code&gt;CalendarCollection&lt;/code&gt; according to the configuration and arguments, and delegates to the multiple &lt;code&gt;controller&lt;/code&gt; functions operations over the collection depending on the requested user action. There are modules for &lt;code&gt;iCalendar&lt;/code&gt; utils, converting strings to &lt;code&gt;datetime&lt;/code&gt; or &lt;code&gt;events&lt;/code&gt; objects, handling the &lt;code&gt;Terminal&lt;/code&gt; (colors, columns), and displaying one or more months along its corresponding events in the console.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;khal.khalendar&lt;/code&gt;: Exports a &lt;code&gt;CalendarCollection&lt;/code&gt; class which allows access to various calendars stored in vdirs and cached in an SQLiteDB for performance. This collection uses an &lt;code&gt;Event&lt;/code&gt; model that represents recurring or non-recurring instances of events. This package also contains a module with tools to read/write from/to &lt;code&gt;vdirs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;khal.settings&lt;/code&gt;: Handles Khal configuration, file paths, and vdir config.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;khal.ui&lt;/code&gt;: Contains multiple widgets for &lt;code&gt;urwid&lt;/code&gt; (a console UI library) and all the code that glues together the &lt;code&gt;khal.khalendar&lt;/code&gt; package with the UI. In this case, the controlling is performed by &lt;code&gt;urwid&lt;/code&gt; which handles events to the UI classes defined in this module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Entry points&lt;/h3&gt;
&lt;p&gt;Two console scripts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;khal = &lt;code&gt;khal.cli:main_khal&lt;/code&gt;, which provides a command-result terminal experience.&lt;/li&gt;
&lt;li&gt;ikhal = &lt;code&gt;khal.cli:main_ikhal&lt;/code&gt;, which provides a TUI (terminal-user interface) handled by code in &lt;code&gt;khal.ui&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Configuration and data files&lt;/h2&gt;
&lt;p&gt;Configuration&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;configuration file&lt;/strong&gt; is looked up into the list of paths defined by the XDG Base Directory Standard using the &lt;code&gt;pyxdg&lt;/code&gt; package. The preferred name of the file is &lt;code&gt;config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;configuration file&lt;/strong&gt; contents are handle using the &lt;code&gt;configobj&lt;/code&gt; library, and its contents specified by the file &lt;code&gt;khal.spec&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Calendars (data)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The path of calendars is defined in the configuration file.&lt;/li&gt;
&lt;li&gt;The used data format is the &lt;a href="https://vdirsyncer.readthedocs.io/en/stable/vdir.html"&gt;Vdir Storage Format&lt;/a&gt;, defined in the &lt;a href="https://vdirsyncer.readthedocs.io/en/stable/index.html"&gt;vdirsyncer&lt;/a&gt; project (also under the &lt;a href="https://github.com/pimutils"&gt;pimutils&lt;/a&gt; organization umbrella).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The following diagram tries to represent the components and relations of the khal (cli) application. This is not complete by any means, many pieces are missing and some might be wrong.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/images/khal-components.png"&gt;&lt;/p&gt;
&lt;h2&gt;Object-oriented design&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;khalendar&lt;/code&gt; package contains code which is more object-oriented than in other khal's modules. Let's analyze its classes:&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;CalendarCollection&lt;/code&gt; class can be seen as this package interface. It uses composition to make use of the &lt;code&gt;SQLiteDb&lt;/code&gt; class implementation and also is composed of one &lt;code&gt;Vdir&lt;/code&gt; object per configured calendar. It also allows CRUD of &lt;code&gt;Event&lt;/code&gt;s saved in the SQLite cache and in &lt;code&gt;vdir&lt;/code&gt;s using multiple criteria.&lt;/p&gt;
&lt;p&gt;Events are represented as &lt;code&gt;Event&lt;/code&gt; class instances, which is a base class for different derived classes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalizedEvent&lt;/code&gt;, which handles start and end datetime timezones.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FloatingEvent&lt;/code&gt;, which are not all day events.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AllDayEvent&lt;/code&gt;, which are all day events.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;vdir&lt;/code&gt; module makes use of many OO design concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The class &lt;code&gt;Vdir&lt;/code&gt;, which is the one used from the outside, is just a mixture of behavior and interfaces from three other classes it inherits from:&lt;ol&gt;
&lt;li&gt;&lt;code&gt;VdirBase&lt;/code&gt; which implements operations in a vdir folder, including &lt;a href="https://vdirsyncer.readthedocs.io/en/stable/vdir.html#metadata"&gt;handling metadata information&lt;/a&gt; or writing, reading and deleting data.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DisplayNameMixin&lt;/code&gt; which provides methods for accesing the &lt;code&gt;displayname&lt;/code&gt; metadata file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ColorMixin&lt;/code&gt; which provides methods for accessing the &lt;code&gt;color&lt;/code&gt; metadata file.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Multiple exception classes inheriting from &lt;code&gt;VdirError(IOError)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;Item&lt;/code&gt; class, used from the controllers to import items in a calendar.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Color&lt;/code&gt; class only used in the &lt;code&gt;ColorMixin&lt;/code&gt; class.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besides the &lt;code&gt;khalendar&lt;/code&gt; package, the other interesting package is &lt;code&gt;ui&lt;/code&gt;, which contains multiple classes following &lt;code&gt;urwid&lt;/code&gt;'s &lt;a href="http://urwid.org/manual/overview.html"&gt;object oriented development design&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Python techniques&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;logging&lt;/code&gt; module is used to show the user warnings in multiple parts of the code. There is &lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/cli.py#L45"&gt;a khal logger&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;logger = logging.getLogger(&amp;#39;khal&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;code&gt;__init__.py&lt;/code&gt; files in subpackages to auto-import "exported" functions/classes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In &lt;code&gt;khal/settings/__init__.py&lt;/code&gt; one function and one class are made available through &lt;code&gt;settings.get_config&lt;/code&gt; and &lt;code&gt;settings.InvalidSettingsError&lt;/code&gt; respectively:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;from .settings import get_config  # noqa
from .exceptions import InvalidSettingsError  # noqa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then, in &lt;code&gt;khal/cli.py&lt;/code&gt; is enough to do:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;from .settings import InvalidSettingsError, get_config
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type hints&lt;/strong&gt; are used in most method arguments of &lt;code&gt;backend.SQLiteDb&lt;/code&gt; (&lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/backend.py#L57"&gt;see code&lt;/a&gt;), while they're not used at all in other classes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Avoid class instantiation to simulate a pure abstract base class (e.g &lt;code&gt;Event&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def __init__(self, vevents, ref=None, **kwargs):
   if self.__class__.__name__ == &amp;#39;Event&amp;#39;:
        raise ValueError(&amp;#39;do not initialize this class directly&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Factory methods&lt;/strong&gt; using &lt;code&gt;@classmethod&lt;/code&gt; vs &lt;code&gt;@staticmethod&lt;/code&gt; in &lt;code&gt;Event&lt;/code&gt; base class (&lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/event.py"&gt;see code&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A simple factory method is provided by &lt;code&gt;Event._create_calendar()&lt;/code&gt; using the &lt;code&gt;@staticmethod&lt;/code&gt; decorator, because the method is logically contained in &lt;code&gt;Event&lt;/code&gt; but does not use any of its methods nor it creates a &lt;code&gt;Event&lt;/code&gt; object.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@classmethod&lt;/code&gt; decorator is used to define a couple of more complex factory methods. They are based on VEvents (&lt;code&gt;Event.fromVEvents()&lt;/code&gt;) or on a string (&lt;code&gt;Event.fromString()&lt;/code&gt;), detecting the correct class to instantiate based on the attributes of the provided elements.&lt;/li&gt;
&lt;li&gt;In the same module, &lt;code&gt;event.py&lt;/code&gt;, there is a factory method for creating icalendar vtimezones from &lt;code&gt;pytz.tzinfo&lt;/code&gt; objects. This method is outside any class definition but is there probably because of its relation with events and is, in fact, used only from a method in the &lt;code&gt;Event&lt;/code&gt; class (&lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/event.py#L749"&gt;see code&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Comparable events&lt;/strong&gt; by overloading &lt;code&gt;__lt__()&lt;/code&gt; in the &lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/event.py#L157"&gt;Event class&lt;/a&gt;. This class handles the different types of date/time objects that can be used in an &lt;code&gt;Event&lt;/code&gt;-derived object.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Context managers&lt;/strong&gt;, for example &lt;code&gt;SQLiteDb.at_once()&lt;/code&gt;, allows to perform multiple operations on the database atomically, by delaying the SQLite transaction commit until the scope is exited without exceptions being raised (&lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/backend.py#L87"&gt;see code&lt;/a&gt;).
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="nd"&gt;@contextlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contextmanager&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;at_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_at_once&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_at_once&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# noqa&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_at_once&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example usage in &lt;code&gt;khalendar.py:update()&lt;/code&gt; (&lt;a href="https://github.com/pimutils/khal/blob/v0.10.1/khal/khalendar/khalendar.py#L165"&gt;see code&lt;/a&gt;) where an event is updated both in a &lt;code&gt;vdir&lt;/code&gt; and in the cache database.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;at_once&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_ctag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_local_ctag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;class-based decorator&lt;/strong&gt; named &lt;code&gt;cached_property&lt;/code&gt; in the &lt;code&gt;khalendar.vdir&lt;/code&gt; module which transforms an instance method into a &lt;strong&gt;descriptor object attribute&lt;/strong&gt;:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Implements the descriptor protocol making this object a descriptor,&lt;/span&gt;
&lt;span class="c1"&gt;# specifically a non-data descriptor because it implements only the&lt;/span&gt;
&lt;span class="c1"&gt;# __get__ method.&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;cached_property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;#39;&amp;#39;&amp;#39;A read-only @property that is only evaluated once. Only usable&lt;/span&gt;
&lt;span class="sd"&gt;    on class instances&amp;#39; methods.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__module__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__module__&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__doc__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;fget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__doc__&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fget&lt;/span&gt;

    &lt;span class="c1"&gt;# This method is called once because an attribute is added to the&lt;/span&gt;
    &lt;span class="c1"&gt;# object via the self.__dict__ object and, in the case of non-data&lt;/span&gt;
    &lt;span class="c1"&gt;# descriptors, the instance&amp;#39;s dictionary entry takes precedence.&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__get__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# pragma: no cover&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;We can use it like this:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# When this method is processed by the interpreter, an instance&lt;/span&gt;
    &lt;span class="c1"&gt;# of the class cached_property is created with this function as&lt;/span&gt;
    &lt;span class="c1"&gt;# argument &amp;#39;fget&amp;#39;.&lt;/span&gt;
    &lt;span class="nd"&gt;@cached_property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prop_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Usage example
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prop_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;prop_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is much more in &lt;code&gt;khal&lt;/code&gt; and I recommend you to take a look at the code and find out by yourself ;-)&lt;/p&gt;</content><category term="posts"></category><category term="codebases"></category><category term="python"></category></entry><entry><title>PHP 7.2 Docker image analysis</title><link href="https://davidalfonso.es/posts/php-docker-image" rel="alternate"></link><published>2019-11-15T00:00:00+01:00</published><updated>2019-11-15T00:00:00+01:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2019-11-15:/posts/php-docker-image</id><summary type="html">&lt;p&gt;A deep review of the Docker official image for PHP 7.2 with Apache 2.4.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Base Docker images are full of sysadmin best practices and interesting tips to learn from. Besides that, knowing what's going on behind the curtains of a FROM is mandatory if you plan to trust a production system with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR image features&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It uses Apache2 from Debian repositories.&lt;/li&gt;
&lt;li&gt;It installs PHP following the official installation recommendations.&lt;/li&gt;
&lt;li&gt;Apache can be run as an arbitrary user: &lt;code&gt;/var/www/html&lt;/code&gt; has 777 permissions.&lt;/li&gt;
&lt;li&gt;PHP configuration resides in: &lt;code&gt;/usr/local/etc/php/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's review all sections of the &lt;a href="https://github.com/docker-library/php/blob/master/7.2/buster/apache/Dockerfile"&gt;php:7.2-apache Docker base image&lt;/a&gt; mentioning some shell, Debian and Dockerfile best practices along the way.&lt;/p&gt;
&lt;h2&gt;Base image&lt;/h2&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;FROM debian:buster-slim
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
The image is based on &lt;a href="https://www.debian.org/releases/buster/"&gt;Debian buster&lt;/a&gt;, aka Debian 10. Specifically, a slimmed down version of it. But, hey, we're not going down this rabbit hole.&lt;/p&gt;
&lt;h2&gt;RUN instructions&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;RUN&lt;/code&gt; instruction, in its shell form, will execute the given command in the &lt;code&gt;/bin/sh&lt;/code&gt; shell using the &lt;code&gt;-c&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;In Debian, &lt;code&gt;/bin/sh&lt;/code&gt; corresponds to the &lt;a href="https://en.wikipedia.org/wiki/Almquist_shell#dash:_Ubuntu,_Debian_and_POSIX_compliance_of_Linux_distributions"&gt;Debian Almquist Shell (dash)&lt;/a&gt; which strives to be a POSIX-compliant and slim shell. This means no "bashisms" in RUN instructions (unless you run &lt;code&gt;/bin/bash -c&lt;/code&gt;, of course).&lt;/p&gt;
&lt;p&gt;As we will see, the first subcommand is always this:
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;set -eux
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;This will enable the following &lt;a href="https://manpages.debian.org/stretch/dash/dash.1.en.html#Argument_List_Processing"&gt;shell options&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;errexit&lt;/code&gt;: exit immediately if any untested command fails.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nounset&lt;/code&gt;: exit immediately if attempting to expand an unset variable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xtrace&lt;/code&gt;: write each command to stderr preceded by a &lt;code&gt;+&lt;/code&gt; before being executed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Block official PHP Debian packages&lt;/h2&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN set -eux; \
    { \
        echo &amp;#39;Package: php*&amp;#39;; \
        echo &amp;#39;Pin: release *&amp;#39;; \
        echo &amp;#39;Pin-Priority: -1&amp;#39;; \
    } &amp;gt; /etc/apt/preferences.d/no-debian-php
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
This image compiles and installs its own PHP version and extensions directly without using the corresponding Debian packages. &lt;a href="https://github.com/docker-library/php/issues/551#issuecomment-355162339"&gt;This is because&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;They strive to use &lt;a href="https://secure.php.net/manual/en/install.unix.php"&gt;the recommended PHP installation method&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;They want to be able to keep up with upstream releases since Debian will only update PHP in stable releases if there are security vulnerabilities.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is implemented using an &lt;a href="https://manpages.debian.org/buster/apt/apt_preferences.5.en.html"&gt;apt preferences file&lt;/a&gt;, by preventing installation (&lt;code&gt;Pin-Priority: -1&lt;/code&gt;) of all packages whose name starts with &lt;code&gt;php&lt;/code&gt; (using a glob expression: &lt;code&gt;Package: php*&lt;/code&gt;) belonging to all distribution releases (&lt;code&gt;Pin: release *&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Installing PHP compilation dependencies&lt;/h2&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV PHPIZE_DEPS \
    # ...
RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
        $PHPIZE_DEPS \
        ca-certificates \
        curl \
        xz-utils \
    ; \
    rm -rf /var/lib/apt/lists/*
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
This is a typical &lt;code&gt;Dockerfile&lt;/code&gt; pattern for installing package dependencies:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update repositories.&lt;/li&gt;
&lt;li&gt;Install packages, assuming yes to all answers and not installing recommended packages.&lt;/li&gt;
&lt;li&gt;Cleaning the storage area for state information for each package resource in the available repositories. This will save hundreds of MB in the resulting image.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Following &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run"&gt;Dockerfile best practices&lt;/a&gt;, it's important to execute them all in the same &lt;code&gt;RUN&lt;/code&gt; command (ie. in the same Docker &lt;em&gt;layer&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Separating &lt;code&gt;phpize&lt;/code&gt; dependencies in an &lt;a href="https://docs.docker.com/engine/reference/builder/#env"&gt;environment variable&lt;/a&gt; allows having them both organized and sorted alphabetically, at the same time that there is only one &lt;code&gt;apt-get install&lt;/code&gt; call (ie. one layer).&lt;/p&gt;
&lt;p&gt;It's worth noting that the &lt;em&gt;package cache&lt;/em&gt; in &lt;code&gt;/var/cache/apt/archives/&lt;/code&gt; is not being explicitly removed. This is because the official Debian base image is already executing &lt;code&gt;apt-get clean&lt;/code&gt; after every install (&lt;a href="https://github.com/moby/moby/blob/03e2923e42446dbb830c654d0eec323a0b4ef02a/contrib/mkimage/debootstrap#L82-L105"&gt;see code&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Setup PHP / Apache directories&lt;/h2&gt;
&lt;p&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV PHP_INI_DIR /usr/local/etc/php
RUN set -eux; \
    mkdir -p &amp;quot;$PHP_INI_DIR/conf.d&amp;quot;; \
# allow running as an arbitrary user
    [ ! -d /var/www/html ]; \
    mkdir -p /var/www/html; \
    chown www-data:www-data /var/www/html; \
    chmod 777 /var/www/html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
Sets &lt;code&gt;PHP_INI_DIR&lt;/code&gt; environment variable, which is later used to specify the php configuration path &lt;code&gt;/usr/local/etc/php&lt;/code&gt;, and create Apache's html root directory with all permissions granted to all users.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Shell tip&lt;/em&gt;: Note that &lt;code&gt;/var/www/html&lt;/code&gt; shouldn't exist before this RUN instruction; otherwise, this will halt in &lt;code&gt;[ ! -d /var/www/html ]&lt;/code&gt; and stop the image building process.&lt;/p&gt;
&lt;h2&gt;Install Apache 2&lt;/h2&gt;
&lt;p&gt;Another &lt;code&gt;RUN&lt;/code&gt; instruction is executed with all the following steps inside (ie. again, all in one &lt;em&gt;layer&lt;/em&gt; to reduce size):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Apache2 from Debian repositories is installed following the aforementioned technique (ie. &lt;code&gt;apt-get install&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Allow &lt;a href="https://httpd.apache.org/docs/2.4/env.html"&gt;Apache environment variables&lt;/a&gt; to be overriden at runtime from the Docker cli (e.g. &lt;code&gt;-e APACHE_RUN_USER=...&lt;/code&gt;). This is accomplished by modifying &lt;code&gt;/etc/apache2/envars&lt;/code&gt; using the following &lt;code&gt;sed&lt;/code&gt; invocation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sed -ri &amp;#39;s/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/&amp;#39; &amp;quot;$APACHE_ENVVARS&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Command options: &lt;code&gt;-r&lt;/code&gt; enables extended regexps and &lt;code&gt;-i&lt;/code&gt; edits files in place.&lt;/li&gt;
&lt;li&gt;Matches lines like: &lt;code&gt;export SOME_VAR=any value&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Transforms these lines to the form:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;: ${SOME_VAR:=any value}
export SOME_VAR
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The form &lt;code&gt;${parameter:=word}&lt;/code&gt; &lt;a href="https://wiki.bash-hackers.org/syntax/pe#assign_a_default_value"&gt;assigns word to parameter if it was not set&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;The colon character (&lt;code&gt;:&lt;/code&gt;) before the string assignment is the &lt;a href="https://manpages.debian.org/stretch/dash/dash.1.en.html#Builtins"&gt;builtin null command&lt;/a&gt; that returns a 0 (true) exit value. This allows executing the string assignment without running the resulting parameter value (and getting an error when doing it).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Using another &lt;a href="https://manpages.debian.org/stretch/dash/dash.1.en.html#Builtins"&gt;builtin command&lt;/a&gt; (&lt;code&gt;.&lt;/code&gt;), reads and executes the just modified Apache &lt;code&gt;/etc/apache2/envars&lt;/code&gt; file in the current shell.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Grant all permissions to all users and changes ownership of Apache's &lt;code&gt;lock&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, and &lt;code&gt;run&lt;/code&gt; folders, creating them from scratch.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;for dir in &amp;quot;$APACHE_LOCK_DIR&amp;quot; &amp;quot;$APACHE_RUN_DIR&amp;quot; &amp;quot;$APACHE_LOG_DIR&amp;quot;; do
    rm -rvf &amp;quot;$dir&amp;quot;;
    mkdir -p &amp;quot;$dir&amp;quot;;
    chown &amp;quot;$APACHE_RUN_USER:$APACHE_RUN_GROUP&amp;quot; &amp;quot;$dir&amp;quot;;
    chmod 777 &amp;quot;$dir&amp;quot;;
done
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete the &lt;code&gt;index.html&lt;/code&gt; created by Apache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rm -rvf /var/www/html/*
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redirect all Apache log contents to &lt;code&gt;stdout&lt;/code&gt; / &lt;code&gt;stderr&lt;/code&gt; by creating &lt;em&gt;symbolic links&lt;/em&gt; to the corresponding &lt;a href="https://manpages.debian.org/buster/freebsd-manpages/stdout.4freebsd.en.html"&gt;device files&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ln -sfT /dev/stderr &amp;quot;$APACHE_LOG_DIR/error.log&amp;quot;
ln -sfT /dev/stdout &amp;quot;$APACHE_LOG_DIR/access.log&amp;quot;
ln -sfT /dev/stdout &amp;quot;$APACHE_LOG_DIR/other_vhosts_access.log&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt;: Create symbolic link (not hard link).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;: Remove existing destination if exists.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-T&lt;/code&gt;: Treat the destination name (the log file) as a normal file (not a directory).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, change the owner of all log files recursively (&lt;code&gt;-R&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chown -R --no-dereference &amp;quot;$APACHE_RUN_USER:$APACHE_RUN_GROUP&amp;quot; &amp;quot;$APACHE_LOG_DIR&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--no-dereference&lt;/code&gt; changes the symbolic link owner (vs the link target).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Enable preforking MPM&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN a2dismod mpm_event &amp;amp;&amp;amp; a2enmod mpm_prefork
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://httpd.apache.org/docs/2.4/en/mpm.html"&gt;Multi Processing Modules (MPMs)&lt;/a&gt; are responsible for binding to network ports, accepting requests, and dispatching children to serve them. Only one &lt;code&gt;mpm&lt;/code&gt; module can be enabled at the same time.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/faq.installation.php#faq.installation.apache2"&gt;According to the PHP manual&lt;/a&gt;, a threaded MPM is not recommended with Apache2; that's why the &lt;a href="https://httpd.apache.org/docs/2.4/en/mod/prefork.html"&gt;&lt;code&gt;prefork&lt;/code&gt; module&lt;/a&gt; is enabled.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Make PHP handle PHP file requests&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN { \
        echo &amp;#39;&amp;lt;FilesMatch \.php$&amp;gt;&amp;#39;; \
        echo &amp;#39;\tSetHandler application/x-httpd-php&amp;#39;; \
        echo &amp;#39;&amp;lt;/FilesMatch&amp;gt;&amp;#39;; \
        echo; \
        echo &amp;#39;DirectoryIndex disabled&amp;#39;; \
        echo &amp;#39;DirectoryIndex index.php index.html&amp;#39;; \
        echo; \
        echo &amp;#39;&amp;lt;Directory /var/www/&amp;gt;&amp;#39;; \
        echo &amp;#39;\tOptions -Indexes&amp;#39;; \
        echo &amp;#39;\tAllowOverride All&amp;#39;; \
        echo &amp;#39;&amp;lt;/Directory&amp;gt;&amp;#39;; \
    } | tee &amp;quot;$APACHE_CONFDIR/conf-available/docker-php.conf&amp;quot; \
    &amp;amp;&amp;amp; a2enconf docker-php
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;A specific configuration file is created, which is later enabled with the &lt;code&gt;a2enconf&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It's interesting how it writes the contents of a file by using a &lt;a href="https://manpages.debian.org/stretch/dash/dash.1.en.html#Grouping_Commands_Together"&gt;grouping command&lt;/a&gt; piped into the &lt;code&gt;tee&lt;/code&gt; utility, which outputs all it reads from stdin to stdout and also to any file given as a parameter.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://manpages.debian.org/stretch/dash/dash.1.en.html#Grouping_Commands_Together"&gt;From the &lt;code&gt;dash&lt;/code&gt; man page&lt;/a&gt;: "Note that “}” must follow a control operator (here, “;”) so that it is recognized as a reserved word and not as another command argument."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Establish PHP compilation options and dependencies&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV PHP_EXTRA_BUILD_DEPS apache2-dev
ENV PHP_EXTRA_CONFIGURE_ARGS --with-apxs2 --disable-cgi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;We'll require the &lt;code&gt;apache2-dev&lt;/code&gt; Debian package which contains development headers and the &lt;code&gt;apxs2&lt;/code&gt; binary, apart from some &lt;a href="https://manpages.debian.org/buster/debhelper/debhelper.7.en.html"&gt;&lt;code&gt;debhelper&lt;/code&gt; scripts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apxs2&lt;/code&gt; is the &lt;a href="https://httpd.apache.org/docs/2.4/programs/apxs.html"&gt;APache eXtenSion tool&lt;/a&gt; which is used for building and installing extension modules: &lt;em&gt;Dynamic Shared Objects (DSOs)&lt;/em&gt;. These extensions are loaded into Apache using the &lt;code&gt;mod_so&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-cgi&lt;/code&gt; avoids building the CGI version of PHP and implicitly enables &lt;a href="https://en.wikipedia.org/wiki/FastCGI"&gt;FastCGI&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV PHP_CFLAGS=&amp;quot;-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64&amp;quot;
ENV PHP_CPPFLAGS=&amp;quot;$PHP_CFLAGS&amp;quot;
ENV PHP_LDFLAGS=&amp;quot;-Wl,-O1 -Wl,--hash-style=both -pie&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;GCC&lt;/em&gt; compiler flags related to &lt;a href="https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Code-Gen-Options.html#Code-Gen-Options"&gt;code generation conventions&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-fpic&lt;/code&gt;: generate position-independent code (PIC) for use in shared libraries.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-fpie&lt;/code&gt;: similar to &lt;code&gt;-fpic&lt;/code&gt; but the PIC generated can only be linked into executables. It requires to link using &lt;code&gt;-pie&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;GCC&lt;/em&gt; compiler flags related to &lt;a href="https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Instrumentation-Options.html#Instrumentation-Options"&gt;program instrumentation for error/attack detection&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-fstack-protector-strong&lt;/code&gt;: check for buffer overflows on functions with vulnerable objects (ie. functions that call &lt;code&gt;alloca&lt;/code&gt;, have buffers larger than 8 bytes, have local array definitions or have references to local frame addresses).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;GCC&lt;/em&gt; compiler flags related to &lt;a href="https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Optimize-Options.html#Optimize-Options"&gt;control optimization&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-O2&lt;/code&gt;: Optimize for reduced code and execution time, without involving space-speed tradeoff optimizations, but taking more time to compile than &lt;code&gt;-O1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Large-file_support"&gt;Large File Support (LFS)&lt;/a&gt; is enabled as per the &lt;a href="https://www.php.net/manual/en/intro.filesystem.php"&gt;PHP documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;GNU linker (ld)&lt;/em&gt; flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-Wl,-O1&lt;/code&gt;: optimize shared library linkage.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From Dockerfile: "this sorts the hash buckets to improve cache locality, and is non-default"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-Wl,--hash-style=both&lt;/code&gt;: set the linker's hash table type for both the classic &lt;code&gt;ELF ".hash"&lt;/code&gt; and the new style &lt;code&gt;GNU ".gnu.hash"&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From Dockerfile: "GNU hash is used if present, and is much faster than sysv hash; in this configuration, sysv hash is also generated".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-pie&lt;/code&gt;: required to use PIC generated objects.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Establish PHP version and validation mechanisms&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV GPG_KEYS 1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Set an environment variable with a couple GPG &lt;a href="https://gnupg.org/faq/gnupg-faq.html#define_key"&gt;key&lt;/a&gt; IDs required to verify PHP source packages.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://superuser.com/a/769488/701"&gt;Key IDs are V4 fingerprints (160-bits)&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENV PHP_VERSION 7.2.25
ENV PHP_URL=&amp;quot;https://www.php.net/get/php-7.2.25.tar.xz/from/this/mirror&amp;quot; \
    PHP_ASC_URL=&amp;quot;https://www.php.net/get/php-7.2.25.tar.xz.asc/from/this/mirror&amp;quot;
ENV PHP_SHA256=&amp;quot;746efeedc38e6ff7b1ec1432440f5fa801537adf6cd21e4afb3f040e5b0760a9&amp;quot; \
    PHP_MD5=&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Set the PHP version, source URL, signature URL and SHA256 hash.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.asc&lt;/code&gt; file extension implies &lt;a href="https://www.gnupg.org/documentation/manuals/gnupg/Operational-GPG-Commands.html#index-clear_002dsign"&gt;the signature is in plain-text&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Download PHP source code and verify its integrity&lt;/h2&gt;
&lt;p&gt;This corresponds to a long &lt;code&gt;RUN&lt;/code&gt; instruction composed of multiple steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN set -eux; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Save the list of manually installed packages using &lt;code&gt;apt-mark showmanual&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From the &lt;a href="https://manpages.debian.org/buster/apt/apt-mark.8.en.html"&gt;man pages&lt;/a&gt;: When you request that a package is installed, and as a result other packages are installed to satisfy its dependencies, the dependencies are marked as being automatically installed, while the package you installed explicitly is marked as manually installed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the &lt;code&gt;gnupg&lt;/code&gt; and &lt;code&gt;dirmngr&lt;/code&gt; packages using &lt;code&gt;apt&lt;/code&gt; as seen previously.&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;a href="https://packages.debian.org/en/buster/gnupg"&gt;gnupg&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;GNU privacy guard - a free PGP replacement&lt;/dd&gt;
&lt;dt&gt;&lt;a href="https://packages.debian.org/en/buster/dirmngr"&gt;dirmngr&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;GNU privacy guard - network certificate management service&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create &lt;code&gt;/usr/src&lt;/code&gt; directory and enter on it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download the PHP sources:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl -fsSL -o php.tar.xz &amp;quot;$PHP_URL&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;: fail silently on server errors (no output at all).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt;: be silent, don't show any progress or messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-S&lt;/code&gt;: show error messages even if &lt;code&gt;-s&lt;/code&gt; is provided.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-L&lt;/code&gt;: follow redirects (&lt;code&gt;Location:&lt;/code&gt; header and a &lt;code&gt;3xx&lt;/code&gt; response code).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o php.tar.xz&lt;/code&gt;: write output to &lt;code&gt;php.tar.xz&lt;/code&gt; instead of &lt;code&gt;stdout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check source code integrity using SHA256 and/or MD5. In this case, only the more secure SHA256 hash was previously defined.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if [ -n &amp;quot;$PHP_SHA256&amp;quot; ]; then \
    echo &amp;quot;$PHP_SHA256 *php.tar.xz&amp;quot; | sha256sum -c -; \
fi; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt;: read from standard input instead of a file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt;: read sums from &lt;code&gt;stdin&lt;/code&gt; and check them.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;From the &lt;a href="https://manpages.debian.org/buster/coreutils/sha256sum.1.en.html"&gt;sha256sum man&lt;/a&gt;: "When checking, the input should be a former output of this program. The default mode is to print &lt;strong&gt;a line with checksum, a space, a character indicating input mode&lt;/strong&gt; ('&lt;em&gt;' for binary, ' ' for text or where binary is insignificant), &lt;/em&gt;&lt;em&gt;and name for each FILE&lt;/em&gt;*."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and verify signature:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if [ -n &amp;quot;$PHP_ASC_URL&amp;quot; ]; then \
    curl -fsSL -o php.tar.xz.asc &amp;quot;$PHP_ASC_URL&amp;quot;; \
    export GNUPGHOME=&amp;quot;$(mktemp -d)&amp;quot;; \
    for key in $GPG_KEYS; do \
        gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys &amp;quot;$key&amp;quot;; \
    done; \
    gpg --batch --verify php.tar.xz.asc php.tar.xz; \
    gpgconf --kill all; \
    rm -rf &amp;quot;$GNUPGHOME&amp;quot;; \
fi; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Download &lt;code&gt;.asc&lt;/code&gt; signature file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a temporary directory and use it as &lt;code&gt;GNUPGHOME&lt;/code&gt; (default is &lt;code&gt;~/.gnupg&lt;/code&gt; otherwise).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;export GNUPGHOME=&amp;quot;$(mktemp -d)&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mktemp&lt;/code&gt; prints to &lt;code&gt;stdout&lt;/code&gt; the file or directory created and we use &lt;code&gt;$()&lt;/code&gt; to substitute its output in place of the command name itself, ie. the temp folder name. See the &lt;a href="https://manpages.debian.org/buster/dash/dash.1.en.html#Command_Substitution"&gt;Dash Command Substituion man section&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Mark for automatic export to the environment the &lt;code&gt;GNUPGHOME&lt;/code&gt; variable to subsequently executed programs using &lt;code&gt;export&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For each provided &lt;code&gt;keyID&lt;/code&gt; download the key itself from the &lt;code&gt;ha.pool.sks-keyservers.net&lt;/code&gt; key server. This works because we have installed the &lt;code&gt;dirmngr&lt;/code&gt; package.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys &amp;quot;$key&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--batch&lt;/code&gt;: use batch mode, ie. don't allow interactive messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--keyserver&lt;/code&gt;: set keyserver to use (this option is deprecated).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--recv-keys "$key"&lt;/code&gt;: import the keys with the given &lt;code&gt;keyIDs&lt;/code&gt; from a keyserver.&lt;/li&gt;
&lt;/ul&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;a href="https://sks-keyservers.net/overview-of-pools.php"&gt;&lt;code&gt;ha.pool.sks-keyservers.net&lt;/code&gt;&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;This is a high-availability subset of the pool that require all servers to be identified as a clustered setup (marked with a blue indicator for reverse proxy in the status pages)&lt;/dd&gt;
&lt;dt&gt;&lt;a href="https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home"&gt;What is an SKS keyserver?&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;SKS is an OpenPGP keyserver whose goal is to provide easy to deploy, decentralized, and highly reliable synchronization. That means that a key submitted to one SKS server will quickly be distributed to all key servers, and even wildly out-of-date servers, or servers that experience spotty connectivity, can fully synchronize with the rest of the system.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify the signature in the detached &lt;code&gt;.asc&lt;/code&gt; signature file without user interaction:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gpg --batch --verify php.tar.xz.asc php.tar.xz; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kill all gpg components running as daemons and remove the GPG temporary key home.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gpgconf --kill all; \
rm -rf &amp;quot;$GNUPGHOME&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Purge all packages installed in this step (ie. remove and delete configuration files) along with all its dependencies.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mark all installed packages as automatically installed discarding any output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-mark auto &amp;#39;.*&amp;#39; &amp;gt; /dev/null; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mark as manually installed the same packages that were marked as manual at the beginning of this step (ie. not including &lt;code&gt;gnupg&lt;/code&gt;, &lt;code&gt;dirmngr&lt;/code&gt; and its installed dependencies) discarding any output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-mark manual $savedAptMark &amp;gt; /dev/null; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Remove automatically installed packges that are no longer needed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-y&lt;/code&gt;: automatic yes to prompts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--auto-remove&lt;/code&gt;: removes unused dependency packages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o APT::AutoRemove::RecommendsImportant=false&lt;/code&gt;: sets this configuration option to false, effectively forcing the removal of recommended packages whose dependent packages are also being removed. &lt;a href="https://github.com/docker-library/php/pull/71"&gt;See explanation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Copy &lt;code&gt;docker-php-source&lt;/code&gt; script&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;COPY docker-php-source /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Make available the &lt;a href="https://github.com/docker-library/php/blob/master/7.2/buster/apache/docker-php-source"&gt;&lt;code&gt;docker-php-source&lt;/code&gt;&lt;/a&gt; script built for managing the php source tarball lifecycle inside the container.&lt;/p&gt;
&lt;h2&gt;Install compilation dependencies, configure build process and compile PHP&lt;/h2&gt;
&lt;p&gt;This is the last big &lt;code&gt;RUN&lt;/code&gt; command and is responsible for building the PHP sources following the recommended procedure. These long &lt;code&gt;RUN&lt;/code&gt; commands are common in Docker to &lt;a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#minimize-the-number-of-layers"&gt;minimize the number of layers&lt;/a&gt;. Let's review all the steps involved:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN set -eux; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First of all, save the list of manually installed packages to clean everything else later:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;savedAptMark=&amp;quot;$(apt-mark showmanual)&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install compilation dependencies using Apt. Packages previously defined in &lt;code&gt;PHP_EXTRA_BUILD_DEPS&lt;/code&gt; are also installed. The typical &lt;code&gt;apt-get update&lt;/code&gt;, &lt;code&gt;apt-get install&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt; cycle is used.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Export the previously defined &lt;code&gt;PHP_*&lt;/code&gt; variables for the GCC compiler suite.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;export \
    CFLAGS=&amp;quot;$PHP_CFLAGS&amp;quot; \
    CPPFLAGS=&amp;quot;$PHP_CPPFLAGS&amp;quot; \
    LDFLAGS=&amp;quot;$PHP_LDFLAGS&amp;quot; \
; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Extract PHP source tarball in &lt;code&gt;/usr/src/php&lt;/code&gt; (default dir):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker-php-source extract; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Obtain package building architecture information using &lt;a href="https://manpages.debian.org/buster/dpkg-dev/dpkg-architecture.1.en.html"&gt;&lt;code&gt;dpkg-architecture&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;gnuArch=&amp;quot;$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)&amp;quot;; \
debMultiarch=&amp;quot;$(dpkg-architecture --query DEB_BUILD_MULTIARCH)&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://wiki.debian.org/Multiarch"&gt;From the Debian wiki:&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Multiarch is the term being used to refer to the capability of a system to install and run applications of multiple different binary targets on the same system.&lt;/p&gt;
&lt;p&gt;Multiarch also simplifies cross-building, where foreign-architecture libraries and headers are needed on a system during building.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://wiki.debian.org/CrossBuildPackagingGuidelines"&gt;From the Debian wiki:&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;BUILD is the machine we are building on&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/docker-library/php/pull/451"&gt;This is use to avoid the disparity between GNU_TYPE and MULTIARCH&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DEB_BUILD_GNU_TYPE=i586-linux-gnu
DEB_BUILD_MULTIARCH=i386-linux-gnu&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Avoid a &lt;a href="https://bugs.php.net/bug.php?id=74125"&gt;PHP compilation bug&lt;/a&gt; where curl development headers are not found.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if [ ! -d /usr/include/curl ]; then \
    ln -sT &amp;quot;/usr/include/$debMultiarch/curl&amp;quot; /usr/local/include/curl; \
fi; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Execute &lt;code&gt;./configure&lt;/code&gt; to setup compilation using &lt;a href="https://www.gnu.org/software/automake/manual/html_node/Autotools-Introduction.html#Autotools-Introduction"&gt;Autotools&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;define the architecture we want to build PHP for (ie. this system).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--build=&amp;quot;$gnuArch&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;define PHP configuration paths:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--with-config-file-path=&amp;quot;$PHP_INI_DIR&amp;quot; \
--with-config-file-scan-dir=&amp;quot;$PHP_INI_DIR/conf.d&amp;quot; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;enable some extensions which can't be easily (or at all) compiled after having built PHP: &lt;code&gt;mhash&lt;/code&gt;, &lt;code&gt;ftp&lt;/code&gt;, &lt;code&gt;mbstring&lt;/code&gt;, &lt;code&gt;mysqlnd&lt;/code&gt;, &lt;code&gt;password-argon2&lt;/code&gt;, &lt;code&gt;sodium&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;libedit&lt;/code&gt;, &lt;code&gt;openssl&lt;/code&gt; and &lt;code&gt;zlib&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;use &lt;a href="https://github.com/php/php-src/commit/6083a387a81dbbd66d6316a3a12a63f06d5f7109"&gt;system SQLite&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--with-pdo-sqlite=/usr \
--with-sqlite3=/usr \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;don't use pcre-jit for &lt;a href="https://en.wikipedia.org/wiki/Linux_on_IBM_Z"&gt;s390x architecture&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$(test &amp;quot;$gnuArch&amp;quot; = &amp;#39;s390x-linux-gnu&amp;#39; &amp;amp;&amp;amp; echo &amp;#39;--without-pcre-jit&amp;#39;) \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;define lib directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--with-libdir=&amp;quot;lib/$debMultiarch&amp;quot; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;add previously defined configure args (apxs2 and disable cgi):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;${PHP_EXTRA_CONFIGURE_ARGS:-} \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Build PHP using all system cores in parallel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;make -j &amp;quot;$(nproc)&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete all generated static libraries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find -type f -name &amp;#39;*.a&amp;#39; -delete; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install generated files in default system location:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;make install; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strip symbols from all generated binaries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find /usr/local/bin /usr/local/sbin -type f -executable -exec strip --strip-all &amp;#39;{}&amp;#39; + || true; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-exec command {} +&lt;/code&gt; variant builds the command line to run by appending the selected file names at the end. This is similar to &lt;code&gt;xargs&lt;/code&gt; and reduces considerably the number of executions. Quoting &lt;code&gt;{}&lt;/code&gt; is recommended when running inside a shell to avoid interpretation.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;|| true&lt;/code&gt; at the end stops the command from failing. This effectively shadows any error running &lt;code&gt;strip&lt;/code&gt; because of binaries without symbols (not really an error).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Clean compilation intermediate files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;make clean; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Copy the sample &lt;code&gt;php.ini&lt;/code&gt; to the default PHP configuration folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp -v php.ini-* &amp;quot;$PHP_INI_DIR/&amp;quot;; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete extracted PHP source folder (&lt;code&gt;/usr/src/php&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cd /; \
docker-php-source delete; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Purge all build dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mark all installed packages as automatically installed discarding any output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-mark auto &amp;#39;.*&amp;#39; &amp;gt; /dev/null; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mark as manually installed the same packages that were marked as manual at the beginning of this step (ie. not including all compilation dependencies) discarding any output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[ -z &amp;quot;$savedAptMark&amp;quot; ] || apt-mark manual $savedAptMark; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mark as manually installed the binary dependencies required for PHP runtime execution (ie. not for compilation). This will stop them from being uninstalled.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;find /usr/local -type f -executable -exec ldd &amp;#39;{}&amp;#39; &amp;#39;;&amp;#39; \
    | awk &amp;#39;/=&amp;gt;/ { print $(NF-1) }&amp;#39; \
    | sort -u \
    | xargs -r dpkg-query --search \
    | cut -d: -f1 \
    | sort -u \
    | xargs -r apt-mark manual \
; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;ldd&lt;/code&gt; against all executable files in &lt;code&gt;/usr/local&lt;/code&gt; to obtain their entire shared library dependency tree. Dependencies are printed in the format &lt;code&gt;libname.so.X =&amp;gt; /full/path/libname.so.X (hex number)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Obtain the full path of every library dependency using &lt;code&gt;awk&lt;/code&gt; matching all lines containing &lt;code&gt;=&amp;gt;&lt;/code&gt; with &lt;code&gt;/=&amp;gt;/&lt;/code&gt; and printing the second last field (&lt;a href="https://www.gnu.org/software/gawk/manual/gawk.html#Fields"&gt;&lt;code&gt;awk&lt;/code&gt; automatically splits each record in fields separated by whitespace&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;For each library dependency path, obtain the package that installed it searching the &lt;em&gt;dpkg&lt;/em&gt; database: &lt;code&gt;xargs -r dpkg-query --search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;-r&lt;/code&gt; in &lt;code&gt;xargs&lt;/code&gt; avoids running the command if the input does not contain any nonblanks.&lt;/li&gt;
&lt;li&gt;As previously seen, mark each package as manually installed to avoid purging it later: &lt;code&gt;xargs -r apt-mark manual&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Remove automatically installed packages that are no longer needed (see command explanation previously in this article):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/docker-library/php/issues/443"&gt;Update &lt;em&gt;pecl&lt;/em&gt; definitions&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pecl update-channels; \
rm -rf /tmp/pear ~/.pearrc; \
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;PECL is &lt;em&gt;"a repository for PHP Extensions, providing a directory of all known extensions and hosting facilities for downloading and development of PHP extensions."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Remove any &lt;a href="https://pear.php.net/"&gt;PEAR (PHP Extension and Application Repository)&lt;/a&gt; local configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test that php runs OK or fail image building otherwise:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;php --version
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Copy all other custom shell scripts inside the image&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;COPY docker-php-ext-* docker-php-entrypoint /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These scripts are located next to the &lt;code&gt;Dockerfile&lt;/code&gt; in &lt;a href="https://github.com/docker-library/php/tree/master/7.2/buster/apache"&gt;the git repository&lt;/a&gt;. Some of them are meant to be used by the image users, while others are internal.&lt;/p&gt;
&lt;h2&gt;Enable sodium PHP extension&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN docker-php-ext-enable sodium
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is an example usage of the &lt;code&gt;docker-php-ext-enable&lt;/code&gt; which is a &lt;a href="https://github.com/docker-library/php/blob/master/7.2/buster/apache/docker-php-ext-enable"&gt;very interesting shell script&lt;/a&gt; that handles enabling one or more PHP extensions.&lt;/p&gt;
&lt;h2&gt;freetype-config bug workaround&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;RUN { echo &amp;#39;#!/bin/sh&amp;#39;; echo &amp;#39;exec pkg-config &amp;quot;$@&amp;quot; freetype2&amp;#39;; } &amp;gt; /usr/local/bin/freetype-config &amp;amp;&amp;amp; chmod +x /usr/local/bin/freetype-config
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As we have already seen, using &lt;code&gt;{}&lt;/code&gt; to group several echo commands to write in a file is a common Dockerfile / sysadmin pattern.&lt;/p&gt;
&lt;h2&gt;Set image entry point&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ENTRYPOINT [&amp;quot;docker-php-entrypoint&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;According to the documentation: &lt;em&gt;"An ENTRYPOINT allows you to configure a container that will run as an executable"&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;exec&lt;/em&gt; form is being used, which is the preferred one (versus the &lt;code&gt;shell&lt;/code&gt; form: &lt;code&gt;ENTRYPOINT command param1 param2&lt;/code&gt;), which makes the run process PID 1, effectively receiving Unix signals (e.g. a SIGTERM sent by a &lt;code&gt;docker stop&lt;/code&gt; command).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/docker-library/php/blob/master/7.2/buster/apache/docker-php-entrypoint"&gt;The very simple script&lt;/a&gt; allows the user to run the &lt;code&gt;apache2-foreground&lt;/code&gt; script (see next steps) with user-defined parameters or to just run any command passed to it (like php or any other executable in the image).&lt;/p&gt;
&lt;h2&gt;Define stop signal&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;STOPSIGNAL SIGWINCH
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://httpd.apache.org/docs/2.4/en/stopping.html"&gt;SIGWINCH is the recommended signal to use for gracefully stopping Apache&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Copy apache2-foreground script&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;COPY apache2-foreground /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/docker-library/php/blob/master/7.2/buster/apache/apache2-foreground"&gt;This script&lt;/a&gt; handles the setup required to launch Apache in the foreground. All params received will be passed to the main apache2 process in the last line of the script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;exec apache2 -DFOREGROUND &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;exec&lt;/code&gt; avoids undesirable resident shell processes, as would be the case if using Apache's &lt;code&gt;apache2ctl&lt;/code&gt; script.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Set the working directory&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;WORKDIR /var/www/html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This affects only Dockerfile instructions following this line, in this case, it will affect the &lt;code&gt;CMD&lt;/code&gt; final instruction.&lt;/p&gt;
&lt;h2&gt;Expose port 80&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;EXPOSE 80
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Inform Docker that this container will listen to port 80 at runtime (by default, a TCP port). It's important to note that this instruction does not publish the port, it's seen as a kind of documentation between the image creators and the person running the image.&lt;/p&gt;
&lt;p&gt;Port publishing is done on &lt;code&gt;docker run&lt;/code&gt; using the &lt;code&gt;-p&lt;/code&gt; or &lt;code&gt;-P&lt;/code&gt; flags.&lt;/p&gt;
&lt;h2&gt;Specify the command to use in an executing container&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;CMD [&amp;quot;apache2-foreground&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because both the &lt;code&gt;CMD&lt;/code&gt; and &lt;code&gt;ENTRYPOINT&lt;/code&gt; instructions are specified with the &lt;code&gt;exec&lt;/code&gt; form, this instruction is defining default parameters to &lt;code&gt;ENTRYPOINT&lt;/code&gt;, which fits nicely with the &lt;code&gt;docker-php-entrypoint&lt;/code&gt; script.&lt;/p&gt;</content><category term="posts"></category><category term="php"></category><category term="linux"></category><category term="docker"></category></entry><entry><title>FOSS zombies</title><link href="https://davidalfonso.es/posts/foss-zombies" rel="alternate"></link><published>2019-10-30T00:00:00+01:00</published><updated>2019-10-30T00:00:00+01:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2019-10-30:/posts/foss-zombies</id><summary type="html">&lt;p&gt;My understanding of a FOSS zombie project and what to do about it.&lt;/p&gt;</summary><content type="html">&lt;p&gt;First and foremost, FOSS projects are usually maintained by volunteers that donate their own time to the community. We can only be thankful for that. Nevertheless, it's our right to find ways to devote our time where it will make more impact (whatever that entails for each of us).&lt;/p&gt;
&lt;p&gt;Now imagine you've been contributing for some months to a FOSS project. You've learned its internals to a large extent and your change proposals have been quite consistently merged.&lt;/p&gt;
&lt;p&gt;Someday, you realize that your patches take more and more time to receive attention; what used to take days, now takes weeks. You start feeling a bit frustrated. You have so many ideas to improve the software, but at this pace, your code will hit master within months if you're lucky.&lt;/p&gt;
&lt;p&gt;At this very time, you start noticing things in the project that you didn't pay much attention in the beginning, but which can explain crystal clear why you've got involved in what I'm calling a "FOSS zombie".&lt;/p&gt;
&lt;p&gt;Let's review some signals and tips to avoid falling in the trap.&lt;/p&gt;
&lt;h2&gt;Zombie signs&lt;/h2&gt;
&lt;h3&gt;Inactive or dismantled organization&lt;/h3&gt;
&lt;p&gt;The organization behind the project is not active anymore.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Is the organization owning the project still alive and kicking?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do some research on the main contributors. Do they used to work for/be part of the organization owning the project, but they are not anymore?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;External contributors are the only contributors&lt;/h3&gt;
&lt;p&gt;Most releases are minor/patch versions proposed by external contributors and new features are rare.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Review the changelog and latest commits; are most of them from external authors? What type of changes contain?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do maintainers write new code? Do they only merge other people's code?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try to find plans for new features. Roadmap anywhere?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Maintainers accumulate all responsibilities&lt;/h3&gt;
&lt;p&gt;Maintainers still have all responsibilities even if they haven't made any significant contribution for months/years.&lt;/p&gt;
&lt;p&gt;Nevertheless, they have not vanished; they still provide feedback, merge contributions and release versions.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Find out what level of involvement do the maintainers have in the project?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Are there external developers allowed to accept contributions in the project?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In general, is there any kind of responsibility delegation?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Initial boost&lt;/h3&gt;
&lt;p&gt;The project received much attention at its creation, but in the following years/months it was pretty much inactive.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Use your preferred tool to show git repo statistics and look for tendencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Watch for messages of the project being completed. Many projects will match this pattern but don't seek further development and this is OK.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Open issues and contributions&lt;/h3&gt;
&lt;p&gt;There are many open issues and contributions, which means ideas and developments that didn't go well for whatever reason.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the contribution open/close ratio. Compare it with other succesful projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Useless work and code&lt;/h3&gt;
&lt;p&gt;There are unmerged contributions which have received a lot of feedback and work, but that didn't work out well and now all that code is in the limbo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What to look for?&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Look for open contributions with a number of reviews/comments strangely above normal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look for open contributions opened months/years ago. Find root causes. Try to be critic and impartial.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;It's important to distinguish between a "FOSS zombie" and a project which is simply done. If that's the case, you'll find out easily by reading the documentation.&lt;/p&gt;
&lt;p&gt;Now that we can identify a "FOSS zombie" and tell the difference from an abandoned/unmaintained one, let's realize that we are not obliged to contribute to it; there are always alternatives:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find a similar project with a more active community.&lt;/li&gt;
&lt;li&gt;Talk to the maintainers in case they are willing to support a fork or hand-off.&lt;/li&gt;
&lt;li&gt;Create a fork and start improving and using it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In my opinion, "FOSS zombies" lead to confusion to potential contributors. If the maintainer(s) don't want to work anymore in the project, which is perfectly fine, &lt;a href="https://drewdevault.com/2018/12/04/How-to-abandon-a-FLOSS-project.html"&gt;they'd be better off letting go of it&lt;/a&gt;. Otherwise, contributors might get quickly burned out and the project will miss opportunities to grow and improve.&lt;/p&gt;</content><category term="posts"></category><category term="foss"></category></entry><entry><title>Book review: How to Become an Expert Software Engineer</title><link href="https://davidalfonso.es/posts/become-expert-software-engineer-book" rel="alternate"></link><published>2019-08-12T00:00:00+02:00</published><updated>2019-08-12T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2019-08-12:/posts/become-expert-software-engineer-book</id><summary type="html">&lt;p&gt;My review of the book "How to Become an Expert Software Engineer", written by Marcus Tomlinson.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="How to Become an Expert Software Engineer cover" src="/images/book-expert-software-engineer.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Title&lt;/strong&gt;: &lt;a href="https://www.goodreads.com/book/show/35114399-how-to-become-an-expert-software-engineer-and-get-any-job-you-want"&gt;How to Become an Expert Software Engineer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subtitle&lt;/strong&gt;: A programmer's guide to the secret art of free and open source software development.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Author&lt;/strong&gt;: Marcus Tomlinson&lt;/p&gt;
&lt;h2&gt;Review&lt;/h2&gt;
&lt;p&gt;The book is oriented towards developers just starting their careers or who are not familiar with open source and the typical tools. The author proposes open source as the main way to obtain experience for your résumé. It is divided into four parts: Define, Design, Develop, and Deliver. &lt;/p&gt;
&lt;p&gt;The first part (Define) seeks to inspire the reader to work for the job they want, and also describes a systematic process to find what technologies and skills you should learn and practice.&lt;/p&gt;
&lt;p&gt;The second part (Design) dives right into how to create an open source project that matches the technologies and subdomains that you identified in the first part. The author seems especially interested in creating a &lt;em&gt;popular&lt;/em&gt; open-source project.&lt;/p&gt;
&lt;p&gt;The third part (Develop) deals with solo project management, productivity, and motivation and explains some API design best practices. It's like a summary of multiple disciplines, but it only manages to point out the most obvious perils along the way.&lt;/p&gt;
&lt;p&gt;The last part (Deliver) explains how to handle software releases and market your project. It is the one I found more irrelevant, as the initial goal (finding your career path) fades in the distance of the first chapters.&lt;/p&gt;
&lt;p&gt;Summing up, the book is not long and can be read in a few hours. It touches many subjects, although very lightly. Nevertheless, it can be a good reference for new developers who are not aware of the multiple elements that are involved in software engineer projects. Some chapters might be motivating to a more experienced developer, even if you don't pick up many new things.&lt;/p&gt;
&lt;h2&gt;Key Insights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Proving your proficiency in the job you’re asking for is by far the most convincing argument you can make.&lt;/li&gt;
&lt;li&gt;Hard work spent doing something worthless will always be worthless no matter how hard you work.&lt;/li&gt;
&lt;li&gt;The trick is to find a job that will be a perfect fit for you, then get to work on making yourself perfectly fit for it.&lt;/li&gt;
&lt;li&gt;Contribute good free software that is well-written and helpful to ourselves and to others.&lt;/li&gt;
&lt;li&gt;Expertise is usually measured in the time you’ve spent deliberately practicing:&lt;ul&gt;
&lt;li&gt;20 hours to learn almost anything and reach a decent level of proficiency.&lt;/li&gt;
&lt;li&gt;10,000 hours to become an expert in almost anything.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Create a prioritized list of job requirements with technologies and subdomains in order to decide what to focus on.&lt;/li&gt;
&lt;li&gt;Manage your project like you would manage a work project: do research, split tasks, define good APIs, implement quality tests, estimate tasks, etc.&lt;/li&gt;
&lt;li&gt;Always consider research time when estimating tasks and rate it according to your relevant experience.&lt;/li&gt;
&lt;li&gt;Plan your project to keep your motivation high and start with the minimum set of must-do features.&lt;/li&gt;
&lt;li&gt;Try to focus exclusively on solving just the immediate problem at hand, be mindful of how long you’re spending on it, and postpone any divergent issues to separate tasks for later.&lt;/li&gt;
&lt;li&gt;Put a few hobbies on hold for a while, work whenever you can find the time, and try to avoid taking breaks that are longer than a week, at least until you reach your project’s first release.&lt;/li&gt;
&lt;li&gt;Motivation is key: All you need to create good software is a personal interest in seeing the problem solved.&lt;/li&gt;
&lt;li&gt;Design and test your interfaces before implementing them.&lt;/li&gt;
&lt;/ul&gt;</content><category term="posts"></category><category term="book-review"></category><category term="foss"></category><category term="software-engineering"></category></entry><entry><title>Watson: Codebase review</title><link href="https://davidalfonso.es/posts/codebase-watson" rel="alternate"></link><published>2019-05-27T00:00:00+02:00</published><updated>2019-05-27T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2019-05-27:/posts/codebase-watson</id><summary type="html">&lt;p&gt;Code review of Watson, a CLI time tracker built with Python.&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Project information&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailordev.github.io/Watson/"&gt;Official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailordev.github.io/Watson/contributing/how-to/"&gt;How to contribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/TailorDev/Watson"&gt;Github project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Python version support&lt;/h2&gt;
&lt;p&gt;At first, I had installed the Debian 10 packages (&lt;a href="https://packages.debian.org/buster/python3-watson"&gt;python3-watson&lt;/a&gt; and &lt;a href="https://packages.debian.org/buster/watson"&gt;watson&lt;/a&gt;), but I wanted to review and use the latest version, preferably in a virtualenv, so I decided to get the latest code from the GitHub repository.&lt;/p&gt;
&lt;p&gt;Just by looking at the Debian package names you can notice that Watson supports Python 3, but does it also support Python 2?&lt;/p&gt;
&lt;p&gt;Let's take a look at the &lt;code&gt;tox.ini&lt;/code&gt; file which is used for running tests in multiple python environments:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[tox]
envlist = flake8,py27,py34,py35,py36,py37
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can see that Watson effectively supports python 2.7 which is pretty typical these days.&lt;/p&gt;
&lt;h2&gt;Dependencies&lt;/h2&gt;
&lt;h3&gt;Runtime&lt;/h3&gt;
&lt;p&gt;Being packaged in Debian, this means we can just look up for its runtime dependencies in a Debian system:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ apt-cache depends watson python3-watson | awk &amp;#39;/Depends: [^&amp;lt;]/ {print $2}&amp;#39; | xargs dpkg -l | grep ii
ii  python3-arrow    0.12.1-2     all   Python3 library to manipulate dates, times, and timestamps
ii  python3-click    7.0-1        all   Wrapper around optparse for command line utilities - Python 3.x
ii  python3-requests 2.21.0-1     all   elegant and simple HTTP library for Python3, built for human beings
ii  python3-watson   1.6.0-6      all   Library for Watson (Python 3)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arrow.readthedocs.io/en/latest/"&gt;Arrow has its own great documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dbader.org/blog/mastering-click-advanced-python-command-line-apps"&gt;You can find a nice tutorial of Click here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Development&lt;/h3&gt;
&lt;p&gt;Besides this high-level info, there are two &lt;a href="https://docs.python-guide.org/writing/structure/#requirements-file"&gt;requirements files&lt;/a&gt; in the root folder, but why? Let's &lt;code&gt;grep&lt;/code&gt; around and see if we can get more info:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# some results are not shown as seemed irrelevant to me
$ grep -r requirements *
Makefile:   $(PIP) install -r requirements-dev.txt
MANIFEST.in:include requirements-dev.txt
MANIFEST.in:include requirements.txt
setup.py:    install_requires=parse_requirements(&amp;#39;requirements.txt&amp;#39;),
setup.py:    tests_require=parse_requirements(&amp;#39;requirements-dev.txt&amp;#39;),
docs/contributing/hack.md:        $ pip install -r requirements-dev.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As for the &lt;code&gt;requirements-dev.txt&lt;/code&gt;, we can see tools for running the tests, generating the documentation, and more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flake8&lt;/code&gt;, which wraps pep8, pyflakes, mccabe and more, to check the quality and style of python code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdocs&lt;/code&gt;, to build the docs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mock&lt;/code&gt;, for creating mocks for testing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;py&lt;/code&gt;, a development support library, which is now deprecated.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest&lt;/code&gt;, a (simpler than unittest) testing framework, along with some extensions (datafiles, mock and runner)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tox&lt;/code&gt;, which aims to standardize testing in python&lt;/li&gt;
&lt;li&gt;&lt;code&gt;twine&lt;/code&gt;, a utility for publishing python packages on PyPI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Folder organization&lt;/h2&gt;
&lt;p&gt;The file structure is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docs/&lt;/code&gt;: markdown documentation; some of it is automatically generated with &lt;code&gt;make docs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/&lt;/code&gt;: auxiliary scripts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/&lt;/code&gt;: tests, mainly unit tests.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;watson/&lt;/code&gt;: source code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Root dir&lt;/code&gt;:&lt;ul&gt;
&lt;li&gt;travis ci&lt;/li&gt;
&lt;li&gt;testing with tox&lt;/li&gt;
&lt;li&gt;setup config for package generation&lt;/li&gt;
&lt;li&gt;Makefile for automation&lt;/li&gt;
&lt;li&gt;shell completion scripts&lt;/li&gt;
&lt;li&gt;python package requirements&lt;/li&gt;
&lt;li&gt;license&lt;/li&gt;
&lt;li&gt;other docs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Code organization&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ cloc --by-file watson
--------------------------------------------------------------------------------
File                              blank        comment           code
--------------------------------------------------------------------------------
watson/cli.py                       237            445            848
watson/watson.py                    110             50            395
watson/fullmoon.py                    7              6            220
watson/utils.py                      53             63            168
watson/frames.py                     38              0            117
watson/config.py                     33             47             32
watson/__main__.py                    1              0              5
watson/__init__.py                    1              0              3
watson/version.py                     1              1              1
--------------------------------------------------------------------------------
SUM:                                481            612           1789
--------------------------------------------------------------------------------

$ cloc --by-file tests
------------------------------------------------------------------------------------
File                                  blank        comment           code
------------------------------------------------------------------------------------
tests/test_watson.py                    251             44            570
tests/test_utils.py                      56              4            170
tests/test_config.py                     24             47             82
tests/__init__.py                        15              1             28
tests/test_fullmoon.py                    6              1             19
tests/conftest.py                         6              1              8
------------------------------------------------------------------------------------
SUM:                                    358             98            877
------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;You can run all tests in all available Python environments in your system like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ tox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Watson uses &lt;code&gt;pytest&lt;/code&gt; along with &lt;code&gt;unittest.mock&lt;/code&gt; to do unit testing.&lt;/p&gt;
&lt;p&gt;Let's understand some testing constructs used in Watson:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mock.patch.object()&lt;/code&gt;: patch the named member (attribute) on an object (target) with a mock object.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# In tests/__init__.py
# path.object(target, attribute, new) with new being the new object to replace the target
return mock.patch.object(dt_module, &amp;#39;datetime&amp;#39;, MockedDateTime)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@pytest.mark.parametrize()&lt;/code&gt;: enables parametrization of arguments for a test function &lt;a href="https://docs.pytest.org/en/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.pytest.org/en/latest/fixture.html"&gt;Pytest fixtures&lt;/a&gt;: some functions receive pre-filled arguments, like &lt;code&gt;watson&lt;/code&gt;, &lt;code&gt;mock&lt;/code&gt;, or &lt;code&gt;config_dir&lt;/code&gt;. &lt;a href="https://docs.pytest.org/en/latest/fixture.html#fixtures-as-function-arguments"&gt;These are fixtures used as function arguments&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ pytest --fixtures
--------------------------------------- fixtures defined from pytest_datafiles ----------------------------------------
datafiles
    pytest fixture to define a &amp;#39;tmpdir&amp;#39; containing files or directories
    specified with a &amp;#39;datafiles&amp;#39; mark.

------------------------------------------ fixtures defined from pytest_mock ------------------------------------------
mocker
    return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.
mock
    Same as &amp;quot;mocker&amp;quot;, but kept only for backward compatibility.

---------------------------------------- fixtures defined from tests.conftest -----------------------------------------
watson
    tests/conftest.py:14: no docstring available
config_dir
    tests/conftest.py:9: no docstring available

--------------------------------------- fixtures defined from tests.test_watson ---------------------------------------
json_mock
    tests/test_watson.py:31: no docstring available
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Contributing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailordev.github.io/Watson/contributing/how-to/"&gt;How to contribute?&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailordev.github.io/Watson/contributing/hack/"&gt;Quick hacking reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailordev.github.io/Watson/contributing/pr-guidelines/"&gt;Pull Request guidelines&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html"&gt;Git commit messages recommended read&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/book/en/v2/Git-Branching-Rebasing"&gt;Rebase your branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html"&gt;Squash your commits if required&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content><category term="posts"></category><category term="codebases"></category><category term="python"></category></entry><entry><title>Migrating from TiddlyWiki to Markdown</title><link href="https://davidalfonso.es/posts/tiddlywiki-to-markdown" rel="alternate"></link><published>2018-07-04T00:00:00+02:00</published><updated>2018-07-04T00:00:00+02:00</updated><author><name>David</name></author><id>tag:davidalfonso.es,2018-07-04:/posts/tiddlywiki-to-markdown</id><summary type="html">&lt;p&gt;Using TiddlyWiki5 (nodejs) and pandoc to transform old TiddlyWiki's tiddlers to markdown files.&lt;/p&gt;</summary><content type="html">&lt;p&gt;After many years of using &lt;a href="https://tiddlywiki.com/"&gt;TiddlyWiki&lt;/a&gt; to keep my notes and journal, both personal and work-related, I have decided to migrate to a more simple solution: markdown files.&lt;/p&gt;
&lt;h2&gt;What is TiddlyWiki?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://tiddlywiki.com/#TiddlyWiki5"&gt;TiddlyWiki 5&lt;/a&gt; (aka, modern TW) was a significant step in its development. The same software supported two platforms: node.js and the browser. A single-file HTML TW could be generated using node.js allowing a myriad of customization options. The client-side part (i.e. HTML/JS) supports multiple &lt;a href="https://tiddlywiki.com/#Saving"&gt;savers&lt;/a&gt;, including the single-file itself or TW running as a backend in node.js. No doubt, TiddlyWiki is an unusual piece of code.&lt;/p&gt;
&lt;h2&gt;Solution design&lt;/h2&gt;
&lt;p&gt;My focus was on &lt;strong&gt;leveraging free and opensource tools&lt;/strong&gt; to build a combination which led to a simple solution to &lt;strong&gt;convert tiddlers to markdown files&lt;/strong&gt;. After reading the development documentation and evaluating several alternatives, including creating a new TW module, I decided to take advantage of TiddlyWiki’s available &lt;a href="https://tiddlywiki.com/#Commands"&gt;commands&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The following block diagram shows the steps, programs and intermediate files that make up the process.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="/images/tiddlywiki_migration_diagram.png"&gt;&lt;/p&gt;
&lt;h2&gt;Solution implementation&lt;/h2&gt;
&lt;p&gt;These are the steps involved in the whole migration process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setup empty TiddlyWiki5&lt;/li&gt;
&lt;li&gt;Load tiddlers&lt;/li&gt;
&lt;li&gt;Render to HTML&lt;/li&gt;
&lt;li&gt;Rename tiddler files&lt;/li&gt;
&lt;li&gt;Convert to Markdown&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Setup empty TiddlyWiki5&lt;/h3&gt;
&lt;p&gt;We initialize an &lt;a href="https://tiddlywiki.com/#Empty%20Edition"&gt;empty edition&lt;/a&gt; TiddlyWiki and add support for markdown and TiddlyWiki’s old format.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$(NODEJS) $(TIDDLYWIKI_JS) $(WIKI_NAME) --init empty
$(NODEJS) $(ADD_PLUGIN_JS) $(TIDDLYWIKI_INFO) tiddlywiki/tw2parser
$(NODEJS) $(ADD_PLUGIN_JS) $(TIDDLYWIKI_INFO) tiddlywiki/markdown
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Load tiddlers&lt;/h3&gt;
&lt;p&gt;We can use the  &lt;a href="https://tiddlywiki.com/#LoadCommand"&gt;load command&lt;/a&gt; to load the tiddlers from several sources into a TiddlyWiki. One of these sources is a single-file html file (classic or modern):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--load wiki.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Render to HTML&lt;/h3&gt;
&lt;p&gt;TW5 has a powerful &lt;a href="https://tiddlywiki.com/#RenderCommand"&gt;render command&lt;/a&gt; that allows to render individual tiddlers, filter its contents and save the results in the filesystem. In our case, we render tiddler content and metadata in separate files to facilitate the conversion process.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;--render [!is[system]] [encodeuricomponent[]addsuffix[.html]] \
--render [!is[system]] [encodeuricomponent[]addsuffix[.meta]] \
    text/plain $$:/core/templates/tiddler-metadata
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[!is[system]]&lt;/code&gt; avoids rendering system tiddlers (99% of TW is built using its own tiddler concept).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;encodeuricomponent[]&lt;/code&gt; encodes the tiddler name to avoid filesystem name issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Important&lt;/em&gt;: Loading and rendering has to be done in the same command-line execution to avoid having to persist tiddlers to disk.&lt;/p&gt;
&lt;h3&gt;Rename tiddler files&lt;/h3&gt;
&lt;p&gt;I want my markdown file names to be as much compatible &lt;a href="https://superuser.com/questions/358855/what-characters-are-safe-in-cross-platform-file-names-for-linux-windows-and-os"&gt;as&lt;/a&gt; &lt;a href="https://serverfault.com/questions/432514/what-character-can-be-safely-used-for-naming-files-on-unix-linux"&gt;possible&lt;/a&gt; with any filesystem. This is implemented in this &lt;a href="https://github.com/davidag/tiddlywiki-migrator/raw/master/scripts/safe-rename.js"&gt;simple Node script&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;// Remove accents/diacritics
.normalize(&amp;#39;NFD&amp;#39;).replace(/[\u0300-\u036f]/g, &amp;quot;&amp;quot;)
// Make lowercase
.toLowerCase()
// Convert separators to low line
.replace(/\s+/g, &amp;#39;_&amp;#39;)
// Remove any non-safe character
.replace(/[^0-9a-zA-Z-._]/g, &amp;quot;&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Convert to Markdown&lt;/h3&gt;
&lt;p&gt;Metadata is prefixed at beginning of the Markdown files using a format equivalent to &lt;a href="http://assemble.io/docs/YAML-front-matter.html"&gt;YFM (YAML Front Matter)&lt;/a&gt;. Tiddlers in HTML are converted to &lt;a href="http://commonmark.org/"&gt;CommonMark&lt;/a&gt; using &lt;a href="https://pandoc.org/"&gt;pandoc (the universal document converter)&lt;/a&gt;. This variant of Markdown was the one that produced more simple results in my tests.&lt;/p&gt;
&lt;p&gt;It’s interesting to see how the &lt;a href="https://www.gnu.org/software/make/"&gt;GNU Make&lt;/a&gt; utility is used to convert all files using one rule:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$(MARKDOWN_DIR)/%.md : $(TW_OUTPUT_DIR)/%.html
    @echo &amp;quot;Generating markdown file &amp;#39;$(@F)&amp;#39;...&amp;quot;
    @echo &amp;quot;---&amp;quot; &amp;gt; &amp;quot;$@&amp;quot;
    @cat &amp;quot;$(^:html=meta)&amp;quot; &amp;gt;&amp;gt; &amp;quot;$@&amp;quot;
    @echo &amp;quot;---&amp;quot; &amp;gt;&amp;gt; &amp;quot;$@&amp;quot;
    @$(PANDOC) -f html-native_divs-native_spans -t commonmark \
        --wrap=preserve -o - &amp;quot;$^&amp;quot; &amp;gt;&amp;gt; &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As html files don’t exist until this final step, make execution must be done in two separate commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$ make # or &amp;#39;make export-html&amp;#39;
$ make convert
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;I am quite happy with the final result. This little project took me three full days, including evaluating other design options and learning a bit about the internals of TiddlyWiki (&lt;a href="https://tiddlywiki.com/dev/"&gt;worth a look!&lt;/a&gt;). It’s amazing how using free and opensource software allows you to put together powerful applications with a few lines of code.&lt;/p&gt;</content><category term="posts"></category><category term="tiddlywiki"></category><category term="markdown"></category><category term="nodejs"></category></entry></feed>