Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Licensing and Distributing a Paid Ruby CLI on macOS

Licensing and Distributing a Paid Ruby CLI on macOS

A talk I gave at Rocky Mountain Ruby in Boulder, Colorado on October 6, 2023. I took the audience through my journey figuring out how to enforce licenses and limitations for my Ruby on Mac product, and how to generate a self-contained Ruby binary.

A blog post version of the talk will be coming soon on the Ruby on Mac blog, and the video should be available by the end of October.

Moncef Belyamani

October 10, 2023

More Decks by Moncef Belyamani

Other Decks in Programming


  1. Moncef Belyamani / rubyonmac.dev / @monfresh Licensing and Distributing a

    Paid Ruby CLI on macOS Rocky Mountain Ruby / October 6, 2023
  2. I spent hours trying to get Jekyll to work on

    my new M2 Mac. Every time I thought I had it something else would break. Scott Lewis
  3. If only I could get those 10 hours back from

    the last few days spent trying to get 2.6.10 installed on this MacBook Air M1. Dan Weaver
  4. I spent around 3 days trying to solve issues on

    a Rails app. I looked and tried and retried so many possible solutions from so many sources on the web... but nothing was working. Monica Randriamialy
  5. I spent probably 4 hours trying to do exactly same

    setup as before and it never worked. Anton Podviaznikov
  6. • Xcode/Command Line Tools version • macOS version • Intel

    vs Apple Silicon • Installing things with and without Rosetta at the same time • The desired Ruby version • The current active Ruby version • The version manager used • Which OpenSSL versions are installed and which one you're trying to compile with • Missing, miscon fi gured, and outdated dev tools • PATH issues • Setting ENV vars and other compilation settings globally in shell startup fi le What can affect Ruby installation?
  7. The script was such a life saver. Finally got my

    Flutter project working on my laptop after literally one year! Nicole Manson
  8. This script is truly a lifesaver! I couldn't get any

    version managers to work properly. Gelo Rosel
  9. You are a lifesaver, I was struggling with installing rails

    on my m1 MBP and your script saved all the hassle. Abdullah Ashraf
  10. Your script was a lifesaver, thank you so much, I

    am incredibly grateful for your work. Justin Festa
  11. • Painless Ruby dev setup in 15 minutes or less

    • Works with all version managers • Installs Ruby faster than any other version manager (2x as fast as rbenv/asdf) • Easily install Ruby versions as old as 2.1 • Set up a new Mac in minutes with all your dev tools, Mac apps, fonts, Git preferences, macOS preferences, and GitHub repos. • Reset mode backs up and cleans up your dev setup in 1 minute rubyonmac.dev Ruby on Mac
  12. • Prime customers: 1 year of updates, 1 Mac at

    a time • Use Ruby for license veri fi cation code • License-related code can't be easily bypassed by customer • Scripts need to be able to run on a brand new Mac with no dev tools • Customer can reliably activate and deactivate Macs Gotta Have Limits
  13. • Supplies a patched Ruby • Compiles that Ruby locally

    (with native extensions) • Packs it into a squashfs archive • Ruby is patched to look inside the squashfs and the real fi lesystem How ruby-packer works
  14. ➜ ls -la /opt/homebrew/etc/[email protected] total 0 drwxr-xr-x@ 3 moncef admin

    96B Sep 12 10:59 ./ drwxrwxr-x@ 16 moncef admin 512B Sep 12 10:59 ../ lrwxr-xr-x@ 1 moncef admin 42B Sep 12 10:59 cert.pem@ -> /opt/homebrew/etc/ca-certificates/cert.pem
  15. ➜ openssl version -a LibreSSL 3.3.6 built on: date not

    available platform: information not available options: bn(64,64) rc4(ptr,int) des(idx,cisc,16,int) blowfish(idx) compiler: information not available OPENSSLDIR: "/private/etc/ssl"
  16. import DeviceCheck public func getDeviceToken(completion: @escaping (String?) -> ()) {

    let currentDevice = DCDevice.current if currentDevice.isSupported { currentDevice.generateToken(completionHandler: { (data, error) in if let tokenData = data { let tokenString = tokenData.base64EncodedString() print("Device token generated 🎉") completion(tokenString) } else { print("Error generating token: \(error!.localizedDescription)") } }) } else { print("Device is not supported") // Simulators or etc. } }
  17. getDeviceToken { deviceToken in var request = URLRequest(url: "http://localhost:3000/test") request.httpMethod

    = "POST" // Header request.setValue(deviceToken, forHTTPHeaderField: "Device-Token") // Body request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") let json = ["device_token": deviceToken] as [String : Any] let jsonData = try! JSONSerialization.data(withJSONObject: json) request.httpBody = jsonData as Data // Send it to server let downloadTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in ... }) }
  18. ??

  19. let homeDirectory = NSHomeDirectory() let fileURL = URL(fileURLWithPath: "\(homeDirectory)/token.txt" if

    let data = token.data(using: .utf8) { do { try data.write(to: fileURL) } catch { print("Error writing token to the file: \(error)") } }
  20. Executable=/Users/moncef/projects/rubyonmac-prime/verify_license Identifier=verify_license Format=Mach-O thin (arm64) Signature size=8981 Timestamp=Sep 9, 2023

    at 16:52:16 Info.plist=not bound TeamIdentifier=5NB2AVZQPV Runtime Version=13.3.0 Sealed Resources=none
  21. require 'json' result = `git for-each-ref --sort=-creatordate --format='% (creatordate:short) |

    %(contents)' refs/tags` tags = result.split("\n\n") versions = tags.each_with_object([]) do |tag, array| date = tag.split(" | ").first version = tag.split("Release ")[1] array.push({date:, version:}) end File.write("prime-versions.json", JSON.generate(versions))
  22. • Generate the license • Store it in S3 with

    ActiveStorage • Respond to Paddle with plain text containing link to the license • Paddle emails customer Go With the Flow
  23. • Customer downloads license and latest Ruby on Mac release

    • Customer runs .pkg installer • Installer puts ROM Token Generator macOS app in /Applications • Installer unzips rubyonmac folder Go With the Flow
  24. • Customer runs Bash script • Bash script checks that

    license veri fi cation binary exists and that it's signed by me • License is decrypted and email and product are extracted • ROM Token Generator app is opened in the background • Token gets saved to a fi le Go With the Flow
  25. • Token is read from the fi le and stored

    in a variable • Token Generator app is quit • Script sends token, email, and product to the Rails app • Rails app performs validations Go With the Flow
  26. • Send request to Apple's "query_two_bits" endpoint • "Failed to

    fi nd bit state" means ROM has not yet been run on this Mac • Send request to "update_two_bits" endpoint to set bit0 and bit1 • If device already activated, Apple returns current bit0 and bit1 values Go With the Flow
  27. • Verify current version was released before license expiration •

    Bash script can continue • License veri fi cation takes ~1 second Go With the Flow
  28. • MacBook Pro with Touch Bar (2016 and 2017) and

    Apple T1 Chip • Intel-based Mac computers with Apple T2 Security Chip • Mac computers with Apple silicon Secure Enclave
  29. Copying latest Ruby on Mac files... The rom-prime_arm64 file is

    missing! You are most likely running SentinelOne, which removed the file. Ruby on Mac Prime is not compatible with SentinelOne. Only the Ultimate version is. Please upgrade to Ruby on Mac Ultimate with coupon code FOOBAR: https://www.rubyonmac.dev/pricing If you're not running SentinelOne, please report this issue to [email protected]
  30. Thank You! And a Gift 5⃣ RMR50 4⃣ RMR40 3⃣

    RMR30 2⃣ RMR20 1⃣ RMR10 rubyonmac.dev/pricing