Moncef Belyamani

Licensing and Distributing a Paid CLI With Ruby, Rails, and SwiftUI

Licensing and Distributing a Paid CLI With Ruby, Rails, and SwiftUI

by Moncef Belyamani

In the presentation "Licensing and Distributing a Paid CLI With Ruby, Rails, and SwiftUI" at Rocky Mountain Ruby 2023, Moncef Belyamani explores the journey of developing his product, Ruby on Mac, which simplifies the Ruby installation process on Mac devices. He reflects on the challenges developers face while installing Ruby and gems, particularly on macOS, drawing from personal experiences and testimonials from users who have encountered similar issues. Through his talk, he outlines a detailed process for implementing a licensing system to manage the distribution of his CLI product, covering various technical tools and strategies utilized throughout the development process.

Key points discussed include:
- Initial Challenges with Ruby Installation: Moncef highlights the common difficulties developers face when trying to install Ruby on a Mac, indicating that many users still struggle with this task years later.
- Introduction of Ruby on Mac: He shares how he created Ruby on Mac to provide an efficient and user-friendly installation process, aiming to reduce the setup time significantly.
- Licensing System Development: Moncef explains his process for designing a licensing system that enforced limitations on product usage, specifically discussing a tiered pricing structure with different functionalities.
- Technical Tools: He describes the various technical components involved in creating the licensing system, including the use of tools like Ruby Packer, SHC, and Apple's DeviceCheck API for validating licenses.
- Building a Seamless User Experience: The development of a macOS app for generating tokens without compromising user experience is discussed as a significant part of the process.
- Deployment Challenges: He details real-world challenges faced after deployment, including issues with unsupported devices and antivirus flagging false positives on binaries, and how he addressed them.

Significant anecdotes include Moncef's nostalgia for the early struggles of his journey as a developer, along with testimonials from users who appreciate the improvements Ruby on Mac has made in their development workflows.

In conclusion, Moncef emphasizes the importance of learning from each step in the process and continuously improving the user experience. His talk offers valuable insights into managing software licensing while ensuring ease of distribution and installation, showcasing a successful integration of technical skill and user-centric design in software development.

