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!