├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.unix ├── LICENSE.md ├── README.md ├── src ├── docker_client.rs ├── main.rs ├── rails_new.rs ├── unix.rs └── windows.rs └── tests └── cli.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "rails-new", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:1-1-bookworm", 7 | "features": { 8 | "ghcr.io/devcontainers/features/github-cli:1": {}, 9 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 10 | } 11 | 12 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 13 | // "mounts": [ 14 | // { 15 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 16 | // "target": "/usr/local/cargo", 17 | // "type": "volume" 18 | // } 19 | // ] 20 | 21 | // Features to add to the dev container. More info: https://containers.dev/features. 22 | // "features": {}, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | 27 | // Use 'postCreateCommand' to run commands after the container is created. 28 | // "postCreateCommand": "rustc --version", 29 | 30 | // Configure tool-specific properties. 31 | // "customizations": {}, 32 | 33 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 34 | // "remoteUser": "root" 35 | } 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Dockerfile.* linguist-language=Dockerfile 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macos-latest] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Run tests 21 | run: cargo test --verbose 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | upload-assets: 12 | strategy: 13 | matrix: 14 | include: 15 | - target: aarch64-unknown-linux-gnu 16 | os: ubuntu-latest 17 | - target: aarch64-apple-darwin 18 | os: macos-latest 19 | - target: x86_64-unknown-linux-gnu 20 | os: ubuntu-latest 21 | - target: x86_64-apple-darwin 22 | os: macos-latest 23 | - target: universal-apple-darwin 24 | os: macos-latest 25 | - target: x86_64-pc-windows-gnu 26 | os: windows-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: taiki-e/upload-rust-binary-action@v1 31 | with: 32 | bin: rails-new 33 | target: ${{ matrix.target }} 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | The Rails team is committed to fostering a welcoming community. 4 | 5 | **Our Code of Conduct can be found here**: 6 | 7 | https://rubyonrails.org/conduct 8 | 9 | For a history of updates, see the page history here: 10 | 11 | https://github.com/rails/website/commits/main/_pages/conduct.html 12 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.13" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "assert_cmd" 64 | version = "2.0.14" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" 67 | dependencies = [ 68 | "anstyle", 69 | "bstr", 70 | "doc-comment", 71 | "predicates", 72 | "predicates-core", 73 | "predicates-tree", 74 | "wait-timeout", 75 | ] 76 | 77 | [[package]] 78 | name = "autocfg" 79 | version = "1.1.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 82 | 83 | [[package]] 84 | name = "bstr" 85 | version = "1.9.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 88 | dependencies = [ 89 | "memchr", 90 | "regex-automata", 91 | "serde", 92 | ] 93 | 94 | [[package]] 95 | name = "clap" 96 | version = "4.5.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" 99 | dependencies = [ 100 | "clap_builder", 101 | "clap_derive", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_builder" 106 | version = "4.5.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" 109 | dependencies = [ 110 | "anstream", 111 | "anstyle", 112 | "clap_lex", 113 | "strsim", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_derive" 118 | version = "4.5.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 121 | dependencies = [ 122 | "heck", 123 | "proc-macro2", 124 | "quote", 125 | "syn", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_lex" 130 | version = "0.7.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 133 | 134 | [[package]] 135 | name = "colorchoice" 136 | version = "1.0.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 139 | 140 | [[package]] 141 | name = "difflib" 142 | version = "0.4.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 145 | 146 | [[package]] 147 | name = "doc-comment" 148 | version = "0.3.3" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 151 | 152 | [[package]] 153 | name = "float-cmp" 154 | version = "0.9.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 157 | dependencies = [ 158 | "num-traits", 159 | ] 160 | 161 | [[package]] 162 | name = "heck" 163 | version = "0.4.1" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 166 | 167 | [[package]] 168 | name = "libc" 169 | version = "0.2.153" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 172 | 173 | [[package]] 174 | name = "log" 175 | version = "0.4.21" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 178 | 179 | [[package]] 180 | name = "memchr" 181 | version = "2.7.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 184 | 185 | [[package]] 186 | name = "normalize-line-endings" 187 | version = "0.3.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 190 | 191 | [[package]] 192 | name = "num-traits" 193 | version = "0.2.18" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 196 | dependencies = [ 197 | "autocfg", 198 | ] 199 | 200 | [[package]] 201 | name = "predicates" 202 | version = "3.1.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" 205 | dependencies = [ 206 | "anstyle", 207 | "difflib", 208 | "float-cmp", 209 | "normalize-line-endings", 210 | "predicates-core", 211 | "regex", 212 | ] 213 | 214 | [[package]] 215 | name = "predicates-core" 216 | version = "1.0.6" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 219 | 220 | [[package]] 221 | name = "predicates-tree" 222 | version = "1.0.9" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 225 | dependencies = [ 226 | "predicates-core", 227 | "termtree", 228 | ] 229 | 230 | [[package]] 231 | name = "proc-macro2" 232 | version = "1.0.78" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 235 | dependencies = [ 236 | "unicode-ident", 237 | ] 238 | 239 | [[package]] 240 | name = "quote" 241 | version = "1.0.35" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 244 | dependencies = [ 245 | "proc-macro2", 246 | ] 247 | 248 | [[package]] 249 | name = "rails-new" 250 | version = "0.5.0" 251 | dependencies = [ 252 | "assert_cmd", 253 | "clap", 254 | "predicates", 255 | "users", 256 | ] 257 | 258 | [[package]] 259 | name = "regex" 260 | version = "1.10.3" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 263 | dependencies = [ 264 | "aho-corasick", 265 | "memchr", 266 | "regex-automata", 267 | "regex-syntax", 268 | ] 269 | 270 | [[package]] 271 | name = "regex-automata" 272 | version = "0.4.6" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 275 | dependencies = [ 276 | "aho-corasick", 277 | "memchr", 278 | "regex-syntax", 279 | ] 280 | 281 | [[package]] 282 | name = "regex-syntax" 283 | version = "0.8.2" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 286 | 287 | [[package]] 288 | name = "serde" 289 | version = "1.0.197" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 292 | dependencies = [ 293 | "serde_derive", 294 | ] 295 | 296 | [[package]] 297 | name = "serde_derive" 298 | version = "1.0.197" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 301 | dependencies = [ 302 | "proc-macro2", 303 | "quote", 304 | "syn", 305 | ] 306 | 307 | [[package]] 308 | name = "strsim" 309 | version = "0.11.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 312 | 313 | [[package]] 314 | name = "syn" 315 | version = "2.0.52" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | "unicode-ident", 322 | ] 323 | 324 | [[package]] 325 | name = "termtree" 326 | version = "0.4.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 329 | 330 | [[package]] 331 | name = "unicode-ident" 332 | version = "1.0.12" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 335 | 336 | [[package]] 337 | name = "users" 338 | version = "0.11.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" 341 | dependencies = [ 342 | "libc", 343 | "log", 344 | ] 345 | 346 | [[package]] 347 | name = "utf8parse" 348 | version = "0.2.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 351 | 352 | [[package]] 353 | name = "wait-timeout" 354 | version = "0.2.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 357 | dependencies = [ 358 | "libc", 359 | ] 360 | 361 | [[package]] 362 | name = "windows-sys" 363 | version = "0.52.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 366 | dependencies = [ 367 | "windows-targets", 368 | ] 369 | 370 | [[package]] 371 | name = "windows-targets" 372 | version = "0.52.4" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 375 | dependencies = [ 376 | "windows_aarch64_gnullvm", 377 | "windows_aarch64_msvc", 378 | "windows_i686_gnu", 379 | "windows_i686_msvc", 380 | "windows_x86_64_gnu", 381 | "windows_x86_64_gnullvm", 382 | "windows_x86_64_msvc", 383 | ] 384 | 385 | [[package]] 386 | name = "windows_aarch64_gnullvm" 387 | version = "0.52.4" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 390 | 391 | [[package]] 392 | name = "windows_aarch64_msvc" 393 | version = "0.52.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 396 | 397 | [[package]] 398 | name = "windows_i686_gnu" 399 | version = "0.52.4" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 402 | 403 | [[package]] 404 | name = "windows_i686_msvc" 405 | version = "0.52.4" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 408 | 409 | [[package]] 410 | name = "windows_x86_64_gnu" 411 | version = "0.52.4" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 414 | 415 | [[package]] 416 | name = "windows_x86_64_gnullvm" 417 | version = "0.52.4" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 420 | 421 | [[package]] 422 | name = "windows_x86_64_msvc" 423 | version = "0.52.4" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 426 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rails-new" 3 | version = "0.5.0" 4 | description = "A CLI tool to generate a new Rails project" 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = { version = "4.5.1", features = ["derive"] } 11 | [target.'cfg(unix)'.dependencies] 12 | users = "0.11.0" 13 | 14 | [dev-dependencies] 15 | assert_cmd = "2.0.14" 16 | predicates = "3.1.0" 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION=3.4.1 2 | FROM ruby:${RUBY_VERSION} 3 | 4 | ARG NODE_VERSION=22 5 | ARG YARN_VERSION=1.22.22 6 | 7 | RUN curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 8 | && apt-get update \ 9 | && apt-get install --yes --no-install-recommends nodejs \ 10 | && npm install -g yarn@$YARN_VERSION 11 | 12 | ARG RAILS_VERSION 13 | # Install Rails based on the version specified but if not specified, install the latest version. 14 | RUN if [ -z "$RAILS_VERSION" ] ; then gem install rails ; else gem install rails -v $RAILS_VERSION ; fi 15 | -------------------------------------------------------------------------------- /Dockerfile.unix: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION=3.4.1 2 | FROM ruby:${RUBY_VERSION} 3 | 4 | ARG USER_ID=1000 5 | ARG GROUP_ID=1000 6 | RUN (getent group $GROUP_ID > /dev/null || groupadd -g $GROUP_ID app) && \ 7 | (getent passwd $USER_ID > /dev/null || useradd -u $USER_ID -g $GROUP_ID -m app) 8 | 9 | ARG NODE_VERSION=22 10 | ARG YARN_VERSION=1.22.22 11 | 12 | RUN curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 13 | && apt-get update \ 14 | && apt-get install --yes --no-install-recommends nodejs \ 15 | && npm install -g yarn@$YARN_VERSION 16 | 17 | USER $USER_ID:$GROUP_ID 18 | 19 | ARG RAILS_VERSION 20 | # Install Rails based on the version specified but if not specified, install the latest version. 21 | RUN if [ -z "$RAILS_VERSION" ] ; then gem install rails ; else gem install rails -v $RAILS_VERSION ; fi 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rails Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | You can use this repository to generate a new Rails application without having to install Ruby on your machine. 2 | 3 | It works by using Docker to generate the Rails application for you. Docker takes care of installing the right Ruby and 4 | Rails versions for you, so you don't have to worry about it. 5 | 6 | ## Prerequisites 7 | 8 | You need to have Docker installed on your machine. You can find instructions on how to install Docker on your machine 9 | [here](https://docs.docker.com/engine/install/). 10 | 11 | **On Linux**, your user needs to be a member of the docker group. rails-new does not work with sudo. You can find instructions on this [here](https://docs.docker.com/engine/install/linux-postinstall/) 12 | 13 | ## Installation 14 | 15 | Go to the [latest release](https://github.com/rails/rails-new/releases/latest) and download the executable for your platform (not the source code). For example, on M1 macOS this would be `rails-new-aarch64-apple-darwin.tar.gz`. Once the download is complete, unzip the `.tar.gz` file, which will create the `rails-new` executable. Move the executable into your path so that it is ready to run from the command line. 16 | 17 | ## Usage 18 | 19 | To generate a new Rails application, you can run the following command: 20 | 21 | ```bash 22 | rails-new myapp 23 | ``` 24 | 25 | Or with options: 26 | ```bash 27 | rails-new myapp --main 28 | ``` 29 | 30 | The first time you attempt to use the executable in macOS you may see a message like this: 31 | 32 | > "rails-new" can’t be opened because Apple cannot check it for malicious software 33 | 34 | In that case, please go to System Settings → Privacy & Security. You'll see a section mentioning "rails-new" with a button labeled "Allow Anyway" that you have to click. 35 | 36 | The list of available options is found in the [Rails guides](https://guides.rubyonrails.org/command_line.html#rails-new). 37 | -------------------------------------------------------------------------------- /src/docker_client.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Stdio}; 2 | 3 | pub struct DockerClient {} 4 | 5 | impl DockerClient { 6 | pub fn build_image( 7 | ruby_version: &str, 8 | maybe_rails_version: Option<&str>, 9 | user_id: Option, 10 | group_id: Option, 11 | rebuild: bool, 12 | ) -> Command { 13 | let mut command = Command::new("docker"); 14 | 15 | command.arg("build"); 16 | 17 | if rebuild { 18 | command.arg("--no-cache"); 19 | } 20 | 21 | Self::set_build_arg(&mut command, "RUBY_VERSION", ruby_version); 22 | if let Some(rails_version) = maybe_rails_version { 23 | Self::set_build_arg(&mut command, "RAILS_VERSION", rails_version); 24 | } 25 | 26 | if let Some(id) = user_id { 27 | Self::set_build_arg(&mut command, "USER_ID", &id.to_string()) 28 | } 29 | if let Some(id) = group_id { 30 | Self::set_build_arg(&mut command, "GROUP_ID", &id.to_string()) 31 | } 32 | 33 | command.arg("-t"); 34 | 35 | Self::set_image_name(&mut command, ruby_version, maybe_rails_version); 36 | 37 | command.arg("-").stdin(Stdio::piped()); 38 | 39 | command 40 | } 41 | 42 | pub fn run_image( 43 | ruby_version: &str, 44 | rails_version: Option<&str>, 45 | args: Vec, 46 | ) -> Command { 47 | let mut command = Self::run(); 48 | 49 | Self::set_workdir(&mut command); 50 | Self::set_image_name(&mut command, ruby_version, rails_version); 51 | Self::set_rails_new(&mut command, args); 52 | 53 | command 54 | } 55 | 56 | pub fn get_help(ruby_version: &str, rails_version: Option<&str>) -> Command { 57 | let mut command = Self::run(); 58 | 59 | Self::set_image_name(&mut command, ruby_version, rails_version); 60 | Self::set_rails_new(&mut command, vec!["--help".to_string()]); 61 | 62 | command 63 | } 64 | 65 | fn run() -> Command { 66 | let mut command = Command::new("docker"); 67 | 68 | command.args(["run", "--rm"]); 69 | 70 | command 71 | } 72 | 73 | fn set_build_arg(command: &mut Command, key: &str, value: &str) { 74 | command.args(["--build-arg", &format!("{}={}", key, value)]); 75 | } 76 | 77 | fn set_workdir(command: &mut Command) { 78 | let path = std::env::current_dir().expect("Failed to get current directory"); 79 | let absolute_path = canonicalize_os_path(&path).expect("Failed to build directory"); 80 | let current_dir = absolute_path 81 | .to_str() 82 | .expect("Failed to get current directory"); 83 | 84 | command 85 | .arg("-v") 86 | .arg(format!("{}:{}", current_dir, current_dir)) 87 | .args(["-w", current_dir]); 88 | } 89 | 90 | fn set_image_name( 91 | command: &mut Command, 92 | ruby_version: &str, 93 | maybe_rails_version: Option<&str>, 94 | ) { 95 | if let Some(rails_version) = maybe_rails_version { 96 | command.arg(format!("rails-new-{}-{}", ruby_version, rails_version)); 97 | } else { 98 | command.arg(format!("rails-new-{}", ruby_version)); 99 | } 100 | } 101 | 102 | fn set_rails_new(command: &mut Command, args: Vec) { 103 | command.args(["rails", "new"]).args(args); 104 | } 105 | } 106 | 107 | fn canonicalize_os_path(path: &std::path::Path) -> std::io::Result { 108 | let canonicalized = std::fs::canonicalize(path)?; 109 | 110 | if cfg!(windows) { 111 | let path_str = canonicalized.to_str().unwrap(); 112 | // On Windows only, check if the path starts with the UNC prefix 113 | // example: \\?\C:\path\to\file 114 | if path_str.starts_with(r"\\?\") { 115 | // drop UNC prefix 116 | let path_str = &path_str[4..]; 117 | // grab the drive letter 118 | let drive_letter = &path_str[0..1]; 119 | // swap \ for / 120 | let rest_of_path = &path_str[2..].replace(r"\", "/"); 121 | // rebuild as /C/path/to/file 122 | return Ok(std::path::PathBuf::from(format!( 123 | "/{}/{}", 124 | drive_letter, rest_of_path 125 | ))); 126 | } 127 | } 128 | Ok(canonicalized) 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | use std::{env::current_dir, ffi::OsStr}; 135 | 136 | #[test] 137 | fn build_image() { 138 | let command = DockerClient::build_image("3.2.3", Some("7.1.3"), None, None, false); 139 | 140 | assert_eq!(command.get_program(), "docker"); 141 | 142 | let args: Vec<&OsStr> = command.get_args().collect(); 143 | 144 | assert_eq!( 145 | args, 146 | &[ 147 | "build", 148 | "--build-arg", 149 | "RUBY_VERSION=3.2.3", 150 | "--build-arg", 151 | "RAILS_VERSION=7.1.3", 152 | "-t", 153 | "rails-new-3.2.3-7.1.3", 154 | "-", 155 | ] 156 | ); 157 | } 158 | 159 | #[test] 160 | fn build_image_with_user_id() { 161 | let command = DockerClient::build_image("3.2.3", Some("7.1.3"), Some(1000), None, false); 162 | 163 | assert_eq!(command.get_program(), "docker"); 164 | 165 | let args: Vec<&OsStr> = command.get_args().collect(); 166 | 167 | assert_eq!( 168 | args, 169 | &[ 170 | "build", 171 | "--build-arg", 172 | "RUBY_VERSION=3.2.3", 173 | "--build-arg", 174 | "RAILS_VERSION=7.1.3", 175 | "--build-arg", 176 | "USER_ID=1000", 177 | "-t", 178 | "rails-new-3.2.3-7.1.3", 179 | "-", 180 | ] 181 | ); 182 | } 183 | 184 | #[test] 185 | fn build_image_with_group_id() { 186 | let command = DockerClient::build_image("3.2.3", Some("7.1.3"), None, Some(1000), false); 187 | 188 | assert_eq!(command.get_program(), "docker"); 189 | 190 | let args: Vec<&OsStr> = command.get_args().collect(); 191 | 192 | assert_eq!( 193 | args, 194 | &[ 195 | "build", 196 | "--build-arg", 197 | "RUBY_VERSION=3.2.3", 198 | "--build-arg", 199 | "RAILS_VERSION=7.1.3", 200 | "--build-arg", 201 | "GROUP_ID=1000", 202 | "-t", 203 | "rails-new-3.2.3-7.1.3", 204 | "-", 205 | ] 206 | ); 207 | } 208 | 209 | #[test] 210 | fn build_image_with_rebuild_flag() { 211 | let command = DockerClient::build_image("3.2.3", Some("7.1.3"), None, None, true); 212 | 213 | let args: Vec<&OsStr> = command.get_args().collect(); 214 | 215 | assert_eq!( 216 | args, 217 | &[ 218 | "build", 219 | "--no-cache", 220 | "--build-arg", 221 | "RUBY_VERSION=3.2.3", 222 | "--build-arg", 223 | "RAILS_VERSION=7.1.3", 224 | "-t", 225 | "rails-new-3.2.3-7.1.3", 226 | "-", 227 | ] 228 | ); 229 | } 230 | 231 | #[test] 232 | fn build_image_without_rails_version() { 233 | let command = DockerClient::build_image("3.2.3", None, None, None, false); 234 | 235 | let args: Vec<&OsStr> = command.get_args().collect(); 236 | 237 | assert_eq!( 238 | args, 239 | &[ 240 | "build", 241 | "--build-arg", 242 | "RUBY_VERSION=3.2.3", 243 | "-t", 244 | "rails-new-3.2.3", 245 | "-", 246 | ] 247 | ); 248 | } 249 | 250 | #[test] 251 | fn build_image_with_both_ids() { 252 | let command = DockerClient::build_image("3.2.3", Some("7.1.3"), Some(1000), Some(1000), false); 253 | 254 | let args: Vec<&OsStr> = command.get_args().collect(); 255 | 256 | assert_eq!( 257 | args, 258 | &[ 259 | "build", 260 | "--build-arg", 261 | "RUBY_VERSION=3.2.3", 262 | "--build-arg", 263 | "RAILS_VERSION=7.1.3", 264 | "--build-arg", 265 | "USER_ID=1000", 266 | "--build-arg", 267 | "GROUP_ID=1000", 268 | "-t", 269 | "rails-new-3.2.3-7.1.3", 270 | "-", 271 | ] 272 | ); 273 | } 274 | 275 | #[test] 276 | fn run_image() { 277 | let command = DockerClient::run_image("3.2.3", Some("7.1.3"), vec!["my_app".to_string()]); 278 | 279 | assert_eq!(command.get_program(), "docker"); 280 | 281 | let binding = current_dir().unwrap(); 282 | let absolute_path = canonicalize_os_path(&binding).unwrap(); 283 | let current_dir = absolute_path.to_str().unwrap(); 284 | 285 | let args: Vec<&OsStr> = command.get_args().collect(); 286 | 287 | assert_eq!( 288 | args, 289 | &[ 290 | "run", 291 | "--rm", 292 | "-v", 293 | &format!("{}:{}", current_dir, current_dir), 294 | "-w", 295 | current_dir, 296 | "rails-new-3.2.3-7.1.3", 297 | "rails", 298 | "new", 299 | "my_app", 300 | ] 301 | ); 302 | } 303 | 304 | #[test] 305 | fn run_image_without_rails_version() { 306 | let command = DockerClient::run_image("3.2.3", None, vec!["my_app".to_string()]); 307 | 308 | let binding = current_dir().unwrap(); 309 | let absolute_path = canonicalize_os_path(&binding).unwrap(); 310 | let current_dir = absolute_path.to_str().unwrap(); 311 | 312 | let args: Vec<&OsStr> = command.get_args().collect(); 313 | 314 | assert_eq!( 315 | args, 316 | &[ 317 | "run", 318 | "--rm", 319 | "-v", 320 | &format!("{}:{}", current_dir, current_dir), 321 | "-w", 322 | current_dir, 323 | "rails-new-3.2.3", 324 | "rails", 325 | "new", 326 | "my_app", 327 | ] 328 | ); 329 | } 330 | 331 | #[test] 332 | fn get_help() { 333 | let command = DockerClient::get_help("3.2.3", Some("7.1.3")); 334 | 335 | assert_eq!(command.get_program(), "docker"); 336 | 337 | let args: Vec<&OsStr> = command.get_args().collect(); 338 | 339 | assert_eq!( 340 | args, 341 | &[ 342 | "run", 343 | "--rm", 344 | "rails-new-3.2.3-7.1.3", 345 | "rails", 346 | "new", 347 | "--help", 348 | ] 349 | ); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Write a CLI program that call the bash file rails-new inside the bin folder. 2 | 3 | // use std::process::Command; 4 | mod docker_client; 5 | mod rails_new; 6 | use rails_new::{Cli, Commands}; 7 | use std::{io::Write, process::Command}; 8 | 9 | use clap::Parser; 10 | 11 | use crate::docker_client::DockerClient; 12 | 13 | #[cfg_attr(all(unix, not(target_os = "macos")), path = "unix.rs")] 14 | #[cfg_attr(any(windows, target_os = "macos"), path = "windows.rs")] 15 | mod os_specific; 16 | 17 | fn main() { 18 | let cli = Cli::parse(); 19 | 20 | let ruby_version = cli.ruby_version; 21 | let rails_version = cli.rails_version.as_deref(); 22 | let rebuild = cli.rebuild; 23 | 24 | // Run docker build --build-arg RUBY_VERSION=$RUBY_VERSION --build-arg RAILS_VERSION=$RAILS_VERSION -t rails-new-$RUBY_VERSION-$RAILS_VERSION 25 | // passing the content of DOCKERFILE to the command stdin 26 | let mut child = DockerClient::build_image( 27 | &ruby_version, 28 | rails_version, 29 | os_specific::get_user_id(), 30 | os_specific::get_group_id(), 31 | rebuild, 32 | ) 33 | .spawn() 34 | .expect("Failed to execute process"); 35 | 36 | let mut stdin = child.stdin.take().expect("Failed to open stdin"); 37 | std::thread::spawn(move || { 38 | stdin.write_all(os_specific::dockerfile_content()).unwrap(); 39 | }); 40 | 41 | let status = child.wait().expect("failed to wait on child"); 42 | 43 | assert!(status.success()); 44 | 45 | let mut command: Command; 46 | 47 | match &cli.command { 48 | Some(Commands::RailsHelp {}) => { 49 | command = DockerClient::get_help(&ruby_version, rails_version) 50 | } 51 | 52 | None => { 53 | // Run the image with docker run -v $(pwd):/$(pwd) -w $(pwd) rails-new-$RUBY_VERSION-$RAILS_VERSION rails new $@ 54 | command = DockerClient::run_image(&ruby_version, rails_version, cli.args) 55 | } 56 | } 57 | 58 | let status = command.status().expect("Failed to execute process"); 59 | 60 | assert!(status.success()); 61 | } 62 | -------------------------------------------------------------------------------- /src/rails_new.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser)] 4 | #[command(version, about, long_about = None, subcommand_negates_reqs = true)] 5 | pub struct Cli { 6 | #[clap(trailing_var_arg = true, required = true)] 7 | /// arguments passed to `rails new` 8 | pub args: Vec, 9 | #[clap(long, short = 'u', default_value = "latest")] 10 | pub ruby_version: String, 11 | #[clap(long, short = 'r')] 12 | pub rails_version: Option, 13 | #[clap(long)] 14 | pub rebuild: bool, 15 | 16 | #[command(subcommand)] 17 | pub command: Option, 18 | } 19 | 20 | #[derive(Subcommand)] 21 | pub enum Commands { 22 | /// Print `rails new --help` 23 | RailsHelp {}, 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | 30 | #[test] 31 | fn verify_cli() { 32 | use clap::CommandFactory; 33 | 34 | Cli::command().debug_assert() 35 | } 36 | 37 | #[test] 38 | fn arguments_are_directed_to_rails_new() -> Result<(), Box> { 39 | use clap::CommandFactory; 40 | 41 | let m = Cli::command().get_matches_from(vec!["rails-new", "my_app", "--main"]); 42 | 43 | let trail: Vec<_> = m.get_many::("args").unwrap().collect(); 44 | 45 | assert_eq!(trail, &["my_app", "--main"]); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn default_values() -> Result<(), Box> { 52 | use clap::CommandFactory; 53 | 54 | let m = Cli::command().get_matches_from(vec!["rails-new", "my_app"]); 55 | 56 | let ruby_version = m.get_one::("ruby_version").unwrap(); 57 | 58 | assert_eq!(ruby_version, "latest"); 59 | 60 | Ok(()) 61 | } 62 | 63 | #[test] 64 | fn rails_help() -> Result<(), Box> { 65 | use clap::CommandFactory; 66 | 67 | let m = Cli::command().get_matches_from(vec!["rails-new", "rails-help"]); 68 | 69 | match m.subcommand_name() { 70 | Some("rails-help") => {} 71 | _ => panic!("Expected subcommand 'rails-help'"), 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn custom_ruby_version() -> Result<(), Box> { 79 | use clap::CommandFactory; 80 | 81 | let m = Cli::command().get_matches_from(vec!["rails-new", "--ruby-version", "3.2.0", "my_app"]); 82 | let ruby_version = m.get_one::("ruby_version").unwrap(); 83 | assert_eq!(ruby_version, "3.2.0"); 84 | 85 | // Test short form 86 | let m = Cli::command().get_matches_from(vec!["rails-new", "-u", "3.2.0", "my_app"]); 87 | let ruby_version = m.get_one::("ruby_version").unwrap(); 88 | assert_eq!(ruby_version, "3.2.0"); 89 | 90 | Ok(()) 91 | } 92 | 93 | #[test] 94 | fn rails_version_flag() -> Result<(), Box> { 95 | use clap::CommandFactory; 96 | 97 | let m = Cli::command().get_matches_from(vec!["rails-new", "--rails-version", "7.1.0", "my_app"]); 98 | let rails_version = m.get_one::("rails_version").unwrap(); 99 | assert_eq!(rails_version, "7.1.0"); 100 | 101 | // Test short form 102 | let m = Cli::command().get_matches_from(vec!["rails-new", "-r", "7.1.0", "my_app"]); 103 | let rails_version = m.get_one::("rails_version").unwrap(); 104 | assert_eq!(rails_version, "7.1.0"); 105 | 106 | Ok(()) 107 | } 108 | 109 | #[test] 110 | fn rebuild_flag() -> Result<(), Box> { 111 | use clap::CommandFactory; 112 | 113 | let m = Cli::command().get_matches_from(vec!["rails-new", "--rebuild", "my_app"]); 114 | let rebuild = m.get_flag("rebuild"); 115 | assert!(rebuild); 116 | 117 | // Test default value (false) 118 | let m = Cli::command().get_matches_from(vec!["rails-new", "my_app"]); 119 | let rebuild = m.get_flag("rebuild"); 120 | assert!(!rebuild); 121 | 122 | Ok(()) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/unix.rs: -------------------------------------------------------------------------------- 1 | pub fn dockerfile_content() -> &'static [u8] { 2 | include_bytes!("../Dockerfile.unix") 3 | } 4 | 5 | pub fn get_user_id() -> Option { 6 | Some(users::get_current_uid()) 7 | } 8 | 9 | pub fn get_group_id() -> Option { 10 | Some(users::get_current_gid()) 11 | } 12 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | pub fn dockerfile_content() -> &'static [u8] { 2 | include_bytes!("../Dockerfile") 3 | } 4 | 5 | pub fn get_user_id() -> Option { 6 | None 7 | } 8 | 9 | pub fn get_group_id() -> Option { 10 | None 11 | } 12 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | 5 | #[test] 6 | fn requires_a_name() -> Result<(), Box> { 7 | let mut cmd = Command::cargo_bin("rails-new")?; 8 | 9 | cmd.assert() 10 | .failure() 11 | .stderr(predicate::str::contains( 12 | "the following required arguments were not provided:", 13 | )) 14 | .stderr(predicate::str::contains("...")); 15 | 16 | Ok(()) 17 | } 18 | --------------------------------------------------------------------------------