00:00:14.040 All right, hi everybody! My name is Moncef Belyamani.
00:00:19.320 I love music and Ruby, but I'm here to talk about Ruby today.
00:00:26.279 To understand what led me here today, we need to go back to March 22nd, 2012.
00:00:31.400 I was trying to install Octopress to start my dev blog. Octopress is a gem static site generator based on Jekyll.
00:00:37.719 It required Ruby 1.9.3, which failed to install using RVM.
00:00:42.920 I don't think Wayne is here; I saw him leave earlier, but he's the creator of RVM.
00:00:48.199 I wanted to acknowledge his contributions. I was going to say thank you, Wayne, but I don't think he's here.
00:00:53.800 So, I spent the next few hours troubleshooting until I found a recipe for installing Ruby on a Mac.
00:01:00.800 That was my very first blog post. Thanks to the Internet Archive, we can see what it looked like.
00:01:06.040 Fast forward eleven years, and the problem remains. Every day, hundreds of people are still having trouble installing Ruby on a Mac.
00:01:11.080 Raise your hand if you've ever had trouble installing Ruby or a gem on a Mac. I see quite a lot of hands.
00:01:16.119 If you open your ears, this is what it sounds like when devs cry.
00:01:22.479 If only I could get back those ten hours spent trying to install Ruby 2.6.10 on my MacBook Pro M1.
00:01:28.320 I spent around three days trying to solve issues with my Rails app.
00:01:34.680 I looked, tried, and retried so many possible solutions from countless sources on the web, but nothing was working.
00:01:40.360 I spent probably four hours trying to recreate the same setup as before, and it just never worked.
00:01:47.719 Are you feeling the pain? Now, here's some more from GitHub.
00:01:54.240 To be clear, this is not necessarily Ruby's fault, nor the fault of the various version managers.
00:02:00.880 There are many factors that can affect Ruby installation on a Mac.
00:02:06.280 Going into all of these factors could be a talk on its own.
00:02:12.200 For now, I just want to point out that there are many factors more than can fit on this slide.
00:02:17.440 This is why a solution that might work for one person might not work for another.
00:02:23.440 Do any of you have one of these version managers set globally in your shell startup file? Raise your hand.
00:02:30.120 I see a few hands, and I bet some of you have one or more of these set right now.
00:02:36.760 Here's a public service announcement: having these set globally in your environment can cause not only Ruby but other tools to fail to install.
00:02:43.959 Please don't do it. As we saw in the previous slide, this is only one of many factors that can mess up Ruby installation.
00:02:50.440 Most people experiencing trouble are trying to install Ruby in an existing dev environment.
00:02:55.959 However, most version managers and various automation scripts assume you're starting with a fresh macOS installation.
00:03:01.480 They expect you to have certain prerequisites installed.
00:03:07.680 Some of these tools are more helpful than others.
00:03:13.280 Some install some or all of the prerequisites for you.
00:03:19.519 However, none can handle a broken dev setup, and few optimize the Ruby compilation settings.
00:03:26.280 That's where I saw an opportunity. I thought, if I can help people with music, maybe I can help them with code.
00:03:33.120 So, I gave it a try, and I'm pretty happy with the results.
00:03:40.920 I'll let you read some of the testimonials I've received. There are over a hundred more like these, which have uplifted me.
00:03:51.120 It feels great to help people like this.
00:03:57.280 My product, Ruby on Mac, provides a painless Ruby development setup in 15 minutes or less.
00:04:03.840 The best part? The ultimate version works with all version managers.
00:04:10.079 It also installs Ruby faster than any other version manager, being twice as fast as RVM and others.
00:04:16.280 Another unique thing is that it makes it easy to install Ruby versions as old as 2.1.
00:04:22.120 On top of that, it'll save you at least half a day when you're setting up a new Mac.
00:04:29.080 Whether you just bought one or started a new job, you can also set up all your Mac apps, fonts, Git preferences,
00:04:35.400 and even macOS preferences and all the repos you need to clone.
00:04:41.039 As I mentioned, people find me because they have a broken dev setup.
00:04:47.039 I have a reset mode that safely backs up your dev setup, then cleans it up in one minute.
00:04:52.240 Then, you can reinstall everything from scratch with a clean slate.
00:04:58.400 My promise is that you won't ever have to waste time troubleshooting Ruby installation issues or remembering which settings to use.
00:05:03.560 In the worst-case scenario, you'll be back up and running in 15 minutes.
00:05:09.240 Now that we have this context, let's dive into the licensing.
00:05:16.960 Since launching Ruby on Mac in February of last year, I experimented with various product tiers and pricing.
00:05:22.440 One of the changes I made about a year ago was to add limitations to the lower tiers.
00:05:29.560 For example, Prime customers only get one year of free updates and can only use the product on one Mac at a time.
00:05:35.240 When I first decided to add the one Mac limit, it was as simple as updating the pricing page.
00:05:41.160 I had a whole year to figure out how to implement it.
00:05:46.199 However, I couldn't mention the limitation of one Mac at a time until I had a way to enforce it.
00:05:52.080 I didn't want to deceive my customers.
00:05:57.919 To think this through, I started writing down my thoughts in an Obsidian note.
00:06:03.880 This is a habit I picked up about two years ago when I quit my job, and I wish I had started it earlier.
00:06:10.960 Now, I document anything worth remembering about my life, which has come in very handy.
00:06:17.240 For technical issues, I write down as much detail as possible about what happened, what I tried, and how I fixed it.
00:06:22.280 This serves as content for talks like this one or blog posts.
00:06:28.520 I also often record a screencast of myself figuring things out. In this case, writing down my thoughts served as rubber duck debugging.
00:06:36.440 That Obsidian note now has 2,364 words.
00:06:42.880 I started with a small list of requirements and began writing down how I might implement the licensing in the simplest way.
00:06:49.400 Here’s what the end-to-end flow looked like at the time.
00:06:55.039 The customer purchases Ruby on Mac, and the order is processed by Paddle, my Merchant of record.
00:07:00.599 Paddle then emails the product to the customer, which at the time was just a zip file containing all the source code.
00:07:07.680 It included instructions to run a specific script.
00:07:14.720 At the same time, Paddle sent an event webhook to my Rails app.
00:07:20.520 Initially, it was just a basic API Rails app with maybe just one controller to handle this.
00:07:27.120 Then, it populated the purchase details into my ConvertKit account.
00:07:33.000 I started talking to myself by writing down whatever came to my mind, welcoming all ideas.
00:07:39.720 Soon, I came to the conclusion that I would need some kind of license file.
00:07:45.000 I also needed a Ruby script that verifies the license, compiled as a self-contained binary for security.
00:07:50.759 Since Ruby on Mac was mostly written in Bash at the time, the main Bash script would call the Ruby script.
00:07:56.440 The installation would proceed only if the license is valid.
00:08:01.800 This meant the Bash script needed to be obfuscated, which if not possible, would require a complete rewrite in Ruby.
00:08:09.520 I realized necessary next steps included searching for Bash and Ruby compilation tools.
00:08:16.199 For shell scripts, I found an open-source project called SHC.
00:08:22.560 It encodes and encrypts your shell script, generating C source code.
00:08:30.599 Then, it uses a system compiler to create a stripped binary.
00:08:37.039 When the binary runs, it decrypts and executes the original script.
00:08:42.399 I looked at the readme and noticed a -u option to create an untraceable binary. I thought it may be more secure.
00:08:48.279 I gave that a try, but the resulting binary didn't work. No big deal—the regular binary was fine for now.
00:08:55.120 Next, I needed to see if the binary could run on my Intel iMac, as it was originally created on my M1.
00:09:02.480 I copied the file from the M1 to the Intel iMac, but got a bad CPU type error, which made sense.
00:09:08.600 Apple Silicon is not compatible with Intel binaries, though it's the other way around.
00:09:15.160 Then I tried generating it directly on the Intel iMac, and that worked.
00:09:21.240 The next step was to confirm that the binary could work on a Mac other than the one where it was generated.
00:09:27.760 I copied the binary from my M1 to my Intel Mini and got the lovely output.
00:09:33.240 When things go wrong with an open-source tool, I usually start with GitHub issues.
00:09:40.279 If it's a CLI, I check the help menu or man page, as there are often things there not documented.
00:09:46.880 In this case, I needed the DHR option for relaxing the security to make a redistributable binary.
00:09:53.800 Now, I had the shell script part figured out. I could create a binary.
00:09:58.920 Now it was time to find a Ruby compiler. While researching, I found several attempts that were mostly unmaintained.
00:10:06.000 Many didn't work on Apple Silicon or newer Ruby versions, which is understandable.
00:10:12.120 The Ruby team does have something called MRuby, but it didn’t support my specific use case.
00:10:17.959 Next, I found a project called Warbler, which is maintained, but works only with JRuby.
00:10:23.200 It produces binaries meant to run with Java, but I wanted to minimize dependencies.
00:10:29.880 I kept searching and found Traveling Ruby, but its last update was almost three years ago and only supports Ruby 2.4.
00:10:36.560 Installing Ruby 2.4 requires Rosetta on an Apple Silicon Mac.
00:10:41.360 Even though Ruby on Mac makes this super easy, I prefer not to use Rosetta.
00:10:46.079 I then found Ruby Packer, introduced in late 2016. It takes a different approach to packaging Ruby.
00:10:52.760 It supplies a patched Ruby, compiles it locally with native extensions, and packs it into a squash filesystem.
00:10:58.480 Ruby is patched to look inside both the squash FS and the real filesystem.
00:11:05.880 The pro is that it’s a single file that works with all native extensions.
00:11:11.200 But the downside is that there's no cross-packaging support.
00:11:17.200 There's no access to generate binaries for Windows and Linux, but I only care about Mac.
00:11:23.720 I looked up the original Ruby Packer repo, but it was last updated three years ago and only supported Ruby 2.7.1.
00:11:29.680 The oldest version you can install on Apple Silicon is Ruby 2.7.2.
00:11:35.999 Luckily, I found a more recent fork by Eric Belland, which added support for Ruby 3.1.3.
00:11:42.560 I compiled a simple Ruby script that made an HTTPS call to Ruby on Mac.
00:11:48.079 I generated the binary, which worked, but then I got an SSL error when I tried to run it.
00:11:54.720 To troubleshoot, I looked for references to OpenSSL in Ruby Packer's source code and tested changes one by one.
00:12:00.880 I first set the OpenSSL dir option to point to Homebrew's OpenSSL 1.1 directory, but it didn't work.
00:12:07.200 Then I realized I needed to point it to the directory where the certificate exists.
00:12:12.480 After some trial and error, I found the correct directory.
00:12:18.200 Then I found the compiler.rb file and compared OpenSSL configuration in Ruby Packer with Homebrew.
00:12:24.560 Homebrew makes this easy via Brew info, showing you compilation arguments.
00:12:31.080 Interestingly, they add additional arguments specifically for macOS.
00:12:37.080 Doing this significantly speeds up performance.
00:12:43.360 I made this change, and it worked.
00:12:49.320 I also removed the implicit function declaration flag from Ruby 3.1.3, as it was unnecessary and dangerous.
00:12:55.120 With those changes, everything worked, but then I realized a potential issue.
00:13:01.080 If the binary requires OpenSSL to be pre-installed, this poses a problem since Ruby on Mac is intended to run on fresh installs.
00:13:07.640 To test this, I ran Ruby on Mac's reset mode on my M1 Mini to simulate a fresh dev environment.
00:13:14.320 As expected, when testing the binary, it failed because the necessary certificates were missing.
00:13:19.960 I searched my Mac for cert.pem, as I knew that was the certificate file.
00:13:26.120 Finding it in private/etc/ssl confirmed my suspicion—macOS has everything stored there.
00:13:32.920 I confirmed this by running OpenSSL version -a on a Mac that only had the default OpenSSL installation.
00:13:42.200 This ensured I knew where the certificate needed to be.
00:13:48.480 Now we can generate the binary pointing to this new location, and it runs fine on a brand new Mac.
00:13:54.880 I then upgraded OpenSSL from 1.1 to 3.1, as Ruby 3.1 and higher support OpenSSL 3.
00:14:01.440 Upgrading was straightforward; I downloaded the latest version from the OpenSSL website.
00:14:07.840 I unzipped it and placed the contents in Ruby Packer's vendor directory.
00:14:14.720 Then I regenerated the Ruby C binary and compiled my test script with this new binary.
00:14:21.240 Once I confirmed everything was working, I opened a PR to merge my fixes.
00:14:26.920 It's still open; I pinged Eric, so hopefully it will be merged soon.
00:14:33.680 This is a significant deal for me because generating Ruby binaries has been challenging.
00:14:39.600 Now, thanks to Eric's efforts and my contributions, it should be easier.
00:14:46.080 As we approach the end of life for Ruby 3.1.3, I plan to learn about patching Ruby and migrations.
00:14:53.360 I next wanted to test downloading the binary from my website to simulate customer behavior.
00:15:00.640 When I tried running it, Apple’s Gatekeeper wouldn't allow it.
00:15:07.040 To be verified as a developer, I needed to register for the Apple Developer Program.
00:15:13.440 While reading the terms of service, I discovered I couldn't use a permanent device-based ID to uniquely identify devices.
00:15:21.480 This put a wrench in my plans, as I originally thought I'd track activations using something like serial numbers.
00:15:28.400 After further searching, I found Apple's DeviceCheck API, which I’ll overview later.
00:15:35.360 However, it wasn't clear how to integrate this with my Ruby script.
00:15:44.320 I wanted a frictionless user experience with only one command run in the terminal.
00:15:51.160 I was avoiding creating a macOS app at all costs since it seemed like a lot of work.
00:15:57.640 The first thing I tried was to generate the token via a standalone Swift script.
00:16:03.200 This example was from Apple’s documentation.
00:16:10.200 I added print statements, which confirmed that the isSupported method was called.
00:16:16.120 My Mac was recognized as a supported device; however, token generation produced no output.
00:16:23.000 I thought maybe this function only works within an official app.
00:16:30.000 While creating a new project in Xcode, I noticed the commandline tool option, and thought it could help Apple recognize my application.
00:16:37.320 Unfortunately, that didn't work either.
00:16:43.440 Next, I considered writing a unique UUID to the keychain from a plain Swift script, which worked.
00:16:49.240 However, this prompted a user access request, and I didn’t like the user experience.
00:16:56.000 On a fresh macOS install, running a Swift script prompts installation of command line tools, which isn't ideal.
00:17:04.000 So, I gave in and decided to write the macOS app.
00:17:12.120 All this time wasted could have been avoided if Apple's documentation stated that token generation needs an app to execute.
00:17:19.400 With sample code from GitHub, I found it wasn't bad, and most time was spent figuring the correct build settings.
00:17:26.840 Here’s a sample code for generating the token, which encodes it in base64 and passes a block to send the token to the server.
00:17:34.200 I tested this locally by adding a route to my Rails app and got everything working.
00:17:41.160 However, I realized the macOS app was sending the token to the Rails server and getting a response back.
00:17:47.240 The plan was to write the license verification in a Ruby script, so how would the Ruby script get the Rails server result?
00:17:54.160 I explored creative solutions, such as embedding a command-line tool within the mac app that could relay the result.
00:18:01.200 I found a YouTube video on command line to Mac app communication, but was unable to make it work.
00:18:08.240 Finally, I realized that the only part requiring the macOS app was token generation.
00:18:15.679 If I store the token in a file, everything else could be done in the Ruby script.
00:18:22.160 The NS home directory is special to macOS apps and would avoid cluttering the user's home.
00:18:29.560 With the device check token accessible in a ruby script, we could solve most of our issues.
00:18:38.120 The idea now was to have the Bash script run the Ruby script and continue only if verification passes.
00:18:44.560 However, how could I prevent someone from creating their own executable with the same name as the Ruby script?
00:18:51.000 One requirement for Apple to mark my binary as trustworthy is to code sign it.
00:18:57.040 One command could be used to read the developer ID from the binary.
00:19:04.200 With that, I could verify that the binary indeed came from me.
00:19:10.240 Next, enforcing perpetual fallback licenses was a priority.
00:19:17.200 Customers get one year of updates but should still use older versions after their expiry.
00:19:23.520 To do this, I needed to determine when versions were released and compare this to expiration.
00:19:30.720 I wrote a Ruby script that uses git to generate JSON update/version pairs.
00:19:37.200 This script runs every time a new version of Ruby on Mac is released and gets published to the website.
00:19:45.040 From the license verification script, I fetch this JSON and extract necessary data.
00:19:52.120 However, the version info was stored only in a text file.
00:19:59.080 Someone with an expired license could easily manipulate the text file to use a pre-expiration version.
00:20:05.440 I needed a more robust method; once code-signed, I could specify an arbitrary prefix.
00:20:11.200 Adding the version number to this allowed me to extract it from the binary.
00:20:17.440 Ruby simplifies this process, which is why it's my favorite language.
00:20:24.920 Here's a tip: when running shell commands in scripts, use the full path to the tool.
00:20:32.320 This helps avoid compatibility issues with different tool versions.
00:20:39.440 By specifying '/usr/bin/codesign', you'll only use the version pre-installed on your Mac.
00:20:45.360 With everything in place, I could finally start enforcing licenses.
00:20:51.680 To avoid reinventing the wheel, I searched and found GitLab license gem.
00:20:57.760 Here’s how it works: generate a key pair just once.
00:21:05.040 Then use it to write to a file; the license generation app, which is my Rails app, tracks it.
00:21:12.320 You extract the public key to give the customer alongside Ruby on Mac.
00:21:18.760 In the generation app, load the private key from the file securely.
00:21:26.560 Now, start building the license, populating it with various attributes.
00:21:34.160 Specify start and expiration dates, then export it to a file sent to the customer.
00:21:40.800 In the license verification script running on the customer’s Mac, load the public key from the file and read the license from it.
00:21:47.720 Import it to decode and decrypt the license.
00:21:55.120 Now, let's cover the entire end-to-end flow again.
00:22:02.320 A customer makes a purchase; as before, Paddle sends a webhook.
00:22:08.960 In addition to saving details in ConvertKit, they now are saved to a database.
00:22:17.000 We also need a second webhook from Paddle to another controller generating the license.
00:22:23.960 The system responds to Paddle with a link so they can send it to the customer.
00:22:29.440 However, these Paddle event webhooks are fired simultaneously, which created a timing issue.
00:22:37.680 So, I want to share a Rails tip I learned regarding this.
00:22:44.000 For attaching the license to the customer, the customer must exist first.
00:22:51.760 Since customer creation happens via the webhook, the license controller had to wait.
00:22:57.440 In Rails, various ways to check if a customer exists but they don't all work the same.
00:23:04.200 So, raise your hand if you think this query will work.
00:23:10.080 How about this?
00:23:16.160 Oh, what if we reload?
00:23:22.440 Nope, why? Because Rails caches these queries.
00:23:28.760 If you ask if a customer exists when it hasn't been created, it will return false.
00:23:35.520 This is why the right way to do it is by using uncached and passing in a block.
00:23:41.520 With that addressed, we can generate the license.
00:23:48.560 We store it in S3 with ActiveStorage, then respond to Paddle in plain text containing the license and its link.
00:23:55.840 Paddle emails the customer, who downloads the license and the latest Ruby on Mac release.
00:24:02.960 They run the package installer that places the app generating the token in the Applications folder.
00:24:09.320 The app unzips Ruby on Mac, and the customer can now run the Bash script.
00:24:16.760 The Bash script checks if the license verification binary exists and is signed by me.
00:24:23.480 Finally, it decrypts the license and extracts the email and product.
00:24:30.200 In the background, the app opens, storing the token file.
00:24:37.000 The token is read from the file and stored in a variable.
00:24:43.360 Then we quit the token generation app because we don't need it anymore.
00:24:50.320 Finally, the script sends the token along with the email and product to the Rails app for validation.
00:24:57.240 If everything is valid, we interact with Apple. Here's how this works: you send a request to the query two bits endpoint.
00:25:05.040 This checks Apple's records for the device state.
00:25:12.320 If Apple responds, 'fail to find bit state,' it means Ruby on Mac hasn't run on this Mac yet.
00:25:19.760 We send a request to the update two bits endpoint and set two boolean bits.
00:25:26.240 If, for example, Bit Zero is true and Bit One is false, it indicates the device has been activated.
00:25:32.800 Apple will return the values of those bits on the machine.
00:25:39.760 If Bit Zero is true and Bit One is false, it's already been activated; we can return a success response.
00:25:46.640 The license verification will confirm everything is good and you can use it.
00:25:53.720 This entire process only takes about one second, depending on your internet connection.
00:26:01.120 Yesterday, Joel's talk on accessibility reminded me that Ruby's net HTTP has a long default timeout.
00:26:07.240 By default, it is 60 seconds for both open and read timeout.
00:26:14.960 Setting these much lower is usually recommended. I learned to test for slow reconnections to ensure timeout settings are appropriate.
00:26:23.280 Now, I’ll give you a quick demo; this showcases the license verification.
00:26:30.560 Also, it highlights Ruby on Mac's feature. If you try uninstalling the current version, it will prevent you from doing this.
00:26:37.920 The license checks only take a few seconds, and it won't allow an uninstall during this process.
00:26:44.760 You won't see any of this internal processing; the user experience is smooth.
00:26:51.360 Now that I have tested this end-to-end flow, it was time for deployment.
00:26:58.000 I did a soft roll-out first, targeting my oldest Prime customers.
00:27:05.080 As is typically the case, interesting bugs emerge only after shipping to production.
00:27:12.200 I encountered two issues. First, some customers using older Macs couldn't generate device check tokens.
00:27:19.680 I initially overlooked details in documentation about unsupported devices, particularly those without secure enclave.
00:27:26.160 When I searched thoroughly, I found the details.
00:27:32.760 I modified the token generator app to create a separate file for unsupported devices.
00:27:39.320 I sent a token value of 'unsupported' to the Rails app to skip calls to Apple.
00:27:45.720 This incrementally counts the customer's activations.
00:27:52.200 The second issue involved antivirus software, specifically some quarantining binaries generated with SHC.
00:27:58.720 I was assured by one customer that the virus scanner flagged them as infected, which caused concern.
00:28:05.840 I researched the infection and felt relieved; it was unrelated to my process.
00:28:12.600 I tested numerous antivirus tools, but not a single one flagged any issues.
00:28:20.320 Eventually, the antivirus updated definitions and stopped flagging any issues.
00:28:27.200 However, another customer faced Sentinel One issues, which many companies use.
00:28:34.000 The only solution was for the customer to keep using the older Ruby on Mac or upgrade to the Ultimate version.
00:28:41.760 The good news is that only the bash binaries were rejected, not the Ruby binaries.
00:28:48.160 In fact, rewriting the script from Bash to Ruby has proven beneficial.
00:28:54.720 It allowed me to validate the code and identify several bugs.
00:29:01.760 Although various testing frameworks exist for Bash, none can mock the file system.
00:29:07.680 This makes testing file modification and deletion challenging without affecting my actual file system.
00:29:14.400 In Ruby, this is straightforward using the FakeFS gem.
00:29:20.920 Another advantage is improving user experience.
00:29:27.080 In the previous version of Ruby on Mac CLI, it required Thor installed on every Ruby version, which slowed things down.
00:29:33.680 I had to detect Ruby's current version and check if Thor was installed, then install if needed.
00:29:40.960 Now, as a self-contained binary, it runs regardless of Ruby’s installation or version.
00:29:47.760 This makes it future proof as only the installation script relies on Bash.
00:29:54.640 Everything else is now written in Ruby and run via the self-contained binary.
00:30:01.440 Even if Apple stops including Ruby, the setup will continue to work.
00:30:07.920 That's all I have for you today, but I’d like to leave you with a gift.
00:30:13.760 I have 15 discounts to give out, five for 50% off on the Ultimate version.
00:30:20.839 You can use these coupon codes. But wait, there's more!
00:30:27.840 Anyone who guesses closest to the number of records in my vinyl collection will win a free copy of Ruby on Mac Ultimate.
00:30:35.760 DM me on Slack or check my contact details on the Ruby on Mac website.
00:30:43.840 I'll announce the winner in Slack, and the offers are valid until midnight Sunday.
00:30:50.640 Thank you!
00:30:56.280 Questions? Yes, it's been raised.
00:31:01.680 The question was whether I reached out to Sentinel One regarding their flags on Bash binaries.
00:31:08.640 I have not; I wonder if they would listen to someone like me.
00:31:15.040 It may just relate to how SHC generates binaries, as the Ruby binaries weren't flagged.
00:31:23.560 The question is about maintaining my own version after finding different forks.
00:31:30.240 For now, it’s working, and I might have to learn about patching Ruby as needed.
00:31:36.960 It seems solid at least for my use case as I don't anticipate issues with OpenSSL.
00:31:44.560 I’ll keep feedback channels open so others can try creating their binaries.
00:31:50.720 For now, I'll ping Eric to see about merging; if not, it'll be in my fork.
00:31:57.680 I'll also plan to blog about this and share through various forums.
00:32:06.560 The final question relates to the consideration of DRM-free versions or watermarking.
00:32:12.000 I have not, but I'll consider it.
00:32:15.560 Thank you all for your time!